@rubytech/create-realagent-code 0.1.590 → 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 +13 -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 +11 -5
- 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 +339 -275
- 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 +510 -96
- 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 +62 -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 +153 -93
- 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 +8 -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 +234 -30
- 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 +349 -38
- 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 +15 -15
- 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__/schedule-gate-write.test.js
CHANGED
|
@@ -13,13 +13,6 @@ const runCalls = [];
|
|
|
13
13
|
const writeCalls = [];
|
|
14
14
|
/** What the pre-read reports the stored row holds; per-test. */
|
|
15
15
|
let storedRow = {};
|
|
16
|
-
/** Task 2634 — what the commit-action read finds on the account; per-test. The
|
|
17
|
-
* two reads share one mocked session, so they are told apart by the only thing
|
|
18
|
-
* that distinguishes them at the driver: the query text. */
|
|
19
|
-
let commitActionRows = [];
|
|
20
|
-
function isCommitActionRead(query) {
|
|
21
|
-
return query.includes("actionTool: 'scheduling-gate'");
|
|
22
|
-
}
|
|
23
16
|
vi.mock("../../lib/embedding.js", () => ({
|
|
24
17
|
computeEmbedding: vi.fn(async () => []),
|
|
25
18
|
}));
|
|
@@ -27,13 +20,6 @@ vi.mock("../../lib/neo4j.js", () => ({
|
|
|
27
20
|
getSession: () => ({
|
|
28
21
|
run: vi.fn(async (query, params) => {
|
|
29
22
|
runCalls.push({ query, params });
|
|
30
|
-
if (isCommitActionRead(query)) {
|
|
31
|
-
return {
|
|
32
|
-
records: commitActionRows.map((row) => ({
|
|
33
|
-
get: (k) => row[k] ?? null,
|
|
34
|
-
})),
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
23
|
return {
|
|
38
24
|
records: [
|
|
39
25
|
{
|
|
@@ -50,9 +36,35 @@ vi.mock("../../../../../../lib/graph-write/dist/index.js", () => ({
|
|
|
50
36
|
writeCalls.push(props);
|
|
51
37
|
}),
|
|
52
38
|
}));
|
|
39
|
+
/** Task 2658 — what the bind-time probe reports, per-test. The real prober is
|
|
40
|
+
* covered by `scheduling-gate.test.ts`; this file owns the GUARD, so the probe
|
|
41
|
+
* is stubbed and its call arguments are asserted instead. Without the stub the
|
|
42
|
+
* pre-existing `mail`-gate tests below would open a live IMAP connection.
|
|
43
|
+
*
|
|
44
|
+
* `gateHasWork` is a SEPARATE knob from `gateErrored` on purpose. Deriving one
|
|
45
|
+
* from the other would make "the sources answered, work pending" and "the
|
|
46
|
+
* sources answered, nothing pending" the same fixture, and a guard that
|
|
47
|
+
* wrongly keys on `hasWork` would pass both. */
|
|
48
|
+
let gateErrored = [];
|
|
49
|
+
let gateHasWork = false;
|
|
50
|
+
const gateRunCalls = [];
|
|
51
|
+
vi.mock("../scheduling-gate.js", async (importOriginal) => {
|
|
52
|
+
const actual = await importOriginal();
|
|
53
|
+
return {
|
|
54
|
+
...actual,
|
|
55
|
+
runSchedulingGate: vi.fn(async (params) => {
|
|
56
|
+
gateRunCalls.push(params);
|
|
57
|
+
return {
|
|
58
|
+
hasWork: gateHasWork || gateErrored.length > 0,
|
|
59
|
+
reasons: gateHasWork ? ["d1"] : [],
|
|
60
|
+
errored: gateErrored,
|
|
61
|
+
};
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
});
|
|
53
65
|
import { scheduleUpdate } from "../schedule-update.js";
|
|
54
66
|
import { scheduleEvent } from "../schedule-event.js";
|
|
55
|
-
import { validateGate,
|
|
67
|
+
import { validateGate, probeGateAtBindTime, assertGateProbed } from "../schedule-event.js";
|
|
56
68
|
const ACCOUNT_ID = "11111111-1111-4111-8111-111111111111";
|
|
57
69
|
const PHONE = "+447700900001";
|
|
58
70
|
const GATE = { plugin: "scheduling", tool: "scheduling-gate", args: { eventId: "e1", checks: [] } };
|
|
@@ -70,7 +82,9 @@ beforeEach(() => {
|
|
|
70
82
|
runCalls.length = 0;
|
|
71
83
|
writeCalls.length = 0;
|
|
72
84
|
storedRow = {};
|
|
73
|
-
|
|
85
|
+
gateErrored = [];
|
|
86
|
+
gateHasWork = false;
|
|
87
|
+
gateRunCalls.length = 0;
|
|
74
88
|
const root = mkdtempSync(join(tmpdir(), "sched-gate-write-"));
|
|
75
89
|
platformRoot = join(root, "platform");
|
|
76
90
|
mkdirSync(platformRoot, { recursive: true });
|
|
@@ -85,17 +99,17 @@ afterEach(() => {
|
|
|
85
99
|
});
|
|
86
100
|
describe("validateGate", () => {
|
|
87
101
|
it("accepts a gate on a complete agent dispatch", () => {
|
|
88
|
-
expect(() => validateGate(GATE, { hasAction: false, hasCompleteAgentDispatch: true,
|
|
102
|
+
expect(() => validateGate(GATE, { hasAction: false, hasCompleteAgentDispatch: true, eventId: null })).not.toThrow();
|
|
89
103
|
});
|
|
90
104
|
it("refuses a gate on an action-mode event", () => {
|
|
91
|
-
expect(() => validateGate(GATE, { hasAction: true, hasCompleteAgentDispatch: false,
|
|
105
|
+
expect(() => validateGate(GATE, { hasAction: true, hasCompleteAgentDispatch: false, eventId: null })).toThrow(/action already runs headlessly/);
|
|
92
106
|
});
|
|
93
107
|
it("refuses a gate with no complete agent dispatch", () => {
|
|
94
|
-
expect(() => validateGate(GATE, { hasAction: false, hasCompleteAgentDispatch: false,
|
|
108
|
+
expect(() => validateGate(GATE, { hasAction: false, hasCompleteAgentDispatch: false, eventId: null })).toThrow(/requires a complete agentDispatch/);
|
|
95
109
|
});
|
|
96
110
|
it("refuses a gate missing plugin or tool", () => {
|
|
97
|
-
expect(() => validateGate({ plugin: "", tool: "t" }, { hasAction: false, hasCompleteAgentDispatch: true,
|
|
98
|
-
expect(() => validateGate({ plugin: "p", tool: "" }, { hasAction: false, hasCompleteAgentDispatch: true,
|
|
111
|
+
expect(() => validateGate({ plugin: "", tool: "t" }, { hasAction: false, hasCompleteAgentDispatch: true, eventId: null })).toThrow(/both 'plugin' and 'tool'/);
|
|
112
|
+
expect(() => validateGate({ plugin: "p", tool: "" }, { hasAction: false, hasCompleteAgentDispatch: true, eventId: null })).toThrow(/both 'plugin' and 'tool'/);
|
|
99
113
|
});
|
|
100
114
|
});
|
|
101
115
|
describe("scheduleEvent — gate", () => {
|
|
@@ -277,235 +291,10 @@ const UNWATERMARKED_GATE = {
|
|
|
277
291
|
],
|
|
278
292
|
},
|
|
279
293
|
};
|
|
280
|
-
// Task
|
|
281
|
-
// is
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
// every seeded routine on its next gate edit.
|
|
285
|
-
const TOKEN_PROMPT = "sweep, then call scheduling-gate with <<gateCommit>>";
|
|
286
|
-
const COMMIT_PROMPT = 'sweep, then call scheduling-gate with {"eventId":"e1","checks":[{"type":"mail","mailbox":"ops@example.com"}],"commit":true}';
|
|
287
|
-
/** The live shape Task 2634 was filed on: names the tool, forbids the call. */
|
|
288
|
-
const FORBIDS_COMMIT_PROMPT = "sweep. Do NOT call scheduling-gate yourself. A separate routine advances the watermark.";
|
|
289
|
-
/** A scheduled action event that commits this row's watermark with no model. */
|
|
290
|
-
function commitActionRow(over = {}) {
|
|
291
|
-
return {
|
|
292
|
-
eventStatus: "scheduled",
|
|
293
|
-
actionPlugin: "scheduling",
|
|
294
|
-
actionTool: "scheduling-gate",
|
|
295
|
-
actionArgs: JSON.stringify({
|
|
296
|
-
eventId: "e1",
|
|
297
|
-
checks: [{ type: "mail", mailbox: "ops@example.com" }],
|
|
298
|
-
commit: true,
|
|
299
|
-
}),
|
|
300
|
-
...over,
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
describe("commitless watermarked gate", () => {
|
|
304
|
-
it("is refused on update when the resulting prompt has no commit step", async () => {
|
|
305
|
-
storedRow = { agentChannel: "whatsapp", agentDestination: PHONE, agentPrompt: "sweep" };
|
|
306
|
-
await expect(scheduleUpdate({ accountId: ACCOUNT_ID, eventId: "e1", gate: MAIL_GATE })).rejects.toThrow(/scheduling-gate/);
|
|
307
|
-
expect(writeCalls).toHaveLength(0);
|
|
308
|
-
expect(lastSet()?.query ?? "").not.toContain("e.gatePlugin");
|
|
309
|
-
});
|
|
310
|
-
it("is accepted on update when the stored prompt carries the commit step", async () => {
|
|
311
|
-
storedRow = { agentChannel: "whatsapp", agentDestination: PHONE, agentPrompt: COMMIT_PROMPT };
|
|
312
|
-
await scheduleUpdate({ accountId: ACCOUNT_ID, eventId: "e1", gate: MAIL_GATE });
|
|
313
|
-
expect(lastSet().query).toContain("e.gatePlugin");
|
|
314
|
-
});
|
|
315
|
-
it("reads the SUPPLIED prompt over the stored one", async () => {
|
|
316
|
-
storedRow = { agentChannel: "whatsapp", agentDestination: PHONE, agentPrompt: "sweep" };
|
|
317
|
-
await scheduleUpdate({
|
|
318
|
-
accountId: ACCOUNT_ID,
|
|
319
|
-
eventId: "e1",
|
|
320
|
-
gate: MAIL_GATE,
|
|
321
|
-
agentDispatch: { channel: "whatsapp", destination: PHONE, prompt: COMMIT_PROMPT },
|
|
322
|
-
});
|
|
323
|
-
expect(lastSet().query).toContain("e.gatePlugin");
|
|
324
|
-
});
|
|
325
|
-
it("accepts an unwatermarked gate against a prompt with no commit step", async () => {
|
|
326
|
-
storedRow = { agentChannel: "whatsapp", agentDestination: PHONE, agentPrompt: "sweep" };
|
|
327
|
-
await scheduleUpdate({ accountId: ACCOUNT_ID, eventId: "e1", gate: UNWATERMARKED_GATE });
|
|
328
|
-
expect(lastSet().query).toContain("e.gatePlugin");
|
|
329
|
-
});
|
|
330
|
-
it("is refused on create too, because both paths share validateGate", () => {
|
|
331
|
-
expect(() => validateGate(MAIL_GATE, {
|
|
332
|
-
hasAction: false,
|
|
333
|
-
hasCompleteAgentDispatch: true,
|
|
334
|
-
resultingPrompt: "sweep",
|
|
335
|
-
eventId: null,
|
|
336
|
-
hasCommitAction: false,
|
|
337
|
-
})).toThrow(/scheduling-gate/);
|
|
338
|
-
expect(() => validateGate(MAIL_GATE, {
|
|
339
|
-
hasAction: false,
|
|
340
|
-
hasCompleteAgentDispatch: true,
|
|
341
|
-
resultingPrompt: COMMIT_PROMPT,
|
|
342
|
-
eventId: null,
|
|
343
|
-
hasCommitAction: false,
|
|
344
|
-
})).not.toThrow();
|
|
345
|
-
});
|
|
346
|
-
// The watermark file is a `scheduling-gate` concept, so the reason this
|
|
347
|
-
// refusal exists does not apply to a different tool's arguments.
|
|
348
|
-
it("does not refuse a gate belonging to another tool", () => {
|
|
349
|
-
expect(() => validateGate({ plugin: "other", tool: "other-gate", args: { checks: [{ type: "mail", mailbox: "m" }] } }, {
|
|
350
|
-
hasAction: false,
|
|
351
|
-
hasCompleteAgentDispatch: true,
|
|
352
|
-
resultingPrompt: "sweep",
|
|
353
|
-
eventId: null,
|
|
354
|
-
hasCommitAction: false,
|
|
355
|
-
})).not.toThrow();
|
|
356
|
-
});
|
|
357
|
-
});
|
|
358
|
-
// ---------------------------------------------------------------------------
|
|
359
|
-
// Task 2634 — the guard above was a substring test on the prompt, so a prompt
|
|
360
|
-
// reading "Do NOT call scheduling-gate yourself" satisfied a guard that exists
|
|
361
|
-
// to prove the prompt calls it. Two routes replace it, and the refusal names
|
|
362
|
-
// both: the prompt carries the commit argument, or a scheduled action-mode
|
|
363
|
-
// event commits this row's watermark with no model in the path.
|
|
364
|
-
// ---------------------------------------------------------------------------
|
|
365
|
-
describe("commit route — prompt (route 1)", () => {
|
|
366
|
-
function check(prompt, over = {}) {
|
|
367
|
-
return () => validateGate(MAIL_GATE, {
|
|
368
|
-
hasAction: false,
|
|
369
|
-
hasCompleteAgentDispatch: true,
|
|
370
|
-
resultingPrompt: prompt,
|
|
371
|
-
eventId: "e1",
|
|
372
|
-
hasCommitAction: false,
|
|
373
|
-
...over,
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
it("refuses a prompt that names the tool only to forbid the call", () => {
|
|
377
|
-
expect(check(FORBIDS_COMMIT_PROMPT)).toThrow(/scheduling-gate/);
|
|
378
|
-
});
|
|
379
|
-
it("names both routes in the refusal", () => {
|
|
380
|
-
expect(check(FORBIDS_COMMIT_PROMPT)).toThrow(/"commit":true/);
|
|
381
|
-
expect(check(FORBIDS_COMMIT_PROMPT)).toThrow(/action-mode event/);
|
|
382
|
-
});
|
|
383
|
-
it("accepts the unexpanded roster token", () => {
|
|
384
|
-
expect(check(TOKEN_PROMPT)).not.toThrow();
|
|
385
|
-
});
|
|
386
|
-
it("accepts the seeded prompt, whose token the seed already expanded", () => {
|
|
387
|
-
expect(check(COMMIT_PROMPT)).not.toThrow();
|
|
388
|
-
});
|
|
389
|
-
it("accepts the commit argument written with a space after the key", () => {
|
|
390
|
-
expect(check('call scheduling-gate with {"eventId":"e1", "commit": true}')).not.toThrow();
|
|
391
|
-
});
|
|
392
|
-
it("refuses prose that says commit without carrying the argument", () => {
|
|
393
|
-
expect(check("sweep, then commit the watermark with scheduling-gate")).toThrow(/scheduling-gate/);
|
|
394
|
-
});
|
|
395
|
-
it("leaves an unwatermarked gate alone whatever the prompt says", () => {
|
|
396
|
-
expect(() => validateGate(UNWATERMARKED_GATE, {
|
|
397
|
-
hasAction: false,
|
|
398
|
-
hasCompleteAgentDispatch: true,
|
|
399
|
-
resultingPrompt: FORBIDS_COMMIT_PROMPT,
|
|
400
|
-
eventId: "e1",
|
|
401
|
-
hasCommitAction: false,
|
|
402
|
-
})).not.toThrow();
|
|
403
|
-
});
|
|
404
|
-
});
|
|
405
|
-
describe("commit route — bound action event (route 2)", () => {
|
|
406
|
-
it("accepts a watermarked gate whose commit is a separate action event", () => {
|
|
407
|
-
expect(() => validateGate(MAIL_GATE, {
|
|
408
|
-
hasAction: false,
|
|
409
|
-
hasCompleteAgentDispatch: true,
|
|
410
|
-
resultingPrompt: FORBIDS_COMMIT_PROMPT,
|
|
411
|
-
eventId: "e1",
|
|
412
|
-
hasCommitAction: true,
|
|
413
|
-
})).not.toThrow();
|
|
414
|
-
});
|
|
415
|
-
describe("hasCommitAction", () => {
|
|
416
|
-
it("matches a scheduled scheduling-gate action naming this event with commit true", () => {
|
|
417
|
-
expect(hasCommitAction([commitActionRow()], "e1")).toBe(true);
|
|
418
|
-
});
|
|
419
|
-
it("rejects commit false", () => {
|
|
420
|
-
const args = JSON.parse(commitActionRow().actionArgs);
|
|
421
|
-
args.commit = false;
|
|
422
|
-
expect(hasCommitAction([commitActionRow({ actionArgs: JSON.stringify(args) })], "e1")).toBe(false);
|
|
423
|
-
});
|
|
424
|
-
it("rejects a different eventId", () => {
|
|
425
|
-
expect(hasCommitAction([commitActionRow()], "e2")).toBe(false);
|
|
426
|
-
});
|
|
427
|
-
it("rejects a row whose eventStatus is not scheduled", () => {
|
|
428
|
-
expect(hasCommitAction([commitActionRow({ eventStatus: "suspended" })], "e1")).toBe(false);
|
|
429
|
-
expect(hasCommitAction([commitActionRow({ eventStatus: "cancelled" })], "e1")).toBe(false);
|
|
430
|
-
});
|
|
431
|
-
it("rejects a row bound to another tool", () => {
|
|
432
|
-
expect(hasCommitAction([commitActionRow({ actionTool: "schedule-list" })], "e1")).toBe(false);
|
|
433
|
-
expect(hasCommitAction([commitActionRow({ actionPlugin: "work" })], "e1")).toBe(false);
|
|
434
|
-
});
|
|
435
|
-
// A row whose args do not parse is a non-match, not a throw: the validator's
|
|
436
|
-
// job is to refuse cleanly, and one malformed row must not take down the
|
|
437
|
-
// write path for every other row on the account.
|
|
438
|
-
it("treats unparseable or absent args as a non-match", () => {
|
|
439
|
-
expect(hasCommitAction([commitActionRow({ actionArgs: "{not json" })], "e1")).toBe(false);
|
|
440
|
-
expect(hasCommitAction([commitActionRow({ actionArgs: null })], "e1")).toBe(false);
|
|
441
|
-
expect(hasCommitAction([commitActionRow({ actionArgs: "[1,2]" })], "e1")).toBe(false);
|
|
442
|
-
});
|
|
443
|
-
// At create time the id is minted after the validator runs, so there is no
|
|
444
|
-
// id for an action event to have named. Route 2 is unreachable on that path
|
|
445
|
-
// by construction, and a null id must never match a row.
|
|
446
|
-
it("never matches when the row has no id yet", () => {
|
|
447
|
-
expect(hasCommitAction([commitActionRow()], null)).toBe(false);
|
|
448
|
-
});
|
|
449
|
-
it("is false on an empty account", () => {
|
|
450
|
-
expect(hasCommitAction([], "e1")).toBe(false);
|
|
451
|
-
});
|
|
452
|
-
// The row Task 2634 was filed on, read from laptop 192.168.88.10 SiteDesk
|
|
453
|
-
// account 5e99bba5 on 2026-08-09. Verbatim, because the stub above chose its
|
|
454
|
-
// own key order and the live writer chose a different one: this is the only
|
|
455
|
-
// assertion that the shape the fleet actually stores parses the same way.
|
|
456
|
-
// Its routine's prompt carries neither spelling of the commit argument
|
|
457
|
-
// (measured: `agentPrompt CONTAINS '"commit":true'` and `CONTAINS
|
|
458
|
-
// '<<gateCommit>>'` are both FALSE, while the old substring test's
|
|
459
|
-
// `CONTAINS 'scheduling-gate'` is TRUE), so route 2 alone is what keeps it
|
|
460
|
-
// valid on its next edit.
|
|
461
|
-
it("matches the live SiteDesk commit action verbatim", () => {
|
|
462
|
-
const live = {
|
|
463
|
-
eventStatus: "scheduled",
|
|
464
|
-
actionPlugin: "scheduling",
|
|
465
|
-
actionTool: "scheduling-gate",
|
|
466
|
-
actionArgs: '{"eventId":"14414285-8549-4ce1-907d-0d9cadd370ee","commit":true,"checks":[{"type":"mail","mailbox":"hello@beagle.direct","folder":"inbox"},{"type":"mail","mailbox":"operations@sitedesk.online","folder":"inbox"}]}',
|
|
467
|
-
};
|
|
468
|
-
expect(hasCommitAction([live], "14414285-8549-4ce1-907d-0d9cadd370ee")).toBe(true);
|
|
469
|
-
expect(hasCommitAction([live], "ae361343-2d85-426d-8eb3-fde621c8cce0")).toBe(false);
|
|
470
|
-
});
|
|
471
|
-
});
|
|
472
|
-
});
|
|
473
|
-
describe("scheduleUpdate — commit routes end to end", () => {
|
|
474
|
-
beforeEach(() => {
|
|
475
|
-
storedRow = { agentChannel: "whatsapp", agentDestination: PHONE, agentPrompt: FORBIDS_COMMIT_PROMPT };
|
|
476
|
-
});
|
|
477
|
-
it("refuses when neither route is taken", async () => {
|
|
478
|
-
await expect(scheduleUpdate({ accountId: ACCOUNT_ID, eventId: "e1", gate: MAIL_GATE })).rejects.toThrow(/scheduling-gate/);
|
|
479
|
-
expect(lastSet()?.query ?? "").not.toContain("e.gatePlugin");
|
|
480
|
-
});
|
|
481
|
-
it("accepts when a bound action event commits the watermark", async () => {
|
|
482
|
-
commitActionRows = [commitActionRow()];
|
|
483
|
-
await scheduleUpdate({ accountId: ACCOUNT_ID, eventId: "e1", gate: MAIL_GATE });
|
|
484
|
-
expect(lastSet().query).toContain("e.gatePlugin");
|
|
485
|
-
});
|
|
486
|
-
it("refuses when the bound action event commits nothing", async () => {
|
|
487
|
-
const args = JSON.parse(commitActionRow().actionArgs);
|
|
488
|
-
args.commit = false;
|
|
489
|
-
commitActionRows = [commitActionRow({ actionArgs: JSON.stringify(args) })];
|
|
490
|
-
await expect(scheduleUpdate({ accountId: ACCOUNT_ID, eventId: "e1", gate: MAIL_GATE })).rejects.toThrow(/scheduling-gate/);
|
|
491
|
-
});
|
|
492
|
-
it("scopes the commit-action read to the calling account", async () => {
|
|
493
|
-
commitActionRows = [commitActionRow()];
|
|
494
|
-
await scheduleUpdate({ accountId: ACCOUNT_ID, eventId: "e1", gate: MAIL_GATE });
|
|
495
|
-
const read = runCalls.find((c) => isCommitActionRead(c.query));
|
|
496
|
-
expect(read?.params.accountId).toBe(ACCOUNT_ID);
|
|
497
|
-
});
|
|
498
|
-
// The read costs a round trip, so it runs only for the gates that have a
|
|
499
|
-
// commit route to look for.
|
|
500
|
-
it("does not read commit actions for an unwatermarked gate", async () => {
|
|
501
|
-
await scheduleUpdate({ accountId: ACCOUNT_ID, eventId: "e1", gate: UNWATERMARKED_GATE });
|
|
502
|
-
expect(runCalls.some((c) => isCommitActionRead(c.query))).toBe(false);
|
|
503
|
-
});
|
|
504
|
-
});
|
|
505
|
-
// A guard that refuses silently is indistinguishable from one that never ran,
|
|
506
|
-
// and an acceptance that does not name its route is indistinguishable from a
|
|
507
|
-
// skipped guard. Both lines carry the check types, so a refusal can be read
|
|
508
|
-
// without going back to the row.
|
|
294
|
+
// Task 2663 — the three prompt fixtures are gone with the guard that read them.
|
|
295
|
+
// A prompt is no longer any part of whether a gate is accepted: the platform
|
|
296
|
+
// advances the mark on every accepted wake, so there is nothing for a prompt to
|
|
297
|
+
// arrange and nothing to inspect it for.
|
|
509
298
|
describe("gate decision evidence", () => {
|
|
510
299
|
let lines;
|
|
511
300
|
beforeEach(() => {
|
|
@@ -519,9 +308,7 @@ describe("gate decision evidence", () => {
|
|
|
519
308
|
validateGate(MAIL_GATE, {
|
|
520
309
|
hasAction: false,
|
|
521
310
|
hasCompleteAgentDispatch: true,
|
|
522
|
-
resultingPrompt: FORBIDS_COMMIT_PROMPT,
|
|
523
311
|
eventId: "e1",
|
|
524
|
-
hasCommitAction: false,
|
|
525
312
|
...over,
|
|
526
313
|
});
|
|
527
314
|
}
|
|
@@ -530,30 +317,18 @@ describe("gate decision evidence", () => {
|
|
|
530
317
|
}
|
|
531
318
|
return lines.filter((l) => l.includes("op=gate-"));
|
|
532
319
|
}
|
|
533
|
-
it("emits
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
expect(decide({
|
|
538
|
-
"[schedule-authoring] op=gate-accepted eventId=e1 route=token checks=mail",
|
|
539
|
-
]);
|
|
540
|
-
});
|
|
541
|
-
it("emits route=action-event when a bound action carries it", () => {
|
|
542
|
-
expect(decide({ hasCommitAction: true })).toEqual([
|
|
543
|
-
"[schedule-authoring] op=gate-accepted eventId=e1 route=action-event checks=mail",
|
|
544
|
-
]);
|
|
545
|
-
});
|
|
546
|
-
it("carries a dash for the id the create path has not minted yet", () => {
|
|
547
|
-
expect(decide({ eventId: null })[0]).toContain("eventId=-");
|
|
320
|
+
it("emits no decision line for a watermarked gate, which needs no arrangement (Task 2663)", () => {
|
|
321
|
+
// The refusal this block was written for is deleted: the platform advances
|
|
322
|
+
// the mark on every accepted wake, so a watermarked check is no longer a
|
|
323
|
+
// reason to refuse a write and there is no route to name.
|
|
324
|
+
expect(decide({})).toEqual([]);
|
|
548
325
|
});
|
|
549
326
|
it("says nothing for a gate with no commit route to take", () => {
|
|
550
327
|
lines = [];
|
|
551
328
|
validateGate(UNWATERMARKED_GATE, {
|
|
552
329
|
hasAction: false,
|
|
553
330
|
hasCompleteAgentDispatch: true,
|
|
554
|
-
resultingPrompt: "sweep",
|
|
555
331
|
eventId: "e1",
|
|
556
|
-
hasCommitAction: false,
|
|
557
332
|
});
|
|
558
333
|
expect(lines.filter((l) => l.includes("op=gate-"))).toEqual([]);
|
|
559
334
|
});
|
|
@@ -576,9 +351,7 @@ const gateWith = (checks) => ({
|
|
|
576
351
|
const ctx = (over = {}) => ({
|
|
577
352
|
hasAction: false,
|
|
578
353
|
hasCompleteAgentDispatch: true,
|
|
579
|
-
resultingPrompt: COMMIT_PROMPT,
|
|
580
354
|
eventId: "e1",
|
|
581
|
-
hasCommitAction: false,
|
|
582
355
|
...over,
|
|
583
356
|
});
|
|
584
357
|
describe("unknown gate check type", () => {
|
|
@@ -613,11 +386,12 @@ describe("unknown gate check type", () => {
|
|
|
613
386
|
it("accepts a telegram check", () => {
|
|
614
387
|
expect(() => validateGate(gateWith([{ type: "telegram", scope: "inbound" }]), ctx())).not.toThrow();
|
|
615
388
|
});
|
|
616
|
-
it("
|
|
617
|
-
// It compares against a stored position exactly as mail and calendar do,
|
|
618
|
-
//
|
|
389
|
+
it("accepts a telegram check with no commit arrangement (Task 2663)", () => {
|
|
390
|
+
// It compares against a stored position exactly as mail and calendar do, and
|
|
391
|
+
// the dispatcher advances that position itself now, so nothing about the
|
|
392
|
+
// prompt is any longer a reason to refuse the write.
|
|
619
393
|
const gate = gateWith([{ type: "telegram", scope: "inbound" }]);
|
|
620
|
-
expect(() => validateGate(gate, ctx(
|
|
394
|
+
expect(() => validateGate(gate, ctx())).not.toThrow();
|
|
621
395
|
});
|
|
622
396
|
it("leaves a gate on another plugin's tool alone", () => {
|
|
623
397
|
// The accepted set is this tool's own vocabulary. A gate pointing at some
|
|
@@ -626,4 +400,294 @@ describe("unknown gate check type", () => {
|
|
|
626
400
|
expect(() => validateGate(foreign, ctx())).not.toThrow();
|
|
627
401
|
});
|
|
628
402
|
});
|
|
403
|
+
// Task 2657 — a mail provider the prober cannot answer, refused at write time.
|
|
404
|
+
//
|
|
405
|
+
// Same reasoning as the unknown check type above: stored, it throws on every
|
|
406
|
+
// fire, the dispatcher fails OPEN, and the routine spends a full model turn
|
|
407
|
+
// while every surface reads "gated".
|
|
408
|
+
describe("unknown mail provider", () => {
|
|
409
|
+
let refusalLines;
|
|
410
|
+
beforeEach(() => {
|
|
411
|
+
refusalLines = [];
|
|
412
|
+
vi.spyOn(console, "error").mockImplementation((line) => {
|
|
413
|
+
refusalLines.push(String(line));
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
const mailGate = (provider) => gateWith([
|
|
417
|
+
provider === undefined
|
|
418
|
+
? { type: "mail", mailbox: "a@b.com" }
|
|
419
|
+
: { type: "mail", mailbox: "a@b.com", provider },
|
|
420
|
+
]);
|
|
421
|
+
it("refuses a provider outside the accepted list", () => {
|
|
422
|
+
expect(() => validateGate(mailGate("pop3"), ctx())).toThrow(/provider not recognised/i);
|
|
423
|
+
});
|
|
424
|
+
it("names the provider and the accepted set on the refusal line", () => {
|
|
425
|
+
expect(() => validateGate(mailGate("pop3"), ctx())).toThrow();
|
|
426
|
+
const line = refusalLines.find((l) => l.includes("reason=unknown-mail-provider"));
|
|
427
|
+
expect(line).toContain("providers=pop3");
|
|
428
|
+
});
|
|
429
|
+
it("accepts outlook", () => {
|
|
430
|
+
expect(() => validateGate(mailGate("outlook"), ctx())).not.toThrow();
|
|
431
|
+
});
|
|
432
|
+
it("accepts imap named explicitly", () => {
|
|
433
|
+
expect(() => validateGate(mailGate("imap"), ctx())).not.toThrow();
|
|
434
|
+
});
|
|
435
|
+
it("accepts a mail check carrying no provider at all", () => {
|
|
436
|
+
expect(() => validateGate(mailGate(), ctx())).not.toThrow();
|
|
437
|
+
});
|
|
438
|
+
it("refuses a provider on a check that is not mail", () => {
|
|
439
|
+
// Task 2662 REVERSES Task 2657's decision here, deliberately.
|
|
440
|
+
//
|
|
441
|
+
// 2657 allowed a stray `provider` on a non-mail check, reasoning that it is
|
|
442
|
+
// the mail check's own field and refusing it "would reject a valid gate". A
|
|
443
|
+
// gate carrying a field nothing reads is not valid, it is misleading: that
|
|
444
|
+
// is the exact defect an operator reported against 0.1.590, having passed a
|
|
445
|
+
// `botId` to a telegram check and watched it report work with the field
|
|
446
|
+
// discarded. `provider` on a d1 check is the same class.
|
|
447
|
+
const gate = gateWith([{ type: "d1", database: "b", sql: "SELECT 1", provider: "pop3" }]);
|
|
448
|
+
expect(() => validateGate(gate, ctx())).toThrow(/provider/);
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
// ---------------------------------------------------------------------------
|
|
452
|
+
// Task 2662 — a field the prober never reads.
|
|
453
|
+
//
|
|
454
|
+
// `validateGate` judged the check's `type`, its `provider` and the commit route,
|
|
455
|
+
// and nothing else. `checks` is `z.array(z.record(z.unknown()))`, so every other
|
|
456
|
+
// key passed and was dropped on the floor. The operator reproduced it twice: a
|
|
457
|
+
// real bot id on a telegram check reported work, then `0000000000` reported work,
|
|
458
|
+
// because neither reached anything that read them.
|
|
459
|
+
// ---------------------------------------------------------------------------
|
|
460
|
+
describe("unknown gate check field", () => {
|
|
461
|
+
let fieldLines;
|
|
462
|
+
beforeEach(() => {
|
|
463
|
+
fieldLines = [];
|
|
464
|
+
vi.spyOn(console, "error").mockImplementation((line) => {
|
|
465
|
+
fieldLines.push(String(line));
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
it("is refused, naming the field and the keys that type accepts", () => {
|
|
469
|
+
const gate = gateWith([{ type: "telegram", scope: "inbound", nonsense: 1 }]);
|
|
470
|
+
expect(() => validateGate(gate, ctx())).toThrow(/nonsense/);
|
|
471
|
+
expect(() => validateGate(gate, ctx())).toThrow(/type.*scope.*botId/);
|
|
472
|
+
});
|
|
473
|
+
it("logs op=gate-refused so the refusal is greppable", () => {
|
|
474
|
+
try {
|
|
475
|
+
validateGate(gateWith([{ type: "telegram", scope: "inbound", nonsense: 1 }]), ctx());
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
/* asserted above */
|
|
479
|
+
}
|
|
480
|
+
const line = fieldLines.find((l) => l.includes("op=gate-refused"));
|
|
481
|
+
expect(line).toContain("reason=unknown-check-field");
|
|
482
|
+
expect(line).toContain("telegram:nonsense");
|
|
483
|
+
});
|
|
484
|
+
it("accepts botId on a telegram check", () => {
|
|
485
|
+
const gate = gateWith([{ type: "telegram", scope: "inbound", botId: "111" }]);
|
|
486
|
+
expect(() => validateGate(gate, ctx())).not.toThrow();
|
|
487
|
+
});
|
|
488
|
+
it("refuses a telegram scope other than inbound", () => {
|
|
489
|
+
// `scope` was decorative: probe()'s telegram arm never read it, so any
|
|
490
|
+
// value passed and behaved as `inbound`.
|
|
491
|
+
const gate = gateWith([{ type: "telegram", scope: "status" }]);
|
|
492
|
+
expect(() => validateGate(gate, ctx())).toThrow(/scope/);
|
|
493
|
+
});
|
|
494
|
+
it("still accepts every shape the four other checks legitimately carry", () => {
|
|
495
|
+
const gate = gateWith([
|
|
496
|
+
{ type: "mail", mailbox: "ops@example.com", folder: "INBOX", subjectPrefixes: ["RE:"] },
|
|
497
|
+
{ type: "mail", mailbox: "ops@example.com", provider: "outlook" },
|
|
498
|
+
{ type: "d1", database: "b", sql: "SELECT count(*) FROM b" },
|
|
499
|
+
{ type: "whatsapp", scope: "status" },
|
|
500
|
+
{ type: "calendar", connector: "google" },
|
|
501
|
+
]);
|
|
502
|
+
expect(() => validateGate(gate, ctx())).not.toThrow();
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
// Task 2658 — the four refusals above all judge the gate's SHAPE. A gate naming
|
|
507
|
+
// an unregistered database, a mailbox whose host does not resolve, or an
|
|
508
|
+
// account whose socket is down is well-formed JSON and passes every one of
|
|
509
|
+
// them. It then stores cleanly, throws inside the tool on each fire, and the
|
|
510
|
+
// dispatcher fails OPEN: two muvin routines cost 96 and 95 model turns in 24
|
|
511
|
+
// hours that way while `schedule-list` and `/routines` both read "gated".
|
|
512
|
+
// ---------------------------------------------------------------------------
|
|
513
|
+
const D1_GATE = {
|
|
514
|
+
plugin: "scheduling",
|
|
515
|
+
tool: "scheduling-gate",
|
|
516
|
+
args: { eventId: "e1", checks: [{ type: "d1", database: "unregistered", sql: "SELECT 1" }] },
|
|
517
|
+
};
|
|
518
|
+
describe("probeGateAtBindTime", () => {
|
|
519
|
+
it("runs the gate exactly once, and the gate writes nothing (Task 2663)", async () => {
|
|
520
|
+
// `commit` is deleted, so a bind-time probe cannot leave a mark behind for a
|
|
521
|
+
// row that does not exist yet: the gate has no write path at all.
|
|
522
|
+
await probeGateAtBindTime(D1_GATE, ACCOUNT_ID, "e1");
|
|
523
|
+
expect(gateRunCalls).toHaveLength(1);
|
|
524
|
+
expect(gateRunCalls[0]).not.toHaveProperty("commit");
|
|
525
|
+
});
|
|
526
|
+
it("passes the row's own id on update, so a watermarked check reads its real position", async () => {
|
|
527
|
+
await probeGateAtBindTime(D1_GATE, ACCOUNT_ID, "e1");
|
|
528
|
+
expect(gateRunCalls[0].eventId).toBe("e1");
|
|
529
|
+
});
|
|
530
|
+
it("passes no id on create, so checks read position zero", async () => {
|
|
531
|
+
await probeGateAtBindTime(D1_GATE, ACCOUNT_ID, null);
|
|
532
|
+
expect(gateRunCalls[0].eventId).toBe("");
|
|
533
|
+
});
|
|
534
|
+
it("returns null for a gate belonging to another tool, and probes nothing", async () => {
|
|
535
|
+
const probe = await probeGateAtBindTime({ plugin: "other", tool: "other-gate", args: { checks: [{ type: "d1" }] } }, ACCOUNT_ID, "e1");
|
|
536
|
+
expect(probe).toBeNull();
|
|
537
|
+
expect(gateRunCalls).toHaveLength(0);
|
|
538
|
+
});
|
|
539
|
+
it("reports every distinct kind the gate carries", async () => {
|
|
540
|
+
const probe = await probeGateAtBindTime({
|
|
541
|
+
plugin: "scheduling",
|
|
542
|
+
tool: "scheduling-gate",
|
|
543
|
+
args: {
|
|
544
|
+
checks: [
|
|
545
|
+
{ type: "d1", database: "db", sql: "SELECT 1" },
|
|
546
|
+
{ type: "d1", database: "db2", sql: "SELECT 1" },
|
|
547
|
+
{ type: "whatsapp", scope: "status" },
|
|
548
|
+
],
|
|
549
|
+
},
|
|
550
|
+
}, ACCOUNT_ID, "e1");
|
|
551
|
+
expect(probe?.kinds).toEqual(["d1", "whatsapp"]);
|
|
552
|
+
});
|
|
553
|
+
it("passes the errored list straight through", async () => {
|
|
554
|
+
gateErrored = [{ kind: "d1", detail: "HTTP 403" }];
|
|
555
|
+
const probe = await probeGateAtBindTime(D1_GATE, ACCOUNT_ID, "e1");
|
|
556
|
+
expect(probe?.errored).toEqual([{ kind: "d1", detail: "HTTP 403" }]);
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
describe("assertGateProbed", () => {
|
|
560
|
+
/** The lines the guard wrote, joined. */
|
|
561
|
+
function captured(fn) {
|
|
562
|
+
const lines = [];
|
|
563
|
+
const spy = vi.spyOn(console, "error").mockImplementation((...a) => {
|
|
564
|
+
lines.push(a.join(" "));
|
|
565
|
+
});
|
|
566
|
+
try {
|
|
567
|
+
fn();
|
|
568
|
+
}
|
|
569
|
+
finally {
|
|
570
|
+
spy.mockRestore();
|
|
571
|
+
}
|
|
572
|
+
return lines.join("\n");
|
|
573
|
+
}
|
|
574
|
+
it("does nothing at all when the gate was not probed", () => {
|
|
575
|
+
let threw = false;
|
|
576
|
+
const out = captured(() => {
|
|
577
|
+
try {
|
|
578
|
+
assertGateProbed(null, "e1");
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
threw = true;
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
expect(threw).toBe(false);
|
|
585
|
+
expect(out).toBe("");
|
|
586
|
+
});
|
|
587
|
+
it("accepts a gate whose checks all answered, and says so", () => {
|
|
588
|
+
const out = captured(() => assertGateProbed({ errored: [], kinds: ["d1", "mail"] }, "e1"));
|
|
589
|
+
expect(out).toContain("op=gate-accepted eventId=e1 route=probed checks=d1,mail");
|
|
590
|
+
});
|
|
591
|
+
it("refuses a gate whose check could not answer, carrying the source's own text", () => {
|
|
592
|
+
let message = "";
|
|
593
|
+
const out = captured(() => {
|
|
594
|
+
try {
|
|
595
|
+
assertGateProbed({
|
|
596
|
+
errored: [{ kind: "d1", detail: "HTTP 403 on POST /api/storage/d1/query" }],
|
|
597
|
+
kinds: ["d1"],
|
|
598
|
+
}, "e1");
|
|
599
|
+
}
|
|
600
|
+
catch (err) {
|
|
601
|
+
message = err instanceof Error ? err.message : String(err);
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
expect(message).toContain("HTTP 403 on POST /api/storage/d1/query");
|
|
605
|
+
expect(out).toContain("op=gate-refused eventId=e1 reason=check-unreachable checks=d1");
|
|
606
|
+
});
|
|
607
|
+
// The refusal names the kinds that FAILED, as `reason=unknown-check-type`
|
|
608
|
+
// names only the offending types: that is the set the author has to fix.
|
|
609
|
+
it("names only the kinds that errored in the refusal line", () => {
|
|
610
|
+
const out = captured(() => {
|
|
611
|
+
try {
|
|
612
|
+
assertGateProbed({ errored: [{ kind: "mail", detail: "getaddrinfo EAI_AGAIN" }], kinds: ["d1", "mail"] }, "e1");
|
|
613
|
+
}
|
|
614
|
+
catch {
|
|
615
|
+
/* asserted by the line, not the throw */
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
expect(out).toContain("reason=check-unreachable checks=mail");
|
|
619
|
+
});
|
|
620
|
+
it("writes '-' for the id on the create path, where none is minted yet", () => {
|
|
621
|
+
const out = captured(() => assertGateProbed({ errored: [], kinds: ["d1"] }, null));
|
|
622
|
+
expect(out).toContain("eventId=- route=probed");
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
// Both paths asserted separately. One path guarded is the drift
|
|
626
|
+
// `gateRequiresCommitRoute`'s docstring exists to prevent.
|
|
627
|
+
describe("unreachable gate — write paths", () => {
|
|
628
|
+
const createBase = {
|
|
629
|
+
accountId: ACCOUNT_ID,
|
|
630
|
+
name: "Quarter-hourly sweep",
|
|
631
|
+
startDate: "2026-08-03T08:00:00.000Z",
|
|
632
|
+
recurrence: "*/15 * * * *",
|
|
633
|
+
sessionKey: "sess-1",
|
|
634
|
+
createdBy: { agent: "admin", session: "sess-1", tool: "schedule-event" },
|
|
635
|
+
};
|
|
636
|
+
it("is refused on create, carrying the source's own text", async () => {
|
|
637
|
+
gateErrored = [{ kind: "d1", detail: "HTTP 403 on POST /api/storage/d1/query" }];
|
|
638
|
+
await expect(scheduleEvent({ ...createBase, agentDispatch: DISPATCH, gate: D1_GATE })).rejects.toThrow(/HTTP 403 on POST \/api\/storage\/d1\/query/);
|
|
639
|
+
expect(writeCalls).toHaveLength(0);
|
|
640
|
+
});
|
|
641
|
+
it("is refused on update, carrying the source's own text", async () => {
|
|
642
|
+
gateErrored = [{ kind: "d1", detail: "HTTP 403 on POST /api/storage/d1/query" }];
|
|
643
|
+
storedRow = { agentChannel: "whatsapp", agentDestination: PHONE, agentPrompt: "sweep" };
|
|
644
|
+
await expect(scheduleUpdate({ accountId: ACCOUNT_ID, eventId: "e1", gate: D1_GATE })).rejects.toThrow(/HTTP 403 on POST \/api\/storage\/d1\/query/);
|
|
645
|
+
expect(lastSet()?.query ?? "").not.toContain("e.gatePlugin");
|
|
646
|
+
});
|
|
647
|
+
// `hasWork` is NOT judged. A guard keying on it passes the false case and
|
|
648
|
+
// silently refuses every account that happens to be busy at bind time, so
|
|
649
|
+
// both answers are asserted and the two fixtures differ only in `hasWork`.
|
|
650
|
+
it("is stored when the checks answer 'work pending'", async () => {
|
|
651
|
+
gateErrored = [];
|
|
652
|
+
gateHasWork = true;
|
|
653
|
+
storedRow = { agentChannel: "whatsapp", agentDestination: PHONE, agentPrompt: "sweep" };
|
|
654
|
+
await scheduleUpdate({ accountId: ACCOUNT_ID, eventId: "e1", gate: D1_GATE });
|
|
655
|
+
expect(lastSet().query).toContain("e.gatePlugin");
|
|
656
|
+
});
|
|
657
|
+
it("is stored when the checks answer 'nothing pending'", async () => {
|
|
658
|
+
gateErrored = [];
|
|
659
|
+
gateHasWork = false;
|
|
660
|
+
storedRow = { agentChannel: "whatsapp", agentDestination: PHONE, agentPrompt: "sweep" };
|
|
661
|
+
await scheduleUpdate({ accountId: ACCOUNT_ID, eventId: "e1", gate: D1_GATE });
|
|
662
|
+
expect(lastSet().query).toContain("e.gatePlugin");
|
|
663
|
+
});
|
|
664
|
+
it("probes with the row's own id on update", async () => {
|
|
665
|
+
storedRow = { agentChannel: "whatsapp", agentDestination: PHONE, agentPrompt: "sweep" };
|
|
666
|
+
await scheduleUpdate({ accountId: ACCOUNT_ID, eventId: "e1", gate: D1_GATE });
|
|
667
|
+
expect(gateRunCalls[0].eventId).toBe("e1");
|
|
668
|
+
expect(gateRunCalls[0]).not.toHaveProperty("commit");
|
|
669
|
+
});
|
|
670
|
+
it("probes with no id on create", async () => {
|
|
671
|
+
await scheduleEvent({ ...createBase, agentDispatch: DISPATCH, gate: D1_GATE });
|
|
672
|
+
expect(gateRunCalls[0].eventId).toBe("");
|
|
673
|
+
expect(gateRunCalls[0]).not.toHaveProperty("commit");
|
|
674
|
+
});
|
|
675
|
+
// The ordering rule the 2471 refusal already states: a caller with no
|
|
676
|
+
// dispatch is told about the dispatch, not about a source it cannot reach.
|
|
677
|
+
it("tells a caller with no agent dispatch about the dispatch first", async () => {
|
|
678
|
+
gateErrored = [{ kind: "d1", detail: "HTTP 403" }];
|
|
679
|
+
await expect(scheduleEvent({ ...createBase, gate: D1_GATE })).rejects.toThrow(/requires a complete agentDispatch/);
|
|
680
|
+
expect(gateRunCalls).toHaveLength(0);
|
|
681
|
+
});
|
|
682
|
+
it("does not probe a gate belonging to another tool", async () => {
|
|
683
|
+
storedRow = { agentChannel: "whatsapp", agentDestination: PHONE, agentPrompt: "sweep" };
|
|
684
|
+
await scheduleUpdate({
|
|
685
|
+
accountId: ACCOUNT_ID,
|
|
686
|
+
eventId: "e1",
|
|
687
|
+
gate: { plugin: "other", tool: "other-gate", args: { checks: [{ type: "d1" }] } },
|
|
688
|
+
});
|
|
689
|
+
expect(gateRunCalls).toHaveLength(0);
|
|
690
|
+
expect(lastSet().query).toContain("e.gatePlugin");
|
|
691
|
+
});
|
|
692
|
+
});
|
|
629
693
|
//# sourceMappingURL=schedule-gate-write.test.js.map
|