@rubytech/create-maxy-code 0.1.591 → 0.1.592
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/platform/lib/routine-templates/dist/index.d.ts +2 -2
- package/payload/platform/lib/routine-templates/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/routine-templates/dist/index.js +2 -9
- package/payload/platform/lib/routine-templates/dist/index.js.map +1 -1
- package/payload/platform/lib/routine-templates/dist/roster.d.ts +1 -11
- package/payload/platform/lib/routine-templates/dist/roster.d.ts.map +1 -1
- package/payload/platform/lib/routine-templates/dist/roster.js +11 -15
- package/payload/platform/lib/routine-templates/dist/roster.js.map +1 -1
- package/payload/platform/lib/routine-templates/src/__tests__/roster.test.ts +0 -0
- package/payload/platform/lib/routine-templates/src/__tests__/seed.test.ts +8 -3
- package/payload/platform/lib/routine-templates/src/index.ts +1 -8
- package/payload/platform/lib/routine-templates/src/roster.ts +10 -14
- package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +4 -0
- package/payload/platform/plugins/scheduling/PLUGIN.md +6 -6
- package/payload/platform/plugins/scheduling/mcp/dist/index.js +4 -12
- package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-audits.test.js +85 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-audits.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate.test.js +55 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.d.ts +10 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.js +42 -6
- package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.d.ts +22 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.js +73 -0
- package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/agent-turn-dispatch.test.js +79 -0
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/agent-turn-dispatch.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts +2 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js +40 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +19 -2
- package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-write.test.js +27 -280
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-write.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js +191 -208
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts +1 -31
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +15 -95
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js +5 -36
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.d.ts +58 -54
- package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.js +85 -98
- package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.js.map +1 -1
package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js
CHANGED
|
@@ -17,7 +17,7 @@ vi.mock("../../lib/neo4j.js", () => ({
|
|
|
17
17
|
close: vi.fn(async () => { }),
|
|
18
18
|
}),
|
|
19
19
|
}));
|
|
20
|
-
import { runSchedulingGate,
|
|
20
|
+
import { runSchedulingGate, advanceGateMark, watermarkPath, readWatermarks, foldMailWatermarks, parseD1Count, digestBusy, } from "../scheduling-gate.js";
|
|
21
21
|
const ACCOUNT = "20028fb5-0000-4000-8000-000000000000";
|
|
22
22
|
const EVENT = "65ef758c-0000-4000-8000-000000000000";
|
|
23
23
|
let platformRoot;
|
|
@@ -58,14 +58,14 @@ afterEach(() => {
|
|
|
58
58
|
vi.doUnmock("imapflow");
|
|
59
59
|
delete process.env.PLATFORM_PORT;
|
|
60
60
|
});
|
|
61
|
-
const run = (checks, sources
|
|
61
|
+
const run = (checks, sources) => runSchedulingGate({ eventId: EVENT, checks }, ACCOUNT, platformRoot, sources);
|
|
62
62
|
describe("d1 check", () => {
|
|
63
63
|
const CHECK = { type: "d1", database: "bookings", sql: "SELECT count(*) FROM b" };
|
|
64
64
|
it("is false on a zero count", async () => {
|
|
65
|
-
expect(await run([CHECK], quietSources())).
|
|
65
|
+
expect(await run([CHECK], quietSources())).toMatchObject({ hasWork: false, reasons: [], errored: [] });
|
|
66
66
|
});
|
|
67
67
|
it("is true on one row", async () => {
|
|
68
|
-
expect(await run([CHECK], quietSources({ d1: async () => 1 }))).
|
|
68
|
+
expect(await run([CHECK], quietSources({ d1: async () => 1 }))).toMatchObject({
|
|
69
69
|
hasWork: true,
|
|
70
70
|
reasons: ["d1"],
|
|
71
71
|
errored: [],
|
|
@@ -77,7 +77,7 @@ describe("d1 check", () => {
|
|
|
77
77
|
throw new Error("no cloudflare credentials");
|
|
78
78
|
},
|
|
79
79
|
});
|
|
80
|
-
expect(await run([CHECK], sources)).
|
|
80
|
+
expect(await run([CHECK], sources)).toMatchObject({
|
|
81
81
|
hasWork: true,
|
|
82
82
|
reasons: ["d1"],
|
|
83
83
|
errored: [{ kind: "d1", detail: "no cloudflare credentials" }],
|
|
@@ -88,14 +88,14 @@ describe("mail check", () => {
|
|
|
88
88
|
const CHECK = { type: "mail", mailbox: "ops@example.com" };
|
|
89
89
|
it("is false when nothing sits above the watermark", async () => {
|
|
90
90
|
seedWatermarks({ mail: { "ops@example.com|INBOX": { uid: 100, uidValidity: 1 } } });
|
|
91
|
-
expect(await run([CHECK], quietSources())).
|
|
91
|
+
expect(await run([CHECK], quietSources())).toMatchObject({ hasWork: false, reasons: [], errored: [] });
|
|
92
92
|
});
|
|
93
93
|
it("is true when a UID above the watermark matches", async () => {
|
|
94
94
|
seedWatermarks({ mail: { "ops@example.com|INBOX": { uid: 100, uidValidity: 1 } } });
|
|
95
95
|
const sources = quietSources({
|
|
96
96
|
mail: async () => ({ matched: true, highestUid: 104, uidValidity: 1 }),
|
|
97
97
|
});
|
|
98
|
-
expect(await run([CHECK], sources)).
|
|
98
|
+
expect(await run([CHECK], sources)).toMatchObject({ hasWork: true, reasons: ["mail"], errored: [] });
|
|
99
99
|
});
|
|
100
100
|
it("probes from UID 0 on a first run, so nothing already there is missed", async () => {
|
|
101
101
|
const seen = [];
|
|
@@ -113,7 +113,7 @@ describe("mail check", () => {
|
|
|
113
113
|
const sources = quietSources({
|
|
114
114
|
mail: async () => ({ matched: false, highestUid: 100, uidValidity: 7 }),
|
|
115
115
|
});
|
|
116
|
-
expect(await run([CHECK], sources)).
|
|
116
|
+
expect(await run([CHECK], sources)).toMatchObject({ hasWork: true, reasons: ["mail"], errored: [] });
|
|
117
117
|
});
|
|
118
118
|
it("keys the watermark by folder, so two folders of one mailbox never collide", async () => {
|
|
119
119
|
seedWatermarks({ mail: { "ops@example.com|Archive": { uid: 100, uidValidity: 1 } } });
|
|
@@ -131,12 +131,12 @@ describe("mail check", () => {
|
|
|
131
131
|
describe("whatsapp check", () => {
|
|
132
132
|
it("status is false while connected", async () => {
|
|
133
133
|
const check = { type: "whatsapp", scope: "status" };
|
|
134
|
-
expect(await run([check], quietSources())).
|
|
134
|
+
expect(await run([check], quietSources())).toMatchObject({ hasWork: false, reasons: [], errored: [] });
|
|
135
135
|
});
|
|
136
136
|
it("status is true while disconnected", async () => {
|
|
137
137
|
const check = { type: "whatsapp", scope: "status" };
|
|
138
138
|
const sources = quietSources({ whatsappStatus: async () => ({ connected: false }) });
|
|
139
|
-
expect(await run([check], sources)).
|
|
139
|
+
expect(await run([check], sources)).toMatchObject({ hasWork: true, reasons: ["whatsapp"], errored: [] });
|
|
140
140
|
});
|
|
141
141
|
it("inbound is true only on a message received since the watermark", async () => {
|
|
142
142
|
const check = { type: "whatsapp", scope: "inbound" };
|
|
@@ -144,7 +144,7 @@ describe("whatsapp check", () => {
|
|
|
144
144
|
const sources = quietSources({
|
|
145
145
|
whatsappInbound: async () => ({ latest: 1_754_000_900, newer: true }),
|
|
146
146
|
});
|
|
147
|
-
expect(await run([check], sources)).
|
|
147
|
+
expect(await run([check], sources)).toMatchObject({ hasWork: true, reasons: ["whatsapp"], errored: [] });
|
|
148
148
|
});
|
|
149
149
|
it("inbound passes the stored watermark to the source", async () => {
|
|
150
150
|
const check = { type: "whatsapp", scope: "inbound" };
|
|
@@ -164,15 +164,15 @@ describe("calendar check", () => {
|
|
|
164
164
|
const CHECK = { type: "calendar", connector: "google" };
|
|
165
165
|
it("is false on an unchanged digest", async () => {
|
|
166
166
|
seedWatermarks({ calendar: { digest: "DIGEST-A" } });
|
|
167
|
-
expect(await run([CHECK], quietSources())).
|
|
167
|
+
expect(await run([CHECK], quietSources())).toMatchObject({ hasWork: false, reasons: [], errored: [] });
|
|
168
168
|
});
|
|
169
169
|
it("is true on a changed digest", async () => {
|
|
170
170
|
seedWatermarks({ calendar: { digest: "DIGEST-A" } });
|
|
171
171
|
const sources = quietSources({ calendarDigest: async () => "DIGEST-B" });
|
|
172
|
-
expect(await run([CHECK], sources)).
|
|
172
|
+
expect(await run([CHECK], sources)).toMatchObject({ hasWork: true, reasons: ["calendar"], errored: [] });
|
|
173
173
|
});
|
|
174
174
|
it("is true on a first run, when nothing has been committed yet", async () => {
|
|
175
|
-
expect(await run([CHECK], quietSources())).
|
|
175
|
+
expect(await run([CHECK], quietSources())).toMatchObject({ hasWork: true, reasons: ["calendar"], errored: [] });
|
|
176
176
|
});
|
|
177
177
|
});
|
|
178
178
|
describe("digestBusy", () => {
|
|
@@ -229,48 +229,49 @@ describe("all four together", () => {
|
|
|
229
229
|
mail: { "ops@example.com|INBOX": { uid: 100, uidValidity: 1 } },
|
|
230
230
|
calendar: { digest: "DIGEST-A" },
|
|
231
231
|
});
|
|
232
|
-
expect(await run(ALL, quietSources())).
|
|
232
|
+
expect(await run(ALL, quietSources())).toMatchObject({ hasWork: false, reasons: [], errored: [] });
|
|
233
233
|
});
|
|
234
234
|
it("one noisy source is enough, and names only itself", async () => {
|
|
235
235
|
seedWatermarks({
|
|
236
236
|
mail: { "ops@example.com|INBOX": { uid: 100, uidValidity: 1 } },
|
|
237
237
|
calendar: { digest: "DIGEST-A" },
|
|
238
238
|
});
|
|
239
|
-
expect(await run(ALL, quietSources({ d1: async () => 2 }))).
|
|
239
|
+
expect(await run(ALL, quietSources({ d1: async () => 2 }))).toMatchObject({
|
|
240
240
|
hasWork: true,
|
|
241
241
|
reasons: ["d1"],
|
|
242
242
|
errored: [],
|
|
243
243
|
});
|
|
244
244
|
});
|
|
245
245
|
});
|
|
246
|
-
|
|
246
|
+
// Task 2663 — the advance is the dispatcher's, so it is tested through
|
|
247
|
+
// `advanceGateMark` against the readings a real gate call returned. The old
|
|
248
|
+
// `commit` describe drove the same ground through a parameter the agent set.
|
|
249
|
+
describe("advanceGateMark", () => {
|
|
247
250
|
const ALL = [
|
|
248
251
|
{ type: "mail", mailbox: "ops@example.com" },
|
|
249
252
|
{ type: "d1", database: "bookings", sql: "SELECT count(*) FROM b" },
|
|
250
253
|
{ type: "whatsapp", scope: "inbound" },
|
|
251
254
|
{ type: "calendar", connector: "google" },
|
|
252
255
|
];
|
|
253
|
-
it("
|
|
256
|
+
it("the gate itself leaves the file bytes unchanged", async () => {
|
|
254
257
|
seedWatermarks({ calendar: { digest: "DIGEST-A" } });
|
|
255
258
|
const path = watermarkPath(EVENT, platformRoot);
|
|
256
259
|
const before = readFileSync(path);
|
|
257
260
|
await run(ALL, quietSources({ calendarDigest: async () => "DIGEST-B" }));
|
|
258
261
|
expect(readFileSync(path).equals(before)).toBe(true);
|
|
259
262
|
});
|
|
260
|
-
it("
|
|
263
|
+
it("the gate on a first run writes no file at all", async () => {
|
|
261
264
|
await run(ALL, quietSources());
|
|
262
265
|
expect(existsSync(watermarkPath(EVENT, platformRoot))).toBe(false);
|
|
263
266
|
});
|
|
264
|
-
it("advances every watermark
|
|
267
|
+
it("advances every watermark the gate read", async () => {
|
|
265
268
|
const sources = quietSources({
|
|
266
269
|
mail: async () => ({ matched: true, highestUid: 250, uidValidity: 9 }),
|
|
267
270
|
whatsappInbound: async () => ({ latest: 1_754_009_999, newer: true }),
|
|
268
271
|
calendarDigest: async () => "DIGEST-Z",
|
|
269
272
|
});
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
await run(ALL, sources);
|
|
273
|
-
const result = await run(ALL, sources, true);
|
|
273
|
+
const gate = await run(ALL, sources);
|
|
274
|
+
const { advanced } = advanceGateMark(EVENT, platformRoot, gate.observed);
|
|
274
275
|
const marks = JSON.parse(readFileSync(watermarkPath(EVENT, platformRoot), "utf-8"));
|
|
275
276
|
expect(marks.mail["ops@example.com|inbox"]).toEqual({ uid: 250, uidValidity: 9 });
|
|
276
277
|
expect(marks.whatsapp.inbound).toBe(1_754_009_999);
|
|
@@ -278,77 +279,60 @@ describe("commit", () => {
|
|
|
278
279
|
expect(marks.lastCommitAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
279
280
|
// d1 has no watermark: its answer is a property of the present, not a
|
|
280
281
|
// position in a stream.
|
|
281
|
-
expect(
|
|
282
|
+
expect(advanced).toEqual(["ops@example.com|inbox", "whatsapp", "calendar"]);
|
|
283
|
+
});
|
|
284
|
+
it("names the position it wrote per advanced key", async () => {
|
|
285
|
+
const gate = await run([{ type: "calendar", connector: "google" }], quietSources({ calendarDigest: async () => "DIGEST-Z" }));
|
|
286
|
+
const { positions } = advanceGateMark(EVENT, platformRoot, gate.observed);
|
|
287
|
+
expect(positions).toEqual(["calendar:DIGEST-Z"]);
|
|
288
|
+
});
|
|
289
|
+
it("stamps lastCommitAt only when something moved", async () => {
|
|
290
|
+
// Stamping it on every accepted wake would make `auditGateMarkStalled`,
|
|
291
|
+
// which counts accepted wakes SINCE this instant, read 0 or 1 for ever — the
|
|
292
|
+
// check built to catch a mark that stopped moving could then never fire, and
|
|
293
|
+
// `auditGateWatermarkAge` would go permanently green for the same reason.
|
|
294
|
+
const { advanced } = advanceGateMark(EVENT, platformRoot, {});
|
|
295
|
+
expect(advanced).toEqual([]);
|
|
296
|
+
expect(existsSync(watermarkPath(EVENT, platformRoot))).toBe(false);
|
|
282
297
|
});
|
|
283
|
-
it("
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
expect(marks.lastCommitAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
298
|
+
it("leaves an existing file untouched when nothing moved", async () => {
|
|
299
|
+
seedWatermarks({ calendar: { digest: "DIGEST-A" }, lastCommitAt: "2026-08-10T10:00:00.000Z" });
|
|
300
|
+
const before = readFileSync(watermarkPath(EVENT, platformRoot));
|
|
301
|
+
advanceGateMark(EVENT, platformRoot, {});
|
|
302
|
+
expect(readFileSync(watermarkPath(EVENT, platformRoot)).equals(before)).toBe(true);
|
|
289
303
|
});
|
|
290
|
-
it("
|
|
304
|
+
it("an advanced watermark makes the next gate call quiet", async () => {
|
|
291
305
|
const sources = quietSources({ calendarDigest: async () => "DIGEST-Z" });
|
|
292
306
|
const checks = [{ type: "calendar", connector: "google" }];
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
307
|
+
const first = await run(checks, sources);
|
|
308
|
+
expect(first).toMatchObject({ hasWork: true, reasons: ["calendar"], errored: [] });
|
|
309
|
+
advanceGateMark(EVENT, platformRoot, first.observed);
|
|
310
|
+
expect(await run(checks, sources)).toMatchObject({ hasWork: false, reasons: [], errored: [] });
|
|
296
311
|
});
|
|
297
|
-
it("a check that
|
|
312
|
+
it("a check that errored is not advanced, so its work is not marked consumed", async () => {
|
|
298
313
|
const checks = [{ type: "calendar", connector: "google" }];
|
|
299
314
|
const sources = quietSources({
|
|
300
315
|
calendarDigest: async () => {
|
|
301
316
|
throw new Error("neo4j down");
|
|
302
317
|
},
|
|
303
318
|
});
|
|
304
|
-
const
|
|
305
|
-
expect(
|
|
306
|
-
const
|
|
307
|
-
expect(
|
|
319
|
+
const gate = await run(checks, sources);
|
|
320
|
+
expect(gate).toMatchObject({ hasWork: true, reasons: ["calendar"], observed: {} });
|
|
321
|
+
const { advanced } = advanceGateMark(EVENT, platformRoot, gate.observed);
|
|
322
|
+
expect(advanced).toEqual([]);
|
|
323
|
+
expect(existsSync(watermarkPath(EVENT, platformRoot))).toBe(false);
|
|
308
324
|
});
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
{ type: "mail", mailbox: "ops@example.com" },
|
|
315
|
-
{ type: "d1", database: "bookings", sql: "SELECT count(*) FROM b" },
|
|
316
|
-
];
|
|
317
|
-
// Task 2654 — the gate call records the position the commit promotes.
|
|
318
|
-
await run(checks, quietSources());
|
|
319
|
-
const result = await run(checks, quietSources(), true);
|
|
320
|
-
// Task 2635 folds the folder segment to lower case on write.
|
|
321
|
-
expect(result.advanced).toEqual(["ops@example.com|inbox"]);
|
|
322
|
-
expect(result.skipped).toEqual(["d1"]);
|
|
323
|
-
});
|
|
324
|
-
it("counts every skipped check, not every skipped kind", async () => {
|
|
325
|
-
const checks = [
|
|
325
|
+
it("a check holding no position contributes no key, so nothing advances for it", async () => {
|
|
326
|
+
// Task 2633 — an agent read an empty `advanced` over four d1 checks,
|
|
327
|
+
// inferred a broken watermark and deleted all four. `d1` and `whatsapp` in
|
|
328
|
+
// `status` scope read a current value; there is no position to record.
|
|
329
|
+
const gate = await run([
|
|
326
330
|
{ type: "d1", database: "bookings", sql: "SELECT max(id) FROM b" },
|
|
327
331
|
{ type: "d1", database: "leads", sql: "SELECT max(id) FROM l" },
|
|
328
332
|
{ type: "whatsapp", scope: "status" },
|
|
329
|
-
];
|
|
330
|
-
|
|
331
|
-
expect(
|
|
332
|
-
});
|
|
333
|
-
it("a plain call reports no skipped array at all", async () => {
|
|
334
|
-
const checks = [
|
|
335
|
-
{ type: "d1", database: "bookings", sql: "SELECT count(*) FROM b" },
|
|
336
|
-
];
|
|
337
|
-
expect(await run(checks, quietSources())).toEqual({ hasWork: false, reasons: [], errored: [] });
|
|
338
|
-
});
|
|
339
|
-
it("a check that errored is neither advanced nor skipped, because its state is unknown", async () => {
|
|
340
|
-
const checks = [
|
|
341
|
-
{ type: "d1", database: "bookings", sql: "SELECT count(*) FROM b" },
|
|
342
|
-
{ type: "calendar", connector: "google" },
|
|
343
|
-
];
|
|
344
|
-
const sources = quietSources({
|
|
345
|
-
calendarDigest: async () => {
|
|
346
|
-
throw new Error("neo4j down");
|
|
347
|
-
},
|
|
348
|
-
});
|
|
349
|
-
const result = await run(checks, sources, true);
|
|
350
|
-
expect(result.advanced).toEqual([]);
|
|
351
|
-
expect(result.skipped).toEqual(["d1"]);
|
|
333
|
+
], quietSources());
|
|
334
|
+
expect(gate.observed).toEqual({});
|
|
335
|
+
expect(advanceGateMark(EVENT, platformRoot, gate.observed).advanced).toEqual([]);
|
|
352
336
|
});
|
|
353
337
|
});
|
|
354
338
|
describe("watermark location", () => {
|
|
@@ -599,14 +583,16 @@ describe("watermark folder-case fold", () => {
|
|
|
599
583
|
const sources = quietSources({
|
|
600
584
|
mail: async (_c, since) => ({ matched: false, highestUid: since + 1, uidValidity: 1 }),
|
|
601
585
|
});
|
|
602
|
-
// Task
|
|
586
|
+
// Task 2663 — each advance promotes what its own gate call read, so both
|
|
603
587
|
// fires run the real two-step sequence.
|
|
604
|
-
await run([lower], sources);
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
const
|
|
609
|
-
expect(
|
|
588
|
+
const firstGate = await run([lower], sources);
|
|
589
|
+
expect(advanceGateMark(EVENT, platformRoot, firstGate.observed).advanced).toEqual([
|
|
590
|
+
"ops@example.com|inbox",
|
|
591
|
+
]);
|
|
592
|
+
const secondGate = await run([upper], sources);
|
|
593
|
+
expect(advanceGateMark(EVENT, platformRoot, secondGate.observed).advanced).toEqual([
|
|
594
|
+
"ops@example.com|inbox",
|
|
595
|
+
]);
|
|
610
596
|
const marks = JSON.parse(readFileSync(watermarkPath(EVENT, platformRoot), "utf-8"));
|
|
611
597
|
expect(Object.keys(marks.mail)).toEqual(["ops@example.com|inbox"]);
|
|
612
598
|
// The second call read the first call's position rather than restarting at 0.
|
|
@@ -664,11 +650,11 @@ describe("telegram check", () => {
|
|
|
664
650
|
};
|
|
665
651
|
const measured = (lines) => lines.every((l) => !l.includes("op=check-error"));
|
|
666
652
|
it("is false when the account holds no inbound message at all", async () => {
|
|
667
|
-
expect(await run([CHECK], quietSources())).
|
|
653
|
+
expect(await run([CHECK], quietSources())).toMatchObject({ hasWork: false, reasons: [], errored: [] });
|
|
668
654
|
});
|
|
669
655
|
it("is false when nothing arrived after the watermark", async () => {
|
|
670
656
|
seedWatermarks({ telegram: { inbound: "2026-08-05T10:00:00.000Z" } });
|
|
671
|
-
expect(await run([CHECK], quietSources(at("2026-08-05T09:00:00.000Z")))).
|
|
657
|
+
expect(await run([CHECK], quietSources(at("2026-08-05T09:00:00.000Z")))).toMatchObject({
|
|
672
658
|
hasWork: false,
|
|
673
659
|
reasons: [],
|
|
674
660
|
errored: [],
|
|
@@ -677,7 +663,7 @@ describe("telegram check", () => {
|
|
|
677
663
|
it("is true when a message arrived after the watermark", async () => {
|
|
678
664
|
seedWatermarks({ telegram: { inbound: "2026-08-05T10:00:00.000Z" } });
|
|
679
665
|
const lines = linesFrom();
|
|
680
|
-
expect(await run([CHECK], quietSources(at("2026-08-05T11:00:00.000Z")))).
|
|
666
|
+
expect(await run([CHECK], quietSources(at("2026-08-05T11:00:00.000Z")))).toMatchObject({
|
|
681
667
|
hasWork: true,
|
|
682
668
|
reasons: ["telegram"],
|
|
683
669
|
errored: [],
|
|
@@ -688,7 +674,7 @@ describe("telegram check", () => {
|
|
|
688
674
|
// The boundary is `>`, not `>=`. A `>=` would re-report the last handled
|
|
689
675
|
// message on every fire and the gate could never go quiet.
|
|
690
676
|
seedWatermarks({ telegram: { inbound: "2026-08-05T10:00:00.000Z" } });
|
|
691
|
-
expect(await run([CHECK], quietSources(at("2026-08-05T10:00:00.000Z")))).
|
|
677
|
+
expect(await run([CHECK], quietSources(at("2026-08-05T10:00:00.000Z")))).toMatchObject({
|
|
692
678
|
hasWork: false,
|
|
693
679
|
reasons: [],
|
|
694
680
|
errored: [],
|
|
@@ -696,7 +682,7 @@ describe("telegram check", () => {
|
|
|
696
682
|
});
|
|
697
683
|
it("is true on a first run, so nothing already waiting is missed", async () => {
|
|
698
684
|
const lines = linesFrom();
|
|
699
|
-
expect(await run([CHECK], quietSources(at("2026-08-05T09:00:00.000Z")))).
|
|
685
|
+
expect(await run([CHECK], quietSources(at("2026-08-05T09:00:00.000Z")))).toMatchObject({
|
|
700
686
|
hasWork: true,
|
|
701
687
|
reasons: ["telegram"],
|
|
702
688
|
errored: [],
|
|
@@ -710,7 +696,7 @@ describe("telegram check", () => {
|
|
|
710
696
|
throw new Error("HTTP 500 on GET /api/telegram-reader/gate-probe");
|
|
711
697
|
},
|
|
712
698
|
});
|
|
713
|
-
expect(await run([CHECK], sources)).
|
|
699
|
+
expect(await run([CHECK], sources)).toMatchObject({
|
|
714
700
|
hasWork: true,
|
|
715
701
|
reasons: ["telegram"],
|
|
716
702
|
errored: [{ kind: "telegram", detail: "HTTP 500 on GET /api/telegram-reader/gate-probe" }],
|
|
@@ -724,13 +710,9 @@ describe("telegram check", () => {
|
|
|
724
710
|
expect(line).toContain("kind=telegram");
|
|
725
711
|
expect(line).toContain("/api/telegram-reader/gate-probe");
|
|
726
712
|
});
|
|
727
|
-
it("
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
// while the turn ran.
|
|
731
|
-
await run([CHECK], quietSources(at("2026-08-05T11:00:00.000Z")));
|
|
732
|
-
const result = await run([CHECK], quietSources(at("2026-08-05T11:00:00.000Z")), true);
|
|
733
|
-
expect(result.advanced).toEqual(["telegram"]);
|
|
713
|
+
it("the advance writes the watermark to what the gate's probe read", async () => {
|
|
714
|
+
const gate = await run([CHECK], quietSources(at("2026-08-05T11:00:00.000Z")));
|
|
715
|
+
expect(advanceGateMark(EVENT, platformRoot, gate.observed).advanced).toEqual(["telegram"]);
|
|
734
716
|
const marks = readWatermarks(EVENT, platformRoot);
|
|
735
717
|
expect(marks.telegram).toEqual({ inbound: "2026-08-05T11:00:00.000Z" });
|
|
736
718
|
});
|
|
@@ -770,21 +752,18 @@ describe("telegram check", () => {
|
|
|
770
752
|
// nobody has read. That is the mute direction, and it loses a booking.
|
|
771
753
|
seedWatermarks({ telegram: { inbound: "2026-08-05T18:00:00.000Z" } });
|
|
772
754
|
const check = { type: "telegram", scope: "inbound", botId: "111" };
|
|
773
|
-
expect(await run([check], quietSources(at("2026-08-05T12:00:00.000Z")))).
|
|
755
|
+
expect(await run([check], quietSources(at("2026-08-05T12:00:00.000Z")))).toMatchObject({
|
|
774
756
|
hasWork: true,
|
|
775
757
|
reasons: ["telegram"],
|
|
776
758
|
errored: [],
|
|
777
759
|
});
|
|
778
760
|
});
|
|
779
|
-
it("a scoped
|
|
761
|
+
it("a scoped advance writes under the bot, leaving the account-wide mark alone", async () => {
|
|
780
762
|
seedWatermarks({ telegram: { inbound: "2026-08-05T18:00:00.000Z" } });
|
|
781
763
|
const check = { type: "telegram", scope: "inbound", botId: "111" };
|
|
782
764
|
const sources = quietSources(at("2026-08-05T12:00:00.000Z"));
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
await run([check], sources);
|
|
786
|
-
const result = await run([check], sources, true);
|
|
787
|
-
expect(result.advanced).toEqual(["telegram:111"]);
|
|
765
|
+
const gate = await run([check], sources);
|
|
766
|
+
expect(advanceGateMark(EVENT, platformRoot, gate.observed).advanced).toEqual(["telegram:111"]);
|
|
788
767
|
const marks = readWatermarks(EVENT, platformRoot);
|
|
789
768
|
expect(marks.telegram?.inbound).toBe("2026-08-05T18:00:00.000Z");
|
|
790
769
|
expect(marks.telegram?.byBot).toEqual({ "111": "2026-08-05T12:00:00.000Z" });
|
|
@@ -828,14 +807,15 @@ describe("telegram check", () => {
|
|
|
828
807
|
expect(line).toContain("kind=telegram");
|
|
829
808
|
expect(line).toContain("0000000000");
|
|
830
809
|
});
|
|
831
|
-
it("a
|
|
810
|
+
it("a quiet account records no position, so nothing advances onto nothing", async () => {
|
|
832
811
|
// A null probe is "nothing to measure from", not "position zero". There is
|
|
833
|
-
// no position to record, so the check
|
|
834
|
-
//
|
|
835
|
-
//
|
|
812
|
+
// no position to record, so the check contributes no key and the stored mark
|
|
813
|
+
// is left alone. Writing null would drop the mark and re-report every stored
|
|
814
|
+
// message on the next fire.
|
|
836
815
|
seedWatermarks({ telegram: { inbound: "2026-08-05T10:00:00.000Z" } });
|
|
837
|
-
const
|
|
838
|
-
expect(
|
|
816
|
+
const gate = await run([CHECK], quietSources());
|
|
817
|
+
expect(gate).toMatchObject({ hasWork: false, observed: {} });
|
|
818
|
+
expect(advanceGateMark(EVENT, platformRoot, gate.observed).advanced).toEqual([]);
|
|
839
819
|
expect(readWatermarks(EVENT, platformRoot).telegram).toEqual({
|
|
840
820
|
inbound: "2026-08-05T10:00:00.000Z",
|
|
841
821
|
});
|
|
@@ -849,7 +829,7 @@ describe("telegram check", () => {
|
|
|
849
829
|
describe("unrecognised check type", () => {
|
|
850
830
|
const CHECK = { type: "nonsense" };
|
|
851
831
|
it("resolves rather than throwing, and counts the unknown type as work", async () => {
|
|
852
|
-
expect(await run([CHECK], quietSources())).
|
|
832
|
+
expect(await run([CHECK], quietSources())).toMatchObject({
|
|
853
833
|
hasWork: true,
|
|
854
834
|
reasons: ["nonsense"],
|
|
855
835
|
errored: [
|
|
@@ -873,7 +853,7 @@ describe("unrecognised check type", () => {
|
|
|
873
853
|
});
|
|
874
854
|
it("still runs the sibling checks — one bad type does not abort the gate", async () => {
|
|
875
855
|
const checks = [CHECK, { type: "d1", database: "b", sql: "SELECT 1" }];
|
|
876
|
-
expect(await run(checks, quietSources())).
|
|
856
|
+
expect(await run(checks, quietSources())).toMatchObject({
|
|
877
857
|
hasWork: true,
|
|
878
858
|
reasons: ["nonsense"],
|
|
879
859
|
errored: [
|
|
@@ -938,8 +918,8 @@ describe("errored", () => {
|
|
|
938
918
|
{ kind: "calendar", detail: "calendar down" },
|
|
939
919
|
]);
|
|
940
920
|
});
|
|
941
|
-
it("is present
|
|
942
|
-
const res = await run([D1], quietSources()
|
|
921
|
+
it("is present and empty when every check answered", async () => {
|
|
922
|
+
const res = await run([D1], quietSources());
|
|
943
923
|
expect(res.errored).toEqual([]);
|
|
944
924
|
});
|
|
945
925
|
});
|
|
@@ -959,23 +939,16 @@ describe("mail check — outlook provider", () => {
|
|
|
959
939
|
const sources = quietSources({
|
|
960
940
|
mailOutlook: async () => ({ newestReceivedAt: "2026-08-10T09:00:00Z" }),
|
|
961
941
|
});
|
|
962
|
-
expect(await run([CHECK], sources)).
|
|
942
|
+
expect(await run([CHECK], sources)).toMatchObject({ hasWork: false, reasons: [], errored: [] });
|
|
963
943
|
});
|
|
964
|
-
it("is true on a message above the watermark, and
|
|
944
|
+
it("is true on a message above the watermark, and the advance writes that key", async () => {
|
|
965
945
|
seedWatermarks({ mailOutlook: { [KEY]: { receivedAt: "2026-08-10T09:00:00Z" } } });
|
|
966
946
|
const sources = quietSources({
|
|
967
947
|
mailOutlook: async () => ({ newestReceivedAt: "2026-08-10T09:05:00Z" }),
|
|
968
948
|
});
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
expect(await run([CHECK], sources, true)).toEqual({
|
|
973
|
-
hasWork: true,
|
|
974
|
-
reasons: ["mail"],
|
|
975
|
-
errored: [],
|
|
976
|
-
advanced: [KEY],
|
|
977
|
-
skipped: [],
|
|
978
|
-
});
|
|
949
|
+
const gate = await run([CHECK], sources);
|
|
950
|
+
expect(gate).toMatchObject({ hasWork: true, reasons: ["mail"], errored: [] });
|
|
951
|
+
expect(advanceGateMark(EVENT, platformRoot, gate.observed).advanced).toEqual([KEY]);
|
|
979
952
|
const written = JSON.parse(readFileSync(watermarkPath(EVENT, platformRoot), "utf-8"));
|
|
980
953
|
expect(written.mailOutlook[KEY]).toEqual({ receivedAt: "2026-08-10T09:05:00Z" });
|
|
981
954
|
});
|
|
@@ -983,7 +956,7 @@ describe("mail check — outlook provider", () => {
|
|
|
983
956
|
const sources = quietSources({
|
|
984
957
|
mailOutlook: async () => ({ newestReceivedAt: "2026-08-10T09:00:00Z" }),
|
|
985
958
|
});
|
|
986
|
-
expect(await run([CHECK], sources)).
|
|
959
|
+
expect(await run([CHECK], sources)).toMatchObject({ hasWork: true, reasons: ["mail"], errored: [] });
|
|
987
960
|
});
|
|
988
961
|
it("orders by instant, not lexically, so a fractional second is not read as older", async () => {
|
|
989
962
|
seedWatermarks({ mailOutlook: { [KEY]: { receivedAt: "2026-08-10T09:00:00Z" } } });
|
|
@@ -991,17 +964,13 @@ describe("mail check — outlook provider", () => {
|
|
|
991
964
|
// Lexically "…00.500Z" sorts BELOW "…00Z" ('.' is 0x2E, 'Z' is 0x5A).
|
|
992
965
|
mailOutlook: async () => ({ newestReceivedAt: "2026-08-10T09:00:00.500Z" }),
|
|
993
966
|
});
|
|
994
|
-
expect(await run([CHECK], sources)).
|
|
967
|
+
expect(await run([CHECK], sources)).toMatchObject({ hasWork: true, reasons: ["mail"], errored: [] });
|
|
995
968
|
});
|
|
996
|
-
it("
|
|
969
|
+
it("records no position for an empty folder rather than advancing onto nothing", async () => {
|
|
997
970
|
const sources = quietSources({ mailOutlook: async () => ({ newestReceivedAt: null }) });
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
errored: [],
|
|
1002
|
-
advanced: [],
|
|
1003
|
-
skipped: ["mail"],
|
|
1004
|
-
});
|
|
971
|
+
const gate = await run([CHECK], sources);
|
|
972
|
+
expect(gate).toEqual({ hasWork: false, reasons: [], errored: [], observed: {} });
|
|
973
|
+
expect(advanceGateMark(EVENT, platformRoot, gate.observed).advanced).toEqual([]);
|
|
1005
974
|
});
|
|
1006
975
|
it("keys the folder segment lower-cased, so two spellings address one entry", async () => {
|
|
1007
976
|
seedWatermarks({ mailOutlook: { [KEY]: { receivedAt: "2026-08-10T09:00:00Z" } } });
|
|
@@ -1009,7 +978,7 @@ describe("mail check — outlook provider", () => {
|
|
|
1009
978
|
const sources = quietSources({
|
|
1010
979
|
mailOutlook: async () => ({ newestReceivedAt: "2026-08-10T09:00:00Z" }),
|
|
1011
980
|
});
|
|
1012
|
-
expect(await run([check], sources)).
|
|
981
|
+
expect(await run([check], sources)).toMatchObject({ hasWork: false, reasons: [], errored: [] });
|
|
1013
982
|
});
|
|
1014
983
|
it("counts a probe that throws as work, and never as quiet", async () => {
|
|
1015
984
|
const sources = quietSources({
|
|
@@ -1017,7 +986,7 @@ describe("mail check — outlook provider", () => {
|
|
|
1017
986
|
throw new Error("No Outlook mailbox admin@muvin.co.uk is connected");
|
|
1018
987
|
},
|
|
1019
988
|
});
|
|
1020
|
-
expect(await run([CHECK], sources)).
|
|
989
|
+
expect(await run([CHECK], sources)).toMatchObject({
|
|
1021
990
|
hasWork: true,
|
|
1022
991
|
reasons: ["mail"],
|
|
1023
992
|
errored: [{ kind: "mail", detail: "No Outlook mailbox admin@muvin.co.uk is connected" }],
|
|
@@ -1025,7 +994,7 @@ describe("mail check — outlook provider", () => {
|
|
|
1025
994
|
});
|
|
1026
995
|
it("throws on a provider outside the accepted list", async () => {
|
|
1027
996
|
const check = { type: "mail", mailbox: "a@b.com", provider: "pop3" };
|
|
1028
|
-
expect(await run([check], quietSources())).
|
|
997
|
+
expect(await run([check], quietSources())).toMatchObject({
|
|
1029
998
|
hasWork: true,
|
|
1030
999
|
reasons: ["mail"],
|
|
1031
1000
|
errored: [{ kind: "mail", detail: 'unknown mail provider "pop3" — accepted: imap, outlook' }],
|
|
@@ -1036,9 +1005,9 @@ describe("mail check — outlook provider", () => {
|
|
|
1036
1005
|
const sources = quietSources({
|
|
1037
1006
|
mailOutlook: async () => ({ newestReceivedAt: "2026-08-10T09:00:00Z" }),
|
|
1038
1007
|
});
|
|
1039
|
-
// Task
|
|
1040
|
-
await run([CHECK], sources);
|
|
1041
|
-
|
|
1008
|
+
// Task 2663 — the gate call records the position the advance promotes.
|
|
1009
|
+
const gate = await run([CHECK], sources);
|
|
1010
|
+
advanceGateMark(EVENT, platformRoot, gate.observed);
|
|
1042
1011
|
const written = JSON.parse(readFileSync(watermarkPath(EVENT, platformRoot), "utf-8"));
|
|
1043
1012
|
expect(written.mail[KEY]).toEqual({ uid: 42, uidValidity: 7 });
|
|
1044
1013
|
expect(written.mailOutlook[KEY]).toEqual({ receivedAt: "2026-08-10T09:00:00Z" });
|
|
@@ -1056,18 +1025,21 @@ describe("mail check — outlook provider", () => {
|
|
|
1056
1025
|
});
|
|
1057
1026
|
});
|
|
1058
1027
|
// ---------------------------------------------------------------------------
|
|
1059
|
-
// Task 2654 — a source that moves between the gate call
|
|
1028
|
+
// Task 2654, kept under Task 2663 — a source that moves between the gate call
|
|
1029
|
+
// and the advance.
|
|
1060
1030
|
//
|
|
1061
|
-
// The
|
|
1062
|
-
//
|
|
1063
|
-
//
|
|
1064
|
-
//
|
|
1031
|
+
// The advance happens after the wake was accepted, by which time the source may
|
|
1032
|
+
// have moved again. Writing what the source holds THEN would put the arrival
|
|
1033
|
+
// below the mark and no later fire would report it. The dispatcher writes what
|
|
1034
|
+
// the GATE returned instead, so the mover stays above. Under 2654 that guarantee
|
|
1035
|
+
// depended on a sidecar file being fresh; it is now structural, because the
|
|
1036
|
+
// positions travel with the result.
|
|
1065
1037
|
//
|
|
1066
|
-
// Each check is driven alone, and each test runs the real sequence: gate,
|
|
1067
|
-
//
|
|
1038
|
+
// Each check is driven alone, and each test runs the real sequence: gate, source
|
|
1039
|
+
// moves, advance, gate again.
|
|
1068
1040
|
// ---------------------------------------------------------------------------
|
|
1069
|
-
describe("a source that moves between the gate call and the
|
|
1070
|
-
it("mail: a UID that arrives mid-turn is still reported after the
|
|
1041
|
+
describe("a source that moves between the gate call and the advance", () => {
|
|
1042
|
+
it("mail: a UID that arrives mid-turn is still reported after the advance", async () => {
|
|
1071
1043
|
let highest = 105;
|
|
1072
1044
|
const sources = quietSources({
|
|
1073
1045
|
mail: async (_c, sinceUid) => ({
|
|
@@ -1077,14 +1049,15 @@ describe("a source that moves between the gate call and the commit", () => {
|
|
|
1077
1049
|
}),
|
|
1078
1050
|
});
|
|
1079
1051
|
const checks = [{ type: "mail", mailbox: "ops@example.com" }];
|
|
1080
|
-
|
|
1052
|
+
const gate = await run(checks, sources);
|
|
1053
|
+
expect(gate).toMatchObject({ hasWork: true, reasons: ["mail"], errored: [] });
|
|
1081
1054
|
highest = 106; // arrives while the turn runs
|
|
1082
|
-
|
|
1055
|
+
advanceGateMark(EVENT, platformRoot, gate.observed);
|
|
1083
1056
|
const marks = JSON.parse(readFileSync(watermarkPath(EVENT, platformRoot), "utf-8"));
|
|
1084
1057
|
expect(marks.mail["ops@example.com|inbox"].uid).toBe(105);
|
|
1085
|
-
expect(await run(checks, sources)).
|
|
1058
|
+
expect(await run(checks, sources)).toMatchObject({ hasWork: true, reasons: ["mail"] });
|
|
1086
1059
|
});
|
|
1087
|
-
it("whatsapp: an inbound message mid-turn is still reported after the
|
|
1060
|
+
it("whatsapp: an inbound message mid-turn is still reported after the advance", async () => {
|
|
1088
1061
|
let latest = 1_754_000_100;
|
|
1089
1062
|
const sources = quietSources({
|
|
1090
1063
|
whatsappInbound: async (_a, since) => ({
|
|
@@ -1093,86 +1066,96 @@ describe("a source that moves between the gate call and the commit", () => {
|
|
|
1093
1066
|
}),
|
|
1094
1067
|
});
|
|
1095
1068
|
const checks = [{ type: "whatsapp", scope: "inbound" }];
|
|
1096
|
-
|
|
1069
|
+
const gate = await run(checks, sources);
|
|
1070
|
+
expect(gate).toMatchObject({ hasWork: true, reasons: ["whatsapp"], errored: [] });
|
|
1097
1071
|
latest = 1_754_000_200;
|
|
1098
|
-
|
|
1072
|
+
advanceGateMark(EVENT, platformRoot, gate.observed);
|
|
1099
1073
|
const marks = JSON.parse(readFileSync(watermarkPath(EVENT, platformRoot), "utf-8"));
|
|
1100
1074
|
expect(marks.whatsapp.inbound).toBe(1_754_000_100);
|
|
1101
|
-
expect(await run(checks, sources)).
|
|
1075
|
+
expect(await run(checks, sources)).toMatchObject({ hasWork: true, reasons: ["whatsapp"] });
|
|
1102
1076
|
});
|
|
1103
|
-
it("telegram: an inbound message mid-turn is still reported after the
|
|
1077
|
+
it("telegram: an inbound message mid-turn is still reported after the advance", async () => {
|
|
1104
1078
|
let newest = "2026-08-10T10:00:00.000Z";
|
|
1105
1079
|
const sources = quietSources({ telegramInbound: async () => ({ newestInboundAt: newest }) });
|
|
1106
1080
|
const checks = [{ type: "telegram", scope: "inbound" }];
|
|
1107
|
-
|
|
1081
|
+
const gate = await run(checks, sources);
|
|
1082
|
+
expect(gate).toMatchObject({ hasWork: true, reasons: ["telegram"], errored: [] });
|
|
1108
1083
|
newest = "2026-08-10T10:30:00.000Z";
|
|
1109
|
-
|
|
1084
|
+
advanceGateMark(EVENT, platformRoot, gate.observed);
|
|
1110
1085
|
const marks = JSON.parse(readFileSync(watermarkPath(EVENT, platformRoot), "utf-8"));
|
|
1111
1086
|
expect(marks.telegram.inbound).toBe("2026-08-10T10:00:00.000Z");
|
|
1112
|
-
expect(await run(checks, sources)).
|
|
1087
|
+
expect(await run(checks, sources)).toMatchObject({ hasWork: true, reasons: ["telegram"] });
|
|
1113
1088
|
});
|
|
1114
|
-
it("calendar: a connector change mid-turn is still reported after the
|
|
1089
|
+
it("calendar: a connector change mid-turn is still reported after the advance", async () => {
|
|
1115
1090
|
let digest = "DIGEST-A";
|
|
1116
1091
|
const sources = quietSources({ calendarDigest: async () => digest });
|
|
1117
1092
|
const checks = [{ type: "calendar", connector: "google" }];
|
|
1118
|
-
|
|
1093
|
+
const gate = await run(checks, sources);
|
|
1094
|
+
expect(gate).toMatchObject({ hasWork: true, reasons: ["calendar"], errored: [] });
|
|
1119
1095
|
digest = "DIGEST-B";
|
|
1120
|
-
|
|
1096
|
+
advanceGateMark(EVENT, platformRoot, gate.observed);
|
|
1121
1097
|
const marks = JSON.parse(readFileSync(watermarkPath(EVENT, platformRoot), "utf-8"));
|
|
1122
1098
|
expect(marks.calendar.digest).toBe("DIGEST-A");
|
|
1123
|
-
expect(await run(checks, sources)).
|
|
1124
|
-
});
|
|
1125
|
-
// Task
|
|
1126
|
-
//
|
|
1127
|
-
//
|
|
1128
|
-
|
|
1099
|
+
expect(await run(checks, sources)).toMatchObject({ hasWork: true, reasons: ["calendar"] });
|
|
1100
|
+
});
|
|
1101
|
+
// Task 2663 — the no-event case. The gate that could never go quiet emitted a
|
|
1102
|
+
// line on every fire that looked exactly like a healthy one, because it named
|
|
1103
|
+
// only what the probe had just read and never the value it was compared
|
|
1104
|
+
// against. Both sides now print, so a mark stuck on an empty slot is legible
|
|
1105
|
+
// from one line without reproducing anything.
|
|
1106
|
+
describe("gate-read names both sides of the comparison", () => {
|
|
1129
1107
|
// `console.error` is already spied in `beforeEach`, so its recorded calls are
|
|
1130
1108
|
// the log. Re-mocking it here would drop the lines written before this point.
|
|
1131
1109
|
const errorLines = () => vi.mocked(console.error).mock.calls.map((c) => c.map(String).join(" "));
|
|
1132
|
-
it("names
|
|
1110
|
+
it("names null stored on a first fire, and the observed position beside it", async () => {
|
|
1133
1111
|
const sources = quietSources({ calendarDigest: async () => "abcdef0123456789" });
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
await run(checks, sources, true);
|
|
1137
|
-
expect(errorLines()).toContainEqual(expect.stringContaining("op=gate-read eventId=65ef758c check=calendar position=abcdef01"));
|
|
1138
|
-
expect(errorLines()).toContainEqual(expect.stringContaining("positions=calendar:abcdef01"));
|
|
1112
|
+
await run([{ type: "calendar", connector: "google" }], sources);
|
|
1113
|
+
expect(errorLines()).toContainEqual(expect.stringContaining("op=gate-read eventId=65ef758c check=calendar stored=null observed=abcdef01"));
|
|
1139
1114
|
});
|
|
1140
|
-
it("names
|
|
1141
|
-
|
|
1142
|
-
// `op=commit` still prints an empty `advanced=` — indistinguishable from a
|
|
1143
|
-
// healthy first commit without a line of its own.
|
|
1144
|
-
const sources = quietSources({ calendarDigest: async () => "DIGEST-Z" });
|
|
1115
|
+
it("names the stored position once the mark has advanced", async () => {
|
|
1116
|
+
const sources = quietSources({ calendarDigest: async () => "abcdef0123456789" });
|
|
1145
1117
|
const checks = [{ type: "calendar", connector: "google" }];
|
|
1118
|
+
const gate = await run(checks, sources);
|
|
1119
|
+
advanceGateMark(EVENT, platformRoot, gate.observed);
|
|
1146
1120
|
await run(checks, sources);
|
|
1147
|
-
|
|
1148
|
-
const result = await run(checks, sources, true);
|
|
1149
|
-
expect(result.advanced).toEqual([]);
|
|
1150
|
-
expect(errorLines()).toContainEqual(expect.stringContaining("op=observation-unreadable eventId=65ef758c reason="));
|
|
1151
|
-
});
|
|
1152
|
-
it("says nothing when no observation file exists, which is a first fire", async () => {
|
|
1153
|
-
const sources = quietSources({ calendarDigest: async () => "DIGEST-Z" });
|
|
1154
|
-
await run([{ type: "calendar", connector: "google" }], sources, true);
|
|
1155
|
-
expect(errorLines().filter((l) => l.includes("op=observation-unreadable"))).toEqual([]);
|
|
1121
|
+
expect(errorLines()).toContainEqual(expect.stringContaining("op=gate-read eventId=65ef758c check=calendar stored=abcdef01 observed=abcdef01"));
|
|
1156
1122
|
});
|
|
1157
1123
|
it("emits no gate-read for a check that holds no position", async () => {
|
|
1158
|
-
// `op=check-error`
|
|
1159
|
-
//
|
|
1124
|
+
// `op=check-error` already covers a check with no position; a line of two
|
|
1125
|
+
// nulls would only add a value nobody can compare.
|
|
1160
1126
|
await run([{ type: "d1", database: "b", sql: "SELECT count(*) FROM b" }], quietSources());
|
|
1161
1127
|
expect(errorLines().filter((l) => l.includes("op=gate-read"))).toEqual([]);
|
|
1162
1128
|
});
|
|
1163
1129
|
});
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
expect(
|
|
1175
|
-
|
|
1130
|
+
});
|
|
1131
|
+
// Task 2663 — the gate hands its readings back to the dispatcher and touches no
|
|
1132
|
+
// sidecar. The commit and the `<eventId>.observed.json` file it promoted from
|
|
1133
|
+
// are both gone: a sidecar survives a gate call that errored, so the stale
|
|
1134
|
+
// positions from an earlier fire would be promoted on the next accepted wake and
|
|
1135
|
+
// mark as seen messages nobody read.
|
|
1136
|
+
describe("observed positions (Task 2663)", () => {
|
|
1137
|
+
it("returns the positions it read, and writes no sidecar", async () => {
|
|
1138
|
+
const result = await run([{ type: "telegram", scope: "inbound", botId: "555" }], quietSources({ telegramInbound: async () => ({ newestInboundAt: "2026-08-10T18:00:00.000Z" }) }));
|
|
1139
|
+
expect(result.observed).toEqual({ telegram: { byBot: { "555": "2026-08-10T18:00:00.000Z" } } });
|
|
1140
|
+
expect(existsSync(join(platformRoot, "..", "state", "scheduling-gate", `${EVENT}.observed.json`))).toBe(false);
|
|
1141
|
+
});
|
|
1142
|
+
it("reports no position for a check that holds none", async () => {
|
|
1143
|
+
// `d1` reads a current value rather than a position in a stream, so there is
|
|
1144
|
+
// nothing for the dispatcher to promote and `observed` is empty rather than
|
|
1145
|
+
// carrying a fabricated key.
|
|
1146
|
+
const result = await run([{ type: "d1", database: "b", sql: "SELECT 1" }], quietSources({ d1: async () => 1 }));
|
|
1147
|
+
expect(result.observed).toEqual({});
|
|
1148
|
+
});
|
|
1149
|
+
it("reports no position for a check that threw", async () => {
|
|
1150
|
+
// A check that errored knows nothing about its source. Recording a position
|
|
1151
|
+
// for it would let the dispatcher advance past work the gate never saw.
|
|
1152
|
+
const result = await run([{ type: "telegram", scope: "inbound", botId: "555" }], quietSources({
|
|
1153
|
+
telegramInbound: async () => {
|
|
1154
|
+
throw new Error("fetch failed");
|
|
1155
|
+
},
|
|
1156
|
+
}));
|
|
1157
|
+
expect(result.hasWork).toBe(true);
|
|
1158
|
+
expect(result.observed).toEqual({});
|
|
1176
1159
|
});
|
|
1177
1160
|
});
|
|
1178
1161
|
//# sourceMappingURL=scheduling-gate.test.js.map
|