@stigmer/runner 3.0.9-dev.20260615153829 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.build-fingerprint +1 -1
- package/dist/__test-utils__/approval-contract/types.d.ts +174 -0
- package/dist/__test-utils__/approval-contract/types.js +24 -0
- package/dist/__test-utils__/approval-contract/types.js.map +1 -0
- package/dist/activities/call-agent-status.d.ts +19 -1
- package/dist/activities/call-agent-status.js +64 -5
- package/dist/activities/call-agent-status.js.map +1 -1
- package/dist/activities/call-llm.js +19 -53
- package/dist/activities/call-llm.js.map +1 -1
- package/dist/activities/classify-tool-approvals.d.ts +24 -0
- package/dist/activities/classify-tool-approvals.js +69 -17
- package/dist/activities/classify-tool-approvals.js.map +1 -1
- package/dist/activities/discover-mcp-server.d.ts +7 -0
- package/dist/activities/discover-mcp-server.js +11 -1
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +140 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +130 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -0
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.d.ts +18 -0
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js +123 -0
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js.map +1 -0
- package/dist/activities/execute-cursor/approval-policy.d.ts +17 -61
- package/dist/activities/execute-cursor/approval-policy.js +8 -120
- package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
- package/dist/activities/execute-cursor/approval-state.d.ts +219 -9
- package/dist/activities/execute-cursor/approval-state.js +233 -17
- package/dist/activities/execute-cursor/approval-state.js.map +1 -1
- package/dist/activities/execute-cursor/capture-flow.d.ts +127 -0
- package/dist/activities/execute-cursor/capture-flow.js +234 -0
- package/dist/activities/execute-cursor/capture-flow.js.map +1 -0
- package/dist/activities/execute-cursor/cas-observations.d.ts +96 -0
- package/dist/activities/execute-cursor/cas-observations.js +184 -0
- package/dist/activities/execute-cursor/cas-observations.js.map +1 -0
- package/dist/activities/execute-cursor/command-provenance.d.ts +62 -0
- package/dist/activities/execute-cursor/command-provenance.js +128 -0
- package/dist/activities/execute-cursor/command-provenance.js.map +1 -0
- package/dist/activities/execute-cursor/exact-apply.d.ts +110 -0
- package/dist/activities/execute-cursor/exact-apply.js +204 -0
- package/dist/activities/execute-cursor/exact-apply.js.map +1 -0
- package/dist/activities/execute-cursor/hook-script.d.ts +53 -24
- package/dist/activities/execute-cursor/hook-script.js +310 -47
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +8 -1
- package/dist/activities/execute-cursor/index.js +500 -64
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/message-translator.d.ts +258 -11
- package/dist/activities/execute-cursor/message-translator.js +836 -90
- package/dist/activities/execute-cursor/message-translator.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +20 -23
- package/dist/activities/execute-cursor/prompt-builder.js +74 -15
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/workspace-setup.d.ts +17 -0
- package/dist/activities/execute-cursor/workspace-setup.js +212 -33
- package/dist/activities/execute-cursor/workspace-setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.d.ts +19 -0
- package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js +143 -0
- package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js.map +1 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.d.ts +88 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js +81 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js.map +1 -0
- package/dist/activities/execute-deep-agent/approval-file-change.d.ts +47 -0
- package/dist/activities/execute-deep-agent/approval-file-change.js +68 -0
- package/dist/activities/execute-deep-agent/approval-file-change.js.map +1 -0
- package/dist/activities/execute-deep-agent/attachment-injector.d.ts +8 -1
- package/dist/activities/execute-deep-agent/attachment-injector.js +7 -7
- package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
- package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +42 -0
- package/dist/activities/execute-deep-agent/cas-capture-backend.js +47 -0
- package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.d.ts +79 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.js +112 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.js.map +1 -0
- package/dist/activities/execute-deep-agent/hitl.d.ts +16 -3
- package/dist/activities/execute-deep-agent/hitl.js +11 -3
- package/dist/activities/execute-deep-agent/hitl.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.d.ts +2 -1
- package/dist/activities/execute-deep-agent/index.js +393 -33
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/inline-publisher.d.ts +7 -1
- package/dist/activities/execute-deep-agent/inline-publisher.js +23 -2
- package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.d.ts +53 -2
- package/dist/activities/execute-deep-agent/setup.js +149 -92
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.d.ts +36 -0
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.js +56 -0
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.js.map +1 -0
- package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +34 -1
- package/dist/activities/execute-deep-agent/status-builder-shared.js +26 -25
- package/dist/activities/execute-deep-agent/status-builder-shared.js.map +1 -1
- package/dist/activities/execute-deep-agent/status-builder.d.ts +17 -5
- package/dist/activities/execute-deep-agent/status-builder.js +44 -3
- package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming-side-effects.js +2 -19
- package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming.js +3 -15
- package/dist/activities/execute-deep-agent/streaming.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +25 -7
- package/dist/activities/execute-deep-agent/subagent-transformer.js +23 -7
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +30 -3
- package/dist/activities/execute-deep-agent/subagent-wiring.js +29 -3
- package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.js +32 -2
- package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
- package/dist/claimcheck/payload-codec.js +9 -5
- package/dist/claimcheck/payload-codec.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +2 -0
- package/dist/client/stigmer-client.js +2 -0
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/middleware/approval-gate.d.ts +85 -4
- package/dist/middleware/approval-gate.js +165 -38
- package/dist/middleware/approval-gate.js.map +1 -1
- package/dist/middleware/types.d.ts +2 -5
- package/dist/shared/activity-input.d.ts +43 -0
- package/dist/shared/activity-input.js +17 -0
- package/dist/shared/activity-input.js.map +1 -0
- package/dist/shared/approval-canonicalize.d.ts +19 -0
- package/dist/shared/approval-canonicalize.js +119 -0
- package/dist/shared/approval-canonicalize.js.map +1 -0
- package/dist/shared/approval-fingerprint.d.ts +106 -0
- package/dist/shared/approval-fingerprint.js +113 -0
- package/dist/shared/approval-fingerprint.js.map +1 -0
- package/dist/shared/approval-policy.d.ts +182 -12
- package/dist/shared/approval-policy.js +213 -27
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/shared/args-preview.d.ts +52 -0
- package/dist/shared/args-preview.js +93 -0
- package/dist/shared/args-preview.js.map +1 -0
- package/dist/shared/artifact-storage.d.ts +19 -1
- package/dist/shared/artifact-storage.js +48 -11
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/file-change.d.ts +44 -0
- package/dist/shared/file-change.js +57 -0
- package/dist/shared/file-change.js.map +1 -0
- package/dist/shared/file-tools.d.ts +107 -0
- package/dist/shared/file-tools.js +168 -0
- package/dist/shared/file-tools.js.map +1 -0
- package/dist/shared/filereview/capture.d.ts +202 -0
- package/dist/shared/filereview/capture.js +498 -0
- package/dist/shared/filereview/capture.js.map +1 -0
- package/dist/shared/filereview/cas-substrate.d.ts +190 -0
- package/dist/shared/filereview/cas-substrate.js +284 -0
- package/dist/shared/filereview/cas-substrate.js.map +1 -0
- package/dist/shared/filereview/digest.d.ts +40 -0
- package/dist/shared/filereview/digest.js +66 -0
- package/dist/shared/filereview/digest.js.map +1 -0
- package/dist/shared/filereview/events.d.ts +170 -0
- package/dist/shared/filereview/events.js +298 -0
- package/dist/shared/filereview/events.js.map +1 -0
- package/dist/shared/filereview/git-substrate.d.ts +175 -0
- package/dist/shared/filereview/git-substrate.js +439 -0
- package/dist/shared/filereview/git-substrate.js.map +1 -0
- package/dist/shared/filereview/index.d.ts +11 -0
- package/dist/shared/filereview/index.js +12 -0
- package/dist/shared/filereview/index.js.map +1 -0
- package/dist/shared/filereview/secret-paths.d.ts +63 -0
- package/dist/shared/filereview/secret-paths.js +105 -0
- package/dist/shared/filereview/secret-paths.js.map +1 -0
- package/dist/shared/fingerprint-secret.d.ts +26 -0
- package/dist/shared/fingerprint-secret.js +47 -0
- package/dist/shared/fingerprint-secret.js.map +1 -0
- package/dist/shared/model-client.d.ts +51 -0
- package/dist/shared/model-client.js +77 -0
- package/dist/shared/model-client.js.map +1 -0
- package/dist/shared/plan-artifact.js +0 -2
- package/dist/shared/plan-artifact.js.map +1 -1
- package/dist/shared/status-offload.d.ts +83 -9
- package/dist/shared/status-offload.js +399 -79
- package/dist/shared/status-offload.js.map +1 -1
- package/dist/shared/status.js +14 -1
- package/dist/shared/status.js.map +1 -1
- package/dist/shared/tool-kind.d.ts +19 -0
- package/dist/shared/tool-kind.js +13 -0
- package/dist/shared/tool-kind.js.map +1 -1
- package/dist/shared/tool-row.d.ts +88 -0
- package/dist/shared/tool-row.js +127 -0
- package/dist/shared/tool-row.js.map +1 -0
- package/dist/shared/workspace/platform-dir.d.ts +25 -0
- package/dist/shared/workspace/platform-dir.js +38 -2
- package/dist/shared/workspace/platform-dir.js.map +1 -1
- package/dist/workflows/call-agent-orchestrator.js +56 -7
- package/dist/workflows/call-agent-orchestrator.js.map +1 -1
- package/dist/workflows/connect-mcp-server.d.ts +50 -0
- package/dist/workflows/connect-mcp-server.js +136 -15
- package/dist/workflows/connect-mcp-server.js.map +1 -1
- package/dist/workflows/types.d.ts +8 -0
- package/package.json +2 -2
- package/src/__test-utils__/approval-contract/contract.ts +224 -0
- package/src/__test-utils__/approval-contract/types.ts +179 -0
- package/src/__test-utils__/fake-artifact-storage.ts +72 -0
- package/src/__tests__/approval-gateway-contract.test.ts +29 -0
- package/src/__tests__/claimcheck-codec.test.ts +16 -53
- package/src/__tests__/golden-e2e.test.ts +2 -0
- package/src/__tests__/runner-token-coordinator.test.ts +3 -3
- package/src/activities/__tests__/call-agent-status.test.ts +135 -0
- package/src/activities/__tests__/call-llm.test.ts +1 -1
- package/src/activities/__tests__/classify-tool-approvals.test.ts +208 -1
- package/src/activities/__tests__/discover-mcp-server.test.ts +30 -0
- package/src/activities/__tests__/workflow-event-activities.test.ts +2 -1
- package/src/activities/call-agent-status.ts +74 -4
- package/src/activities/call-llm.ts +18 -63
- package/src/activities/classify-tool-approvals.ts +101 -19
- package/src/activities/discover-mcp-server.ts +29 -1
- package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +216 -0
- package/src/activities/execute-cursor/__test-utils__/gateway-substrate.ts +148 -0
- package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +41 -9
- package/src/activities/execute-cursor/__tests__/approval-state.test.ts +292 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +68 -1
- package/src/activities/execute-cursor/__tests__/capture-flow.test.ts +1005 -0
- package/src/activities/execute-cursor/__tests__/cas-observations.test.ts +187 -0
- package/src/activities/execute-cursor/__tests__/coarse-fingerprint.test.ts +97 -0
- package/src/activities/execute-cursor/__tests__/command-provenance.test.ts +240 -0
- package/src/activities/execute-cursor/__tests__/deny-gate-exact-apply.test.ts +203 -0
- package/src/activities/execute-cursor/__tests__/exact-apply.test.ts +375 -0
- package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +1294 -24
- package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +446 -0
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +384 -110
- package/src/activities/execute-cursor/__tests__/message-translator.test.ts +171 -25
- package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +189 -0
- package/src/activities/execute-cursor/__tests__/tool-result-image.test.ts +44 -23
- package/src/activities/execute-cursor/__tests__/workspace-setup.test.ts +190 -10
- package/src/activities/execute-cursor/approval-policy.ts +28 -159
- package/src/activities/execute-cursor/approval-state.ts +366 -18
- package/src/activities/execute-cursor/capture-flow.ts +323 -0
- package/src/activities/execute-cursor/cas-observations.ts +204 -0
- package/src/activities/execute-cursor/command-provenance.ts +168 -0
- package/src/activities/execute-cursor/exact-apply.ts +253 -0
- package/src/activities/execute-cursor/hook-script.ts +317 -51
- package/src/activities/execute-cursor/index.ts +575 -67
- package/src/activities/execute-cursor/message-translator.ts +963 -89
- package/src/activities/execute-cursor/prompt-builder.ts +80 -14
- package/src/activities/execute-cursor/workspace-setup.ts +257 -42
- package/src/activities/execute-deep-agent/__test-utils__/gateway-substrate.ts +180 -0
- package/src/activities/execute-deep-agent/__test-utils__/scripted-model.ts +134 -0
- package/src/activities/execute-deep-agent/__tests__/approval-file-change.test.ts +84 -0
- package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +11 -24
- package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +64 -0
- package/src/activities/execute-deep-agent/__tests__/cas-capture-observer.test.ts +163 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-integration.test.ts +8 -5
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +342 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +284 -0
- package/src/activities/execute-deep-agent/__tests__/hitl.test.ts +14 -36
- package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +31 -13
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +349 -0
- package/src/activities/execute-deep-agent/__tests__/stamp-flowed-rows.test.ts +119 -0
- package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +91 -10
- package/src/activities/execute-deep-agent/__tests__/streaming-v3.test.ts +9 -9
- package/src/activities/execute-deep-agent/__tests__/subagent-approval-propagation.test.ts +160 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-gitignored-capture.test.ts +213 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-transformer.test.ts +3 -6
- package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +84 -1
- package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +4 -1
- package/src/activities/execute-deep-agent/approval-file-change.ts +80 -0
- package/src/activities/execute-deep-agent/attachment-injector.ts +20 -11
- package/src/activities/execute-deep-agent/cas-capture-backend.ts +66 -0
- package/src/activities/execute-deep-agent/cas-capture-observer.ts +125 -0
- package/src/activities/execute-deep-agent/hitl.ts +23 -7
- package/src/activities/execute-deep-agent/index.ts +463 -40
- package/src/activities/execute-deep-agent/inline-publisher.ts +27 -4
- package/src/activities/execute-deep-agent/setup.ts +223 -125
- package/src/activities/execute-deep-agent/stamp-flowed-rows.ts +64 -0
- package/src/activities/execute-deep-agent/status-builder-shared.ts +62 -23
- package/src/activities/execute-deep-agent/status-builder.ts +63 -8
- package/src/activities/execute-deep-agent/streaming-side-effects.ts +2 -16
- package/src/activities/execute-deep-agent/streaming.ts +3 -13
- package/src/activities/execute-deep-agent/subagent-transformer.ts +53 -13
- package/src/activities/execute-deep-agent/subagent-wiring.ts +50 -3
- package/src/activities/execute-deep-agent/v3-status-builder.ts +36 -2
- package/src/claimcheck/payload-codec.ts +8 -8
- package/src/client/stigmer-client.ts +9 -1
- package/src/middleware/__tests__/approval-gate.test.ts +488 -4
- package/src/middleware/approval-gate.ts +247 -38
- package/src/middleware/types.ts +5 -5
- package/src/shared/__tests__/activity-input.test.ts +78 -0
- package/src/shared/__tests__/approval-canonicalize.test.ts +106 -0
- package/src/shared/__tests__/approval-fingerprint.test.ts +115 -0
- package/src/shared/__tests__/approval-policy.test.ts +274 -40
- package/src/shared/__tests__/args-preview.test.ts +78 -0
- package/src/shared/__tests__/artifact-storage-extended.test.ts +62 -10
- package/src/shared/__tests__/artifact-storage.test.ts +123 -11
- package/src/shared/__tests__/file-change.test.ts +85 -0
- package/src/shared/__tests__/file-tools.test.ts +90 -0
- package/src/shared/__tests__/fingerprint-secret.test.ts +51 -0
- package/src/shared/__tests__/lease-scope-corpus.test.ts +56 -0
- package/src/shared/__tests__/model-client.test.ts +162 -0
- package/src/shared/__tests__/plan-artifact.test.ts +11 -26
- package/src/shared/__tests__/policy-source-corpus.test.ts +58 -0
- package/src/shared/__tests__/status-offload.test.ts +573 -16
- package/src/shared/__tests__/status.test.ts +4 -5
- package/src/shared/__tests__/tool-kind.test.ts +24 -1
- package/src/shared/__tests__/tool-row.test.ts +221 -0
- package/src/shared/activity-input.ts +57 -0
- package/src/shared/approval-canonicalize.ts +159 -0
- package/src/shared/approval-fingerprint.ts +148 -0
- package/src/shared/approval-policy.ts +303 -27
- package/src/shared/args-preview.ts +98 -0
- package/src/shared/artifact-storage.ts +62 -11
- package/src/shared/checkpointer/__tests__/http-saver.test.ts +1 -2
- package/src/shared/file-change.ts +64 -0
- package/src/shared/file-tools.ts +169 -0
- package/src/shared/filereview/__tests__/capture.test.ts +856 -0
- package/src/shared/filereview/__tests__/cas-substrate.test.ts +404 -0
- package/src/shared/filereview/__tests__/digest.test.ts +100 -0
- package/src/shared/filereview/__tests__/events.test.ts +245 -0
- package/src/shared/filereview/__tests__/git-substrate.test.ts +362 -0
- package/src/shared/filereview/__tests__/proxy-reconcile.test.ts +286 -0
- package/src/shared/filereview/__tests__/secret-paths.test.ts +121 -0
- package/src/shared/filereview/capture.ts +727 -0
- package/src/shared/filereview/cas-substrate.ts +401 -0
- package/src/shared/filereview/digest.ts +83 -0
- package/src/shared/filereview/events.ts +449 -0
- package/src/shared/filereview/git-substrate.ts +555 -0
- package/src/shared/filereview/index.ts +60 -0
- package/src/shared/filereview/secret-paths.ts +121 -0
- package/src/shared/fingerprint-secret.ts +53 -0
- package/src/shared/model-client.ts +122 -0
- package/src/shared/plan-artifact.ts +0 -2
- package/src/shared/status-offload.ts +433 -77
- package/src/shared/status.ts +13 -0
- package/src/shared/tool-kind.ts +33 -0
- package/src/shared/tool-row.ts +135 -0
- package/src/shared/workspace/platform-dir.ts +41 -2
- package/src/workflow-engine/__tests__/golden-execution.test.ts +35 -18
- package/src/workflow-engine/__tests__/tasks/try.test.ts +1 -1
- package/src/workflows/__tests__/connect-mcp-server.test.ts +304 -29
- package/src/workflows/call-agent-orchestrator.ts +53 -6
- package/src/workflows/connect-mcp-server.ts +179 -24
- package/src/workflows/types.ts +8 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the CAS snapshot/restore substrate (Phase 3).
|
|
3
|
+
*
|
|
4
|
+
* The substrate is exercised in isolation with an in-memory {@link ArtifactStorage}
|
|
5
|
+
* fake and a map-backed {@link BlobReader}; workspace writes go to a real temp
|
|
6
|
+
* dir. These prove the capture classification, content-addressed dedup,
|
|
7
|
+
* manifest determinism, and byte-exact idempotent reconcile — the loop the
|
|
8
|
+
* offline guard later exercises end-to-end.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
12
|
+
import { mkdtempSync, readFileSync, existsSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import {
|
|
16
|
+
FileCaptureClass,
|
|
17
|
+
FileChangeKind,
|
|
18
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
19
|
+
import { LocalArtifactStorage, ProxyArtifactStorage, type ArtifactStorage } from "../../artifact-storage.js";
|
|
20
|
+
import { makeInMemoryArtifactStorage } from "../../../__test-utils__/fake-artifact-storage.js";
|
|
21
|
+
import { sha256Bytes } from "../digest.js";
|
|
22
|
+
import {
|
|
23
|
+
applyCasApproved,
|
|
24
|
+
casBlobKey,
|
|
25
|
+
casBlobReader,
|
|
26
|
+
casManifestKey,
|
|
27
|
+
loadCasManifest,
|
|
28
|
+
restoreCasToBaseline,
|
|
29
|
+
snapshotCasChangeSet,
|
|
30
|
+
type BlobReader,
|
|
31
|
+
type CasCapturedFile,
|
|
32
|
+
} from "../cas-substrate.js";
|
|
33
|
+
|
|
34
|
+
const EXEC = "exec-1";
|
|
35
|
+
const CHANGE_SET = "exec-1:1";
|
|
36
|
+
const IGNORED = FileCaptureClass.GIT_IGNORED_CAPTURED;
|
|
37
|
+
|
|
38
|
+
function makeFakeStorage(): {
|
|
39
|
+
storage: ArtifactStorage;
|
|
40
|
+
readBlob: BlobReader;
|
|
41
|
+
blobs: Map<string, Buffer>;
|
|
42
|
+
uploadCount: () => number;
|
|
43
|
+
} {
|
|
44
|
+
const { storage, blobs } = makeInMemoryArtifactStorage();
|
|
45
|
+
const readBlob: BlobReader = (key) => storage.download(key);
|
|
46
|
+
return {
|
|
47
|
+
storage,
|
|
48
|
+
readBlob,
|
|
49
|
+
blobs,
|
|
50
|
+
uploadCount: () => storage.upload.mock.calls.length,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function bytes(s: string): Uint8Array {
|
|
55
|
+
return new TextEncoder().encode(s);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let workspace: string;
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
workspace = mkdtempSync(join(tmpdir(), "cas-sub-"));
|
|
61
|
+
});
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
rmSync(workspace, { recursive: true, force: true });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("snapshotCasChangeSet — classification", () => {
|
|
67
|
+
it("classifies ADD / MODIFY / DELETE and computes content hashes", async () => {
|
|
68
|
+
const { storage } = makeFakeStorage();
|
|
69
|
+
const { manifest } = await snapshotCasChangeSet({
|
|
70
|
+
storage,
|
|
71
|
+
executionId: EXEC,
|
|
72
|
+
changeSetId: CHANGE_SET,
|
|
73
|
+
captures: [
|
|
74
|
+
{ path: "created.env", before: null, after: bytes("NEW=1"), captureClass: IGNORED },
|
|
75
|
+
{ path: "changed.env", before: bytes("OLD=1"), after: bytes("OLD=2"), captureClass: IGNORED },
|
|
76
|
+
{ path: "removed.env", before: bytes("BYE=1"), after: null, captureClass: IGNORED },
|
|
77
|
+
],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const byPath = new Map(manifest.files.map((f) => [f.pathAfter || f.pathBefore, f]));
|
|
81
|
+
|
|
82
|
+
const add = byPath.get("created.env")!;
|
|
83
|
+
expect(add.kind).toBe(FileChangeKind.ADD);
|
|
84
|
+
expect(add.pathBefore).toBe("");
|
|
85
|
+
expect(add.before).toBeUndefined();
|
|
86
|
+
expect(add.after?.sha256).toBe(sha256Bytes(bytes("NEW=1")));
|
|
87
|
+
|
|
88
|
+
const mod = byPath.get("changed.env")!;
|
|
89
|
+
expect(mod.kind).toBe(FileChangeKind.MODIFY);
|
|
90
|
+
expect(mod.before?.sha256).toBe(sha256Bytes(bytes("OLD=1")));
|
|
91
|
+
expect(mod.after?.sha256).toBe(sha256Bytes(bytes("OLD=2")));
|
|
92
|
+
|
|
93
|
+
const del = byPath.get("removed.env")!;
|
|
94
|
+
expect(del.kind).toBe(FileChangeKind.DELETE);
|
|
95
|
+
expect(del.pathAfter).toBe("");
|
|
96
|
+
expect(del.after).toBeUndefined();
|
|
97
|
+
expect(del.before?.sha256).toBe(sha256Bytes(bytes("BYE=1")));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("drops a touched-but-unchanged path (before == after)", async () => {
|
|
101
|
+
const { storage } = makeFakeStorage();
|
|
102
|
+
const { manifest } = await snapshotCasChangeSet({
|
|
103
|
+
storage,
|
|
104
|
+
executionId: EXEC,
|
|
105
|
+
changeSetId: CHANGE_SET,
|
|
106
|
+
captures: [
|
|
107
|
+
{ path: "same.env", before: bytes("X=1"), after: bytes("X=1"), captureClass: IGNORED },
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
expect(manifest.files).toHaveLength(0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("drops a fully absent capture (before and after null)", async () => {
|
|
114
|
+
const { storage } = makeFakeStorage();
|
|
115
|
+
const { manifest } = await snapshotCasChangeSet({
|
|
116
|
+
storage,
|
|
117
|
+
executionId: EXEC,
|
|
118
|
+
changeSetId: CHANGE_SET,
|
|
119
|
+
captures: [{ path: "ghost", before: null, after: null, captureClass: IGNORED }],
|
|
120
|
+
});
|
|
121
|
+
expect(manifest.files).toHaveLength(0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("flags a binary side as not diff-complete", async () => {
|
|
125
|
+
const { storage } = makeFakeStorage();
|
|
126
|
+
const bin = new Uint8Array([1, 0, 2, 0, 3]); // contains NUL
|
|
127
|
+
const { manifest } = await snapshotCasChangeSet({
|
|
128
|
+
storage,
|
|
129
|
+
executionId: EXEC,
|
|
130
|
+
changeSetId: CHANGE_SET,
|
|
131
|
+
captures: [{ path: "blob.bin", before: null, after: bin, captureClass: IGNORED }],
|
|
132
|
+
});
|
|
133
|
+
expect(manifest.files[0].diffComplete).toBe(false);
|
|
134
|
+
expect(manifest.files[0].after?.isBinary).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("stamps the capture class through to the manifest", async () => {
|
|
138
|
+
const { storage } = makeFakeStorage();
|
|
139
|
+
const { manifest } = await snapshotCasChangeSet({
|
|
140
|
+
storage,
|
|
141
|
+
executionId: EXEC,
|
|
142
|
+
changeSetId: CHANGE_SET,
|
|
143
|
+
captures: [
|
|
144
|
+
{ path: "a", before: null, after: bytes("a"), captureClass: FileCaptureClass.NON_GIT_CAS },
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
expect(manifest.files[0].captureClass).toBe(FileCaptureClass.NON_GIT_CAS);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("snapshotCasChangeSet — storage", () => {
|
|
152
|
+
it("content-addresses blobs and dedupes identical bodies across paths", async () => {
|
|
153
|
+
const { storage, uploadCount } = makeFakeStorage();
|
|
154
|
+
await snapshotCasChangeSet({
|
|
155
|
+
storage,
|
|
156
|
+
executionId: EXEC,
|
|
157
|
+
changeSetId: CHANGE_SET,
|
|
158
|
+
captures: [
|
|
159
|
+
{ path: "a", before: null, after: bytes("SHARED"), captureClass: IGNORED },
|
|
160
|
+
{ path: "b", before: null, after: bytes("SHARED"), captureClass: IGNORED },
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
// Two files but one shared blob + one manifest = 2 uploads, not 3.
|
|
164
|
+
expect(uploadCount()).toBe(2);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("stores blobs under the execution-scoped content-address key", async () => {
|
|
168
|
+
const { storage, blobs } = makeFakeStorage();
|
|
169
|
+
await snapshotCasChangeSet({
|
|
170
|
+
storage,
|
|
171
|
+
executionId: EXEC,
|
|
172
|
+
changeSetId: CHANGE_SET,
|
|
173
|
+
captures: [{ path: "a", before: null, after: bytes("body"), captureClass: IGNORED }],
|
|
174
|
+
});
|
|
175
|
+
const expectedKey = casBlobKey(EXEC, sha256Bytes(bytes("body")));
|
|
176
|
+
expect(blobs.has(expectedKey)).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("persists a manifest whose digest is order-independent and reloadable", async () => {
|
|
180
|
+
const a = makeFakeStorage();
|
|
181
|
+
const b = makeFakeStorage();
|
|
182
|
+
const capA = { path: "a", before: null, after: bytes("A"), captureClass: IGNORED };
|
|
183
|
+
const capB = { path: "b", before: null, after: bytes("B"), captureClass: IGNORED };
|
|
184
|
+
|
|
185
|
+
const r1 = await snapshotCasChangeSet({
|
|
186
|
+
storage: a.storage, executionId: EXEC, changeSetId: CHANGE_SET, captures: [capA, capB],
|
|
187
|
+
});
|
|
188
|
+
const r2 = await snapshotCasChangeSet({
|
|
189
|
+
storage: b.storage, executionId: EXEC, changeSetId: CHANGE_SET, captures: [capB, capA],
|
|
190
|
+
});
|
|
191
|
+
// Same files in a different input order -> identical manifest digest.
|
|
192
|
+
expect(r1.ref.manifestDigest).toBe(r2.ref.manifestDigest);
|
|
193
|
+
expect(r1.ref.artifactUri).toBe(casManifestKey(EXEC, CHANGE_SET));
|
|
194
|
+
|
|
195
|
+
const loaded = await loadCasManifest({ readBlob: a.readBlob, ref: r1.ref });
|
|
196
|
+
expect(loaded.files.map((f) => f.pathAfter)).toEqual(["a", "b"]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("loadCasManifest surfaces the reader error when the manifest is absent (callers gate on the candidate snapshot, never on a storage probe)", async () => {
|
|
200
|
+
const { readBlob } = makeFakeStorage();
|
|
201
|
+
// loadCasManifest no longer probes existence — it is invoked ONLY when the
|
|
202
|
+
// change set's CANDIDATE snapshot says this was a CAS turn, so a missing blob
|
|
203
|
+
// is a genuine integrity error, not a "git-only turn" signal.
|
|
204
|
+
await expect(
|
|
205
|
+
loadCasManifest({
|
|
206
|
+
readBlob,
|
|
207
|
+
ref: { manifestDigest: "0".repeat(64), artifactUri: casManifestKey("other", "nope:1") },
|
|
208
|
+
}),
|
|
209
|
+
).rejects.toThrow(/Artifact not found/);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("loadCasManifest fails closed when the downloaded bytes do not hash to the ledger's manifestDigest", async () => {
|
|
213
|
+
// The per-blob content addresses protect blob BODIES; only this check
|
|
214
|
+
// protects the path→blob mapping. A tampered manifest must never drive a
|
|
215
|
+
// reconcile, even when every blob it references is individually intact.
|
|
216
|
+
const { storage, readBlob, blobs } = makeFakeStorage();
|
|
217
|
+
const { manifest, ref } = await snapshotCasChangeSet({
|
|
218
|
+
storage, executionId: EXEC, changeSetId: CHANGE_SET,
|
|
219
|
+
captures: [{ path: "a.env", before: null, after: bytes("A=1"), captureClass: IGNORED }],
|
|
220
|
+
});
|
|
221
|
+
// Swap the stored path→blob mapping while keeping it valid JSON: the blob
|
|
222
|
+
// refs still verify individually, but the manifest bytes no longer match
|
|
223
|
+
// the digest the CANDIDATE event pinned.
|
|
224
|
+
const tampered = { ...manifest, files: manifest.files.map((f) => ({ ...f, pathAfter: "b.env" })) };
|
|
225
|
+
blobs.set(ref.artifactUri, Buffer.from(JSON.stringify(tampered), "utf8"));
|
|
226
|
+
|
|
227
|
+
await expect(loadCasManifest({ readBlob, ref })).rejects.toThrow(
|
|
228
|
+
/CAS manifest integrity check failed/,
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("reconcile — applyCasApproved", () => {
|
|
234
|
+
it("writes approved after bytes for a MODIFY", async () => {
|
|
235
|
+
const { storage, readBlob } = makeFakeStorage();
|
|
236
|
+
const abs = join(workspace, "changed.env");
|
|
237
|
+
mkdirSync(workspace, { recursive: true });
|
|
238
|
+
writeFileSync(abs, "OLD=2"); // the applied (after) state from the review window
|
|
239
|
+
const { manifest } = await snapshotCasChangeSet({
|
|
240
|
+
storage, executionId: EXEC, changeSetId: CHANGE_SET,
|
|
241
|
+
captures: [{ path: "changed.env", before: bytes("OLD=1"), after: bytes("OLD=2"), captureClass: IGNORED }],
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
await applyCasApproved({ readBlob, workspaceRoot: workspace, files: manifest.files });
|
|
245
|
+
expect(readFileSync(abs, "utf8")).toBe("OLD=2");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("removes the file for an approved DELETE", async () => {
|
|
249
|
+
const { storage, readBlob } = makeFakeStorage();
|
|
250
|
+
const abs = join(workspace, "removed.env");
|
|
251
|
+
const { manifest } = await snapshotCasChangeSet({
|
|
252
|
+
storage, executionId: EXEC, changeSetId: CHANGE_SET,
|
|
253
|
+
captures: [{ path: "removed.env", before: bytes("BYE=1"), after: null, captureClass: IGNORED }],
|
|
254
|
+
});
|
|
255
|
+
await applyCasApproved({ readBlob, workspaceRoot: workspace, files: manifest.files });
|
|
256
|
+
expect(existsSync(abs)).toBe(false);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("throws on a blob integrity mismatch rather than writing wrong bytes", async () => {
|
|
260
|
+
const { storage, blobs } = makeFakeStorage();
|
|
261
|
+
const { manifest } = await snapshotCasChangeSet({
|
|
262
|
+
storage, executionId: EXEC, changeSetId: CHANGE_SET,
|
|
263
|
+
captures: [{ path: "a", before: null, after: bytes("good"), captureClass: IGNORED }],
|
|
264
|
+
});
|
|
265
|
+
// Corrupt the stored blob so its bytes no longer hash to the content address.
|
|
266
|
+
const key = manifest.files[0].after!.storageKey;
|
|
267
|
+
blobs.set(key, Buffer.from("tampered"));
|
|
268
|
+
const corruptReader: BlobReader = async (k) => blobs.get(k)!;
|
|
269
|
+
|
|
270
|
+
await expect(
|
|
271
|
+
applyCasApproved({ readBlob: corruptReader, workspaceRoot: workspace, files: manifest.files }),
|
|
272
|
+
).rejects.toThrow(/integrity check failed/);
|
|
273
|
+
expect(existsSync(join(workspace, "a"))).toBe(false);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("is idempotent — applying twice converges", async () => {
|
|
277
|
+
const { storage, readBlob } = makeFakeStorage();
|
|
278
|
+
const { manifest } = await snapshotCasChangeSet({
|
|
279
|
+
storage, executionId: EXEC, changeSetId: CHANGE_SET,
|
|
280
|
+
captures: [{ path: "a", before: null, after: bytes("v1"), captureClass: IGNORED }],
|
|
281
|
+
});
|
|
282
|
+
await applyCasApproved({ readBlob, workspaceRoot: workspace, files: manifest.files });
|
|
283
|
+
await applyCasApproved({ readBlob, workspaceRoot: workspace, files: manifest.files });
|
|
284
|
+
expect(readFileSync(join(workspace, "a"), "utf8")).toBe("v1");
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe("reconcile — restoreCasToBaseline", () => {
|
|
289
|
+
it("removes an agent-created file (rejected ADD)", async () => {
|
|
290
|
+
const { storage, readBlob } = makeFakeStorage();
|
|
291
|
+
const abs = join(workspace, "created.env");
|
|
292
|
+
writeFileSync(abs, "NEW=1"); // the applied create, to be reverted
|
|
293
|
+
const { manifest } = await snapshotCasChangeSet({
|
|
294
|
+
storage, executionId: EXEC, changeSetId: CHANGE_SET,
|
|
295
|
+
captures: [{ path: "created.env", before: null, after: bytes("NEW=1"), captureClass: IGNORED }],
|
|
296
|
+
});
|
|
297
|
+
await restoreCasToBaseline({ readBlob, workspaceRoot: workspace, files: manifest.files });
|
|
298
|
+
expect(existsSync(abs)).toBe(false);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("restores the before bytes for a rejected MODIFY", async () => {
|
|
302
|
+
const { storage, readBlob } = makeFakeStorage();
|
|
303
|
+
const abs = join(workspace, "changed.env");
|
|
304
|
+
writeFileSync(abs, "OLD=2"); // applied edit
|
|
305
|
+
const { manifest } = await snapshotCasChangeSet({
|
|
306
|
+
storage, executionId: EXEC, changeSetId: CHANGE_SET,
|
|
307
|
+
captures: [{ path: "changed.env", before: bytes("OLD=1"), after: bytes("OLD=2"), captureClass: IGNORED }],
|
|
308
|
+
});
|
|
309
|
+
await restoreCasToBaseline({ readBlob, workspaceRoot: workspace, files: manifest.files });
|
|
310
|
+
expect(readFileSync(abs, "utf8")).toBe("OLD=1");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("recreates a rejected DELETE from its before bytes", async () => {
|
|
314
|
+
const { storage, readBlob } = makeFakeStorage();
|
|
315
|
+
const abs = join(workspace, "removed.env");
|
|
316
|
+
const { manifest } = await snapshotCasChangeSet({
|
|
317
|
+
storage, executionId: EXEC, changeSetId: CHANGE_SET,
|
|
318
|
+
captures: [{ path: "removed.env", before: bytes("BYE=1"), after: null, captureClass: IGNORED }],
|
|
319
|
+
});
|
|
320
|
+
// The delete was applied during review (file gone); rejecting restores it.
|
|
321
|
+
await restoreCasToBaseline({ readBlob, workspaceRoot: workspace, files: manifest.files });
|
|
322
|
+
expect(readFileSync(abs, "utf8")).toBe("BYE=1");
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe("casBlobReader", () => {
|
|
327
|
+
it("adapts the storage port's download into a BlobReader", async () => {
|
|
328
|
+
const { storage } = makeFakeStorage();
|
|
329
|
+
await snapshotCasChangeSet({
|
|
330
|
+
storage, executionId: EXEC, changeSetId: CHANGE_SET,
|
|
331
|
+
captures: [{ path: "a", before: null, after: bytes("hello"), captureClass: IGNORED }],
|
|
332
|
+
});
|
|
333
|
+
const key = casBlobKey(EXEC, sha256Bytes(bytes("hello")));
|
|
334
|
+
|
|
335
|
+
const reader = casBlobReader(storage);
|
|
336
|
+
const got = await reader(key);
|
|
337
|
+
expect(got.toString("utf8")).toBe("hello");
|
|
338
|
+
expect(storage.download).toHaveBeenCalledWith(key);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
describe("casBlobReader — real LocalArtifactStorage serve path (OSS-local)", () => {
|
|
343
|
+
it("reads uploaded bytes byte-exact straight off disk (no HTTP server running)", async () => {
|
|
344
|
+
const basePath = mkdtempSync(join(tmpdir(), "cas-serve-"));
|
|
345
|
+
try {
|
|
346
|
+
// Deliberately point the serve URL at an unroutable base: if the reader
|
|
347
|
+
// fetched over HTTP this would fail. Reading directly off disk proves the
|
|
348
|
+
// OSS-local reconcile read-back needs no serve endpoint at all.
|
|
349
|
+
const storage = new LocalArtifactStorage(basePath, "http://127.0.0.1:0");
|
|
350
|
+
|
|
351
|
+
const payload = Buffer.from("SECRET_TREASURE=42\n", "utf8");
|
|
352
|
+
const key = casBlobKey(EXEC, sha256Bytes(payload));
|
|
353
|
+
await storage.upload(key, payload, "application/octet-stream");
|
|
354
|
+
|
|
355
|
+
const reader = casBlobReader(storage);
|
|
356
|
+
const got = await reader(key);
|
|
357
|
+
expect(got.equals(payload)).toBe(true);
|
|
358
|
+
} finally {
|
|
359
|
+
rmSync(basePath, { recursive: true, force: true });
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
describe("casBlobReader — ProxyArtifactStorage presigned+fetch path (cloud)", () => {
|
|
365
|
+
it("resolves a presigned URL then fetches the exact bytes over HTTP", async () => {
|
|
366
|
+
const payload = Buffer.from("CLOUD_BLOB=1\n", "utf8");
|
|
367
|
+
const key = casBlobKey(EXEC, sha256Bytes(payload));
|
|
368
|
+
|
|
369
|
+
const fetchMock = vi.fn(async (input: string | URL | Request) => {
|
|
370
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
371
|
+
if (url.includes("presigned-download-url")) {
|
|
372
|
+
return new Response(JSON.stringify({ url: "https://r2.example.com/dl" }), { status: 200 });
|
|
373
|
+
}
|
|
374
|
+
// Isolate the ArrayBuffer (a Node Buffer's .buffer is a shared pool).
|
|
375
|
+
const copy = new Uint8Array(payload);
|
|
376
|
+
return new Response(copy, { status: 200 });
|
|
377
|
+
});
|
|
378
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
379
|
+
try {
|
|
380
|
+
const storage = new ProxyArtifactStorage("https://proxy.example.com", "tok");
|
|
381
|
+
const reader = casBlobReader(storage);
|
|
382
|
+
const got = await reader(key);
|
|
383
|
+
expect(got.equals(payload)).toBe(true);
|
|
384
|
+
} finally {
|
|
385
|
+
vi.unstubAllGlobals();
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
describe("digest independence from capture class", () => {
|
|
391
|
+
it("orders files deterministically by path", async () => {
|
|
392
|
+
const { storage } = makeFakeStorage();
|
|
393
|
+
const { manifest } = await snapshotCasChangeSet({
|
|
394
|
+
storage, executionId: EXEC, changeSetId: CHANGE_SET,
|
|
395
|
+
captures: [
|
|
396
|
+
{ path: "z", before: null, after: bytes("z"), captureClass: IGNORED },
|
|
397
|
+
{ path: "a", before: null, after: bytes("a"), captureClass: IGNORED },
|
|
398
|
+
{ path: "m", before: null, after: bytes("m"), captureClass: IGNORED },
|
|
399
|
+
],
|
|
400
|
+
});
|
|
401
|
+
const files = manifest.files as CasCapturedFile[];
|
|
402
|
+
expect(files.map((f) => f.pathAfter)).toEqual(["a", "m", "z"]);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The TypeScript driver of the cross-edition file-digest corpus
|
|
3
|
+
* (`apis/testdata/hitl/file-digest/vectors.json`). The Go (`digest_test.go`) and
|
|
4
|
+
* Java (`FileDigestTest`) suites replay the SAME vectors, so a passing run in all
|
|
5
|
+
* three proves the runner computes byte-identical `file_digest` /
|
|
6
|
+
* `aggregate_digest` to both backends — the load-bearing parity contract for the
|
|
7
|
+
* producer.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
14
|
+
import { describe, expect, it } from "vitest";
|
|
15
|
+
import { FileChangeKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
16
|
+
import { aggregateDigest, fileChangeKindName, fileDigest, type FileDigestInput } from "../digest.js";
|
|
17
|
+
|
|
18
|
+
interface VectorCase {
|
|
19
|
+
name: string;
|
|
20
|
+
path_before: string;
|
|
21
|
+
path_after: string;
|
|
22
|
+
kind: string;
|
|
23
|
+
before_sha256: string;
|
|
24
|
+
after_sha256: string;
|
|
25
|
+
file_digest: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface VectorAggregate {
|
|
29
|
+
name: string;
|
|
30
|
+
change_names: string[];
|
|
31
|
+
aggregate_digest: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface Vectors {
|
|
35
|
+
cases: VectorCase[];
|
|
36
|
+
aggregates: VectorAggregate[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Walk up from this file until the shared HITL fixtures are found. */
|
|
40
|
+
function findVectorsFile(): string {
|
|
41
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
42
|
+
for (let i = 0; i < 12; i++) {
|
|
43
|
+
const candidate = join(dir, "apis", "testdata", "hitl", "file-digest", "vectors.json");
|
|
44
|
+
if (existsSync(candidate)) return candidate;
|
|
45
|
+
dir = dirname(dir);
|
|
46
|
+
}
|
|
47
|
+
throw new Error("could not locate apis/testdata/hitl/file-digest/vectors.json");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Map the proto enum value name in the corpus back to the TS enum member. */
|
|
51
|
+
function kindFromName(name: string): FileChangeKind {
|
|
52
|
+
for (const k of [
|
|
53
|
+
FileChangeKind.UNSPECIFIED,
|
|
54
|
+
FileChangeKind.ADD,
|
|
55
|
+
FileChangeKind.MODIFY,
|
|
56
|
+
FileChangeKind.DELETE,
|
|
57
|
+
FileChangeKind.RENAME,
|
|
58
|
+
FileChangeKind.BINARY_CHANGE,
|
|
59
|
+
]) {
|
|
60
|
+
if (fileChangeKindName(k) === name) return k;
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`unknown FileChangeKind name in corpus: ${name}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const vectors: Vectors = JSON.parse(readFileSync(findVectorsFile(), "utf8"));
|
|
66
|
+
|
|
67
|
+
function toInput(c: VectorCase): FileDigestInput {
|
|
68
|
+
return {
|
|
69
|
+
pathBefore: c.path_before,
|
|
70
|
+
pathAfter: c.path_after,
|
|
71
|
+
kind: kindFromName(c.kind),
|
|
72
|
+
beforeSha256: c.before_sha256,
|
|
73
|
+
afterSha256: c.after_sha256,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe("file-digest cross-edition corpus", () => {
|
|
78
|
+
it("loads the shared corpus (guard against an empty/missing file)", () => {
|
|
79
|
+
expect(vectors.cases.length).toBeGreaterThanOrEqual(4);
|
|
80
|
+
expect(vectors.aggregates.length).toBeGreaterThanOrEqual(3);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
for (const c of vectors.cases) {
|
|
84
|
+
it(`file_digest matches for "${c.name}"`, () => {
|
|
85
|
+
expect(fileDigest(toInput(c))).toBe(c.file_digest);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const byName = new Map(vectors.cases.map((c) => [c.name, c]));
|
|
90
|
+
for (const agg of vectors.aggregates) {
|
|
91
|
+
it(`aggregate_digest matches for "${agg.name}"`, () => {
|
|
92
|
+
const inputs = agg.change_names.map((n) => {
|
|
93
|
+
const c = byName.get(n);
|
|
94
|
+
if (!c) throw new Error(`aggregate references unknown case ${n}`);
|
|
95
|
+
return toInput(c);
|
|
96
|
+
});
|
|
97
|
+
expect(aggregateDigest(inputs)).toBe(agg.aggregate_digest);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
});
|