@stigmer/runner 3.14.1 → 3.15.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__/execution-record-fixture.d.ts +47 -0
- package/dist/__test-utils__/execution-record-fixture.js +68 -0
- package/dist/__test-utils__/execution-record-fixture.js.map +1 -0
- package/dist/__test-utils__/harness-contract/types.d.ts +166 -0
- package/dist/__test-utils__/harness-contract/types.js +49 -0
- package/dist/__test-utils__/harness-contract/types.js.map +1 -0
- package/dist/__test-utils__/hermetic-activity.d.ts +28 -5
- package/dist/__test-utils__/hermetic-activity.js +23 -7
- package/dist/__test-utils__/hermetic-activity.js.map +1 -1
- package/dist/__test-utils__/mock-workspace.d.ts +15 -0
- package/dist/__test-utils__/mock-workspace.js +21 -0
- package/dist/__test-utils__/mock-workspace.js.map +1 -0
- package/dist/__test-utils__/model-registry-fixture.d.ts +56 -0
- package/dist/__test-utils__/model-registry-fixture.js +66 -0
- package/dist/__test-utils__/model-registry-fixture.js.map +1 -0
- package/dist/__test-utils__/turn-input-fixture.d.ts +60 -0
- package/dist/__test-utils__/turn-input-fixture.js +118 -0
- package/dist/__test-utils__/turn-input-fixture.js.map +1 -0
- package/dist/activities/execute-cursor/__test-utils__/contract-subject.d.ts +107 -0
- package/dist/activities/execute-cursor/__test-utils__/contract-subject.js +344 -0
- package/dist/activities/execute-cursor/__test-utils__/contract-subject.js.map +1 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +20 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +58 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js +2 -32
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.d.ts +38 -62
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +47 -121
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/scripted-agent.d.ts +17 -1
- package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js +27 -4
- package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.d.ts +52 -11
- package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js +66 -7
- package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js.map +1 -1
- package/dist/activities/execute-cursor/adapter.d.ts +55 -0
- package/dist/activities/execute-cursor/adapter.js +119 -0
- package/dist/activities/execute-cursor/adapter.js.map +1 -0
- package/dist/activities/execute-cursor/approval-state.d.ts +2 -2
- package/dist/activities/execute-cursor/approval-state.js.map +1 -1
- package/dist/activities/execute-cursor/capture-flow.d.ts +15 -30
- package/dist/activities/execute-cursor/capture-flow.js +17 -26
- package/dist/activities/execute-cursor/capture-flow.js.map +1 -1
- package/dist/activities/execute-cursor/cursor-capabilities.d.ts +23 -0
- package/dist/activities/execute-cursor/cursor-capabilities.js +33 -0
- package/dist/activities/execute-cursor/cursor-capabilities.js.map +1 -0
- package/dist/activities/execute-cursor/cursor-mcp-config.d.ts +2 -2
- package/dist/activities/execute-cursor/cursor-mcp-config.js.map +1 -1
- package/dist/activities/execute-cursor/fetch-interceptor.d.ts +15 -25
- package/dist/activities/execute-cursor/fetch-interceptor.js +39 -33
- package/dist/activities/execute-cursor/fetch-interceptor.js.map +1 -1
- package/dist/activities/execute-cursor/http2-interceptor.d.ts +21 -17
- package/dist/activities/execute-cursor/http2-interceptor.js +38 -31
- package/dist/activities/execute-cursor/http2-interceptor.js.map +1 -1
- package/dist/activities/execute-cursor/message-translator.d.ts +28 -19
- package/dist/activities/execute-cursor/message-translator.js +76 -27
- package/dist/activities/execute-cursor/message-translator.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +180 -11
- package/dist/activities/execute-cursor/prompt-builder.js +171 -0
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/rejection-capture.d.ts +3 -6
- package/dist/activities/execute-cursor/rejection-capture.js +5 -7
- package/dist/activities/execute-cursor/rejection-capture.js.map +1 -1
- package/dist/activities/execute-cursor/session-lifecycle.d.ts +1 -4
- package/dist/activities/execute-cursor/session-lifecycle.js.map +1 -1
- package/dist/activities/execute-cursor/turn-boundary.d.ts +1 -1
- package/dist/activities/execute-cursor/turn-settle.d.ts +49 -0
- package/dist/activities/execute-cursor/turn-settle.js +507 -0
- package/dist/activities/execute-cursor/turn-settle.js.map +1 -0
- package/dist/activities/execute-cursor/turn-setup.d.ts +173 -0
- package/dist/activities/execute-cursor/turn-setup.js +496 -0
- package/dist/activities/execute-cursor/turn-setup.js.map +1 -0
- package/dist/activities/execute-cursor/turn-stream.d.ts +41 -71
- package/dist/activities/execute-cursor/turn-stream.js +70 -135
- package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
- package/dist/activities/execute-cursor/turn.d.ts +43 -0
- package/dist/activities/execute-cursor/turn.js +222 -0
- package/dist/activities/execute-cursor/turn.js.map +1 -0
- package/dist/activities/execute-cursor/usage-pricing.d.ts +38 -0
- package/dist/activities/execute-cursor/usage-pricing.js +50 -0
- package/dist/activities/execute-cursor/usage-pricing.js.map +1 -0
- package/dist/activities/execute-deep-agent/approval-file-change.d.ts +1 -1
- package/dist/activities/execute-deep-agent/approval-file-change.js +1 -1
- package/dist/activities/execute-deep-agent/environment.js +1 -1
- package/dist/activities/execute-deep-agent/environment.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.js +3 -2
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/hydrate-workflow-execution.js +1 -1
- package/dist/activities/hydrate-workflow-execution.js.map +1 -1
- package/dist/config.d.ts +15 -0
- package/dist/config.js.map +1 -1
- package/dist/harness/capabilities.d.ts +16 -0
- package/dist/harness/capabilities.js +1 -0
- package/dist/harness/capabilities.js.map +1 -1
- package/dist/harness/persist-chokepoint.d.ts +74 -0
- package/dist/harness/persist-chokepoint.js +88 -0
- package/dist/harness/persist-chokepoint.js.map +1 -0
- package/dist/harness/registry.d.ts +44 -5
- package/dist/harness/registry.js +61 -5
- package/dist/harness/registry.js.map +1 -1
- package/dist/harness/run-turn.d.ts +53 -0
- package/dist/harness/run-turn.js +528 -0
- package/dist/harness/run-turn.js.map +1 -0
- package/dist/harness/stop-controller.d.ts +50 -0
- package/dist/harness/stop-controller.js +62 -0
- package/dist/harness/stop-controller.js.map +1 -0
- package/dist/harness/terminal-table.d.ts +141 -0
- package/dist/harness/terminal-table.js +239 -0
- package/dist/harness/terminal-table.js.map +1 -0
- package/dist/harness/turn-context.d.ts +523 -0
- package/dist/harness/turn-context.js +739 -0
- package/dist/harness/turn-context.js.map +1 -0
- package/dist/harness/types.d.ts +232 -52
- package/dist/harness/types.js +17 -8
- package/dist/harness/types.js.map +1 -1
- package/dist/harness/usage-accumulator.d.ts +68 -0
- package/dist/harness/usage-accumulator.js +101 -0
- package/dist/harness/usage-accumulator.js.map +1 -0
- package/dist/harness-adapters.d.ts +27 -0
- package/dist/harness-adapters.js +28 -0
- package/dist/harness-adapters.js.map +1 -0
- package/dist/main.js.map +1 -1
- package/dist/runner-manager.js +64 -48
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.d.ts +5 -4
- package/dist/runner.js +65 -64
- package/dist/runner.js.map +1 -1
- package/dist/shared/approval-policy.d.ts +1 -1
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/{activities/execute-cursor → shared}/attachment-resolver.d.ts +2 -2
- package/dist/{activities/execute-cursor → shared}/attachment-resolver.js +5 -5
- package/dist/shared/attachment-resolver.js.map +1 -0
- package/dist/{activities/execute-cursor → shared}/blueprint-resolver.d.ts +7 -5
- package/dist/{activities/execute-cursor → shared}/blueprint-resolver.js +3 -63
- package/dist/shared/blueprint-resolver.js.map +1 -0
- package/dist/shared/channel-attachment.d.ts +1 -1
- package/dist/shared/channel-attachment.js.map +1 -1
- package/dist/{activities/execute-cursor → shared}/cost-guard.d.ts +5 -1
- package/dist/{activities/execute-cursor → shared}/cost-guard.js +5 -1
- package/dist/shared/cost-guard.js.map +1 -0
- package/dist/{activities/execute-cursor → shared}/env-resolver.d.ts +1 -1
- package/dist/shared/env-resolver.js.map +1 -0
- package/dist/{activities/execute-cursor → shared}/exact-apply.d.ts +1 -1
- package/dist/{activities/execute-cursor → shared}/exact-apply.js +6 -6
- package/dist/shared/exact-apply.js.map +1 -0
- package/dist/shared/execution-context.d.ts +35 -0
- package/dist/shared/execution-context.js +38 -0
- package/dist/shared/execution-context.js.map +1 -0
- package/dist/{activities/execute-cursor → shared}/extract-structured-output.d.ts +5 -2
- package/dist/{activities/execute-cursor → shared}/extract-structured-output.js +9 -6
- package/dist/shared/extract-structured-output.js.map +1 -0
- package/dist/shared/filereview/capture.d.ts +1 -1
- package/dist/shared/filereview/capture.js +1 -1
- package/dist/shared/heartbeat.d.ts +13 -11
- package/dist/shared/heartbeat.js +14 -29
- package/dist/shared/heartbeat.js.map +1 -1
- package/dist/{activities/execute-cursor → shared}/persist-decision.d.ts +5 -2
- package/dist/{activities/execute-cursor → shared}/persist-decision.js +4 -1
- package/dist/shared/persist-decision.js.map +1 -0
- package/dist/{activities/execute-cursor → shared}/skill-resolver.d.ts +16 -3
- package/dist/{activities/execute-cursor → shared}/skill-resolver.js +12 -4
- package/dist/shared/skill-resolver.js.map +1 -0
- package/dist/shared/subagent-rows.d.ts +21 -0
- package/dist/shared/subagent-rows.js +34 -0
- package/dist/shared/subagent-rows.js.map +1 -0
- package/dist/shared/worker-shutdown.d.ts +43 -16
- package/dist/shared/worker-shutdown.js +28 -13
- package/dist/shared/worker-shutdown.js.map +1 -1
- package/dist/shared/workspace/platform-dir.d.ts +1 -1
- package/dist/shared/workspace/platform-dir.js +1 -1
- package/dist/shared/workspace/session-provision.d.ts +54 -0
- package/dist/{activities/execute-cursor/workspace-provision.js → shared/workspace/session-provision.js} +15 -12
- package/dist/shared/workspace/session-provision.js.map +1 -0
- package/package.json +4 -4
- package/src/__test-utils__/__tests__/harness-contract-self-check.test.ts +110 -14
- package/src/__test-utils__/execution-record-fixture.ts +104 -0
- package/src/__test-utils__/harness-boot-order-child.ts +58 -0
- package/src/__test-utils__/harness-contract/contract.ts +128 -83
- package/src/__test-utils__/harness-contract/recording-sink.ts +15 -4
- package/src/__test-utils__/harness-contract/runtime-contract.ts +666 -0
- package/src/__test-utils__/harness-contract/scripted-adapter.ts +92 -32
- package/src/__test-utils__/harness-contract/types.ts +85 -9
- package/src/__test-utils__/hermetic-activity.ts +33 -8
- package/src/__test-utils__/model-registry-fixture.ts +72 -0
- package/src/__test-utils__/module-specifiers.ts +82 -0
- package/src/__test-utils__/turn-input-fixture.ts +155 -0
- package/src/__tests__/harness-boot-order.test.ts +77 -0
- package/src/__tests__/harness-contract.test.ts +7 -5
- package/src/__tests__/runner-manager.test.ts +18 -0
- package/src/activities/execute-cursor/__test-utils__/__tests__/scripted-double.test.ts +102 -0
- package/src/activities/execute-cursor/__test-utils__/contract-subject.ts +396 -0
- package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +66 -0
- package/src/activities/execute-cursor/__test-utils__/gateway-substrate.ts +2 -43
- package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +65 -145
- package/src/activities/execute-cursor/__test-utils__/scripted-agent.ts +30 -4
- package/src/activities/execute-cursor/__test-utils__/scripted-sdk.ts +96 -8
- package/src/activities/execute-cursor/__tests__/adapter-is-temporal-free.test.ts +45 -0
- package/src/activities/execute-cursor/__tests__/approval-decisions-agree.test.ts +90 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +7 -8
- package/src/activities/execute-cursor/__tests__/capture-flow.test.ts +28 -1
- package/src/activities/execute-cursor/__tests__/deny-gate-exact-apply.test.ts +1 -1
- package/src/activities/execute-cursor/__tests__/fetch-interceptor.test.ts +64 -6
- package/src/activities/execute-cursor/__tests__/hermetic/deny-and-retry.test.ts +1 -1
- package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +1 -1
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/cost-cap.status.json +53 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/pause.status.json +1 -6
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/platform-stop.status.json +56 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/recovery-transport-timeout.status.json +48 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/resolution-error.status.json +18 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/run-cancelled.status.json +36 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/run-error-non-retryable.status.json +37 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/sdk-error-at-create.status.json +38 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/stall.status.json +59 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/unattributed-hook-block.status.json +79 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/worker-shutdown.status.json +2 -7
- package/src/activities/execute-cursor/__tests__/hermetic/harness-contract.test.ts +170 -0
- package/src/activities/execute-cursor/__tests__/hermetic/pause-vs-shutdown.test.ts +22 -18
- package/src/activities/execute-cursor/__tests__/hermetic/plain-turn.test.ts +1 -1
- package/src/activities/execute-cursor/__tests__/hermetic/recovery-fresh-agent.test.ts +1 -1
- package/src/activities/execute-cursor/__tests__/hermetic/run-wait-arms.test.ts +286 -0
- package/src/activities/execute-cursor/__tests__/hermetic/stream-self-stop-arms.test.ts +332 -0
- package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +193 -0
- package/src/activities/execute-cursor/__tests__/hermetic/tool-call.test.ts +1 -1
- package/src/activities/execute-cursor/__tests__/hermetic/unattributed-hook-block.test.ts +185 -0
- package/src/activities/execute-cursor/__tests__/hermetic/workspace-lock-timeout.test.ts +151 -0
- package/src/activities/execute-cursor/__tests__/http2-interceptor.test.ts +65 -35
- package/src/activities/execute-cursor/__tests__/message-translator.test.ts +0 -40
- package/src/activities/execute-cursor/__tests__/same-identity-reproposal.test.ts +171 -0
- package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +118 -243
- package/src/activities/execute-cursor/__tests__/usage-pricing.test.ts +84 -0
- package/src/activities/execute-cursor/adapter.ts +138 -0
- package/src/activities/execute-cursor/approval-state.ts +2 -2
- package/src/activities/execute-cursor/capture-flow.ts +18 -41
- package/src/activities/execute-cursor/cursor-capabilities.ts +35 -0
- package/src/activities/execute-cursor/cursor-mcp-config.ts +2 -2
- package/src/activities/execute-cursor/fetch-interceptor.ts +48 -45
- package/src/activities/execute-cursor/http2-interceptor.ts +47 -34
- package/src/activities/execute-cursor/message-translator.ts +90 -45
- package/src/activities/execute-cursor/prompt-builder.ts +327 -12
- package/src/activities/execute-cursor/rejection-capture.ts +5 -12
- package/src/activities/execute-cursor/session-lifecycle.ts +1 -5
- package/src/activities/execute-cursor/turn-boundary.ts +1 -1
- package/src/activities/execute-cursor/turn-settle.ts +601 -0
- package/src/activities/execute-cursor/turn-setup.ts +645 -0
- package/src/activities/execute-cursor/turn-stream.ts +84 -210
- package/src/activities/execute-cursor/turn.ts +240 -0
- package/src/activities/execute-cursor/usage-pricing.ts +67 -0
- package/src/activities/execute-deep-agent/approval-file-change.ts +1 -1
- package/src/activities/execute-deep-agent/environment.ts +1 -1
- package/src/activities/execute-deep-agent/setup.ts +3 -2
- package/src/activities/hydrate-workflow-execution.ts +1 -1
- package/src/config.ts +13 -0
- package/src/harness/__tests__/import-direction.test.ts +168 -0
- package/src/harness/__tests__/persist-chokepoint.test.ts +146 -0
- package/src/harness/__tests__/registry.test.ts +1 -0
- package/src/harness/__tests__/run-turn.test.ts +165 -0
- package/src/harness/__tests__/stop-controller.test.ts +49 -0
- package/src/harness/__tests__/turn-context.test.ts +178 -0
- package/src/harness/__tests__/usage-accumulator.test.ts +83 -0
- package/src/harness/capabilities.ts +17 -0
- package/src/harness/persist-chokepoint.ts +112 -0
- package/src/harness/registry.ts +79 -5
- package/src/harness/run-turn.ts +618 -0
- package/src/harness/stop-controller.ts +83 -0
- package/src/harness/terminal-table.ts +276 -0
- package/src/harness/turn-context.ts +1028 -0
- package/src/harness/types.ts +248 -53
- package/src/harness/usage-accumulator.ts +122 -0
- package/src/harness-adapters.ts +30 -0
- package/src/main.ts +1 -1
- package/src/runner-manager.ts +68 -61
- package/src/runner.ts +67 -78
- package/src/shared/__tests__/attachment-naming.test.ts +1 -1
- package/src/{activities/execute-cursor → shared}/__tests__/attachment-resolver.test.ts +6 -4
- package/src/{activities/execute-cursor → shared}/__tests__/cost-guard.test.ts +2 -1
- package/src/{activities/execute-cursor → shared}/__tests__/exact-apply.test.ts +3 -3
- package/src/{activities/execute-cursor → shared}/__tests__/extract-structured-output.test.ts +8 -8
- package/src/{activities/execute-cursor → shared}/__tests__/persist-decision.test.ts +4 -2
- package/src/{activities/execute-cursor → shared}/__tests__/skill-resolver.test.ts +1 -1
- package/src/shared/__tests__/subagent-rows.test.ts +34 -0
- package/src/shared/__tests__/worker-shutdown.test.ts +44 -39
- package/src/shared/approval-policy.ts +1 -1
- package/src/{activities/execute-cursor → shared}/attachment-resolver.ts +6 -6
- package/src/{activities/execute-cursor → shared}/blueprint-resolver.ts +7 -78
- package/src/shared/channel-attachment.ts +1 -1
- package/src/{activities/execute-cursor → shared}/cost-guard.ts +5 -1
- package/src/{activities/execute-cursor → shared}/env-resolver.ts +1 -1
- package/src/{activities/execute-cursor → shared}/exact-apply.ts +7 -7
- package/src/shared/execution-context.ts +45 -0
- package/src/{activities/execute-cursor → shared}/extract-structured-output.ts +10 -7
- package/src/shared/filereview/capture.ts +1 -1
- package/src/shared/heartbeat.ts +13 -33
- package/src/{activities/execute-cursor → shared}/persist-decision.ts +5 -2
- package/src/{activities/execute-cursor → shared}/skill-resolver.ts +20 -6
- package/src/shared/subagent-rows.ts +39 -0
- package/src/shared/worker-shutdown.ts +55 -25
- package/src/{activities/execute-cursor/__tests__/workspace-provision.test.ts → shared/workspace/__tests__/session-provision.test.ts} +25 -28
- package/src/shared/workspace/platform-dir.ts +1 -1
- package/src/{activities/execute-cursor/workspace-provision.ts → shared/workspace/session-provision.ts} +29 -16
- package/dist/activities/execute-cursor/attachment-resolver.js.map +0 -1
- package/dist/activities/execute-cursor/blueprint-resolver.js.map +0 -1
- package/dist/activities/execute-cursor/cost-guard.js.map +0 -1
- package/dist/activities/execute-cursor/env-resolver.js.map +0 -1
- package/dist/activities/execute-cursor/exact-apply.js.map +0 -1
- package/dist/activities/execute-cursor/extract-structured-output.js.map +0 -1
- package/dist/activities/execute-cursor/index.d.ts +0 -209
- package/dist/activities/execute-cursor/index.js +0 -2345
- package/dist/activities/execute-cursor/index.js.map +0 -1
- package/dist/activities/execute-cursor/persist-decision.js.map +0 -1
- package/dist/activities/execute-cursor/skill-resolver.js.map +0 -1
- package/dist/activities/execute-cursor/usage-accumulator.d.ts +0 -89
- package/dist/activities/execute-cursor/usage-accumulator.js +0 -121
- package/dist/activities/execute-cursor/usage-accumulator.js.map +0 -1
- package/dist/activities/execute-cursor/workspace-provision.d.ts +0 -42
- package/dist/activities/execute-cursor/workspace-provision.js.map +0 -1
- package/src/activities/execute-cursor/__tests__/usage-accumulator.test.ts +0 -164
- package/src/activities/execute-cursor/index.ts +0 -2876
- package/src/activities/execute-cursor/usage-accumulator.ts +0 -169
- /package/dist/{activities/execute-cursor → shared}/env-resolver.js +0 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hermetic goldens: the THROWN-ERROR ARMS of the outer catch — an error that
|
|
3
|
+
* escapes a setup phase, before any stream ran, becomes EXECUTION_FAILED with
|
|
4
|
+
* two system messages and the activity RETURNS.
|
|
5
|
+
*
|
|
6
|
+
* The outer `catch` of the activity has two arms for an error that is not a
|
|
7
|
+
* Temporal cancellation and not a pause: a thrown `CursorSdkError` (routed
|
|
8
|
+
* through the SAME classifier as the `run.wait()` error path, so its
|
|
9
|
+
* category and diagnostics survive) and everything else (formatted by
|
|
10
|
+
* `describeExecutionError` as `[<ErrorType>] <message>`). Both write the same
|
|
11
|
+
* pair of system messages — the generic "Internal system error occurred..."
|
|
12
|
+
* and an "Error details: ..." carrying the classified or formatted text — and
|
|
13
|
+
* both RETURN the failed status rather than throwing: a Temporal retry would
|
|
14
|
+
* replay the same setup against the same fault.
|
|
15
|
+
*
|
|
16
|
+
* Two scenarios, one per arm, each failing at a DIFFERENT setup phase:
|
|
17
|
+
*
|
|
18
|
+
* 1. `Agent.create` throws a `CursorSdkError` (Phase 8: the SDK refuses the
|
|
19
|
+
* key). The error propagates unretried — `resolveAgentWithTransportRecovery`
|
|
20
|
+
* retries only a TimeoutError — and the SDK arm classifies it `auth`.
|
|
21
|
+
* Golden `goldens/sdk-error-at-create.status.json`.
|
|
22
|
+
*
|
|
23
|
+
* 2. `client.getAgent` rejects with a plain `Error` (Phase 2: a control-plane
|
|
24
|
+
* fault while resolving the blueprint). The generic arm formats it with
|
|
25
|
+
* the error's constructor name. Golden `goldens/resolution-error.status.json`.
|
|
26
|
+
*
|
|
27
|
+
* Both fail BEFORE the first status persist, so the ONLY phase the control
|
|
28
|
+
* plane ever sees for these executions is EXECUTION_FAILED — `persistedPhases`
|
|
29
|
+
* is `[FAILED]`, not `[IN_PROGRESS, FAILED]`.
|
|
30
|
+
*
|
|
31
|
+
* Regenerate ONLY after a deliberate behavior change:
|
|
32
|
+
* npx vitest run src/activities/execute-cursor/__tests__/hermetic -u
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
36
|
+
import { toJson } from "@bufbuild/protobuf";
|
|
37
|
+
import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
38
|
+
import { ExecutionPhase, MessageType } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
39
|
+
|
|
40
|
+
vi.mock("@cursor/sdk", async () =>
|
|
41
|
+
(await import("../../__test-utils__/scripted-sdk.js")).scriptedCursorSdkModule(),
|
|
42
|
+
);
|
|
43
|
+
vi.mock("../../../../client/stigmer-client.js", async () =>
|
|
44
|
+
(await import("../../../../__test-utils__/hermetic-activity.js")).hermeticStigmerClientModule(),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
import {
|
|
48
|
+
ScriptedClock,
|
|
49
|
+
createHermeticEnvironment,
|
|
50
|
+
type HermeticEnvironment,
|
|
51
|
+
} from "../../../../__test-utils__/hermetic-activity.js";
|
|
52
|
+
import { _parkedAgentCountForTests } from "../../agent-session-cache.js";
|
|
53
|
+
import { ScriptedCursorAgent } from "../../__test-utils__/scripted-agent.js";
|
|
54
|
+
import { ScriptedCursorSdkError } from "../../__test-utils__/scripted-sdk.js";
|
|
55
|
+
import {
|
|
56
|
+
SDK_CATALOG,
|
|
57
|
+
beginCursorScenario,
|
|
58
|
+
cursorExecutionRecord,
|
|
59
|
+
runCursorTurn,
|
|
60
|
+
} from "../../__test-utils__/hermetic-cursor.js";
|
|
61
|
+
import { stubRegistryFetch } from "../../../../__test-utils__/model-registry-fixture.js";
|
|
62
|
+
|
|
63
|
+
const USER_MESSAGE = "Draft the release notes.";
|
|
64
|
+
const GENERIC_SYSTEM_MESSAGE = "Internal system error occurred. Please contact support if this issue persists.";
|
|
65
|
+
// The SDK refusing the key: the shape the real CursorSdkError carries for a 401.
|
|
66
|
+
const SDK_ERROR_MESSAGE = "Cursor API rejected the request: invalid API key";
|
|
67
|
+
// A control-plane fault with deliberately neutral text — no provider, network
|
|
68
|
+
// or billing word in it — so the GENERIC arm is what the golden pins, not a
|
|
69
|
+
// specific diagnosis (`describeExecutionError` classifies known prose first).
|
|
70
|
+
const CONTROL_PLANE_FAULT = "hermetic fault injected at getAgent";
|
|
71
|
+
|
|
72
|
+
function systemMessages(status: { messages: { type: MessageType; content: string }[] }): string[] {
|
|
73
|
+
return status.messages.filter((m) => m.type === MessageType.MESSAGE_SYSTEM).map((m) => m.content);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe("ExecuteCursor hermetic — thrown-error arms of the outer catch", () => {
|
|
77
|
+
let env: HermeticEnvironment;
|
|
78
|
+
let registry: ReturnType<typeof stubRegistryFetch>;
|
|
79
|
+
const clock = new ScriptedClock();
|
|
80
|
+
|
|
81
|
+
beforeAll(() => {
|
|
82
|
+
env = createHermeticEnvironment();
|
|
83
|
+
registry = stubRegistryFetch();
|
|
84
|
+
clock.install();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
afterAll(() => {
|
|
88
|
+
clock.uninstall();
|
|
89
|
+
registry.restore();
|
|
90
|
+
env.dispose();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("a CursorSdkError thrown by Agent.create is classified and fails the turn", async () => {
|
|
94
|
+
// ── Arrange ──────────────────────────────────────────────────────────────
|
|
95
|
+
clock.reset(); // both goldens read from :00
|
|
96
|
+
// Declared so a create that unexpectedly SUCCEEDS is a loud test failure
|
|
97
|
+
// (its script has no result step) rather than a quiet pass.
|
|
98
|
+
const neverUsed = new ScriptedCursorAgent({ agentId: "agent-hermetic-never-created", turns: [] });
|
|
99
|
+
const record = cursorExecutionRecord({ message: USER_MESSAGE });
|
|
100
|
+
const scenario = beginCursorScenario({
|
|
101
|
+
env,
|
|
102
|
+
clock,
|
|
103
|
+
record,
|
|
104
|
+
sdk: {
|
|
105
|
+
agents: [neverUsed],
|
|
106
|
+
createFailure: new ScriptedCursorSdkError(SDK_ERROR_MESSAGE, {
|
|
107
|
+
code: "unauthenticated",
|
|
108
|
+
status: 401,
|
|
109
|
+
operation: "Agent.create",
|
|
110
|
+
}),
|
|
111
|
+
catalog: SDK_CATALOG,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ── Act ──────────────────────────────────────────────────────────────────
|
|
116
|
+
const invocation = await runCursorTurn(scenario);
|
|
117
|
+
|
|
118
|
+
// ── Assert: outcome and phases ───────────────────────────────────────────
|
|
119
|
+
expect(invocation.outcome.kind, "the SDK arm RETURNS the failed status").toBe("returned");
|
|
120
|
+
const slim = (invocation.outcome as { value: Record<string, unknown> }).value;
|
|
121
|
+
expect(slim.phase).toBe("EXECUTION_FAILED");
|
|
122
|
+
expect(record.persistedPhases, "nothing was persisted before the failure").toEqual([
|
|
123
|
+
ExecutionPhase.EXECUTION_FAILED,
|
|
124
|
+
]);
|
|
125
|
+
const final = record.lastFullStatus!;
|
|
126
|
+
expect(final.error).toBe(`${SDK_ERROR_MESSAGE} [category=auth, source=sdk, retryable=false]`);
|
|
127
|
+
expect(final.completedAt).not.toBe("");
|
|
128
|
+
expect(systemMessages(final)).toEqual([GENERIC_SYSTEM_MESSAGE, `Error details: ${final.error}`]);
|
|
129
|
+
|
|
130
|
+
// ── Assert: where it stopped ─────────────────────────────────────────────
|
|
131
|
+
expect(scenario.sdk.resolutions.map((r) => r.kind), "one create attempt, unretried").toEqual(["create"]);
|
|
132
|
+
expect(neverUsed.sends, "no agent ever ran").toHaveLength(0);
|
|
133
|
+
expect(record.sessionUpdates, "no harness_state_id to bind").toHaveLength(0);
|
|
134
|
+
expect(_parkedAgentCountForTests()).toBe(0);
|
|
135
|
+
|
|
136
|
+
// ── Assert: hermeticity ──────────────────────────────────────────────────
|
|
137
|
+
expect(registry.urls.every((u) => u.includes("/model-registry"))).toBe(true);
|
|
138
|
+
expect(invocation.heartbeats.length).toBeGreaterThan(0);
|
|
139
|
+
|
|
140
|
+
// ── Assert: the golden ───────────────────────────────────────────────────
|
|
141
|
+
const json = JSON.stringify(toJson(AgentExecutionStatusSchema, final), null, 2) + "\n";
|
|
142
|
+
await expect(json).toMatchFileSnapshot("./goldens/sdk-error-at-create.status.json");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("a plain Error during blueprint resolution fails the turn through the generic arm", async () => {
|
|
146
|
+
// ── Arrange ──────────────────────────────────────────────────────────────
|
|
147
|
+
clock.reset();
|
|
148
|
+
const neverUsed = new ScriptedCursorAgent({ agentId: "agent-hermetic-never-resolved", turns: [] });
|
|
149
|
+
const record = cursorExecutionRecord({ message: USER_MESSAGE });
|
|
150
|
+
const scenario = beginCursorScenario({
|
|
151
|
+
env,
|
|
152
|
+
clock,
|
|
153
|
+
record,
|
|
154
|
+
sdk: { agents: [neverUsed], catalog: SDK_CATALOG },
|
|
155
|
+
clientOverrides: {
|
|
156
|
+
getAgent: vi.fn(async () => {
|
|
157
|
+
throw new Error(CONTROL_PLANE_FAULT);
|
|
158
|
+
}),
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// ── Act ──────────────────────────────────────────────────────────────────
|
|
163
|
+
const invocation = await runCursorTurn(scenario);
|
|
164
|
+
|
|
165
|
+
// ── Assert: outcome and phases ───────────────────────────────────────────
|
|
166
|
+
expect(invocation.outcome.kind, "the generic arm RETURNS the failed status").toBe("returned");
|
|
167
|
+
const slim = (invocation.outcome as { value: Record<string, unknown> }).value;
|
|
168
|
+
expect(slim.phase).toBe("EXECUTION_FAILED");
|
|
169
|
+
expect(record.persistedPhases).toEqual([ExecutionPhase.EXECUTION_FAILED]);
|
|
170
|
+
const final = record.lastFullStatus!;
|
|
171
|
+
expect(final.error).toBe(`Execution failed: [Error] ${CONTROL_PLANE_FAULT}`);
|
|
172
|
+
expect(final.completedAt).not.toBe("");
|
|
173
|
+
expect(systemMessages(final)).toEqual([
|
|
174
|
+
GENERIC_SYSTEM_MESSAGE,
|
|
175
|
+
`Error details: [Error] ${CONTROL_PLANE_FAULT}`,
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
// ── Assert: where it stopped ─────────────────────────────────────────────
|
|
179
|
+
expect(record.setupProgress, "Phase 2 was reported, nothing after it").toEqual([
|
|
180
|
+
"Fetching execution",
|
|
181
|
+
"Resolving agent blueprint",
|
|
182
|
+
]);
|
|
183
|
+
expect(scenario.sdk.resolutions, "the SDK was never reached").toHaveLength(0);
|
|
184
|
+
expect(record.sessionUpdates).toHaveLength(0);
|
|
185
|
+
|
|
186
|
+
// ── Assert: hermeticity ──────────────────────────────────────────────────
|
|
187
|
+
expect(registry.urls.every((u) => u.includes("/model-registry"))).toBe(true);
|
|
188
|
+
|
|
189
|
+
// ── Assert: the golden ───────────────────────────────────────────────────
|
|
190
|
+
const json = JSON.stringify(toJson(AgentExecutionStatusSchema, final), null, 2) + "\n";
|
|
191
|
+
await expect(json).toMatchFileSnapshot("./goldens/resolution-error.status.json");
|
|
192
|
+
});
|
|
193
|
+
});
|
|
@@ -44,8 +44,8 @@ import {
|
|
|
44
44
|
beginCursorScenario,
|
|
45
45
|
cursorExecutionRecord,
|
|
46
46
|
runCursorTurn,
|
|
47
|
-
stubRegistryFetch,
|
|
48
47
|
} from "../../__test-utils__/hermetic-cursor.js";
|
|
48
|
+
import { stubRegistryFetch } from "../../../../__test-utils__/model-registry-fixture.js";
|
|
49
49
|
|
|
50
50
|
const AGENT_ID = "agent-hermetic-tool-0001";
|
|
51
51
|
const RUN_ID = "run-hermetic-tool-0001";
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hermetic golden: UNATTRIBUTED HOOK BLOCK (issue #205) — a tool blocked by a
|
|
3
|
+
* hook Stigmer does not own fails the turn with a diagnosable reason, never a
|
|
4
|
+
* silent completion.
|
|
5
|
+
*
|
|
6
|
+
* Invariant pinned: Cursor runs EVERY hook in the workspace's `.cursor/hooks.json`
|
|
7
|
+
* and a deny from any of them blocks the tool. Stigmer's own hook records every
|
|
8
|
+
* deny it issues to the denial ledger, so a FAILED tool call carrying Cursor's
|
|
9
|
+
* hook-block error text with NO ledger entry was blocked by a FOREIGN hook. No
|
|
10
|
+
* approval can unblock it (an approval grants a token only OUR hook reads), so
|
|
11
|
+
* the turn boundary reports it and the activity RETURNS EXECUTION_FAILED with an
|
|
12
|
+
* error that names the blocked tool and the foreign hook command the gate
|
|
13
|
+
* install found beside ours. The golden (`goldens/unattributed-hook-block.status.json`)
|
|
14
|
+
* pins that copy.
|
|
15
|
+
*
|
|
16
|
+
* What the scenario ALSO pins, because it is the point of #173 and the reason
|
|
17
|
+
* #205 exists at all: the gate MERGES into the user's `hooks.json` (their hook
|
|
18
|
+
* survives the turn and is reported as the culprit) and the teardown restores
|
|
19
|
+
* their file byte for byte.
|
|
20
|
+
*
|
|
21
|
+
* Engine disposition on this arm, today: the handle is `close()`d, not parked
|
|
22
|
+
* (index.ts `enterUnattributedHookBlockFailure`).
|
|
23
|
+
*
|
|
24
|
+
* Parent phase rows exercised beyond the earlier scenarios: the gate install's
|
|
25
|
+
* foreign-hook detection (`workspace-setup.ts` `mergeHooks`); the FAILED
|
|
26
|
+
* tool-call row built from a single `status: "error"` event; the boundary's
|
|
27
|
+
* `detectUnattributedHookBlocks` pass over an EMPTY ledger; the
|
|
28
|
+
* `enterUnattributedHookBlockFailure` terminal; the `finally`'s hooks.json restore.
|
|
29
|
+
*
|
|
30
|
+
* Regenerate ONLY after a deliberate behavior change:
|
|
31
|
+
* npx vitest run src/activities/execute-cursor/__tests__/hermetic -u
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
35
|
+
import { join } from "node:path";
|
|
36
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
37
|
+
import { toJson } from "@bufbuild/protobuf";
|
|
38
|
+
import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
39
|
+
import {
|
|
40
|
+
ExecutionPhase,
|
|
41
|
+
MessageType,
|
|
42
|
+
ToolCallStatus,
|
|
43
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
44
|
+
|
|
45
|
+
vi.mock("@cursor/sdk", async () =>
|
|
46
|
+
(await import("../../__test-utils__/scripted-sdk.js")).scriptedCursorSdkModule(),
|
|
47
|
+
);
|
|
48
|
+
vi.mock("../../../../client/stigmer-client.js", async () =>
|
|
49
|
+
(await import("../../../../__test-utils__/hermetic-activity.js")).hermeticStigmerClientModule(),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
import {
|
|
53
|
+
ScriptedClock,
|
|
54
|
+
createHermeticEnvironment,
|
|
55
|
+
type HermeticEnvironment,
|
|
56
|
+
} from "../../../../__test-utils__/hermetic-activity.js";
|
|
57
|
+
import { _parkedAgentCountForTests } from "../../agent-session-cache.js";
|
|
58
|
+
import { ScriptedCursorAgent, sdkEvents, step } from "../../__test-utils__/scripted-agent.js";
|
|
59
|
+
import {
|
|
60
|
+
SDK_CATALOG,
|
|
61
|
+
beginCursorScenario,
|
|
62
|
+
cursorExecutionRecord,
|
|
63
|
+
runCursorTurn,
|
|
64
|
+
sessionWorkspaceDir,
|
|
65
|
+
} from "../../__test-utils__/hermetic-cursor.js";
|
|
66
|
+
import { stubRegistryFetch } from "../../../../__test-utils__/model-registry-fixture.js";
|
|
67
|
+
|
|
68
|
+
const AGENT_ID = "agent-hermetic-hookblock-0001";
|
|
69
|
+
const RUN_ID = "run-hermetic-hookblock-0001";
|
|
70
|
+
const CALL_ID = "call-hermetic-hookblock-0001";
|
|
71
|
+
const USER_MESSAGE = "Clean the build directory.";
|
|
72
|
+
const SHELL_ARGS = { command: "rm -rf build" };
|
|
73
|
+
/** The user's own gating hook — a team policy script the merge must preserve. */
|
|
74
|
+
const FOREIGN_HOOK_COMMAND = "./scripts/team-policy-hook.sh";
|
|
75
|
+
/**
|
|
76
|
+
* The user's hooks.json exactly as they wrote it (formatting included): the
|
|
77
|
+
* teardown must hand back these bytes, not a re-serialization.
|
|
78
|
+
*/
|
|
79
|
+
const USER_HOOKS_JSON =
|
|
80
|
+
JSON.stringify({ version: 1, hooks: { preToolUse: [{ command: FOREIGN_HOOK_COMMAND }] } }, null, 2) + "\n";
|
|
81
|
+
/**
|
|
82
|
+
* The error text Cursor stamps on a hook-blocked tool call
|
|
83
|
+
* (`message-translator.ts` HOOK_BLOCK_ERROR_MARKERS). The SDK has no structured
|
|
84
|
+
* "denied by hook" signal; this text is the only stream-side trace.
|
|
85
|
+
*/
|
|
86
|
+
const HOOK_BLOCK_ERROR = "blocked by a hook";
|
|
87
|
+
const FINAL_TEXT = "I could not clean the build directory: the shell command was blocked.";
|
|
88
|
+
|
|
89
|
+
describe("ExecuteCursor hermetic — unattributed hook block (#205)", () => {
|
|
90
|
+
let env: HermeticEnvironment;
|
|
91
|
+
let registry: ReturnType<typeof stubRegistryFetch>;
|
|
92
|
+
const clock = new ScriptedClock();
|
|
93
|
+
|
|
94
|
+
beforeAll(() => {
|
|
95
|
+
env = createHermeticEnvironment();
|
|
96
|
+
registry = stubRegistryFetch();
|
|
97
|
+
clock.install();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
afterAll(() => {
|
|
101
|
+
clock.uninstall();
|
|
102
|
+
registry.restore();
|
|
103
|
+
env.dispose();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("fails the turn naming the blocked tool and the foreign hook, and restores the user's hooks.json", async () => {
|
|
107
|
+
// ── Arrange: the user's own gating hook is already in the workspace ─────
|
|
108
|
+
const workspaceDir = sessionWorkspaceDir(env);
|
|
109
|
+
const hooksJsonPath = join(workspaceDir, ".cursor", "hooks.json");
|
|
110
|
+
mkdirSync(join(workspaceDir, ".cursor"), { recursive: true });
|
|
111
|
+
writeFileSync(hooksJsonPath, USER_HOOKS_JSON, "utf-8");
|
|
112
|
+
|
|
113
|
+
const ev = sdkEvents(AGENT_ID, RUN_ID);
|
|
114
|
+
const agent = new ScriptedCursorAgent({
|
|
115
|
+
agentId: AGENT_ID,
|
|
116
|
+
runIds: [RUN_ID],
|
|
117
|
+
observeStep: () => clock.tick(),
|
|
118
|
+
turns: [
|
|
119
|
+
[
|
|
120
|
+
step.event(ev.init()),
|
|
121
|
+
step.event(ev.assistant("Removing the build directory.")),
|
|
122
|
+
// The foreign hook denied the shell; Cursor reports the call as
|
|
123
|
+
// failed with its generic hook-block text. Our ledger never saw it.
|
|
124
|
+
step.event(ev.toolCall(CALL_ID, "shell", "error", SHELL_ARGS, HOOK_BLOCK_ERROR)),
|
|
125
|
+
step.event(ev.assistant(FINAL_TEXT)),
|
|
126
|
+
step.turnEnded({ inputTokens: 1_500, outputTokens: 40 }),
|
|
127
|
+
step.finished({ result: FINAL_TEXT }),
|
|
128
|
+
],
|
|
129
|
+
],
|
|
130
|
+
});
|
|
131
|
+
const record = cursorExecutionRecord({ message: USER_MESSAGE });
|
|
132
|
+
const scenario = beginCursorScenario({ env, clock, record, sdk: { agents: [agent], catalog: SDK_CATALOG } });
|
|
133
|
+
|
|
134
|
+
// ── Act ──────────────────────────────────────────────────────────────────
|
|
135
|
+
const invocation = await runCursorTurn(scenario);
|
|
136
|
+
|
|
137
|
+
// ── Assert: outcome and phases ───────────────────────────────────────────
|
|
138
|
+
expect(invocation.outcome.kind, "an unattributed block RETURNS a terminal status").toBe("returned");
|
|
139
|
+
const slim = (invocation.outcome as { value: Record<string, unknown> }).value;
|
|
140
|
+
expect(slim.phase).toBe("EXECUTION_FAILED");
|
|
141
|
+
expect(record.persistedPhases).toEqual([
|
|
142
|
+
ExecutionPhase.EXECUTION_IN_PROGRESS,
|
|
143
|
+
ExecutionPhase.EXECUTION_FAILED,
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
// ── Assert: the diagnosable reason ───────────────────────────────────────
|
|
147
|
+
const final = record.lastFullStatus!;
|
|
148
|
+
expect(final.error).toBe(
|
|
149
|
+
`A Cursor hook outside Stigmer's approval gate blocked tool(s): shell.` +
|
|
150
|
+
` The workspace's .cursor/hooks.json registers hook(s) outside Stigmer's control ` +
|
|
151
|
+
`[${FOREIGN_HOOK_COMMAND}], which most likely denied it.` +
|
|
152
|
+
` Stigmer cannot request approval on a foreign hook's behalf — remove or adjust ` +
|
|
153
|
+
`the hook in .cursor/hooks.json and retry.`,
|
|
154
|
+
);
|
|
155
|
+
expect(final.completedAt, "a failed turn is complete").not.toBe("");
|
|
156
|
+
expect(
|
|
157
|
+
final.messages.filter((m) => m.type === MessageType.MESSAGE_SYSTEM).map((m) => m.content),
|
|
158
|
+
).toEqual([`Execution failed: ${final.error}`]);
|
|
159
|
+
|
|
160
|
+
// The blocked row stays FAILED with Cursor's text — never silently
|
|
161
|
+
// completed, never collapsed into a Stigmer gate row.
|
|
162
|
+
const rows = record.toolCalls();
|
|
163
|
+
expect(rows).toHaveLength(1);
|
|
164
|
+
expect(rows[0].status).toBe(ToolCallStatus.TOOL_CALL_FAILED);
|
|
165
|
+
expect(rows[0].error).toBe(HOOK_BLOCK_ERROR);
|
|
166
|
+
expect(record.waitingToolCalls(), "no approval is pending — none could unblock it").toHaveLength(0);
|
|
167
|
+
|
|
168
|
+
// ── Assert: engine disposition (today's) ─────────────────────────────────
|
|
169
|
+
expect(agent.closeCalls, "the handle is closed on this failure").toBe(1);
|
|
170
|
+
expect(_parkedAgentCountForTests(), "and not parked").toBe(0);
|
|
171
|
+
expect(agent.runs[0].cancelCalls, "the run completed on its own — nothing cancelled it").toHaveLength(0);
|
|
172
|
+
expect(record.sessionUpdates, "the one harness_state_id bind").toHaveLength(1);
|
|
173
|
+
|
|
174
|
+
// ── Assert: the user's hooks.json survived the turn byte for byte (#173) ─
|
|
175
|
+
expect(readFileSync(hooksJsonPath, "utf-8")).toBe(USER_HOOKS_JSON);
|
|
176
|
+
|
|
177
|
+
// ── Assert: hermeticity ──────────────────────────────────────────────────
|
|
178
|
+
expect(registry.urls.every((u) => u.includes("/model-registry"))).toBe(true);
|
|
179
|
+
expect(invocation.heartbeats.length).toBeGreaterThan(0);
|
|
180
|
+
|
|
181
|
+
// ── Assert: the golden ───────────────────────────────────────────────────
|
|
182
|
+
const json = JSON.stringify(toJson(AgentExecutionStatusSchema, final), null, 2) + "\n";
|
|
183
|
+
await expect(json).toMatchFileSnapshot("./goldens/unattributed-hook-block.status.json");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hermetic scenario: WORKSPACE LOCK TIMEOUT — a turn that cannot acquire its
|
|
3
|
+
* workspace within the configured wait fails with an actionable reason and
|
|
4
|
+
* RETURNS, before any agent exists.
|
|
5
|
+
*
|
|
6
|
+
* Invariant pinned: every turn serializes on its primary workspace directory
|
|
7
|
+
* (shared/workspace/workspace-lock.ts) before the first tree mutation. While
|
|
8
|
+
* another turn holds the lock the activity reports a visible waiting state and
|
|
9
|
+
* heartbeats; past `Config.workspaceLockTimeoutMs` it maps
|
|
10
|
+
* `WorkspaceLockTimeoutError` to EXECUTION_FAILED with the lock's own message
|
|
11
|
+
* (which names the directory) and one system message, persists ONCE, and
|
|
12
|
+
* returns — a Temporal retry would only queue behind the same holder. No
|
|
13
|
+
* `Agent.create`, no `harness_state_id` bind, nothing streamed.
|
|
14
|
+
*
|
|
15
|
+
* Why this is the one terminal arm WITHOUT a file golden (owner ruling,
|
|
16
|
+
* 2026-09-11, entry 20260911.03 M0): the wire copy embeds the absolute
|
|
17
|
+
* workspace path, which is a per-run temp directory and differs between macOS
|
|
18
|
+
* and CI. The S0 driver's rule is to control a volatile source at its origin
|
|
19
|
+
* or escalate — never redact — so this scenario pins every byte the path does
|
|
20
|
+
* not touch with explicit assertions and builds the expected message with the
|
|
21
|
+
* SAME error class over the real resolved path.
|
|
22
|
+
*
|
|
23
|
+
* Determinism: the lock wait is `waitedMs + pollIntervalMs > timeoutMs`, so a
|
|
24
|
+
* timeout below the 1 s poll interval fails on the FIRST contended attempt with
|
|
25
|
+
* no sleep; `waitedMs` is measured on the scripted clock, so it is 0.
|
|
26
|
+
*
|
|
27
|
+
* Regenerate: nothing to regenerate — no snapshot.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { mkdirSync } from "node:fs";
|
|
31
|
+
import { realpath } from "node:fs/promises";
|
|
32
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
33
|
+
import { ExecutionPhase, MessageType } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
34
|
+
|
|
35
|
+
vi.mock("@cursor/sdk", async () =>
|
|
36
|
+
(await import("../../__test-utils__/scripted-sdk.js")).scriptedCursorSdkModule(),
|
|
37
|
+
);
|
|
38
|
+
vi.mock("../../../../client/stigmer-client.js", async () =>
|
|
39
|
+
(await import("../../../../__test-utils__/hermetic-activity.js")).hermeticStigmerClientModule(),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
import {
|
|
43
|
+
ScriptedClock,
|
|
44
|
+
createHermeticEnvironment,
|
|
45
|
+
type HermeticEnvironment,
|
|
46
|
+
} from "../../../../__test-utils__/hermetic-activity.js";
|
|
47
|
+
import {
|
|
48
|
+
WorkspaceLockTimeoutError,
|
|
49
|
+
acquireWorkspaceLock,
|
|
50
|
+
type ReleaseWorkspaceLock,
|
|
51
|
+
} from "../../../../shared/workspace/workspace-lock.js";
|
|
52
|
+
import { _parkedAgentCountForTests } from "../../agent-session-cache.js";
|
|
53
|
+
import { ScriptedCursorAgent } from "../../__test-utils__/scripted-agent.js";
|
|
54
|
+
import {
|
|
55
|
+
SDK_CATALOG,
|
|
56
|
+
beginCursorScenario,
|
|
57
|
+
cursorExecutionRecord,
|
|
58
|
+
runCursorTurn,
|
|
59
|
+
sessionWorkspaceDir,
|
|
60
|
+
} from "../../__test-utils__/hermetic-cursor.js";
|
|
61
|
+
import { stubRegistryFetch } from "../../../../__test-utils__/model-registry-fixture.js";
|
|
62
|
+
|
|
63
|
+
const USER_MESSAGE = "Rename the config module.";
|
|
64
|
+
/** Below the lock's 1 s poll interval, so the first contended attempt times out. */
|
|
65
|
+
const LOCK_TIMEOUT_MS = 500;
|
|
66
|
+
|
|
67
|
+
describe("ExecuteCursor hermetic — workspace lock timeout", () => {
|
|
68
|
+
let env: HermeticEnvironment;
|
|
69
|
+
let registry: ReturnType<typeof stubRegistryFetch>;
|
|
70
|
+
const clock = new ScriptedClock();
|
|
71
|
+
|
|
72
|
+
beforeAll(() => {
|
|
73
|
+
env = createHermeticEnvironment();
|
|
74
|
+
registry = stubRegistryFetch();
|
|
75
|
+
clock.install();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
afterAll(() => {
|
|
79
|
+
clock.uninstall();
|
|
80
|
+
registry.restore();
|
|
81
|
+
env.dispose();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("fails the turn with the lock's message when another turn holds the workspace", async () => {
|
|
85
|
+
// ── Arrange: another turn already holds this session's workspace ─────────
|
|
86
|
+
// The directory is created BEFORE either side locks it so both resolve the
|
|
87
|
+
// same realpath (macOS's /var -> /private/var) and contend on ONE key.
|
|
88
|
+
const workspaceDir = sessionWorkspaceDir(env);
|
|
89
|
+
mkdirSync(workspaceDir, { recursive: true });
|
|
90
|
+
const resolvedDir = await realpath(workspaceDir);
|
|
91
|
+
let releaseHeldLock: ReleaseWorkspaceLock | undefined = await acquireWorkspaceLock(workspaceDir);
|
|
92
|
+
|
|
93
|
+
const neverUsed = new ScriptedCursorAgent({ agentId: "agent-hermetic-never-locked", turns: [] });
|
|
94
|
+
const record = cursorExecutionRecord({ message: USER_MESSAGE });
|
|
95
|
+
const scenario = beginCursorScenario({
|
|
96
|
+
env,
|
|
97
|
+
clock,
|
|
98
|
+
record,
|
|
99
|
+
sdk: { agents: [neverUsed], catalog: SDK_CATALOG },
|
|
100
|
+
config: { workspaceLockTimeoutMs: LOCK_TIMEOUT_MS },
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
// ── Act ────────────────────────────────────────────────────────────────
|
|
105
|
+
const invocation = await runCursorTurn(scenario);
|
|
106
|
+
|
|
107
|
+
// ── Assert: outcome and phases ─────────────────────────────────────────
|
|
108
|
+
expect(invocation.outcome.kind, "a lock timeout RETURNS — a retry would queue behind the same holder").toBe(
|
|
109
|
+
"returned",
|
|
110
|
+
);
|
|
111
|
+
const slim = (invocation.outcome as { value: Record<string, unknown> }).value;
|
|
112
|
+
expect(slim.phase).toBe("EXECUTION_FAILED");
|
|
113
|
+
expect(record.persistedPhases, "the one and only full persist").toEqual([ExecutionPhase.EXECUTION_FAILED]);
|
|
114
|
+
|
|
115
|
+
// ── Assert: the copy, built with the same class over the real path ─────
|
|
116
|
+
const expectedMessage = new WorkspaceLockTimeoutError(resolvedDir, 0).message;
|
|
117
|
+
expect(expectedMessage).toBe(
|
|
118
|
+
`Workspace is in use by another session: ${resolvedDir} (waited 0s). ` +
|
|
119
|
+
`Another agent execution is operating on this workspace directory; retry after it finishes.`,
|
|
120
|
+
);
|
|
121
|
+
const final = record.lastFullStatus!;
|
|
122
|
+
expect(final.error).toBe(expectedMessage);
|
|
123
|
+
expect(final.completedAt).not.toBe("");
|
|
124
|
+
expect(final.startedAt).not.toBe("");
|
|
125
|
+
expect(
|
|
126
|
+
final.messages.filter((m) => m.type === MessageType.MESSAGE_SYSTEM).map((m) => m.content),
|
|
127
|
+
).toEqual([`Execution failed: ${expectedMessage}`]);
|
|
128
|
+
expect(final.messages, "the system message is the whole transcript").toHaveLength(1);
|
|
129
|
+
|
|
130
|
+
// ── Assert: the visible waiting state, then nothing past the lock ──────
|
|
131
|
+
expect(record.setupProgress).toEqual([
|
|
132
|
+
"Fetching execution",
|
|
133
|
+
"Resolving agent blueprint",
|
|
134
|
+
"Resolving environment",
|
|
135
|
+
"Provisioning workspace",
|
|
136
|
+
"Waiting for workspace — in use by another session",
|
|
137
|
+
]);
|
|
138
|
+
expect(scenario.sdk.resolutions, "no agent was created").toHaveLength(0);
|
|
139
|
+
expect(neverUsed.sends).toHaveLength(0);
|
|
140
|
+
expect(record.sessionUpdates, "no harness_state_id to bind").toHaveLength(0);
|
|
141
|
+
expect(_parkedAgentCountForTests()).toBe(0);
|
|
142
|
+
|
|
143
|
+
// ── Assert: hermeticity ────────────────────────────────────────────────
|
|
144
|
+
expect(registry.urls.every((u) => u.includes("/model-registry"))).toBe(true);
|
|
145
|
+
expect(invocation.heartbeats.length, "the activity heartbeat before the lock").toBeGreaterThan(0);
|
|
146
|
+
} finally {
|
|
147
|
+
await releaseHeldLock?.();
|
|
148
|
+
releaseHeldLock = undefined;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|