@stigmer/runner 3.0.9-dev.20260616060535 → 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 +10 -0
- package/dist/activities/execute-deep-agent/hitl.js +5 -1
- 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 +370 -56
- 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 +11 -5
- package/dist/activities/execute-deep-agent/status-builder.js +6 -2
- 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 +6 -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 +2 -5
- 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 +12 -11
- 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 +15 -1
- package/src/activities/execute-deep-agent/index.ts +434 -64
- 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 +19 -7
- 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 +8 -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,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep-agent adapter for the HITL gateway Contract Test Kit.
|
|
3
|
+
*
|
|
4
|
+
* Drives the REAL in-process gateway: a `createDeepAgent` graph carrying the
|
|
5
|
+
* production {@link createApprovalGateMiddleware}, a `MemorySaver` checkpointer,
|
|
6
|
+
* and a scripted model that proposes the action under test. `authorize` runs the
|
|
7
|
+
* graph to its first pause and, for an approving/non-approving decision, resumes
|
|
8
|
+
* via `Command(resume=…)` — exactly the production pause/resume cycle proven in
|
|
9
|
+
* `__tests__/subagent-approval-propagation.test.ts`. There is no mocked
|
|
10
|
+
* `interrupt()`: the gate's own `interrupt()` fires, so the contract exercises the
|
|
11
|
+
* gateway end-to-end.
|
|
12
|
+
*
|
|
13
|
+
* Capabilities: this substrate IS the side effect, so it observes (and counts)
|
|
14
|
+
* execution; resource-exact leasing is not a property of this gate (sameness comes
|
|
15
|
+
* from checkpoint replay), so `enforcesExactResource` is false and
|
|
16
|
+
* `authorizeAfterGrant` is intentionally not implemented.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { HumanMessage } from "@langchain/core/messages";
|
|
20
|
+
import { tool } from "@langchain/core/tools";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
import { Command, MemorySaver } from "@langchain/langgraph";
|
|
23
|
+
import { createDeepAgent, StateBackend } from "deepagents";
|
|
24
|
+
|
|
25
|
+
import { createApprovalGateMiddleware } from "../../../middleware/approval-gate.js";
|
|
26
|
+
import { deriveLeaseScope } from "../../../shared/approval-policy.js";
|
|
27
|
+
import type { ToolApprovalCategory } from "../../../shared/tool-kind.js";
|
|
28
|
+
import { ScriptedModel, readPendingInterrupts } from "./scripted-model.js";
|
|
29
|
+
import type {
|
|
30
|
+
GatewayDecision,
|
|
31
|
+
GatewayOutcome,
|
|
32
|
+
GatewaySubstrate,
|
|
33
|
+
ProposedAction,
|
|
34
|
+
} from "../../../__test-utils__/approval-contract/types.js";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Translate an abstract action into a deep-agent tool name + args + MCP server.
|
|
38
|
+
* The names are deliberately ALIASES, not deepagents built-ins, so we can attach
|
|
39
|
+
* a counting handler without shadowing an injected built-in, while still hitting
|
|
40
|
+
* the right approval category via the shared classifier:
|
|
41
|
+
* - write → `overwrite_file` (FILE_WRITE), delete → `remove_file` (FILE_DELETE),
|
|
42
|
+
* shell → `execute_command` (SHELL): each gated fail-closed by category.
|
|
43
|
+
* - read → an unknown, non-mutating name: auto-approved (fail-open), no gate.
|
|
44
|
+
* - mcp → the MCP tool name mapped to its server: governed by policy, absent
|
|
45
|
+
* policy ⇒ auto-approved.
|
|
46
|
+
*/
|
|
47
|
+
function toDeepAgentTool(action: ProposedAction): {
|
|
48
|
+
name: string;
|
|
49
|
+
args: Record<string, unknown>;
|
|
50
|
+
serverSlug: string;
|
|
51
|
+
} {
|
|
52
|
+
switch (action.kind) {
|
|
53
|
+
case "write":
|
|
54
|
+
return { name: "overwrite_file", args: { path: action.resource, content: "x" }, serverSlug: "" };
|
|
55
|
+
case "shell":
|
|
56
|
+
return { name: "execute_command", args: { command: action.resource }, serverSlug: "" };
|
|
57
|
+
case "delete":
|
|
58
|
+
return { name: "remove_file", args: { path: action.resource }, serverSlug: "" };
|
|
59
|
+
case "read":
|
|
60
|
+
return { name: "inspect_resource", args: { path: action.resource }, serverSlug: "" };
|
|
61
|
+
case "mcp":
|
|
62
|
+
return { name: action.mcpToolName ?? "mcp_tool", args: {}, serverSlug: action.mcpServerSlug ?? "srv" };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Map a contract decision to the gate's resume verdict (anything unrecognized = unknown). */
|
|
67
|
+
function toResumeAction(decision: GatewayDecision): string {
|
|
68
|
+
switch (decision) {
|
|
69
|
+
case "approve":
|
|
70
|
+
return "approve";
|
|
71
|
+
case "skip":
|
|
72
|
+
return "skip";
|
|
73
|
+
case "reject":
|
|
74
|
+
return "reject";
|
|
75
|
+
default:
|
|
76
|
+
return "unrecognized-verdict";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let threadSeq = 0;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Drive one probe action through a freshly built gate and report the outcome.
|
|
84
|
+
* `leasedCategories` pre-arms a run-lifetime class lease (empty for the plain
|
|
85
|
+
* authorize path); `decision` resumes a pause for the non-lease drives.
|
|
86
|
+
*/
|
|
87
|
+
async function runProbe(
|
|
88
|
+
action: ProposedAction,
|
|
89
|
+
decision: GatewayDecision,
|
|
90
|
+
leasedCategories: ReadonlySet<ToolApprovalCategory>,
|
|
91
|
+
): Promise<GatewayOutcome> {
|
|
92
|
+
const { name, args, serverSlug } = toDeepAgentTool(action);
|
|
93
|
+
|
|
94
|
+
let executionCount = 0;
|
|
95
|
+
const countingTool = tool(
|
|
96
|
+
async () => {
|
|
97
|
+
executionCount += 1;
|
|
98
|
+
return "ok";
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name,
|
|
102
|
+
description: `contract probe tool ${name}`,
|
|
103
|
+
schema: z.object({}).passthrough(),
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const toolServerMap = serverSlug ? new Map([[name, serverSlug]]) : new Map<string, string>();
|
|
108
|
+
const gate = createApprovalGateMiddleware({
|
|
109
|
+
policies: new Map(),
|
|
110
|
+
toolServerMap,
|
|
111
|
+
leasedCategories,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const agent = await createDeepAgent({
|
|
115
|
+
model: new ScriptedModel(() => ({
|
|
116
|
+
toolCalls: [{ name, args, id: "call_1" }],
|
|
117
|
+
done: "done",
|
|
118
|
+
})),
|
|
119
|
+
checkpointer: new MemorySaver() as never,
|
|
120
|
+
backend: new StateBackend(),
|
|
121
|
+
tools: [countingTool],
|
|
122
|
+
middleware: [gate],
|
|
123
|
+
} as unknown as Parameters<typeof createDeepAgent>[0]);
|
|
124
|
+
|
|
125
|
+
const config = { configurable: { thread_id: `contract-${threadSeq++}` }, recursionLimit: 50 };
|
|
126
|
+
|
|
127
|
+
// Run to the first pause. A gated tool interrupts before its handler runs.
|
|
128
|
+
await agent.invoke({ messages: [new HumanMessage({ content: "go" })] }, config);
|
|
129
|
+
const pending = readPendingInterrupts((await agent.getState(config)) as never);
|
|
130
|
+
const gated = pending.length > 0;
|
|
131
|
+
// The gate stamps the authorization provenance on every interrupt it raises;
|
|
132
|
+
// surface it so the contract can assert no side effect is gated without one.
|
|
133
|
+
const policySource = gated ? pending[0].policySource : "";
|
|
134
|
+
|
|
135
|
+
// `none` is the "no decision yet" probe: leave the action withheld.
|
|
136
|
+
if (gated && decision !== "none") {
|
|
137
|
+
const resume: Record<string, { action: string }> = {};
|
|
138
|
+
for (const p of pending) resume[p.interruptId] = { action: toResumeAction(decision) };
|
|
139
|
+
await agent.invoke(new Command({ resume }), config);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { executed: executionCount > 0, gated, executionCount, policySource };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const NO_LEASED_CATEGORIES: ReadonlySet<ToolApprovalCategory> = new Set();
|
|
146
|
+
|
|
147
|
+
export function createDeepAgentSubstrate(): GatewaySubstrate {
|
|
148
|
+
return {
|
|
149
|
+
name: "deep-agent",
|
|
150
|
+
available: true,
|
|
151
|
+
capabilities: {
|
|
152
|
+
observesExecution: true,
|
|
153
|
+
enforcesExactResource: false,
|
|
154
|
+
// The deep-agent gate re-checks every distinct call and relies on
|
|
155
|
+
// checkpoint replay for sameness; content isolation is not a property of
|
|
156
|
+
// the gate itself, so the content-exact invariant does not apply here.
|
|
157
|
+
enforcesExactContent: false,
|
|
158
|
+
appliesRunLifetimeLease: true,
|
|
159
|
+
surfacesGatePolicySource: true,
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
async authorize(action: ProposedAction, decision: GatewayDecision): Promise<GatewayOutcome> {
|
|
163
|
+
return runProbe(action, decision, NO_LEASED_CATEGORIES);
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
async authorizeUnderClassLease(
|
|
167
|
+
leased: ProposedAction,
|
|
168
|
+
probe: ProposedAction,
|
|
169
|
+
): Promise<GatewayOutcome> {
|
|
170
|
+
// Reduce the leased action to its class exactly as the runner does, then
|
|
171
|
+
// arm the gate with that category lease and run the probe with no fresh
|
|
172
|
+
// decision — so only the lease can clear it.
|
|
173
|
+
const leasedTool = toDeepAgentTool(leased);
|
|
174
|
+
const scope = deriveLeaseScope(leasedTool.name, leasedTool.serverSlug);
|
|
175
|
+
const leasedCategories =
|
|
176
|
+
scope?.kind === "category" ? new Set([scope.category]) : NO_LEASED_CATEGORIES;
|
|
177
|
+
return runProbe(probe, "none", leasedCategories);
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scripted deep-agent test harness: a deterministic chat model plus the
|
|
3
|
+
* parent-checkpoint interrupt reader.
|
|
4
|
+
*
|
|
5
|
+
* These two helpers are what every "drive a real deepagents/LangGraph graph
|
|
6
|
+
* through the approval gate" test needs, and they were previously copy-pasted
|
|
7
|
+
* inline. They are shared by:
|
|
8
|
+
* - `__tests__/subagent-approval-propagation.test.ts` (sub-agent interrupt
|
|
9
|
+
* propagation against the real runtime), and
|
|
10
|
+
* - `__test-utils__/gateway-substrate.ts` (the deep-agent adapter for the
|
|
11
|
+
* gateway Contract Test Kit).
|
|
12
|
+
*
|
|
13
|
+
* Keeping one copy means a change to how the runner reads pending interrupts (or
|
|
14
|
+
* how a scripted run is driven) updates every consumer at once.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { BaseChatModel } from "@langchain/core/language_models/chat_models";
|
|
18
|
+
import { AIMessage, ToolMessage, type BaseMessage } from "@langchain/core/messages";
|
|
19
|
+
import type { ChatResult } from "@langchain/core/outputs";
|
|
20
|
+
|
|
21
|
+
/** A single tool call the scripted model proposes on a non-terminal turn. */
|
|
22
|
+
export interface ScriptedToolCall {
|
|
23
|
+
name: string;
|
|
24
|
+
args: Record<string, unknown>;
|
|
25
|
+
id: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* What the scripted model does for a given role: propose `toolCalls` while no
|
|
30
|
+
* tool result is pending, then emit the `done` text once a tool result returns.
|
|
31
|
+
*/
|
|
32
|
+
export interface ScriptStep {
|
|
33
|
+
toolCalls: ScriptedToolCall[];
|
|
34
|
+
done: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Picks the {@link ScriptStep} for the currently-bound tool set. A single-agent
|
|
39
|
+
* test ignores the argument; a parent/sub-agent test keys the role on a tool
|
|
40
|
+
* unique to one role (e.g. the sub-agent's gated tool), because `createDeepAgent`
|
|
41
|
+
* injects the `task` tool into both the parent and its sub-agents.
|
|
42
|
+
*/
|
|
43
|
+
export type ScriptSelector = (boundToolNames: string[]) => ScriptStep;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A deterministic two-turn chat model: on the first turn it proposes the
|
|
47
|
+
* selected role's tool calls; once a tool result comes back it returns the
|
|
48
|
+
* role's terminating text. No LLM, no network — the role is keyed on the bound
|
|
49
|
+
* tool names so the same model instance drives a parent and its sub-agents.
|
|
50
|
+
*/
|
|
51
|
+
export class ScriptedModel extends BaseChatModel {
|
|
52
|
+
toolNames: string[] = [];
|
|
53
|
+
private readonly select: ScriptSelector;
|
|
54
|
+
|
|
55
|
+
constructor(select: ScriptSelector) {
|
|
56
|
+
super({});
|
|
57
|
+
this.select = select;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_llmType(): string {
|
|
61
|
+
return "scripted";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
bindTools(tools: unknown[]): this {
|
|
65
|
+
const next = new ScriptedModel(this.select);
|
|
66
|
+
next.toolNames = (tools as Array<{ name?: string }>).map((t) => t?.name ?? "");
|
|
67
|
+
return next as unknown as this;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async _generate(messages: BaseMessage[]): Promise<ChatResult> {
|
|
71
|
+
const step = this.select(this.toolNames);
|
|
72
|
+
const lastIsToolResult = messages[messages.length - 1] instanceof ToolMessage;
|
|
73
|
+
|
|
74
|
+
const message = lastIsToolResult
|
|
75
|
+
? new AIMessage({ content: step.done })
|
|
76
|
+
: new AIMessage({
|
|
77
|
+
content: "",
|
|
78
|
+
tool_calls: step.toolCalls.map((tc) => ({ ...tc, type: "tool_call" as const })),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const text = typeof message.content === "string" ? message.content : "";
|
|
82
|
+
return { generations: [{ message, text }] };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A pending approval interrupt surfaced at the parent checkpoint. */
|
|
87
|
+
export interface PendingInterrupt {
|
|
88
|
+
taskId: string;
|
|
89
|
+
interruptId: string;
|
|
90
|
+
toolCallId: string;
|
|
91
|
+
toolName: string;
|
|
92
|
+
message: string;
|
|
93
|
+
/**
|
|
94
|
+
* Authorization provenance the gate attached to the interrupt
|
|
95
|
+
* (approval-gate.ts `policy_source`) — the PolicySource union string, or "" for
|
|
96
|
+
* legacy. Lets the gateway contract assert every gated side effect is
|
|
97
|
+
* provenance-tagged.
|
|
98
|
+
*/
|
|
99
|
+
policySource: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Read the pending approval interrupts from a graph state, exactly as the
|
|
104
|
+
* production resume path does (`hitl.ts` / `index.ts`): scan the parent's
|
|
105
|
+
* top-level `tasks[].interrupts`, skipping any already carrying a resume value.
|
|
106
|
+
*
|
|
107
|
+
* Crucially `interruptId` is the interrupt's OWN id (`interrupts[].id`), not the
|
|
108
|
+
* owning `task.id` — for a nested sub-agent interrupt the two differ, and only
|
|
109
|
+
* the interrupt id routes a `Command(resume=...)` value back into the nested
|
|
110
|
+
* `interrupt()`.
|
|
111
|
+
*/
|
|
112
|
+
export function readPendingInterrupts(state: {
|
|
113
|
+
tasks?: ReadonlyArray<{
|
|
114
|
+
id: string;
|
|
115
|
+
interrupts?: ReadonlyArray<{ id?: string; value?: unknown; resumeValue?: unknown }>;
|
|
116
|
+
}>;
|
|
117
|
+
}): PendingInterrupt[] {
|
|
118
|
+
const out: PendingInterrupt[] = [];
|
|
119
|
+
for (const task of state.tasks ?? []) {
|
|
120
|
+
for (const intr of task.interrupts ?? []) {
|
|
121
|
+
if (intr.resumeValue !== undefined) continue;
|
|
122
|
+
const v = (intr.value ?? {}) as Record<string, unknown>;
|
|
123
|
+
out.push({
|
|
124
|
+
taskId: task.id,
|
|
125
|
+
interruptId: (intr.id as string) ?? task.id,
|
|
126
|
+
toolCallId: (v.tool_call_id as string) ?? "",
|
|
127
|
+
toolName: (v.tool_name as string) ?? "",
|
|
128
|
+
message: (v.message as string) ?? "",
|
|
129
|
+
policySource: (v.policy_source as string) ?? "",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
findAiMessageToolCallArgs,
|
|
4
|
+
captureApprovalArtifacts,
|
|
5
|
+
} from "../approval-file-change.js";
|
|
6
|
+
|
|
7
|
+
/** An AI message shaped like LangChain's, carrying tool calls with args. */
|
|
8
|
+
function aiMessage(toolCalls: Array<{ id: string; name: string; args: unknown }>) {
|
|
9
|
+
return { _getType: () => "ai", tool_calls: toolCalls };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe("findAiMessageToolCallArgs", () => {
|
|
13
|
+
it("returns the args of the tool call matching the id", () => {
|
|
14
|
+
const messages = [
|
|
15
|
+
{ _getType: () => "human", content: "do it" },
|
|
16
|
+
aiMessage([
|
|
17
|
+
{ id: "call-a", name: "write_file", args: { file_path: "a.txt", content: "A" } },
|
|
18
|
+
{ id: "call-b", name: "edit_file", args: { file_path: "b.txt", old_string: "x", new_string: "y" } },
|
|
19
|
+
]),
|
|
20
|
+
];
|
|
21
|
+
expect(findAiMessageToolCallArgs(messages, "call-b")).toEqual({
|
|
22
|
+
file_path: "b.txt",
|
|
23
|
+
old_string: "x",
|
|
24
|
+
new_string: "y",
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("finds the call across multiple AI messages", () => {
|
|
29
|
+
const messages = [
|
|
30
|
+
aiMessage([{ id: "call-1", name: "write_file", args: { file_path: "1", content: "" } }]),
|
|
31
|
+
aiMessage([{ id: "call-2", name: "write_file", args: { file_path: "2", content: "" } }]),
|
|
32
|
+
];
|
|
33
|
+
expect(findAiMessageToolCallArgs(messages, "call-2")).toEqual({ file_path: "2", content: "" });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("ignores ToolMessages (singular tool_call_id, no tool_calls array)", () => {
|
|
37
|
+
const messages = [
|
|
38
|
+
{ _getType: () => "tool", tool_call_id: "call-x", content: "result" },
|
|
39
|
+
];
|
|
40
|
+
expect(findAiMessageToolCallArgs(messages, "call-x")).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("returns undefined when no message emitted the id (sub-agent-nested miss)", () => {
|
|
44
|
+
const messages = [aiMessage([{ id: "other", name: "write_file", args: {} }])];
|
|
45
|
+
expect(findAiMessageToolCallArgs(messages, "call-missing")).toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("returns an empty object when the matched call carries no args", () => {
|
|
49
|
+
const messages = [aiMessage([{ id: "call-1", name: "write_file", args: undefined }])];
|
|
50
|
+
expect(findAiMessageToolCallArgs(messages, "call-1")).toEqual({});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("captureApprovalArtifacts", () => {
|
|
55
|
+
it("returns a sanitized args preview for a correlated tool call", () => {
|
|
56
|
+
const messages = [
|
|
57
|
+
aiMessage([
|
|
58
|
+
{ id: "call-1", name: "write_file", args: { file_path: "a.txt", content: "hi", token: "sk-secret" } },
|
|
59
|
+
]),
|
|
60
|
+
];
|
|
61
|
+
const { argsPreview } = captureApprovalArtifacts({ toolCallId: "call-1", messages });
|
|
62
|
+
|
|
63
|
+
expect(argsPreview).toBeDefined();
|
|
64
|
+
expect(argsPreview).toContain("a.txt");
|
|
65
|
+
expect(argsPreview).toContain("[REDACTED]");
|
|
66
|
+
expect(argsPreview).not.toContain("sk-secret");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("returns nothing when the interrupt cannot be correlated", () => {
|
|
70
|
+
const result = captureApprovalArtifacts({
|
|
71
|
+
toolCallId: "missing",
|
|
72
|
+
messages: [aiMessage([{ id: "other", name: "write_file", args: { file_path: "a", content: "" } }])],
|
|
73
|
+
});
|
|
74
|
+
expect(result).toEqual({});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("returns a preview for a correlated non-file tool", () => {
|
|
78
|
+
const messages = [
|
|
79
|
+
aiMessage([{ id: "call-1", name: "search", args: { query: "needle" } }]),
|
|
80
|
+
];
|
|
81
|
+
const { argsPreview } = captureApprovalArtifacts({ toolCallId: "call-1", messages });
|
|
82
|
+
expect(argsPreview).toContain("needle");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
MAX_ZIP_EXTRACTED_SIZE,
|
|
15
15
|
} from "../attachment-injector.js";
|
|
16
16
|
import { mockWorkspaceBackend } from "../../../__test-utils__/mock-workspace.js";
|
|
17
|
+
import { makeInMemoryArtifactStorage } from "../../../__test-utils__/fake-artifact-storage.js";
|
|
17
18
|
|
|
18
19
|
// ── ZIP Construction Helpers ─────────────────────────────────────────
|
|
19
20
|
|
|
@@ -156,15 +157,11 @@ function makeAttachment(overrides: Partial<{
|
|
|
156
157
|
} as any;
|
|
157
158
|
}
|
|
158
159
|
|
|
159
|
-
function makeMockStorage(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
exists: vi.fn().mockResolvedValue(true),
|
|
165
|
-
_content: downloadContent,
|
|
166
|
-
_url: url,
|
|
167
|
-
};
|
|
160
|
+
function makeMockStorage() {
|
|
161
|
+
// The canonical in-memory double; cloud downloads read from what was uploaded,
|
|
162
|
+
// so tests seed a blob via `storage.upload` and assert on `storage.download`.
|
|
163
|
+
const { storage } = makeInMemoryArtifactStorage();
|
|
164
|
+
return storage;
|
|
168
165
|
}
|
|
169
166
|
|
|
170
167
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -359,15 +356,8 @@ describe("injectAttachments", () => {
|
|
|
359
356
|
|
|
360
357
|
it("injects single non-ZIP attachment in cloud mode", async () => {
|
|
361
358
|
const content = Buffer.from("cloud data");
|
|
362
|
-
const storage = makeMockStorage(
|
|
363
|
-
|
|
364
|
-
const fetchMock = vi.fn().mockResolvedValue({
|
|
365
|
-
ok: true,
|
|
366
|
-
arrayBuffer: () => Promise.resolve(content.buffer.slice(
|
|
367
|
-
content.byteOffset, content.byteOffset + content.byteLength,
|
|
368
|
-
)),
|
|
369
|
-
});
|
|
370
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
359
|
+
const storage = makeMockStorage();
|
|
360
|
+
await storage.upload("attachments/xyz/data.csv", content);
|
|
371
361
|
|
|
372
362
|
const backend = mockWorkspaceBackend();
|
|
373
363
|
|
|
@@ -385,10 +375,7 @@ describe("injectAttachments", () => {
|
|
|
385
375
|
expect(result[0].filename).toBe("data.csv");
|
|
386
376
|
expect(result[0].path).toBe(".stigmer/inputs/data.csv");
|
|
387
377
|
expect(result[0].sizeBytes).toBe(content.length);
|
|
388
|
-
expect(storage.
|
|
389
|
-
expect(fetchMock).toHaveBeenCalledWith(storage._url);
|
|
390
|
-
|
|
391
|
-
vi.unstubAllGlobals();
|
|
378
|
+
expect(storage.download).toHaveBeenCalledWith("attachments/xyz/data.csv");
|
|
392
379
|
});
|
|
393
380
|
|
|
394
381
|
it("extracts ZIP when extract=true", async () => {
|
|
@@ -534,7 +521,7 @@ describe("injectAttachments", () => {
|
|
|
534
521
|
|
|
535
522
|
it("propagates error when storage download fails", async () => {
|
|
536
523
|
const storage = makeMockStorage();
|
|
537
|
-
storage.
|
|
524
|
+
storage.download.mockRejectedValue(new Error("network timeout"));
|
|
538
525
|
|
|
539
526
|
const backend = mockWorkspaceBackend();
|
|
540
527
|
|
|
@@ -602,7 +589,7 @@ describe("injectAttachments", () => {
|
|
|
602
589
|
})).rejects.toThrow(/collides with/);
|
|
603
590
|
|
|
604
591
|
// Verify no downloads were attempted
|
|
605
|
-
expect(storage.
|
|
592
|
+
expect(storage.download).not.toHaveBeenCalled();
|
|
606
593
|
});
|
|
607
594
|
|
|
608
595
|
it("derives filename from storageKey when filename is empty", async () => {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtemp, mkdir, rm, writeFile, readFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { CasCaptureFilesystemBackend } from "../cas-capture-backend.js";
|
|
6
|
+
import { CasCaptureObserver } from "../cas-capture-observer.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The backend is a thin adapter: it forwards each mutation point to the shared
|
|
10
|
+
* {@link CasCaptureObserver} (which owns all capture state and logic) and then
|
|
11
|
+
* delegates to the base `FilesystemBackend` to apply the write to disk. These
|
|
12
|
+
* tests assert exactly that delegation contract — the write lands on disk AND the
|
|
13
|
+
* observer sees the pre-turn bytes. The recording semantics themselves (first
|
|
14
|
+
* touch, concurrency, memoization) are covered in cas-capture-observer.test.ts.
|
|
15
|
+
*/
|
|
16
|
+
describe("CasCaptureFilesystemBackend (thin CAS adapter)", () => {
|
|
17
|
+
let root: string;
|
|
18
|
+
|
|
19
|
+
const isIgnored = async (relPath: string): Promise<boolean> => relPath.startsWith("ignored/");
|
|
20
|
+
|
|
21
|
+
function makeBackend(): { backend: CasCaptureFilesystemBackend; observer: CasCaptureObserver } {
|
|
22
|
+
const observer = new CasCaptureObserver({ rootDir: root, isIgnored });
|
|
23
|
+
const backend = new CasCaptureFilesystemBackend({ rootDir: root }, { observer });
|
|
24
|
+
return { backend, observer };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
root = await mkdtemp(join(tmpdir(), "cas-backend-"));
|
|
29
|
+
await mkdir(join(root, "ignored"), { recursive: true });
|
|
30
|
+
await mkdir(join(root, "src"), { recursive: true });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
await rm(root, { recursive: true, force: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("write applies to disk AND records before=null for a new gitignored file", async () => {
|
|
38
|
+
const { backend, observer } = makeBackend();
|
|
39
|
+
|
|
40
|
+
await backend.write("ignored/new.txt", "hello");
|
|
41
|
+
|
|
42
|
+
expect(await readFile(join(root, "ignored/new.txt"), "utf8")).toBe("hello");
|
|
43
|
+
expect(observer.before.get("ignored/new.txt")).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("edit applies to disk AND records the pre-turn bytes for a gitignored file", async () => {
|
|
47
|
+
await writeFile(join(root, "ignored/cfg.txt"), "v0");
|
|
48
|
+
const { backend, observer } = makeBackend();
|
|
49
|
+
|
|
50
|
+
await backend.edit("ignored/cfg.txt", "v0", "v1");
|
|
51
|
+
|
|
52
|
+
expect(await readFile(join(root, "ignored/cfg.txt"), "utf8")).toBe("v1");
|
|
53
|
+
expect(Buffer.from(observer.before.get("ignored/cfg.txt")!).toString("utf8")).toBe("v0");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("a git-tracked write applies to disk but is NOT observed (the git diff owns it)", async () => {
|
|
57
|
+
const { backend, observer } = makeBackend();
|
|
58
|
+
|
|
59
|
+
await backend.write("src/app.ts", "export const x = 1;");
|
|
60
|
+
|
|
61
|
+
expect(await readFile(join(root, "src/app.ts"), "utf8")).toBe("export const x = 1;");
|
|
62
|
+
expect(observer.before.size).toBe(0);
|
|
63
|
+
});
|
|
64
|
+
});
|