@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
|
@@ -67,6 +67,20 @@ describe("LocalArtifactStorage", () => {
|
|
|
67
67
|
});
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
+
describe("download", () => {
|
|
71
|
+
it("round-trips uploaded content byte-exact", async () => {
|
|
72
|
+
await storage.upload("exec-1/file.txt", Buffer.from("round-trip"));
|
|
73
|
+
const got = await storage.download("exec-1/file.txt");
|
|
74
|
+
expect(got.toString("utf-8")).toBe("round-trip");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("throws a key-scoped error for a missing key", async () => {
|
|
78
|
+
await expect(storage.download("missing.txt")).rejects.toThrow(
|
|
79
|
+
/Artifact not found for key 'missing\.txt'/,
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
70
84
|
describe("exists", () => {
|
|
71
85
|
it("returns true after upload", async () => {
|
|
72
86
|
await storage.upload("present.txt", Buffer.from("data"));
|
|
@@ -140,24 +154,62 @@ describe("ProxyArtifactStorage", () => {
|
|
|
140
154
|
expect(url).toBe("https://r2.example.com/download/key.txt");
|
|
141
155
|
});
|
|
142
156
|
|
|
143
|
-
it("
|
|
157
|
+
it("download resolves a presigned URL then fetches the bytes", async () => {
|
|
144
158
|
const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
|
|
145
159
|
|
|
146
|
-
globalThis.fetch = vi.fn()
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
160
|
+
globalThis.fetch = vi.fn()
|
|
161
|
+
.mockResolvedValueOnce({
|
|
162
|
+
ok: true,
|
|
163
|
+
json: async () => ({ url: "https://r2.example.com/dl/key.txt" }),
|
|
164
|
+
} as any)
|
|
165
|
+
.mockResolvedValueOnce({
|
|
166
|
+
ok: true,
|
|
167
|
+
arrayBuffer: async () => new TextEncoder().encode("proxy-bytes").buffer,
|
|
168
|
+
} as any);
|
|
169
|
+
|
|
170
|
+
const got = await storage.download("key.txt");
|
|
171
|
+
expect(got.toString("utf-8")).toBe("proxy-bytes");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("download throws with HTTP status and key on a failed fetch", async () => {
|
|
175
|
+
const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
|
|
176
|
+
|
|
177
|
+
globalThis.fetch = vi.fn()
|
|
178
|
+
.mockResolvedValueOnce({
|
|
179
|
+
ok: true,
|
|
180
|
+
json: async () => ({ url: "https://r2.example.com/dl/missing.txt" }),
|
|
181
|
+
} as any)
|
|
182
|
+
.mockResolvedValueOnce({
|
|
183
|
+
ok: false,
|
|
184
|
+
status: 404,
|
|
185
|
+
text: async () => "not found",
|
|
186
|
+
} as any);
|
|
187
|
+
|
|
188
|
+
await expect(storage.download("missing.txt")).rejects.toThrow(
|
|
189
|
+
/Artifact download failed \(HTTP 404\) for key 'missing\.txt'/,
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("exists returns true when the object is present (presign + ranged GET)", async () => {
|
|
194
|
+
const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
|
|
195
|
+
|
|
196
|
+
// Two-step probe: presign mints the URL, then a ranged GET hits the object.
|
|
197
|
+
// Presign success alone is NOT existence — the object fetch is authoritative.
|
|
198
|
+
globalThis.fetch = vi.fn(async (input: string | URL | Request) => {
|
|
199
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
200
|
+
if (url.includes("presigned-download-url")) {
|
|
201
|
+
return new Response(JSON.stringify({ url: "https://r2.example.com/dl" }), { status: 200 });
|
|
202
|
+
}
|
|
203
|
+
return new Response(Buffer.from("x"), { status: 206 });
|
|
204
|
+
}) as typeof fetch;
|
|
150
205
|
|
|
151
206
|
expect(await storage.exists("key.txt")).toBe(true);
|
|
152
207
|
});
|
|
153
208
|
|
|
154
|
-
it("exists returns false when presign fails", async () => {
|
|
209
|
+
it("exists returns false when the presign endpoint fails", async () => {
|
|
155
210
|
const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
|
|
156
211
|
|
|
157
|
-
globalThis.fetch = vi.fn()
|
|
158
|
-
ok: false,
|
|
159
|
-
status: 404,
|
|
160
|
-
} as any);
|
|
212
|
+
globalThis.fetch = vi.fn(async () => new Response("nope", { status: 404 })) as typeof fetch;
|
|
161
213
|
|
|
162
214
|
expect(await storage.exists("missing.txt")).toBe(false);
|
|
163
215
|
});
|
|
@@ -65,6 +65,38 @@ describe("LocalArtifactStorage", () => {
|
|
|
65
65
|
const written = await readFile(join(tempDir, key));
|
|
66
66
|
expect(written.toString()).toBe("v2");
|
|
67
67
|
});
|
|
68
|
+
|
|
69
|
+
it("downloads uploaded text bytes byte-exact (inverse of upload)", async () => {
|
|
70
|
+
const key = "artifacts/exec-1/report.txt";
|
|
71
|
+
await storage.upload(key, Buffer.from("hello world"));
|
|
72
|
+
const got = await storage.download(key);
|
|
73
|
+
expect(got.toString()).toBe("hello world");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("downloads binary bytes (incl. NUL) byte-exact", async () => {
|
|
77
|
+
const key = "artifacts/exec-1/blob.bin";
|
|
78
|
+
const bytes = Buffer.from([0x00, 0x01, 0xff, 0xfe, 0x00, 0x89, 0x50]);
|
|
79
|
+
await storage.upload(key, bytes);
|
|
80
|
+
const got = await storage.download(key);
|
|
81
|
+
expect(Buffer.compare(got, bytes)).toBe(0);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("throws a key-scoped error when downloading a missing key", async () => {
|
|
85
|
+
await expect(storage.download("artifacts/exec-1/missing.bin")).rejects.toThrow(
|
|
86
|
+
/Artifact not found for key 'artifacts\/exec-1\/missing\.bin'/,
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("reads directly off disk even with an unreachable serve URL (no HTTP dependency)", async () => {
|
|
91
|
+
// A bogus, unroutable serve base: if `download` fetched over HTTP this would
|
|
92
|
+
// hang/throw. Reading straight off disk proves the runner's read-back does
|
|
93
|
+
// not depend on the serve URL being set or reachable.
|
|
94
|
+
const s = new LocalArtifactStorage(tempDir, "http://127.0.0.1:0");
|
|
95
|
+
const key = "artifacts/exec-1/offline.txt";
|
|
96
|
+
await s.upload(key, Buffer.from("served-from-disk"));
|
|
97
|
+
const got = await s.download(key);
|
|
98
|
+
expect(got.toString()).toBe("served-from-disk");
|
|
99
|
+
});
|
|
68
100
|
});
|
|
69
101
|
|
|
70
102
|
// ── ProxyArtifactStorage ─────────────────────────────────────────────
|
|
@@ -126,7 +158,7 @@ describe("ProxyArtifactStorage", () => {
|
|
|
126
158
|
}
|
|
127
159
|
// Build a real Headers from what the runner passed, so concatenation /
|
|
128
160
|
// case-folding behavior matches a live PUT.
|
|
129
|
-
resolve(new Headers(init?.headers as
|
|
161
|
+
resolve(new Headers(init?.headers as ConstructorParameters<typeof Headers>[0]));
|
|
130
162
|
return new Response(null, { status: 200 });
|
|
131
163
|
}) as typeof fetch;
|
|
132
164
|
});
|
|
@@ -219,32 +251,112 @@ describe("ProxyArtifactStorage", () => {
|
|
|
219
251
|
expect(url).toBe("https://r2.example.com/dl");
|
|
220
252
|
});
|
|
221
253
|
|
|
222
|
-
it("
|
|
223
|
-
globalThis.fetch = vi.fn(async () =>
|
|
224
|
-
|
|
225
|
-
|
|
254
|
+
it("downloads by resolving a presigned URL then fetching it", async () => {
|
|
255
|
+
globalThis.fetch = vi.fn(async (input: string | URL | Request) => {
|
|
256
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
257
|
+
if (url.includes("presigned-download-url")) {
|
|
258
|
+
return new Response(JSON.stringify({ url: "https://r2.example.com/dl" }), { status: 200 });
|
|
259
|
+
}
|
|
260
|
+
if (url === "https://r2.example.com/dl") {
|
|
261
|
+
return new Response(Buffer.from("payload-bytes"), { status: 200 });
|
|
262
|
+
}
|
|
263
|
+
return new Response("not found", { status: 404 });
|
|
264
|
+
}) as typeof fetch;
|
|
265
|
+
|
|
266
|
+
const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
|
|
267
|
+
const got = await storage.download("artifacts/exec-1/f.txt");
|
|
268
|
+
expect(got.toString()).toBe("payload-bytes");
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("throws with the HTTP status and key when the download fetch fails", async () => {
|
|
272
|
+
globalThis.fetch = vi.fn(async (input: string | URL | Request) => {
|
|
273
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
274
|
+
if (url.includes("presigned-download-url")) {
|
|
275
|
+
return new Response(JSON.stringify({ url: "https://r2.example.com/dl" }), { status: 200 });
|
|
276
|
+
}
|
|
277
|
+
return new Response("gone", { status: 404 });
|
|
278
|
+
}) as typeof fetch;
|
|
279
|
+
|
|
280
|
+
const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
|
|
281
|
+
await expect(storage.download("artifacts/exec-1/missing.txt")).rejects.toThrow(
|
|
282
|
+
/Artifact download failed \(HTTP 404\) for key 'artifacts\/exec-1\/missing\.txt'/,
|
|
283
|
+
);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// exists() is a TWO-step probe: presign the download URL, then a 1-byte ranged
|
|
287
|
+
// GET against the object. The presign endpoint mints a URL for ANY key, so the
|
|
288
|
+
// OBJECT fetch — not the presign — is the source of truth for existence.
|
|
289
|
+
function mockProxyFetch(objectResponse: () => Response) {
|
|
290
|
+
globalThis.fetch = vi.fn(async (input: string | URL | Request) => {
|
|
291
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
292
|
+
if (url.includes("presigned-download-url")) {
|
|
293
|
+
return new Response(JSON.stringify({ url: "https://r2.example.com/obj" }), { status: 200 });
|
|
294
|
+
}
|
|
295
|
+
return objectResponse();
|
|
296
|
+
}) as typeof fetch;
|
|
297
|
+
}
|
|
226
298
|
|
|
299
|
+
it("exists returns true when the object GET is 200", async () => {
|
|
300
|
+
mockProxyFetch(() => new Response(Buffer.from("x"), { status: 200 }));
|
|
227
301
|
const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
|
|
228
302
|
expect(await storage.exists("key")).toBe(true);
|
|
229
303
|
});
|
|
230
304
|
|
|
231
|
-
it("exists returns
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
)
|
|
305
|
+
it("exists returns true when the object GET is 206 (ranged)", async () => {
|
|
306
|
+
mockProxyFetch(() => new Response(Buffer.from("x"), { status: 206 }));
|
|
307
|
+
const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
|
|
308
|
+
expect(await storage.exists("key")).toBe(true);
|
|
309
|
+
});
|
|
235
310
|
|
|
311
|
+
it("exists returns true on 416 (0-byte object, range unsatisfiable but present)", async () => {
|
|
312
|
+
mockProxyFetch(() => new Response("", { status: 416 }));
|
|
313
|
+
const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
|
|
314
|
+
expect(await storage.exists("key")).toBe(true);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("exists returns false when presign succeeds but the object 404s (regression: presign != existence)", async () => {
|
|
318
|
+
// The exact proxy bug: the presign endpoint mints a URL for a nonexistent key;
|
|
319
|
+
// only the object fetch reveals it is absent. The old exists() returned true
|
|
320
|
+
// here (presign.ok), which crashed the file-review reconcile on the R2 404.
|
|
321
|
+
mockProxyFetch(() => new Response("<Error><Code>NoSuchKey</Code></Error>", { status: 404 }));
|
|
236
322
|
const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
|
|
237
323
|
expect(await storage.exists("key")).toBe(false);
|
|
238
324
|
});
|
|
239
325
|
|
|
240
|
-
it("exists
|
|
326
|
+
it("exists probes the object with a 1-byte ranged GET (never a HEAD, which breaks the presign signature)", async () => {
|
|
327
|
+
const seen: Array<{ url: string; range: string | null; method: string }> = [];
|
|
328
|
+
globalThis.fetch = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
|
|
329
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
330
|
+
if (url.includes("presigned-download-url")) {
|
|
331
|
+
return new Response(JSON.stringify({ url: "https://r2.example.com/obj" }), { status: 200 });
|
|
332
|
+
}
|
|
333
|
+
seen.push({
|
|
334
|
+
url,
|
|
335
|
+
range: new Headers(init?.headers).get("range"),
|
|
336
|
+
method: (init?.method ?? "GET").toUpperCase(),
|
|
337
|
+
});
|
|
338
|
+
return new Response(Buffer.from("x"), { status: 206 });
|
|
339
|
+
}) as typeof fetch;
|
|
340
|
+
const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
|
|
341
|
+
await storage.exists("key");
|
|
342
|
+
expect(seen).toEqual([{ url: "https://r2.example.com/obj", range: "bytes=0-0", method: "GET" }]);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("exists returns false when the presign endpoint is unreachable", async () => {
|
|
241
346
|
globalThis.fetch = vi.fn(async () => {
|
|
242
347
|
throw new Error("network unreachable");
|
|
243
348
|
}) as typeof fetch;
|
|
244
|
-
|
|
245
349
|
const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
|
|
246
350
|
expect(await storage.exists("key")).toBe(false);
|
|
247
351
|
});
|
|
352
|
+
|
|
353
|
+
it("exists throws on an unexpected object status (a fault, not an existence answer)", async () => {
|
|
354
|
+
mockProxyFetch(() => new Response("boom", { status: 500 }));
|
|
355
|
+
const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
|
|
356
|
+
await expect(storage.exists("key")).rejects.toThrow(
|
|
357
|
+
/Artifact existence check failed \(HTTP 500\) for key 'key'/,
|
|
358
|
+
);
|
|
359
|
+
});
|
|
248
360
|
});
|
|
249
361
|
|
|
250
362
|
// ── Factory ──────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
looksBinary,
|
|
4
|
+
bytesLookBinary,
|
|
5
|
+
resolveWorkspacePath,
|
|
6
|
+
} from "../file-change.js";
|
|
7
|
+
|
|
8
|
+
describe("looksBinary", () => {
|
|
9
|
+
it("flags content containing a NUL byte", () => {
|
|
10
|
+
expect(looksBinary("PNG\u0000\u0000IHDR")).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("does not flag plain UTF-8 text, including unicode and empty", () => {
|
|
14
|
+
expect(looksBinary("hello world")).toBe(false);
|
|
15
|
+
expect(looksBinary("héllo — 世界 \n\t")).toBe(false);
|
|
16
|
+
expect(looksBinary("")).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("bytesLookBinary", () => {
|
|
21
|
+
it("flags raw bytes containing a NUL", () => {
|
|
22
|
+
expect(bytesLookBinary(Buffer.from([0x89, 0x50, 0x00, 0x4e]))).toBe(true);
|
|
23
|
+
expect(bytesLookBinary(new Uint8Array([0x00]))).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("does not flag NUL-free bytes (text, or high bytes without a NUL)", () => {
|
|
27
|
+
expect(bytesLookBinary(Buffer.from("hello world", "utf8"))).toBe(false);
|
|
28
|
+
expect(bytesLookBinary(Buffer.from("héllo — 世界", "utf8"))).toBe(false);
|
|
29
|
+
expect(bytesLookBinary(Buffer.from([0xff, 0xfe, 0x80]))).toBe(false);
|
|
30
|
+
expect(bytesLookBinary(Buffer.alloc(0))).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("resolveWorkspacePath", () => {
|
|
35
|
+
const root = "/work/space";
|
|
36
|
+
|
|
37
|
+
describe("native (virtualRoot=true)", () => {
|
|
38
|
+
it("strips a leading slash and joins under the root", () => {
|
|
39
|
+
// "/src/app.ts" is the deepagents virtual-root convention; the display
|
|
40
|
+
// path drops the leading slash and the absolute path lives under root.
|
|
41
|
+
expect(resolveWorkspacePath("/src/app.ts", root, true)).toEqual({
|
|
42
|
+
path: "src/app.ts",
|
|
43
|
+
absolutePath: "/work/space/src/app.ts",
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("passes a bare relative path through and joins it", () => {
|
|
48
|
+
expect(resolveWorkspacePath("src/app.ts", root, true)).toEqual({
|
|
49
|
+
path: "src/app.ts",
|
|
50
|
+
absolutePath: "/work/space/src/app.ts",
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("normalizes a leading ./ segment", () => {
|
|
55
|
+
expect(resolveWorkspacePath("./README.md", root, true)).toEqual({
|
|
56
|
+
path: "README.md",
|
|
57
|
+
absolutePath: "/work/space/README.md",
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("cursor (virtualRoot=false)", () => {
|
|
63
|
+
it("makes an absolute path under the root relative for display", () => {
|
|
64
|
+
expect(resolveWorkspacePath("/work/space/src/app.ts", root, false)).toEqual({
|
|
65
|
+
path: "src/app.ts",
|
|
66
|
+
absolutePath: "/work/space/src/app.ts",
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("displays an absolute path outside the root as-is (never an escaping ../)", () => {
|
|
71
|
+
const outside = "/etc/hosts";
|
|
72
|
+
expect(resolveWorkspacePath(outside, root, false)).toEqual({
|
|
73
|
+
path: outside,
|
|
74
|
+
absolutePath: outside,
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("treats a relative path the same as the virtual case", () => {
|
|
79
|
+
expect(resolveWorkspacePath("src/app.ts", root, false)).toEqual({
|
|
80
|
+
path: "src/app.ts",
|
|
81
|
+
absolutePath: "/work/space/src/app.ts",
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the unified file-modifying-tool introspection.
|
|
3
|
+
*
|
|
4
|
+
* This module is the single source of truth both harnesses' gate capture now
|
|
5
|
+
* shares, so these pin the cross-taxonomy field coverage AND the load-bearing
|
|
6
|
+
* guardrail: whole-file content (`content`/`contents`/`file_content`) must stay
|
|
7
|
+
* strictly separate from an edit's replacement (`new_string`/…). Conflating them
|
|
8
|
+
* would make an edit look like a whole-file capture and read the wrong `before`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from "vitest";
|
|
12
|
+
import {
|
|
13
|
+
isFileModifyingTool,
|
|
14
|
+
extractFilePath,
|
|
15
|
+
extractWriteContent,
|
|
16
|
+
extractEditOldString,
|
|
17
|
+
extractEditNewString,
|
|
18
|
+
} from "../file-tools.js";
|
|
19
|
+
|
|
20
|
+
describe("isFileModifyingTool", () => {
|
|
21
|
+
it("recognizes the native (deepagents) taxonomy", () => {
|
|
22
|
+
for (const name of ["write_file", "edit_file", "create_file", "write", "edit", "create", "str_replace_editor"]) {
|
|
23
|
+
expect(isFileModifyingTool(name), name).toBe(true);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("recognizes the Cursor taxonomy (hook + SDK names)", () => {
|
|
28
|
+
for (const name of ["Write", "StrReplace", "EditNotebook"]) {
|
|
29
|
+
expect(isFileModifyingTool(name), name).toBe(true);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("excludes read-only, delete, and unknown tools (no content/hunk to preview)", () => {
|
|
34
|
+
for (const name of ["read", "Read", "delete", "Delete", "shell", "Shell", "grep", "mystery"]) {
|
|
35
|
+
expect(isFileModifyingTool(name), name).toBe(false);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("extractFilePath", () => {
|
|
41
|
+
it("spans both taxonomies' path fields, including notebook target", () => {
|
|
42
|
+
expect(extractFilePath({ path: "a.ts" })).toBe("a.ts");
|
|
43
|
+
expect(extractFilePath({ file_path: "b.ts" })).toBe("b.ts");
|
|
44
|
+
expect(extractFilePath({ target_notebook: "nb.ipynb" })).toBe("nb.ipynb");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("returns null when no recognized path key is present", () => {
|
|
48
|
+
expect(extractFilePath({ content: "no path here" })).toBeNull();
|
|
49
|
+
expect(extractFilePath({})).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("extractWriteContent (whole-file body)", () => {
|
|
54
|
+
it("reads the whole-file content fields and preserves an empty file", () => {
|
|
55
|
+
expect(extractWriteContent({ content: "x" })).toBe("x");
|
|
56
|
+
expect(extractWriteContent({ contents: "y" })).toBe("y");
|
|
57
|
+
expect(extractWriteContent({ file_content: "z" })).toBe("z");
|
|
58
|
+
expect(extractWriteContent({ content: "" })).toBe(""); // empty file is real
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("GUARDRAIL: never treats an edit's new_string as whole-file content", () => {
|
|
62
|
+
// An edit fragment must NOT be picked up here, or the gate would read the
|
|
63
|
+
// wrong before and mislabel an edit as a whole-file rewrite.
|
|
64
|
+
expect(extractWriteContent({ new_string: "frag" })).toBeNull();
|
|
65
|
+
expect(extractWriteContent({ old_string: "a", new_string: "b" })).toBeNull();
|
|
66
|
+
expect(extractWriteContent({ replacement: "r" })).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("extractEditOldString / extractEditNewString (edit fragments)", () => {
|
|
71
|
+
it("reads the old/new replacement fields across variants", () => {
|
|
72
|
+
expect(extractEditOldString({ old_string: "a" })).toBe("a");
|
|
73
|
+
expect(extractEditOldString({ old_text: "a" })).toBe("a");
|
|
74
|
+
expect(extractEditOldString({ oldText: "a" })).toBe("a");
|
|
75
|
+
expect(extractEditNewString({ new_string: "b" })).toBe("b");
|
|
76
|
+
expect(extractEditNewString({ new_text: "b" })).toBe("b");
|
|
77
|
+
expect(extractEditNewString({ newText: "b" })).toBe("b");
|
|
78
|
+
expect(extractEditNewString({ replacement: "b" })).toBe("b");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("preserves an empty string (insertion / deletion fragments)", () => {
|
|
82
|
+
expect(extractEditOldString({ old_string: "" })).toBe(""); // insertion
|
|
83
|
+
expect(extractEditNewString({ new_string: "" })).toBe(""); // deletion
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("returns null when the fragment is absent", () => {
|
|
87
|
+
expect(extractEditOldString({ content: "whole" })).toBeNull();
|
|
88
|
+
expect(extractEditNewString({ content: "whole" })).toBeNull();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const ENV_VAR = "STIGMER_RUNNER_HITL_SECRET";
|
|
4
|
+
|
|
5
|
+
// The module memoizes its secret, so each test re-imports a fresh module copy via
|
|
6
|
+
// vi.resetModules() to exercise the source-precedence + fallback paths cleanly.
|
|
7
|
+
async function freshSecretModule() {
|
|
8
|
+
vi.resetModules();
|
|
9
|
+
return import("../fingerprint-secret.js");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe("getRunnerHitlMasterSecret", () => {
|
|
13
|
+
const original = process.env[ENV_VAR];
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
delete process.env[ENV_VAR];
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
if (original === undefined) delete process.env[ENV_VAR];
|
|
21
|
+
else process.env[ENV_VAR] = original;
|
|
22
|
+
vi.restoreAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("uses the env var (UTF-8) when set", async () => {
|
|
26
|
+
process.env[ENV_VAR] = "my-stable-secret";
|
|
27
|
+
const { getRunnerHitlMasterSecret } = await freshSecretModule();
|
|
28
|
+
expect(Buffer.from(getRunnerHitlMasterSecret() as Buffer).toString("utf-8")).toBe("my-stable-secret");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("falls back to a per-process random secret and warns once when unset", async () => {
|
|
32
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
33
|
+
const { getRunnerHitlMasterSecret } = await freshSecretModule();
|
|
34
|
+
|
|
35
|
+
const a = Buffer.from(getRunnerHitlMasterSecret() as Buffer);
|
|
36
|
+
const b = Buffer.from(getRunnerHitlMasterSecret() as Buffer);
|
|
37
|
+
|
|
38
|
+
expect(a.length).toBe(32);
|
|
39
|
+
expect(a.equals(b)).toBe(true); // memoized: stable within the process
|
|
40
|
+
expect(warn).toHaveBeenCalledTimes(1); // warned once, not per call
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("two separate processes (module loads) get different random fallbacks", async () => {
|
|
44
|
+
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
45
|
+
const m1 = await freshSecretModule();
|
|
46
|
+
const s1 = Buffer.from(m1.getRunnerHitlMasterSecret() as Buffer);
|
|
47
|
+
const m2 = await freshSecretModule();
|
|
48
|
+
const s2 = Buffer.from(m2.getRunnerHitlMasterSecret() as Buffer);
|
|
49
|
+
expect(s1.equals(s2)).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-edition lease-scope derivation parity.
|
|
3
|
+
*
|
|
4
|
+
* Loads the shared corpus (apis/testdata/hitl/lease-scope/vectors.json) and
|
|
5
|
+
* asserts the TS {@link deriveLeaseScope} agrees with it. The Go
|
|
6
|
+
* (lease_scope_corpus_test.go) and Java (LeaseScopeFixtureTest) editions load
|
|
7
|
+
* the same file, so a drift in any edition fails one of the three suites — the
|
|
8
|
+
* guarantee that an APPROVE_ALL clicked in one edition leases the same class
|
|
9
|
+
* everywhere.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect } from "vitest";
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { deriveLeaseScope } from "../approval-policy.js";
|
|
16
|
+
|
|
17
|
+
interface LeaseScopeVector {
|
|
18
|
+
name: string;
|
|
19
|
+
input: { toolName: string; mcpServerSlug?: string };
|
|
20
|
+
expected: { category: string } | { server: string } | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const vectorsPath = fileURLToPath(
|
|
24
|
+
new URL(
|
|
25
|
+
"../../../../../../apis/testdata/hitl/lease-scope/vectors.json",
|
|
26
|
+
import.meta.url,
|
|
27
|
+
),
|
|
28
|
+
);
|
|
29
|
+
const corpus = JSON.parse(readFileSync(vectorsPath, "utf-8")) as {
|
|
30
|
+
vectors: LeaseScopeVector[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Normalize the discriminated union to the corpus's plain JSON shape. */
|
|
34
|
+
function asExpected(
|
|
35
|
+
scope: ReturnType<typeof deriveLeaseScope>,
|
|
36
|
+
): { category: string } | { server: string } | null {
|
|
37
|
+
if (!scope) return null;
|
|
38
|
+
return scope.kind === "server"
|
|
39
|
+
? { server: scope.server }
|
|
40
|
+
: { category: scope.category };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("lease-scope derivation vector corpus", () => {
|
|
44
|
+
it("loads a non-trivial corpus", () => {
|
|
45
|
+
expect(corpus.vectors.length).toBeGreaterThanOrEqual(10);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
for (const v of corpus.vectors) {
|
|
49
|
+
it(`vector: ${v.name}`, () => {
|
|
50
|
+
const actual = asExpected(
|
|
51
|
+
deriveLeaseScope(v.input.toolName, v.input.mcpServerSlug ?? ""),
|
|
52
|
+
);
|
|
53
|
+
expect(actual).toEqual(v.expected);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|