@desplega.ai/agent-swarm 1.166.0 → 1.168.0
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/dist/acp-adapter-zhejfecx.js +34 -0
- package/dist/{actions-qz3m692w.js → actions-7jg840nr.js} +25 -25
- package/dist/{amp-adapter-j1xbmsqz.js → amp-adapter-ye6rnetm.js} +4 -4
- package/dist/{anthropic-messages-hst62ws9.js → anthropic-messages-13yn502s.js} +4 -4
- package/dist/{app-43vrdmv8.js → app-ffes2kyf.js} +10 -9
- package/dist/{assistant-ertffmka.js → assistant-fhm5smyz.js} +26 -26
- package/dist/{azure-openai-responses-mh8tvf78.js → azure-openai-responses-9st34pfz.js} +5 -5
- package/dist/{boot-reembed-998jtpdg.js → boot-reembed-20q3nzvg.js} +9 -8
- package/dist/{boot-reembed-s7k7c6v2.js → boot-reembed-jvfadhsk.js} +9 -8
- package/dist/{boot-scrub-sweep-nepsk4t1.js → boot-scrub-sweep-jtfajzve.js} +10 -9
- package/dist/{boot-seeds-vvzs1zhk.js → boot-seeds-9qt0j8j2.js} +12 -11
- package/dist/{claude-adapter-2zvmekrp.js → claude-adapter-a8znhthz.js} +24 -22
- package/dist/{claude-managed-adapter-s49wbk8t.js → claude-managed-adapter-tzwxy803.js} +4 -4
- package/dist/{claude-managed-setup-cvhqq8qn.js → claude-managed-setup-nrhfx5b0.js} +3 -3
- package/dist/{claude-sdk-session-kkbxtw5q.js → claude-sdk-session-7x7chdrs.js} +7608 -7596
- package/dist/{cli-zsh093hk.js → cli-014vns2y.js} +63 -1
- package/dist/{cli-8kgvmkak.js → cli-0jngabtb.js} +1 -1
- package/dist/{cli-qvfk83fy.js → cli-13xwcqvj.js} +2 -2
- package/dist/{cli-0nhdwxd5.js → cli-43jg9xdw.js} +3 -3
- package/dist/{cli-cpfmqz3n.js → cli-47zfzshm.js} +5 -5
- package/dist/{cli-zy7vwsne.js → cli-5qkcdv8c.js} +16 -9
- package/dist/{cli-cwxhbf43.js → cli-5t5my0h8.js} +85 -2
- package/dist/{cli-y59h2pk9.js → cli-69egj654.js} +1 -1
- package/dist/{cli-68r1j4sg.js → cli-6bpvv8h4.js} +3 -3
- package/dist/{cli-yshaazs1.js → cli-74ez9b66.js} +63 -20
- package/dist/{cli-32bca7nr.js → cli-7avy4v8w.js} +1 -1
- package/dist/{cli-f2n17zhp.js → cli-7mhy3ths.js} +21 -18
- package/dist/{cli-q38n4bq0.js → cli-95zs84qw.js} +3 -3
- package/dist/{cli-fn697bnt.js → cli-9q0bh411.js} +21 -8
- package/dist/{cli-rggdq8wq.js → cli-9q24yyvs.js} +3 -3
- package/dist/cli-a35hzdve.js +33 -0
- package/dist/{cli-3qn4r334.js → cli-aj1e0h0d.js} +20 -14
- package/dist/{cli-rsy933yz.js → cli-bcspch6h.js} +30 -19
- package/dist/{cli-x3x0qwky.js → cli-bxqez1p0.js} +51 -23
- package/dist/{cli-ng97fjq7.js → cli-cmxkbzzs.js} +33 -7
- package/dist/{cli-df56tqdw.js → cli-ee0402n1.js} +198 -70
- package/dist/{cli-mwegs4af.js → cli-ew0c3a1j.js} +3 -3
- package/dist/{cli-ysnyhws9.js → cli-ewhwpyrb.js} +54 -51
- package/dist/{cli-j2jbr8af.js → cli-fhj4y16f.js} +1 -1
- package/dist/{cli-8t229cmr.js → cli-fqq06kkb.js} +30 -17
- package/dist/{cli-jvj6a0cv.js → cli-g6gx2dn1.js} +54 -19
- package/dist/{cli-nexwh8zg.js → cli-g7k7zbcz.js} +12 -5
- package/dist/{cli-ecknp7p1.js → cli-j05x7pnb.js} +10 -10
- package/dist/{cli-gt8azyet.js → cli-jb931ba2.js} +1 -1
- package/dist/{cli-nvvfd8bq.js → cli-jbzbr36n.js} +4091 -3793
- package/dist/{cli-3ddybbbx.js → cli-jvmtxqtc.js} +26 -14
- package/dist/{cli-pkke6geq.js → cli-km4x66j0.js} +83 -65
- package/dist/{cli-b0f70z24.js → cli-mn8qkd8v.js} +1 -1
- package/dist/{cli-0qjcsxgz.js → cli-nctfv2yy.js} +99 -9
- package/dist/{cli-4r1sjfc4.js → cli-ng1pmdx6.js} +10 -10
- package/dist/cli-nkqjj42q.js +104 -0
- package/dist/{cli-aqcey741.js → cli-p5jqk8fr.js} +1 -1
- package/dist/{cli-66zh26sp.js → cli-pgehz8e1.js} +57 -17
- package/dist/cli-q1vnz8wv.js +96 -0
- package/dist/{cli-fj86xr3c.js → cli-qd97v6z2.js} +2 -2
- package/dist/{cli-p4ztb80h.js → cli-r7eeddkq.js} +13 -8
- package/dist/{cli-8ssjez7e.js → cli-rhy7z1rp.js} +22 -11
- package/dist/{cli-vzc73wet.js → cli-rr5z1j4p.js} +87 -69
- package/dist/{cli-4mevd1hf.js → cli-s9y81s7j.js} +5 -5
- package/dist/{cli-dw2q9f7g.js → cli-svah6b6w.js} +4 -4
- package/dist/{cli-h3x613ze.js → cli-tc5zec7v.js} +6 -6
- package/dist/{cli-ttbsse3f.js → cli-tmhsxhb8.js} +1 -1
- package/dist/{cli-feh3dkah.js → cli-v1w7pbd8.js} +2 -1
- package/dist/{cli-10zmp1p7.js → cli-vr5avem2.js} +27 -32
- package/dist/{cli-bzr0ftht.js → cli-wnb9b2g3.js} +19 -0
- package/dist/{cli-krqm5f11.js → cli-xezn9nkh.js} +16 -13
- package/dist/{cli-8kt0bmgx.js → cli-xf713zz0.js} +2 -2
- package/dist/{cli-6zcfw0x7.js → cli-y195hv1a.js} +1 -1
- package/dist/{cli-x7kpr72a.js → cli-ytmmyj7f.js} +2 -2
- package/dist/{cli-v2a8x45d.js → cli-z48ggfyq.js} +11 -6
- package/dist/{cli-qbksc292.js → cli-zy39d433.js} +2 -0
- package/dist/cli.js +28 -28
- package/dist/{cloudflare-workers-ai-system-one-tt484t61.js → cloudflare-workers-ai-system-one-sta45jf9.js} +7 -5
- package/dist/{codex-adapter-dw1wxqt8.js → codex-adapter-n63k33d3.js} +18 -18
- package/dist/{codex-login-terbdsqq.js → codex-login-g1cyt5zm.js} +1 -1
- package/dist/{codex-session-runner-3cgb8y1g.js → codex-session-runner-xkxfd81a.js} +18 -18
- package/dist/{commands-mhxam6z2.js → commands-q4mfhn51.js} +9 -8
- package/dist/{cursor-adapter-2qda3st7.js → cursor-adapter-v8mm6bp5.js} +5 -5
- package/dist/{db-sjks418k.js → db-1t0bj3pc.js} +9 -8
- package/dist/{dispatcher-1es759rn.js → dispatcher-k1fhavcf.js} +24 -24
- package/dist/{dsh-adapter-bek4cyw9.js → dsh-adapter-5423p70m.js} +5 -5
- package/dist/{easter-egg-3d-mtpnw06s.js → easter-egg-3d-66zxervv.js} +2 -2
- package/dist/{execute-s4ccjq3n.js → execute-h4jgevyw.js} +61 -11
- package/dist/{google-generative-ai-d6nphg3q.js → google-generative-ai-b8zxrpax.js} +5 -5
- package/dist/{google-vertex-cpshkref.js → google-vertex-nrdd697t.js} +5 -5
- package/dist/{grok-adapter-ar9cw9jg.js → grok-adapter-phrndeb8.js} +18 -8
- package/dist/{handlers-1gkbhktr.js → handlers-59ywqwzh.js} +27 -27
- package/dist/{hook-vxa3tcsy.js → hook-atj0swc2.js} +14 -14
- package/dist/{hook-ds2zxq9v.js → hook-mew1chq9.js} +42 -31
- package/dist/{http-3h21z9md.js → http-ef2fnn9v.js} +953 -98
- package/dist/{index-n99fycq7.js → index-48pb9erw.js} +24 -24
- package/dist/{index-bn6cy8ps.js → index-9fc2h7ww.js} +24 -24
- package/dist/{index-9b65j1m5.js → index-gr69c43q.js} +9 -8
- package/dist/{index-f8xnqrye.js → index-h26wjcfy.js} +43 -24
- package/dist/{index-dkfxpkmm.js → index-w2yhye23.js} +25 -25
- package/dist/{keepalive-3xspa9jw.js → keepalive-qhcqz56v.js} +11 -10
- package/dist/{lead-6qphc124.js → lead-hpk5r3b1.js} +47 -47
- package/dist/{llama-cpp-classify-tgj35k8d.js → llama-cpp-classify-6erb7wwv.js} +3 -1
- package/dist/{maintenance-0teem1bb.js → maintenance-nvkyp2xe.js} +9 -8
- package/dist/{mistral-conversations-8445ddjn.js → mistral-conversations-059y51zt.js} +4 -4
- package/dist/{node-9sg7ajby.js → node-qm9eb6z0.js} +1 -1
- package/dist/{oauth-refresh-sweep-sh9q25xp.js → oauth-refresh-sweep-ngskthwh.js} +10 -9
- package/dist/{onboard-54qmnqxe.js → onboard-yfnkqhxc.js} +2 -2
- package/dist/{openai-codex-responses-9ynm88zy.js → openai-codex-responses-jzxkta9b.js} +8 -7
- package/dist/{openai-completions-nhqwjmaw.js → openai-completions-663yncfq.js} +4 -4
- package/dist/openai-decisions-vj9nkpf8.js +162 -0
- package/dist/{openai-responses-xx1c4kgm.js → openai-responses-m3g43rrt.js} +5 -5
- package/dist/{opencode-adapter-wjfa2wvd.js → opencode-adapter-yjyqynzf.js} +7 -7
- package/dist/{openrouter-images-8yx3487g.js → openrouter-images-kr60n6b9.js} +1 -1
- package/dist/{otel-impl-ycqzp2a1.js → otel-impl-hhm64r6m.js} +1 -1
- package/dist/{pi-messages-vxdt8v7j.js → pi-messages-xszk613n.js} +1 -1
- package/dist/{pi-mono-adapter-e77e9zst.js → pi-mono-adapter-0eg8kw2c.js} +68 -51
- package/dist/{pricing-refresh-atnkngqs.js → pricing-refresh-35f91nx2.js} +11 -10
- package/dist/{rbac-roles-j78wdksm.js → rbac-roles-97w370qj.js} +10 -9
- package/dist/{rbac-roles-9pjphh9q.js → rbac-roles-z6sje3gy.js} +9 -8
- package/dist/{render-v2-pxjs49fx.js → render-v2-n4v66bj3.js} +10 -9
- package/dist/{runtime-v2t1r79e.js → runtime-h9ha1mq6.js} +4 -4
- package/dist/{setup-ymej0xtx.js → setup-m3hhyv5f.js} +2 -2
- package/dist/{typesafe-system-one-datfr82p.js → typesafe-system-one-e988vty7.js} +7 -5
- package/dist/{virtual-modules-wm8mxdxx.js → virtual-modules-ftr9kwe8.js} +11 -11
- package/dist/{work-objects-3nh4ymzb.js → work-objects-mxf2c842.js} +27 -27
- package/dist/{worker-qa9fkz5s.js → worker-9t07z1gq.js} +47 -47
- package/openapi.json +72 -1
- package/package.json +10 -10
- package/src/be/memory/providers/openai-embedding.ts +7 -2
- package/src/be/memory/raters/llm-summarizer.ts +7 -2
- package/src/be/modelsdev-cache.json +61 -0
- package/src/be/script-connections.ts +40 -6
- package/src/be/seed-pricing.ts +84 -53
- package/src/be/swarm-config-guard.ts +14 -0
- package/src/commands/provider-credentials.ts +28 -5
- package/src/commands/runner.ts +7 -12
- package/src/extensions/catalog.generated.json +17 -0
- package/src/heartbeat/heartbeat.ts +91 -19
- package/src/hooks/hook.ts +72 -27
- package/src/http/mcp.ts +5 -0
- package/src/http/onboarding.ts +2 -0
- package/src/http/script-connections.ts +35 -0
- package/src/providers/acp-targets.ts +10 -5
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/claude-adapter.ts +32 -0
- package/src/providers/claude-background-keepalive.ts +100 -0
- package/src/providers/claude-sdk-session.ts +6 -0
- package/src/providers/codex-adapter.ts +30 -3
- package/src/providers/codex-oauth/auth-json.ts +21 -4
- package/src/providers/codex-oauth/types.ts +2 -0
- package/src/providers/dsh-adapter.ts +11 -2
- package/src/providers/grok-adapter.ts +18 -2
- package/src/providers/opencode-adapter.ts +21 -5
- package/src/providers/pi-mono-extension.ts +56 -25
- package/src/providers/pricing-sources.md +2 -0
- package/src/scheduler/scheduler.ts +82 -13
- package/src/scripts-runtime/executors/registry.ts +1 -1
- package/src/server.ts +63 -3
- package/src/tests/acp-adapter.test.ts +152 -12
- package/src/tests/ci-unit-test-report.test.ts +73 -0
- package/src/tests/claude-background-keepalive-wiring.test.ts +220 -0
- package/src/tests/claude-background-keepalive.fixture.ts +162 -0
- package/src/tests/claude-background-keepalive.test.ts +139 -0
- package/src/tests/claude-default-route.test.ts +75 -0
- package/src/tests/claude-hook-output.test.ts +233 -0
- package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +4 -2
- package/src/tests/codex-adapter.test.ts +166 -26
- package/src/tests/codex-oauth-auth-timestamps.test.ts +76 -0
- package/src/tests/codex-oauth.test.ts +4 -1
- package/src/tests/connection-embedded-auth.test.ts +51 -0
- package/src/tests/context-window.test.ts +2 -2
- package/src/tests/credential-check.test.ts +51 -1
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/dsh-adapter.test.ts +24 -2
- package/src/tests/enabled-tools.test.ts +269 -0
- package/src/tests/entrypoint-codex-oauth-seed.test.ts +12 -0
- package/src/tests/extensions-catalog.test.ts +1 -0
- package/src/tests/extensions-example-slack-reply-gate.test.ts +756 -0
- package/src/tests/grok-adapter.test.ts +13 -0
- package/src/tests/haiku-5-5-pricing.test.ts +38 -0
- package/src/tests/heartbeat-checklist.test.ts +158 -1
- package/src/tests/onboarding.test.ts +44 -0
- package/src/tests/opencode-adapter.test.ts +66 -3
- package/src/tests/opencode-plugin.test.ts +37 -0
- package/src/tests/openrouter-attribution-call-sites.test.ts +204 -0
- package/src/tests/openrouter-base-url.test.ts +30 -39
- package/src/tests/pi-mono-extension.test.ts +161 -2
- package/src/tests/pricing-refresh.test.ts +70 -1
- package/src/tests/reasoning-effort.test.ts +3 -1
- package/src/tests/schedule-target-type.test.ts +136 -0
- package/src/tests/script-connections-http.test.ts +45 -0
- package/src/tests/script-connections-mcp.test.ts +41 -0
- package/src/tests/script-executor-registry.test.ts +18 -0
- package/src/tests/send-task-self-send.test.ts +107 -0
- package/src/tests/telemetry-trigger.test.ts +12 -1
- package/src/tests/workflow-llm.test.ts +14 -0
- package/src/tests/workflow-system-one-decision-providers.test.ts +33 -0
- package/src/tools/request-human-input.ts +7 -2
- package/src/tools/script-connections/tool.ts +35 -3
- package/src/tools/send-task.ts +9 -1
- package/src/utils/enabled-tools.ts +27 -0
- package/src/utils/internal-ai/complete-structured.ts +11 -3
- package/src/utils/openrouter-base-url.ts +23 -0
- package/src/workflows/executors/raw-llm.ts +1 -0
- package/src/workflows/executors/system-one-decision.ts +2 -0
- package/src/workflows/executors/validate.ts +1 -0
- package/src/workflows/executors/workflow-llm.ts +9 -2
- package/templates/extensions/README.md +1 -0
- package/templates/extensions/slack-reply-gate/README.md +98 -0
- package/templates/extensions/slack-reply-gate/hooks.ts +582 -0
- package/templates/extensions/slack-reply-gate/manifest.json +8 -0
- package/templates/skills/agent-browser/SKILL.md +2 -0
- package/templates/skills/agent-browser/content.md +2 -0
- package/templates/skills/code-quality/content.md +1 -0
- package/templates/skills/heartbeat-runbook/content.md +1 -1
- package/templates/skills/swarm-scripts/SKILL.md +10 -0
- package/templates/skills/swarm-scripts/content.md +10 -0
- package/templates/skills/work-on-task/SKILL.md +4 -0
- package/templates/skills/work-on-task/content.md +4 -0
- package/templates/skills/workflow-iterate/content.md +9 -0
- package/dist/acp-adapter-yzfxn8vj.js +0 -23
- package/dist/cli-3dpj7atn.js +0 -11
- package/dist/cli-gvcgmx69.js +0 -183
- package/dist/{cli-jhm6hzjn.js → cli-mxvnnv5q.js} +7 -7
- package/dist/{codex-hook-d2rkvf39.js → codex-hook-15dgc4cb.js} +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, expect, spyOn, test } from "bun:test";
|
|
2
2
|
import { chmodSync, existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
3
|
-
import { createServer, type IncomingMessage, type
|
|
3
|
+
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import type { Usage } from "@agentclientprotocol/sdk";
|
|
@@ -64,20 +64,29 @@ function baseConfig(overrides: Partial<ProviderSessionConfig> = {}): ProviderSes
|
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* Start a minimal swarm API stub that handles POST /api/sessions/tokens and
|
|
67
|
-
* DELETE /api/sessions/tokens/:id. Returns
|
|
68
|
-
*
|
|
67
|
+
* DELETE /api/sessions/tokens/:id. Returns its base URL and `close()`, which
|
|
68
|
+
* the caller must await. The adapter revokes a minted token fire-and-forget,
|
|
69
|
+
* so closing before that DELETE lands fails it and leaks a revoke warning
|
|
70
|
+
* into a later test.
|
|
69
71
|
*/
|
|
70
72
|
async function startTokenStubServer(
|
|
71
73
|
tokenId: string,
|
|
72
74
|
plaintext: string,
|
|
73
|
-
): Promise<{
|
|
75
|
+
): Promise<{ apiUrl: string; close: () => Promise<void> }> {
|
|
76
|
+
let minted = false;
|
|
77
|
+
let resolveRevoked!: () => void;
|
|
78
|
+
const revoked = new Promise<void>((resolve) => {
|
|
79
|
+
resolveRevoked = resolve;
|
|
80
|
+
});
|
|
74
81
|
const stub = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
75
82
|
if (req.method === "POST" && req.url === "/api/sessions/tokens") {
|
|
83
|
+
minted = true;
|
|
76
84
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
77
85
|
res.end(JSON.stringify({ tokenId, plaintext }));
|
|
78
86
|
} else if (req.method === "DELETE" && req.url?.startsWith("/api/sessions/tokens/")) {
|
|
79
87
|
res.writeHead(204);
|
|
80
88
|
res.end();
|
|
89
|
+
resolveRevoked();
|
|
81
90
|
} else {
|
|
82
91
|
res.writeHead(404);
|
|
83
92
|
res.end();
|
|
@@ -85,7 +94,13 @@ async function startTokenStubServer(
|
|
|
85
94
|
});
|
|
86
95
|
await new Promise<void>((resolve) => stub.listen(0, "127.0.0.1", resolve));
|
|
87
96
|
const addr = stub.address() as import("net").AddressInfo;
|
|
88
|
-
|
|
97
|
+
const close = async () => {
|
|
98
|
+
// A minted token is always revoked; wait for it, bounded so a failed test
|
|
99
|
+
// still tears down.
|
|
100
|
+
if (minted) await Promise.race([revoked, Bun.sleep(5_000)]);
|
|
101
|
+
await new Promise<void>((resolve) => stub.close(() => resolve()));
|
|
102
|
+
};
|
|
103
|
+
return { apiUrl: `http://127.0.0.1:${addr.port}`, close };
|
|
89
104
|
}
|
|
90
105
|
|
|
91
106
|
describe("ACPAdapter", () => {
|
|
@@ -305,7 +320,7 @@ new AgentSideConnection((connection) => new FakeAgent(connection), stream);
|
|
|
305
320
|
`,
|
|
306
321
|
);
|
|
307
322
|
|
|
308
|
-
const {
|
|
323
|
+
const { close: closeTokenStub, apiUrl } = await startTokenStubServer(
|
|
309
324
|
"stub-token-id",
|
|
310
325
|
"aseph_stubtokenfortest1234567890",
|
|
311
326
|
);
|
|
@@ -503,7 +518,7 @@ new AgentSideConnection((connection) => new FakeAgent(connection), stream);
|
|
|
503
518
|
closeDb();
|
|
504
519
|
}
|
|
505
520
|
} finally {
|
|
506
|
-
await
|
|
521
|
+
await closeTokenStub();
|
|
507
522
|
}
|
|
508
523
|
});
|
|
509
524
|
|
|
@@ -537,10 +552,132 @@ new AgentSideConnection((connection) => new FakeAgent(connection), stream);
|
|
|
537
552
|
),
|
|
538
553
|
).toMatchObject({
|
|
539
554
|
OPENAI_API_KEY: "example-test-key",
|
|
540
|
-
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
555
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
556
|
+
theme: "dark",
|
|
557
|
+
model: "opencode/model",
|
|
558
|
+
provider: {
|
|
559
|
+
openrouter: {
|
|
560
|
+
options: {
|
|
561
|
+
headers: {
|
|
562
|
+
"HTTP-Referer": "https://agent-swarm.dev",
|
|
563
|
+
"X-OpenRouter-Title": "Agent Swarm",
|
|
564
|
+
"X-OpenRouter-Categories": "personal-agent,cloud-agent",
|
|
565
|
+
"X-Title": "Agent Swarm",
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
}),
|
|
541
571
|
});
|
|
542
572
|
});
|
|
543
573
|
|
|
574
|
+
test("OpenCode preset merges the OpenRouter gateway with model and provider config", () => {
|
|
575
|
+
const config = baseConfig({
|
|
576
|
+
model: " openrouter/openai/gpt-4o-mini ",
|
|
577
|
+
env: {
|
|
578
|
+
ACP_TARGET: "opencode",
|
|
579
|
+
OPENROUTER_BASE_URL: " http://127.0.0.1:8080/api/v1/ ",
|
|
580
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
581
|
+
theme: "dark",
|
|
582
|
+
provider: { openrouter: { options: { apiKey: "dummy" } } },
|
|
583
|
+
}),
|
|
584
|
+
},
|
|
585
|
+
});
|
|
586
|
+
const env = resolveAcpTarget(config).env(config);
|
|
587
|
+
expect(JSON.parse(env.OPENCODE_CONFIG_CONTENT!)).toEqual({
|
|
588
|
+
theme: "dark",
|
|
589
|
+
model: "openrouter/openai/gpt-4o-mini",
|
|
590
|
+
provider: {
|
|
591
|
+
openrouter: { options: { apiKey: "dummy", baseURL: "http://127.0.0.1:8080/api/v1" } },
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
test("OpenCode preset omits gateway override for unset, blank, or default URL", () => {
|
|
597
|
+
for (const url of [undefined, "", "https://openrouter.ai/api/v1/"]) {
|
|
598
|
+
const config = baseConfig({
|
|
599
|
+
env: { ACP_TARGET: "opencode", OPENROUTER_BASE_URL: url },
|
|
600
|
+
});
|
|
601
|
+
const env = resolveAcpTarget(config).env(config);
|
|
602
|
+
const content = JSON.parse(env.OPENCODE_CONFIG_CONTENT!);
|
|
603
|
+
expect(content).toEqual({
|
|
604
|
+
model: config.model,
|
|
605
|
+
provider: {
|
|
606
|
+
openrouter: {
|
|
607
|
+
options: {
|
|
608
|
+
headers: {
|
|
609
|
+
"HTTP-Referer": "https://agent-swarm.dev",
|
|
610
|
+
"X-OpenRouter-Title": "Agent Swarm",
|
|
611
|
+
"X-OpenRouter-Categories": "personal-agent,cloud-agent",
|
|
612
|
+
"X-Title": "Agent Swarm",
|
|
613
|
+
},
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
expect(content.provider.openrouter.options.baseURL).toBeUndefined();
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
test("OpenCode preset adds attribution headers to OPENCODE_CONFIG_CONTENT for an OpenRouter host", () => {
|
|
623
|
+
const config = baseConfig({
|
|
624
|
+
model: "",
|
|
625
|
+
env: {
|
|
626
|
+
ACP_TARGET: "opencode",
|
|
627
|
+
OPENROUTER_BASE_URL: "https://openrouter.ai/api/v1",
|
|
628
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
629
|
+
theme: "dark",
|
|
630
|
+
provider: { openrouter: { options: { headers: { "X-Custom": "preserved" } } } },
|
|
631
|
+
}),
|
|
632
|
+
},
|
|
633
|
+
});
|
|
634
|
+
const env = resolveAcpTarget(config).env(config);
|
|
635
|
+
expect(JSON.parse(env.OPENCODE_CONFIG_CONTENT!)).toEqual({
|
|
636
|
+
theme: "dark",
|
|
637
|
+
provider: {
|
|
638
|
+
openrouter: {
|
|
639
|
+
options: {
|
|
640
|
+
headers: {
|
|
641
|
+
"X-Custom": "preserved",
|
|
642
|
+
"HTTP-Referer": "https://agent-swarm.dev",
|
|
643
|
+
"X-OpenRouter-Title": "Agent Swarm",
|
|
644
|
+
"X-OpenRouter-Categories": "personal-agent,cloud-agent",
|
|
645
|
+
"X-Title": "Agent Swarm",
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
},
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
test("OpenCode preset leaves OPENCODE_CONFIG_CONTENT unchanged when attribution is disabled", () => {
|
|
654
|
+
const content = JSON.stringify({
|
|
655
|
+
theme: "dark",
|
|
656
|
+
provider: { openrouter: { options: { headers: { "X-Custom": "preserved" } } } },
|
|
657
|
+
});
|
|
658
|
+
const config = baseConfig({
|
|
659
|
+
model: "",
|
|
660
|
+
env: {
|
|
661
|
+
ACP_TARGET: "opencode",
|
|
662
|
+
OPENROUTER_BASE_URL: "https://openrouter.ai/api/v1",
|
|
663
|
+
OPENROUTER_APP_ATTRIBUTION: "false",
|
|
664
|
+
OPENCODE_CONFIG_CONTENT: content,
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
expect(resolveAcpTarget(config).env(config).OPENCODE_CONFIG_CONTENT).toBe(content);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
test("OpenCode preset injects gateway even without a model fallback", () => {
|
|
671
|
+
const config = baseConfig({
|
|
672
|
+
model: "",
|
|
673
|
+
env: { ACP_TARGET: "opencode", OPENROUTER_BASE_URL: "http://127.0.0.1:8080/api/v1" },
|
|
674
|
+
});
|
|
675
|
+
const env = resolveAcpTarget(config).env(config);
|
|
676
|
+
expect(JSON.parse(env.OPENCODE_CONFIG_CONTENT!).provider.openrouter.options.baseURL).toBe(
|
|
677
|
+
"http://127.0.0.1:8080/api/v1",
|
|
678
|
+
);
|
|
679
|
+
});
|
|
680
|
+
|
|
544
681
|
test("Gemini preset supplies command, credentials, trust, and a model environment fallback", () => {
|
|
545
682
|
const config = baseConfig({
|
|
546
683
|
model: "gemini-3-flash-preview",
|
|
@@ -687,7 +824,10 @@ new AgentSideConnection((connection) => new FakeCopilot(connection), stream);
|
|
|
687
824
|
chmodSync(copilotStub, 0o755);
|
|
688
825
|
mkdirSync(join(binDir, "home"));
|
|
689
826
|
|
|
690
|
-
const {
|
|
827
|
+
const { close: closeTokenStub, apiUrl } = await startTokenStubServer(
|
|
828
|
+
"copilot-token-id",
|
|
829
|
+
"example-aseph_x",
|
|
830
|
+
);
|
|
691
831
|
try {
|
|
692
832
|
const config = baseConfig({
|
|
693
833
|
cwd,
|
|
@@ -721,7 +861,7 @@ new AgentSideConnection((connection) => new FakeCopilot(connection), stream);
|
|
|
721
861
|
while (existsSync(captured.dir) && Date.now() < deadline) await Bun.sleep(10);
|
|
722
862
|
expect(existsSync(captured.dir)).toBe(false);
|
|
723
863
|
} finally {
|
|
724
|
-
await
|
|
864
|
+
await closeTokenStub();
|
|
725
865
|
}
|
|
726
866
|
});
|
|
727
867
|
|
|
@@ -1077,7 +1217,7 @@ const stream = ndJsonStream(Writable.toWeb(process.stdout), Readable.toWeb(proce
|
|
|
1077
1217
|
new AgentSideConnection((connection) => new FakeAgent(connection), stream);
|
|
1078
1218
|
`,
|
|
1079
1219
|
);
|
|
1080
|
-
const {
|
|
1220
|
+
const { close: closeTokenStub, apiUrl } = await startTokenStubServer(
|
|
1081
1221
|
"stub-token-id",
|
|
1082
1222
|
"aseph_stubtokenfortest1234567890",
|
|
1083
1223
|
);
|
|
@@ -1140,7 +1280,7 @@ new AgentSideConnection((connection) => new FakeAgent(connection), stream);
|
|
|
1140
1280
|
closeDb();
|
|
1141
1281
|
}
|
|
1142
1282
|
} finally {
|
|
1143
|
-
await
|
|
1283
|
+
await closeTokenStub();
|
|
1144
1284
|
}
|
|
1145
1285
|
});
|
|
1146
1286
|
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { buildPayload, parseJunit } from "../../scripts/ci-unit-test-report";
|
|
3
|
+
|
|
4
|
+
// Shape emitted by `bun test --reporter=junit` (bun 1.4): one testsuite per file,
|
|
5
|
+
// nested testsuites per describe, and attribute values XML-escaped.
|
|
6
|
+
const JUNIT = `<?xml version="1.0" encoding="UTF-8"?>
|
|
7
|
+
<testsuites name="bun test" tests="6" assertions="5" failures="1" skipped="2" time="0.3">
|
|
8
|
+
<testsuite name="src/a.test.ts" file="src/a.test.ts" tests="4" failures="1" skipped="1" time="0.2">
|
|
9
|
+
<testsuite name="outer" file="src/a.test.ts" line="3" tests="4" time="0">
|
|
10
|
+
<testsuite name="inner & "nested"" file="src/a.test.ts" line="4" tests="2" time="0">
|
|
11
|
+
<testcase name="slow one" classname="inner" time="0.120" file="src/a.test.ts" line="5" assertions="1" />
|
|
12
|
+
<testcase name="breaks <sometimes>" classname="inner" time="0.004" file="src/a.test.ts" line="6" assertions="1">
|
|
13
|
+
<failure type="AssertionError" message="expect(received).toBe(expected) ">AssertionError </failure>
|
|
14
|
+
</testcase>
|
|
15
|
+
</testsuite>
|
|
16
|
+
<testcase name="fast" classname="outer" time="0.000010" file="src/a.test.ts" line="8" assertions="1" />
|
|
17
|
+
<testcase name="later" classname="outer" time="0" file="src/a.test.ts" line="9" assertions="0">
|
|
18
|
+
<skipped message="TODO" />
|
|
19
|
+
</testcase>
|
|
20
|
+
</testsuite>
|
|
21
|
+
</testsuite>
|
|
22
|
+
<testsuite name="src/b.test.ts" file="src/b.test.ts" tests="2" failures="0" skipped="1" time="0.1">
|
|
23
|
+
<testcase name="top level" classname="" time="0.06" file="src/b.test.ts" line="2" assertions="2" />
|
|
24
|
+
<testcase name="skipped" classname="" time="0" file="src/b.test.ts" line="3" assertions="0">
|
|
25
|
+
<skipped />
|
|
26
|
+
</testcase>
|
|
27
|
+
</testsuite>
|
|
28
|
+
</testsuites>`;
|
|
29
|
+
|
|
30
|
+
describe("ci-unit-test-report", () => {
|
|
31
|
+
test("parses describe chains, statuses, durations and XML entities", () => {
|
|
32
|
+
expect(parseJunit(JUNIT)).toEqual([
|
|
33
|
+
{
|
|
34
|
+
file: "src/a.test.ts",
|
|
35
|
+
name: 'outer > inner & "nested" > slow one',
|
|
36
|
+
ms: 120,
|
|
37
|
+
status: "pass",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
file: "src/a.test.ts",
|
|
41
|
+
name: 'outer > inner & "nested" > breaks <sometimes>',
|
|
42
|
+
ms: 4,
|
|
43
|
+
status: "fail",
|
|
44
|
+
},
|
|
45
|
+
{ file: "src/a.test.ts", name: "outer > fast", ms: 0, status: "pass" },
|
|
46
|
+
{ file: "src/a.test.ts", name: "outer > later", ms: 0, status: "skip" },
|
|
47
|
+
{ file: "src/b.test.ts", name: "top level", ms: 60, status: "pass" },
|
|
48
|
+
{ file: "src/b.test.ts", name: "skipped", ms: 0, status: "skip" },
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("keeps every non-pass and every test at or above minMs, with per-file totals", () => {
|
|
53
|
+
const payload = buildPayload(parseJunit(JUNIT), new Map([["src/a.test.ts", "abc123"]]), 50);
|
|
54
|
+
expect(payload.totals).toEqual({ tests: 6, pass: 3, fail: 1, skip: 2, testMs: 184 });
|
|
55
|
+
expect(payload.files).toEqual([
|
|
56
|
+
["src/a.test.ts", "abc123", 4, 1, 124],
|
|
57
|
+
["src/b.test.ts", null, 2, 0, 60],
|
|
58
|
+
]);
|
|
59
|
+
expect(payload.tests).toEqual([
|
|
60
|
+
[0, 'outer > inner & "nested" > slow one', 120, "p"],
|
|
61
|
+
[0, 'outer > inner & "nested" > breaks <sometimes>', 4, "f"],
|
|
62
|
+
[0, "outer > later", 0, "s"],
|
|
63
|
+
[1, "top level", 60, "p"],
|
|
64
|
+
[1, "skipped", 0, "s"],
|
|
65
|
+
]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("an empty report parses to no tests", () => {
|
|
69
|
+
expect(
|
|
70
|
+
parseJunit('<?xml version="1.0"?><testsuites name="bun test" tests="0"></testsuites>'),
|
|
71
|
+
).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { appendFile, mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { ClaudeAdapter } from "../providers/claude-adapter";
|
|
6
|
+
import { BACKGROUND_KEEPALIVE_INTERVAL_MS } from "../providers/claude-background-keepalive";
|
|
7
|
+
import type { ProviderResult, ProviderSession, ProviderSessionConfig } from "../providers/types";
|
|
8
|
+
import { CHILD_PROCESS_TEST_BUDGET_MS } from "./test-proc";
|
|
9
|
+
|
|
10
|
+
// These tests go through ClaudeAdapter.createSession, so they cover the wiring
|
|
11
|
+
// in each transport: observe() on every parsed message and stop() at teardown.
|
|
12
|
+
// A fake Claude binary ends its turn and goes silent; a local server records the
|
|
13
|
+
// session heartbeats the keepalive sends while it waits.
|
|
14
|
+
|
|
15
|
+
const fixturePath = join(import.meta.dir, "claude-background-keepalive.fixture.ts");
|
|
16
|
+
const TICK_MS = 20;
|
|
17
|
+
|
|
18
|
+
// The keepalive pings every 60s. Run its interval at TICK_MS instead; every
|
|
19
|
+
// other timer keeps its real period.
|
|
20
|
+
const realSetInterval = globalThis.setInterval;
|
|
21
|
+
beforeAll(() => {
|
|
22
|
+
globalThis.setInterval = ((handler: TimerHandler, timeout?: number, ...rest: unknown[]) =>
|
|
23
|
+
realSetInterval(
|
|
24
|
+
handler,
|
|
25
|
+
timeout === BACKGROUND_KEEPALIVE_INTERVAL_MS ? TICK_MS : timeout,
|
|
26
|
+
...rest,
|
|
27
|
+
)) as typeof setInterval;
|
|
28
|
+
});
|
|
29
|
+
afterAll(() => {
|
|
30
|
+
globalThis.setInterval = realSetInterval;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const heartbeats = new Map<string, number>();
|
|
34
|
+
const server = Bun.serve({
|
|
35
|
+
port: 0,
|
|
36
|
+
fetch(req) {
|
|
37
|
+
const match = new URL(req.url).pathname.match(/^\/api\/active-sessions\/heartbeat\/(.+)$/);
|
|
38
|
+
if (req.method === "PUT" && match?.[1]) {
|
|
39
|
+
const taskId = decodeURIComponent(match[1]);
|
|
40
|
+
heartbeats.set(taskId, (heartbeats.get(taskId) ?? 0) + 1);
|
|
41
|
+
}
|
|
42
|
+
return Response.json({ updated: true });
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
afterAll(() => server.stop(true));
|
|
46
|
+
|
|
47
|
+
const temporaryDirectories: string[] = [];
|
|
48
|
+
let originalHome: string | undefined;
|
|
49
|
+
|
|
50
|
+
beforeEach(async () => {
|
|
51
|
+
// Every session seeds Claude trust into `$HOME/.claude.json`; keep that off the real home.
|
|
52
|
+
originalHome = process.env.HOME;
|
|
53
|
+
const home = await mkdtemp(join(tmpdir(), "claude-bg-keepalive-home-"));
|
|
54
|
+
temporaryDirectories.push(home);
|
|
55
|
+
process.env.HOME = home;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterEach(async () => {
|
|
59
|
+
if (originalHome === undefined) delete process.env.HOME;
|
|
60
|
+
else process.env.HOME = originalHome;
|
|
61
|
+
await Promise.all(
|
|
62
|
+
temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true, force: true })),
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
type Transport = "cli" | "sdk";
|
|
67
|
+
|
|
68
|
+
interface Harness {
|
|
69
|
+
session: ProviderSession;
|
|
70
|
+
taskId: string;
|
|
71
|
+
send(command: "empty" | "exit" | "fail"): Promise<void>;
|
|
72
|
+
count(): number;
|
|
73
|
+
completed(): boolean;
|
|
74
|
+
turnEnded(): Promise<void>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function startSession(transport: Transport, withBackgroundTask: boolean): Promise<Harness> {
|
|
78
|
+
const directory = await mkdtemp(join(tmpdir(), "claude-bg-keepalive-"));
|
|
79
|
+
temporaryDirectories.push(directory);
|
|
80
|
+
const controlFile = join(directory, "control");
|
|
81
|
+
const logFile = join(directory, "session.jsonl");
|
|
82
|
+
const taskId = crypto.randomUUID();
|
|
83
|
+
const config: ProviderSessionConfig = {
|
|
84
|
+
prompt: "wait on the background job",
|
|
85
|
+
systemPrompt: "",
|
|
86
|
+
model: "claude-haiku-4-5",
|
|
87
|
+
role: "worker",
|
|
88
|
+
agentId: crypto.randomUUID(),
|
|
89
|
+
taskId,
|
|
90
|
+
apiUrl: `http://127.0.0.1:${server.port}`,
|
|
91
|
+
apiKey: "test-key",
|
|
92
|
+
cwd: directory,
|
|
93
|
+
logFile,
|
|
94
|
+
env: {
|
|
95
|
+
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
|
96
|
+
CLAUDE_CODE_OAUTH_TOKEN: "example-fixture-oauth-token",
|
|
97
|
+
CLAUDE_TRANSPORT: transport,
|
|
98
|
+
CLAUDE_BINARY: `${process.execPath} ${fixturePath}`,
|
|
99
|
+
BG_FIXTURE_TRANSPORT: transport,
|
|
100
|
+
BG_FIXTURE_CONTROL: controlFile,
|
|
101
|
+
BG_FIXTURE_BACKGROUND: withBackgroundTask ? "1" : "0",
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
const session = await new ClaudeAdapter(async () => {}).createSession(config);
|
|
105
|
+
let completed = false;
|
|
106
|
+
void session.waitForCompletion().finally(() => {
|
|
107
|
+
completed = true;
|
|
108
|
+
});
|
|
109
|
+
return {
|
|
110
|
+
session,
|
|
111
|
+
taskId,
|
|
112
|
+
completed: () => completed,
|
|
113
|
+
send: (command) => appendFile(controlFile, `${command}\n`),
|
|
114
|
+
count: () => heartbeats.get(taskId) ?? 0,
|
|
115
|
+
// Both transports log every protocol line; the result line marks the end of the turn.
|
|
116
|
+
turnEnded: () =>
|
|
117
|
+
waitFor(async () => {
|
|
118
|
+
const file = Bun.file(logFile);
|
|
119
|
+
return (await file.exists()) && (await file.text()).includes('"type":"result"');
|
|
120
|
+
}, "the fixture's result message"),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function waitFor(
|
|
125
|
+
condition: () => boolean | Promise<boolean>,
|
|
126
|
+
what: string,
|
|
127
|
+
timeoutMs = 5_000,
|
|
128
|
+
): Promise<void> {
|
|
129
|
+
const deadline = Date.now() + timeoutMs;
|
|
130
|
+
while (!(await condition())) {
|
|
131
|
+
if (Date.now() >= deadline) throw new Error(`Timed out waiting for ${what}`);
|
|
132
|
+
await Bun.sleep(TICK_MS / 2);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Waits out in-flight requests, then asserts no further heartbeat arrives. */
|
|
137
|
+
async function expectHeartbeatsStopped(harness: Harness): Promise<void> {
|
|
138
|
+
await Bun.sleep(TICK_MS * 3);
|
|
139
|
+
const settled = harness.count();
|
|
140
|
+
await Bun.sleep(TICK_MS * 8);
|
|
141
|
+
expect(harness.count()).toBe(settled);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function liveSessionWithHeartbeats(transport: Transport): Promise<Harness> {
|
|
145
|
+
const harness = await startSession(transport, true);
|
|
146
|
+
await harness.turnEnded();
|
|
147
|
+
// The turn is over and the process is silent; only the keepalive is sending.
|
|
148
|
+
await waitFor(() => harness.count() >= 3, "keepalive heartbeats after the turn ended");
|
|
149
|
+
return harness;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
describe.each(["cli", "sdk"] as const)("Claude %s transport background keepalive", (transport) => {
|
|
153
|
+
test(
|
|
154
|
+
"heartbeats while a background task stays live after the turn ends, and stops when the set empties",
|
|
155
|
+
async () => {
|
|
156
|
+
const harness = await liveSessionWithHeartbeats(transport);
|
|
157
|
+
try {
|
|
158
|
+
await harness.send("empty");
|
|
159
|
+
await expectHeartbeatsStopped(harness);
|
|
160
|
+
// The session is still running: the empty set stopped the pings, not teardown.
|
|
161
|
+
expect(harness.completed()).toBe(false);
|
|
162
|
+
} finally {
|
|
163
|
+
await harness.send("exit");
|
|
164
|
+
await harness.session.waitForCompletion();
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
CHILD_PROCESS_TEST_BUDGET_MS,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
test(
|
|
171
|
+
"normal end stops the keepalive",
|
|
172
|
+
async () => {
|
|
173
|
+
const harness = await liveSessionWithHeartbeats(transport);
|
|
174
|
+
await harness.send("exit");
|
|
175
|
+
const result = await harness.session.waitForCompletion();
|
|
176
|
+
expect(result.isError).toBe(false);
|
|
177
|
+
await expectHeartbeatsStopped(harness);
|
|
178
|
+
},
|
|
179
|
+
CHILD_PROCESS_TEST_BUDGET_MS,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
test(
|
|
183
|
+
"error end stops the keepalive",
|
|
184
|
+
async () => {
|
|
185
|
+
const harness = await liveSessionWithHeartbeats(transport);
|
|
186
|
+
await harness.send("fail");
|
|
187
|
+
const result: ProviderResult = await harness.session.waitForCompletion();
|
|
188
|
+
expect(result.exitCode).not.toBe(0);
|
|
189
|
+
await expectHeartbeatsStopped(harness);
|
|
190
|
+
},
|
|
191
|
+
CHILD_PROCESS_TEST_BUDGET_MS,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
test(
|
|
195
|
+
"abort stops the keepalive",
|
|
196
|
+
async () => {
|
|
197
|
+
const harness = await liveSessionWithHeartbeats(transport);
|
|
198
|
+
await harness.session.abort();
|
|
199
|
+
await harness.session.waitForCompletion();
|
|
200
|
+
await expectHeartbeatsStopped(harness);
|
|
201
|
+
},
|
|
202
|
+
CHILD_PROCESS_TEST_BUDGET_MS,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
test(
|
|
206
|
+
"a session with no background work sends no keepalive heartbeats",
|
|
207
|
+
async () => {
|
|
208
|
+
const harness = await startSession(transport, false);
|
|
209
|
+
try {
|
|
210
|
+
await harness.turnEnded();
|
|
211
|
+
await Bun.sleep(TICK_MS * 10);
|
|
212
|
+
expect(harness.count()).toBe(0);
|
|
213
|
+
} finally {
|
|
214
|
+
await harness.send("exit");
|
|
215
|
+
await harness.session.waitForCompletion();
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
CHILD_PROCESS_TEST_BUDGET_MS,
|
|
219
|
+
);
|
|
220
|
+
});
|