@desplega.ai/agent-swarm 1.162.0 → 1.164.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/README.md +2 -1
- package/dist/{acp-adapter-74m73ae9.js → acp-adapter-8t22d18g.js} +2 -2
- package/dist/{actions-4sr14gnt.js → actions-49xrhww7.js} +32 -29
- package/dist/amp-adapter-p230tajt.js +34 -0
- package/dist/{anthropic-messages-8xxrzky9.js → anthropic-messages-hst62ws9.js} +3 -3
- package/dist/{app-pt68fbwp.js → app-tgzw3f3d.js} +11 -10
- package/dist/{assistant-cv3jytks.js → assistant-dgfjm07j.js} +33 -30
- package/dist/{azure-openai-responses-f5cqsyj8.js → azure-openai-responses-mh8tvf78.js} +9 -68
- package/dist/{boot-reembed-4ht4gqth.js → boot-reembed-0qzs614r.js} +13 -12
- package/dist/{boot-reembed-nxpcvpj9.js → boot-reembed-dgp6peyx.js} +12 -11
- package/dist/{boot-scrub-logs-k93kyw8k.js → boot-scrub-logs-fxnk021a.js} +10 -9
- package/dist/{boot-seeds-1skcrs0e.js → boot-seeds-xe7adjvm.js} +14 -13
- package/dist/{claude-adapter-r5g0tzge.js → claude-adapter-hx0n8a31.js} +22 -20
- package/dist/{claude-managed-adapter-yxkfxg8j.js → claude-managed-adapter-hskapeen.js} +3 -3
- package/dist/{claude-managed-setup-pkyrgshb.js → claude-managed-setup-p7mt6dfq.js} +1 -1
- package/dist/{claude-sdk-session-jxyxnrwd.js → claude-sdk-session-d61a9c8x.js} +22 -20
- package/dist/{cli-twrh04ap.js → cli-0y30kqz4.js} +3 -3
- package/dist/{cli-5xhpb3kv.js → cli-0z9dj97t.js} +9 -9
- package/dist/{cli-t845n9xk.js → cli-1nzb8ftr.js} +2 -2
- package/dist/{cli-kdkwd3yr.js → cli-1wd1n2ya.js} +38 -10
- package/dist/{cli-vxe6qa4h.js → cli-23az412e.js} +1 -1
- package/dist/{cli-bsky8g7w.js → cli-26x4d89j.js} +1 -1
- package/dist/{cli-swfmp1fx.js → cli-2ar0x7cz.js} +9 -9
- package/dist/cli-2ksgp01n.js +933 -0
- package/dist/{cli-rnyzn44p.js → cli-2zhhnz2h.js} +1 -1
- package/dist/{cli-vc9bp061.js → cli-3qaw32we.js} +5 -5
- package/dist/{cli-vy3ed88x.js → cli-402m8btc.js} +31 -3
- package/dist/{cli-5bcd7sfy.js → cli-43fh8fzw.js} +3 -3
- package/dist/{cli-5k6k8ta3.js → cli-4f7s6k3q.js} +13 -5
- package/dist/{cli-sqvfa090.js → cli-4ntg6tkp.js} +6 -5
- package/dist/{cli-kh04eqwe.js → cli-4y9bnpvy.js} +1 -1
- package/dist/{cli-qjjvzwzd.js → cli-5c2j5yvx.js} +1 -1
- package/dist/{cli-2zy95vhm.js → cli-6891ft0y.js} +2 -2
- package/dist/{cli-tre4bdgq.js → cli-6behs2bt.js} +1 -1
- package/dist/cli-7mm2r1kp.js +17 -0
- package/dist/cli-7xcgj20z.js +68 -0
- package/dist/{cli-4cwxn7gp.js → cli-812vt3ht.js} +2 -2
- package/dist/{cli-mbssg1xv.js → cli-8kt0bmgx.js} +1 -1
- package/dist/{cli-wjgs5wxe.js → cli-8rpq35c0.js} +7 -7
- package/dist/{cli-jxs2ygaw.js → cli-9m8fk2v7.js} +74 -29
- package/dist/{cli-pkd6s2wy.js → cli-9q3wt23f.js} +2 -2
- package/dist/{cli-s3hwpt9v.js → cli-9s6fsg07.js} +30 -10
- package/dist/{cli-xqpk3rp7.js → cli-9x3bwfd3.js} +28 -5
- package/dist/{cli-qrgt5bqf.js → cli-agamgm0p.js} +1 -1
- package/dist/{cli-13hw4z91.js → cli-b0f70z24.js} +1 -1
- package/dist/{cli-gpqpmns7.js → cli-cgrvsz2t.js} +2 -2
- package/dist/{cli-2hhfhfq5.js → cli-cn53e7vc.js} +8 -1
- package/dist/{cli-p0r8sxj5.js → cli-crjazzsg.js} +222 -4
- package/dist/{cli-tcen9p5w.js → cli-d37n8b7c.js} +5 -5
- package/dist/{cli-wxkkd0e0.js → cli-dgh781z9.js} +3 -3
- package/dist/cli-dsdg2wrk.js +516 -0
- package/dist/{cli-q24re23n.js → cli-e2xnc4m5.js} +4 -4
- package/dist/{cli-f7kvq46p.js → cli-erxeg64j.js} +20 -0
- package/dist/{cli-8ev9xqnx.js → cli-fajbc7sa.js} +17 -219
- package/dist/{cli-g739zqzt.js → cli-fj86xr3c.js} +2 -2
- package/dist/{cli-zvhd3f64.js → cli-fpbvcf55.js} +3 -3
- package/dist/{cli-kp42yek0.js → cli-fyra5y8k.js} +424 -71
- package/dist/{cli-jyy6pm2f.js → cli-g3qgrzck.js} +15 -3
- package/dist/{cli-eg0b3p0k.js → cli-g5erf0zt.js} +23 -14
- package/dist/{cli-cvatsam7.js → cli-gcvzvq3e.js} +4 -4
- package/dist/{cli-89s4ngw3.js → cli-gvcgmx69.js} +1 -1
- package/dist/{cli-7tre70zp.js → cli-j261c831.js} +138 -128
- package/dist/{cli-sptrj5f5.js → cli-j3me0jw2.js} +2 -2
- package/dist/{cli-zxqx25nz.js → cli-k6w44yvq.js} +6 -6
- package/dist/cli-ndx5yz5n.js +3299 -0
- package/dist/{cli-ykk5jaax.js → cli-nexwh8zg.js} +34 -29
- package/dist/{cli-rfjw9jw2.js → cli-ng97fjq7.js} +2 -2
- package/dist/{cli-4x0csdjq.js → cli-ngne1dng.js} +2 -2
- package/dist/{cli-4dcpaefp.js → cli-pfzrhb8n.js} +2 -2
- package/dist/{cli-hb5c2x5y.js → cli-q38n4bq0.js} +2 -2
- package/dist/{cli-cebmgwsc.js → cli-r109nq7n.js} +4 -4
- package/dist/{cli-nq7yqw7b.js → cli-t7g0vmtc.js} +4 -4
- package/dist/{cli-jf7wmv1r.js → cli-tsvj33y0.js} +58 -19
- package/dist/{cli-7af8yhav.js → cli-vbr859rz.js} +7 -4
- package/dist/{cli-nt8hecg9.js → cli-wk3v6h55.js} +67 -38
- package/dist/{cli-vyqs9ya8.js → cli-wwvk0tsy.js} +7 -7
- package/dist/{cli-rj0gqwwp.js → cli-y2de3bzp.js} +1 -1
- package/dist/{cli-hmsw1gpy.js → cli-y74170pb.js} +4 -4
- package/dist/cli-ydt3zkwv.js +70 -0
- package/dist/{cli-xk79303e.js → cli-yj790e19.js} +9 -1
- package/dist/cli-zt3w1cn1.js +137 -0
- package/dist/{cli-trc24wk2.js → cli-zxg80zs9.js} +1 -1
- package/dist/cli.js +23 -22
- package/dist/{cloudflare-workers-ai-system-one-k8ddqjkq.js → cloudflare-workers-ai-system-one-tt484t61.js} +2 -2
- package/dist/{codex-adapter-esvmnhn7.js → codex-adapter-e3pky8aj.js} +14 -13
- package/dist/{codex-hook-m73jmqgx.js → codex-hook-b1fb7c2k.js} +4 -3
- package/dist/{codex-session-runner-ban9excr.js → codex-session-runner-1gq27w2t.js} +14 -13
- package/dist/{commands-75413p2n.js → commands-q07ezjzn.js} +10 -9
- package/dist/cursor-adapter-50cz7yba.js +32 -0
- package/dist/{db-7f5pecwg.js → db-wwt0sq9q.js} +18 -9
- package/dist/{dispatcher-fh9at8nz.js → dispatcher-wrg35vdd.js} +31 -28
- package/dist/{dsh-adapter-wpze65d4.js → dsh-adapter-sa6rxfpc.js} +6 -6
- package/dist/{e2b-gjgdjp91.js → e2b-v5np73yj.js} +4 -4
- package/dist/{easter-egg-3d-3w5c2n4c.js → easter-egg-3d-h2hx9b64.js} +2 -2
- package/dist/{event-bus-sfx8dx3t.js → event-bus-vkbhzt4n.js} +1 -1
- package/dist/{execute-f50wg9nh.js → execute-6gczxy8v.js} +42 -13
- package/dist/{google-generative-ai-jxxpnegy.js → google-generative-ai-d6nphg3q.js} +3 -3
- package/dist/{google-vertex-x7q1angm.js → google-vertex-cpshkref.js} +3 -3
- package/dist/{handlers-w13787bb.js → handlers-rv3gxvsp.js} +34 -31
- package/dist/{hook-46g1jrvx.js → hook-bcf5m80y.js} +15 -13
- package/dist/{hook-4tpts2ry.js → hook-z8b1jm2d.js} +16 -14
- package/dist/{http-19pzbfrt.js → http-56f7yt97.js} +932 -134
- package/dist/{index-scptvdxg.js → index-0kncjnrm.js} +30 -23
- package/dist/{index-dqmp15bq.js → index-1e6cg96n.js} +31 -28
- package/dist/{index-qb4q1y3e.js → index-8xgdb3k8.js} +31 -28
- package/dist/index-adw4zr9j.js +1020 -0
- package/dist/{index-h5gs261z.js → index-etdrp7yp.js} +140 -140
- package/dist/{index-8fd4dm4h.js → index-f9zscmft.js} +32 -29
- package/dist/index-fdk1t4d6.js +91297 -0
- package/dist/{index-s75wc4wd.js → index-jvb37h77.js} +12 -11
- package/dist/{keepalive-xa5ts9cd.js → keepalive-rp8daj4s.js} +12 -11
- package/dist/{lead-qp4xkvjq.js → lead-dfp77erc.js} +45 -40
- package/dist/{maintenance-rm5885hm.js → maintenance-wtmw3jqv.js} +13 -12
- package/dist/{mistral-conversations-rw7fbt22.js → mistral-conversations-8445ddjn.js} +2 -2
- package/dist/{oauth-refresh-sweep-1csdfebf.js → oauth-refresh-sweep-yhzqt95v.js} +11 -10
- package/dist/{onboard-r6b78cs4.js → onboard-9z8rwbcy.js} +2 -2
- package/dist/{openai-codex-responses-efvrgrq3.js → openai-codex-responses-9ynm88zy.js} +3 -3
- package/dist/{openai-completions-bbbrrz4z.js → openai-completions-nhqwjmaw.js} +2 -2
- package/dist/{openai-responses-9jjyrcm1.js → openai-responses-xx1c4kgm.js} +3 -3
- package/dist/{opencode-adapter-kb3g6pkf.js → opencode-adapter-1nxb6555.js} +5 -5
- package/dist/{otel-impl-0y9knpky.js → otel-impl-b83gaxw2.js} +1 -1
- package/dist/{pi-mono-adapter-z6s1bme2.js → pi-mono-adapter-ddxz8pjp.js} +18 -17
- package/dist/{pricing-refresh-mth26tgp.js → pricing-refresh-tmjvazb6.js} +12 -11
- package/dist/{rbac-roles-kwntb23q.js → rbac-roles-pa0gzjws.js} +11 -10
- package/dist/{rbac-roles-vpk4ebm8.js → rbac-roles-rhexa9s2.js} +12 -11
- package/dist/{render-v2-hcm7h00y.js → render-v2-ask84n8y.js} +11 -10
- package/dist/{runtime-vfjsfsc5.js → runtime-ckjnj3vf.js} +4 -4
- package/dist/{setup-hbyzd65h.js → setup-5ga5gdsy.js} +2 -2
- package/dist/{typesafe-system-one-2ef30pec.js → typesafe-system-one-datfr82p.js} +2 -2
- package/dist/{virtual-modules-9bv7ry91.js → virtual-modules-bmp3zwjf.js} +12 -11
- package/dist/{work-objects-m09sxnhe.js → work-objects-snnh0t7j.js} +34 -31
- package/dist/{worker-0h35rvk4.js → worker-rxyymrhb.js} +45 -40
- package/openapi.json +997 -39
- package/package.json +6 -5
- package/src/be/db/active-sessions.ts +30 -0
- package/src/be/db/approvals.ts +54 -0
- package/src/be/db.ts +25 -4
- package/src/be/mcp-server-checked-update.ts +34 -0
- package/src/be/mcp-server-stdio-gate.ts +34 -0
- package/src/be/memory/key-browser.ts +304 -0
- package/src/be/memory/providers/sqlite-store.ts +16 -3
- package/src/be/migrations/194_memory_orphan_chunks.sql +15 -0
- package/src/be/migrations/195_approval_request_approvals.sql +6 -0
- package/src/be/model-validation.ts +12 -2
- package/src/be/multi-runtime.ts +4 -3
- package/src/be/pricing-normalize.ts +11 -0
- package/src/be/repo-checked-update.ts +37 -0
- package/src/be/repo-merge-policy.ts +15 -0
- package/src/be/seed-pricing.ts +48 -1
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/commands/provider-credentials.ts +44 -2
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +100 -0
- package/src/extensions/dispatcher.ts +19 -0
- package/src/extensions/post-bridge.ts +191 -0
- package/src/github/handlers.ts +128 -2
- package/src/gitlab/workflow-action-name.ts +15 -0
- package/src/heartbeat/heartbeat.ts +4 -3
- package/src/http/active-sessions.ts +6 -5
- package/src/http/agents.ts +10 -1
- package/src/http/approval-requests.ts +271 -35
- package/src/http/index.ts +1 -1
- package/src/http/mcp-servers.ts +74 -50
- package/src/http/memory.ts +275 -6
- package/src/http/repos.ts +58 -18
- package/src/http/request-principal.ts +19 -0
- package/src/http/session-cost-recompute.ts +64 -0
- package/src/http/session-data.ts +11 -0
- package/src/http/tasks.ts +9 -1
- package/src/http/webhooks.ts +17 -5
- package/src/prompts/base-prompt.ts +9 -4
- package/src/prompts/resolver.ts +1 -1
- package/src/prompts/session-templates.ts +28 -0
- package/src/providers/amp-adapter.ts +1232 -0
- package/src/providers/cursor-adapter.ts +638 -0
- package/src/providers/index.ts +9 -1
- package/src/providers/pricing-sources.md +39 -0
- package/src/providers/reasoning-effort.ts +15 -1
- package/src/providers/types.ts +11 -1
- package/src/rbac/legacy-policy.ts +23 -8
- package/src/rbac/permissions.ts +27 -1
- package/src/scripts-runtime/types/swarm-extension.d.ts +119 -0
- package/src/tests/amp-adapter.test.ts +822 -0
- package/src/tests/amp-budget-export-failed.test.ts +133 -0
- package/src/tests/amp-pricing-helpers.ts +16 -0
- package/src/tests/amp-pricing.test.ts +218 -0
- package/src/tests/api-drain-worker-e2e.test.ts +18 -7
- package/src/tests/approval-respond-auth.test.ts +371 -0
- package/src/tests/base-prompt.test.ts +13 -0
- package/src/tests/claude-adapter-binary.test.ts +24 -0
- package/src/tests/claude-adapter.test.ts +8 -0
- package/src/tests/cursor-adapter.test.ts +690 -0
- package/src/tests/db-task-read-contract.test.ts +5 -0
- package/src/tests/db-task-write-contract.test.ts +5 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/extensions-post-bridge.test.ts +669 -0
- package/src/tests/extensions-run-context.test.ts +61 -0
- package/src/tests/fixtures/fake-amp.ts +304 -0
- package/src/tests/github-handlers-inline-comments.test.ts +147 -1
- package/src/tests/gitlab-workflow-event-names.test.ts +136 -0
- package/src/tests/mcp-input-ergonomics.test.ts +15 -10
- package/src/tests/mcp-server-stdio-gate.test.ts +438 -0
- package/src/tests/memory-http-access.test.ts +221 -0
- package/src/tests/memory-keys-endpoint.test.ts +406 -0
- package/src/tests/memory-store.test.ts +166 -0
- package/src/tests/model-tier-resolution.test.ts +3 -3
- package/src/tests/pricing-refresh.test.ts +5 -4
- package/src/tests/prompt-template-session.test.ts +5 -3
- package/src/tests/provider-process-group.test.ts +41 -0
- package/src/tests/provider-registration.test.ts +3 -1
- package/src/tests/rbac-charact-misc-tools.test.ts +18 -3
- package/src/tests/rbac-engine.test.ts +15 -11
- package/src/tests/rbac-wire-e2e.test.ts +4 -9
- package/src/tests/repo-allow-merge-gate.test.ts +382 -0
- package/src/tests/script-connections.test.ts +4 -0
- package/src/tests/send-task-requester-gate.test.ts +260 -0
- package/src/tests/session-activity-liveness.test.ts +248 -0
- package/src/tests/session-costs-golden.test.ts +28 -0
- package/src/tests/webhooks-task-blocked.test.ts +4 -4
- package/src/tests/write-gate.ts +68 -0
- package/src/tests/x402-client.test.ts +124 -0
- package/src/tests/x402-spending-tracker.test.ts +66 -0
- package/src/tools/mcp-servers/mcp-server-create.ts +23 -1
- package/src/tools/mcp-servers/mcp-server-update.ts +46 -28
- package/src/tools/memory-delete.ts +3 -3
- package/src/tools/repos/update-repo.ts +26 -5
- package/src/tools/send-task.ts +24 -1
- package/src/tools/steer-task.ts +1 -1
- package/src/types.ts +37 -2
- package/src/utils/amp-models.ts +114 -0
- package/src/utils/credentials.ts +8 -0
- package/src/utils/process-group.ts +79 -0
- package/src/workflows/event-bus.ts +28 -0
- package/src/x402/client.ts +36 -14
- package/src/x402/spending-tracker.ts +64 -3
- package/templates/skills/agent-browser/SKILL.md +5 -0
- package/templates/skills/agent-browser/content.md +5 -0
- package/templates/skills/heartbeat-runbook/content.md +1 -0
- package/templates/skills/swarm-extensions/content.md +4 -0
- package/dist/cli-y89t4nca.js +0 -67
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who may answer an approval request, and who the answer is recorded as.
|
|
3
|
+
*
|
|
4
|
+
* Drives the real `handleApprovalRequests` handler behind the same auth
|
|
5
|
+
* resolution `handleCore` performs (`resolveHttpRequestAuth` + `setRequestAuth`),
|
|
6
|
+
* so each case presents a real credential: the shared key, the shared key with
|
|
7
|
+
* an `X-Agent-ID`, an `aswt_` user token, an `aseph_` agent session token, or a
|
|
8
|
+
* page session.
|
|
9
|
+
*/
|
|
10
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
11
|
+
import { unlink } from "node:fs/promises";
|
|
12
|
+
import { createServer, type Server } from "node:http";
|
|
13
|
+
import {
|
|
14
|
+
closeDb,
|
|
15
|
+
createAgent,
|
|
16
|
+
createApprovalRequest,
|
|
17
|
+
createPage,
|
|
18
|
+
createUser,
|
|
19
|
+
getApprovalRequestById,
|
|
20
|
+
initDb,
|
|
21
|
+
} from "../be/db";
|
|
22
|
+
import { type IdentityActor, mintSessionToken, mintToken } from "../be/users";
|
|
23
|
+
import { handleApprovalRequests } from "../http/approval-requests";
|
|
24
|
+
import { resolveHttpRequestAuth } from "../http/auth";
|
|
25
|
+
import { signPageSession } from "../utils/page-session";
|
|
26
|
+
import { setRequestAuth } from "../utils/request-auth-context";
|
|
27
|
+
import { listenOnFreePort } from "./test-net";
|
|
28
|
+
|
|
29
|
+
const TEST_DB_PATH = "./test-approval-respond-auth.sqlite";
|
|
30
|
+
const API_KEY = "approval-respond-auth-test-key";
|
|
31
|
+
const ACTOR: IdentityActor = { kind: "operator", id: "approval-respond-auth-test" };
|
|
32
|
+
|
|
33
|
+
let server: Server;
|
|
34
|
+
let baseUrl = "";
|
|
35
|
+
let workerId = "";
|
|
36
|
+
let leadId = "";
|
|
37
|
+
let alice: { id: string; email: string; token: string };
|
|
38
|
+
let bob: { id: string; email: string; token: string };
|
|
39
|
+
let carol: { id: string; token: string };
|
|
40
|
+
let workerSessionToken = "";
|
|
41
|
+
let pageId = "";
|
|
42
|
+
|
|
43
|
+
function createAuthedServer(): Server {
|
|
44
|
+
return createServer(async (req, res) => {
|
|
45
|
+
const auth = await resolveHttpRequestAuth(req, API_KEY);
|
|
46
|
+
if (!auth) {
|
|
47
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
48
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
setRequestAuth(req, auth);
|
|
52
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
53
|
+
const handled = await handleApprovalRequests(
|
|
54
|
+
req,
|
|
55
|
+
res,
|
|
56
|
+
url.pathname.split("/").filter(Boolean),
|
|
57
|
+
url.searchParams,
|
|
58
|
+
);
|
|
59
|
+
if (!handled) {
|
|
60
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
61
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type Approvers = {
|
|
67
|
+
users?: string[];
|
|
68
|
+
roles?: string[];
|
|
69
|
+
policy: "any" | "all" | { min: number };
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
async function pendingRequest(approvers: Approvers = { policy: "any" }) {
|
|
73
|
+
return createApprovalRequest({
|
|
74
|
+
id: crypto.randomUUID(),
|
|
75
|
+
title: "Ship the release",
|
|
76
|
+
questions: [{ id: "q1", type: "approval", label: "Approve?", required: true }],
|
|
77
|
+
approvers,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function respond(
|
|
82
|
+
id: string,
|
|
83
|
+
headers: Record<string, string>,
|
|
84
|
+
body: Record<string, unknown> = { responses: { q1: { approved: true } } },
|
|
85
|
+
) {
|
|
86
|
+
const res = await fetch(`${baseUrl}/api/approval-requests/${id}/respond`, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
89
|
+
body: JSON.stringify(body),
|
|
90
|
+
});
|
|
91
|
+
return { status: res.status, body: (await res.json()) as Record<string, any> };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const operator = () => ({ Authorization: `Bearer ${API_KEY}` });
|
|
95
|
+
const asUser = (token: string) => ({ Authorization: `Bearer ${token}` });
|
|
96
|
+
|
|
97
|
+
beforeAll(async () => {
|
|
98
|
+
try {
|
|
99
|
+
await unlink(TEST_DB_PATH);
|
|
100
|
+
} catch {}
|
|
101
|
+
initDb(TEST_DB_PATH);
|
|
102
|
+
|
|
103
|
+
workerId = crypto.randomUUID();
|
|
104
|
+
leadId = crypto.randomUUID();
|
|
105
|
+
await createAgent({ id: workerId, name: "respond-worker", isLead: false, status: "idle" });
|
|
106
|
+
await createAgent({ id: leadId, name: "respond-lead", isLead: true, status: "idle" });
|
|
107
|
+
|
|
108
|
+
const a = await createUser({
|
|
109
|
+
name: "Alice",
|
|
110
|
+
email: "alice@example.com",
|
|
111
|
+
role: "release-manager",
|
|
112
|
+
});
|
|
113
|
+
const b = await createUser({ name: "Bob", email: "bob@example.com" });
|
|
114
|
+
const c = await createUser({ name: "Carol", role: "release-manager" });
|
|
115
|
+
alice = {
|
|
116
|
+
id: a.id,
|
|
117
|
+
email: "alice@example.com",
|
|
118
|
+
token: (await mintToken(a.id, "alice", ACTOR)).plaintext,
|
|
119
|
+
};
|
|
120
|
+
bob = {
|
|
121
|
+
id: b.id,
|
|
122
|
+
email: "bob@example.com",
|
|
123
|
+
token: (await mintToken(b.id, "bob", ACTOR)).plaintext,
|
|
124
|
+
};
|
|
125
|
+
carol = { id: c.id, token: (await mintToken(c.id, "carol", ACTOR)).plaintext };
|
|
126
|
+
|
|
127
|
+
workerSessionToken = (await mintSessionToken(workerId, crypto.randomUUID(), 60_000)).plaintext;
|
|
128
|
+
|
|
129
|
+
const page = await createPage({
|
|
130
|
+
agentId: workerId,
|
|
131
|
+
slug: `respond-page-${crypto.randomUUID().slice(0, 8)}`,
|
|
132
|
+
title: "Respond page",
|
|
133
|
+
contentType: "text/html",
|
|
134
|
+
body: "<p>hi</p>",
|
|
135
|
+
});
|
|
136
|
+
pageId = page.id;
|
|
137
|
+
|
|
138
|
+
server = createAuthedServer();
|
|
139
|
+
const port = await listenOnFreePort(server);
|
|
140
|
+
baseUrl = `http://127.0.0.1:${port}`;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
afterAll(async () => {
|
|
144
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
145
|
+
closeDb();
|
|
146
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
147
|
+
try {
|
|
148
|
+
await unlink(`${TEST_DB_PATH}${suffix}`);
|
|
149
|
+
} catch {}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("agent principals cannot answer an approval request", () => {
|
|
154
|
+
test("shared key with X-Agent-ID (worker) is refused and the request stays pending", async () => {
|
|
155
|
+
const request = await pendingRequest();
|
|
156
|
+
const res = await respond(
|
|
157
|
+
request.id,
|
|
158
|
+
{ ...operator(), "X-Agent-ID": workerId },
|
|
159
|
+
{
|
|
160
|
+
responses: { q1: { approved: true } },
|
|
161
|
+
respondedBy: alice.email,
|
|
162
|
+
},
|
|
163
|
+
);
|
|
164
|
+
expect(res.status).toBe(403);
|
|
165
|
+
const stored = await getApprovalRequestById(request.id);
|
|
166
|
+
expect(stored?.status).toBe("pending");
|
|
167
|
+
expect(stored?.resolvedBy).toBeNull();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("shared key with a lead's X-Agent-ID is refused", async () => {
|
|
171
|
+
const request = await pendingRequest();
|
|
172
|
+
const res = await respond(request.id, { ...operator(), "X-Agent-ID": leadId });
|
|
173
|
+
expect(res.status).toBe(403);
|
|
174
|
+
expect((await getApprovalRequestById(request.id))?.status).toBe("pending");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("an aseph_ agent session token is refused", async () => {
|
|
178
|
+
const request = await pendingRequest();
|
|
179
|
+
const res = await respond(request.id, asUser(workerSessionToken));
|
|
180
|
+
expect(res.status).toBe(403);
|
|
181
|
+
expect((await getApprovalRequestById(request.id))?.status).toBe("pending");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("a page session with no signed-in user is refused", async () => {
|
|
185
|
+
const request = await pendingRequest();
|
|
186
|
+
const session = await signPageSession({ pageId, exp: Math.floor(Date.now() / 1000) + 600 });
|
|
187
|
+
const res = await respond(request.id, {
|
|
188
|
+
...operator(),
|
|
189
|
+
"X-Page-Id": pageId,
|
|
190
|
+
"X-Page-Session": session,
|
|
191
|
+
});
|
|
192
|
+
expect(res.status).toBe(403);
|
|
193
|
+
expect((await getApprovalRequestById(request.id))?.status).toBe("pending");
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe("the responder comes from the credential, not the body", () => {
|
|
198
|
+
test("shared key alone resolves as operator; respondedBy is kept only as a claim", async () => {
|
|
199
|
+
const request = await pendingRequest();
|
|
200
|
+
const res = await respond(request.id, operator(), {
|
|
201
|
+
responses: { q1: { approved: true } },
|
|
202
|
+
respondedBy: "someone-else@example.com",
|
|
203
|
+
});
|
|
204
|
+
expect(res.status).toBe(200);
|
|
205
|
+
expect(res.body.approvalRequest.status).toBe("approved");
|
|
206
|
+
expect(res.body.approvalRequest.resolvedBy).toBe("operator");
|
|
207
|
+
expect(res.body.approvalRequest.approvals).toEqual([
|
|
208
|
+
expect.objectContaining({
|
|
209
|
+
responder: "operator",
|
|
210
|
+
approved: true,
|
|
211
|
+
claimedRespondedBy: "someone-else@example.com",
|
|
212
|
+
}),
|
|
213
|
+
]);
|
|
214
|
+
expect((await getApprovalRequestById(request.id))?.resolvedBy).toBe("operator");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("a user token resolves as that user's id, whatever the body claims", async () => {
|
|
218
|
+
const request = await pendingRequest();
|
|
219
|
+
const res = await respond(request.id, asUser(bob.token), {
|
|
220
|
+
responses: { q1: { approved: false } },
|
|
221
|
+
respondedBy: alice.email,
|
|
222
|
+
});
|
|
223
|
+
expect(res.status).toBe(200);
|
|
224
|
+
expect(res.body.approvalRequest.status).toBe("rejected");
|
|
225
|
+
expect(res.body.approvalRequest.resolvedBy).toBe(bob.id);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("a page session signed for a user resolves as that user", async () => {
|
|
229
|
+
const request = await pendingRequest();
|
|
230
|
+
const session = await signPageSession({
|
|
231
|
+
pageId,
|
|
232
|
+
exp: Math.floor(Date.now() / 1000) + 600,
|
|
233
|
+
uid: alice.id,
|
|
234
|
+
});
|
|
235
|
+
const res = await respond(request.id, {
|
|
236
|
+
...operator(),
|
|
237
|
+
"X-Page-Id": pageId,
|
|
238
|
+
"X-Page-Session": session,
|
|
239
|
+
});
|
|
240
|
+
expect(res.status).toBe(200);
|
|
241
|
+
expect(res.body.approvalRequest.resolvedBy).toBe(alice.id);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe("the approvers policy is enforced", () => {
|
|
246
|
+
test("a user outside approvers.users and approvers.roles is refused", async () => {
|
|
247
|
+
const request = await pendingRequest({ users: [alice.id], policy: "any" });
|
|
248
|
+
const res = await respond(request.id, asUser(bob.token));
|
|
249
|
+
expect(res.status).toBe(403);
|
|
250
|
+
expect((await getApprovalRequestById(request.id))?.status).toBe("pending");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("a listed user may answer; a user may also be listed by email", async () => {
|
|
254
|
+
const byId = await pendingRequest({ users: [alice.id], policy: "any" });
|
|
255
|
+
expect((await respond(byId.id, asUser(alice.token))).body.approvalRequest.status).toBe(
|
|
256
|
+
"approved",
|
|
257
|
+
);
|
|
258
|
+
const byEmail = await pendingRequest({ users: [bob.email], policy: "any" });
|
|
259
|
+
expect((await respond(byEmail.id, asUser(bob.token))).body.approvalRequest.status).toBe(
|
|
260
|
+
"approved",
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("a user holding a listed role may answer", async () => {
|
|
265
|
+
const request = await pendingRequest({ roles: ["release-manager"], policy: "any" });
|
|
266
|
+
expect((await respond(request.id, asUser(bob.token))).status).toBe(403);
|
|
267
|
+
const res = await respond(request.id, asUser(carol.token));
|
|
268
|
+
expect(res.status).toBe(200);
|
|
269
|
+
expect(res.body.approvalRequest.resolvedBy).toBe(carol.id);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("the operator key may answer a restricted request", async () => {
|
|
273
|
+
const request = await pendingRequest({ users: [alice.id], policy: "any" });
|
|
274
|
+
const res = await respond(request.id, operator());
|
|
275
|
+
expect(res.status).toBe(200);
|
|
276
|
+
expect(res.body.approvalRequest.resolvedBy).toBe("operator");
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("min: N distinct approvals are needed; a repeat answer from one responder is refused", async () => {
|
|
280
|
+
const request = await pendingRequest({ policy: { min: 2 } });
|
|
281
|
+
|
|
282
|
+
const first = await respond(request.id, asUser(alice.token));
|
|
283
|
+
expect(first.status).toBe(200);
|
|
284
|
+
expect(first.body.approvalRequest.status).toBe("pending");
|
|
285
|
+
expect(first.body.approvalRequest.resolvedBy).toBeNull();
|
|
286
|
+
expect(first.body.approvalRequest.approvalProgress).toEqual({ approved: 1, required: 2 });
|
|
287
|
+
|
|
288
|
+
const repeat = await respond(request.id, asUser(alice.token));
|
|
289
|
+
expect(repeat.status).toBe(409);
|
|
290
|
+
|
|
291
|
+
const second = await respond(request.id, asUser(bob.token));
|
|
292
|
+
expect(second.status).toBe(200);
|
|
293
|
+
expect(second.body.approvalRequest.status).toBe("approved");
|
|
294
|
+
expect(second.body.approvalRequest.resolvedBy).toBe(bob.id);
|
|
295
|
+
expect(second.body.approvalRequest.approvalProgress).toBeNull();
|
|
296
|
+
expect(
|
|
297
|
+
(second.body.approvalRequest.approvals as { responder: string }[]).map((a) => a.responder),
|
|
298
|
+
).toEqual([alice.id, bob.id]);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("concurrent answers to a min-2 request both land and resolve it", async () => {
|
|
302
|
+
const request = await pendingRequest({ policy: { min: 2 } });
|
|
303
|
+
const results = await Promise.all([
|
|
304
|
+
respond(request.id, asUser(alice.token)),
|
|
305
|
+
respond(request.id, asUser(bob.token)),
|
|
306
|
+
]);
|
|
307
|
+
expect(results.map((r) => r.status)).toEqual([200, 200]);
|
|
308
|
+
const stored = await getApprovalRequestById(request.id);
|
|
309
|
+
expect(stored?.status).toBe("approved");
|
|
310
|
+
expect(stored?.approvals?.map((a) => a.responder).sort()).toEqual([alice.id, bob.id].sort());
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("concurrent repeat answers from one responder count once", async () => {
|
|
314
|
+
const request = await pendingRequest({ policy: { min: 2 } });
|
|
315
|
+
const results = await Promise.all([
|
|
316
|
+
respond(request.id, asUser(alice.token)),
|
|
317
|
+
respond(request.id, asUser(alice.token)),
|
|
318
|
+
]);
|
|
319
|
+
expect(results.map((r) => r.status).sort()).toEqual([200, 409]);
|
|
320
|
+
const stored = await getApprovalRequestById(request.id);
|
|
321
|
+
expect(stored?.status).toBe("pending");
|
|
322
|
+
expect(stored?.approvals).toHaveLength(1);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("all: every listed user must approve; the operator does not stand in for one", async () => {
|
|
326
|
+
const request = await pendingRequest({ users: [alice.id, bob.id], policy: "all" });
|
|
327
|
+
|
|
328
|
+
const byOperator = (await respond(request.id, operator())).body.approvalRequest;
|
|
329
|
+
expect(byOperator.status).toBe("pending");
|
|
330
|
+
expect(byOperator.approvalProgress).toEqual({ approved: 0, required: 2 });
|
|
331
|
+
const byAlice = (await respond(request.id, asUser(alice.token))).body.approvalRequest;
|
|
332
|
+
expect(byAlice.status).toBe("pending");
|
|
333
|
+
expect(byAlice.approvalProgress).toEqual({ approved: 1, required: 2 });
|
|
334
|
+
const last = await respond(request.id, asUser(bob.token));
|
|
335
|
+
expect(last.body.approvalRequest.status).toBe("approved");
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("the slim list and the detail read carry quorum progress for pending requests", async () => {
|
|
339
|
+
const request = await pendingRequest({ policy: { min: 3 } });
|
|
340
|
+
expect((await respond(request.id, asUser(alice.token))).status).toBe(200);
|
|
341
|
+
|
|
342
|
+
const list = await fetch(`${baseUrl}/api/approval-requests?fields=slim&status=pending`, {
|
|
343
|
+
headers: operator(),
|
|
344
|
+
});
|
|
345
|
+
const { approvalRequests } = (await list.json()) as {
|
|
346
|
+
approvalRequests: { id: string; approvalProgress: unknown; approvals?: unknown }[];
|
|
347
|
+
};
|
|
348
|
+
const row = approvalRequests.find((r) => r.id === request.id);
|
|
349
|
+
expect(row?.approvalProgress).toEqual({ approved: 1, required: 3 });
|
|
350
|
+
expect(row && "approvals" in row).toBe(false);
|
|
351
|
+
|
|
352
|
+
const detail = await fetch(`${baseUrl}/api/approval-requests/${request.id}`, {
|
|
353
|
+
headers: operator(),
|
|
354
|
+
});
|
|
355
|
+
const { approvalRequest } = (await detail.json()) as Record<string, any>;
|
|
356
|
+
expect(approvalRequest.approvalProgress).toEqual({ approved: 1, required: 3 });
|
|
357
|
+
expect(approvalRequest.approvals).toHaveLength(1);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("one rejection from an eligible responder rejects a multi-approver request", async () => {
|
|
361
|
+
const request = await pendingRequest({ users: [alice.id, bob.id], policy: "all" });
|
|
362
|
+
expect((await respond(request.id, asUser(alice.token))).body.approvalRequest.status).toBe(
|
|
363
|
+
"pending",
|
|
364
|
+
);
|
|
365
|
+
const res = await respond(request.id, asUser(bob.token), {
|
|
366
|
+
responses: { q1: { approved: false } },
|
|
367
|
+
});
|
|
368
|
+
expect(res.body.approvalRequest.status).toBe("rejected");
|
|
369
|
+
expect(res.body.approvalRequest.resolvedBy).toBe(bob.id);
|
|
370
|
+
});
|
|
371
|
+
});
|
|
@@ -1135,6 +1135,19 @@ describe("getBasePrompt: harness tool discovery", () => {
|
|
|
1135
1135
|
expect(result).toContain("Swarm tools are already in your tool list.");
|
|
1136
1136
|
});
|
|
1137
1137
|
|
|
1138
|
+
test("amp gets its own guidance: tool_search plus code_exec, underscored names", async () => {
|
|
1139
|
+
const result = await getBasePrompt({
|
|
1140
|
+
...minimalArgs,
|
|
1141
|
+
provider: "amp",
|
|
1142
|
+
traits: { ...localTraits, hasToolSearch: true },
|
|
1143
|
+
});
|
|
1144
|
+
expect(result).not.toContain(deferred);
|
|
1145
|
+
expect(result).not.toContain("Swarm tools are already in your tool list.");
|
|
1146
|
+
expect(result).toContain("`tool_search` and `code_exec`");
|
|
1147
|
+
expect(result).toContain("`store-progress` is `store_progress`");
|
|
1148
|
+
expect(result).toContain('import { store_progress } from "agent-swarm"');
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1138
1151
|
test("omitted tool-search capability defaults to direct tools", async () => {
|
|
1139
1152
|
const result = await getBasePrompt({ ...minimalArgs, traits: localTraits });
|
|
1140
1153
|
expect(result).not.toContain(deferred);
|
|
@@ -367,6 +367,7 @@ describe("preseedClaudeTrustDialog", () => {
|
|
|
367
367
|
|
|
368
368
|
describe("CLAUDE_BINARY env override", () => {
|
|
369
369
|
// Cache the originals and restore after each test so the suite stays clean.
|
|
370
|
+
let originalClaudeTransport: string | undefined;
|
|
370
371
|
let originalClaudeBinary: string | undefined;
|
|
371
372
|
let originalUseClaudeBridge: string | undefined;
|
|
372
373
|
let originalOauthToken: string | undefined;
|
|
@@ -378,6 +379,8 @@ describe("CLAUDE_BINARY env override", () => {
|
|
|
378
379
|
let spawnedEnvs: Array<Record<string, string> | undefined>;
|
|
379
380
|
|
|
380
381
|
beforeEach(async () => {
|
|
382
|
+
originalClaudeTransport = process.env.CLAUDE_TRANSPORT;
|
|
383
|
+
delete process.env.CLAUDE_TRANSPORT;
|
|
381
384
|
originalClaudeBinary = process.env.CLAUDE_BINARY;
|
|
382
385
|
originalUseClaudeBridge = process.env.SWARM_USE_CLAUDE_BRIDGE;
|
|
383
386
|
originalOauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
@@ -416,6 +419,11 @@ describe("CLAUDE_BINARY env override", () => {
|
|
|
416
419
|
} else {
|
|
417
420
|
process.env.HOME = originalHome;
|
|
418
421
|
}
|
|
422
|
+
if (originalClaudeTransport === undefined) {
|
|
423
|
+
delete process.env.CLAUDE_TRANSPORT;
|
|
424
|
+
} else {
|
|
425
|
+
process.env.CLAUDE_TRANSPORT = originalClaudeTransport;
|
|
426
|
+
}
|
|
419
427
|
if (originalClaudeBinary === undefined) {
|
|
420
428
|
delete process.env.CLAUDE_BINARY;
|
|
421
429
|
} else {
|
|
@@ -677,6 +685,7 @@ describe("CLAUDE_BINARY env override", () => {
|
|
|
677
685
|
});
|
|
678
686
|
|
|
679
687
|
describe("Claude Bridge tmux fail-fast gate", () => {
|
|
688
|
+
let originalClaudeTransport: string | undefined;
|
|
680
689
|
let originalClaudeBinary: string | undefined;
|
|
681
690
|
let originalUseClaudeBridge: string | undefined;
|
|
682
691
|
let originalOauthToken: string | undefined;
|
|
@@ -686,6 +695,8 @@ describe("Claude Bridge tmux fail-fast gate", () => {
|
|
|
686
695
|
let whichSpy: ReturnType<typeof spyOn>;
|
|
687
696
|
|
|
688
697
|
beforeEach(async () => {
|
|
698
|
+
originalClaudeTransport = process.env.CLAUDE_TRANSPORT;
|
|
699
|
+
delete process.env.CLAUDE_TRANSPORT;
|
|
689
700
|
originalClaudeBinary = process.env.CLAUDE_BINARY;
|
|
690
701
|
originalUseClaudeBridge = process.env.SWARM_USE_CLAUDE_BRIDGE;
|
|
691
702
|
originalOauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
@@ -708,6 +719,11 @@ describe("Claude Bridge tmux fail-fast gate", () => {
|
|
|
708
719
|
} else {
|
|
709
720
|
process.env.HOME = originalHome;
|
|
710
721
|
}
|
|
722
|
+
if (originalClaudeTransport === undefined) {
|
|
723
|
+
delete process.env.CLAUDE_TRANSPORT;
|
|
724
|
+
} else {
|
|
725
|
+
process.env.CLAUDE_TRANSPORT = originalClaudeTransport;
|
|
726
|
+
}
|
|
711
727
|
if (originalClaudeBinary === undefined) {
|
|
712
728
|
delete process.env.CLAUDE_BINARY;
|
|
713
729
|
} else {
|
|
@@ -794,6 +810,7 @@ describe("Claude Bridge tmux fail-fast gate", () => {
|
|
|
794
810
|
});
|
|
795
811
|
|
|
796
812
|
describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
|
|
813
|
+
let originalClaudeTransport: string | undefined;
|
|
797
814
|
let originalClaudeBinary: string | undefined;
|
|
798
815
|
let originalUseClaudeBridge: string | undefined;
|
|
799
816
|
let originalOauthToken: string | undefined;
|
|
@@ -803,6 +820,8 @@ describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
|
|
|
803
820
|
let whichSpy: ReturnType<typeof spyOn>;
|
|
804
821
|
|
|
805
822
|
beforeEach(async () => {
|
|
823
|
+
originalClaudeTransport = process.env.CLAUDE_TRANSPORT;
|
|
824
|
+
delete process.env.CLAUDE_TRANSPORT;
|
|
806
825
|
originalClaudeBinary = process.env.CLAUDE_BINARY;
|
|
807
826
|
originalUseClaudeBridge = process.env.SWARM_USE_CLAUDE_BRIDGE;
|
|
808
827
|
originalOauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
@@ -828,6 +847,11 @@ describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
|
|
|
828
847
|
} else {
|
|
829
848
|
process.env.HOME = originalHome;
|
|
830
849
|
}
|
|
850
|
+
if (originalClaudeTransport === undefined) {
|
|
851
|
+
delete process.env.CLAUDE_TRANSPORT;
|
|
852
|
+
} else {
|
|
853
|
+
process.env.CLAUDE_TRANSPORT = originalClaudeTransport;
|
|
854
|
+
}
|
|
831
855
|
if (originalClaudeBinary === undefined) {
|
|
832
856
|
delete process.env.CLAUDE_BINARY;
|
|
833
857
|
} else {
|
|
@@ -206,6 +206,7 @@ describe("Claude stream-json event parsing", () => {
|
|
|
206
206
|
// stdout) rather than asserting on hand-built fixtures, so a message-shape
|
|
207
207
|
// regression here is caught instead of silently shipping the wrong text.
|
|
208
208
|
describe("ClaudeSession processStreams — ProviderResult.output capture", () => {
|
|
209
|
+
let originalClaudeTransport: string | undefined;
|
|
209
210
|
let spawnSpy: ReturnType<typeof spyOn>;
|
|
210
211
|
const CLEAN_ENV: Record<string, string> = { CLAUDE_CODE_OAUTH_TOKEN: "example-test-oauth-token" };
|
|
211
212
|
|
|
@@ -244,11 +245,18 @@ describe("ClaudeSession processStreams — ProviderResult.output capture", () =>
|
|
|
244
245
|
}
|
|
245
246
|
|
|
246
247
|
beforeEach(() => {
|
|
248
|
+
originalClaudeTransport = process.env.CLAUDE_TRANSPORT;
|
|
249
|
+
delete process.env.CLAUDE_TRANSPORT;
|
|
247
250
|
spawnSpy = spyOn(Bun, "spawn");
|
|
248
251
|
});
|
|
249
252
|
|
|
250
253
|
afterEach(() => {
|
|
251
254
|
spawnSpy.mockRestore();
|
|
255
|
+
if (originalClaudeTransport === undefined) {
|
|
256
|
+
delete process.env.CLAUDE_TRANSPORT;
|
|
257
|
+
} else {
|
|
258
|
+
process.env.CLAUDE_TRANSPORT = originalClaudeTransport;
|
|
259
|
+
}
|
|
252
260
|
});
|
|
253
261
|
|
|
254
262
|
test("multiple assistant turns: the last non-empty text wins", async () => {
|