@intentius/chant 0.46.0 → 0.50.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/audit/catalog.d.ts +13 -3
- package/dist/audit/catalog.d.ts.map +1 -1
- package/dist/audit/core.d.ts +30 -3
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +9 -2
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/fetch.d.ts.map +1 -1
- package/dist/audit/report-html.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +6 -0
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/report.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/audit/secrets.d.ts +95 -0
- package/dist/audit/secrets.d.ts.map +1 -0
- package/dist/audit/wrangler.d.ts +33 -0
- package/dist/audit/wrangler.d.ts.map +1 -0
- package/dist/build.d.ts +3 -3
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +7 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +23 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts +31 -0
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +12 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +32 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -0
- package/dist/cli/handlers/scenario.d.ts +39 -0
- package/dist/cli/handlers/scenario.d.ts.map +1 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
- package/dist/cli/mcp/server.d.ts +35 -2
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/tools/explain.d.ts +6 -0
- package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
- package/dist/cli/mcp/types.d.ts +29 -1
- package/dist/cli/mcp/types.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +1 -1
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +14 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/reporters/stylish.d.ts +15 -1
- package/dist/cli/reporters/stylish.d.ts.map +1 -1
- package/dist/codegen/docs-rule-scanning.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +4 -0
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/capability.d.ts +17 -2
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +7 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/component.d.ts +15 -0
- package/dist/components/component.d.ts.map +1 -1
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts +2 -0
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/components/verbs/ensure-secret.d.ts +50 -0
- package/dist/components/verbs/ensure-secret.d.ts.map +1 -0
- package/dist/components/verbs/index.d.ts +13 -0
- package/dist/components/verbs/index.d.ts.map +1 -1
- package/dist/components/verbs/r2-sync.d.ts +76 -0
- package/dist/components/verbs/r2-sync.d.ts.map +1 -0
- package/dist/components/verbs/run-agent.d.ts +499 -0
- package/dist/components/verbs/run-agent.d.ts.map +1 -0
- package/dist/components/verbs/sign.d.ts +30 -0
- package/dist/components/verbs/sign.d.ts.map +1 -1
- package/dist/components/verbs/wrangler.d.ts +108 -0
- package/dist/components/verbs/wrangler.d.ts.map +1 -0
- package/dist/composite.d.ts +6 -1
- package/dist/composite.d.ts.map +1 -1
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/deep-observation.d.ts +14 -0
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +15 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-rank.d.ts +66 -0
- package/dist/discovery/fold-rank.d.ts.map +1 -0
- package/dist/discovery/index.d.ts +15 -0
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/param-deps.d.ts +17 -0
- package/dist/discovery/param-deps.d.ts.map +1 -0
- package/dist/effect-receipt.d.ts +177 -0
- package/dist/effect-receipt.d.ts.map +1 -0
- package/dist/fold/fold.d.ts +55 -2
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +20 -0
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +2 -0
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +137 -3
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +33 -5
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/converge-ledger.d.ts +90 -0
- package/dist/lifecycle/converge-ledger.d.ts.map +1 -0
- package/dist/lifecycle/deep-diff.d.ts +18 -0
- package/dist/lifecycle/deep-diff.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts +9 -1
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +33 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -0
- package/dist/lifecycle/git.d.ts +145 -21
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +6 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/lease.d.ts +113 -0
- package/dist/lifecycle/lease.d.ts.map +1 -0
- package/dist/lifecycle/observation-baseline.d.ts +21 -3
- package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
- package/dist/lifecycle/receipt-plan.d.ts +62 -0
- package/dist/lifecycle/receipt-plan.d.ts.map +1 -0
- package/dist/lifecycle/release-ledger.d.ts +20 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/scenario-eval.d.ts +42 -0
- package/dist/lifecycle/scenario-eval.d.ts.map +1 -0
- package/dist/lifecycle/scenario.d.ts +163 -0
- package/dist/lifecycle/scenario.d.ts.map +1 -0
- package/dist/lifecycle/symptoms.d.ts +63 -0
- package/dist/lifecycle/symptoms.d.ts.map +1 -0
- package/dist/lifecycle/teardown.d.ts +6 -4
- package/dist/lifecycle/teardown.d.ts.map +1 -1
- package/dist/lifecycle/unobserved-gate.d.ts +67 -0
- package/dist/lifecycle/unobserved-gate.d.ts.map +1 -0
- package/dist/lint/knowledge-checks.d.ts +48 -0
- package/dist/lint/knowledge-checks.d.ts.map +1 -0
- package/dist/lint/output-checks.d.ts +5 -0
- package/dist/lint/output-checks.d.ts.map +1 -0
- package/dist/lint/output-docs.d.ts +94 -0
- package/dist/lint/output-docs.d.ts.map +1 -0
- package/dist/lint/pipeline-change-gate.d.ts +101 -0
- package/dist/lint/pipeline-change-gate.d.ts.map +1 -0
- package/dist/lint/post-synth.d.ts +41 -0
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/lint/receipt-checks.d.ts +9 -0
- package/dist/lint/receipt-checks.d.ts.map +1 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts +11 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts.map +1 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts +13 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts.map +1 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts +3 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts.map +1 -0
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/index.d.ts +3 -1
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/lsp/lexicon-providers.d.ts +7 -0
- package/dist/lsp/lexicon-providers.d.ts.map +1 -1
- package/dist/okf-read.d.ts +78 -0
- package/dist/okf-read.d.ts.map +1 -0
- package/dist/op/activity-contract.d.ts +139 -0
- package/dist/op/activity-contract.d.ts.map +1 -0
- package/dist/op/builders.d.ts +140 -3
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/converge-rule.d.ts +161 -0
- package/dist/op/converge-rule.d.ts.map +1 -0
- package/dist/op/generate-pipeline.d.ts +39 -0
- package/dist/op/generate-pipeline.d.ts.map +1 -0
- package/dist/op/index.d.ts +18 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +2 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/op-verb-class.d.ts +42 -0
- package/dist/op/op-verb-class.d.ts.map +1 -0
- package/dist/op/operator.d.ts +128 -0
- package/dist/op/operator.d.ts.map +1 -0
- package/dist/op/receipt-store.d.ts +138 -0
- package/dist/op/receipt-store.d.ts.map +1 -0
- package/dist/op/step-output-ref.d.ts +187 -0
- package/dist/op/step-output-ref.d.ts.map +1 -0
- package/dist/op/types.d.ts +49 -2
- package/dist/op/types.d.ts.map +1 -1
- package/dist/provenance.d.ts +73 -3
- package/dist/provenance.d.ts.map +1 -1
- package/dist/runtime-adapter.d.ts +7 -1
- package/dist/runtime-adapter.d.ts.map +1 -1
- package/dist/secret-materialization.d.ts +138 -0
- package/dist/secret-materialization.d.ts.map +1 -0
- package/dist/secret-provenance.d.ts +218 -0
- package/dist/secret-provenance.d.ts.map +1 -0
- package/dist/serializer.d.ts +29 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/toml.d.ts +40 -5
- package/dist/toml.d.ts.map +1 -1
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/audit/catalog.test.ts +1 -1
- package/src/audit/catalog.ts +75 -3
- package/src/audit/core.test.ts +57 -0
- package/src/audit/core.ts +0 -0
- package/src/audit/detect-bundle.test.ts +1 -1
- package/src/audit/discover.test.ts +24 -0
- package/src/audit/discover.ts +40 -4
- package/src/audit/fetch.test.ts +216 -3
- package/src/audit/fetch.ts +270 -59
- package/src/audit/report-html.ts +5 -2
- package/src/audit/report-model.ts +9 -0
- package/src/audit/report.test.ts +22 -0
- package/src/audit/report.ts +3 -2
- package/src/audit/rules-doc.ts +13 -1
- package/src/audit/secrets.test.ts +303 -0
- package/src/audit/secrets.ts +406 -0
- package/src/audit/wrangler.test.ts +230 -0
- package/src/audit/wrangler.ts +290 -0
- package/src/build.test.ts +41 -0
- package/src/build.ts +39 -6
- package/src/cli/command-group.ts +1 -1
- package/src/cli/commands/__fixtures__/audit-fountain/agents/fleet.yaml +27 -0
- package/src/cli/commands/__fixtures__/audit-fountain/k8s/deploy.yaml +16 -0
- package/src/cli/commands/__fixtures__/audit-fountain-clean/fleet.yaml +20 -0
- package/src/cli/commands/__fixtures__/schemas/sarif-2.1.0.schema.json +2882 -0
- package/src/cli/commands/audit.test.ts +268 -1
- package/src/cli/commands/audit.ts +87 -18
- package/src/cli/commands/build.test.ts +245 -0
- package/src/cli/commands/build.ts +210 -21
- package/src/cli/commands/lint.ts +15 -3
- package/src/cli/handlers/build.ts +2 -0
- package/src/cli/handlers/components.test.ts +199 -1
- package/src/cli/handlers/components.ts +160 -3
- package/src/cli/handlers/explain.test.ts +70 -1
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +12 -3
- package/src/cli/handlers/lifecycle.test.ts +115 -1
- package/src/cli/handlers/lifecycle.ts +96 -15
- package/src/cli/handlers/operator.test.ts +255 -0
- package/src/cli/handlers/operator.ts +240 -0
- package/src/cli/handlers/scenario.test.ts +456 -0
- package/src/cli/handlers/scenario.ts +330 -0
- package/src/cli/main.test.ts +23 -0
- package/src/cli/main.ts +72 -1
- package/src/cli/mcp/resource-handlers.ts +38 -1
- package/src/cli/mcp/server.test.ts +323 -3
- package/src/cli/mcp/server.ts +84 -7
- package/src/cli/mcp/tools/explain.ts +51 -2
- package/src/cli/mcp/types.ts +27 -1
- package/src/cli/plugins.ts +4 -2
- package/src/cli/registry.ts +14 -2
- package/src/cli/reporters/stylish.test.ts +154 -0
- package/src/cli/reporters/stylish.ts +154 -33
- package/src/codegen/docs-rule-scanning.test.ts +42 -0
- package/src/codegen/docs-rule-scanning.ts +25 -2
- package/src/components/README.md +7 -0
- package/src/components/auto-release.ts +6 -0
- package/src/components/capability.ts +17 -2
- package/src/components/cli-support.test.ts +17 -0
- package/src/components/cli-support.ts +13 -1
- package/src/components/component-schema.test.ts +32 -0
- package/src/components/component.schema.json +6 -0
- package/src/components/component.test.ts +21 -0
- package/src/components/component.ts +15 -0
- package/src/components/driver.ts +12 -4
- package/src/components/registry.test.ts +7 -2
- package/src/components/starter-plugin.ts +17 -0
- package/src/components/verbs/ensure-secret.test.ts +130 -0
- package/src/components/verbs/ensure-secret.ts +79 -0
- package/src/components/verbs/index.ts +13 -0
- package/src/components/verbs/r2-sync.test.ts +107 -0
- package/src/components/verbs/r2-sync.ts +124 -0
- package/src/components/verbs/run-agent.test.ts +683 -0
- package/src/components/verbs/run-agent.ts +786 -0
- package/src/components/verbs/sign.test.ts +19 -0
- package/src/components/verbs/sign.ts +34 -2
- package/src/components/verbs/wrangler.test.ts +170 -0
- package/src/components/verbs/wrangler.ts +241 -0
- package/src/composite.ts +31 -2
- package/src/config.test.ts +15 -0
- package/src/config.ts +30 -0
- package/src/deep-observation.test.ts +19 -0
- package/src/deep-observation.ts +17 -0
- package/src/discovery/collect.ts +11 -2
- package/src/discovery/fold-import.test.ts +54 -0
- package/src/discovery/fold-import.ts +178 -38
- package/src/discovery/fold-rank.test.ts +197 -0
- package/src/discovery/fold-rank.ts +346 -0
- package/src/discovery/index.ts +16 -1
- package/src/discovery/param-deps.test.ts +118 -0
- package/src/discovery/param-deps.ts +170 -0
- package/src/effect-receipt-exclusion.test.ts +190 -0
- package/src/effect-receipt.test.ts +419 -0
- package/src/effect-receipt.ts +412 -0
- package/src/fold/fold.test.ts +6 -2
- package/src/fold/fold.ts +184 -3
- package/src/fold/subset.test.ts +95 -6
- package/src/fold/subset.ts +66 -2
- package/src/index.ts +4 -0
- package/src/lexicon-schema.ts +3 -0
- package/src/lexicon.ts +151 -5
- package/src/lifecycle/change-set.ts +46 -7
- package/src/lifecycle/converge-ledger.test.ts +199 -0
- package/src/lifecycle/converge-ledger.ts +179 -0
- package/src/lifecycle/deep-diff.test.ts +79 -1
- package/src/lifecycle/deep-diff.ts +23 -0
- package/src/lifecycle/deep-observe.ts +13 -2
- package/src/lifecycle/gate-ledger.test.ts +103 -0
- package/src/lifecycle/gate-ledger.ts +140 -0
- package/src/lifecycle/git.test.ts +430 -0
- package/src/lifecycle/git.ts +446 -84
- package/src/lifecycle/index.ts +6 -0
- package/src/lifecycle/lease.test.ts +343 -0
- package/src/lifecycle/lease.ts +270 -0
- package/src/lifecycle/observation-baseline.test.ts +46 -0
- package/src/lifecycle/observation-baseline.ts +33 -1
- package/src/lifecycle/receipt-plan.test.ts +250 -0
- package/src/lifecycle/receipt-plan.ts +249 -0
- package/src/lifecycle/release-ledger.ts +20 -0
- package/src/lifecycle/scenario-eval.test.ts +199 -0
- package/src/lifecycle/scenario-eval.ts +158 -0
- package/src/lifecycle/scenario.test.ts +195 -0
- package/src/lifecycle/scenario.ts +321 -0
- package/src/lifecycle/symptoms.test.ts +116 -0
- package/src/lifecycle/symptoms.ts +126 -0
- package/src/lifecycle/teardown.test.ts +31 -0
- package/src/lifecycle/teardown.ts +6 -4
- package/src/lifecycle/unobserved-gate.test.ts +109 -0
- package/src/lifecycle/unobserved-gate.ts +102 -0
- package/src/lint/knowledge-checks.test.ts +80 -0
- package/src/lint/knowledge-checks.ts +74 -0
- package/src/lint/output-checks.test.ts +85 -0
- package/src/lint/output-checks.ts +99 -0
- package/src/lint/output-docs.test.ts +220 -0
- package/src/lint/output-docs.ts +204 -0
- package/src/lint/pipeline-change-gate.test.ts +144 -0
- package/src/lint/pipeline-change-gate.ts +153 -0
- package/src/lint/post-synth.test.ts +97 -0
- package/src/lint/post-synth.ts +60 -0
- package/src/lint/receipt-checks.test.ts +101 -0
- package/src/lint/receipt-checks.ts +93 -0
- package/src/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.ts +26 -0
- package/src/lint/rules/comp/comp.test.ts +49 -1
- package/src/lint/rules/cor022-receipt-leaf.test.ts +116 -0
- package/src/lint/rules/cor022-receipt-leaf.ts +130 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.test.ts +121 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.ts +218 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +35 -3
- package/src/lint/rules/evl001-non-literal-expression.ts +7 -0
- package/src/lint/rules/index.ts +7 -1
- package/src/lsp/lexicon-providers.test.ts +44 -0
- package/src/lsp/lexicon-providers.ts +11 -1
- package/src/okf-read.test.ts +149 -0
- package/src/okf-read.ts +197 -0
- package/src/op/activity-contract.test.ts +180 -0
- package/src/op/activity-contract.ts +278 -0
- package/src/op/builders-exports.test.ts +17 -1
- package/src/op/builders.ts +198 -6
- package/src/op/converge-rule.test.ts +179 -0
- package/src/op/converge-rule.ts +311 -0
- package/src/op/effect-step.test.ts +311 -0
- package/src/op/generate-pipeline.test.ts +53 -0
- package/src/op/generate-pipeline.ts +99 -0
- package/src/op/index.ts +40 -3
- package/src/op/local-executor.test.ts +92 -0
- package/src/op/local-executor.ts +212 -29
- package/src/op/op-verb-class.test.ts +126 -0
- package/src/op/op-verb-class.ts +115 -0
- package/src/op/op.test.ts +25 -2
- package/src/op/operator.test.ts +346 -0
- package/src/op/operator.ts +213 -0
- package/src/op/receipt-store.ts +211 -0
- package/src/op/step-output-ref.test.ts +334 -0
- package/src/op/step-output-ref.ts +453 -0
- package/src/op/types.ts +51 -2
- package/src/provenance.test.ts +151 -4
- package/src/provenance.ts +118 -4
- package/src/runtime-adapter.ts +31 -10
- package/src/secret-materialization.test.ts +199 -0
- package/src/secret-materialization.ts +235 -0
- package/src/secret-provenance.test.ts +388 -0
- package/src/secret-provenance.ts +475 -0
- package/src/serializer.ts +30 -0
- package/src/toml.test.ts +157 -384
- package/src/toml.ts +371 -5
- package/src/yaml.test.ts +88 -0
- package/src/yaml.ts +76 -6
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests `run-agent` (#1941 phase 1 — capability schema + registry entry;
|
|
3
|
+
* #1942 phase 2 — sprite lifecycle wiring, ./run-agent.ts). The schema/
|
|
4
|
+
* registration tests (kind, `rollbackPolicy`, COMP003-relevant registry
|
|
5
|
+
* behavior) are unchanged from phase 1; the sequencing tests below
|
|
6
|
+
* (successful turn, failed turn, rollback, unwind-on-throw, runtime command
|
|
7
|
+
* parameterization) exercise `run()`/`rollback()`'s real logic against a
|
|
8
|
+
* hand-written `SpriteActivities` fake — no HTTP/WS, no real or emulated
|
|
9
|
+
* sprite. The fly lexicon's own tests (`lexicons/fly/src/components/
|
|
10
|
+
* run-agent.test.ts`) cover the real adapter (the exec-throw reclassification
|
|
11
|
+
* in particular) against the offline `sprites-fake`; #1944 owns the full
|
|
12
|
+
* contract/conformance suite (saga-unwind restore, COMP003 refusal) beyond
|
|
13
|
+
* both.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, expect, it } from "vitest";
|
|
17
|
+
import { CapabilityRegistry } from "../capability";
|
|
18
|
+
import {
|
|
19
|
+
buildRunAgentProvenanceStatement,
|
|
20
|
+
buildRuntimeCommand,
|
|
21
|
+
computeTranscriptDigest,
|
|
22
|
+
createRunAgentCapability,
|
|
23
|
+
defaultSpriteActivities,
|
|
24
|
+
extractTranscriptDigest,
|
|
25
|
+
RUN_AGENT_BUILD_TYPE,
|
|
26
|
+
runAgentCapability,
|
|
27
|
+
SpriteActivitiesNotWiredError,
|
|
28
|
+
toRunAgentArchiveEntry,
|
|
29
|
+
type RunAgentInput,
|
|
30
|
+
type RunAgentOutput,
|
|
31
|
+
type SpriteActivities,
|
|
32
|
+
} from "./run-agent";
|
|
33
|
+
|
|
34
|
+
const ctx = { env: "dev", component: "review-agent" };
|
|
35
|
+
|
|
36
|
+
const MINIMAL_INPUT: RunAgentInput = {
|
|
37
|
+
agent: "code-reviewer",
|
|
38
|
+
task: { prompt: "Review the open PR for regressions." },
|
|
39
|
+
workspace: {},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
describe("run-agent — capability schema + registry entry (#1941)", () => {
|
|
43
|
+
it("declares kind \"run-agent\"", () => {
|
|
44
|
+
const capability = createRunAgentCapability();
|
|
45
|
+
expect(capability.kind).toBe("run-agent");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("declares rollbackPolicy: \"native\" — COMP003 never requires a noRollback opt-out for this verb", () => {
|
|
49
|
+
const capability = createRunAgentCapability();
|
|
50
|
+
expect(capability.rollbackPolicy).toBe("native");
|
|
51
|
+
// The same read `chant lint` performs when building `ctx.rollbackPolicies`
|
|
52
|
+
// (packages/core/src/cli/commands/lint.ts): `rollbackPolicy ?? (rollback ? "native" : "none-by-design")`.
|
|
53
|
+
const derived = capability.rollbackPolicy ?? (capability.rollback ? "native" : "none-by-design");
|
|
54
|
+
expect(derived).toBe("native");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("registers into a fresh CapabilityRegistry and resolves by kind", () => {
|
|
58
|
+
const registry = new CapabilityRegistry();
|
|
59
|
+
registry.register(createRunAgentCapability());
|
|
60
|
+
expect(registry.has("run-agent")).toBe(true);
|
|
61
|
+
const resolved = registry.resolve("run-agent");
|
|
62
|
+
expect(resolved.kind).toBe("run-agent");
|
|
63
|
+
expect(resolved.rollbackPolicy).toBe("native");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("exports a default instance (runAgentCapability), backed by the not-wired-yet SpriteActivities", () => {
|
|
67
|
+
expect(runAgentCapability.kind).toBe("run-agent");
|
|
68
|
+
expect(runAgentCapability.rollbackPolicy).toBe("native");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("RunAgentInput/RunAgentOutput compile against the schema described in the issue (#1941)", () => {
|
|
72
|
+
const input: RunAgentInput = {
|
|
73
|
+
agent: "code-reviewer",
|
|
74
|
+
task: {
|
|
75
|
+
prompt: "Summarize the diff.",
|
|
76
|
+
images: [{ data: "base64==", media_type: "image/png" }],
|
|
77
|
+
},
|
|
78
|
+
workspace: {
|
|
79
|
+
spriteName: "warm-review-sprite",
|
|
80
|
+
image: "sprites/base:latest",
|
|
81
|
+
checkpointComment: "pre-run",
|
|
82
|
+
},
|
|
83
|
+
sourceRef: "abc1234:packages/core",
|
|
84
|
+
};
|
|
85
|
+
const output: RunAgentOutput = {
|
|
86
|
+
spriteId: "s-1",
|
|
87
|
+
checkpointId: "v3",
|
|
88
|
+
turn: { status: "completed", exitCode: 0, startedAt: "2026-08-25T00:00:00Z", endedAt: "2026-08-25T00:01:00Z" },
|
|
89
|
+
artifacts: { files: [{ path: "report.md", digest: "sha256:" + "a".repeat(64) }], diff: "--- a\n+++ b\n" },
|
|
90
|
+
provenance: { sourceRef: "abc1234:packages/core@sha256:" + "b".repeat(64), artifactDigest: "sha256:" + "a".repeat(64) },
|
|
91
|
+
attestationRef: "review-agent/run-agent@sha256:" + "b".repeat(64),
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
expect(input.agent).toBe("code-reviewer");
|
|
95
|
+
expect(output.turn.status).toBe("completed");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("defaultSpriteActivities — phase 1's not-wired-yet placeholder", () => {
|
|
100
|
+
it("every method rejects with SpriteActivitiesNotWiredError, naming itself", async () => {
|
|
101
|
+
const sprites = defaultSpriteActivities();
|
|
102
|
+
await expect(sprites.create({ name: "s-1" })).rejects.toBeInstanceOf(SpriteActivitiesNotWiredError);
|
|
103
|
+
await expect(sprites.create({ name: "s-1" })).rejects.toThrow(/SpriteActivities\.create: not wired/);
|
|
104
|
+
await expect(sprites.checkpoint({ id: "s-1" })).rejects.toThrow(/SpriteActivities\.checkpoint: not wired/);
|
|
105
|
+
await expect(sprites.exec({ id: "s-1", cmd: "true" })).rejects.toThrow(/SpriteActivities\.exec: not wired/);
|
|
106
|
+
await expect(sprites.restore({ id: "s-1" })).rejects.toThrow(/SpriteActivities\.restore: not wired/);
|
|
107
|
+
await expect(sprites.destroy({ id: "s-1" })).rejects.toThrow(/SpriteActivities\.destroy: not wired/);
|
|
108
|
+
await expect(sprites.writeFile({ id: "s-1", path: "/x", content: "y" })).rejects.toThrow(
|
|
109
|
+
/SpriteActivities\.writeFile: not wired/,
|
|
110
|
+
);
|
|
111
|
+
await expect(sprites.readFile({ id: "s-1", path: "/x" })).rejects.toThrow(
|
|
112
|
+
/SpriteActivities\.readFile: not wired/,
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("the error message points at #1942", async () => {
|
|
117
|
+
const sprites = defaultSpriteActivities();
|
|
118
|
+
await expect(sprites.create({ name: "s-1" })).rejects.toThrow(/#1942/);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("the default runAgentCapability still throws (now on the first real activity call, not CapabilityNotImplementedError) when nothing is injected", async () => {
|
|
122
|
+
await expect(runAgentCapability.run(ctx, MINIMAL_INPUT)).rejects.toBeInstanceOf(SpriteActivitiesNotWiredError);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ── run()/rollback() sequencing (#1942) ─────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A tiny in-memory `SpriteActivities` fake: one sprite's filesystem plus a
|
|
130
|
+
* checkpoint list (id + comment + snapshot) — enough to prove checkpoint/
|
|
131
|
+
* restore is observable without any HTTP/WS (mirrors, at a smaller scale,
|
|
132
|
+
* `lexicons/fly/src/op/activities/sprites-fake.ts`'s in-process model).
|
|
133
|
+
* `execImpl` is the one piece a test overrides per scenario.
|
|
134
|
+
*
|
|
135
|
+
* `restore` mirrors the real `spriteRestore`'s resolution order
|
|
136
|
+
* (`lexicons/fly/src/op/activities/sprites.ts`): an explicit `checkpoint` id
|
|
137
|
+
* wins outright; otherwise the *newest* checkpoint carrying `comment` — so a
|
|
138
|
+
* reused sprite with two "pre-run"-commented checkpoints exercises the exact
|
|
139
|
+
* ambiguity the real backend has (regression coverage for #1942 review
|
|
140
|
+
* finding 1).
|
|
141
|
+
*/
|
|
142
|
+
function makeFakeSprites(
|
|
143
|
+
execImpl: (args: { id: string; cmd: string }) => Promise<{ stdout: string; stderr: string; exitCode: number }>,
|
|
144
|
+
): { sprites: SpriteActivities; calls: string[]; fs: Record<string, string> } {
|
|
145
|
+
const calls: string[] = [];
|
|
146
|
+
const fs: Record<string, string> = {};
|
|
147
|
+
const checkpoints: Array<{ id: string; comment: string; snapshot: Record<string, string> }> = [];
|
|
148
|
+
|
|
149
|
+
const sprites: SpriteActivities = {
|
|
150
|
+
async create(args) {
|
|
151
|
+
calls.push(`create:${args.name}`);
|
|
152
|
+
return { id: args.name, url: `fake://${args.name}` };
|
|
153
|
+
},
|
|
154
|
+
async checkpoint(args) {
|
|
155
|
+
const comment = args.comment ?? "";
|
|
156
|
+
const id = `v${checkpoints.length + 1}`;
|
|
157
|
+
calls.push(`checkpoint:${comment}`);
|
|
158
|
+
checkpoints.push({ id, comment, snapshot: { ...fs } });
|
|
159
|
+
return { checkpointId: id };
|
|
160
|
+
},
|
|
161
|
+
async exec(args) {
|
|
162
|
+
calls.push(`exec:${args.cmd}`);
|
|
163
|
+
return execImpl(args);
|
|
164
|
+
},
|
|
165
|
+
async restore(args) {
|
|
166
|
+
let entry: { id: string; comment: string; snapshot: Record<string, string> } | undefined;
|
|
167
|
+
if (args.checkpoint !== undefined) {
|
|
168
|
+
calls.push(`restore:${args.id}:checkpoint=${args.checkpoint}`);
|
|
169
|
+
entry = checkpoints.find((c) => c.id === args.checkpoint);
|
|
170
|
+
if (!entry) throw new Error(`no checkpoint "${args.checkpoint}" for sprite ${args.id}`);
|
|
171
|
+
} else {
|
|
172
|
+
const comment = args.comment ?? "";
|
|
173
|
+
calls.push(`restore:${args.id}:comment=${comment}`);
|
|
174
|
+
// Newest matching comment — mirrors `pickCheckpointByComment`.
|
|
175
|
+
entry = [...checkpoints].reverse().find((c) => c.comment === comment);
|
|
176
|
+
if (!entry) throw new Error(`no checkpoint for comment "${comment}"`);
|
|
177
|
+
}
|
|
178
|
+
for (const key of Object.keys(fs)) delete fs[key];
|
|
179
|
+
Object.assign(fs, entry.snapshot);
|
|
180
|
+
},
|
|
181
|
+
async destroy(args) {
|
|
182
|
+
calls.push(`destroy:${args.id}`);
|
|
183
|
+
},
|
|
184
|
+
async writeFile(args) {
|
|
185
|
+
calls.push(`writeFile:${args.path}`);
|
|
186
|
+
fs[args.path] = args.content;
|
|
187
|
+
},
|
|
188
|
+
async readFile(args) {
|
|
189
|
+
calls.push(`readFile:${args.path}`);
|
|
190
|
+
if (!(args.path in fs)) throw new Error(`sprite ${args.id} read ${args.path}: not found`);
|
|
191
|
+
return { content: fs[args.path] };
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
return { sprites, calls, fs };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const succeed = async (): Promise<{ stdout: string; stderr: string; exitCode: number }> => ({
|
|
198
|
+
stdout: "ok\n",
|
|
199
|
+
stderr: "",
|
|
200
|
+
exitCode: 0,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("run() — successful turn (#1942)", () => {
|
|
204
|
+
it("sequences create -> checkpoint -> writeFile -> exec -> readFile -> destroy, and reports status \"completed\"", async () => {
|
|
205
|
+
const { sprites, calls, fs } = makeFakeSprites(async () => {
|
|
206
|
+
fs["/work/output"] = "ok\n";
|
|
207
|
+
return succeed();
|
|
208
|
+
});
|
|
209
|
+
const capability = createRunAgentCapability(sprites);
|
|
210
|
+
|
|
211
|
+
const output = await capability.run(ctx, MINIMAL_INPUT);
|
|
212
|
+
|
|
213
|
+
expect(output.turn.status).toBe("completed");
|
|
214
|
+
expect(output.turn.exitCode).toBe(0);
|
|
215
|
+
expect(output.spriteId).toBeTruthy();
|
|
216
|
+
expect(output.checkpointId).toBe("v1");
|
|
217
|
+
expect(output.artifacts.files).toEqual([
|
|
218
|
+
{ path: "/work/output", digest: expect.stringMatching(/^sha256:[0-9a-f]{64}$/) },
|
|
219
|
+
]);
|
|
220
|
+
// #1943: no input.sourceRef supplied -> provenance.sourceRef is the bare
|
|
221
|
+
// transcript digest, no folding.
|
|
222
|
+
expect(output.provenance.sourceRef).toMatch(/^sha256:[0-9a-f]{64}$/);
|
|
223
|
+
expect(output.provenance).toEqual({ sourceRef: output.provenance.sourceRef, artifactDigest: output.artifacts.files[0]?.digest });
|
|
224
|
+
expect(output.attestationRef).toBe(`${ctx.component}/run-agent@${output.provenance.sourceRef}`);
|
|
225
|
+
|
|
226
|
+
// Order matters: writeFile stages the prompt before exec runs, destroy is last.
|
|
227
|
+
expect(calls[0]).toMatch(/^create:/);
|
|
228
|
+
expect(calls[1]).toMatch(/^checkpoint:pre-run$/);
|
|
229
|
+
expect(calls[2]).toBe("writeFile:/work/prompt");
|
|
230
|
+
expect(calls[3]).toMatch(/^exec:/);
|
|
231
|
+
expect(calls[4]).toBe("readFile:/work/output");
|
|
232
|
+
expect(calls[5]).toMatch(/^destroy:/);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("folds input.sourceRef with the transcript digest via '@' (#1943)", async () => {
|
|
236
|
+
const { sprites } = makeFakeSprites(succeed);
|
|
237
|
+
const capability = createRunAgentCapability(sprites);
|
|
238
|
+
const output = await capability.run(ctx, { ...MINIMAL_INPUT, sourceRef: "abc123:packages/core" });
|
|
239
|
+
expect(output.provenance.sourceRef).toMatch(/^abc123:packages\/core@sha256:[0-9a-f]{64}$/);
|
|
240
|
+
expect(extractTranscriptDigest(output.provenance.sourceRef)).toMatch(/^sha256:[0-9a-f]{64}$/);
|
|
241
|
+
expect(output.attestationRef).toBe(`${ctx.component}/run-agent@${extractTranscriptDigest(output.provenance.sourceRef)}`);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("skips create() and destroy() when reusing an existing sprite (workspace.spriteName)", async () => {
|
|
245
|
+
const { sprites, calls } = makeFakeSprites(succeed);
|
|
246
|
+
const capability = createRunAgentCapability(sprites);
|
|
247
|
+
const input: RunAgentInput = { ...MINIMAL_INPUT, workspace: { spriteName: "warm-sprite" } };
|
|
248
|
+
|
|
249
|
+
const output = await capability.run(ctx, input);
|
|
250
|
+
|
|
251
|
+
expect(output.spriteId).toBe("warm-sprite");
|
|
252
|
+
expect(calls.some((c) => c.startsWith("create:"))).toBe(false);
|
|
253
|
+
expect(calls.some((c) => c.startsWith("destroy:"))).toBe(false);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe("run() — failed turn: non-zero exit surfaces as status \"failed\", never a throw (#1942, the exec-throw finding)", () => {
|
|
258
|
+
it("resolves (does not reject) with turn.status \"failed\" and the parsed exit code", async () => {
|
|
259
|
+
const { sprites, calls } = makeFakeSprites(async () => ({ stdout: "", stderr: "boom\n", exitCode: 3 }));
|
|
260
|
+
const capability = createRunAgentCapability(sprites);
|
|
261
|
+
|
|
262
|
+
const output = await capability.run(ctx, MINIMAL_INPUT);
|
|
263
|
+
|
|
264
|
+
expect(output.turn.status).toBe("failed");
|
|
265
|
+
expect(output.turn.exitCode).toBe(3);
|
|
266
|
+
// Left alive — no destroy on a failed turn, so a caller can inspect/rollback it.
|
|
267
|
+
expect(calls.some((c) => c.startsWith("destroy:"))).toBe(false);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("still collects artifacts.files when the failing turn wrote output before exiting non-zero", async () => {
|
|
271
|
+
const { sprites, fs } = makeFakeSprites(async () => {
|
|
272
|
+
fs["/work/output"] = "partial-corrupt";
|
|
273
|
+
return { stdout: "", stderr: "", exitCode: 1 };
|
|
274
|
+
});
|
|
275
|
+
const capability = createRunAgentCapability(sprites);
|
|
276
|
+
const output = await capability.run(ctx, MINIMAL_INPUT);
|
|
277
|
+
expect(output.artifacts.files).toEqual([
|
|
278
|
+
{ path: "/work/output", digest: expect.stringMatching(/^sha256:[0-9a-f]{64}$/) },
|
|
279
|
+
]);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("leaves artifacts.files empty when the failing turn never wrote output at all", async () => {
|
|
283
|
+
const { sprites } = makeFakeSprites(async () => ({ stdout: "", stderr: "", exitCode: 1 }));
|
|
284
|
+
const capability = createRunAgentCapability(sprites);
|
|
285
|
+
const output = await capability.run(ctx, MINIMAL_INPUT);
|
|
286
|
+
expect(output.artifacts.files).toEqual([]);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe("collectArtifacts — only a \"not found\" read means \"no artifact\"; a genuine infra failure propagates (#1942 review finding 2)", () => {
|
|
291
|
+
it("branch A: readFile rejecting with the sprite-fs \"not found\" shape resolves to empty artifacts.files, not a run() rejection", async () => {
|
|
292
|
+
const { sprites } = makeFakeSprites(succeed); // never writes /work/output — readFile hits the fake's "not found" branch
|
|
293
|
+
const capability = createRunAgentCapability(sprites);
|
|
294
|
+
await expect(capability.run(ctx, MINIMAL_INPUT)).resolves.toMatchObject({ artifacts: { files: [] } });
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("branch B: readFile rejecting with a genuine infra-failure shape propagates as a run() rejection, not swallowed into empty artifacts.files", async () => {
|
|
298
|
+
const { sprites } = makeFakeSprites(succeed);
|
|
299
|
+
sprites.readFile = async () => {
|
|
300
|
+
throw new Error("sprite s-1 read /work/output failed (500): backend unavailable");
|
|
301
|
+
};
|
|
302
|
+
const capability = createRunAgentCapability(sprites);
|
|
303
|
+
await expect(capability.run(ctx, MINIMAL_INPUT)).rejects.toThrow(/failed \(500\): backend unavailable/);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("branch B (variant): a non-Error/non-string rejection from readFile still propagates rather than being treated as \"not found\"", async () => {
|
|
307
|
+
const { sprites } = makeFakeSprites(succeed);
|
|
308
|
+
sprites.readFile = async () => {
|
|
309
|
+
throw new Error("ECONNRESET");
|
|
310
|
+
};
|
|
311
|
+
const capability = createRunAgentCapability(sprites);
|
|
312
|
+
await expect(capability.run(ctx, MINIMAL_INPUT)).rejects.toThrow(/ECONNRESET/);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe("rollback() restores the pre-run checkpoint (#1942)", () => {
|
|
317
|
+
it("restores the sprite's filesystem to its pre-run state after a failed turn", async () => {
|
|
318
|
+
const { sprites, fs } = makeFakeSprites(async () => {
|
|
319
|
+
fs["/work/output"] = "corrupted";
|
|
320
|
+
return { stdout: "", stderr: "it broke", exitCode: 1 };
|
|
321
|
+
});
|
|
322
|
+
const capability = createRunAgentCapability(sprites);
|
|
323
|
+
|
|
324
|
+
const output = await capability.run(ctx, MINIMAL_INPUT);
|
|
325
|
+
expect(output.turn.status).toBe("failed");
|
|
326
|
+
expect(fs["/work/output"]).toBe("corrupted");
|
|
327
|
+
expect(fs["/work/prompt"]).toBe(MINIMAL_INPUT.task.prompt); // staged after the checkpoint
|
|
328
|
+
|
|
329
|
+
await capability.rollback?.(ctx, MINIMAL_INPUT);
|
|
330
|
+
|
|
331
|
+
// The pre-run checkpoint predates both the staged prompt and the exec's
|
|
332
|
+
// output — restore rewinds the sprite to before either existed.
|
|
333
|
+
expect(fs["/work/output"]).toBeUndefined();
|
|
334
|
+
expect(fs["/work/prompt"]).toBeUndefined();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("resolves the sprite id AND restores by the exact recorded checkpoint id (not comment) from the WeakMap keyed by the exact input object run() was called with", async () => {
|
|
338
|
+
const { sprites, calls } = makeFakeSprites(async () => ({ stdout: "", stderr: "", exitCode: 1 }));
|
|
339
|
+
const capability = createRunAgentCapability(sprites);
|
|
340
|
+
const output = await capability.run(ctx, MINIMAL_INPUT);
|
|
341
|
+
|
|
342
|
+
await capability.rollback?.(ctx, MINIMAL_INPUT);
|
|
343
|
+
|
|
344
|
+
expect(calls.at(-1)).toBe(`restore:${output.spriteId}:checkpoint=${output.checkpointId}`);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("falls back to workspace.spriteName + comment-based restore when there is no in-process run() record for this input", async () => {
|
|
348
|
+
const { sprites, calls } = makeFakeSprites(succeed);
|
|
349
|
+
// Simulate an earlier process/instance having already checkpointed this
|
|
350
|
+
// warm sprite — otherwise there is nothing for a bare restore to find,
|
|
351
|
+
// regardless of which sprite id it resolves.
|
|
352
|
+
await sprites.checkpoint({ id: "warm-sprite", comment: "pre-run" });
|
|
353
|
+
calls.length = 0;
|
|
354
|
+
const capability = createRunAgentCapability(sprites);
|
|
355
|
+
const input: RunAgentInput = { ...MINIMAL_INPUT, workspace: { spriteName: "warm-sprite" } };
|
|
356
|
+
|
|
357
|
+
// No run() call at all — a fresh capability instance calling rollback()
|
|
358
|
+
// directly, the same as a caller resuming against an already-created sprite.
|
|
359
|
+
// With no in-process record, there is no known checkpoint id, so this is
|
|
360
|
+
// the one case that still falls back to comment-based resolution.
|
|
361
|
+
await capability.rollback?.(ctx, input);
|
|
362
|
+
|
|
363
|
+
expect(calls).toEqual([`restore:warm-sprite:comment=pre-run`]);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("degrades to an explicit no-op (does not throw, does not call restore) when neither an in-process record nor workspace.spriteName is available (#1942 review finding 3)", async () => {
|
|
367
|
+
const { sprites, calls } = makeFakeSprites(succeed);
|
|
368
|
+
const capability = createRunAgentCapability(sprites);
|
|
369
|
+
await expect(capability.rollback?.(ctx, MINIMAL_INPUT)).resolves.toBeUndefined();
|
|
370
|
+
expect(calls.some((c) => c.startsWith("restore:"))).toBe(false);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("honors a custom workspace.checkpointComment for the checkpoint call, but still restores by the exact recorded checkpoint id, not the comment", async () => {
|
|
374
|
+
const { sprites, calls, fs } = makeFakeSprites(async () => ({ stdout: "", stderr: "", exitCode: 1 }));
|
|
375
|
+
const capability = createRunAgentCapability(sprites);
|
|
376
|
+
const input: RunAgentInput = { ...MINIMAL_INPUT, workspace: { checkpointComment: "before-turn" } };
|
|
377
|
+
|
|
378
|
+
const output = await capability.run(ctx, input);
|
|
379
|
+
await capability.rollback?.(ctx, input);
|
|
380
|
+
|
|
381
|
+
expect(calls).toContain("checkpoint:before-turn");
|
|
382
|
+
expect(calls).toContain(`restore:${output.spriteId}:checkpoint=${output.checkpointId}`);
|
|
383
|
+
expect(fs["/work/prompt"]).toBeUndefined(); // restore actually rewound the fs
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("rollback of run 1 restores run 1's own checkpoint, not run 2's — two runs sharing the default \"pre-run\" comment on a reused sprite (regression, #1942 review finding 1)", async () => {
|
|
387
|
+
const { sprites, calls, fs } = makeFakeSprites(async (args) => {
|
|
388
|
+
// Each exec mutates a run-specific marker so the two runs' post-states
|
|
389
|
+
// are distinguishable.
|
|
390
|
+
fs[`/work/marker-${args.cmd}`] = "done";
|
|
391
|
+
return { stdout: "", stderr: "", exitCode: 0 };
|
|
392
|
+
});
|
|
393
|
+
const capability = createRunAgentCapability(sprites);
|
|
394
|
+
const input1: RunAgentInput = {
|
|
395
|
+
agent: "run1-marker",
|
|
396
|
+
task: { prompt: "first" },
|
|
397
|
+
workspace: { spriteName: "warm-sprite" },
|
|
398
|
+
};
|
|
399
|
+
const input2: RunAgentInput = {
|
|
400
|
+
agent: "run2-marker",
|
|
401
|
+
task: { prompt: "second" },
|
|
402
|
+
workspace: { spriteName: "warm-sprite" },
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const output1 = await capability.run(ctx, input1);
|
|
406
|
+
const output2 = await capability.run(ctx, input2);
|
|
407
|
+
|
|
408
|
+
// Both checkpoints share the default "pre-run" comment, and run2's
|
|
409
|
+
// checkpoint (taken after run1's mutation) is the newer of the two — the
|
|
410
|
+
// exact ambiguity a comment-based restore could not resolve correctly.
|
|
411
|
+
expect(output1.checkpointId).not.toBe(output2.checkpointId);
|
|
412
|
+
expect(fs["/work/marker-run1-marker"]).toBe("done");
|
|
413
|
+
expect(fs["/work/marker-run2-marker"]).toBe("done");
|
|
414
|
+
|
|
415
|
+
await capability.rollback?.(ctx, input1);
|
|
416
|
+
|
|
417
|
+
// A comment-based ("pre-run") restore would have resolved to run2's
|
|
418
|
+
// newer checkpoint, which already contains run1's marker — the bug this
|
|
419
|
+
// regression test guards against. The fix restores run1's own checkpoint
|
|
420
|
+
// (taken before run1 ran at all), so run1's marker must be gone too.
|
|
421
|
+
expect(calls.at(-1)).toBe(`restore:warm-sprite:checkpoint=${output1.checkpointId}`);
|
|
422
|
+
expect(fs["/work/marker-run1-marker"]).toBeUndefined();
|
|
423
|
+
expect(fs["/work/marker-run2-marker"]).toBeUndefined();
|
|
424
|
+
expect(fs["/work/prompt"]).toBeUndefined();
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe("unwind-on-throw: a genuine infra failure propagates as a run() rejection (#1942)", () => {
|
|
429
|
+
it("a rejecting checkpoint() call fails run() outright — no synthesized turn.status", async () => {
|
|
430
|
+
const { sprites, calls } = makeFakeSprites(succeed);
|
|
431
|
+
sprites.checkpoint = async () => {
|
|
432
|
+
throw new Error("sprite backend unreachable");
|
|
433
|
+
};
|
|
434
|
+
const capability = createRunAgentCapability(sprites);
|
|
435
|
+
|
|
436
|
+
await expect(capability.run(ctx, MINIMAL_INPUT)).rejects.toThrow(/sprite backend unreachable/);
|
|
437
|
+
// exec/writeFile never ran — the failure happened before them.
|
|
438
|
+
expect(calls.some((c) => c.startsWith("exec:"))).toBe(false);
|
|
439
|
+
expect(calls.some((c) => c.startsWith("writeFile:"))).toBe(false);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it("a rejecting exec() call (a real transport failure, not an ordinary non-zero exit) fails run() outright", async () => {
|
|
443
|
+
const { sprites } = makeFakeSprites(async () => {
|
|
444
|
+
throw new Error("sprite exec aborted");
|
|
445
|
+
});
|
|
446
|
+
const capability = createRunAgentCapability(sprites);
|
|
447
|
+
await expect(capability.run(ctx, MINIMAL_INPUT)).rejects.toThrow(/sprite exec aborted/);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it("rollback() still restores after a post-checkpoint throw, mirroring driver.ts's in-process saga unwind", async () => {
|
|
451
|
+
const { sprites, fs } = makeFakeSprites(succeed);
|
|
452
|
+
const realWriteFile = sprites.writeFile.bind(sprites);
|
|
453
|
+
let failNext = true;
|
|
454
|
+
sprites.writeFile = async (args, signal) => {
|
|
455
|
+
if (failNext) {
|
|
456
|
+
failNext = false;
|
|
457
|
+
throw new Error("sprite fs unreachable");
|
|
458
|
+
}
|
|
459
|
+
return realWriteFile(args, signal);
|
|
460
|
+
};
|
|
461
|
+
const capability = createRunAgentCapability(sprites);
|
|
462
|
+
|
|
463
|
+
await expect(capability.run(ctx, MINIMAL_INPUT)).rejects.toThrow(/sprite fs unreachable/);
|
|
464
|
+
// driver.ts's saga unwind would call rollback() with this exact input next.
|
|
465
|
+
await capability.rollback?.(ctx, MINIMAL_INPUT);
|
|
466
|
+
expect(fs["/work/prompt"]).toBeUndefined(); // checkpoint predates the write that never landed
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
describe("buildRuntimeCommand — runtime parameterization (#1942 acceptance: not hard-coded to one runtime)", () => {
|
|
471
|
+
it("dispatches a distinct, real invocation per known Agent.runtime value", () => {
|
|
472
|
+
const claude = buildRuntimeCommand("claude");
|
|
473
|
+
const codex = buildRuntimeCommand("codex");
|
|
474
|
+
const gemini = buildRuntimeCommand("gemini");
|
|
475
|
+
const opencode = buildRuntimeCommand("opencode");
|
|
476
|
+
|
|
477
|
+
expect(claude).toContain("claude");
|
|
478
|
+
expect(codex).toContain("codex");
|
|
479
|
+
expect(gemini).toContain("gemini");
|
|
480
|
+
expect(opencode).toContain("opencode");
|
|
481
|
+
|
|
482
|
+
const commands = [claude, codex, gemini, opencode];
|
|
483
|
+
expect(new Set(commands).size).toBe(commands.length); // all four differ
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it("reads the staged prompt file rather than embedding the prompt text literally", () => {
|
|
487
|
+
expect(buildRuntimeCommand("claude")).toContain("/work/prompt");
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("passes an unrecognized agent value through verbatim (the offline-fake escape hatch)", () => {
|
|
491
|
+
expect(buildRuntimeCommand("./risky.sh")).toBe("./risky.sh");
|
|
492
|
+
expect(buildRuntimeCommand("true")).toBe("true");
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it("run() builds the exec command from input.agent, proving the runtime is not hard-coded", async () => {
|
|
496
|
+
const { sprites, calls } = makeFakeSprites(succeed);
|
|
497
|
+
const capability = createRunAgentCapability(sprites);
|
|
498
|
+
await capability.run(ctx, { ...MINIMAL_INPUT, agent: "codex" });
|
|
499
|
+
expect(calls.find((c) => c.startsWith("exec:"))).toBe(`exec:${buildRuntimeCommand("codex")}`);
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// ── #1943: transcript hash + attestation interop ────────────────────────────
|
|
504
|
+
|
|
505
|
+
describe("computeTranscriptDigest — deterministic hash basis (#1943)", () => {
|
|
506
|
+
type TranscriptParams = Parameters<typeof computeTranscriptDigest>[0];
|
|
507
|
+
const turn = { status: "completed" as const, exitCode: 0, startedAt: "2026-08-25T00:00:00Z", endedAt: "2026-08-25T00:01:00Z" };
|
|
508
|
+
const base: TranscriptParams = {
|
|
509
|
+
agent: "code-reviewer",
|
|
510
|
+
prompt: "Review the diff.",
|
|
511
|
+
turn,
|
|
512
|
+
stdout: "ok\n",
|
|
513
|
+
stderr: "",
|
|
514
|
+
artifacts: { files: [{ path: "/work/output", digest: "sha256:" + "a".repeat(64) }] },
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
it("is a sha256:<hex> digest", () => {
|
|
518
|
+
expect(computeTranscriptDigest(base)).toMatch(/^sha256:[0-9a-f]{64}$/);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it("same turn -> same sourceRef: fully deterministic across repeated calls", () => {
|
|
522
|
+
expect(computeTranscriptDigest(base)).toBe(computeTranscriptDigest({ ...base }));
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it("is insensitive to wall-clock time — startedAt/endedAt are excluded from the basis by design", () => {
|
|
526
|
+
const laterTurn = { ...turn, startedAt: "2099-01-01T00:00:00Z", endedAt: "2099-01-01T00:01:00Z" };
|
|
527
|
+
expect(computeTranscriptDigest({ ...base, turn: laterTurn })).toBe(computeTranscriptDigest(base));
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it("is insensitive to artifacts.files collection order (sorted by path before hashing)", () => {
|
|
531
|
+
const twoFiles = {
|
|
532
|
+
...base,
|
|
533
|
+
artifacts: {
|
|
534
|
+
files: [
|
|
535
|
+
{ path: "b.txt", digest: "sha256:" + "b".repeat(64) },
|
|
536
|
+
{ path: "a.txt", digest: "sha256:" + "a".repeat(64) },
|
|
537
|
+
],
|
|
538
|
+
},
|
|
539
|
+
};
|
|
540
|
+
const reordered = { ...twoFiles, artifacts: { files: [...twoFiles.artifacts.files].reverse() } };
|
|
541
|
+
expect(computeTranscriptDigest(twoFiles)).toBe(computeTranscriptDigest(reordered));
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
const mutations: Array<[string, (b: TranscriptParams) => TranscriptParams]> = [
|
|
545
|
+
["agent", (b) => ({ ...b, agent: "other-agent" })],
|
|
546
|
+
["prompt", (b) => ({ ...b, prompt: b.prompt + " " })],
|
|
547
|
+
["turn.status", (b) => ({ ...b, turn: { ...b.turn, status: "failed" as const } })],
|
|
548
|
+
["turn.exitCode", (b) => ({ ...b, turn: { ...b.turn, exitCode: 1 } })],
|
|
549
|
+
["stdout", (b) => ({ ...b, stdout: b.stdout + "x" })],
|
|
550
|
+
["stderr", (b) => ({ ...b, stderr: "warning" })],
|
|
551
|
+
["artifacts.files[0].digest", (b) => ({ ...b, artifacts: { files: [{ ...b.artifacts.files[0]!, digest: "sha256:" + "f".repeat(64) }] } })],
|
|
552
|
+
["artifacts.diff", (b) => ({ ...b, artifacts: { ...b.artifacts, diff: "--- a\n+++ b\n" } })],
|
|
553
|
+
];
|
|
554
|
+
|
|
555
|
+
it.each(mutations)("any single-byte change (%s) changes the digest — tamper-evident by construction", (_label, mutate) => {
|
|
556
|
+
expect(computeTranscriptDigest(mutate(base))).not.toBe(computeTranscriptDigest(base));
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it("is sensitive to image bytes, even though images are folded in as their own digest, not embedded verbatim", () => {
|
|
560
|
+
const withImage = { ...base, images: [{ data: "aGVsbG8=", media_type: "image/png" as const }] };
|
|
561
|
+
const withDifferentImage = { ...base, images: [{ data: "d29ybGQ=", media_type: "image/png" as const }] };
|
|
562
|
+
expect(computeTranscriptDigest(withImage)).not.toBe(computeTranscriptDigest(withDifferentImage));
|
|
563
|
+
expect(computeTranscriptDigest(withImage)).not.toBe(computeTranscriptDigest(base));
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
describe("run() — sourceRef/attestationRef determinism and tamper-evidence end to end (#1943)", () => {
|
|
568
|
+
it("two runs with byte-identical scripted turns produce the same sourceRef/attestationRef", async () => {
|
|
569
|
+
const { sprites: sprites1 } = makeFakeSprites(succeed);
|
|
570
|
+
const { sprites: sprites2 } = makeFakeSprites(succeed);
|
|
571
|
+
|
|
572
|
+
const cap1 = createRunAgentCapability(sprites1);
|
|
573
|
+
const cap2 = createRunAgentCapability(sprites2);
|
|
574
|
+
|
|
575
|
+
const out1 = await cap1.run(ctx, MINIMAL_INPUT);
|
|
576
|
+
const out2 = await cap2.run(ctx, MINIMAL_INPUT);
|
|
577
|
+
|
|
578
|
+
expect(out1.provenance.sourceRef).toBe(out2.provenance.sourceRef);
|
|
579
|
+
expect(out1.attestationRef).toBe(out2.attestationRef);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
it("a differing prompt (one byte) produces a different sourceRef/attestationRef", async () => {
|
|
583
|
+
const { sprites: sprites1 } = makeFakeSprites(succeed);
|
|
584
|
+
const { sprites: sprites2 } = makeFakeSprites(succeed);
|
|
585
|
+
const cap1 = createRunAgentCapability(sprites1);
|
|
586
|
+
const cap2 = createRunAgentCapability(sprites2);
|
|
587
|
+
|
|
588
|
+
const out1 = await cap1.run(ctx, MINIMAL_INPUT);
|
|
589
|
+
const out2 = await cap2.run(ctx, { ...MINIMAL_INPUT, task: { prompt: MINIMAL_INPUT.task.prompt + "!" } });
|
|
590
|
+
|
|
591
|
+
expect(out1.provenance.sourceRef).not.toBe(out2.provenance.sourceRef);
|
|
592
|
+
expect(out1.attestationRef).not.toBe(out2.attestationRef);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it("a differing collected artifact (the produced output byte differs) produces a different sourceRef/attestationRef", async () => {
|
|
596
|
+
const fake1 = makeFakeSprites(async () => succeed());
|
|
597
|
+
const fake2 = makeFakeSprites(async () => succeed());
|
|
598
|
+
// Pre-seed each fake's fs so its own exec (which never itself writes
|
|
599
|
+
// /work/output — `succeed()` is a no-op body) collects a distinct artifact.
|
|
600
|
+
fake1.fs["/work/output"] = "result-a";
|
|
601
|
+
fake2.fs["/work/output"] = "result-b";
|
|
602
|
+
const cap1 = createRunAgentCapability(fake1.sprites);
|
|
603
|
+
const cap2 = createRunAgentCapability(fake2.sprites);
|
|
604
|
+
|
|
605
|
+
const out1 = await cap1.run(ctx, MINIMAL_INPUT);
|
|
606
|
+
const out2 = await cap2.run(ctx, MINIMAL_INPUT);
|
|
607
|
+
|
|
608
|
+
expect(out1.artifacts.files[0]?.digest).not.toBe(out2.artifacts.files[0]?.digest);
|
|
609
|
+
expect(out1.provenance.sourceRef).not.toBe(out2.provenance.sourceRef);
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
describe("toRunAgentArchiveEntry — folds RunAgentOutput into a BuildArchiveEntry (#1943 design point 2)", () => {
|
|
614
|
+
const output: RunAgentOutput = {
|
|
615
|
+
spriteId: "s-1",
|
|
616
|
+
checkpointId: "v1",
|
|
617
|
+
turn: { status: "completed", exitCode: 0, startedAt: "2026-08-25T00:00:00Z", endedAt: "2026-08-25T00:01:00Z" },
|
|
618
|
+
artifacts: { files: [{ path: "/work/output", digest: "sha256:" + "a".repeat(64) }] },
|
|
619
|
+
provenance: { sourceRef: "sha256:" + "b".repeat(64), artifactDigest: "sha256:" + "a".repeat(64) },
|
|
620
|
+
attestationRef: "review-agent/run-agent@sha256:" + "b".repeat(64),
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
it("returns an asset-kind entry, content-addressed by the transcript digest", () => {
|
|
624
|
+
const entry = toRunAgentArchiveEntry("review-agent", output);
|
|
625
|
+
expect(entry.kind).toBe("asset");
|
|
626
|
+
expect(entry.digest).toBe("sha256:" + "b".repeat(64));
|
|
627
|
+
expect(entry.path).toBe("run-agent/review-agent/s-1-turn.json");
|
|
628
|
+
expect(entry.provenance).toEqual(output.provenance);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it("throws when provenance.sourceRef carries no transcript digest (extractTranscriptDigest's guard)", () => {
|
|
632
|
+
const malformed: RunAgentOutput = { ...output, provenance: { sourceRef: "not-a-digest", artifactDigest: output.provenance.artifactDigest } };
|
|
633
|
+
expect(() => toRunAgentArchiveEntry("review-agent", malformed)).toThrow(/does not end in a "sha256:<hex>"/);
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
describe("buildRunAgentProvenanceStatement — SLSA statement over a turn (#1943 design points 3+4)", () => {
|
|
638
|
+
const input: RunAgentInput = {
|
|
639
|
+
agent: "code-reviewer",
|
|
640
|
+
task: { prompt: "Review the diff." },
|
|
641
|
+
workspace: {},
|
|
642
|
+
};
|
|
643
|
+
const output: RunAgentOutput = {
|
|
644
|
+
spriteId: "s-1",
|
|
645
|
+
checkpointId: "v1",
|
|
646
|
+
turn: { status: "completed", exitCode: 0, startedAt: "2026-08-25T00:00:00Z", endedAt: "2026-08-25T00:01:00Z" },
|
|
647
|
+
artifacts: { files: [{ path: "/work/output", digest: "sha256:" + "a".repeat(64) }] },
|
|
648
|
+
provenance: { sourceRef: "sha256:" + "b".repeat(64), artifactDigest: "sha256:" + "a".repeat(64) },
|
|
649
|
+
attestationRef: "review-agent/run-agent@sha256:" + "b".repeat(64),
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
it("reuses predicateType https://slsa.dev/provenance/v1 — no minted run-agent-specific predicate type", () => {
|
|
653
|
+
const statement = buildRunAgentProvenanceStatement(input, output, "https://github.com/actions/runner");
|
|
654
|
+
expect(statement.predicateType).toBe("https://slsa.dev/provenance/v1");
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it("sets buildType to RUN_AGENT_BUILD_TYPE, distinguishing a turn from a container build", () => {
|
|
658
|
+
const statement = buildRunAgentProvenanceStatement(input, output, "https://github.com/actions/runner");
|
|
659
|
+
expect(statement.predicate.buildDefinition.buildType).toBe(RUN_AGENT_BUILD_TYPE);
|
|
660
|
+
expect(RUN_AGENT_BUILD_TYPE).toBe("https://chant.dev/agent-turn/v1");
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it("the subject is output.attestationRef, digest-qualified", () => {
|
|
664
|
+
const statement = buildRunAgentProvenanceStatement(input, output, "builder");
|
|
665
|
+
expect(statement.subject).toEqual([{ name: output.attestationRef, digest: { sha256: "b".repeat(64) } }]);
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it("folds input.agent into externalParameters and the imperative facts into internalParameters", () => {
|
|
669
|
+
const statement = buildRunAgentProvenanceStatement(input, output, "builder");
|
|
670
|
+
expect(statement.predicate.buildDefinition.externalParameters).toMatchObject({ agent: "code-reviewer" });
|
|
671
|
+
expect(statement.predicate.buildDefinition.internalParameters).toMatchObject({
|
|
672
|
+
spriteId: "s-1",
|
|
673
|
+
checkpointId: "v1",
|
|
674
|
+
turnStatus: "completed",
|
|
675
|
+
turnExitCode: 0,
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it("defaults finishedOn from turn.endedAt", () => {
|
|
680
|
+
const statement = buildRunAgentProvenanceStatement(input, output, "builder");
|
|
681
|
+
expect(statement.predicate.runDetails.metadata?.finishedOn).toBe(output.turn.endedAt);
|
|
682
|
+
});
|
|
683
|
+
});
|