feat(06-07): scheduled() cron tick wired — factory, loadGraph street memo, crons trigger (GREEN)
- webhook.ts: createExportedHandler(opts) factory (offline send capture via baseFetch); scheduled() wraps runGuidanceCronTick in ctx.waitUntil — fail-closed unconfigured gate, paginated guidance: KV list (list_complete respected), loadGraph awaited ONCE per tick (the shared R2 memo), per-key getGuidanceSession → projectTurnCards → sendPhoto to envelope.chat_id ONLY (T-06-02), every log a [guidance-cron] category, zero KV writes - guidance-sessions.ts: GUIDANCE_KEY_PREFIX exported — the ONE key-scheme authority (list scans derive prefix + slice ids from it) - wrangler.jsonc: triggers.crons */1 * * * * (60 s scan, cadence in code) - guidance-cron.test.mjs GREEN completion: GC-C3/C5/C8/C9 wire the recording baseFetch through the factory (the header-documented seam — they defined the recorder but never passed it); GC-C7 uses readdirSync (require is undefined in ESM); GC-C1/C8/C9 drain inside the withConsole window (the tick's logs fire under waitUntil) - guidance-envelope.test.mjs: GE7 allows the GUIDANCE_KEY_PREFIX named import - 125/125 bot tests green; bot:typecheck clean
This commit is contained in:
@@ -35,7 +35,7 @@
|
||||
* synthetic 1e-6-grid points near KL center) — no user coordinate ever does.
|
||||
*/
|
||||
import { strict as assert } from "node:assert";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { test } from "./harness.mjs";
|
||||
@@ -453,10 +453,10 @@ test("GC-C1 due tick: the alive envelope's card sends to ITS OWN chat, street-be
|
||||
};
|
||||
const { ctx, drain } = makeCtx();
|
||||
const worker = mod.createExportedHandler({ baseFetch });
|
||||
const { logs } = await withConsole(() =>
|
||||
worker.scheduled(controllerFor(TICK_SEC), makeEnv(kv, r2With(MINI_GRAPH)), ctx),
|
||||
);
|
||||
await drain();
|
||||
const { logs } = await withConsole(async () => {
|
||||
await worker.scheduled(controllerFor(TICK_SEC), makeEnv(kv, r2With(MINI_GRAPH)), ctx);
|
||||
await drain(); // INSIDE the capture window — the tick's logs fire under waitUntil
|
||||
});
|
||||
const photos = await sentPhotos(rec.calls);
|
||||
assert.equal(photos.length, 1, "exactly the due, alive envelope sends");
|
||||
assert.equal(photos[0].chat_id, 202, "chat_id comes ONLY from the envelope (T-06-02)");
|
||||
@@ -496,7 +496,7 @@ test("GC-C2 non-due tick: zero sends (the scan still runs; the cadence gates the
|
||||
});
|
||||
|
||||
test("GC-C3 R2-null graph: the bare-verb street-less card still sends (degradation, not silence)", async () => {
|
||||
const { default: worker } = await import("../src/webhook.ts");
|
||||
const mod = await import("../src/webhook.ts");
|
||||
const envA = envelope({ points: miniStreetPoints(), distanceM: 89, durationS: 90, startedAtSec: TICK_SEC - 45 });
|
||||
const map = new Map([["guidance:" + envA.id, JSON.stringify(envA)]]);
|
||||
const kv = makeListKV(map, [{ keys: [{ name: `guidance:${envA.id}` }], list_complete: true }]);
|
||||
@@ -506,8 +506,11 @@ test("GC-C3 R2-null graph: the bare-verb street-less card still sends (degradati
|
||||
return { ok: true, json: async () => ({ ok: true, result: {} }) };
|
||||
};
|
||||
const { ctx, drain } = makeCtx();
|
||||
await withConsole(() => worker.scheduled(controllerFor(TICK_SEC), makeEnv(kv, r2With(null)), ctx));
|
||||
await drain();
|
||||
const worker = mod.createExportedHandler({ baseFetch });
|
||||
await withConsole(async () => {
|
||||
await worker.scheduled(controllerFor(TICK_SEC), makeEnv(kv, r2With(null)), ctx);
|
||||
await drain();
|
||||
});
|
||||
const photos = await sentPhotos(rec.calls);
|
||||
assert.equal(photos.length, 1, "the send fires even without a graph");
|
||||
assert.ok(photos[0].caption.includes("— TURN LEFT"), "bare verb (graph absent)");
|
||||
@@ -534,7 +537,7 @@ test("GC-C4 fail-closed: missing BOT_TOKEN → skipped: unconfigured, zero sends
|
||||
});
|
||||
|
||||
test("GC-C5 pagination: list_complete respected — envelopes across two pages all processed", async () => {
|
||||
const { default: worker } = await import("../src/webhook.ts");
|
||||
const mod = await import("../src/webhook.ts");
|
||||
const envA = envelope({ points: miniStreetPoints(), distanceM: 89, durationS: 90, startedAtSec: TICK_SEC - 45, chatId: 204 });
|
||||
const envA2 = envelope({ points: miniStreetPoints(), distanceM: 89, durationS: 90, startedAtSec: TICK_SEC - 45, chatId: 205 });
|
||||
const envB = envelope({ points: threeLegPoints(), distanceM: 10000, durationS: 900, startedAtSec: TICK_SEC - 10000, chatId: 206 });
|
||||
@@ -552,8 +555,11 @@ test("GC-C5 pagination: list_complete respected — envelopes across two pages a
|
||||
return { ok: true, json: async () => ({ ok: true, result: {} }) };
|
||||
};
|
||||
const { ctx, drain } = makeCtx();
|
||||
await withConsole(() => worker.scheduled(controllerFor(TICK_SEC), makeEnv(kv, r2With(MINI_GRAPH)), ctx));
|
||||
await drain();
|
||||
const worker = mod.createExportedHandler({ baseFetch });
|
||||
await withConsole(async () => {
|
||||
await worker.scheduled(controllerFor(TICK_SEC), makeEnv(kv, r2With(MINI_GRAPH)), ctx);
|
||||
await drain();
|
||||
});
|
||||
const photos = await sentPhotos(rec.calls);
|
||||
assert.equal(kv.listCalls, 2, "the pagination loop fetched both pages");
|
||||
assert.equal(photos.length, 2, "both alive envelopes carded across pages");
|
||||
@@ -572,7 +578,7 @@ test("GC-C7 shared-memo wiring: loadGraph feeds the scheduled arm; no second ROA
|
||||
assert.ok(webhookCode.includes("loadGraph"), "scheduled() awaits the worker-router memo");
|
||||
assert.ok(webhookCode.includes("makeStreetAt"), "the graph-fed street reconstruction is wired");
|
||||
assert.ok(webhookCode.includes("projectTurnCards"), "the projection is the decision authority");
|
||||
const readdir = (dir) => require("node:fs").readdirSync(dir, { withFileTypes: true });
|
||||
const readdir = (dir) => readdirSync(dir, { withFileTypes: true });
|
||||
const walk = (dir, acc = []) => {
|
||||
for (const e of readdir(dir)) {
|
||||
const full = dir + "/" + e.name;
|
||||
@@ -594,7 +600,7 @@ test("GC-C7 shared-memo wiring: loadGraph feeds the scheduled arm; no second ROA
|
||||
});
|
||||
|
||||
test("GC-C8 one bad key cannot kill the tick: a throwing KV read is skipped, the rest still card", async () => {
|
||||
const { default: worker } = await import("../src/webhook.ts");
|
||||
const mod = await import("../src/webhook.ts");
|
||||
const envA = envelope({ points: miniStreetPoints(), distanceM: 89, durationS: 90, startedAtSec: TICK_SEC - 45, chatId: 207 });
|
||||
const BAD_KEY = "guidance:3f2a2c3e-1b4d-4e5a-9c8d-0000000000b8";
|
||||
const map = new Map();
|
||||
@@ -607,8 +613,11 @@ test("GC-C8 one bad key cannot kill the tick: a throwing KV read is skipped, the
|
||||
return { ok: true, json: async () => ({ ok: true, result: {} }) };
|
||||
};
|
||||
const { ctx, drain } = makeCtx();
|
||||
const { logs } = await withConsole(() => worker.scheduled(controllerFor(TICK_SEC), makeEnv(kv, r2With(MINI_GRAPH)), ctx));
|
||||
await drain();
|
||||
const worker = mod.createExportedHandler({ baseFetch });
|
||||
const { logs } = await withConsole(async () => {
|
||||
await worker.scheduled(controllerFor(TICK_SEC), makeEnv(kv, r2With(MINI_GRAPH)), ctx);
|
||||
await drain();
|
||||
});
|
||||
const photos = await sentPhotos(rec.calls);
|
||||
assert.equal(photos.length, 1, "the tick survived the throwing read");
|
||||
assert.equal(photos[0].chat_id, 207);
|
||||
@@ -616,7 +625,7 @@ test("GC-C8 one bad key cannot kill the tick: a throwing KV read is skipped, the
|
||||
});
|
||||
|
||||
test("GC-C9 a failed send logs and continues — delivery variance is documented best-effort (D-13)", async () => {
|
||||
const { default: worker } = await import("../src/webhook.ts");
|
||||
const mod = await import("../src/webhook.ts");
|
||||
const envA = envelope({ points: miniStreetPoints(), distanceM: 89, durationS: 90, startedAtSec: TICK_SEC - 45, chatId: 208 });
|
||||
const envA2 = envelope({ points: miniStreetPoints(), distanceM: 89, durationS: 90, startedAtSec: TICK_SEC - 45, chatId: 209 });
|
||||
const map = new Map();
|
||||
@@ -637,8 +646,11 @@ test("GC-C9 a failed send logs and continues — delivery variance is documented
|
||||
return { ok: true, json: async () => ({ ok: true, result: {} }) };
|
||||
};
|
||||
const { ctx, drain } = makeCtx();
|
||||
const { logs } = await withConsole(() => worker.scheduled(controllerFor(TICK_SEC), makeEnv(kv, r2With(MINI_GRAPH)), ctx));
|
||||
await drain();
|
||||
const worker = mod.createExportedHandler({ baseFetch });
|
||||
const { logs } = await withConsole(async () => {
|
||||
await worker.scheduled(controllerFor(TICK_SEC), makeEnv(kv, r2With(MINI_GRAPH)), ctx);
|
||||
await drain();
|
||||
});
|
||||
const photos = await sentPhotos(rec.calls);
|
||||
assert.equal(photos.length, 1, "the failed first send did not stop the second");
|
||||
assert.equal(photos[0].chat_id, 209);
|
||||
|
||||
Reference in New Issue
Block a user