@desplega.ai/agent-swarm 2.7.4 → 2.7.5
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/README.md +1 -1
- package/dist/acp-adapter-g3ads55s.js +15 -0
- package/dist/{actions-gfrr9vb7.js → actions-zwfykqqk.js} +18 -18
- package/dist/{amp-adapter-0s58gm46.js → amp-adapter-z3s0ssdt.js} +3 -3
- package/dist/{app-stvq692g.js → app-vmp4fgep.js} +3 -3
- package/dist/{assistant-5s37fr9n.js → assistant-vwde653f.js} +28 -21
- package/dist/{boot-reembed-yzy1841n.js → boot-reembed-375t7tbk.js} +1 -1
- package/dist/{boot-reembed-cv82gz7y.js → boot-reembed-gq9rcpnj.js} +1 -1
- package/dist/{boot-scrub-sweep-8k4q95ap.js → boot-scrub-sweep-nd938kyf.js} +4 -4
- package/dist/{boot-seeds-ftt9htah.js → boot-seeds-aykx51v3.js} +4 -4
- package/dist/{boxd-515v1t8z.js → boxd-k1j26g3h.js} +1 -1
- package/dist/{claude-adapter-t00wrzeh.js → claude-adapter-8j4hm099.js} +7 -7
- package/dist/{claude-managed-adapter-g6vmjccb.js → claude-managed-adapter-dvs2xw5n.js} +2 -2
- package/dist/{claude-managed-setup-afpa82f7.js → claude-managed-setup-0wyynpn1.js} +3 -3
- package/dist/{claude-sdk-session-vj0c2azx.js → claude-sdk-session-q6wj5hmy.js} +9 -9
- package/dist/{cli-wndv9p7g.js → cli-0731hvbw.js} +6 -6
- package/dist/{cli-d2pzqej1.js → cli-0bxqnvq9.js} +719 -7
- package/dist/{cli-jkt52mgt.js → cli-0gey5fzy.js} +2 -2
- package/dist/{cli-1yekbyqm.js → cli-1338796d.js} +205 -48
- package/dist/{cli-1s1dqn1j.js → cli-1ayk1eas.js} +171 -123
- package/dist/{cli-a4btqqft.js → cli-1b7btd38.js} +1 -1
- package/dist/{cli-a2jw6ee5.js → cli-1p89xny0.js} +1 -1
- package/dist/{cli-v726afdm.js → cli-2pnswgh8.js} +1 -1
- package/dist/{cli-kvtc7aez.js → cli-3h4nhjn8.js} +42 -14
- package/dist/{cli-655xm9ms.js → cli-583frbyr.js} +1 -1
- package/dist/{cli-4r12t6w6.js → cli-5c4hjzne.js} +101 -4
- package/dist/{cli-az0bpf8p.js → cli-5pag6rmn.js} +3 -3
- package/dist/{cli-36g23z21.js → cli-6s7vdcta.js} +4 -4
- package/dist/{cli-rtk9g7av.js → cli-73jzwksr.js} +6 -2
- package/dist/{cli-jg0nfda6.js → cli-75gkpjvw.js} +52 -21
- package/dist/{cli-a81qevyg.js → cli-7a42nfy0.js} +20 -36
- package/dist/{cli-0sbkss7h.js → cli-7wcyk2s5.js} +2 -2
- package/dist/{cli-41z8xwj0.js → cli-7wspm5gm.js} +14 -4
- package/dist/{cli-byqfbnjw.js → cli-82ae98pz.js} +9 -9
- package/dist/{cli-bq2pcphn.js → cli-8hs41zy2.js} +1 -1
- package/dist/{cli-vqgnyh76.js → cli-98rw5dt5.js} +15 -3
- package/dist/{cli-9cctx1tn.js → cli-a22h291q.js} +1 -1
- package/dist/{cli-j4kt1mpb.js → cli-a47xy8eq.js} +3 -3
- package/dist/{cli-ycm6180s.js → cli-a7v854rf.js} +5 -3
- package/dist/{cli-98pke4vk.js → cli-ae7mrdf9.js} +2 -2
- package/dist/{cli-ahd90bwt.js → cli-afrrppsw.js} +6 -6
- package/dist/{cli-8yy525ad.js → cli-bhjt02p8.js} +2 -2
- package/dist/{cli-v1yw3wha.js → cli-bnz7a7qf.js} +4 -4
- package/dist/{cli-j4kw3rsx.js → cli-cm5yqn35.js} +18 -0
- package/dist/{cli-h55y2bjc.js → cli-dhar9qay.js} +2 -2
- package/dist/{cli-5gr1g711.js → cli-fqzx7gwa.js} +2 -2
- package/dist/{cli-tzrpv5sc.js → cli-ftyxyspd.js} +1 -1
- package/dist/cli-fw2w0kva.js +47 -0
- package/dist/{cli-2dfbv8qr.js → cli-fy6x31ps.js} +1 -0
- package/dist/{cli-mvckssyq.js → cli-g61qxmge.js} +5 -5
- package/dist/{cli-aq58fk5m.js → cli-g83s9ewh.js} +7 -1
- package/dist/{cli-224epwwf.js → cli-hn9hxmz9.js} +3 -3
- package/dist/{cli-5hjced2v.js → cli-j3jvnpe8.js} +6 -6
- package/dist/{cli-dae4sbgy.js → cli-jm2vfpfd.js} +2 -2
- package/dist/{cli-zn15c46z.js → cli-mjzynrse.js} +1 -1
- package/dist/{cli-a1fhj4n9.js → cli-mmwm7zbf.js} +2 -2
- package/dist/{cli-cq1pf336.js → cli-mry0qzym.js} +5 -5
- package/dist/{cli-bwty35gx.js → cli-mthwvjre.js} +1 -1
- package/dist/{cli-s0exq74r.js → cli-nmbmy64z.js} +3 -3
- package/dist/{cli-wcmnan2g.js → cli-nn4d8dne.js} +5 -5
- package/dist/{cli-jg4p5t36.js → cli-nzxr556s.js} +121 -51
- package/dist/{cli-bcz074zx.js → cli-p599f1qm.js} +1 -1
- package/dist/{cli-k76tgq03.js → cli-pywf3eh5.js} +2 -2
- package/dist/{cli-wk9md5px.js → cli-qxnf8z2z.js} +2 -2
- package/dist/{cli-je8yeyc5.js → cli-rhw40jgb.js} +5 -5
- package/dist/{cli-avgcz4ra.js → cli-rm6hqj3z.js} +406 -35
- package/dist/{cli-0fgr1w9b.js → cli-rpm2sgv0.js} +2 -2
- package/dist/{cli-t282jgab.js → cli-rqkxrmgw.js} +12 -12
- package/dist/{cli-2qpvmmkw.js → cli-s0qnd4wv.js} +1 -1
- package/dist/{cli-pwyevs29.js → cli-spknvxbq.js} +22 -21
- package/dist/{cli-9y354g3j.js → cli-srye3vdt.js} +1 -1
- package/dist/{cli-pe09p6dv.js → cli-tqgg40ek.js} +1 -1
- package/dist/{cli-f255yaqn.js → cli-vbyfwh92.js} +3 -3
- package/dist/{cli-gcbdjt47.js → cli-vfym7wmz.js} +6 -6
- package/dist/{cli-edmm9d9y.js → cli-vqypxzh4.js} +7 -7
- package/dist/{cli-1gm8jcyr.js → cli-wvaawacd.js} +13 -13
- package/dist/{cli-eny54typ.js → cli-wx3rhnm0.js} +1 -1
- package/dist/{cli-b1afgtj3.js → cli-xbkdm710.js} +3 -3
- package/dist/{cli-mc97xy2s.js → cli-xtjhfmpc.js} +16 -4
- package/dist/{cli-eqx6k993.js → cli-y24sc29k.js} +25 -13
- package/dist/{cli-ktmvb20s.js → cli-z6jkx23q.js} +12 -12
- package/dist/{cli-7n5sbw0z.js → cli-z8eh3ex8.js} +5 -5
- package/dist/cli-zh3fp8v2.js +151 -0
- package/dist/{cli-13jvmew3.js → cli-zppwg9p9.js} +7 -7
- package/dist/cli.js +18 -18
- package/dist/{codex-adapter-g0gctw29.js → codex-adapter-q6k3gnyg.js} +5 -5
- package/dist/{codex-hook-vrzz2xhr.js → codex-hook-9efm2x1m.js} +1 -1
- package/dist/{codex-login-0gg46ygn.js → codex-login-y0pmff1c.js} +1 -1
- package/dist/{codex-session-runner-dr2cxe1c.js → codex-session-runner-3mwmk3d9.js} +6 -6
- package/dist/{commands-1p07cvff.js → commands-evrenjn9.js} +2 -2
- package/dist/{controller-z0s7sv6r.js → controller-xjp8vswh.js} +4 -4
- package/dist/{controllers-connect-x9kvvhyk.js → controllers-connect-pp5jnxvc.js} +20 -20
- package/dist/{cursor-adapter-m5b98pvk.js → cursor-adapter-yf47w4t0.js} +2 -2
- package/dist/{db-rsh04kh3.js → db-rgfsh91f.js} +2 -2
- package/dist/{db-capk2raj.js → db-zxpw6aht.js} +3 -3
- package/dist/{devin-adapter-yxm87fe9.js → devin-adapter-r9phbnc9.js} +1 -1
- package/dist/{dispatcher-304q6fwa.js → dispatcher-smasgws7.js} +17 -17
- package/dist/{docker-7rxt0rry.js → docker-rrjhaqt9.js} +2 -2
- package/dist/{dsh-adapter-btewem0y.js → dsh-adapter-ftyy8dnp.js} +2 -2
- package/dist/{e2b-renc0k8q.js → e2b-qcaj675y.js} +2 -2
- package/dist/{e2b-7fc9b9bd.js → e2b-w1p0bnpm.js} +2 -2
- package/dist/{easter-egg-3d-fm1p3n4d.js → easter-egg-3d-yyts7p16.js} +1 -1
- package/dist/{embedding-space-s68v49ct.js → embedding-space-1z0mbg95.js} +6 -6
- package/dist/{embeddings-4ms3604w.js → embeddings-ygeg7tm7.js} +1 -1
- package/dist/{event-bus-dfjx2qdh.js → event-bus-g0ka3pbw.js} +1 -1
- package/dist/{execute-mcdgccjs.js → execute-14qhqp4d.js} +1 -1
- package/dist/{external-channel-9dxwfztn.js → external-channel-ttgrs3sx.js} +2 -2
- package/dist/fx-adapter-gt6pncj2.js +18 -0
- package/dist/{grok-adapter-05ecv4h0.js → grok-adapter-sm4m5k4g.js} +8 -8
- package/dist/handlers-vwmba1m2.js +31 -0
- package/dist/{heartbeat-3n94heh3.js → heartbeat-qahz4p62.js} +20 -20
- package/dist/{hook-zxwje86c.js → hook-4fd5wq5w.js} +4 -4
- package/dist/{hook-aadrn69x.js → hook-kj3d398m.js} +6 -6
- package/dist/{http-xzxk9mst.js → http-1ted0j1k.js} +100 -64
- package/dist/index-5r71rf98.js +32 -0
- package/dist/{index-d34f6r87.js → index-6rq3r5zb.js} +1 -1
- package/dist/{index-9j4zwcht.js → index-7v51awbn.js} +158 -672
- package/dist/index-anmwxs87.js +29 -0
- package/dist/{index-qeb82z37.js → index-q4b917zs.js} +1 -1
- package/dist/index-spp0ra5c.js +36 -0
- package/dist/{keepalive-w4aakbyk.js → keepalive-6cf311t5.js} +4 -4
- package/dist/lead-fjy418cw.js +69 -0
- package/dist/{maintenance-hbptpk6p.js → maintenance-a4bm1bmt.js} +1 -1
- package/dist/{oauth-refresh-sweep-217semrv.js → oauth-refresh-sweep-3h0dfw1k.js} +3 -3
- package/dist/{onboard-vz2s5df3.js → onboard-myn5hraa.js} +3 -3
- package/dist/{opencode-adapter-gtfd7d7v.js → opencode-adapter-p2nkb5am.js} +5 -5
- package/dist/{otel-impl-aefave37.js → otel-impl-a1a0ef6b.js} +2 -2
- package/dist/{pi-mono-adapter-f8h5yseh.js → pi-mono-adapter-rzmpb72a.js} +117 -23
- package/dist/{pricing-refresh-m1yhh9ya.js → pricing-refresh-83drrnxj.js} +3 -3
- package/dist/{rbac-roles-svj72cdf.js → rbac-roles-0fb6e7pj.js} +2 -2
- package/dist/{rbac-roles-rrw8twdz.js → rbac-roles-s8y6ecb5.js} +1 -1
- package/dist/{render-v2-amwbnfxh.js → render-v2-twmbgrz4.js} +3 -3
- package/dist/{resume-9cd6wf6z.js → resume-zfqc36xj.js} +7 -7
- package/dist/{runtime-fp4vrx7m.js → runtime-qwggdpqr.js} +1 -1
- package/dist/{session-access-21t6yjyg.js → session-access-sc9pn9da.js} +4 -4
- package/dist/{session-source-backfill-pxbzcsc6.js → session-source-backfill-qh07xr8g.js} +3 -3
- package/dist/{setup-4592tqz4.js → setup-4fr0m7a1.js} +1 -1
- package/dist/{virtual-modules-gzfnpmnz.js → virtual-modules-fh4ha15b.js} +2 -2
- package/dist/{work-objects-t9tcf174.js → work-objects-0wr8zxct.js} +20 -20
- package/dist/worker-0c5tyzqe.js +69 -0
- package/dist/{worker-follow-up-q32jh4yk.js → worker-follow-up-ktyyazyp.js} +17 -17
- package/dist/{x-b6wxntdt.js → x-vmmye1kb.js} +2 -2
- package/openapi.json +97 -32
- package/package.json +1 -1
- package/src/be/db/session-summaries.ts +8 -2
- package/src/be/db/session-tree.ts +174 -61
- package/src/be/db/session-visibility.ts +5 -4
- package/src/be/db.ts +8 -4
- package/src/be/migrations/259_session_summaries_open_tasks.sql +111 -0
- package/src/be/model-validation.ts +5 -1
- package/src/be/pricing-normalize.ts +7 -2
- package/src/be/scripts/typecheck.ts +1 -0
- package/src/be/seed/schedules-seeder.ts +135 -9
- package/src/be/seed/workflows-seeder.ts +65 -1
- package/src/be/seed-pricing.ts +23 -0
- package/src/be/steering.ts +50 -3
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/commands/provider-credentials.ts +29 -2
- package/src/commands/runner.ts +86 -5
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +27 -1
- package/src/extensions/dispatcher.ts +4 -0
- package/src/http/agents.ts +1 -0
- package/src/http/mcp-bridge.ts +4 -0
- package/src/integrations/chat-sdk/inbound.ts +15 -5
- package/src/integrations/chat-sdk/steering.ts +3 -1
- package/src/kv-reserved-namespaces.ts +2 -0
- package/src/prompts/base-prompt.ts +4 -1
- package/src/prompts/session-templates.ts +26 -0
- package/src/providers/acp-adapter.ts +190 -26
- package/src/providers/acp-targets.ts +306 -6
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +9 -44
- package/src/providers/codex-oauth/flow.ts +24 -3
- package/src/providers/codex-oauth/storage.ts +56 -17
- package/src/providers/codex-oauth/subscription.ts +67 -0
- package/src/providers/fx-adapter.ts +211 -0
- package/src/providers/index.ts +5 -1
- package/src/providers/mcp-loopback-proxy.ts +94 -0
- package/src/providers/pi-mono-adapter.ts +98 -13
- package/src/providers/pricing-sources.md +8 -0
- package/src/providers/reasoning-effort.ts +17 -1
- package/src/providers/types.ts +2 -1
- package/src/scheduler/scheduler.ts +36 -3
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +5 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +33 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +5 -0
- package/src/server.ts +7 -0
- package/src/slack/assistant.ts +17 -1
- package/src/slack/handlers.ts +18 -5
- package/src/slack/left-threads.ts +65 -0
- package/src/slack/steering.ts +3 -0
- package/src/slack/thread-buffer.ts +40 -16
- package/src/tests/acp-adapter.test.ts +79 -0
- package/src/tests/base-prompt.test.ts +12 -0
- package/src/tests/chat-sdk-inbound.test.ts +30 -0
- package/src/tests/chatgpt-subscription-model-groups.test.ts +62 -0
- package/src/tests/chatgpt-subscription-pi.test.ts +170 -0
- package/src/tests/chatgpt-subscription-session-bench.test.ts +198 -0
- package/src/tests/codex-oauth-credential-info.test.ts +40 -0
- package/src/tests/codex-oauth-refresh-egress.test.ts +194 -0
- package/src/tests/context-window.test.ts +7 -0
- package/src/tests/explicit-model-validation.test.ts +99 -0
- package/src/tests/extensions-run-context.test.ts +11 -0
- package/src/tests/extensions-steering.test.ts +353 -0
- package/src/tests/extensions-validate.test.ts +5 -0
- package/src/tests/fixtures/extensions/steering-policy/hooks.ts +39 -0
- package/src/tests/fixtures/extensions/steering-policy/manifest.json +7 -0
- package/src/tests/fixtures/fake-fx.ts +167 -0
- package/src/tests/fixtures/fx/acp-responses-recorded.json +157 -0
- package/src/tests/fixtures/fx/acp-updates-recorded.jsonl +15 -0
- package/src/tests/fx-adapter.test.ts +771 -0
- package/src/tests/grok-adapter.test.ts +36 -0
- package/src/tests/mcp-loopback-proxy.test.ts +193 -0
- package/src/tests/model-tier-resolution.test.ts +3 -3
- package/src/tests/pricing-refresh.test.ts +5 -5
- package/src/tests/prompt-template-session.test.ts +5 -3
- package/src/tests/provider-registration.test.ts +2 -1
- package/src/tests/query-audit-plans.test.ts +3 -2
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/seed-model-profiles.test.ts +2 -2
- package/src/tests/seed-schedules.test.ts +138 -0
- package/src/tests/session-cost-recompute-unit.test.ts +39 -0
- package/src/tests/session-costs-golden.test.ts +26 -0
- package/src/tests/session-summaries-plans.test.ts +120 -0
- package/src/tests/session-summaries.test.ts +72 -12
- package/src/tests/sessions-tree.test.ts +11 -3
- package/src/tests/slack-leave-thread.test.ts +431 -0
- package/src/tests/tool-annotations.test.ts +2 -1
- package/src/tests/workflow-engine-v2.test.ts +42 -0
- package/src/tests/workflow-script-principal.test.ts +207 -7
- package/src/tools/slack-leave-thread.ts +104 -0
- package/src/tools/steer-task.ts +3 -2
- package/src/tools/tool-config.ts +2 -1
- package/src/types.ts +19 -0
- package/src/utils/codex-auth-failure.ts +30 -0
- package/src/utils/context-window.ts +4 -1
- package/src/utils/credentials.ts +7 -0
- package/src/utils/fx-models.ts +43 -0
- package/src/workflows/cooldown.ts +3 -2
- package/src/workflows/engine.ts +24 -9
- package/templates/schedules/dreaming-daily/config.json +1 -1
- package/templates/schedules/dreaming-daily/content.md +6 -14
- package/templates/skills/swarm-docs/content.md +1 -0
- package/templates/workflows/dreaming/content.md +5 -1
- package/dist/acp-adapter-96dc64m2.js +0 -15
- package/dist/handlers-jg7v74ty.js +0 -31
- package/dist/index-8c1zbz59.js +0 -29
- package/dist/index-8k8tmfxd.js +0 -32
- package/dist/index-hq617xdj.js +0 -36
- package/dist/lead-567kw7x3.js +0 -68
- package/dist/worker-fjdqra6q.js +0 -68
|
@@ -145,6 +145,8 @@ class FakeGrok {
|
|
|
145
145
|
}
|
|
146
146
|
async newSession(params) {
|
|
147
147
|
capture.newSession = { meta: params._meta ?? null, mcpServers: params.mcpServers.map((s) => s.name) };
|
|
148
|
+
capture.newSessionKeys = Object.keys(params).sort();
|
|
149
|
+
capture.mcpUrls = params.mcpServers.map((s) => s.url);
|
|
148
150
|
writeFileSync("capture.json", JSON.stringify(capture));
|
|
149
151
|
if (${JSON.stringify(mode)} === "auth-required") throw RequestError.authRequired();
|
|
150
152
|
return { sessionId: "grok-session-1" };
|
|
@@ -425,6 +427,40 @@ describe("GrokAdapter", () => {
|
|
|
425
427
|
}
|
|
426
428
|
});
|
|
427
429
|
|
|
430
|
+
test("the opt-in ACP hooks leave grok's session/new payload, MCP URL and steering unchanged", async () => {
|
|
431
|
+
const dir = makeTempDir();
|
|
432
|
+
const grok = writeFakeGrok(dir, "ok");
|
|
433
|
+
const { server, apiUrl } = await startTokenStub();
|
|
434
|
+
// Reachable but plain-HTTP and non-loopback: a target with requiresSecureMcpUrl would proxy it.
|
|
435
|
+
const publicStyleUrl = apiUrl.replace("127.0.0.1", "0.0.0.0");
|
|
436
|
+
try {
|
|
437
|
+
const config = baseConfig({
|
|
438
|
+
apiUrl: publicStyleUrl,
|
|
439
|
+
resumeSessionId: "ignored-without-a-resume-hook",
|
|
440
|
+
env: {
|
|
441
|
+
PATH: process.env.PATH ?? "",
|
|
442
|
+
HOME: makeTempDir(),
|
|
443
|
+
GROK_BINARY: grok,
|
|
444
|
+
XAI_API_KEY: "xai-test-key",
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
const adapter = new GrokAdapter();
|
|
448
|
+
const session = await adapter.createSession(config);
|
|
449
|
+
await session.waitForCompletion();
|
|
450
|
+
expect(session.deliverSteering).toBeUndefined();
|
|
451
|
+
expect(await adapter.canResume("grok-session-1")).toBe(false);
|
|
452
|
+
const capture = JSON.parse(await Bun.file(join(config.cwd, "capture.json")).text());
|
|
453
|
+
expect(capture.newSession).toEqual({
|
|
454
|
+
meta: { rules: "You are a swarm worker.", yoloMode: true },
|
|
455
|
+
mcpServers: ["swarm"],
|
|
456
|
+
});
|
|
457
|
+
expect(capture.newSessionKeys).toEqual(["_meta", "cwd", "mcpServers"]);
|
|
458
|
+
expect(capture.mcpUrls).toEqual([`${publicStyleUrl}/mcp`]);
|
|
459
|
+
} finally {
|
|
460
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
428
464
|
test("runs an openrouter/ model as an OpenAI-compatible model on OPENROUTER_API_KEY", async () => {
|
|
429
465
|
const dir = makeTempDir();
|
|
430
466
|
// A BYOK route reports the endpoint's id and no ticks.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { secureMcpServers } from "../providers/acp-adapter";
|
|
3
|
+
import {
|
|
4
|
+
type McpLoopbackProxy,
|
|
5
|
+
needsLoopbackProxy,
|
|
6
|
+
startMcpLoopbackProxy,
|
|
7
|
+
} from "../providers/mcp-loopback-proxy";
|
|
8
|
+
import { getFreePort } from "./test-net";
|
|
9
|
+
|
|
10
|
+
type Upstream = ReturnType<typeof Bun.serve>;
|
|
11
|
+
|
|
12
|
+
const cleanups: Array<() => Promise<void> | void> = [];
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
for (const cleanup of cleanups.splice(0).reverse()) await cleanup();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function track(proxy: McpLoopbackProxy): McpLoopbackProxy {
|
|
19
|
+
cleanups.push(() => proxy.stop());
|
|
20
|
+
return proxy;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A stand-in MCP endpoint: JSON echo, a slow SSE stream and a redirect. */
|
|
24
|
+
function startUpstream(): Upstream {
|
|
25
|
+
const server = Bun.serve({
|
|
26
|
+
hostname: "127.0.0.1",
|
|
27
|
+
port: 0,
|
|
28
|
+
idleTimeout: 0,
|
|
29
|
+
async fetch(req) {
|
|
30
|
+
const url = new URL(req.url);
|
|
31
|
+
if (url.pathname === "/prefix/mcp" && req.method === "POST") {
|
|
32
|
+
return Response.json({
|
|
33
|
+
path: url.pathname,
|
|
34
|
+
search: url.search,
|
|
35
|
+
method: req.method,
|
|
36
|
+
body: await req.json(),
|
|
37
|
+
authorization: req.headers.get("authorization"),
|
|
38
|
+
agentId: req.headers.get("x-agent-id"),
|
|
39
|
+
host: req.headers.get("host"),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (url.pathname === "/prefix/sse") {
|
|
43
|
+
const encoder = new TextEncoder();
|
|
44
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
45
|
+
async start(controller) {
|
|
46
|
+
for (const n of [1, 2, 3]) {
|
|
47
|
+
controller.enqueue(encoder.encode(`event: message\ndata: {"n":${n}}\n\n`));
|
|
48
|
+
await Bun.sleep(30);
|
|
49
|
+
}
|
|
50
|
+
controller.close();
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
return new Response(stream, { headers: { "Content-Type": "text/event-stream" } });
|
|
54
|
+
}
|
|
55
|
+
if (url.pathname === "/prefix/redirect") {
|
|
56
|
+
return new Response(null, { status: 302, headers: { Location: "https://example.test/" } });
|
|
57
|
+
}
|
|
58
|
+
return new Response("not found", { status: 404 });
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
cleanups.push(() => server.stop(true));
|
|
62
|
+
return server;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe("needsLoopbackProxy", () => {
|
|
66
|
+
test("only plain HTTP to a non-loopback host needs the forwarder", () => {
|
|
67
|
+
expect(needsLoopbackProxy("http://api:3013/mcp")).toBe(true);
|
|
68
|
+
expect(needsLoopbackProxy("http://10.0.0.5/mcp")).toBe(true);
|
|
69
|
+
// fx accepts only these loopback names; 127.0.0.2 is rejected (fx 0.0.15).
|
|
70
|
+
expect(needsLoopbackProxy("http://127.0.0.2:3013/mcp")).toBe(true);
|
|
71
|
+
expect(needsLoopbackProxy("http://127.0.0.1:3013/mcp")).toBe(false);
|
|
72
|
+
expect(needsLoopbackProxy("http://localhost:3013/mcp")).toBe(false);
|
|
73
|
+
expect(needsLoopbackProxy("http://[::1]:3013/mcp")).toBe(false);
|
|
74
|
+
expect(needsLoopbackProxy("https://swarm.example/mcp")).toBe(false);
|
|
75
|
+
expect(needsLoopbackProxy("not a url")).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("startMcpLoopbackProxy", () => {
|
|
80
|
+
test("round-trips a JSON POST with its headers, under the upstream path prefix", async () => {
|
|
81
|
+
const upstream = startUpstream();
|
|
82
|
+
const proxy = track(await startMcpLoopbackProxy(`http://127.0.0.1:${upstream.port}/prefix/`));
|
|
83
|
+
expect(proxy.url).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/);
|
|
84
|
+
|
|
85
|
+
const response = await fetch(`${proxy.url}/mcp?session=1`, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: {
|
|
88
|
+
Authorization: "Bearer aseph_proxytest",
|
|
89
|
+
"X-Agent-ID": "agent-1",
|
|
90
|
+
"Content-Type": "application/json",
|
|
91
|
+
},
|
|
92
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),
|
|
93
|
+
});
|
|
94
|
+
expect(response.status).toBe(200);
|
|
95
|
+
expect(await response.json()).toEqual({
|
|
96
|
+
path: "/prefix/mcp",
|
|
97
|
+
search: "?session=1",
|
|
98
|
+
method: "POST",
|
|
99
|
+
body: { jsonrpc: "2.0", id: 1, method: "tools/list" },
|
|
100
|
+
authorization: "Bearer aseph_proxytest",
|
|
101
|
+
agentId: "agent-1",
|
|
102
|
+
// The upstream sees its own host, not the forwarder's.
|
|
103
|
+
host: `127.0.0.1:${upstream.port}`,
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("streams SSE events in order as they arrive", async () => {
|
|
108
|
+
const upstream = startUpstream();
|
|
109
|
+
const proxy = track(await startMcpLoopbackProxy(`http://127.0.0.1:${upstream.port}/prefix`));
|
|
110
|
+
const response = await fetch(`${proxy.url}/sse`);
|
|
111
|
+
expect(response.headers.get("content-type")).toContain("text/event-stream");
|
|
112
|
+
const reader = response.body!.getReader();
|
|
113
|
+
const decoder = new TextDecoder();
|
|
114
|
+
let text = "";
|
|
115
|
+
let reads = 0;
|
|
116
|
+
while (true) {
|
|
117
|
+
const { done, value } = await reader.read();
|
|
118
|
+
if (done) break;
|
|
119
|
+
reads += 1;
|
|
120
|
+
text += decoder.decode(value, { stream: true });
|
|
121
|
+
}
|
|
122
|
+
expect([...text.matchAll(/"n":(\d)/g)].map((m) => m[1])).toEqual(["1", "2", "3"]);
|
|
123
|
+
// Delivered incrementally, not buffered into one body.
|
|
124
|
+
expect(reads).toBeGreaterThan(1);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("passes a redirect through instead of following it", async () => {
|
|
128
|
+
const upstream = startUpstream();
|
|
129
|
+
const proxy = track(await startMcpLoopbackProxy(`http://127.0.0.1:${upstream.port}/prefix`));
|
|
130
|
+
const response = await fetch(`${proxy.url}/redirect`, { redirect: "manual" });
|
|
131
|
+
expect(response.status).toBe(302);
|
|
132
|
+
expect(response.headers.get("location")).toBe("https://example.test/");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("answers 502 when the upstream is down", async () => {
|
|
136
|
+
const port = await getFreePort();
|
|
137
|
+
const proxy = track(await startMcpLoopbackProxy(`http://127.0.0.1:${port}`));
|
|
138
|
+
const response = await fetch(`${proxy.url}/mcp`, { method: "POST", body: "{}" });
|
|
139
|
+
expect(response.status).toBe(502);
|
|
140
|
+
expect(((await response.json()) as { error: string }).error).toContain(
|
|
141
|
+
"MCP loopback proxy: upstream failed",
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("stop() frees the port and is idempotent", async () => {
|
|
146
|
+
const upstream = startUpstream();
|
|
147
|
+
const proxy = await startMcpLoopbackProxy(`http://127.0.0.1:${upstream.port}/prefix`);
|
|
148
|
+
await proxy.stop();
|
|
149
|
+
await proxy.stop();
|
|
150
|
+
await expect(fetch(`${proxy.url}/sse`)).rejects.toThrow();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("secureMcpServers", () => {
|
|
155
|
+
test("forwards plain-HTTP non-loopback servers, one forwarder per origin, and drops SSE", async () => {
|
|
156
|
+
const proxies: McpLoopbackProxy[] = [];
|
|
157
|
+
cleanups.push(async () => {
|
|
158
|
+
for (const proxy of proxies) await proxy.stop();
|
|
159
|
+
});
|
|
160
|
+
const servers = await secureMcpServers(
|
|
161
|
+
[
|
|
162
|
+
{ type: "http", name: "swarm", url: "http://api:3013/mcp", headers: [] },
|
|
163
|
+
{ type: "http", name: "same-origin", url: "http://api:3013/other?x=1", headers: [] },
|
|
164
|
+
{ type: "http", name: "secure", url: "https://mcp.example/mcp", headers: [] },
|
|
165
|
+
{ type: "http", name: "local", url: "http://127.0.0.1:9/mcp", headers: [] },
|
|
166
|
+
{ type: "sse", name: "legacy", url: "https://sse.example/sse", headers: [] },
|
|
167
|
+
{ name: "stdio", command: "true", args: [], env: [] },
|
|
168
|
+
],
|
|
169
|
+
false,
|
|
170
|
+
proxies,
|
|
171
|
+
);
|
|
172
|
+
expect(proxies).toHaveLength(1);
|
|
173
|
+
const forwarded = proxies[0]!.url;
|
|
174
|
+
expect(servers.map((server) => [server.name, "url" in server ? server.url : null])).toEqual([
|
|
175
|
+
["swarm", `${forwarded}/mcp`],
|
|
176
|
+
["same-origin", `${forwarded}/other?x=1`],
|
|
177
|
+
["secure", "https://mcp.example/mcp"],
|
|
178
|
+
["local", "http://127.0.0.1:9/mcp"],
|
|
179
|
+
["stdio", null],
|
|
180
|
+
]);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("keeps SSE servers when the agent supports them", async () => {
|
|
184
|
+
const proxies: McpLoopbackProxy[] = [];
|
|
185
|
+
const servers = await secureMcpServers(
|
|
186
|
+
[{ type: "sse", name: "legacy", url: "https://sse.example/sse", headers: [] }],
|
|
187
|
+
true,
|
|
188
|
+
proxies,
|
|
189
|
+
);
|
|
190
|
+
expect(servers.map((server) => server.name)).toEqual(["legacy"]);
|
|
191
|
+
expect(proxies).toEqual([]);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
@@ -674,8 +674,8 @@ describe("previewModelTiers", () => {
|
|
|
674
674
|
|
|
675
675
|
// acp has no portable tier mapping, so it has nothing to preview.
|
|
676
676
|
expect(rows.some((r) => r.provider === "acp")).toBe(false);
|
|
677
|
-
expect(new Set(rows.map((r) => r.provider)).size).toBe(
|
|
678
|
-
expect(rows).toHaveLength(
|
|
677
|
+
expect(new Set(rows.map((r) => r.provider)).size).toBe(11);
|
|
678
|
+
expect(rows).toHaveLength(44);
|
|
679
679
|
});
|
|
680
680
|
|
|
681
681
|
test("an alias that resolves to nothing reports the default it falls back to", async () => {
|
|
@@ -730,7 +730,7 @@ describe("GET /api/models-catalog/tiers", () => {
|
|
|
730
730
|
expect(handled).toBe(true);
|
|
731
731
|
expect(status).toBe(200);
|
|
732
732
|
const body = JSON.parse(bodyStr) as { tiers: { provider: string; tier: string }[] };
|
|
733
|
-
expect(body.tiers).toHaveLength(
|
|
733
|
+
expect(body.tiers).toHaveLength(44);
|
|
734
734
|
expect(body.tiers.find((t) => t.provider === "claude" && t.tier === "smart")).toMatchObject({
|
|
735
735
|
key: "MODEL_TIER_CLAUDE_SMART",
|
|
736
736
|
source: "tier-config",
|
|
@@ -102,14 +102,14 @@ describe("models.dev runtime pricing refresh", () => {
|
|
|
102
102
|
VALUES ('codex', 'gpt-refresh-test', 'input', 0, 1, 0, 0)`,
|
|
103
103
|
);
|
|
104
104
|
|
|
105
|
-
// codex, pi, cursor and
|
|
105
|
+
// codex, pi, cursor, amp and fx each project the OpenAI rows: 5 providers x 2 token classes.
|
|
106
106
|
const first = await refreshPricingFromModelsDev({
|
|
107
107
|
now: 1_000,
|
|
108
108
|
fetchImpl: async () => responseFor(openAiCache(2, 8), '"etag-1"'),
|
|
109
109
|
});
|
|
110
110
|
expect(first.status).toBe("refreshed");
|
|
111
|
-
expect(first.candidateRows).toBe(
|
|
112
|
-
expect(first.inserted).toBe(
|
|
111
|
+
expect(first.candidateRows).toBe(10);
|
|
112
|
+
expect(first.inserted).toBe(10);
|
|
113
113
|
expect(first.unchanged).toBe(0);
|
|
114
114
|
|
|
115
115
|
const activeChanged = await getActivePricingRow("codex", "gpt-refresh-test", "input", 1_000);
|
|
@@ -121,7 +121,7 @@ describe("models.dev runtime pricing refresh", () => {
|
|
|
121
121
|
fetchImpl: async () => responseFor(openAiCache(2, 8), '"etag-2"'),
|
|
122
122
|
});
|
|
123
123
|
expect(second.inserted).toBe(0);
|
|
124
|
-
expect(second.unchanged).toBe(
|
|
124
|
+
expect(second.unchanged).toBe(10);
|
|
125
125
|
|
|
126
126
|
const rows = await getDbClient().query<{ effective_from: number }>(
|
|
127
127
|
`SELECT effective_from FROM pricing
|
|
@@ -278,7 +278,7 @@ describe("models.dev runtime pricing refresh", () => {
|
|
|
278
278
|
|
|
279
279
|
const logs = await getLogsByEventType("pricing.refresh");
|
|
280
280
|
expect(logs).toHaveLength(1);
|
|
281
|
-
expect(logs[0]?.newValue).toContain("inserted=
|
|
281
|
+
expect(logs[0]?.newValue).toContain("inserted=10");
|
|
282
282
|
expect(logs[0]?.metadata).toContain('"etag":"\\"etag-log\\""');
|
|
283
283
|
});
|
|
284
284
|
});
|
|
@@ -24,6 +24,7 @@ const SYSTEM_TEMPLATES = [
|
|
|
24
24
|
"system.agent.compaction.goal_reminder",
|
|
25
25
|
"system.agent.context.bound_note",
|
|
26
26
|
"system.agent.cursor.first_message",
|
|
27
|
+
"system.agent.fx.system_prompt_file",
|
|
27
28
|
"system.agent.lead",
|
|
28
29
|
"system.agent.lead.docs",
|
|
29
30
|
"system.agent.memory",
|
|
@@ -42,6 +43,7 @@ const SYSTEM_TEMPLATES = [
|
|
|
42
43
|
"system.agent.tool_discovery.search",
|
|
43
44
|
"system.agent.tool_discovery.amp",
|
|
44
45
|
"system.agent.tool_discovery.codex",
|
|
46
|
+
"system.agent.tool_discovery.fx",
|
|
45
47
|
"system.agent.tool_discovery.opencode",
|
|
46
48
|
"system.agent.tool_discovery.direct",
|
|
47
49
|
"system.agent.tools_skills",
|
|
@@ -121,11 +123,11 @@ describe("Session templates: registration", () => {
|
|
|
121
123
|
expect(sessionSystemEventTypes("session")).toEqual([...SESSION_TEMPLATES].sort());
|
|
122
124
|
});
|
|
123
125
|
|
|
124
|
-
test("registers
|
|
126
|
+
test("registers 37 system and session templates in total", () => {
|
|
125
127
|
const all = getAllTemplateDefinitions();
|
|
126
128
|
const sessionSystem = all.filter((d) => d.category === "system" || d.category === "session");
|
|
127
|
-
//
|
|
128
|
-
expect(sessionSystem.length).toBe(
|
|
129
|
+
// 32 system blocks + 5 session composites.
|
|
130
|
+
expect(sessionSystem.length).toBe(37);
|
|
129
131
|
});
|
|
130
132
|
|
|
131
133
|
test("drops the v1 blocks that prompt v2 deleted", () => {
|
|
@@ -235,6 +235,7 @@ const BOOTSTRAP: Record<ProviderName, { expect: string } | { exempt: string }> =
|
|
|
235
235
|
amp: { expect: "Warning: amp provider has no credentials yet" },
|
|
236
236
|
cursor: { expect: "Warning: cursor provider has no credentials yet" },
|
|
237
237
|
grok: { expect: "Warning: grok provider has no credentials yet" },
|
|
238
|
+
fx: { expect: "Warning: fx provider has no credentials yet" },
|
|
238
239
|
pi: { expect: "Warning: pi provider has no credentials yet" },
|
|
239
240
|
opencode: { expect: "Warning: opencode provider has no credentials yet" },
|
|
240
241
|
"claude-managed": { expect: "Warning: claude-managed provider missing:" },
|
|
@@ -284,7 +285,7 @@ function extractMarkedBlock(name: string): string {
|
|
|
284
285
|
/** Resolvable no-op stubs for every binary the verify chain may look up. */
|
|
285
286
|
function makeBinaryStubDir(): string {
|
|
286
287
|
const dir = mkdtempSync(join(tmpdir(), "provider-registration-bin-"));
|
|
287
|
-
for (const name of ["claude", "codex", "opencode", "dsh", "amp", "grok"]) {
|
|
288
|
+
for (const name of ["claude", "codex", "opencode", "dsh", "amp", "grok", "fx"]) {
|
|
288
289
|
const file = join(dir, name);
|
|
289
290
|
writeFileSync(file, "#!/bin/sh\nexit 0\n");
|
|
290
291
|
chmodSync(file, 0o755);
|
|
@@ -152,8 +152,9 @@ const CASES: Array<{
|
|
|
152
152
|
name: "countSessions by source",
|
|
153
153
|
match: /COUNT/,
|
|
154
154
|
run: () => countSessions({ source: ["slack"] }),
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
// Counts read session_summaries since migration 259.
|
|
156
|
+
uses: ["COVERING INDEX idx_session_summaries_source (source=?)"],
|
|
157
|
+
revert: { dropIndex: "idx_session_summaries_source" },
|
|
157
158
|
},
|
|
158
159
|
{
|
|
159
160
|
// Few hits: the page reads the agent_tasks_fts candidates by rowid. The
|
|
@@ -665,6 +665,8 @@ describe("script connections", () => {
|
|
|
665
665
|
markMigrationApplied(database, "254_query_audit_indexes.sql");
|
|
666
666
|
// 256 alters agents, which this migration-112-only fixture does not create.
|
|
667
667
|
markMigrationApplied(database, "256_agent_slack_icon.sql");
|
|
668
|
+
// 259 alters session_summaries, which 253 (skipped above) creates.
|
|
669
|
+
markMigrationApplied(database, "259_session_summaries_open_tasks.sql");
|
|
668
670
|
|
|
669
671
|
const id = crypto.randomUUID();
|
|
670
672
|
const now = new Date().toISOString();
|
|
@@ -62,9 +62,9 @@ beforeEach(async () => {
|
|
|
62
62
|
|
|
63
63
|
describe("tier seeds", () => {
|
|
64
64
|
test("one profile per harness and tier, copied from the tier map; none for acp", async () => {
|
|
65
|
-
expect(await seedModelProfiles()).toBe(
|
|
65
|
+
expect(await seedModelProfiles()).toBe(44);
|
|
66
66
|
const profiles = await listModelProfiles();
|
|
67
|
-
expect(profiles).toHaveLength(
|
|
67
|
+
expect(profiles).toHaveLength(44);
|
|
68
68
|
expect(profiles.some((p) => p.harness === "acp")).toBe(false);
|
|
69
69
|
for (const profile of profiles) {
|
|
70
70
|
const tier = profile.seededFrom?.replace("tier:", "") as (typeof MODEL_TIERS)[number];
|
|
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { unlink } from "node:fs/promises";
|
|
3
3
|
import {
|
|
4
4
|
closeDb,
|
|
5
|
+
createWorkflow,
|
|
5
6
|
getScheduledTaskByName,
|
|
6
7
|
getScheduledTasks,
|
|
7
8
|
initDb,
|
|
@@ -13,6 +14,7 @@ import {
|
|
|
13
14
|
loadSeedSchedules,
|
|
14
15
|
type ScheduleTemplateSource,
|
|
15
16
|
} from "../be/seed/schedules-seeder";
|
|
17
|
+
import { createWorkflowsSeeder } from "../be/seed/workflows-seeder";
|
|
16
18
|
|
|
17
19
|
const TEST_DB_PATH = "./test-seed-schedules.sqlite";
|
|
18
20
|
|
|
@@ -72,6 +74,31 @@ function zeroConfigScheduleSource(opts?: {
|
|
|
72
74
|
} satisfies ScheduleTemplateSource;
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
/** A schedule template with an explicit target block and an optional task prompt. */
|
|
78
|
+
function targetScheduleSource(target: Record<string, unknown>, prompt?: string) {
|
|
79
|
+
return {
|
|
80
|
+
config: JSON.stringify({
|
|
81
|
+
name: "test-target-schedule",
|
|
82
|
+
title: "Test target schedule",
|
|
83
|
+
description: "Seeded schedule with a target.",
|
|
84
|
+
placeholders: [],
|
|
85
|
+
requires: [],
|
|
86
|
+
runAllSeedersCandidate: true,
|
|
87
|
+
autoEnableCandidate: true,
|
|
88
|
+
}),
|
|
89
|
+
content: `# Test\n\n## Schedule\n\n\`\`\`json\n${JSON.stringify({
|
|
90
|
+
cron: "0 9 * * *",
|
|
91
|
+
timezone: "UTC",
|
|
92
|
+
enabled: true,
|
|
93
|
+
...target,
|
|
94
|
+
})}\n\`\`\`\n${prompt === undefined ? "" : `\n## Scheduled Task\n\n${prompt}`}`,
|
|
95
|
+
} satisfies ScheduleTemplateSource;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function makeWorkflow(name: string) {
|
|
99
|
+
return createWorkflow({ name, definition: { nodes: [] } });
|
|
100
|
+
}
|
|
101
|
+
|
|
75
102
|
const ORIGINAL_SEED_AUTOMATIONS_ENABLED = process.env.SEED_AUTOMATIONS_ENABLED;
|
|
76
103
|
|
|
77
104
|
beforeEach(async () => {
|
|
@@ -133,6 +160,8 @@ describe("schedules seeder", () => {
|
|
|
133
160
|
|
|
134
161
|
test("SEED_AUTOMATIONS_ENABLED=false still seeds all schedules disabled", async () => {
|
|
135
162
|
process.env.SEED_AUTOMATIONS_ENABLED = "false";
|
|
163
|
+
// dreaming-daily targets the seeded dreaming workflow.
|
|
164
|
+
await runSeeder(createWorkflowsSeeder(), { quiet: true });
|
|
136
165
|
const result = await runSeeder(createSchedulesSeeder(), { quiet: true });
|
|
137
166
|
expect(result).toMatchObject({ created: 12, failed: [] });
|
|
138
167
|
const schedules = await getScheduledTasks();
|
|
@@ -141,6 +170,8 @@ describe("schedules seeder", () => {
|
|
|
141
170
|
});
|
|
142
171
|
|
|
143
172
|
test("seeds a disabled schedule whose timezone still needs setup", async () => {
|
|
173
|
+
// dreaming-daily targets the seeded dreaming workflow.
|
|
174
|
+
await runSeeder(createWorkflowsSeeder(), { quiet: true });
|
|
144
175
|
const result = await runSeeder(createSchedulesSeeder(), { quiet: true });
|
|
145
176
|
expect(result).toMatchObject({ created: 12, failed: [] });
|
|
146
177
|
expect(await getScheduledTaskByName("weekly-dependabot-triage")).toMatchObject({
|
|
@@ -245,6 +276,113 @@ describe("schedules seeder", () => {
|
|
|
245
276
|
);
|
|
246
277
|
});
|
|
247
278
|
|
|
279
|
+
test("dreaming-daily is a workflow schedule with no task prompt", async () => {
|
|
280
|
+
await runSeeder(createWorkflowsSeeder(), { quiet: true });
|
|
281
|
+
const dreaming = loadSeedSchedules().find((schedule) => schedule.name === "dreaming-daily");
|
|
282
|
+
expect(dreaming).toMatchObject({
|
|
283
|
+
targetType: "workflow",
|
|
284
|
+
workflow: "dreaming",
|
|
285
|
+
taskTemplate: "",
|
|
286
|
+
});
|
|
287
|
+
// Agent-task templates carry no target keys, so their seed hash is unchanged.
|
|
288
|
+
const digest = loadSeedSchedules().find((schedule) => schedule.name === "daily-blocker-digest");
|
|
289
|
+
expect(digest && "targetType" in digest).toBe(false);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("seeds a workflow target by name and re-runs as a no-op", async () => {
|
|
293
|
+
const workflow = await makeWorkflow("test-target-workflow");
|
|
294
|
+
const seeder = createSchedulesSeeder([
|
|
295
|
+
targetScheduleSource({ targetType: "workflow", workflow: "test-target-workflow" }),
|
|
296
|
+
]);
|
|
297
|
+
expect(await runSeeder(seeder, { quiet: true })).toMatchObject({ created: 1, failed: [] });
|
|
298
|
+
const row = await getScheduledTaskByName("test-target-schedule");
|
|
299
|
+
expect(row).toMatchObject({ targetType: "workflow", workflowId: workflow.id });
|
|
300
|
+
expect(row?.taskTemplate).toBeUndefined();
|
|
301
|
+
expect(await runSeeder(seeder, { quiet: true })).toMatchObject({
|
|
302
|
+
skippedUnchanged: 1,
|
|
303
|
+
updated: 0,
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("seeds a script target and fails on a missing script", async () => {
|
|
308
|
+
const seeder = createSchedulesSeeder([
|
|
309
|
+
targetScheduleSource({ targetType: "script", scriptName: "no-such-script", scriptArgs: {} }),
|
|
310
|
+
]);
|
|
311
|
+
const result = await runSeeder(seeder, { quiet: true });
|
|
312
|
+
expect(result.created).toBe(0);
|
|
313
|
+
expect(result.failed[0]?.error).toContain('Global script "no-such-script" not found');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("fails a workflow target whose workflow does not exist, without creating a row", async () => {
|
|
317
|
+
const result = await runSeeder(
|
|
318
|
+
createSchedulesSeeder([
|
|
319
|
+
targetScheduleSource({ targetType: "workflow", workflow: "missing" }),
|
|
320
|
+
]),
|
|
321
|
+
{ quiet: true },
|
|
322
|
+
);
|
|
323
|
+
expect(result.created).toBe(0);
|
|
324
|
+
expect(result.failed[0]?.error).toContain('Workflow "missing" not found');
|
|
325
|
+
expect(await getScheduledTaskByName("test-target-schedule")).toBeNull();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("rejects target blocks that ship fields or prompts the target ignores", () => {
|
|
329
|
+
const cases: [Record<string, unknown>, string | undefined, string][] = [
|
|
330
|
+
[{ targetType: "workflow", workflow: "w" }, "Do it.", "ships a ## Scheduled Task prompt"],
|
|
331
|
+
[{ targetType: "script", scriptName: "s" }, "Do it.", "ships a ## Scheduled Task prompt"],
|
|
332
|
+
[{ targetType: "workflow" }, undefined, "has no workflow name"],
|
|
333
|
+
[{ targetType: "script" }, undefined, "has no scriptName"],
|
|
334
|
+
[{ targetType: "workflow", workflow: "w", scriptName: "s" }, undefined, "sets scriptName"],
|
|
335
|
+
[{ workflow: "w" }, "Do it.", "(targetType agent-task) sets workflow"],
|
|
336
|
+
[{ targetType: "lambda" }, undefined, 'unknown targetType "lambda"'],
|
|
337
|
+
];
|
|
338
|
+
for (const [target, prompt, message] of cases) {
|
|
339
|
+
expect(() => loadSeedSchedules([targetScheduleSource(target, prompt)])).toThrow(message);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("migrates a pristine agent-task schedule to a workflow target and clears its prompt", async () => {
|
|
344
|
+
const workflow = await makeWorkflow("test-target-workflow");
|
|
345
|
+
await runSeeder(createSchedulesSeeder([targetScheduleSource({}, "Trigger the workflow.")]), {
|
|
346
|
+
quiet: true,
|
|
347
|
+
});
|
|
348
|
+
expect(await getScheduledTaskByName("test-target-schedule")).toMatchObject({
|
|
349
|
+
targetType: "agent-task",
|
|
350
|
+
taskTemplate: "Trigger the workflow.",
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const result = await runSeeder(
|
|
354
|
+
createSchedulesSeeder([
|
|
355
|
+
targetScheduleSource({ targetType: "workflow", workflow: "test-target-workflow" }),
|
|
356
|
+
]),
|
|
357
|
+
{ quiet: true },
|
|
358
|
+
);
|
|
359
|
+
expect(result).toMatchObject({ updated: 1, failed: [] });
|
|
360
|
+
const row = await getScheduledTaskByName("test-target-schedule");
|
|
361
|
+
expect(row).toMatchObject({ targetType: "workflow", workflowId: workflow.id });
|
|
362
|
+
expect(row?.taskTemplate).toBeUndefined();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("preserves an operator-edited agent-task schedule when its template moves to a workflow", async () => {
|
|
366
|
+
await makeWorkflow("test-target-workflow");
|
|
367
|
+
await runSeeder(createSchedulesSeeder([targetScheduleSource({}, "Trigger the workflow.")]), {
|
|
368
|
+
quiet: true,
|
|
369
|
+
});
|
|
370
|
+
const seeded = await getScheduledTaskByName("test-target-schedule");
|
|
371
|
+
await updateScheduledTask(seeded!.id, { taskTemplate: "My own prompt." });
|
|
372
|
+
|
|
373
|
+
const result = await runSeeder(
|
|
374
|
+
createSchedulesSeeder([
|
|
375
|
+
targetScheduleSource({ targetType: "workflow", workflow: "test-target-workflow" }),
|
|
376
|
+
]),
|
|
377
|
+
{ quiet: true },
|
|
378
|
+
);
|
|
379
|
+
expect(result.skippedUserModified).toBe(1);
|
|
380
|
+
expect(await getScheduledTaskByName("test-target-schedule")).toMatchObject({
|
|
381
|
+
targetType: "agent-task",
|
|
382
|
+
taskTemplate: "My own prompt.",
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
248
386
|
test("reports an invalid cron without creating a row", async () => {
|
|
249
387
|
const result = await runSeeder(createSchedulesSeeder([scheduleSource("not a cron")]), {
|
|
250
388
|
quiet: true,
|
|
@@ -261,3 +261,42 @@ describe("nan and helmcode sessions price at $0 from the seeded table", () => {
|
|
|
261
261
|
}
|
|
262
262
|
});
|
|
263
263
|
});
|
|
264
|
+
|
|
265
|
+
describe("pi on the ChatGPT subscription prices like codex on the same pool", () => {
|
|
266
|
+
const cache = loadModelsDevCache();
|
|
267
|
+
const rows = cache ? buildModelsDevSeedRows(cache) : [];
|
|
268
|
+
const book: RateBook = Object.fromEntries(
|
|
269
|
+
rows.map((row) => [`${row.provider}:${row.model}:${row.tokenClass}`, row.pricePerMillionUsd]),
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
test("openai-codex/<id> on pi records the codex <id> pricing-table cost", async () => {
|
|
273
|
+
// One request: 69,308 input tokens, 55,680 of them cached, 376 output.
|
|
274
|
+
// codex reports input including cache reads; pi-ai reports them disjoint.
|
|
275
|
+
const codex = await recomputeSessionCost(
|
|
276
|
+
{
|
|
277
|
+
...BASE,
|
|
278
|
+
provider: "codex",
|
|
279
|
+
model: "gpt-5.6-luna",
|
|
280
|
+
inputTokens: 69_308,
|
|
281
|
+
cacheReadTokens: 55_680,
|
|
282
|
+
outputTokens: 376,
|
|
283
|
+
},
|
|
284
|
+
lookupFrom(book),
|
|
285
|
+
);
|
|
286
|
+
const pi = await recomputeSessionCost(
|
|
287
|
+
{
|
|
288
|
+
...BASE,
|
|
289
|
+
provider: "pi",
|
|
290
|
+
model: "openai-codex/gpt-5.6-luna",
|
|
291
|
+
inputTokens: 69_308 - 55_680,
|
|
292
|
+
cacheReadTokens: 55_680,
|
|
293
|
+
outputTokens: 376,
|
|
294
|
+
},
|
|
295
|
+
lookupFrom(book),
|
|
296
|
+
);
|
|
297
|
+
expect(codex.costSource).toBe("pricing-table");
|
|
298
|
+
expect(pi.costSource).toBe("pricing-table");
|
|
299
|
+
expect(pi.totalCostUsd).toBeCloseTo(codex.totalCostUsd, 12);
|
|
300
|
+
expect(pi.totalCostUsd).toBeGreaterThan(0);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
@@ -361,6 +361,32 @@ describe("Phase 3 — session cost recompute golden fixtures", () => {
|
|
|
361
361
|
expectExact(body.cost.totalCostUsd, expected);
|
|
362
362
|
});
|
|
363
363
|
|
|
364
|
+
// Boot-seeded rows only: fx reports the swarm model string with its route
|
|
365
|
+
// prefix and no cache split, so input and output price at the table rate.
|
|
366
|
+
test("fx runs price from the seeded rates on each route", async () => {
|
|
367
|
+
const openRouter = await postCost({
|
|
368
|
+
sessionId: "fx-openrouter",
|
|
369
|
+
provider: "fx",
|
|
370
|
+
model: "openrouter/deepseek/deepseek-v4.1-flash",
|
|
371
|
+
totalCostUsd: 0,
|
|
372
|
+
inputTokens: 58_620,
|
|
373
|
+
outputTokens: 1_183,
|
|
374
|
+
});
|
|
375
|
+
expect(openRouter.cost.costSource).toBe("pricing-table");
|
|
376
|
+
expectExact(openRouter.cost.totalCostUsd, (58_620 * 0.14 + 1_183 * 0.42) / 1_000_000);
|
|
377
|
+
|
|
378
|
+
const openAi = await postCost({
|
|
379
|
+
sessionId: "fx-openai",
|
|
380
|
+
provider: "fx",
|
|
381
|
+
model: "gpt-5-mini",
|
|
382
|
+
totalCostUsd: 0,
|
|
383
|
+
inputTokens: 9_286,
|
|
384
|
+
outputTokens: 506,
|
|
385
|
+
});
|
|
386
|
+
expect(openAi.cost.costSource).toBe("pricing-table");
|
|
387
|
+
expectExact(openAi.cost.totalCostUsd, (9_286 * 0.25 + 506 * 2) / 1_000_000);
|
|
388
|
+
});
|
|
389
|
+
|
|
364
390
|
test("legacy cache writes without a TTL split keep the 5m class", async () => {
|
|
365
391
|
await seedModelRates("claude", "claude-legacy-cache-write", {
|
|
366
392
|
input: 5,
|