@desplega.ai/agent-swarm 1.164.1 → 1.166.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 +1 -1
- package/dist/acp-adapter-yzfxn8vj.js +23 -0
- package/dist/{actions-hp2h32dx.js → actions-qz3m692w.js} +24 -28
- package/dist/{amp-adapter-zfd7djyx.js → amp-adapter-j1xbmsqz.js} +7 -7
- package/dist/{app-w2d26h4f.js → app-43vrdmv8.js} +9 -10
- package/dist/{assistant-45pc4en7.js → assistant-ertffmka.js} +25 -29
- package/dist/{boot-reembed-k1q2s4fc.js → boot-reembed-998jtpdg.js} +12 -15
- package/dist/{boot-reembed-6rhcpsjf.js → boot-reembed-s7k7c6v2.js} +13 -15
- package/dist/boot-scrub-sweep-nepsk4t1.js +211 -0
- package/dist/{boot-seeds-ex93edx7.js → boot-seeds-vvzs1zhk.js} +12 -12
- package/dist/{claude-adapter-hcxd0szm.js → claude-adapter-2zvmekrp.js} +15 -17
- package/dist/{claude-managed-adapter-fz7xps2r.js → claude-managed-adapter-s49wbk8t.js} +5 -5
- package/dist/{claude-managed-setup-xx6f15h9.js → claude-managed-setup-cvhqq8qn.js} +4 -4
- package/dist/{claude-sdk-session-6nb9raem.js → claude-sdk-session-kkbxtw5q.js} +2624 -2606
- package/dist/{cli-etbrt8x1.js → cli-0nhdwxd5.js} +4 -4
- package/dist/{cli-jeefgppq.js → cli-0qjcsxgz.js} +7 -7
- package/dist/{cli-6behs2bt.js → cli-32bca7nr.js} +1 -1
- package/dist/{cli-gt8v96qx.js → cli-3ddybbbx.js} +4 -4
- package/dist/{acp-adapter-rxvek4ym.js → cli-3qn4r334.js} +317 -35
- package/dist/{cli-1d3w4xm3.js → cli-4r1sjfc4.js} +14 -6
- package/dist/{cli-691f2rxm.js → cli-66zh26sp.js} +68 -16
- package/dist/{cli-et8wdyqh.js → cli-68r1j4sg.js} +9 -9
- package/dist/{cli-xb7kyanp.js → cli-8kgvmkak.js} +3 -3
- package/dist/{cli-fdpkw8vk.js → cli-8t229cmr.js} +8 -8
- package/dist/{cli-gc54qc0p.js → cli-aqcey741.js} +1 -1
- package/dist/{cli-4q4cmr4t.js → cli-cpfmqz3n.js} +8 -8
- package/dist/{cli-a9bw0xxt.js → cli-df56tqdw.js} +997 -408
- package/dist/{cli-n7h1kqpf.js → cli-dmtrxqhg.js} +6 -1
- package/dist/{cli-xfb7333f.js → cli-dw2q9f7g.js} +5 -5
- package/dist/{cli-4drstbb9.js → cli-ecknp7p1.js} +81 -23
- package/dist/{cli-v0pvvhfq.js → cli-efnd2kce.js} +36 -1
- package/dist/{cli-1wetf34q.js → cli-f2n17zhp.js} +1 -1
- package/dist/{cli-2c8eky5y.js → cli-feh3dkah.js} +1 -1
- package/dist/cli-fn697bnt.js +126 -0
- package/dist/{cli-v5pr2fgt.js → cli-gt8azyet.js} +3 -3
- package/dist/{cli-dbszf3rw.js → cli-h3x613ze.js} +4 -4
- package/dist/{cli-nwhb8y2e.js → cli-hf5s9pgf.js} +1 -1
- package/dist/cli-j2jbr8af.js +516 -0
- package/dist/{cli-agjzafns.js → cli-jhm6hzjn.js} +593 -10
- package/dist/{cli-j0tmc908.js → cli-jvj6a0cv.js} +484 -477
- package/dist/{cli-xxz89c2n.js → cli-mb75wpxf.js} +1 -1
- package/dist/{cli-07s39g1n.js → cli-mv2520nm.js} +1 -1
- package/dist/{cli-v90vfwyb.js → cli-mwegs4af.js} +5 -5
- package/dist/{cli-bz1gbac9.js → cli-p4ztb80h.js} +2 -2
- package/dist/{cli-1wd1n2ya.js → cli-rfphkzh5.js} +1 -1
- package/dist/{cli-976r1zqx.js → cli-rggdq8wq.js} +5 -5
- package/dist/{cli-3ajw5v5v.js → cli-rsy933yz.js} +6 -6
- package/dist/{cli-yj790e19.js → cli-tadfgd0t.js} +8 -1
- package/dist/{cli-cn53e7vc.js → cli-ttbsse3f.js} +6 -1
- package/dist/{cli-dyqsrqcf.js → cli-v2a8x45d.js} +4 -4
- package/dist/{cli-wbvnn6tk.js → cli-v6vv7dy8.js} +91 -9
- package/dist/{cli-z2ap2c0c.js → cli-vzc73wet.js} +90 -55
- package/dist/cli-wt7ccy4r.js +2654 -0
- package/dist/{cli-4cn19jvn.js → cli-x7kpr72a.js} +3 -3
- package/dist/{cli-003785v9.js → cli-xvc3q9br.js} +1 -1
- package/dist/{cli-v0e7jpr5.js → cli-xvdgasgy.js} +13 -4
- package/dist/{cli-asqeg4ve.js → cli-y59h2pk9.js} +2 -2
- package/dist/{cli-0wn3ypqv.js → cli-yshaazs1.js} +14121 -3478
- package/dist/{cli-wg8yjqtw.js → cli-ysnyhws9.js} +16 -3
- package/dist/cli-zp67qg5x.js +92 -0
- package/dist/{cli-g3qgrzck.js → cli-zsh093hk.js} +71 -3
- package/dist/{cli-ma3zvh39.js → cli-zy7vwsne.js} +6066 -5829
- package/dist/cli.js +79 -26
- package/dist/{codex-adapter-pcxdn10v.js → codex-adapter-dw1wxqt8.js} +10 -10
- package/dist/{codex-hook-b1fb7c2k.js → codex-hook-d2rkvf39.js} +1 -1
- package/dist/{codex-login-059nm145.js → codex-login-terbdsqq.js} +2 -2
- package/dist/{codex-session-runner-xzwwddp8.js → codex-session-runner-3cgb8y1g.js} +10 -10
- package/dist/{commands-98at8mkn.js → commands-mhxam6z2.js} +9 -9
- package/dist/{cursor-adapter-dkg6qnkp.js → cursor-adapter-2qda3st7.js} +6 -6
- package/dist/{db-01g8wtvw.js → db-sjks418k.js} +11 -9
- package/dist/{devin-adapter-qmttt2fz.js → devin-adapter-bffjj5n4.js} +2 -2
- package/dist/{dispatcher-nn2k1wxd.js → dispatcher-1es759rn.js} +22 -26
- package/dist/{dsh-adapter-w4d4vwdq.js → dsh-adapter-bek4cyw9.js} +6 -6
- package/dist/{e2b-9e4g2hvt.js → e2b-nd50eccg.js} +1 -1
- package/dist/{event-bus-z7v2t26e.js → event-bus-vnfthafg.js} +2 -2
- package/dist/grok-adapter-ar9cw9jg.js +29 -0
- package/dist/{handlers-b0kqsbg8.js → handlers-1gkbhktr.js} +25 -29
- package/dist/{hook-sap7v1aj.js → hook-ds2zxq9v.js} +16 -18
- package/dist/{hook-pz50gqep.js → hook-vxa3tcsy.js} +10 -12
- package/dist/{http-ncr8rmd0.js → http-3h21z9md.js} +679 -227
- package/dist/{index-c4aqcyz1.js → index-9b65j1m5.js} +11 -11
- package/dist/{index-r46s01nq.js → index-bn6cy8ps.js} +22 -26
- package/dist/{index-38kqdt3e.js → index-dkfxpkmm.js} +23 -27
- package/dist/{index-630kj5vd.js → index-f8xnqrye.js} +55 -43
- package/dist/{index-fdk1t4d6.js → index-n0rxa4ca.js} +11962 -9900
- package/dist/{index-9nrj7797.js → index-n99fycq7.js} +22 -26
- package/dist/{keepalive-31613r2h.js → keepalive-3xspa9jw.js} +14 -11
- package/dist/{lead-mx59tr5s.js → lead-6qphc124.js} +43 -43
- package/dist/{maintenance-55ff39ca.js → maintenance-0teem1bb.js} +10 -12
- package/dist/{node-f0ykebbv.js → node-9sg7ajby.js} +25 -7
- package/dist/{oauth-refresh-sweep-anma954w.js → oauth-refresh-sweep-sh9q25xp.js} +13 -10
- package/dist/{onboard-b2fy8ftb.js → onboard-54qmnqxe.js} +4 -3
- package/dist/{opencode-adapter-p1s92tmy.js → opencode-adapter-wjfa2wvd.js} +6 -6
- package/dist/{otel-impl-9brvt7xv.js → otel-impl-ycqzp2a1.js} +2 -2
- package/dist/{pi-mono-adapter-kz4rrwvd.js → pi-mono-adapter-e77e9zst.js} +6 -6
- package/dist/{pricing-refresh-7w8mbmva.js → pricing-refresh-atnkngqs.js} +10 -10
- package/dist/{rbac-roles-ejp7qwvb.js → rbac-roles-9pjphh9q.js} +10 -10
- package/dist/{rbac-roles-nhk4r7bc.js → rbac-roles-j78wdksm.js} +10 -10
- package/dist/{render-v2-2gzt8skq.js → render-v2-pxjs49fx.js} +9 -10
- package/dist/{setup-5et8t412.js → setup-ymej0xtx.js} +2 -2
- package/dist/{work-objects-tb0er7qa.js → work-objects-3nh4ymzb.js} +26 -30
- package/dist/{worker-62cv075d.js → worker-qa9fkz5s.js} +43 -43
- package/dist/{x-vbxb31jr.js → x-ma4gnfgy.js} +2 -2
- package/openapi.json +457 -31
- package/package.json +14 -6
- package/plugin/commands/create-pr.md +7 -1
- package/plugin/pi-skills/create-pr/SKILL.md +7 -1
- package/src/agentmail/app.ts +5 -4
- package/src/be/automation-preflight.ts +11 -9
- package/src/be/boot-scrub-sweep.ts +302 -0
- package/src/be/connector-codes.ts +81 -0
- package/src/be/crypto/key-bootstrap.ts +1 -1
- package/src/be/db/approvals.ts +15 -1
- package/src/be/db/codex-oauth-device-flows.ts +40 -0
- package/src/be/db/config-secrets.ts +31 -0
- package/src/be/db/runtime.ts +4 -0
- package/src/be/db/script-runs.ts +15 -0
- package/src/be/db/tasks/write.ts +1 -1
- package/src/be/db-queries/mcp-oauth.ts +5 -0
- package/src/be/db-queries/oauth.ts +77 -0
- package/src/be/db.ts +162 -59
- package/src/be/events.ts +17 -1
- package/src/be/memory/index-content.ts +2 -2
- package/src/be/memory/providers/sqlite-store.ts +63 -11
- package/src/be/memory/types.ts +14 -0
- package/src/be/migrations/197_connector_codes.sql +23 -0
- package/src/be/migrations/198_permission_audit_guest.sql +33 -0
- package/src/be/migrations/200_backfill_auto_memory_expiry.sql +20 -0
- package/src/be/migrations/201_workflow_replay_columns.sql +10 -0
- package/src/be/migrations/202_codex_oauth_device_flows.sql +24 -0
- package/src/be/model-tier-resolution.ts +2 -1
- package/src/be/model-validation.ts +1 -1
- package/src/be/pricing-normalize.ts +3 -0
- package/src/be/rbac-audit.ts +2 -1
- package/src/be/scripts/db.ts +28 -4
- package/src/be/scripts/source-secrets.ts +37 -0
- package/src/be/scripts/typecheck.ts +23 -2
- package/src/be/scrub-json.ts +32 -0
- package/src/be/sealed-json.ts +59 -0
- package/src/be/secret-registry.ts +131 -0
- package/src/be/seed-pricing.ts +14 -0
- package/src/be/users.ts +7 -1
- package/src/be/workflow-replay.ts +77 -0
- package/src/cli.tsx +7 -0
- package/src/commands/provider-credentials.ts +46 -8
- package/src/commands/runner.ts +2 -10
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/github/app.ts +4 -4
- package/src/heartbeat/heartbeat.ts +20 -8
- package/src/hooks/pr-body-guard.ts +1 -1
- package/src/http/agents.ts +1 -0
- package/src/http/approval-requests.ts +1 -0
- package/src/http/apps.ts +3 -0
- package/src/http/auth.ts +4 -1
- package/src/http/codex-oauth-device.ts +8 -10
- package/src/http/codex-oauth-keep-warm.ts +2 -2
- package/src/http/config-values.ts +59 -0
- package/src/http/config.ts +5 -0
- package/src/http/db-query-bounded.ts +3 -0
- package/src/http/db-query-shared.ts +107 -15
- package/src/http/db-query.ts +10 -2
- package/src/http/extensions.ts +1 -0
- package/src/http/index.ts +23 -6
- package/src/http/integrations.ts +1 -38
- package/src/http/ip-rate-limit.ts +82 -0
- package/src/http/kv.ts +38 -4
- package/src/http/mcp-servers.ts +1 -0
- package/src/http/memory.ts +9 -0
- package/src/http/models-catalog.ts +2 -0
- package/src/http/operator-actor.ts +4 -0
- package/src/http/page-proxy.ts +22 -0
- package/src/http/pages.ts +4 -2
- package/src/http/realtime.ts +1 -1
- package/src/http/request-principal.ts +14 -1
- package/src/http/script-connections.ts +8 -0
- package/src/http/script-runs.ts +12 -4
- package/src/http/scripts.ts +15 -0
- package/src/http/session-cost-recompute.ts +3 -2
- package/src/http/tasks.ts +9 -0
- package/src/http/users.ts +226 -0
- package/src/http/workflows.ts +3 -2
- package/src/http.ts +1 -0
- package/src/integrations/kapso/client.ts +3 -1
- package/src/jira/app.ts +2 -2
- package/src/kv-view.ts +324 -0
- package/src/linear/app.ts +2 -2
- package/src/oauth/mcp-wrapper.ts +1 -1
- package/src/prompts/memories.ts +42 -1
- package/src/providers/acp-adapter.ts +85 -24
- package/src/providers/acp-target-catalog.ts +54 -1
- package/src/providers/acp-targets.ts +358 -4
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/providers/grok-adapter.ts +186 -0
- package/src/providers/index.ts +5 -1
- package/src/providers/pricing-sources.md +6 -0
- package/src/providers/reasoning-effort.ts +17 -1
- package/src/providers/types.ts +2 -1
- package/src/rbac/can.ts +4 -3
- package/src/rbac/legacy-policy.ts +3 -3
- package/src/rbac/types.ts +6 -0
- package/src/script-workflows/supervisor.ts +11 -2
- package/src/scripts-runtime/eval-harness.ts +2 -12
- package/src/scripts-runtime/executors/quickjs-bundle.ts +152 -0
- package/src/scripts-runtime/executors/quickjs-prelude.ts +285 -0
- package/src/scripts-runtime/executors/quickjs-runner.ts +580 -0
- package/src/scripts-runtime/executors/quickjs-worker.ts +81 -0
- package/src/scripts-runtime/executors/quickjs.ts +196 -0
- package/src/scripts-runtime/executors/registry.ts +2 -0
- package/src/scripts-runtime/executors/sourcemap.ts +79 -0
- package/src/scripts-runtime/script-error-hints.ts +14 -0
- package/src/scripts-runtime/swarm-sdk.ts +9 -2
- package/src/scripts-runtime/types/stdlib.d.ts +27 -8
- package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -8
- package/src/server-user.ts +2 -20
- package/src/server.ts +3 -1
- package/src/slack/app.ts +4 -0
- package/src/slack/config.ts +3 -1
- package/src/slack/egress-scrub.ts +87 -0
- package/src/tests/acp-adapter.test.ts +191 -2
- package/src/tests/boot-scrub-sweep.test.ts +390 -0
- package/src/tests/budget-user-scope.test.ts +7 -21
- package/src/tests/check-pr-body.test.ts +149 -6
- package/src/tests/check-text-columns.test.ts +145 -0
- package/src/tests/claude-adapter-binary.test.ts +1 -1
- package/src/tests/claude-sdk-process.fixture.ts +13 -2
- package/src/tests/codex-oauth-device.test.ts +23 -37
- package/src/tests/codex-oauth-keep-warm.test.ts +29 -0
- package/src/tests/console-scrub.test.ts +141 -0
- package/src/tests/credential-check.test.ts +90 -0
- package/src/tests/db-query-denylist.test.ts +202 -0
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/db-task-write-contract.test.ts +1 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/entrypoint-acp-target-binary.test.ts +44 -1
- package/src/tests/events-scrub.test.ts +88 -0
- package/src/tests/explicit-model-validation.test.ts +81 -0
- package/src/tests/extensions-pre-task.test.ts +33 -1
- package/src/tests/fixtures/grok/acp-updates-recorded.jsonl +53 -0
- package/src/tests/fixtures/grok/prompt-response-meta.json +36 -0
- package/src/tests/fixtures/secret-corpus.ts +320 -0
- package/src/tests/grok-adapter.test.ts +647 -0
- package/src/tests/heartbeat-checklist.test.ts +44 -0
- package/src/tests/integration-disable-flags.test.ts +156 -0
- package/src/tests/kv-http.test.ts +34 -0
- package/src/tests/kv-tool.test.ts +274 -1
- package/src/tests/kv-view.test.ts +147 -0
- package/src/tests/mcp-user-route.test.ts +257 -15
- package/src/tests/memory-injection-outcome.test.ts +143 -0
- package/src/tests/model-tier-resolution.test.ts +76 -3
- package/src/tests/page-guest-session.test.ts +424 -0
- package/src/tests/page-proxy.test.ts +1 -1
- package/src/tests/page-viewer-auth.test.ts +22 -1
- package/src/tests/pr-body-leaks.test.ts +157 -2
- package/src/tests/preload.ts +10 -0
- package/src/tests/provider-registration.test.ts +3 -1
- package/src/tests/rbac-audit.test.ts +32 -0
- package/src/tests/rbac-mcp-admission-e2e.test.ts +4 -62
- package/src/tests/script-connections.test.ts +9 -0
- package/src/tests/script-executor-conformance.test.ts +3 -0
- package/src/tests/script-executor-quickjs.test.ts +442 -0
- package/src/tests/script-executor-registry.test.ts +6 -1
- package/src/tests/script-runs-http.test.ts +126 -1
- package/src/tests/script-runs-scrub.test.ts +275 -0
- package/src/tests/scripts-http.test.ts +82 -1
- package/src/tests/scripts-mcp-e2e.test.ts +42 -6
- package/src/tests/scripts-runtime.test.ts +37 -0
- package/src/tests/scrubbed-text-12e.test.ts +289 -0
- package/src/tests/sealed-key-guard.test.ts +166 -0
- package/src/tests/secret-canary-sweep.test.ts +958 -0
- package/src/tests/secret-registry.test.ts +355 -0
- package/src/tests/secret-rules.test.ts +230 -0
- package/src/tests/secret-scrubber.test.ts +317 -1
- package/src/tests/session-costs-golden.test.ts +17 -0
- package/src/tests/slack-egress-scrub.test.ts +216 -0
- package/src/tests/swarm-tool-result-gate.test.ts +143 -30
- package/src/tests/task-tools-ctx.test.ts +3 -0
- package/src/tests/user-token-routes.test.ts +342 -3
- package/src/tests/workflow-replay.test.ts +518 -0
- package/src/tests/workflow-scrub.test.ts +242 -0
- package/src/tests/workflow-system-one-decision-providers.test.ts +737 -0
- package/src/tools/db-query.ts +1 -1
- package/src/tools/inject-learning.ts +1 -1
- package/src/tools/kv/kv-get.ts +136 -6
- package/src/tools/send-task.ts +61 -17
- package/src/tools/utils.ts +186 -43
- package/src/tools/workflows/get-workflow-run.ts +3 -2
- package/src/types.ts +16 -1
- package/src/utils/console-scrub-install.ts +5 -0
- package/src/utils/console-scrub.ts +57 -0
- package/src/utils/credentials.ts +8 -0
- package/src/utils/page-session.ts +6 -2
- package/src/utils/pr-body-leaks.ts +149 -10
- package/src/utils/request-auth-context.ts +2 -0
- package/src/utils/secret-rules.generated.ts +2055 -0
- package/src/utils/secret-scrubber.ts +662 -36
- package/src/workflows/engine.ts +39 -4
- package/src/workflows/executors/notify.ts +6 -1
- package/src/workflows/executors/system-one-decision.ts +31 -12
- package/src/workflows/executors/system-one-openai-wire.ts +168 -0
- package/src/workflows/executors/system-one-providers.ts +112 -11
- package/src/workflows/executors/system-one-wire.ts +79 -0
- package/src/workflows/recovery.ts +10 -1
- package/src/workflows/resume.ts +62 -3
- package/src/workflows/retry-poller.ts +3 -0
- package/templates/ai-toolbox.manifest.json +2 -2
- package/templates/skills/agent-browser/SKILL.md +5 -5
- package/templates/skills/agent-browser/config.json +1 -1
- package/templates/skills/agent-browser/content.md +4 -4
- package/templates/skills/artifacts/content.md +17 -7
- package/templates/skills/jira-interaction/config.json +1 -1
- package/templates/skills/jira-interaction/content.md +56 -205
- package/templates/skills/linear-interaction/config.json +1 -1
- package/templates/skills/linear-interaction/content.md +31 -50
- package/templates/skills/pages/content.md +8 -4
- package/templates/skills/qa/content.md +2 -2
- package/templates/skills/workflow-iterate/content.md +4 -4
- package/tsconfig.json +3 -3
- package/dist/boot-scrub-logs-t8xg7wd7.js +0 -99
- package/dist/cli-0a7r5331.js +0 -168
- package/dist/cli-93rxw9yd.js +0 -951
- package/dist/cli-cvk6yzw5.js +0 -40
- package/dist/cli-dgnj0zcc.js +0 -40
- package/dist/cli-qt5wzq6v.js +0 -256
- package/dist/cli-v7fqehqd.js +0 -9281
- package/dist/cli-vbr859rz.js +0 -572
- package/src/be/boot-scrub-logs.ts +0 -132
|
@@ -3,6 +3,10 @@ import {
|
|
|
3
3
|
checkPrBody,
|
|
4
4
|
isFixTitle,
|
|
5
5
|
pickedChoices,
|
|
6
|
+
prBodyWarnings,
|
|
7
|
+
proseWordCount,
|
|
8
|
+
riskLevel,
|
|
9
|
+
templateLeadLines,
|
|
6
10
|
templateSections,
|
|
7
11
|
} from "../../scripts/check-pr-body";
|
|
8
12
|
|
|
@@ -40,6 +44,13 @@ describe("check-pr-body", () => {
|
|
|
40
44
|
]);
|
|
41
45
|
});
|
|
42
46
|
|
|
47
|
+
test("headings inside HTML comments or still-open fences are not sections", () => {
|
|
48
|
+
const hidden = "<!--\n## Intent\n-->\n````md\n```\n## Intent\n````\n";
|
|
49
|
+
expect(checkPrBody(TEMPLATE, `${hidden}${URGENT}`, "feat: x")).toEqual([
|
|
50
|
+
"missing section: ## Intent",
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
53
|
+
|
|
43
54
|
test("fix titles are detected by conventional-commit type", () => {
|
|
44
55
|
expect(isFixTitle("fix: x")).toBe(true);
|
|
45
56
|
expect(isFixTitle("fix(slack): x")).toBe(true);
|
|
@@ -91,15 +102,147 @@ describe("check-pr-body", () => {
|
|
|
91
102
|
expect(checkPrBody(TEMPLATE, body)).toEqual(["missing section: ## Urgency"]);
|
|
92
103
|
});
|
|
93
104
|
|
|
94
|
-
test("the real template fails unedited and passes once every
|
|
105
|
+
test("the real template fails unedited and passes once every part is filled", async () => {
|
|
95
106
|
const template = await Bun.file(".github/pull_request_template.md").text();
|
|
107
|
+
const leads = templateLeadLines(template);
|
|
108
|
+
expect(leads).toEqual(["Why", "Risk"]);
|
|
96
109
|
const sections = templateSections(template).filter((s) => s.when !== "optional");
|
|
97
110
|
expect(sections.length).toBeGreaterThan(0);
|
|
98
|
-
|
|
111
|
+
const all = { title: "fix: x", author: "desplega-bot", changedFiles: ["apps/ui/a.tsx"] };
|
|
112
|
+
// Unknown risk keeps the outline required, so every section and lead line is reported.
|
|
113
|
+
expect(checkPrBody(template, template, all)).toHaveLength(sections.length + leads.length);
|
|
114
|
+
|
|
115
|
+
const filled = [
|
|
116
|
+
"**Why:** because.",
|
|
117
|
+
"**Risk:** medium",
|
|
118
|
+
...sections.map(
|
|
119
|
+
(s) => `## ${s.heading}\n\n${s.choices.length ? `- [x] ${s.choices[0]}` : "filled"}\n`,
|
|
120
|
+
),
|
|
121
|
+
].join("\n");
|
|
122
|
+
expect(checkPrBody(template, filled, all)).toEqual([]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("the real template's markers", async () => {
|
|
126
|
+
const template = await Bun.file(".github/pull_request_template.md").text();
|
|
127
|
+
const when = Object.fromEntries(templateSections(template).map((s) => [s.heading, s.when]));
|
|
128
|
+
expect(when).toMatchObject({
|
|
129
|
+
"Review map": "always",
|
|
130
|
+
"Change outline": "outline",
|
|
131
|
+
"Before / after": "ui",
|
|
132
|
+
Repro: "fix",
|
|
133
|
+
Urgency: "always",
|
|
134
|
+
"Swarm provenance": "bot",
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("check-pr-body: lead lines, risk and conditional sections", () => {
|
|
140
|
+
const T = `<!-- top -->
|
|
141
|
+
|
|
142
|
+
**Why:** <!-- one sentence -->
|
|
143
|
+
|
|
144
|
+
**Risk:** <!-- low | medium | high -->
|
|
145
|
+
|
|
146
|
+
## Outline <!-- outline -->
|
|
147
|
+
|
|
148
|
+
<!-- g -->
|
|
149
|
+
|
|
150
|
+
## Before / after <!-- ui -->
|
|
151
|
+
|
|
152
|
+
<!-- g -->
|
|
153
|
+
|
|
154
|
+
## Swarm provenance <!-- bot -->
|
|
155
|
+
|
|
156
|
+
<!-- g -->
|
|
157
|
+
`;
|
|
158
|
+
const human = { author: "octocat" };
|
|
159
|
+
const leads = (risk: string) => `**Why:** It broke.\n\n**Risk:** ${risk}\n\n`;
|
|
160
|
+
const outline = "## Outline\n\n```\n+ new\n```\n";
|
|
161
|
+
|
|
162
|
+
test("lead lines are read from the template preamble", () => {
|
|
163
|
+
expect(templateLeadLines(T)).toEqual(["Why", "Risk"]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("the Why and Risk lines are required and Risk must be a known level", () => {
|
|
167
|
+
expect(checkPrBody(T, outline, human)).toEqual([
|
|
168
|
+
"missing line: **Why:**",
|
|
169
|
+
"missing line: **Risk:**",
|
|
170
|
+
]);
|
|
171
|
+
expect(checkPrBody(T, `**Why:**\n**Risk:** <!-- x -->\n${outline}`, human)).toEqual([
|
|
172
|
+
"empty line: **Why:**",
|
|
173
|
+
"empty line: **Risk:**",
|
|
174
|
+
]);
|
|
175
|
+
expect(checkPrBody(T, `${leads("spicy")}${outline}`, human)).toEqual([
|
|
176
|
+
"**Risk:** must start with one of: low, medium, high",
|
|
177
|
+
]);
|
|
178
|
+
expect(checkPrBody(T, `${leads("High (secrets)")}${outline}`, human)).toEqual([]);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("a lead line after the first heading does not count", () => {
|
|
182
|
+
expect(checkPrBody(T, `${outline}\n**Why:** x\n**Risk:** low\n`, human)).toContain(
|
|
183
|
+
"missing line: **Why:**",
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("riskLevel parses the first word only", () => {
|
|
188
|
+
expect(riskLevel("**Risk:** high (secrets)")).toBe("high");
|
|
189
|
+
expect(riskLevel("**Risk:** medium")).toBe("medium");
|
|
190
|
+
expect(riskLevel("**Risk:** lowish")).toBeUndefined();
|
|
191
|
+
expect(riskLevel("no risk line")).toBeUndefined();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("the outline may be skipped only when Risk is low and the diff is small", () => {
|
|
195
|
+
const missing = ["missing section: ## Outline"];
|
|
196
|
+
expect(checkPrBody(T, leads("medium"), { ...human, changedLines: 3 })).toEqual(missing);
|
|
197
|
+
expect(checkPrBody(T, leads("low"), { ...human, changedLines: 51 })).toEqual(missing);
|
|
198
|
+
expect(checkPrBody(T, leads("low"), { ...human, changedLines: 50 })).toEqual([]);
|
|
199
|
+
expect(checkPrBody(T, leads("low"), human)).toEqual([]);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("Before / after is required only when the diff touches a UI app", () => {
|
|
203
|
+
const body = `${leads("medium")}${outline}`;
|
|
204
|
+
const missing = ["missing section: ## Before / after"];
|
|
205
|
+
expect(checkPrBody(T, body, { ...human, changedFiles: ["apps/ui/src/App.tsx"] })).toEqual(
|
|
206
|
+
missing,
|
|
207
|
+
);
|
|
208
|
+
expect(checkPrBody(T, body, { ...human, changedFiles: ["apps/templates-ui/x.ts"] })).toEqual(
|
|
209
|
+
missing,
|
|
210
|
+
);
|
|
211
|
+
expect(checkPrBody(T, body, { ...human, changedFiles: ["src/apps/ui-thing.ts"] })).toEqual([]);
|
|
212
|
+
expect(checkPrBody(T, body, human)).toEqual([]);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("Swarm provenance is required for the bot and for a local run without an author", () => {
|
|
216
|
+
const body = `${leads("medium")}${outline}`;
|
|
217
|
+
const missing = ["missing section: ## Swarm provenance"];
|
|
218
|
+
expect(checkPrBody(T, body, { author: "desplega-bot" })).toEqual(missing);
|
|
219
|
+
expect(checkPrBody(T, body, {})).toEqual(missing);
|
|
220
|
+
expect(checkPrBody(T, body, human)).toEqual([]);
|
|
221
|
+
const withProvenance = `${body}\n## Swarm provenance\n- Task: link\n`;
|
|
222
|
+
expect(checkPrBody(T, withProvenance, { author: "desplega-bot" })).toEqual([]);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("check-pr-body: prose budget", () => {
|
|
227
|
+
test("code fences, tables, details, comments, headings and URLs do not count", () => {
|
|
228
|
+
const body = [
|
|
229
|
+
"**Why:** three words here.",
|
|
230
|
+
"## Review map",
|
|
231
|
+
"| Area | Depth | Why |",
|
|
232
|
+
"|---|---|---|",
|
|
233
|
+
"```",
|
|
234
|
+
"lots of code words here",
|
|
235
|
+
"```",
|
|
236
|
+
"<details>hidden words here</details>",
|
|
237
|
+
"<!-- hidden comment words -->",
|
|
238
|
+
"See https://example.com/a/b and #12.",
|
|
239
|
+
].join("\n");
|
|
240
|
+
// "**Why:**", "three", "words", "here.", "See", "and". "#12." has no letter.
|
|
241
|
+
expect(proseWordCount(body)).toBe(6);
|
|
242
|
+
});
|
|
99
243
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
expect(checkPrBody(template, filled, "fix: x")).toEqual([]);
|
|
244
|
+
test("a long body warns but never fails", () => {
|
|
245
|
+
expect(prBodyWarnings("word ".repeat(300))).toEqual([]);
|
|
246
|
+
expect(prBodyWarnings("word ".repeat(301))).toHaveLength(1);
|
|
104
247
|
});
|
|
105
248
|
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
3
|
+
import { unlink } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
checkTextColumns,
|
|
7
|
+
listTextColumns,
|
|
8
|
+
TEXT_COLUMNS_PATH,
|
|
9
|
+
type TextColumnClassification,
|
|
10
|
+
} from "../../scripts/check-text-columns";
|
|
11
|
+
import {
|
|
12
|
+
closeDb,
|
|
13
|
+
createAgent,
|
|
14
|
+
createTask,
|
|
15
|
+
getDbClient,
|
|
16
|
+
initDb,
|
|
17
|
+
updateTaskProgress,
|
|
18
|
+
} from "../be/db";
|
|
19
|
+
import { runMigrations } from "../be/migrations/runner";
|
|
20
|
+
import { type SyntheticSecret, syntheticSecret } from "./synthetic-secret-helpers";
|
|
21
|
+
|
|
22
|
+
const repoRoot = join(import.meta.dir, "..", "..");
|
|
23
|
+
|
|
24
|
+
async function loadCommitted(): Promise<TextColumnClassification> {
|
|
25
|
+
return (await Bun.file(join(repoRoot, TEXT_COLUMNS_PATH)).json()) as TextColumnClassification;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("TEXT-column classification check", () => {
|
|
29
|
+
let db: Database;
|
|
30
|
+
|
|
31
|
+
beforeAll(() => {
|
|
32
|
+
db = new Database(":memory:");
|
|
33
|
+
const originalLog = console.log;
|
|
34
|
+
console.log = () => {};
|
|
35
|
+
try {
|
|
36
|
+
runMigrations(db);
|
|
37
|
+
} finally {
|
|
38
|
+
console.log = originalLog;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterAll(() => {
|
|
43
|
+
db.close();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("the committed classification covers the migrated schema exactly", async () => {
|
|
47
|
+
const classification = await loadCommitted();
|
|
48
|
+
expect(listTextColumns(db).length).toBeGreaterThan(100);
|
|
49
|
+
expect(checkTextColumns(db, classification)).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("a new TEXT column from a later migration fails until it is classified", async () => {
|
|
53
|
+
const classification = await loadCommitted();
|
|
54
|
+
const scratch = new Database(":memory:");
|
|
55
|
+
try {
|
|
56
|
+
const originalLog = console.log;
|
|
57
|
+
console.log = () => {};
|
|
58
|
+
try {
|
|
59
|
+
runMigrations(scratch);
|
|
60
|
+
} finally {
|
|
61
|
+
console.log = originalLog;
|
|
62
|
+
}
|
|
63
|
+
// Stand-in for a future migration that adds a free-text sink.
|
|
64
|
+
scratch.run("CREATE TABLE canary_notes (id TEXT PRIMARY KEY, body TEXT NOT NULL, n INTEGER)");
|
|
65
|
+
|
|
66
|
+
const violations = checkTextColumns(scratch, classification);
|
|
67
|
+
expect(violations).toEqual([
|
|
68
|
+
"canary_notes.id: unclassified TEXT column",
|
|
69
|
+
"canary_notes.body: unclassified TEXT column",
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
const classified: TextColumnClassification = {
|
|
73
|
+
...classification,
|
|
74
|
+
canary_notes: { id: { exempt: "identifier" }, body: { pending: "test batch" } },
|
|
75
|
+
};
|
|
76
|
+
expect(checkTextColumns(scratch, classified)).toEqual([]);
|
|
77
|
+
} finally {
|
|
78
|
+
scratch.close();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("stale and malformed entries are rejected", async () => {
|
|
83
|
+
const classification = await loadCommitted();
|
|
84
|
+
const firstTable = Object.keys(classification)[0] as string;
|
|
85
|
+
const firstColumn = Object.keys(classification[firstTable] ?? {})[0] as string;
|
|
86
|
+
const bad = {
|
|
87
|
+
...classification,
|
|
88
|
+
[firstTable]: { ...classification[firstTable], [firstColumn]: { exempt: "" } },
|
|
89
|
+
gone_table: { note: "scrubbed" },
|
|
90
|
+
} as TextColumnClassification;
|
|
91
|
+
|
|
92
|
+
const violations = checkTextColumns(db, bad);
|
|
93
|
+
expect(violations).toContain("gone_table.note: stale entry, no such TEXT column");
|
|
94
|
+
expect(
|
|
95
|
+
violations.some((v) => v.startsWith(`${firstTable}.${firstColumn}: invalid entry`)),
|
|
96
|
+
).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("ScrubbedText at a production writer", () => {
|
|
101
|
+
const dbPath = "./test-check-text-columns-writer.sqlite";
|
|
102
|
+
let secret: SyntheticSecret;
|
|
103
|
+
|
|
104
|
+
async function removeDbFiles(): Promise<void> {
|
|
105
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
106
|
+
try {
|
|
107
|
+
await unlink(dbPath + suffix);
|
|
108
|
+
} catch {}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
beforeAll(async () => {
|
|
113
|
+
await removeDbFiles();
|
|
114
|
+
initDb(dbPath);
|
|
115
|
+
secret = syntheticSecret("textcol");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
afterAll(async () => {
|
|
119
|
+
secret.cleanup();
|
|
120
|
+
closeDb();
|
|
121
|
+
await removeDbFiles();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("a column classified scrubbed holds no secret after its real writer runs", async () => {
|
|
125
|
+
const committed = await loadCommitted();
|
|
126
|
+
expect(committed.agent_tasks?.progress).toBe("scrubbed");
|
|
127
|
+
|
|
128
|
+
const agent = await createAgent({
|
|
129
|
+
name: "text-columns-writer",
|
|
130
|
+
isLead: false,
|
|
131
|
+
status: "idle",
|
|
132
|
+
capabilities: [],
|
|
133
|
+
});
|
|
134
|
+
const task = await createTask(agent.id, "classification writer check");
|
|
135
|
+
await updateTaskProgress(task.id, `step 2 of 3 token=${secret.value} done`);
|
|
136
|
+
|
|
137
|
+
const row = await getDbClient().get<{ progress: string }>(
|
|
138
|
+
"SELECT progress FROM agent_tasks WHERE id = ?",
|
|
139
|
+
[task.id],
|
|
140
|
+
);
|
|
141
|
+
expect(row?.progress).toContain("step 2 of 3");
|
|
142
|
+
expect(row?.progress).toContain(`[REDACTED:${secret.name}]`);
|
|
143
|
+
expect(row?.progress.includes(secret.value)).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -639,7 +639,7 @@ describe("preseedClaudeTrustDialog", () => {
|
|
|
639
639
|
const data = JSON.parse(await readFile(join(homeDir, ".claude.json"), "utf-8"));
|
|
640
640
|
expect(data.projects["/a"].hasTrustDialogAccepted).toBe(true);
|
|
641
641
|
expect(data.projects["/b"].hasTrustDialogAccepted).toBe(true);
|
|
642
|
-
expect(await readdir(homeDir)).toEqual([".claude.json", ".claude.json.swarm-lock"]);
|
|
642
|
+
expect((await readdir(homeDir)).sort()).toEqual([".claude.json", ".claude.json.swarm-lock"]);
|
|
643
643
|
});
|
|
644
644
|
|
|
645
645
|
test("many writers on an expired mkdir lock keep every entry of every round", async () => {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { rename } from "node:fs/promises";
|
|
2
|
+
|
|
1
3
|
const mode = process.env.CLAUDE_SDK_FIXTURE_MODE;
|
|
2
4
|
const stateFile = process.env.CLAUDE_SDK_FIXTURE_STATE_FILE;
|
|
3
5
|
const sessionId = "11111111-1111-4111-8111-111111111111";
|
|
@@ -13,6 +15,15 @@ if (!mode || !stateFile) {
|
|
|
13
15
|
process.exit(2);
|
|
14
16
|
}
|
|
15
17
|
|
|
18
|
+
// The test polls for the state file and parses it as soon as it exists.
|
|
19
|
+
// Bun.write creates the file before filling it, so write-then-rename keeps
|
|
20
|
+
// the reader from ever seeing an empty file.
|
|
21
|
+
async function writeStateFile(path: string, state: Record<string, unknown>): Promise<void> {
|
|
22
|
+
const temporaryPath = `${path}.tmp`;
|
|
23
|
+
await Bun.write(temporaryPath, JSON.stringify(state));
|
|
24
|
+
await rename(temporaryPath, path);
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
function writeMessage(message: Record<string, unknown>): void {
|
|
17
28
|
console.log(JSON.stringify(message));
|
|
18
29
|
}
|
|
@@ -145,7 +156,7 @@ if (mode === "cancel") {
|
|
|
145
156
|
stdout: "ignore",
|
|
146
157
|
stderr: "ignore",
|
|
147
158
|
});
|
|
148
|
-
await
|
|
159
|
+
await writeStateFile(stateFile, { fixturePid: process.pid, childPid: child.pid });
|
|
149
160
|
}
|
|
150
161
|
|
|
151
162
|
async function handleLine(line: string): Promise<boolean> {
|
|
@@ -191,7 +202,7 @@ async function handleLine(line: string): Promise<boolean> {
|
|
|
191
202
|
}
|
|
192
203
|
|
|
193
204
|
if (mode !== "queue" || userMessages.length < 3) return false;
|
|
194
|
-
await
|
|
205
|
+
await writeStateFile(stateFile, { argv, userMessages });
|
|
195
206
|
for (let index = 0; index < userMessages.length; index++) {
|
|
196
207
|
const turn = index + 1;
|
|
197
208
|
writeMessage(assistantMessage(`accepted:${userMessages[index]}`, turn));
|
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { createServer, type Server } from "node:http";
|
|
3
3
|
import { decryptSecret, encryptSecret, getEncryptionKey } from "../be/crypto";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
getDbClient,
|
|
7
|
-
getKv,
|
|
8
|
-
getSwarmConfigs,
|
|
9
|
-
initDb,
|
|
10
|
-
upsertKv,
|
|
11
|
-
upsertSwarmConfig,
|
|
12
|
-
} from "../be/db";
|
|
4
|
+
import { closeDb, getDbClient, getSwarmConfigs, initDb, upsertSwarmConfig } from "../be/db";
|
|
5
|
+
import { getCodexDeviceFlow, upsertCodexDeviceFlow } from "../be/db/codex-oauth-device-flows";
|
|
13
6
|
import type { OnboardingState } from "../be/onboarding";
|
|
14
7
|
import { handleCodexOAuthDevice } from "../http/codex-oauth-device";
|
|
15
8
|
import { handleCore } from "../http/core";
|
|
@@ -97,7 +90,7 @@ afterAll(async () => {
|
|
|
97
90
|
});
|
|
98
91
|
|
|
99
92
|
beforeEach(async () => {
|
|
100
|
-
await getDbClient().run("DELETE FROM
|
|
93
|
+
await getDbClient().run("DELETE FROM codex_oauth_device_flows");
|
|
101
94
|
await getDbClient().run("DELETE FROM swarm_config");
|
|
102
95
|
fetchImpl = async () => {
|
|
103
96
|
throw new Error("Unexpected fetch");
|
|
@@ -221,8 +214,8 @@ describe("Codex device HTTP routes", () => {
|
|
|
221
214
|
|
|
222
215
|
const started = await startFlow();
|
|
223
216
|
expect(Number.isNaN(Date.parse(started.expiresAt))).toBe(false);
|
|
224
|
-
const encrypted = await
|
|
225
|
-
expect(encrypted?.
|
|
217
|
+
const encrypted = await getCodexDeviceFlow(started.flowId);
|
|
218
|
+
expect(encrypted?.state).not.toContain("device-auth-full");
|
|
226
219
|
|
|
227
220
|
const poll = await request(`/api/codex-oauth/device/${started.flowId}/poll`);
|
|
228
221
|
expect(await poll.json()).toEqual({ status: "complete", slot: 1 });
|
|
@@ -318,17 +311,15 @@ describe("Codex device HTTP routes", () => {
|
|
|
318
311
|
const secondResponse = await request(path);
|
|
319
312
|
const secondBody = await secondResponse.json();
|
|
320
313
|
const callsBeforeRelease = { pollCalls, exchangeCalls };
|
|
321
|
-
const entry = await
|
|
322
|
-
const state = JSON.parse(decryptSecret(
|
|
314
|
+
const entry = await getCodexDeviceFlow(started.flowId);
|
|
315
|
+
const state = JSON.parse(decryptSecret(entry!.state, getEncryptionKey())) as Record<
|
|
323
316
|
string,
|
|
324
317
|
unknown
|
|
325
318
|
>;
|
|
326
319
|
state.pollLeaseExpiresAt = Date.now() - 1;
|
|
327
|
-
await
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
value: encryptSecret(JSON.stringify(state), getEncryptionKey()),
|
|
331
|
-
valueType: "string",
|
|
320
|
+
await upsertCodexDeviceFlow({
|
|
321
|
+
flowId: started.flowId,
|
|
322
|
+
state: encryptSecret(JSON.stringify(state), getEncryptionKey()),
|
|
332
323
|
expiresAt: Number(state.expiresAt),
|
|
333
324
|
});
|
|
334
325
|
releaseExchange();
|
|
@@ -359,19 +350,17 @@ describe("Codex device HTTP routes", () => {
|
|
|
359
350
|
}) as typeof fetch;
|
|
360
351
|
|
|
361
352
|
const started = await startFlow();
|
|
362
|
-
const entry = await
|
|
363
|
-
const state = JSON.parse(decryptSecret(
|
|
353
|
+
const entry = await getCodexDeviceFlow(started.flowId);
|
|
354
|
+
const state = JSON.parse(decryptSecret(entry!.state, getEncryptionKey())) as Record<
|
|
364
355
|
string,
|
|
365
356
|
unknown
|
|
366
357
|
>;
|
|
367
358
|
state.lastPolledAt = Date.now() - 10_000;
|
|
368
359
|
state.pollLeaseId = crypto.randomUUID();
|
|
369
360
|
state.pollLeaseExpiresAt = Date.now() - 1;
|
|
370
|
-
await
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
value: encryptSecret(JSON.stringify(state), getEncryptionKey()),
|
|
374
|
-
valueType: "string",
|
|
361
|
+
await upsertCodexDeviceFlow({
|
|
362
|
+
flowId: started.flowId,
|
|
363
|
+
state: encryptSecret(JSON.stringify(state), getEncryptionKey()),
|
|
375
364
|
expiresAt: Number(state.expiresAt),
|
|
376
365
|
});
|
|
377
366
|
|
|
@@ -380,9 +369,9 @@ describe("Codex device HTTP routes", () => {
|
|
|
380
369
|
});
|
|
381
370
|
expect(pollCalls).toBe(1);
|
|
382
371
|
|
|
383
|
-
const recovered = await
|
|
372
|
+
const recovered = await getCodexDeviceFlow(started.flowId);
|
|
384
373
|
const recoveredState = JSON.parse(
|
|
385
|
-
decryptSecret(
|
|
374
|
+
decryptSecret(recovered!.state, getEncryptionKey()),
|
|
386
375
|
) as Record<string, unknown>;
|
|
387
376
|
expect(recoveredState.pollLeaseId).toBeNull();
|
|
388
377
|
expect(recoveredState.pollLeaseExpiresAt).toBeNull();
|
|
@@ -403,17 +392,15 @@ describe("Codex device HTTP routes", () => {
|
|
|
403
392
|
}) as typeof fetch;
|
|
404
393
|
|
|
405
394
|
const started = await startFlow();
|
|
406
|
-
const entry = await
|
|
407
|
-
const state = JSON.parse(decryptSecret(
|
|
395
|
+
const entry = await getCodexDeviceFlow(started.flowId);
|
|
396
|
+
const state = JSON.parse(decryptSecret(entry!.state, getEncryptionKey())) as Record<
|
|
408
397
|
string,
|
|
409
398
|
unknown
|
|
410
399
|
>;
|
|
411
400
|
state.expiresAt = Date.now() - 1;
|
|
412
|
-
await
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
value: encryptSecret(JSON.stringify(state), getEncryptionKey()),
|
|
416
|
-
valueType: "string",
|
|
401
|
+
await upsertCodexDeviceFlow({
|
|
402
|
+
flowId: started.flowId,
|
|
403
|
+
state: encryptSecret(JSON.stringify(state), getEncryptionKey()),
|
|
417
404
|
expiresAt: Number(state.expiresAt),
|
|
418
405
|
});
|
|
419
406
|
|
|
@@ -482,8 +469,7 @@ describe("Codex device HTTP routes", () => {
|
|
|
482
469
|
}) as typeof fetch;
|
|
483
470
|
await getDbClient().run(
|
|
484
471
|
`CREATE TRIGGER fail_device_flow_write
|
|
485
|
-
BEFORE INSERT ON
|
|
486
|
-
WHEN NEW.namespace = 'codex-oauth-device'
|
|
472
|
+
BEFORE INSERT ON codex_oauth_device_flows
|
|
487
473
|
BEGIN
|
|
488
474
|
SELECT RAISE(ABORT, 'forced local write failure');
|
|
489
475
|
END`,
|
|
@@ -255,6 +255,35 @@ describe("POST /api/oauth/keep-warm/codex", () => {
|
|
|
255
255
|
expect(body.results).toEqual([]);
|
|
256
256
|
});
|
|
257
257
|
|
|
258
|
+
it("loads slots through MCP_BASE_URL, not the public base URL", async () => {
|
|
259
|
+
const savedMcp = process.env.MCP_BASE_URL;
|
|
260
|
+
const savedPublic = process.env.PUBLIC_MCP_BASE_URL;
|
|
261
|
+
process.env.MCP_BASE_URL = "http://swarm-internal.example.test:3013";
|
|
262
|
+
process.env.PUBLIC_MCP_BASE_URL = "https://swarm-public.example.test";
|
|
263
|
+
installMockTransport(new Map());
|
|
264
|
+
const transport = globalThis.fetch;
|
|
265
|
+
const requested: string[] = [];
|
|
266
|
+
globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
|
|
267
|
+
requested.push(typeof url === "string" ? url : url.toString());
|
|
268
|
+
return transport(url, init);
|
|
269
|
+
}) as typeof fetch;
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const { req, res, captured } = fakeReqRes();
|
|
273
|
+
await handleCodexOAuthKeepWarm(req, res, ["api", "oauth", "keep-warm", "codex"]);
|
|
274
|
+
|
|
275
|
+
expect(captured.status).toBe(200);
|
|
276
|
+
const configLoad = requested.find((u) => u.includes("/api/config/resolved"));
|
|
277
|
+
expect(configLoad).toBeDefined();
|
|
278
|
+
expect(new URL(configLoad as string).origin).toBe("http://swarm-internal.example.test:3013");
|
|
279
|
+
} finally {
|
|
280
|
+
if (savedMcp === undefined) delete process.env.MCP_BASE_URL;
|
|
281
|
+
else process.env.MCP_BASE_URL = savedMcp;
|
|
282
|
+
if (savedPublic === undefined) delete process.env.PUBLIC_MCP_BASE_URL;
|
|
283
|
+
else process.env.PUBLIC_MCP_BASE_URL = savedPublic;
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
258
287
|
it("does not match unrelated paths", async () => {
|
|
259
288
|
const { req, res } = fakeReqRes();
|
|
260
289
|
const handled = await handleCodexOAuthKeepWarm(req, res, [
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { inspect } from "node:util";
|
|
3
|
+
import { installConsoleLogBridge, uninstallConsoleLogBridge } from "../otel-impl";
|
|
4
|
+
import { installConsoleScrub, uninstallConsoleScrub } from "../utils/console-scrub";
|
|
5
|
+
|
|
6
|
+
// Built at runtime so no secret-shaped literal sits in the repo.
|
|
7
|
+
const SECRET = `ghp_${"Q".repeat(36)}`;
|
|
8
|
+
const REDACTED = "[REDACTED:";
|
|
9
|
+
|
|
10
|
+
const METHODS = ["log", "info", "warn", "error", "debug"] as const;
|
|
11
|
+
type Method = (typeof METHODS)[number];
|
|
12
|
+
|
|
13
|
+
let saved: Record<Method, (...args: unknown[]) => void>;
|
|
14
|
+
let written: { method: Method; args: unknown[] }[];
|
|
15
|
+
|
|
16
|
+
// Stand-ins for the underlying stream writers; the scrub wraps these.
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
written = [];
|
|
19
|
+
saved = { ...console } as typeof saved;
|
|
20
|
+
for (const method of METHODS) {
|
|
21
|
+
console[method] = (...args: unknown[]) => {
|
|
22
|
+
written.push({ method, args });
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
uninstallConsoleLogBridge();
|
|
29
|
+
uninstallConsoleScrub();
|
|
30
|
+
for (const method of METHODS) console[method] = saved[method];
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function lastLine(): string {
|
|
34
|
+
const entry = written.at(-1);
|
|
35
|
+
if (!entry) throw new Error("nothing written");
|
|
36
|
+
expect(entry.args).toHaveLength(1);
|
|
37
|
+
return entry.args[0] as string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("installConsoleScrub", () => {
|
|
41
|
+
test("redacts strings, Error message and stack, and objects on every method", () => {
|
|
42
|
+
installConsoleScrub();
|
|
43
|
+
for (const method of METHODS) {
|
|
44
|
+
console[method]("token is", SECRET);
|
|
45
|
+
const line = lastLine();
|
|
46
|
+
expect(written.at(-1)?.method).toBe(method);
|
|
47
|
+
expect(line).toStartWith("token is ");
|
|
48
|
+
expect(line).not.toContain(SECRET);
|
|
49
|
+
expect(line).toContain(REDACTED);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const err = new Error(`auth failed with ${SECRET}`);
|
|
53
|
+
console.error("boom:", err);
|
|
54
|
+
const errLine = lastLine();
|
|
55
|
+
expect(errLine).toContain("auth failed with");
|
|
56
|
+
expect(errLine).toContain("console-scrub.test.ts"); // the stack survived
|
|
57
|
+
expect(errLine).not.toContain(SECRET);
|
|
58
|
+
expect(errLine).toContain(REDACTED);
|
|
59
|
+
|
|
60
|
+
console.log({ service: "github", nested: { value: SECRET } });
|
|
61
|
+
const objLine = lastLine();
|
|
62
|
+
expect(objLine).toContain("github");
|
|
63
|
+
expect(objLine).not.toContain(SECRET);
|
|
64
|
+
expect(objLine).toContain(REDACTED);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("keeps printf-style substitution", () => {
|
|
68
|
+
installConsoleScrub();
|
|
69
|
+
console.log("%s has %d items and key %s", "queue", 3, SECRET);
|
|
70
|
+
const line = lastLine();
|
|
71
|
+
expect(line).toStartWith("queue has 3 items and key ");
|
|
72
|
+
expect(line).not.toContain(SECRET);
|
|
73
|
+
expect(line).not.toContain("%s");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("a second install is a no-op", () => {
|
|
77
|
+
installConsoleScrub();
|
|
78
|
+
const wrapped = console.log;
|
|
79
|
+
installConsoleScrub();
|
|
80
|
+
expect(console.log).toBe(wrapped);
|
|
81
|
+
uninstallConsoleScrub();
|
|
82
|
+
console.log("plain", SECRET);
|
|
83
|
+
expect(written.at(-1)?.args).toEqual(["plain", SECRET]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("an OTel bridge installed afterwards forwards only scrubbed text", () => {
|
|
87
|
+
installConsoleScrub();
|
|
88
|
+
installConsoleLogBridge();
|
|
89
|
+
console.warn(`leaked ${SECRET}`);
|
|
90
|
+
const line = lastLine();
|
|
91
|
+
expect(line).not.toContain(SECRET);
|
|
92
|
+
expect(line).toContain(REDACTED);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("a log written by a custom inspector during formatting fails closed", () => {
|
|
96
|
+
installConsoleScrub();
|
|
97
|
+
const noisy = {
|
|
98
|
+
[inspect.custom]() {
|
|
99
|
+
console.log("nested", SECRET);
|
|
100
|
+
return "noisy-object";
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
console.log("outer", noisy);
|
|
104
|
+
expect(written).toHaveLength(2);
|
|
105
|
+
const [nested, outer] = written.map((entry) => entry.args);
|
|
106
|
+
expect(nested).toEqual([
|
|
107
|
+
"[console-scrub] suppressed a log line written while formatting another",
|
|
108
|
+
]);
|
|
109
|
+
expect(outer).toEqual(["outer noisy-object"]);
|
|
110
|
+
expect(JSON.stringify(written)).not.toContain(SECRET);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("a formatting throw writes a fixed line, never the raw arguments", () => {
|
|
114
|
+
installConsoleScrub();
|
|
115
|
+
const broken = {
|
|
116
|
+
[inspect.custom]() {
|
|
117
|
+
throw new Error(`inspector failed near ${SECRET}`);
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
console.error("payload", SECRET, broken);
|
|
121
|
+
expect(written).toHaveLength(1);
|
|
122
|
+
expect(written[0]?.args).toEqual(["[console-scrub] failed to scrub log line"]);
|
|
123
|
+
expect(JSON.stringify(written)).not.toContain(SECRET);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("zero arguments still print an empty line", () => {
|
|
127
|
+
installConsoleScrub();
|
|
128
|
+
console.log();
|
|
129
|
+
expect(written.at(-1)?.args).toEqual([]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("10k lines stay cheap", () => {
|
|
133
|
+
installConsoleScrub();
|
|
134
|
+
const started = performance.now();
|
|
135
|
+
for (let i = 0; i < 10_000; i++) console.log(`[worker] polling task ${i}`, { attempt: i });
|
|
136
|
+
const elapsedMs = performance.now() - started;
|
|
137
|
+
expect(written).toHaveLength(10_000);
|
|
138
|
+
// Generous bound for slow CI runners; measured about 450 ms in a worker container.
|
|
139
|
+
expect(elapsedMs).toBeLessThan(3_000);
|
|
140
|
+
});
|
|
141
|
+
});
|