@stigmer/runner 3.0.9-dev.20260615153829 → 3.1.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/.build-fingerprint +1 -1
- package/dist/__test-utils__/approval-contract/types.d.ts +174 -0
- package/dist/__test-utils__/approval-contract/types.js +24 -0
- package/dist/__test-utils__/approval-contract/types.js.map +1 -0
- package/dist/activities/call-agent-status.d.ts +19 -1
- package/dist/activities/call-agent-status.js +64 -5
- package/dist/activities/call-agent-status.js.map +1 -1
- package/dist/activities/call-llm.js +19 -53
- package/dist/activities/call-llm.js.map +1 -1
- package/dist/activities/classify-tool-approvals.d.ts +24 -0
- package/dist/activities/classify-tool-approvals.js +69 -17
- package/dist/activities/classify-tool-approvals.js.map +1 -1
- package/dist/activities/discover-mcp-server.d.ts +7 -0
- package/dist/activities/discover-mcp-server.js +11 -1
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +140 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +130 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -0
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.d.ts +18 -0
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js +123 -0
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js.map +1 -0
- package/dist/activities/execute-cursor/approval-policy.d.ts +17 -61
- package/dist/activities/execute-cursor/approval-policy.js +8 -120
- package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
- package/dist/activities/execute-cursor/approval-state.d.ts +219 -9
- package/dist/activities/execute-cursor/approval-state.js +233 -17
- package/dist/activities/execute-cursor/approval-state.js.map +1 -1
- package/dist/activities/execute-cursor/capture-flow.d.ts +127 -0
- package/dist/activities/execute-cursor/capture-flow.js +234 -0
- package/dist/activities/execute-cursor/capture-flow.js.map +1 -0
- package/dist/activities/execute-cursor/cas-observations.d.ts +96 -0
- package/dist/activities/execute-cursor/cas-observations.js +184 -0
- package/dist/activities/execute-cursor/cas-observations.js.map +1 -0
- package/dist/activities/execute-cursor/command-provenance.d.ts +62 -0
- package/dist/activities/execute-cursor/command-provenance.js +128 -0
- package/dist/activities/execute-cursor/command-provenance.js.map +1 -0
- package/dist/activities/execute-cursor/exact-apply.d.ts +110 -0
- package/dist/activities/execute-cursor/exact-apply.js +204 -0
- package/dist/activities/execute-cursor/exact-apply.js.map +1 -0
- package/dist/activities/execute-cursor/hook-script.d.ts +53 -24
- package/dist/activities/execute-cursor/hook-script.js +310 -47
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +8 -1
- package/dist/activities/execute-cursor/index.js +500 -64
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/message-translator.d.ts +258 -11
- package/dist/activities/execute-cursor/message-translator.js +836 -90
- package/dist/activities/execute-cursor/message-translator.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +20 -23
- package/dist/activities/execute-cursor/prompt-builder.js +74 -15
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/workspace-setup.d.ts +17 -0
- package/dist/activities/execute-cursor/workspace-setup.js +212 -33
- package/dist/activities/execute-cursor/workspace-setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.d.ts +19 -0
- package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js +143 -0
- package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js.map +1 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.d.ts +88 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js +81 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js.map +1 -0
- package/dist/activities/execute-deep-agent/approval-file-change.d.ts +47 -0
- package/dist/activities/execute-deep-agent/approval-file-change.js +68 -0
- package/dist/activities/execute-deep-agent/approval-file-change.js.map +1 -0
- package/dist/activities/execute-deep-agent/attachment-injector.d.ts +8 -1
- package/dist/activities/execute-deep-agent/attachment-injector.js +7 -7
- package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
- package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +42 -0
- package/dist/activities/execute-deep-agent/cas-capture-backend.js +47 -0
- package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.d.ts +79 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.js +112 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.js.map +1 -0
- package/dist/activities/execute-deep-agent/hitl.d.ts +16 -3
- package/dist/activities/execute-deep-agent/hitl.js +11 -3
- package/dist/activities/execute-deep-agent/hitl.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.d.ts +2 -1
- package/dist/activities/execute-deep-agent/index.js +393 -33
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/inline-publisher.d.ts +7 -1
- package/dist/activities/execute-deep-agent/inline-publisher.js +23 -2
- package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.d.ts +53 -2
- package/dist/activities/execute-deep-agent/setup.js +149 -92
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.d.ts +36 -0
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.js +56 -0
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.js.map +1 -0
- package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +34 -1
- package/dist/activities/execute-deep-agent/status-builder-shared.js +26 -25
- package/dist/activities/execute-deep-agent/status-builder-shared.js.map +1 -1
- package/dist/activities/execute-deep-agent/status-builder.d.ts +17 -5
- package/dist/activities/execute-deep-agent/status-builder.js +44 -3
- package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming-side-effects.js +2 -19
- package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming.js +3 -15
- package/dist/activities/execute-deep-agent/streaming.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +25 -7
- package/dist/activities/execute-deep-agent/subagent-transformer.js +23 -7
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +30 -3
- package/dist/activities/execute-deep-agent/subagent-wiring.js +29 -3
- package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.js +32 -2
- package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
- package/dist/claimcheck/payload-codec.js +9 -5
- package/dist/claimcheck/payload-codec.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +2 -0
- package/dist/client/stigmer-client.js +2 -0
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/middleware/approval-gate.d.ts +85 -4
- package/dist/middleware/approval-gate.js +165 -38
- package/dist/middleware/approval-gate.js.map +1 -1
- package/dist/middleware/types.d.ts +2 -5
- package/dist/shared/activity-input.d.ts +43 -0
- package/dist/shared/activity-input.js +17 -0
- package/dist/shared/activity-input.js.map +1 -0
- package/dist/shared/approval-canonicalize.d.ts +19 -0
- package/dist/shared/approval-canonicalize.js +119 -0
- package/dist/shared/approval-canonicalize.js.map +1 -0
- package/dist/shared/approval-fingerprint.d.ts +106 -0
- package/dist/shared/approval-fingerprint.js +113 -0
- package/dist/shared/approval-fingerprint.js.map +1 -0
- package/dist/shared/approval-policy.d.ts +182 -12
- package/dist/shared/approval-policy.js +213 -27
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/shared/args-preview.d.ts +52 -0
- package/dist/shared/args-preview.js +93 -0
- package/dist/shared/args-preview.js.map +1 -0
- package/dist/shared/artifact-storage.d.ts +19 -1
- package/dist/shared/artifact-storage.js +48 -11
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/file-change.d.ts +44 -0
- package/dist/shared/file-change.js +57 -0
- package/dist/shared/file-change.js.map +1 -0
- package/dist/shared/file-tools.d.ts +107 -0
- package/dist/shared/file-tools.js +168 -0
- package/dist/shared/file-tools.js.map +1 -0
- package/dist/shared/filereview/capture.d.ts +202 -0
- package/dist/shared/filereview/capture.js +498 -0
- package/dist/shared/filereview/capture.js.map +1 -0
- package/dist/shared/filereview/cas-substrate.d.ts +190 -0
- package/dist/shared/filereview/cas-substrate.js +284 -0
- package/dist/shared/filereview/cas-substrate.js.map +1 -0
- package/dist/shared/filereview/digest.d.ts +40 -0
- package/dist/shared/filereview/digest.js +66 -0
- package/dist/shared/filereview/digest.js.map +1 -0
- package/dist/shared/filereview/events.d.ts +170 -0
- package/dist/shared/filereview/events.js +298 -0
- package/dist/shared/filereview/events.js.map +1 -0
- package/dist/shared/filereview/git-substrate.d.ts +175 -0
- package/dist/shared/filereview/git-substrate.js +439 -0
- package/dist/shared/filereview/git-substrate.js.map +1 -0
- package/dist/shared/filereview/index.d.ts +11 -0
- package/dist/shared/filereview/index.js +12 -0
- package/dist/shared/filereview/index.js.map +1 -0
- package/dist/shared/filereview/secret-paths.d.ts +63 -0
- package/dist/shared/filereview/secret-paths.js +105 -0
- package/dist/shared/filereview/secret-paths.js.map +1 -0
- package/dist/shared/fingerprint-secret.d.ts +26 -0
- package/dist/shared/fingerprint-secret.js +47 -0
- package/dist/shared/fingerprint-secret.js.map +1 -0
- package/dist/shared/model-client.d.ts +51 -0
- package/dist/shared/model-client.js +77 -0
- package/dist/shared/model-client.js.map +1 -0
- package/dist/shared/plan-artifact.js +0 -2
- package/dist/shared/plan-artifact.js.map +1 -1
- package/dist/shared/status-offload.d.ts +83 -9
- package/dist/shared/status-offload.js +399 -79
- package/dist/shared/status-offload.js.map +1 -1
- package/dist/shared/status.js +14 -1
- package/dist/shared/status.js.map +1 -1
- package/dist/shared/tool-kind.d.ts +19 -0
- package/dist/shared/tool-kind.js +13 -0
- package/dist/shared/tool-kind.js.map +1 -1
- package/dist/shared/tool-row.d.ts +88 -0
- package/dist/shared/tool-row.js +127 -0
- package/dist/shared/tool-row.js.map +1 -0
- package/dist/shared/workspace/platform-dir.d.ts +25 -0
- package/dist/shared/workspace/platform-dir.js +38 -2
- package/dist/shared/workspace/platform-dir.js.map +1 -1
- package/dist/workflows/call-agent-orchestrator.js +56 -7
- package/dist/workflows/call-agent-orchestrator.js.map +1 -1
- package/dist/workflows/connect-mcp-server.d.ts +50 -0
- package/dist/workflows/connect-mcp-server.js +136 -15
- package/dist/workflows/connect-mcp-server.js.map +1 -1
- package/dist/workflows/types.d.ts +8 -0
- package/package.json +2 -2
- package/src/__test-utils__/approval-contract/contract.ts +224 -0
- package/src/__test-utils__/approval-contract/types.ts +179 -0
- package/src/__test-utils__/fake-artifact-storage.ts +72 -0
- package/src/__tests__/approval-gateway-contract.test.ts +29 -0
- package/src/__tests__/claimcheck-codec.test.ts +16 -53
- package/src/__tests__/golden-e2e.test.ts +2 -0
- package/src/__tests__/runner-token-coordinator.test.ts +3 -3
- package/src/activities/__tests__/call-agent-status.test.ts +135 -0
- package/src/activities/__tests__/call-llm.test.ts +1 -1
- package/src/activities/__tests__/classify-tool-approvals.test.ts +208 -1
- package/src/activities/__tests__/discover-mcp-server.test.ts +30 -0
- package/src/activities/__tests__/workflow-event-activities.test.ts +2 -1
- package/src/activities/call-agent-status.ts +74 -4
- package/src/activities/call-llm.ts +18 -63
- package/src/activities/classify-tool-approvals.ts +101 -19
- package/src/activities/discover-mcp-server.ts +29 -1
- package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +216 -0
- package/src/activities/execute-cursor/__test-utils__/gateway-substrate.ts +148 -0
- package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +41 -9
- package/src/activities/execute-cursor/__tests__/approval-state.test.ts +292 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +68 -1
- package/src/activities/execute-cursor/__tests__/capture-flow.test.ts +1005 -0
- package/src/activities/execute-cursor/__tests__/cas-observations.test.ts +187 -0
- package/src/activities/execute-cursor/__tests__/coarse-fingerprint.test.ts +97 -0
- package/src/activities/execute-cursor/__tests__/command-provenance.test.ts +240 -0
- package/src/activities/execute-cursor/__tests__/deny-gate-exact-apply.test.ts +203 -0
- package/src/activities/execute-cursor/__tests__/exact-apply.test.ts +375 -0
- package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +1294 -24
- package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +446 -0
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +384 -110
- package/src/activities/execute-cursor/__tests__/message-translator.test.ts +171 -25
- package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +189 -0
- package/src/activities/execute-cursor/__tests__/tool-result-image.test.ts +44 -23
- package/src/activities/execute-cursor/__tests__/workspace-setup.test.ts +190 -10
- package/src/activities/execute-cursor/approval-policy.ts +28 -159
- package/src/activities/execute-cursor/approval-state.ts +366 -18
- package/src/activities/execute-cursor/capture-flow.ts +323 -0
- package/src/activities/execute-cursor/cas-observations.ts +204 -0
- package/src/activities/execute-cursor/command-provenance.ts +168 -0
- package/src/activities/execute-cursor/exact-apply.ts +253 -0
- package/src/activities/execute-cursor/hook-script.ts +317 -51
- package/src/activities/execute-cursor/index.ts +575 -67
- package/src/activities/execute-cursor/message-translator.ts +963 -89
- package/src/activities/execute-cursor/prompt-builder.ts +80 -14
- package/src/activities/execute-cursor/workspace-setup.ts +257 -42
- package/src/activities/execute-deep-agent/__test-utils__/gateway-substrate.ts +180 -0
- package/src/activities/execute-deep-agent/__test-utils__/scripted-model.ts +134 -0
- package/src/activities/execute-deep-agent/__tests__/approval-file-change.test.ts +84 -0
- package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +11 -24
- package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +64 -0
- package/src/activities/execute-deep-agent/__tests__/cas-capture-observer.test.ts +163 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-integration.test.ts +8 -5
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +342 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +284 -0
- package/src/activities/execute-deep-agent/__tests__/hitl.test.ts +14 -36
- package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +31 -13
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +349 -0
- package/src/activities/execute-deep-agent/__tests__/stamp-flowed-rows.test.ts +119 -0
- package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +91 -10
- package/src/activities/execute-deep-agent/__tests__/streaming-v3.test.ts +9 -9
- package/src/activities/execute-deep-agent/__tests__/subagent-approval-propagation.test.ts +160 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-gitignored-capture.test.ts +213 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-transformer.test.ts +3 -6
- package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +84 -1
- package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +4 -1
- package/src/activities/execute-deep-agent/approval-file-change.ts +80 -0
- package/src/activities/execute-deep-agent/attachment-injector.ts +20 -11
- package/src/activities/execute-deep-agent/cas-capture-backend.ts +66 -0
- package/src/activities/execute-deep-agent/cas-capture-observer.ts +125 -0
- package/src/activities/execute-deep-agent/hitl.ts +23 -7
- package/src/activities/execute-deep-agent/index.ts +463 -40
- package/src/activities/execute-deep-agent/inline-publisher.ts +27 -4
- package/src/activities/execute-deep-agent/setup.ts +223 -125
- package/src/activities/execute-deep-agent/stamp-flowed-rows.ts +64 -0
- package/src/activities/execute-deep-agent/status-builder-shared.ts +62 -23
- package/src/activities/execute-deep-agent/status-builder.ts +63 -8
- package/src/activities/execute-deep-agent/streaming-side-effects.ts +2 -16
- package/src/activities/execute-deep-agent/streaming.ts +3 -13
- package/src/activities/execute-deep-agent/subagent-transformer.ts +53 -13
- package/src/activities/execute-deep-agent/subagent-wiring.ts +50 -3
- package/src/activities/execute-deep-agent/v3-status-builder.ts +36 -2
- package/src/claimcheck/payload-codec.ts +8 -8
- package/src/client/stigmer-client.ts +9 -1
- package/src/middleware/__tests__/approval-gate.test.ts +488 -4
- package/src/middleware/approval-gate.ts +247 -38
- package/src/middleware/types.ts +5 -5
- package/src/shared/__tests__/activity-input.test.ts +78 -0
- package/src/shared/__tests__/approval-canonicalize.test.ts +106 -0
- package/src/shared/__tests__/approval-fingerprint.test.ts +115 -0
- package/src/shared/__tests__/approval-policy.test.ts +274 -40
- package/src/shared/__tests__/args-preview.test.ts +78 -0
- package/src/shared/__tests__/artifact-storage-extended.test.ts +62 -10
- package/src/shared/__tests__/artifact-storage.test.ts +123 -11
- package/src/shared/__tests__/file-change.test.ts +85 -0
- package/src/shared/__tests__/file-tools.test.ts +90 -0
- package/src/shared/__tests__/fingerprint-secret.test.ts +51 -0
- package/src/shared/__tests__/lease-scope-corpus.test.ts +56 -0
- package/src/shared/__tests__/model-client.test.ts +162 -0
- package/src/shared/__tests__/plan-artifact.test.ts +11 -26
- package/src/shared/__tests__/policy-source-corpus.test.ts +58 -0
- package/src/shared/__tests__/status-offload.test.ts +573 -16
- package/src/shared/__tests__/status.test.ts +4 -5
- package/src/shared/__tests__/tool-kind.test.ts +24 -1
- package/src/shared/__tests__/tool-row.test.ts +221 -0
- package/src/shared/activity-input.ts +57 -0
- package/src/shared/approval-canonicalize.ts +159 -0
- package/src/shared/approval-fingerprint.ts +148 -0
- package/src/shared/approval-policy.ts +303 -27
- package/src/shared/args-preview.ts +98 -0
- package/src/shared/artifact-storage.ts +62 -11
- package/src/shared/checkpointer/__tests__/http-saver.test.ts +1 -2
- package/src/shared/file-change.ts +64 -0
- package/src/shared/file-tools.ts +169 -0
- package/src/shared/filereview/__tests__/capture.test.ts +856 -0
- package/src/shared/filereview/__tests__/cas-substrate.test.ts +404 -0
- package/src/shared/filereview/__tests__/digest.test.ts +100 -0
- package/src/shared/filereview/__tests__/events.test.ts +245 -0
- package/src/shared/filereview/__tests__/git-substrate.test.ts +362 -0
- package/src/shared/filereview/__tests__/proxy-reconcile.test.ts +286 -0
- package/src/shared/filereview/__tests__/secret-paths.test.ts +121 -0
- package/src/shared/filereview/capture.ts +727 -0
- package/src/shared/filereview/cas-substrate.ts +401 -0
- package/src/shared/filereview/digest.ts +83 -0
- package/src/shared/filereview/events.ts +449 -0
- package/src/shared/filereview/git-substrate.ts +555 -0
- package/src/shared/filereview/index.ts +60 -0
- package/src/shared/filereview/secret-paths.ts +121 -0
- package/src/shared/fingerprint-secret.ts +53 -0
- package/src/shared/model-client.ts +122 -0
- package/src/shared/plan-artifact.ts +0 -2
- package/src/shared/status-offload.ts +433 -77
- package/src/shared/status.ts +13 -0
- package/src/shared/tool-kind.ts +33 -0
- package/src/shared/tool-row.ts +135 -0
- package/src/shared/workspace/platform-dir.ts +41 -2
- package/src/workflow-engine/__tests__/golden-execution.test.ts +35 -18
- package/src/workflow-engine/__tests__/tasks/try.test.ts +1 -1
- package/src/workflows/__tests__/connect-mcp-server.test.ts +304 -29
- package/src/workflows/call-agent-orchestrator.ts +53 -6
- package/src/workflows/connect-mcp-server.ts +179 -24
- package/src/workflows/types.ts +8 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single canonical in-memory {@link ArtifactStorage} test double.
|
|
3
|
+
*
|
|
4
|
+
* Every runner test that needs a fake storage should use this — do NOT hand-roll
|
|
5
|
+
* another partial `ArtifactStorage` literal. Before this helper existed, ~16 test
|
|
6
|
+
* files each invented their own fake (and their own `getDownloadUrl` convention:
|
|
7
|
+
* `mem://`, `https://artifacts.local/`, `mock://`, identity, …), which drifted
|
|
8
|
+
* from the real port and from each other. This double is the one place the port
|
|
9
|
+
* is modeled for tests.
|
|
10
|
+
*
|
|
11
|
+
* Design:
|
|
12
|
+
* - `upload` / `download` / `exists` are backed by one shared `Map`, so a key
|
|
13
|
+
* that was uploaded reads back byte-exact — the double behaves like a real
|
|
14
|
+
* content store, not a set of disconnected stubs.
|
|
15
|
+
* - Every method is a `vi.fn`, so call assertions still work
|
|
16
|
+
* (`expect(storage.download).toHaveBeenCalledWith(key)`), and any method can
|
|
17
|
+
* be overridden per-test to force a failure
|
|
18
|
+
* (`storage.download.mockRejectedValueOnce(new Error("… HTTP 404 …"))`).
|
|
19
|
+
* - `getDownloadUrl` returns `${urlBase}${key}`; tests that assert on the URL
|
|
20
|
+
* string pass their own `urlBase`.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { vi } from "vitest";
|
|
24
|
+
import type { ArtifactStorage } from "../shared/artifact-storage.js";
|
|
25
|
+
|
|
26
|
+
/** An {@link ArtifactStorage} whose methods are `vi.fn` spies over a shared Map. */
|
|
27
|
+
export type InMemoryArtifactStorage = {
|
|
28
|
+
[K in keyof ArtifactStorage]: ReturnType<typeof vi.fn>;
|
|
29
|
+
} & ArtifactStorage;
|
|
30
|
+
|
|
31
|
+
export interface InMemoryArtifactStorageOptions {
|
|
32
|
+
/** Prefix for the URLs `getDownloadUrl` returns. Defaults to `mem://`. */
|
|
33
|
+
readonly urlBase?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface InMemoryArtifactStorageHandle {
|
|
37
|
+
/** The storage double to inject; all methods are spy-able `vi.fn`s. */
|
|
38
|
+
readonly storage: InMemoryArtifactStorage;
|
|
39
|
+
/** The backing store — inspect or seed it directly in a test. */
|
|
40
|
+
readonly blobs: Map<string, Buffer>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build an in-memory {@link ArtifactStorage} backed by a shared `Map`.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* const { storage, blobs } = makeInMemoryArtifactStorage();
|
|
48
|
+
* await storage.upload("k", Buffer.from("hi"));
|
|
49
|
+
* expect((await storage.download("k")).toString()).toBe("hi");
|
|
50
|
+
*/
|
|
51
|
+
export function makeInMemoryArtifactStorage(
|
|
52
|
+
opts: InMemoryArtifactStorageOptions = {},
|
|
53
|
+
): InMemoryArtifactStorageHandle {
|
|
54
|
+
const urlBase = opts.urlBase ?? "mem://";
|
|
55
|
+
const blobs = new Map<string, Buffer>();
|
|
56
|
+
|
|
57
|
+
const storage = {
|
|
58
|
+
upload: vi.fn(async (key: string, content: Buffer, _contentType?: string) => {
|
|
59
|
+
blobs.set(key, Buffer.from(content));
|
|
60
|
+
return key;
|
|
61
|
+
}),
|
|
62
|
+
getDownloadUrl: vi.fn(async (key: string) => `${urlBase}${key}`),
|
|
63
|
+
download: vi.fn(async (key: string) => {
|
|
64
|
+
const b = blobs.get(key);
|
|
65
|
+
if (!b) throw new Error(`Artifact not found for key '${key}'`);
|
|
66
|
+
return Buffer.from(b);
|
|
67
|
+
}),
|
|
68
|
+
exists: vi.fn(async (key: string) => blobs.has(key)),
|
|
69
|
+
} as InMemoryArtifactStorage;
|
|
70
|
+
|
|
71
|
+
return { storage, blobs };
|
|
72
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runnable HITL gateway P0 safety contract.
|
|
3
|
+
*
|
|
4
|
+
* Runs the single, authoritative invariant catalog (see
|
|
5
|
+
* `__test-utils__/approval-contract/contract.ts`) against BOTH real enforcement
|
|
6
|
+
* substrates — the in-process deep-agent gate (a real LangGraph graph) and the
|
|
7
|
+
* out-of-process Cursor deny-oracle (the real bash hook) — and then asserts the
|
|
8
|
+
* two substrates AGREE on the same logical action.
|
|
9
|
+
*
|
|
10
|
+
* This is the consolidation home for the Phase-2 (T03) gateway safety behaviors:
|
|
11
|
+
* reverting any P0 behavior on either substrate fails here, and a future
|
|
12
|
+
* substrate joins the safety net by implementing `GatewaySubstrate` and adding
|
|
13
|
+
* one line below. The Cursor suite self-skips where `bash` is unavailable.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
describeGatewayContract,
|
|
18
|
+
describeCrossSubstrateAgreement,
|
|
19
|
+
} from "../__test-utils__/approval-contract/contract.js";
|
|
20
|
+
import { createDeepAgentSubstrate } from "../activities/execute-deep-agent/__test-utils__/gateway-substrate.js";
|
|
21
|
+
import { createCursorSubstrate } from "../activities/execute-cursor/__test-utils__/gateway-substrate.js";
|
|
22
|
+
|
|
23
|
+
const deepAgent = createDeepAgentSubstrate();
|
|
24
|
+
const cursor = createCursorSubstrate();
|
|
25
|
+
|
|
26
|
+
describeGatewayContract(deepAgent);
|
|
27
|
+
describeGatewayContract(cursor);
|
|
28
|
+
|
|
29
|
+
describeCrossSubstrateAgreement([deepAgent, cursor]);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
2
|
import { ClaimcheckPayloadCodec } from "../claimcheck/payload-codec.js";
|
|
3
3
|
import { compress } from "../claimcheck/compressor.js";
|
|
4
4
|
import type { ClaimcheckConfig } from "../claimcheck/config.js";
|
|
5
|
-
import
|
|
5
|
+
import { makeInMemoryArtifactStorage } from "../__test-utils__/fake-artifact-storage.js";
|
|
6
6
|
import type { Payload } from "@temporalio/common";
|
|
7
7
|
|
|
8
8
|
function makeConfig(overrides: Partial<ClaimcheckConfig> = {}): ClaimcheckConfig {
|
|
@@ -15,23 +15,12 @@ function makeConfig(overrides: Partial<ClaimcheckConfig> = {}): ClaimcheckConfig
|
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
function makeStorage()
|
|
19
|
-
uploads
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
async upload(key: string, content: Buffer) {
|
|
25
|
-
uploads.set(key, content);
|
|
26
|
-
return key;
|
|
27
|
-
},
|
|
28
|
-
async getDownloadUrl(key: string) {
|
|
29
|
-
return `mock://storage/${key}`;
|
|
30
|
-
},
|
|
31
|
-
async exists(key: string) {
|
|
32
|
-
return uploads.has(key);
|
|
33
|
-
},
|
|
34
|
-
};
|
|
18
|
+
function makeStorage() {
|
|
19
|
+
// The canonical in-memory double; `uploads` aliases its backing Map so the
|
|
20
|
+
// existing `storage.uploads.*` assertions keep working, and `download` reads
|
|
21
|
+
// straight from what `encode` uploaded (no fetch to stub).
|
|
22
|
+
const { storage, blobs } = makeInMemoryArtifactStorage();
|
|
23
|
+
return Object.assign(storage, { uploads: blobs });
|
|
35
24
|
}
|
|
36
25
|
|
|
37
26
|
function makePayload(data: string | Buffer): Payload {
|
|
@@ -158,21 +147,9 @@ describe("ClaimcheckPayloadCodec", () => {
|
|
|
158
147
|
const original = makeLargePayload(2048);
|
|
159
148
|
const [encoded] = await codec.encode([original]);
|
|
160
149
|
|
|
161
|
-
|
|
162
|
-
const storedData = storage.uploads.get(marker.key)!;
|
|
163
|
-
|
|
164
|
-
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
|
|
165
|
-
ok: true,
|
|
166
|
-
arrayBuffer: () => Promise.resolve(storedData.buffer.slice(
|
|
167
|
-
storedData.byteOffset,
|
|
168
|
-
storedData.byteOffset + storedData.byteLength,
|
|
169
|
-
)),
|
|
170
|
-
}));
|
|
171
|
-
|
|
150
|
+
// decode reads back through storage.download from what encode uploaded.
|
|
172
151
|
const [decoded] = await codec.decode([encoded]);
|
|
173
152
|
expect(Buffer.from(decoded.data!)).toEqual(original.data);
|
|
174
|
-
|
|
175
|
-
vi.unstubAllGlobals();
|
|
176
153
|
});
|
|
177
154
|
|
|
178
155
|
it("retrieves uncompressed payloads correctly", async () => {
|
|
@@ -184,24 +161,11 @@ describe("ClaimcheckPayloadCodec", () => {
|
|
|
184
161
|
const original = makeLargePayload(1500);
|
|
185
162
|
const [encoded] = await codec.encode([original]);
|
|
186
163
|
|
|
187
|
-
const marker = JSON.parse(Buffer.from(encoded.data!).toString());
|
|
188
|
-
const storedData = storage.uploads.get(marker.key)!;
|
|
189
|
-
|
|
190
|
-
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
|
|
191
|
-
ok: true,
|
|
192
|
-
arrayBuffer: () => Promise.resolve(storedData.buffer.slice(
|
|
193
|
-
storedData.byteOffset,
|
|
194
|
-
storedData.byteOffset + storedData.byteLength,
|
|
195
|
-
)),
|
|
196
|
-
}));
|
|
197
|
-
|
|
198
164
|
const [decoded] = await codec.decode([encoded]);
|
|
199
165
|
expect(Buffer.from(decoded.data!)).toEqual(original.data);
|
|
200
|
-
|
|
201
|
-
vi.unstubAllGlobals();
|
|
202
166
|
});
|
|
203
167
|
|
|
204
|
-
it("
|
|
168
|
+
it("wraps a download failure with the claimcheck error contract", async () => {
|
|
205
169
|
const encoded: Payload = {
|
|
206
170
|
metadata: { encoding: Buffer.from("binary/claimcheck") },
|
|
207
171
|
data: Buffer.from(JSON.stringify({
|
|
@@ -211,16 +175,15 @@ describe("ClaimcheckPayloadCodec", () => {
|
|
|
211
175
|
})),
|
|
212
176
|
};
|
|
213
177
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
178
|
+
// Drive the failure through the port; the proxy download surfaces the HTTP
|
|
179
|
+
// status, which the codec must preserve in its wrapped, key-scoped message.
|
|
180
|
+
storage.download.mockRejectedValueOnce(
|
|
181
|
+
new Error("Artifact download failed (HTTP 404) for key 'claimcheck/missing'"),
|
|
182
|
+
);
|
|
218
183
|
|
|
219
184
|
await expect(codec.decode([encoded])).rejects.toThrow(
|
|
220
|
-
/Claimcheck retrieve failed.*HTTP 404/,
|
|
185
|
+
/Claimcheck retrieve failed for key claimcheck\/missing.*HTTP 404/,
|
|
221
186
|
);
|
|
222
|
-
|
|
223
|
-
vi.unstubAllGlobals();
|
|
224
187
|
});
|
|
225
188
|
});
|
|
226
189
|
|
|
@@ -78,6 +78,8 @@ function createMockActivities() {
|
|
|
78
78
|
RunShell: async (): Promise<unknown> => "hello from shell",
|
|
79
79
|
UpdateWorkflowTaskApprovalStatus: async (): Promise<void> => {},
|
|
80
80
|
ClearWorkflowApprovalStatus: async (): Promise<void> => {},
|
|
81
|
+
UpdateWorkflowFileReviewStatus: async (): Promise<void> => {},
|
|
82
|
+
GetAwaitingFileReviewChangeSetIds: async (): Promise<string[]> => [],
|
|
81
83
|
};
|
|
82
84
|
}
|
|
83
85
|
|
|
@@ -92,7 +92,7 @@ describe("createRunnerTokenCoordinator", () => {
|
|
|
92
92
|
it("re-mints from the control-plane token before expiry and reschedules", async () => {
|
|
93
93
|
const applyProxyToken = vi.fn();
|
|
94
94
|
const reMint = vi
|
|
95
|
-
.fn<
|
|
95
|
+
.fn<() => Promise<RefreshedRunnerToken | undefined>>()
|
|
96
96
|
.mockResolvedValueOnce({ token: "rt_v2", expiresInSeconds: 100 })
|
|
97
97
|
.mockResolvedValueOnce({ token: "rt_v3", expiresInSeconds: 100 });
|
|
98
98
|
|
|
@@ -121,7 +121,7 @@ describe("createRunnerTokenCoordinator", () => {
|
|
|
121
121
|
it("retries (without clobbering) when a re-mint fails, then recovers", async () => {
|
|
122
122
|
const applyProxyToken = vi.fn();
|
|
123
123
|
const reMint = vi
|
|
124
|
-
.fn<
|
|
124
|
+
.fn<() => Promise<RefreshedRunnerToken | undefined>>()
|
|
125
125
|
.mockResolvedValueOnce(undefined) // first refresh fails to mint
|
|
126
126
|
.mockResolvedValueOnce({ token: "rt_recovered", expiresInSeconds: 100 });
|
|
127
127
|
|
|
@@ -149,7 +149,7 @@ describe("createRunnerTokenCoordinator", () => {
|
|
|
149
149
|
});
|
|
150
150
|
|
|
151
151
|
it("stop() cancels the refresh timer", async () => {
|
|
152
|
-
const reMint = vi.fn<
|
|
152
|
+
const reMint = vi.fn<() => Promise<RefreshedRunnerToken | undefined>>();
|
|
153
153
|
const coordinator = createRunnerTokenCoordinator({
|
|
154
154
|
applyProxyToken: vi.fn(),
|
|
155
155
|
reMint,
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import { FileChangeSetStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Unit tests for the call-agent-status activities that surface a child agent's
|
|
6
|
+
* HITL state on the parent workflow. They verify the runner sends the correct
|
|
7
|
+
* per-child SCOPED writes; the actual per-child merge (never clobbering a
|
|
8
|
+
* sibling) is enforced and tested in the Go + Java backend handlers.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
interface CapturedUpdate {
|
|
12
|
+
executionId: string;
|
|
13
|
+
status: any;
|
|
14
|
+
options: any;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let capturedUpdates: CapturedUpdate[];
|
|
18
|
+
let mockGetExecutionResult: any;
|
|
19
|
+
|
|
20
|
+
vi.mock("../../client/stigmer-client.js", () => ({
|
|
21
|
+
StigmerClient: vi.fn().mockImplementation(() => ({
|
|
22
|
+
updateWorkflowExecutionStatus: (executionId: string, status: any, options: any) => {
|
|
23
|
+
capturedUpdates.push({ executionId, status, options });
|
|
24
|
+
return Promise.resolve({});
|
|
25
|
+
},
|
|
26
|
+
getExecution: (_id: string) => Promise.resolve(mockGetExecutionResult),
|
|
27
|
+
})),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock("../../config.js", () => ({
|
|
31
|
+
loadConfig: () => ({ stigmerBackendEndpoint: "http://localhost:7234", stigmerToken: "t" }),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
updateWorkflowFileReviewStatus,
|
|
36
|
+
getAwaitingFileReviewChangeSetIds,
|
|
37
|
+
updateWorkflowTaskApprovalStatus,
|
|
38
|
+
clearWorkflowApprovalStatus,
|
|
39
|
+
} from "../call-agent-status.js";
|
|
40
|
+
|
|
41
|
+
describe("call-agent-status file-review activities", () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
capturedUpdates = [];
|
|
44
|
+
mockGetExecutionResult = undefined;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("updateWorkflowFileReviewStatus", () => {
|
|
48
|
+
it("writes a single per-child reference scoped to the child", async () => {
|
|
49
|
+
await updateWorkflowFileReviewStatus("wfx_1", "aex_child", ["fcs_1", "fcs_2"]);
|
|
50
|
+
|
|
51
|
+
expect(capturedUpdates).toHaveLength(1);
|
|
52
|
+
const { executionId, status, options } = capturedUpdates[0];
|
|
53
|
+
expect(executionId).toBe("wfx_1");
|
|
54
|
+
expect(options.updatePendingFileReviews).toBe(true);
|
|
55
|
+
expect(options.pendingUpdateChildAgentExecutionId).toBe("aex_child");
|
|
56
|
+
expect(status.pendingFileReviews).toHaveLength(1);
|
|
57
|
+
expect(status.pendingFileReviews[0].childAgentExecutionId).toBe("aex_child");
|
|
58
|
+
expect(status.pendingFileReviews[0].changeSetId).toEqual(["fcs_1", "fcs_2"]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("empty changeSetIds writes an empty list (scoped clear for the child)", async () => {
|
|
62
|
+
await updateWorkflowFileReviewStatus("wfx_1", "aex_child", []);
|
|
63
|
+
|
|
64
|
+
expect(capturedUpdates).toHaveLength(1);
|
|
65
|
+
const { status, options } = capturedUpdates[0];
|
|
66
|
+
expect(options.updatePendingFileReviews).toBe(true);
|
|
67
|
+
expect(options.pendingUpdateChildAgentExecutionId).toBe("aex_child");
|
|
68
|
+
expect(status.pendingFileReviews).toHaveLength(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("is a no-op when executionId or childExecutionId is missing", async () => {
|
|
72
|
+
await updateWorkflowFileReviewStatus("", "aex_child", ["fcs_1"]);
|
|
73
|
+
await updateWorkflowFileReviewStatus("wfx_1", "", ["fcs_1"]);
|
|
74
|
+
expect(capturedUpdates).toHaveLength(0);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("getAwaitingFileReviewChangeSetIds", () => {
|
|
79
|
+
it("returns only the ids of change sets that are AWAITING_REVIEW", async () => {
|
|
80
|
+
mockGetExecutionResult = {
|
|
81
|
+
status: {
|
|
82
|
+
fileChangeSets: [
|
|
83
|
+
{ id: "fcs_await1", status: FileChangeSetStatus.AWAITING_REVIEW },
|
|
84
|
+
{ id: "fcs_capturing", status: FileChangeSetStatus.CAPTURING },
|
|
85
|
+
{ id: "fcs_decided", status: FileChangeSetStatus.DECIDED },
|
|
86
|
+
{ id: "fcs_await2", status: FileChangeSetStatus.AWAITING_REVIEW },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const ids = await getAwaitingFileReviewChangeSetIds("aex_child");
|
|
92
|
+
expect(ids).toEqual(["fcs_await1", "fcs_await2"]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("returns empty when the child has no file change sets", async () => {
|
|
96
|
+
mockGetExecutionResult = { status: {} };
|
|
97
|
+
expect(await getAwaitingFileReviewChangeSetIds("aex_child")).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("returns empty (non-fatal) on a missing child id", async () => {
|
|
101
|
+
expect(await getAwaitingFileReviewChangeSetIds("")).toEqual([]);
|
|
102
|
+
expect(capturedUpdates).toHaveLength(0);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("approval activities are scoped per-child (unify)", () => {
|
|
107
|
+
it("updateWorkflowTaskApprovalStatus scopes the write to the notifying child", async () => {
|
|
108
|
+
await updateWorkflowTaskApprovalStatus("wfx_1", "task_a", {
|
|
109
|
+
executionId: "aex_child",
|
|
110
|
+
pendingApprovals: [{ toolCallId: "tc_1", toolName: "deploy" } as any],
|
|
111
|
+
} as any);
|
|
112
|
+
|
|
113
|
+
expect(capturedUpdates).toHaveLength(1);
|
|
114
|
+
const { status, options } = capturedUpdates[0];
|
|
115
|
+
expect(options.updatePendingApprovals).toBe(true);
|
|
116
|
+
expect(options.pendingUpdateChildAgentExecutionId).toBe("aex_child");
|
|
117
|
+
expect(status.pendingApprovals[0].childAgentExecutionId).toBe("aex_child");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("clearWorkflowApprovalStatus scopes the clear to the given child", async () => {
|
|
121
|
+
await clearWorkflowApprovalStatus("wfx_1", "aex_child");
|
|
122
|
+
|
|
123
|
+
expect(capturedUpdates).toHaveLength(1);
|
|
124
|
+
const { status, options } = capturedUpdates[0];
|
|
125
|
+
expect(options.updatePendingApprovals).toBe(true);
|
|
126
|
+
expect(options.pendingUpdateChildAgentExecutionId).toBe("aex_child");
|
|
127
|
+
expect(status.pendingApprovals).toHaveLength(0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("clearWorkflowApprovalStatus is a no-op without a child id", async () => {
|
|
131
|
+
await clearWorkflowApprovalStatus("wfx_1", "");
|
|
132
|
+
expect(capturedUpdates).toHaveLength(0);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -32,7 +32,7 @@ describe("callLlmAction", () => {
|
|
|
32
32
|
|
|
33
33
|
// The LangChain provider SDKs pass a `Headers` instance (not a plain object)
|
|
34
34
|
// to fetch, so read header values case-insensitively via the Headers API.
|
|
35
|
-
function getHeader(options: { headers?:
|
|
35
|
+
function getHeader(options: { headers?: ConstructorParameters<typeof Headers>[0] }, name: string): string | null {
|
|
36
36
|
return new Headers(options.headers).get(name);
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -8,6 +8,9 @@ vi.mock("../../idle-watchdog.js", () => ({
|
|
|
8
8
|
|
|
9
9
|
vi.mock("../../shared/model-registry.js", () => ({
|
|
10
10
|
getSummarizationModel: vi.fn().mockResolvedValue("gpt-4o-mini"),
|
|
11
|
+
// buildChatModel resolves registry ids; identity keeps the economy model name
|
|
12
|
+
// intact so provider inference drives client selection in these tests.
|
|
13
|
+
resolveToApiModelId: vi.fn((m: string) => Promise.resolve(m)),
|
|
11
14
|
}));
|
|
12
15
|
|
|
13
16
|
const mockInvoke = vi.fn();
|
|
@@ -20,6 +23,14 @@ vi.mock("@langchain/openai", () => ({
|
|
|
20
23
|
})),
|
|
21
24
|
}));
|
|
22
25
|
|
|
26
|
+
vi.mock("@langchain/anthropic", () => ({
|
|
27
|
+
ChatAnthropic: vi.fn().mockImplementation(() => ({
|
|
28
|
+
withStructuredOutput: vi.fn().mockReturnValue({
|
|
29
|
+
invoke: mockInvoke,
|
|
30
|
+
}),
|
|
31
|
+
})),
|
|
32
|
+
}));
|
|
33
|
+
|
|
23
34
|
vi.mock("@langchain/core/messages", () => ({
|
|
24
35
|
SystemMessage: vi.fn().mockImplementation((content: string) => ({ content, role: "system" })),
|
|
25
36
|
HumanMessage: vi.fn().mockImplementation((content: string) => ({ content, role: "human" })),
|
|
@@ -223,6 +234,106 @@ describe("ClassifyToolApprovals activity", () => {
|
|
|
223
234
|
});
|
|
224
235
|
});
|
|
225
236
|
|
|
237
|
+
describe("reconcileBatchClassifications — fail closed on partial output", () => {
|
|
238
|
+
it("fails closed for a tool the model omitted from its output", async () => {
|
|
239
|
+
const { reconcileBatchClassifications } = await import("../classify-tool-approvals.js");
|
|
240
|
+
|
|
241
|
+
const batch: ToolDescriptor[] = [
|
|
242
|
+
{ name: "delete_file", description: "Delete a file" },
|
|
243
|
+
{ name: "send_email", description: "Send an email" },
|
|
244
|
+
];
|
|
245
|
+
// Model only classified one of the two tools.
|
|
246
|
+
const llmResults = [
|
|
247
|
+
{ tool_name: "delete_file", requires_approval: true, message: "Delete {{args.path}}" },
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
const { reconciled, failedClosedCount } = reconcileBatchClassifications(batch, llmResults);
|
|
251
|
+
|
|
252
|
+
expect(failedClosedCount).toBe(1);
|
|
253
|
+
expect(reconciled).toEqual([
|
|
254
|
+
{ tool_name: "delete_file", requires_approval: true, message: "Delete {{args.path}}" },
|
|
255
|
+
{ tool_name: "send_email", requires_approval: true, message: "Execute send_email" },
|
|
256
|
+
]);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("keeps full classifications unchanged when every tool is present", async () => {
|
|
260
|
+
const { reconcileBatchClassifications } = await import("../classify-tool-approvals.js");
|
|
261
|
+
|
|
262
|
+
const batch: ToolDescriptor[] = [
|
|
263
|
+
{ name: "search_code", description: "Search" },
|
|
264
|
+
{ name: "delete_file", description: "Delete" },
|
|
265
|
+
];
|
|
266
|
+
const llmResults = [
|
|
267
|
+
{ tool_name: "search_code", requires_approval: false, message: "" },
|
|
268
|
+
{ tool_name: "delete_file", requires_approval: true, message: "Delete {{args.path}}" },
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
const { reconciled, failedClosedCount } = reconcileBatchClassifications(batch, llmResults);
|
|
272
|
+
|
|
273
|
+
expect(failedClosedCount).toBe(0);
|
|
274
|
+
expect(reconciled).toEqual(llmResults);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("drops hallucinated output names that were never in the batch", async () => {
|
|
278
|
+
const { reconcileBatchClassifications } = await import("../classify-tool-approvals.js");
|
|
279
|
+
|
|
280
|
+
const batch: ToolDescriptor[] = [{ name: "real_tool", description: "Real" }];
|
|
281
|
+
const llmResults = [
|
|
282
|
+
{ tool_name: "real_tool", requires_approval: false, message: "" },
|
|
283
|
+
{ tool_name: "ghost_tool", requires_approval: false, message: "" },
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
const { reconciled } = reconcileBatchClassifications(batch, llmResults);
|
|
287
|
+
|
|
288
|
+
expect(reconciled).toHaveLength(1);
|
|
289
|
+
expect(reconciled[0].tool_name).toBe("real_tool");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("fails closed for the entire batch when the model returns nothing", async () => {
|
|
293
|
+
const { reconcileBatchClassifications } = await import("../classify-tool-approvals.js");
|
|
294
|
+
|
|
295
|
+
const batch: ToolDescriptor[] = [
|
|
296
|
+
{ name: "tool_a", description: "" },
|
|
297
|
+
{ name: "tool_b", description: "" },
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
const { reconciled, failedClosedCount } = reconcileBatchClassifications(batch, []);
|
|
301
|
+
|
|
302
|
+
expect(failedClosedCount).toBe(2);
|
|
303
|
+
expect(reconciled.every((r) => r.requires_approval)).toBe(true);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("end-to-end: an omitted mutating tool is gated, not silently dropped", async () => {
|
|
307
|
+
const { classifyTools } = await import("../classify-tool-approvals.js");
|
|
308
|
+
|
|
309
|
+
// The model omits `wire_transfer` entirely — only classifies the read tool.
|
|
310
|
+
mockInvoke.mockResolvedValueOnce({
|
|
311
|
+
approvals: [
|
|
312
|
+
{ tool_name: "get_balance", requires_approval: false, message: "" },
|
|
313
|
+
],
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const result = await classifyTools(
|
|
317
|
+
{
|
|
318
|
+
tools: [
|
|
319
|
+
{ name: "get_balance", description: "Get account balance" },
|
|
320
|
+
{ name: "wire_transfer", description: "Transfer money" },
|
|
321
|
+
],
|
|
322
|
+
serverName: "bank",
|
|
323
|
+
serverDescription: "",
|
|
324
|
+
mcpServerId: null,
|
|
325
|
+
},
|
|
326
|
+
makeOptions(),
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
// get_balance auto-approved by the classifier; wire_transfer (omitted)
|
|
330
|
+
// fails closed via reconciliation.
|
|
331
|
+
expect(result).toHaveLength(1);
|
|
332
|
+
expect(result[0].tool_name).toBe("wire_transfer");
|
|
333
|
+
expect(result[0].requires_approval).toBe(true);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
226
337
|
describe("buildToolsPayload", () => {
|
|
227
338
|
it("formats tools as compact JSON with parameter names only", async () => {
|
|
228
339
|
const { buildToolsPayload } = await import("../classify-tool-approvals.js");
|
|
@@ -280,7 +391,7 @@ describe("ClassifyToolApprovals activity", () => {
|
|
|
280
391
|
});
|
|
281
392
|
|
|
282
393
|
describe("fallbackApprovals", () => {
|
|
283
|
-
it("marks
|
|
394
|
+
it("marks ambiguous tools as requiring approval with default message", async () => {
|
|
284
395
|
const { fallbackApprovals } = await import("../classify-tool-approvals.js");
|
|
285
396
|
|
|
286
397
|
const tools: ToolDescriptor[] = [
|
|
@@ -295,6 +406,79 @@ describe("ClassifyToolApprovals activity", () => {
|
|
|
295
406
|
{ tool_name: "tool_b", requires_approval: true, message: "Execute tool_b" },
|
|
296
407
|
]);
|
|
297
408
|
});
|
|
409
|
+
|
|
410
|
+
it("fails closed for every tool, including read-only-looking names", async () => {
|
|
411
|
+
const { fallbackApprovals } = await import("../classify-tool-approvals.js");
|
|
412
|
+
|
|
413
|
+
// Full fail-closed: with no trusted classification, even a `get_*` name is
|
|
414
|
+
// gated. A name is an untrusted signal and must not relax a gate on outage.
|
|
415
|
+
const result = fallbackApprovals([
|
|
416
|
+
{ name: "get_app_state", description: "" },
|
|
417
|
+
{ name: "delete_file", description: "" },
|
|
418
|
+
]);
|
|
419
|
+
|
|
420
|
+
expect(result).toEqual([
|
|
421
|
+
{ tool_name: "get_app_state", requires_approval: true, message: "Execute get_app_state" },
|
|
422
|
+
{ tool_name: "delete_file", requires_approval: true, message: "Execute delete_file" },
|
|
423
|
+
]);
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
describe("read-only authority is the LLM classifier alone (no name relax)", () => {
|
|
428
|
+
it("keeps a classifier-gated tool gated even if its name leads with a read verb", async () => {
|
|
429
|
+
const { classifyTools } = await import("../classify-tool-approvals.js");
|
|
430
|
+
|
|
431
|
+
// The classic hole: `get_and_delete_stale_records` leads with `get` but
|
|
432
|
+
// deletes. The old name heuristic auto-approved it; now the trusted LLM
|
|
433
|
+
// decision (gate it) must stand.
|
|
434
|
+
mockInvoke.mockResolvedValueOnce({
|
|
435
|
+
approvals: [
|
|
436
|
+
{ tool_name: "get_and_delete_stale_records", requires_approval: true, message: "Delete stale records" },
|
|
437
|
+
],
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const result = await classifyTools(
|
|
441
|
+
{
|
|
442
|
+
tools: [{ name: "get_and_delete_stale_records", description: "Reads then deletes stale rows" }],
|
|
443
|
+
serverName: "db",
|
|
444
|
+
serverDescription: "",
|
|
445
|
+
mcpServerId: null,
|
|
446
|
+
},
|
|
447
|
+
makeOptions(),
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
expect(result).toHaveLength(1);
|
|
451
|
+
expect(result[0].tool_name).toBe("get_and_delete_stale_records");
|
|
452
|
+
expect(result[0].requires_approval).toBe(true);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it("respects the classifier auto-approving a genuine read-only tool", async () => {
|
|
456
|
+
const { classifyTools } = await import("../classify-tool-approvals.js");
|
|
457
|
+
|
|
458
|
+
mockInvoke.mockResolvedValueOnce({
|
|
459
|
+
approvals: [
|
|
460
|
+
{ tool_name: "get_app_state", requires_approval: false, message: "" },
|
|
461
|
+
{ tool_name: "click", requires_approval: true, message: "Click {{args.element}}" },
|
|
462
|
+
],
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
const result = await classifyTools(
|
|
466
|
+
{
|
|
467
|
+
tools: [
|
|
468
|
+
{ name: "get_app_state", description: "Reads UI state" },
|
|
469
|
+
{ name: "click", description: "Clicks an element" },
|
|
470
|
+
],
|
|
471
|
+
serverName: "open-computer-use",
|
|
472
|
+
serverDescription: "",
|
|
473
|
+
mcpServerId: null,
|
|
474
|
+
},
|
|
475
|
+
makeOptions(),
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
// Only what the classifier gated remains; the read tool it cleared is gone.
|
|
479
|
+
expect(result).toHaveLength(1);
|
|
480
|
+
expect(result[0].tool_name).toBe("click");
|
|
481
|
+
});
|
|
298
482
|
});
|
|
299
483
|
|
|
300
484
|
describe("idle watchdog integration", () => {
|
|
@@ -345,6 +529,29 @@ describe("ClassifyToolApprovals activity", () => {
|
|
|
345
529
|
});
|
|
346
530
|
});
|
|
347
531
|
|
|
532
|
+
describe("provider routing", () => {
|
|
533
|
+
it("routes Anthropic economy models to ChatAnthropic, not the OpenAI path", async () => {
|
|
534
|
+
const { ChatAnthropic } = await import("@langchain/anthropic");
|
|
535
|
+
const { ChatOpenAI } = await import("@langchain/openai");
|
|
536
|
+
const { getSummarizationModel } = await import("../../shared/model-registry.js");
|
|
537
|
+
vi.mocked(getSummarizationModel).mockResolvedValueOnce("claude-haiku-4.5");
|
|
538
|
+
|
|
539
|
+
const { classifyTools } = await import("../classify-tool-approvals.js");
|
|
540
|
+
|
|
541
|
+
mockInvoke.mockResolvedValueOnce({
|
|
542
|
+
approvals: [{ tool_name: "t", requires_approval: false, message: "" }],
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
await classifyTools(
|
|
546
|
+
{ tools: [{ name: "t", description: "d" }], serverName: "s", serverDescription: "", mcpServerId: null },
|
|
547
|
+
{ proxyEndpoint: "http://proxy:8080", stigmerToken: "tok", primaryModel: "claude-opus-4" },
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
expect(ChatAnthropic).toHaveBeenCalledTimes(1);
|
|
551
|
+
expect(ChatOpenAI).not.toHaveBeenCalled();
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
|
|
348
555
|
describe("proxy headers", () => {
|
|
349
556
|
it("passes X-Stigmer-Mcp-Server-Id when mcpServerId is provided", async () => {
|
|
350
557
|
const { ChatOpenAI } = await import("@langchain/openai");
|