@rubytech/create-realagent-code 0.1.590 → 0.1.591
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/plugins/admin/skills/whats-new/SKILL.md +9 -0
- package/payload/platform/plugins/outlook/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/outlook/PLUGIN.md +6 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-gate-probe.test.d.ts +2 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-gate-probe.test.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-gate-probe.test.js +75 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-gate-probe.test.js.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/index.js +14 -0
- package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-gate-probe.d.ts +33 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-gate-probe.d.ts.map +1 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-gate-probe.js +32 -0
- package/payload/platform/plugins/outlook/mcp/dist/tools/mail-gate-probe.js.map +1 -0
- package/payload/platform/plugins/scheduling/PLUGIN.md +8 -2
- package/payload/platform/plugins/scheduling/mcp/dist/index.js +2 -2
- package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-write.test.js +318 -1
- 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 +452 -21
- 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 +61 -0
- 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 +141 -1
- 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 +4 -1
- 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 +215 -15
- 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 +353 -29
- package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.js.map +1 -1
- package/payload/platform/scripts/check-specialist-tool-surface.mjs +15 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +1 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/server/public/activity.html +6 -6
- package/payload/server/public/agents.html +5 -5
- package/payload/server/public/assets/{AdminLoginScreens-CemYu7ho.js → AdminLoginScreens-dBZJlMbp.js} +1 -1
- package/payload/server/public/assets/AdminLoginScreens-dBZJlMbp.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-dBZJlMbp.js.gz +0 -0
- package/payload/server/public/assets/{AdminShell-B5vEeSLw.js → AdminShell-D94sgtsb.js} +1 -1
- package/payload/server/public/assets/AdminShell-D94sgtsb.js.br +0 -0
- package/payload/server/public/assets/AdminShell-D94sgtsb.js.gz +0 -0
- package/payload/server/public/assets/{activity-BHSuZrqG.js → activity-D6NEUvT7.js} +1 -1
- package/payload/server/public/assets/activity-D6NEUvT7.js.br +0 -0
- package/payload/server/public/assets/activity-D6NEUvT7.js.gz +0 -0
- package/payload/server/public/assets/{admin-DnvauP59.js → admin-BluqXSKn.js} +1 -1
- package/payload/server/public/assets/admin-BluqXSKn.js.br +0 -0
- package/payload/server/public/assets/admin-BluqXSKn.js.gz +0 -0
- package/payload/server/public/assets/{agents-CwTJjfyV.js → agents-D_6V66dr.js} +1 -1
- package/payload/server/public/assets/agents-D_6V66dr.js.br +0 -0
- package/payload/server/public/assets/agents-D_6V66dr.js.gz +0 -0
- package/payload/server/public/assets/{browser-D4yjnKEH.js → browser-Bz9s6ztS.js} +1 -1
- package/payload/server/public/assets/browser-Bz9s6ztS.js.br +0 -0
- package/payload/server/public/assets/browser-Bz9s6ztS.js.gz +0 -0
- package/payload/server/public/assets/{calendar-DUi1XFyY.js → calendar-D5OCrmwA.js} +1 -1
- package/payload/server/public/assets/calendar-D5OCrmwA.js.br +0 -0
- package/payload/server/public/assets/calendar-D5OCrmwA.js.gz +0 -0
- package/payload/server/public/assets/chat-CjlbEzZ-.js +1 -0
- package/payload/server/public/assets/chat-CjlbEzZ-.js.br +1 -0
- package/payload/server/public/assets/chat-CjlbEzZ-.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-b_r2jNvg.js +1 -0
- package/payload/server/public/assets/chevron-left-b_r2jNvg.js.br +0 -0
- package/payload/server/public/assets/clock-D8tMbEZD.js +1 -0
- package/payload/server/public/assets/clock-D8tMbEZD.js.br +0 -0
- package/payload/server/public/assets/clock-D8tMbEZD.js.gz +0 -0
- package/payload/server/public/assets/{copy-Bw2h4ofb.js → copy-CXBu2lHL.js} +1 -1
- package/payload/server/public/assets/copy-CXBu2lHL.js.br +0 -0
- package/payload/server/public/assets/copy-CXBu2lHL.js.gz +0 -0
- package/payload/server/public/assets/data-ChHg_Lq9.js +1 -0
- package/payload/server/public/assets/data-ChHg_Lq9.js.br +0 -0
- package/payload/server/public/assets/data-ChHg_Lq9.js.gz +0 -0
- package/payload/server/public/assets/{field-BS-mu5Jx.js → field-D4fXu_SP.js} +1 -1
- package/payload/server/public/assets/field-D4fXu_SP.js.br +0 -0
- package/payload/server/public/assets/field-D4fXu_SP.js.gz +0 -0
- package/payload/server/public/assets/{file-text-xUlDn7ui.js → file-text-BxZ1UMcQ.js} +1 -1
- package/payload/server/public/assets/file-text-BxZ1UMcQ.js.br +0 -0
- package/payload/server/public/assets/file-text-BxZ1UMcQ.js.gz +0 -0
- package/payload/server/public/assets/{graph-DEEXiQLx.js → graph-DMleOCBZ.js} +1 -1
- package/payload/server/public/assets/graph-DMleOCBZ.js.br +0 -0
- package/payload/server/public/assets/graph-DMleOCBZ.js.gz +0 -0
- package/payload/server/public/assets/{graph-labels-F6hQJKCn.js → graph-labels-BHapdrTm.js} +1 -1
- package/payload/server/public/assets/graph-labels-BHapdrTm.js.br +0 -0
- package/payload/server/public/assets/graph-labels-BHapdrTm.js.gz +0 -0
- package/payload/server/public/assets/{jsx-runtime-DDrveXI8.css → jsx-runtime-gqFnq1MC.css} +1 -1
- package/payload/server/public/assets/jsx-runtime-gqFnq1MC.css.br +0 -0
- package/payload/server/public/assets/{jsx-runtime-DDrveXI8.css.gz → jsx-runtime-gqFnq1MC.css.gz} +0 -0
- package/payload/server/public/assets/{operator-CkWK9uRB.js → operator-BpZTRiga.js} +1 -1
- package/payload/server/public/assets/operator-BpZTRiga.js.br +0 -0
- package/payload/server/public/assets/operator-BpZTRiga.js.gz +0 -0
- package/payload/server/public/assets/{page-Dh6fCuUi.js → page-CsHWZZ0U.js} +1 -1
- package/payload/server/public/assets/page-CsHWZZ0U.js.br +0 -0
- package/payload/server/public/assets/page-CsHWZZ0U.js.gz +0 -0
- package/payload/server/public/assets/{page-DbWIOK60.js → page-Dczx6CPP.js} +1 -1
- package/payload/server/public/assets/page-Dczx6CPP.js.br +0 -0
- package/payload/server/public/assets/page-Dczx6CPP.js.gz +0 -0
- package/payload/server/public/assets/play-D_xdb6gy.js +1 -0
- package/payload/server/public/assets/play-D_xdb6gy.js.br +0 -0
- package/payload/server/public/assets/play-D_xdb6gy.js.gz +0 -0
- package/payload/server/public/assets/{public-DmD6vvFM.js → public-BzpELWaZ.js} +1 -1
- package/payload/server/public/assets/public-BzpELWaZ.js.br +0 -0
- package/payload/server/public/assets/public-BzpELWaZ.js.gz +0 -0
- package/payload/server/public/assets/{rotate-ccw-BDdOWTDl.js → rotate-ccw-Bkpst1na.js} +1 -1
- package/payload/server/public/assets/rotate-ccw-Bkpst1na.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-Bkpst1na.js.gz +0 -0
- package/payload/server/public/assets/{routines-CZoKUK0p.js → routines-Cmuf8pPP.js} +2 -2
- package/payload/server/public/assets/routines-Cmuf8pPP.js.br +0 -0
- package/payload/server/public/assets/routines-Cmuf8pPP.js.gz +0 -0
- package/payload/server/public/assets/{skills-D0yS0qLX.js → skills-Bfq3Pbw-.js} +1 -1
- package/payload/server/public/assets/skills-Bfq3Pbw-.js.br +0 -0
- package/payload/server/public/assets/skills-Bfq3Pbw-.js.gz +0 -0
- package/payload/server/public/assets/{tasks-Dtn1uDRY.js → tasks-nyWLrolq.js} +1 -1
- package/payload/server/public/assets/tasks-nyWLrolq.js.br +0 -0
- package/payload/server/public/assets/tasks-nyWLrolq.js.gz +0 -0
- package/payload/server/public/assets/{triangle-alert-DN-Eswuf.js → triangle-alert-DkENIQJ6.js} +1 -1
- package/payload/server/public/assets/triangle-alert-DkENIQJ6.js.br +0 -0
- package/payload/server/public/assets/triangle-alert-DkENIQJ6.js.gz +0 -0
- package/payload/server/public/assets/{useCopyFeedback-BmOZr_Mw.js → useCopyFeedback-DmNjbXe9.js} +1 -1
- package/payload/server/public/assets/useCopyFeedback-DmNjbXe9.js.br +1 -0
- package/payload/server/public/assets/useCopyFeedback-DmNjbXe9.js.gz +0 -0
- package/payload/server/public/assets/{useMediaQuery-fFM7rxUD.js → useMediaQuery-DL-C1y_F.js} +1 -1
- package/payload/server/public/assets/useMediaQuery-DL-C1y_F.js.br +0 -0
- package/payload/server/public/assets/useMediaQuery-DL-C1y_F.js.gz +0 -0
- package/payload/server/public/assets/{useVoiceRecorder-D0MWFIde.js → useVoiceRecorder-DseLVULD.js} +1 -1
- package/payload/server/public/assets/useVoiceRecorder-DseLVULD.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-DseLVULD.js.gz +0 -0
- package/payload/server/public/assets/{wrench-C-yn7q1Q.js → wrench-C7tdp6z9.js} +1 -1
- package/payload/server/public/assets/wrench-C7tdp6z9.js.br +0 -0
- package/payload/server/public/assets/wrench-C7tdp6z9.js.gz +0 -0
- package/payload/server/public/browser.html +5 -5
- package/payload/server/public/calendar.html +8 -8
- package/payload/server/public/chat.html +14 -14
- package/payload/server/public/data.html +12 -12
- package/payload/server/public/field.html +3 -3
- package/payload/server/public/graph.html +10 -10
- package/payload/server/public/index.html +14 -14
- package/payload/server/public/operator.html +16 -16
- package/payload/server/public/public.html +14 -14
- package/payload/server/public/routines.html +8 -8
- package/payload/server/public/skills.html +6 -6
- package/payload/server/public/tasks.html +7 -7
- package/payload/server/server.js +41 -2
- package/payload/server/public/assets/AdminLoginScreens-CemYu7ho.js.br +0 -0
- package/payload/server/public/assets/AdminLoginScreens-CemYu7ho.js.gz +0 -0
- package/payload/server/public/assets/AdminShell-B5vEeSLw.js.br +0 -0
- package/payload/server/public/assets/AdminShell-B5vEeSLw.js.gz +0 -0
- package/payload/server/public/assets/activity-BHSuZrqG.js.br +0 -0
- package/payload/server/public/assets/activity-BHSuZrqG.js.gz +0 -0
- package/payload/server/public/assets/admin-DnvauP59.js.br +0 -0
- package/payload/server/public/assets/admin-DnvauP59.js.gz +0 -0
- package/payload/server/public/assets/agents-CwTJjfyV.js.br +0 -0
- package/payload/server/public/assets/agents-CwTJjfyV.js.gz +0 -0
- package/payload/server/public/assets/browser-D4yjnKEH.js.br +0 -0
- package/payload/server/public/assets/browser-D4yjnKEH.js.gz +0 -0
- package/payload/server/public/assets/calendar-DUi1XFyY.js.br +0 -0
- package/payload/server/public/assets/calendar-DUi1XFyY.js.gz +0 -0
- package/payload/server/public/assets/chat-BJ1p4kuh.js +0 -1
- package/payload/server/public/assets/chat-BJ1p4kuh.js.br +0 -1
- package/payload/server/public/assets/chat-BJ1p4kuh.js.gz +0 -0
- package/payload/server/public/assets/chevron-left-Ctw-3JsV.js +0 -1
- package/payload/server/public/assets/chevron-left-Ctw-3JsV.js.br +0 -0
- package/payload/server/public/assets/clock-BLOp42yY.js +0 -1
- package/payload/server/public/assets/clock-BLOp42yY.js.br +0 -0
- package/payload/server/public/assets/clock-BLOp42yY.js.gz +0 -0
- package/payload/server/public/assets/copy-Bw2h4ofb.js.br +0 -0
- package/payload/server/public/assets/copy-Bw2h4ofb.js.gz +0 -0
- package/payload/server/public/assets/data-uR7BZKVU.js +0 -1
- package/payload/server/public/assets/data-uR7BZKVU.js.br +0 -0
- package/payload/server/public/assets/data-uR7BZKVU.js.gz +0 -0
- package/payload/server/public/assets/field-BS-mu5Jx.js.br +0 -0
- package/payload/server/public/assets/field-BS-mu5Jx.js.gz +0 -0
- package/payload/server/public/assets/file-text-xUlDn7ui.js.br +0 -0
- package/payload/server/public/assets/file-text-xUlDn7ui.js.gz +0 -0
- package/payload/server/public/assets/graph-DEEXiQLx.js.br +0 -0
- package/payload/server/public/assets/graph-DEEXiQLx.js.gz +0 -0
- package/payload/server/public/assets/graph-labels-F6hQJKCn.js.br +0 -0
- package/payload/server/public/assets/graph-labels-F6hQJKCn.js.gz +0 -0
- package/payload/server/public/assets/jsx-runtime-DDrveXI8.css.br +0 -0
- package/payload/server/public/assets/operator-CkWK9uRB.js.br +0 -0
- package/payload/server/public/assets/operator-CkWK9uRB.js.gz +0 -0
- package/payload/server/public/assets/page-DbWIOK60.js.br +0 -0
- package/payload/server/public/assets/page-DbWIOK60.js.gz +0 -0
- package/payload/server/public/assets/page-Dh6fCuUi.js.br +0 -0
- package/payload/server/public/assets/page-Dh6fCuUi.js.gz +0 -0
- package/payload/server/public/assets/play-fy3tdi7M.js +0 -1
- package/payload/server/public/assets/play-fy3tdi7M.js.br +0 -0
- package/payload/server/public/assets/play-fy3tdi7M.js.gz +0 -0
- package/payload/server/public/assets/public-DmD6vvFM.js.br +0 -0
- package/payload/server/public/assets/public-DmD6vvFM.js.gz +0 -0
- package/payload/server/public/assets/rotate-ccw-BDdOWTDl.js.br +0 -0
- package/payload/server/public/assets/rotate-ccw-BDdOWTDl.js.gz +0 -0
- package/payload/server/public/assets/routines-CZoKUK0p.js.br +0 -0
- package/payload/server/public/assets/routines-CZoKUK0p.js.gz +0 -0
- package/payload/server/public/assets/skills-D0yS0qLX.js.br +0 -0
- package/payload/server/public/assets/skills-D0yS0qLX.js.gz +0 -0
- package/payload/server/public/assets/tasks-Dtn1uDRY.js.br +0 -0
- package/payload/server/public/assets/tasks-Dtn1uDRY.js.gz +0 -0
- package/payload/server/public/assets/triangle-alert-DN-Eswuf.js.br +0 -0
- package/payload/server/public/assets/triangle-alert-DN-Eswuf.js.gz +0 -0
- package/payload/server/public/assets/useCopyFeedback-BmOZr_Mw.js.br +0 -0
- package/payload/server/public/assets/useCopyFeedback-BmOZr_Mw.js.gz +0 -0
- package/payload/server/public/assets/useMediaQuery-fFM7rxUD.js.br +0 -0
- package/payload/server/public/assets/useMediaQuery-fFM7rxUD.js.gz +0 -0
- package/payload/server/public/assets/useVoiceRecorder-D0MWFIde.js.br +0 -0
- package/payload/server/public/assets/useVoiceRecorder-D0MWFIde.js.gz +0 -0
- package/payload/server/public/assets/wrench-C-yn7q1Q.js.br +0 -0
- package/payload/server/public/assets/wrench-C-yn7q1Q.js.gz +0 -0
- /package/payload/server/public/assets/{jsx-runtime-DXWyHqht.js → jsx-runtime-BcrVQeV-.js} +0 -0
- /package/payload/server/public/assets/{jsx-runtime-DXWyHqht.js.br → jsx-runtime-BcrVQeV-.js.br} +0 -0
- /package/payload/server/public/assets/{jsx-runtime-DXWyHqht.js.gz → jsx-runtime-BcrVQeV-.js.gz} +0 -0
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, watermarkPath, readWatermarks, foldMailWatermarks, parseD1Count, digestBusy, } from "../scheduling-gate.js";
|
|
20
|
+
import { runSchedulingGate, watermarkPath, observationPath, 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;
|
|
@@ -32,6 +32,8 @@ function quietSources(over = {}) {
|
|
|
32
32
|
// Task 2653 — "quiet" for Telegram is an account that holds no inbound row at
|
|
33
33
|
// all, which is what a fresh install answers.
|
|
34
34
|
telegramInbound: async () => ({ newestInboundAt: null }),
|
|
35
|
+
// Task 2657 — "quiet" for the Outlook arm is a folder holding no message.
|
|
36
|
+
mailOutlook: async () => ({ newestReceivedAt: null }),
|
|
35
37
|
...over,
|
|
36
38
|
};
|
|
37
39
|
}
|
|
@@ -60,12 +62,13 @@ const run = (checks, sources, commit = false) => runSchedulingGate({ eventId: EV
|
|
|
60
62
|
describe("d1 check", () => {
|
|
61
63
|
const CHECK = { type: "d1", database: "bookings", sql: "SELECT count(*) FROM b" };
|
|
62
64
|
it("is false on a zero count", async () => {
|
|
63
|
-
expect(await run([CHECK], quietSources())).toEqual({ hasWork: false, reasons: [] });
|
|
65
|
+
expect(await run([CHECK], quietSources())).toEqual({ hasWork: false, reasons: [], errored: [] });
|
|
64
66
|
});
|
|
65
67
|
it("is true on one row", async () => {
|
|
66
68
|
expect(await run([CHECK], quietSources({ d1: async () => 1 }))).toEqual({
|
|
67
69
|
hasWork: true,
|
|
68
70
|
reasons: ["d1"],
|
|
71
|
+
errored: [],
|
|
69
72
|
});
|
|
70
73
|
});
|
|
71
74
|
it("reports work rather than an error when the source is unreachable", async () => {
|
|
@@ -74,21 +77,25 @@ describe("d1 check", () => {
|
|
|
74
77
|
throw new Error("no cloudflare credentials");
|
|
75
78
|
},
|
|
76
79
|
});
|
|
77
|
-
expect(await run([CHECK], sources)).toEqual({
|
|
80
|
+
expect(await run([CHECK], sources)).toEqual({
|
|
81
|
+
hasWork: true,
|
|
82
|
+
reasons: ["d1"],
|
|
83
|
+
errored: [{ kind: "d1", detail: "no cloudflare credentials" }],
|
|
84
|
+
});
|
|
78
85
|
});
|
|
79
86
|
});
|
|
80
87
|
describe("mail check", () => {
|
|
81
88
|
const CHECK = { type: "mail", mailbox: "ops@example.com" };
|
|
82
89
|
it("is false when nothing sits above the watermark", async () => {
|
|
83
90
|
seedWatermarks({ mail: { "ops@example.com|INBOX": { uid: 100, uidValidity: 1 } } });
|
|
84
|
-
expect(await run([CHECK], quietSources())).toEqual({ hasWork: false, reasons: [] });
|
|
91
|
+
expect(await run([CHECK], quietSources())).toEqual({ hasWork: false, reasons: [], errored: [] });
|
|
85
92
|
});
|
|
86
93
|
it("is true when a UID above the watermark matches", async () => {
|
|
87
94
|
seedWatermarks({ mail: { "ops@example.com|INBOX": { uid: 100, uidValidity: 1 } } });
|
|
88
95
|
const sources = quietSources({
|
|
89
96
|
mail: async () => ({ matched: true, highestUid: 104, uidValidity: 1 }),
|
|
90
97
|
});
|
|
91
|
-
expect(await run([CHECK], sources)).toEqual({ hasWork: true, reasons: ["mail"] });
|
|
98
|
+
expect(await run([CHECK], sources)).toEqual({ hasWork: true, reasons: ["mail"], errored: [] });
|
|
92
99
|
});
|
|
93
100
|
it("probes from UID 0 on a first run, so nothing already there is missed", async () => {
|
|
94
101
|
const seen = [];
|
|
@@ -106,7 +113,7 @@ describe("mail check", () => {
|
|
|
106
113
|
const sources = quietSources({
|
|
107
114
|
mail: async () => ({ matched: false, highestUid: 100, uidValidity: 7 }),
|
|
108
115
|
});
|
|
109
|
-
expect(await run([CHECK], sources)).toEqual({ hasWork: true, reasons: ["mail"] });
|
|
116
|
+
expect(await run([CHECK], sources)).toEqual({ hasWork: true, reasons: ["mail"], errored: [] });
|
|
110
117
|
});
|
|
111
118
|
it("keys the watermark by folder, so two folders of one mailbox never collide", async () => {
|
|
112
119
|
seedWatermarks({ mail: { "ops@example.com|Archive": { uid: 100, uidValidity: 1 } } });
|
|
@@ -124,12 +131,12 @@ describe("mail check", () => {
|
|
|
124
131
|
describe("whatsapp check", () => {
|
|
125
132
|
it("status is false while connected", async () => {
|
|
126
133
|
const check = { type: "whatsapp", scope: "status" };
|
|
127
|
-
expect(await run([check], quietSources())).toEqual({ hasWork: false, reasons: [] });
|
|
134
|
+
expect(await run([check], quietSources())).toEqual({ hasWork: false, reasons: [], errored: [] });
|
|
128
135
|
});
|
|
129
136
|
it("status is true while disconnected", async () => {
|
|
130
137
|
const check = { type: "whatsapp", scope: "status" };
|
|
131
138
|
const sources = quietSources({ whatsappStatus: async () => ({ connected: false }) });
|
|
132
|
-
expect(await run([check], sources)).toEqual({ hasWork: true, reasons: ["whatsapp"] });
|
|
139
|
+
expect(await run([check], sources)).toEqual({ hasWork: true, reasons: ["whatsapp"], errored: [] });
|
|
133
140
|
});
|
|
134
141
|
it("inbound is true only on a message received since the watermark", async () => {
|
|
135
142
|
const check = { type: "whatsapp", scope: "inbound" };
|
|
@@ -137,7 +144,7 @@ describe("whatsapp check", () => {
|
|
|
137
144
|
const sources = quietSources({
|
|
138
145
|
whatsappInbound: async () => ({ latest: 1_754_000_900, newer: true }),
|
|
139
146
|
});
|
|
140
|
-
expect(await run([check], sources)).toEqual({ hasWork: true, reasons: ["whatsapp"] });
|
|
147
|
+
expect(await run([check], sources)).toEqual({ hasWork: true, reasons: ["whatsapp"], errored: [] });
|
|
141
148
|
});
|
|
142
149
|
it("inbound passes the stored watermark to the source", async () => {
|
|
143
150
|
const check = { type: "whatsapp", scope: "inbound" };
|
|
@@ -157,15 +164,15 @@ describe("calendar check", () => {
|
|
|
157
164
|
const CHECK = { type: "calendar", connector: "google" };
|
|
158
165
|
it("is false on an unchanged digest", async () => {
|
|
159
166
|
seedWatermarks({ calendar: { digest: "DIGEST-A" } });
|
|
160
|
-
expect(await run([CHECK], quietSources())).toEqual({ hasWork: false, reasons: [] });
|
|
167
|
+
expect(await run([CHECK], quietSources())).toEqual({ hasWork: false, reasons: [], errored: [] });
|
|
161
168
|
});
|
|
162
169
|
it("is true on a changed digest", async () => {
|
|
163
170
|
seedWatermarks({ calendar: { digest: "DIGEST-A" } });
|
|
164
171
|
const sources = quietSources({ calendarDigest: async () => "DIGEST-B" });
|
|
165
|
-
expect(await run([CHECK], sources)).toEqual({ hasWork: true, reasons: ["calendar"] });
|
|
172
|
+
expect(await run([CHECK], sources)).toEqual({ hasWork: true, reasons: ["calendar"], errored: [] });
|
|
166
173
|
});
|
|
167
174
|
it("is true on a first run, when nothing has been committed yet", async () => {
|
|
168
|
-
expect(await run([CHECK], quietSources())).toEqual({ hasWork: true, reasons: ["calendar"] });
|
|
175
|
+
expect(await run([CHECK], quietSources())).toEqual({ hasWork: true, reasons: ["calendar"], errored: [] });
|
|
169
176
|
});
|
|
170
177
|
});
|
|
171
178
|
describe("digestBusy", () => {
|
|
@@ -222,7 +229,7 @@ describe("all four together", () => {
|
|
|
222
229
|
mail: { "ops@example.com|INBOX": { uid: 100, uidValidity: 1 } },
|
|
223
230
|
calendar: { digest: "DIGEST-A" },
|
|
224
231
|
});
|
|
225
|
-
expect(await run(ALL, quietSources())).toEqual({ hasWork: false, reasons: [] });
|
|
232
|
+
expect(await run(ALL, quietSources())).toEqual({ hasWork: false, reasons: [], errored: [] });
|
|
226
233
|
});
|
|
227
234
|
it("one noisy source is enough, and names only itself", async () => {
|
|
228
235
|
seedWatermarks({
|
|
@@ -232,6 +239,7 @@ describe("all four together", () => {
|
|
|
232
239
|
expect(await run(ALL, quietSources({ d1: async () => 2 }))).toEqual({
|
|
233
240
|
hasWork: true,
|
|
234
241
|
reasons: ["d1"],
|
|
242
|
+
errored: [],
|
|
235
243
|
});
|
|
236
244
|
});
|
|
237
245
|
});
|
|
@@ -259,6 +267,9 @@ describe("commit", () => {
|
|
|
259
267
|
whatsappInbound: async () => ({ latest: 1_754_009_999, newer: true }),
|
|
260
268
|
calendarDigest: async () => "DIGEST-Z",
|
|
261
269
|
});
|
|
270
|
+
// Task 2654 — the commit promotes what the gate read, so the gate call is
|
|
271
|
+
// part of the sequence. A bare commit advances nothing, by design.
|
|
272
|
+
await run(ALL, sources);
|
|
262
273
|
const result = await run(ALL, sources, true);
|
|
263
274
|
const marks = JSON.parse(readFileSync(watermarkPath(EVENT, platformRoot), "utf-8"));
|
|
264
275
|
expect(marks.mail["ops@example.com|inbox"]).toEqual({ uid: 250, uidValidity: 9 });
|
|
@@ -279,9 +290,9 @@ describe("commit", () => {
|
|
|
279
290
|
it("a committed watermark makes the next plain call quiet", async () => {
|
|
280
291
|
const sources = quietSources({ calendarDigest: async () => "DIGEST-Z" });
|
|
281
292
|
const checks = [{ type: "calendar", connector: "google" }];
|
|
282
|
-
expect(await run(checks, sources)).toEqual({ hasWork: true, reasons: ["calendar"] });
|
|
293
|
+
expect(await run(checks, sources)).toEqual({ hasWork: true, reasons: ["calendar"], errored: [] });
|
|
283
294
|
await run(checks, sources, true);
|
|
284
|
-
expect(await run(checks, sources)).toEqual({ hasWork: false, reasons: [] });
|
|
295
|
+
expect(await run(checks, sources)).toEqual({ hasWork: false, reasons: [], errored: [] });
|
|
285
296
|
});
|
|
286
297
|
it("a check that errors is not committed, so its work is not marked consumed", async () => {
|
|
287
298
|
const checks = [{ type: "calendar", connector: "google" }];
|
|
@@ -303,6 +314,8 @@ describe("commit", () => {
|
|
|
303
314
|
{ type: "mail", mailbox: "ops@example.com" },
|
|
304
315
|
{ type: "d1", database: "bookings", sql: "SELECT count(*) FROM b" },
|
|
305
316
|
];
|
|
317
|
+
// Task 2654 — the gate call records the position the commit promotes.
|
|
318
|
+
await run(checks, quietSources());
|
|
306
319
|
const result = await run(checks, quietSources(), true);
|
|
307
320
|
// Task 2635 folds the folder segment to lower case on write.
|
|
308
321
|
expect(result.advanced).toEqual(["ops@example.com|inbox"]);
|
|
@@ -321,7 +334,7 @@ describe("commit", () => {
|
|
|
321
334
|
const checks = [
|
|
322
335
|
{ type: "d1", database: "bookings", sql: "SELECT count(*) FROM b" },
|
|
323
336
|
];
|
|
324
|
-
expect(await run(checks, quietSources())).toEqual({ hasWork: false, reasons: [] });
|
|
337
|
+
expect(await run(checks, quietSources())).toEqual({ hasWork: false, reasons: [], errored: [] });
|
|
325
338
|
});
|
|
326
339
|
it("a check that errored is neither advanced nor skipped, because its state is unknown", async () => {
|
|
327
340
|
const checks = [
|
|
@@ -586,8 +599,12 @@ describe("watermark folder-case fold", () => {
|
|
|
586
599
|
const sources = quietSources({
|
|
587
600
|
mail: async (_c, since) => ({ matched: false, highestUid: since + 1, uidValidity: 1 }),
|
|
588
601
|
});
|
|
602
|
+
// Task 2654 — each commit promotes what its own gate call read, so both
|
|
603
|
+
// fires run the real two-step sequence.
|
|
604
|
+
await run([lower], sources);
|
|
589
605
|
const first = await run([lower], sources, true);
|
|
590
606
|
expect(first.advanced).toEqual(["ops@example.com|inbox"]);
|
|
607
|
+
await run([upper], sources);
|
|
591
608
|
const second = await run([upper], sources, true);
|
|
592
609
|
expect(second.advanced).toEqual(["ops@example.com|inbox"]);
|
|
593
610
|
const marks = JSON.parse(readFileSync(watermarkPath(EVENT, platformRoot), "utf-8"));
|
|
@@ -647,13 +664,14 @@ describe("telegram check", () => {
|
|
|
647
664
|
};
|
|
648
665
|
const measured = (lines) => lines.every((l) => !l.includes("op=check-error"));
|
|
649
666
|
it("is false when the account holds no inbound message at all", async () => {
|
|
650
|
-
expect(await run([CHECK], quietSources())).toEqual({ hasWork: false, reasons: [] });
|
|
667
|
+
expect(await run([CHECK], quietSources())).toEqual({ hasWork: false, reasons: [], errored: [] });
|
|
651
668
|
});
|
|
652
669
|
it("is false when nothing arrived after the watermark", async () => {
|
|
653
670
|
seedWatermarks({ telegram: { inbound: "2026-08-05T10:00:00.000Z" } });
|
|
654
671
|
expect(await run([CHECK], quietSources(at("2026-08-05T09:00:00.000Z")))).toEqual({
|
|
655
672
|
hasWork: false,
|
|
656
673
|
reasons: [],
|
|
674
|
+
errored: [],
|
|
657
675
|
});
|
|
658
676
|
});
|
|
659
677
|
it("is true when a message arrived after the watermark", async () => {
|
|
@@ -662,6 +680,7 @@ describe("telegram check", () => {
|
|
|
662
680
|
expect(await run([CHECK], quietSources(at("2026-08-05T11:00:00.000Z")))).toEqual({
|
|
663
681
|
hasWork: true,
|
|
664
682
|
reasons: ["telegram"],
|
|
683
|
+
errored: [],
|
|
665
684
|
});
|
|
666
685
|
expect(measured(lines)).toBe(true);
|
|
667
686
|
});
|
|
@@ -672,6 +691,7 @@ describe("telegram check", () => {
|
|
|
672
691
|
expect(await run([CHECK], quietSources(at("2026-08-05T10:00:00.000Z")))).toEqual({
|
|
673
692
|
hasWork: false,
|
|
674
693
|
reasons: [],
|
|
694
|
+
errored: [],
|
|
675
695
|
});
|
|
676
696
|
});
|
|
677
697
|
it("is true on a first run, so nothing already waiting is missed", async () => {
|
|
@@ -679,6 +699,7 @@ describe("telegram check", () => {
|
|
|
679
699
|
expect(await run([CHECK], quietSources(at("2026-08-05T09:00:00.000Z")))).toEqual({
|
|
680
700
|
hasWork: true,
|
|
681
701
|
reasons: ["telegram"],
|
|
702
|
+
errored: [],
|
|
682
703
|
});
|
|
683
704
|
expect(measured(lines)).toBe(true);
|
|
684
705
|
});
|
|
@@ -689,7 +710,11 @@ describe("telegram check", () => {
|
|
|
689
710
|
throw new Error("HTTP 500 on GET /api/telegram-reader/gate-probe");
|
|
690
711
|
},
|
|
691
712
|
});
|
|
692
|
-
expect(await run([CHECK], sources)).toEqual({
|
|
713
|
+
expect(await run([CHECK], sources)).toEqual({
|
|
714
|
+
hasWork: true,
|
|
715
|
+
reasons: ["telegram"],
|
|
716
|
+
errored: [{ kind: "telegram", detail: "HTTP 500 on GET /api/telegram-reader/gate-probe" }],
|
|
717
|
+
});
|
|
693
718
|
// The opposite of the two assertions above: this one MUST name the failure,
|
|
694
719
|
// or a permanently broken probe reads as a permanently busy account. The
|
|
695
720
|
// DETAIL is asserted, not just the kind — an unrecognised check type also
|
|
@@ -699,12 +724,110 @@ describe("telegram check", () => {
|
|
|
699
724
|
expect(line).toContain("kind=telegram");
|
|
700
725
|
expect(line).toContain("/api/telegram-reader/gate-probe");
|
|
701
726
|
});
|
|
702
|
-
it("a commit advances the watermark to what the probe
|
|
727
|
+
it("a commit advances the watermark to what the gate's probe read", async () => {
|
|
728
|
+
// Task 2654 — the gate's probe, not the commit's. The commit runs at the end
|
|
729
|
+
// of the turn, so its own reading already includes anything that arrived
|
|
730
|
+
// while the turn ran.
|
|
731
|
+
await run([CHECK], quietSources(at("2026-08-05T11:00:00.000Z")));
|
|
703
732
|
const result = await run([CHECK], quietSources(at("2026-08-05T11:00:00.000Z")), true);
|
|
704
733
|
expect(result.advanced).toEqual(["telegram"]);
|
|
705
734
|
const marks = readWatermarks(EVENT, platformRoot);
|
|
706
735
|
expect(marks.telegram).toEqual({ inbound: "2026-08-05T11:00:00.000Z" });
|
|
707
736
|
});
|
|
737
|
+
// Task 2662 — the bot scope. Before it the check named no bot and the probe
|
|
738
|
+
// read every conversation on the account, so a routine watching a booking bot
|
|
739
|
+
// woke for every message to a relay bot sharing that account. A `botId` on the
|
|
740
|
+
// check was accepted by the schema and reached nothing that read it.
|
|
741
|
+
it("forwards botId to the source, so the probe can scope to one bot", async () => {
|
|
742
|
+
const seen = [];
|
|
743
|
+
const sources = quietSources({
|
|
744
|
+
telegramInbound: async (_a, botId) => {
|
|
745
|
+
seen.push(botId);
|
|
746
|
+
return { newestInboundAt: null };
|
|
747
|
+
},
|
|
748
|
+
});
|
|
749
|
+
await run([{ type: "telegram", scope: "inbound", botId: "111" }], sources);
|
|
750
|
+
expect(seen).toEqual(["111"]);
|
|
751
|
+
});
|
|
752
|
+
it("calls the source with no botId when the check names none", async () => {
|
|
753
|
+
// The account-wide shape 2653 shipped. A gate bound before 2660 carries no
|
|
754
|
+
// botId and must keep answering for the whole account.
|
|
755
|
+
const seen = [];
|
|
756
|
+
const sources = quietSources({
|
|
757
|
+
telegramInbound: async (_a, botId) => {
|
|
758
|
+
seen.push(botId);
|
|
759
|
+
return { newestInboundAt: null };
|
|
760
|
+
},
|
|
761
|
+
});
|
|
762
|
+
await run([CHECK], sources);
|
|
763
|
+
expect(seen).toEqual([undefined]);
|
|
764
|
+
});
|
|
765
|
+
it("keeps each bot scope on its own watermark, so narrowing a gate cannot skip work", async () => {
|
|
766
|
+
// The mail check keys `<mailbox>|<folder>` for this reason (Task 2635): two
|
|
767
|
+
// scopes sharing one position means the narrower one inherits a mark taken
|
|
768
|
+
// for a wider set. Here the account-wide mark is LATER than bot 111's own
|
|
769
|
+
// newest, so a shared key would answer "no work" while 111 holds a message
|
|
770
|
+
// nobody has read. That is the mute direction, and it loses a booking.
|
|
771
|
+
seedWatermarks({ telegram: { inbound: "2026-08-05T18:00:00.000Z" } });
|
|
772
|
+
const check = { type: "telegram", scope: "inbound", botId: "111" };
|
|
773
|
+
expect(await run([check], quietSources(at("2026-08-05T12:00:00.000Z")))).toEqual({
|
|
774
|
+
hasWork: true,
|
|
775
|
+
reasons: ["telegram"],
|
|
776
|
+
errored: [],
|
|
777
|
+
});
|
|
778
|
+
});
|
|
779
|
+
it("a scoped commit writes under the bot, leaving the account-wide mark alone", async () => {
|
|
780
|
+
seedWatermarks({ telegram: { inbound: "2026-08-05T18:00:00.000Z" } });
|
|
781
|
+
const check = { type: "telegram", scope: "inbound", botId: "111" };
|
|
782
|
+
const sources = quietSources(at("2026-08-05T12:00:00.000Z"));
|
|
783
|
+
// Task 2654 — the commit promotes what the GATE read, so the gate call is
|
|
784
|
+
// part of the sequence. A bare commit advances nothing, by design.
|
|
785
|
+
await run([check], sources);
|
|
786
|
+
const result = await run([check], sources, true);
|
|
787
|
+
expect(result.advanced).toEqual(["telegram:111"]);
|
|
788
|
+
const marks = readWatermarks(EVENT, platformRoot);
|
|
789
|
+
expect(marks.telegram?.inbound).toBe("2026-08-05T18:00:00.000Z");
|
|
790
|
+
expect(marks.telegram?.byBot).toEqual({ "111": "2026-08-05T12:00:00.000Z" });
|
|
791
|
+
});
|
|
792
|
+
it("two bots on one event hold two positions", async () => {
|
|
793
|
+
seedWatermarks({
|
|
794
|
+
telegram: { byBot: { "111": "2026-08-05T12:00:00.000Z", "222": "2026-08-05T20:00:00.000Z" } },
|
|
795
|
+
});
|
|
796
|
+
const a = { type: "telegram", scope: "inbound", botId: "111" };
|
|
797
|
+
const b = { type: "telegram", scope: "inbound", botId: "222" };
|
|
798
|
+
expect(await run([a], quietSources(at("2026-08-05T13:00:00.000Z")))).toMatchObject({
|
|
799
|
+
hasWork: true,
|
|
800
|
+
});
|
|
801
|
+
expect(await run([b], quietSources(at("2026-08-05T13:00:00.000Z")))).toMatchObject({
|
|
802
|
+
hasWork: false,
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
it("reports work and names the bot when the probe refuses an unknown one", async () => {
|
|
806
|
+
// The operator's own reproduction: id `0000000000`. The probe answers 404,
|
|
807
|
+
// which reaches here as an unreadable check. It must report WORK, never
|
|
808
|
+
// quiet, or the routine is muted for ever with nothing in the log.
|
|
809
|
+
const lines = linesFrom();
|
|
810
|
+
const sources = quietSources({
|
|
811
|
+
telegramInbound: async (_a, botId) => {
|
|
812
|
+
throw new Error(`HTTP 404 on GET /api/telegram-reader/gate-probe?botId=${botId}`);
|
|
813
|
+
},
|
|
814
|
+
});
|
|
815
|
+
const check = { type: "telegram", scope: "inbound", botId: "0000000000" };
|
|
816
|
+
const result = await run([check], sources);
|
|
817
|
+
expect(result).toMatchObject({ hasWork: true, reasons: ["telegram"] });
|
|
818
|
+
// Task 2658's structured arm carries the reason, so a caller can tell a bot
|
|
819
|
+
// with messages from a bot id that resolves to nothing. `reasons` alone
|
|
820
|
+
// cannot: it holds "telegram" either way.
|
|
821
|
+
expect(result.errored).toEqual([
|
|
822
|
+
{
|
|
823
|
+
kind: "telegram",
|
|
824
|
+
detail: "HTTP 404 on GET /api/telegram-reader/gate-probe?botId=0000000000",
|
|
825
|
+
},
|
|
826
|
+
]);
|
|
827
|
+
const line = lines.find((l) => l.includes("op=check-error"));
|
|
828
|
+
expect(line).toContain("kind=telegram");
|
|
829
|
+
expect(line).toContain("0000000000");
|
|
830
|
+
});
|
|
708
831
|
it("a commit on a quiet account is skipped, not advanced onto nothing", async () => {
|
|
709
832
|
// A null probe is "nothing to measure from", not "position zero". There is
|
|
710
833
|
// no position to record, so the check reports as `skipped` exactly as `d1`
|
|
@@ -726,7 +849,16 @@ describe("telegram check", () => {
|
|
|
726
849
|
describe("unrecognised check type", () => {
|
|
727
850
|
const CHECK = { type: "nonsense" };
|
|
728
851
|
it("resolves rather than throwing, and counts the unknown type as work", async () => {
|
|
729
|
-
expect(await run([CHECK], quietSources())).toEqual({
|
|
852
|
+
expect(await run([CHECK], quietSources())).toEqual({
|
|
853
|
+
hasWork: true,
|
|
854
|
+
reasons: ["nonsense"],
|
|
855
|
+
errored: [
|
|
856
|
+
{
|
|
857
|
+
kind: "nonsense",
|
|
858
|
+
detail: 'unknown check type "nonsense" — accepted: mail, d1, whatsapp, calendar, telegram',
|
|
859
|
+
},
|
|
860
|
+
],
|
|
861
|
+
});
|
|
730
862
|
});
|
|
731
863
|
it("names the type and the accepted set in op=check-error", async () => {
|
|
732
864
|
const lines = [];
|
|
@@ -741,7 +873,306 @@ describe("unrecognised check type", () => {
|
|
|
741
873
|
});
|
|
742
874
|
it("still runs the sibling checks — one bad type does not abort the gate", async () => {
|
|
743
875
|
const checks = [CHECK, { type: "d1", database: "b", sql: "SELECT 1" }];
|
|
744
|
-
expect(await run(checks, quietSources())).toEqual({
|
|
876
|
+
expect(await run(checks, quietSources())).toEqual({
|
|
877
|
+
hasWork: true,
|
|
878
|
+
reasons: ["nonsense"],
|
|
879
|
+
errored: [
|
|
880
|
+
{
|
|
881
|
+
kind: "nonsense",
|
|
882
|
+
detail: 'unknown check type "nonsense" — accepted: mail, d1, whatsapp, calendar, telegram',
|
|
883
|
+
},
|
|
884
|
+
],
|
|
885
|
+
});
|
|
886
|
+
});
|
|
887
|
+
});
|
|
888
|
+
// ---------------------------------------------------------------------------
|
|
889
|
+
// Task 2658 — `reasons` carries the check type on the real-work arm and the
|
|
890
|
+
// error arm alike, so `{"hasWork":true,"reasons":["d1"]}` is byte-identical for
|
|
891
|
+
// a database holding rows and one that 403s. `errored` is the only place that
|
|
892
|
+
// difference survives the call, and no bind-time guard is writable without it.
|
|
893
|
+
// ---------------------------------------------------------------------------
|
|
894
|
+
describe("errored", () => {
|
|
895
|
+
const D1 = { type: "d1", database: "db", sql: "SELECT 1" };
|
|
896
|
+
it("is empty when every check answers", async () => {
|
|
897
|
+
const res = await run([D1], quietSources());
|
|
898
|
+
expect(res.errored).toEqual([]);
|
|
899
|
+
});
|
|
900
|
+
it("names the kind and the source's own message when a check throws", async () => {
|
|
901
|
+
const sources = quietSources({
|
|
902
|
+
d1: async () => {
|
|
903
|
+
throw new Error("HTTP 403 on POST /api/storage/d1/query");
|
|
904
|
+
},
|
|
905
|
+
});
|
|
906
|
+
const res = await run([D1], sources);
|
|
907
|
+
expect(res.errored).toEqual([{ kind: "d1", detail: "HTTP 403 on POST /api/storage/d1/query" }]);
|
|
908
|
+
});
|
|
909
|
+
it("is not distinguishable from a real hit by hasWork or reasons alone", async () => {
|
|
910
|
+
const thrown = await run([D1], quietSources({
|
|
911
|
+
d1: async () => {
|
|
912
|
+
throw new Error("HTTP 403 on POST /api/storage/d1/query");
|
|
913
|
+
},
|
|
914
|
+
}));
|
|
915
|
+
const real = await run([D1], quietSources({ d1: async () => 7 }));
|
|
916
|
+
expect(thrown.hasWork).toBe(real.hasWork);
|
|
917
|
+
expect(thrown.reasons).toEqual(real.reasons);
|
|
918
|
+
expect(thrown.errored).toHaveLength(1);
|
|
919
|
+
expect(real.errored).toEqual([]);
|
|
920
|
+
});
|
|
921
|
+
it("carries one entry per erroring check, in checks order", async () => {
|
|
922
|
+
const sources = quietSources({
|
|
923
|
+
d1: async () => {
|
|
924
|
+
throw new Error("d1 down");
|
|
925
|
+
},
|
|
926
|
+
calendarDigest: async () => {
|
|
927
|
+
throw new Error("calendar down");
|
|
928
|
+
},
|
|
929
|
+
});
|
|
930
|
+
const checks = [
|
|
931
|
+
D1,
|
|
932
|
+
{ type: "whatsapp", scope: "status" },
|
|
933
|
+
{ type: "calendar", connector: "google" },
|
|
934
|
+
];
|
|
935
|
+
const res = await run(checks, sources);
|
|
936
|
+
expect(res.errored).toEqual([
|
|
937
|
+
{ kind: "d1", detail: "d1 down" },
|
|
938
|
+
{ kind: "calendar", detail: "calendar down" },
|
|
939
|
+
]);
|
|
940
|
+
});
|
|
941
|
+
it("is present on a commit too", async () => {
|
|
942
|
+
const res = await run([D1], quietSources(), true);
|
|
943
|
+
expect(res.errored).toEqual([]);
|
|
944
|
+
});
|
|
945
|
+
});
|
|
946
|
+
// Task 2657 — the same check, on a mailbox the outlook plugin holds.
|
|
947
|
+
//
|
|
948
|
+
// The first test is the one that matters. Graph's
|
|
949
|
+
// `$top=1&$orderby=receivedDateTime desc` returns the newest message whether or
|
|
950
|
+
// not anything arrived, exactly as `UID SEARCH n+1:*` returns the last message
|
|
951
|
+
// above the highest UID — so an implementation that reports work on any
|
|
952
|
+
// non-empty response passes every other test here, which is how Task 2471
|
|
953
|
+
// shipped.
|
|
954
|
+
describe("mail check — outlook provider", () => {
|
|
955
|
+
const CHECK = { type: "mail", mailbox: "admin@muvin.co.uk", provider: "outlook" };
|
|
956
|
+
const KEY = "admin@muvin.co.uk|inbox";
|
|
957
|
+
it("is false when the newest message equals the watermark", async () => {
|
|
958
|
+
seedWatermarks({ mailOutlook: { [KEY]: { receivedAt: "2026-08-10T09:00:00Z" } } });
|
|
959
|
+
const sources = quietSources({
|
|
960
|
+
mailOutlook: async () => ({ newestReceivedAt: "2026-08-10T09:00:00Z" }),
|
|
961
|
+
});
|
|
962
|
+
expect(await run([CHECK], sources)).toEqual({ hasWork: false, reasons: [], errored: [] });
|
|
963
|
+
});
|
|
964
|
+
it("is true on a message above the watermark, and advances that key on commit", async () => {
|
|
965
|
+
seedWatermarks({ mailOutlook: { [KEY]: { receivedAt: "2026-08-10T09:00:00Z" } } });
|
|
966
|
+
const sources = quietSources({
|
|
967
|
+
mailOutlook: async () => ({ newestReceivedAt: "2026-08-10T09:05:00Z" }),
|
|
968
|
+
});
|
|
969
|
+
// Task 2654 — the commit promotes what the gate read, so the gate call is
|
|
970
|
+
// part of the sequence. A bare commit advances nothing, by design.
|
|
971
|
+
await run([CHECK], sources);
|
|
972
|
+
expect(await run([CHECK], sources, true)).toEqual({
|
|
973
|
+
hasWork: true,
|
|
974
|
+
reasons: ["mail"],
|
|
975
|
+
errored: [],
|
|
976
|
+
advanced: [KEY],
|
|
977
|
+
skipped: [],
|
|
978
|
+
});
|
|
979
|
+
const written = JSON.parse(readFileSync(watermarkPath(EVENT, platformRoot), "utf-8"));
|
|
980
|
+
expect(written.mailOutlook[KEY]).toEqual({ receivedAt: "2026-08-10T09:05:00Z" });
|
|
981
|
+
});
|
|
982
|
+
it("is true on a first run, when nothing is stored", async () => {
|
|
983
|
+
const sources = quietSources({
|
|
984
|
+
mailOutlook: async () => ({ newestReceivedAt: "2026-08-10T09:00:00Z" }),
|
|
985
|
+
});
|
|
986
|
+
expect(await run([CHECK], sources)).toEqual({ hasWork: true, reasons: ["mail"], errored: [] });
|
|
987
|
+
});
|
|
988
|
+
it("orders by instant, not lexically, so a fractional second is not read as older", async () => {
|
|
989
|
+
seedWatermarks({ mailOutlook: { [KEY]: { receivedAt: "2026-08-10T09:00:00Z" } } });
|
|
990
|
+
const sources = quietSources({
|
|
991
|
+
// Lexically "…00.500Z" sorts BELOW "…00Z" ('.' is 0x2E, 'Z' is 0x5A).
|
|
992
|
+
mailOutlook: async () => ({ newestReceivedAt: "2026-08-10T09:00:00.500Z" }),
|
|
993
|
+
});
|
|
994
|
+
expect(await run([CHECK], sources)).toEqual({ hasWork: true, reasons: ["mail"], errored: [] });
|
|
995
|
+
});
|
|
996
|
+
it("reports an empty folder as skipped on a commit rather than advancing onto nothing", async () => {
|
|
997
|
+
const sources = quietSources({ mailOutlook: async () => ({ newestReceivedAt: null }) });
|
|
998
|
+
expect(await run([CHECK], sources, true)).toEqual({
|
|
999
|
+
hasWork: false,
|
|
1000
|
+
reasons: [],
|
|
1001
|
+
errored: [],
|
|
1002
|
+
advanced: [],
|
|
1003
|
+
skipped: ["mail"],
|
|
1004
|
+
});
|
|
1005
|
+
});
|
|
1006
|
+
it("keys the folder segment lower-cased, so two spellings address one entry", async () => {
|
|
1007
|
+
seedWatermarks({ mailOutlook: { [KEY]: { receivedAt: "2026-08-10T09:00:00Z" } } });
|
|
1008
|
+
const check = { ...CHECK, folder: "Inbox" };
|
|
1009
|
+
const sources = quietSources({
|
|
1010
|
+
mailOutlook: async () => ({ newestReceivedAt: "2026-08-10T09:00:00Z" }),
|
|
1011
|
+
});
|
|
1012
|
+
expect(await run([check], sources)).toEqual({ hasWork: false, reasons: [], errored: [] });
|
|
1013
|
+
});
|
|
1014
|
+
it("counts a probe that throws as work, and never as quiet", async () => {
|
|
1015
|
+
const sources = quietSources({
|
|
1016
|
+
mailOutlook: async () => {
|
|
1017
|
+
throw new Error("No Outlook mailbox admin@muvin.co.uk is connected");
|
|
1018
|
+
},
|
|
1019
|
+
});
|
|
1020
|
+
expect(await run([CHECK], sources)).toEqual({
|
|
1021
|
+
hasWork: true,
|
|
1022
|
+
reasons: ["mail"],
|
|
1023
|
+
errored: [{ kind: "mail", detail: "No Outlook mailbox admin@muvin.co.uk is connected" }],
|
|
1024
|
+
});
|
|
1025
|
+
});
|
|
1026
|
+
it("throws on a provider outside the accepted list", async () => {
|
|
1027
|
+
const check = { type: "mail", mailbox: "a@b.com", provider: "pop3" };
|
|
1028
|
+
expect(await run([check], quietSources())).toEqual({
|
|
1029
|
+
hasWork: true,
|
|
1030
|
+
reasons: ["mail"],
|
|
1031
|
+
errored: [{ kind: "mail", detail: 'unknown mail provider "pop3" — accepted: imap, outlook' }],
|
|
1032
|
+
});
|
|
1033
|
+
});
|
|
1034
|
+
it("leaves the IMAP watermark map untouched", async () => {
|
|
1035
|
+
seedWatermarks({ mail: { [KEY]: { uid: 42, uidValidity: 7 } } });
|
|
1036
|
+
const sources = quietSources({
|
|
1037
|
+
mailOutlook: async () => ({ newestReceivedAt: "2026-08-10T09:00:00Z" }),
|
|
1038
|
+
});
|
|
1039
|
+
// Task 2654 — the gate call records the position the commit promotes.
|
|
1040
|
+
await run([CHECK], sources);
|
|
1041
|
+
await run([CHECK], sources, true);
|
|
1042
|
+
const written = JSON.parse(readFileSync(watermarkPath(EVENT, platformRoot), "utf-8"));
|
|
1043
|
+
expect(written.mail[KEY]).toEqual({ uid: 42, uidValidity: 7 });
|
|
1044
|
+
expect(written.mailOutlook[KEY]).toEqual({ receivedAt: "2026-08-10T09:00:00Z" });
|
|
1045
|
+
});
|
|
1046
|
+
it("takes the IMAP arm when provider is absent", async () => {
|
|
1047
|
+
let outlookCalled = false;
|
|
1048
|
+
const sources = quietSources({
|
|
1049
|
+
mailOutlook: async () => {
|
|
1050
|
+
outlookCalled = true;
|
|
1051
|
+
return { newestReceivedAt: "2026-08-10T09:00:00Z" };
|
|
1052
|
+
},
|
|
1053
|
+
});
|
|
1054
|
+
await run([{ type: "mail", mailbox: "admin@muvin.co.uk" }], sources);
|
|
1055
|
+
expect(outlookCalled).toBe(false);
|
|
1056
|
+
});
|
|
1057
|
+
});
|
|
1058
|
+
// ---------------------------------------------------------------------------
|
|
1059
|
+
// Task 2654 — a source that moves between the gate call and the commit.
|
|
1060
|
+
//
|
|
1061
|
+
// The commit runs at the END of the agent turn, so its own probe already
|
|
1062
|
+
// includes whatever arrived while the turn ran. Writing that position puts the
|
|
1063
|
+
// arrival BELOW the watermark and no later fire reports it. The commit writes
|
|
1064
|
+
// what the GATE read instead, so the mover stays above.
|
|
1065
|
+
//
|
|
1066
|
+
// Each check is driven alone, and each test runs the real sequence: gate,
|
|
1067
|
+
// source moves, commit, gate again.
|
|
1068
|
+
// ---------------------------------------------------------------------------
|
|
1069
|
+
describe("a source that moves between the gate call and the commit", () => {
|
|
1070
|
+
it("mail: a UID that arrives mid-turn is still reported after the commit", async () => {
|
|
1071
|
+
let highest = 105;
|
|
1072
|
+
const sources = quietSources({
|
|
1073
|
+
mail: async (_c, sinceUid) => ({
|
|
1074
|
+
matched: highest > sinceUid,
|
|
1075
|
+
highestUid: Math.max(highest, sinceUid),
|
|
1076
|
+
uidValidity: 1,
|
|
1077
|
+
}),
|
|
1078
|
+
});
|
|
1079
|
+
const checks = [{ type: "mail", mailbox: "ops@example.com" }];
|
|
1080
|
+
expect(await run(checks, sources)).toEqual({ hasWork: true, reasons: ["mail"], errored: [] });
|
|
1081
|
+
highest = 106; // arrives while the turn runs
|
|
1082
|
+
await run(checks, sources, true);
|
|
1083
|
+
const marks = JSON.parse(readFileSync(watermarkPath(EVENT, platformRoot), "utf-8"));
|
|
1084
|
+
expect(marks.mail["ops@example.com|inbox"].uid).toBe(105);
|
|
1085
|
+
expect(await run(checks, sources)).toEqual({ hasWork: true, reasons: ["mail"], errored: [] });
|
|
1086
|
+
});
|
|
1087
|
+
it("whatsapp: an inbound message mid-turn is still reported after the commit", async () => {
|
|
1088
|
+
let latest = 1_754_000_100;
|
|
1089
|
+
const sources = quietSources({
|
|
1090
|
+
whatsappInbound: async (_a, since) => ({
|
|
1091
|
+
latest: Math.max(latest, since),
|
|
1092
|
+
newer: latest > since,
|
|
1093
|
+
}),
|
|
1094
|
+
});
|
|
1095
|
+
const checks = [{ type: "whatsapp", scope: "inbound" }];
|
|
1096
|
+
expect(await run(checks, sources)).toEqual({ hasWork: true, reasons: ["whatsapp"], errored: [] });
|
|
1097
|
+
latest = 1_754_000_200;
|
|
1098
|
+
await run(checks, sources, true);
|
|
1099
|
+
const marks = JSON.parse(readFileSync(watermarkPath(EVENT, platformRoot), "utf-8"));
|
|
1100
|
+
expect(marks.whatsapp.inbound).toBe(1_754_000_100);
|
|
1101
|
+
expect(await run(checks, sources)).toEqual({ hasWork: true, reasons: ["whatsapp"], errored: [] });
|
|
1102
|
+
});
|
|
1103
|
+
it("telegram: an inbound message mid-turn is still reported after the commit", async () => {
|
|
1104
|
+
let newest = "2026-08-10T10:00:00.000Z";
|
|
1105
|
+
const sources = quietSources({ telegramInbound: async () => ({ newestInboundAt: newest }) });
|
|
1106
|
+
const checks = [{ type: "telegram", scope: "inbound" }];
|
|
1107
|
+
expect(await run(checks, sources)).toEqual({ hasWork: true, reasons: ["telegram"], errored: [] });
|
|
1108
|
+
newest = "2026-08-10T10:30:00.000Z";
|
|
1109
|
+
await run(checks, sources, true);
|
|
1110
|
+
const marks = JSON.parse(readFileSync(watermarkPath(EVENT, platformRoot), "utf-8"));
|
|
1111
|
+
expect(marks.telegram.inbound).toBe("2026-08-10T10:00:00.000Z");
|
|
1112
|
+
expect(await run(checks, sources)).toEqual({ hasWork: true, reasons: ["telegram"], errored: [] });
|
|
1113
|
+
});
|
|
1114
|
+
it("calendar: a connector change mid-turn is still reported after the commit", async () => {
|
|
1115
|
+
let digest = "DIGEST-A";
|
|
1116
|
+
const sources = quietSources({ calendarDigest: async () => digest });
|
|
1117
|
+
const checks = [{ type: "calendar", connector: "google" }];
|
|
1118
|
+
expect(await run(checks, sources)).toEqual({ hasWork: true, reasons: ["calendar"], errored: [] });
|
|
1119
|
+
digest = "DIGEST-B";
|
|
1120
|
+
await run(checks, sources, true);
|
|
1121
|
+
const marks = JSON.parse(readFileSync(watermarkPath(EVENT, platformRoot), "utf-8"));
|
|
1122
|
+
expect(marks.calendar.digest).toBe("DIGEST-A");
|
|
1123
|
+
expect(await run(checks, sources)).toEqual({ hasWork: true, reasons: ["calendar"], errored: [] });
|
|
1124
|
+
});
|
|
1125
|
+
// Task 2654 — the no-event case. A message swallowed in production emitted
|
|
1126
|
+
// nothing at all, and re-running the gate reproduced nothing, so the pair below
|
|
1127
|
+
// is the only way to see a watermark that jumped past an unseen window.
|
|
1128
|
+
describe("gate-read and commit positions pair on one fire", () => {
|
|
1129
|
+
// `console.error` is already spied in `beforeEach`, so its recorded calls are
|
|
1130
|
+
// the log. Re-mocking it here would drop the lines written before this point.
|
|
1131
|
+
const errorLines = () => vi.mocked(console.error).mock.calls.map((c) => c.map(String).join(" "));
|
|
1132
|
+
it("names the position read at gate time and the position written at commit time", async () => {
|
|
1133
|
+
const sources = quietSources({ calendarDigest: async () => "abcdef0123456789" });
|
|
1134
|
+
const checks = [{ type: "calendar", connector: "google" }];
|
|
1135
|
+
await run(checks, sources);
|
|
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"));
|
|
1139
|
+
});
|
|
1140
|
+
it("names an unreadable observation, which otherwise reads as a first commit", async () => {
|
|
1141
|
+
// A present-but-unusable file stops every watermark advancing while
|
|
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" });
|
|
1145
|
+
const checks = [{ type: "calendar", connector: "google" }];
|
|
1146
|
+
await run(checks, sources);
|
|
1147
|
+
writeFileSync(observationPath(EVENT, platformRoot), "{ not json");
|
|
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([]);
|
|
1156
|
+
});
|
|
1157
|
+
it("emits no gate-read for a check that holds no position", async () => {
|
|
1158
|
+
// `op=check-error` and `skipped=` already cover a check with no position;
|
|
1159
|
+
// a `position=null` line would only add a value nobody can compare.
|
|
1160
|
+
await run([{ type: "d1", database: "b", sql: "SELECT count(*) FROM b" }], quietSources());
|
|
1161
|
+
expect(errorLines().filter((l) => l.includes("op=gate-read"))).toEqual([]);
|
|
1162
|
+
});
|
|
1163
|
+
});
|
|
1164
|
+
it("a commit with no gate call before it advances nothing", async () => {
|
|
1165
|
+
// Route two (PLUGIN.md) lets a separate action event issue the commit. With
|
|
1166
|
+
// no observation on file there is no position anyone saw, so the watermark
|
|
1167
|
+
// stays put and the next fire reports the work again — the fail-open
|
|
1168
|
+
// direction the whole tool holds to.
|
|
1169
|
+
const sources = quietSources({ calendarDigest: async () => "DIGEST-Z" });
|
|
1170
|
+
const checks = [{ type: "calendar", connector: "google" }];
|
|
1171
|
+
const result = await run(checks, sources, true);
|
|
1172
|
+
expect(result.advanced).toEqual([]);
|
|
1173
|
+
const marks = JSON.parse(readFileSync(watermarkPath(EVENT, platformRoot), "utf-8"));
|
|
1174
|
+
expect(marks.calendar).toBeUndefined();
|
|
1175
|
+
expect(await run(checks, sources)).toEqual({ hasWork: true, reasons: ["calendar"], errored: [] });
|
|
745
1176
|
});
|
|
746
1177
|
});
|
|
747
1178
|
//# sourceMappingURL=scheduling-gate.test.js.map
|