@stigmer/runner 3.14.0 → 3.14.1
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__/hermetic-activity.d.ts +245 -0
- package/dist/__test-utils__/hermetic-activity.js +369 -0
- package/dist/__test-utils__/hermetic-activity.js.map +1 -0
- package/dist/__test-utils__/mock-client.d.ts +13 -0
- package/dist/__test-utils__/mock-client.js +45 -0
- package/dist/__test-utils__/mock-client.js.map +1 -0
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.d.ts +172 -0
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +331 -0
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -0
- package/dist/activities/execute-cursor/__test-utils__/scripted-agent.d.ts +167 -0
- package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js +239 -0
- package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js.map +1 -0
- package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.d.ts +97 -0
- package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js +132 -0
- package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js.map +1 -0
- package/dist/harness/capabilities.d.ts +71 -0
- package/dist/harness/capabilities.js +36 -0
- package/dist/harness/capabilities.js.map +1 -0
- package/dist/harness/registry.d.ts +67 -0
- package/dist/harness/registry.js +112 -0
- package/dist/harness/registry.js.map +1 -0
- package/dist/harness/types.d.ts +268 -0
- package/dist/harness/types.js +55 -0
- package/dist/harness/types.js.map +1 -0
- package/package.json +4 -4
- package/src/__test-utils__/__tests__/harness-contract-self-check.test.ts +229 -0
- package/src/__test-utils__/config-fixture.ts +63 -0
- package/src/__test-utils__/harness-contract/contract.ts +536 -0
- package/src/__test-utils__/harness-contract/recording-sink.ts +96 -0
- package/src/__test-utils__/harness-contract/scripted-adapter.ts +289 -0
- package/src/__test-utils__/harness-contract/types.ts +100 -0
- package/src/__test-utils__/hermetic-activity.ts +477 -0
- package/src/__test-utils__/proto-helpers.ts +25 -0
- package/src/__tests__/harness-contract.test.ts +25 -0
- package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +422 -0
- package/src/activities/execute-cursor/__test-utils__/scripted-agent.ts +342 -0
- package/src/activities/execute-cursor/__test-utils__/scripted-sdk.ts +166 -0
- package/src/activities/execute-cursor/__tests__/hermetic/deny-and-retry.test.ts +228 -0
- package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +180 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/deny-and-retry.turn1.status.json +55 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/deny-and-retry.turn2.status.json +77 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/file-review-capture.status.json +126 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/pause.status.json +45 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/plain-turn.status.json +48 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/recovery-fresh-agent.status.json +53 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/tool-call.status.json +68 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/worker-shutdown.status.json +47 -0
- package/src/activities/execute-cursor/__tests__/hermetic/pause-vs-shutdown.test.ts +201 -0
- package/src/activities/execute-cursor/__tests__/hermetic/plain-turn.test.ts +171 -0
- package/src/activities/execute-cursor/__tests__/hermetic/recovery-fresh-agent.test.ts +156 -0
- package/src/activities/execute-cursor/__tests__/hermetic/tool-call.test.ts +137 -0
- package/src/harness/__tests__/registry.test.ts +167 -0
- package/src/harness/capabilities.ts +75 -0
- package/src/harness/registry.ts +123 -0
- package/src/harness/types.ts +278 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"messages": [
|
|
3
|
+
{
|
|
4
|
+
"type": "MESSAGE_AI",
|
|
5
|
+
"content": "Starting on the parser.",
|
|
6
|
+
"timestamp": "2026-01-01T00:00:02.000Z"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"type": "MESSAGE_SYSTEM",
|
|
10
|
+
"content": "Execution paused by user. Use resume to continue.",
|
|
11
|
+
"timestamp": "2026-01-01T00:00:04.000Z"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"type": "MESSAGE_SYSTEM",
|
|
15
|
+
"content": "Execution paused by user. Use resume to continue.",
|
|
16
|
+
"timestamp": "2026-01-01T00:00:04.000Z"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"phase": "EXECUTION_PAUSED",
|
|
20
|
+
"startedAt": "2026-01-01T00:00:00.000Z",
|
|
21
|
+
"fileReviewEventStream": {
|
|
22
|
+
"executionId": "aex_hermetic_0001",
|
|
23
|
+
"events": [
|
|
24
|
+
{
|
|
25
|
+
"eventId": "aex_hermetic_0001:0:aex_hermetic_0001:0:FILE_REVIEW_EVENT_TYPE_BASELINE_CAPTURED",
|
|
26
|
+
"changeSetId": "aex_hermetic_0001:0",
|
|
27
|
+
"eventType": "FILE_REVIEW_EVENT_TYPE_BASELINE_CAPTURED",
|
|
28
|
+
"timestamp": "2026-01-01T00:00:00.000Z",
|
|
29
|
+
"actor": "runner",
|
|
30
|
+
"baselineCaptured": {
|
|
31
|
+
"changeSetId": "aex_hermetic_0001:0",
|
|
32
|
+
"turnId": "aex_hermetic_0001:0",
|
|
33
|
+
"harnessId": "cursor",
|
|
34
|
+
"baselineSnapshot": {
|
|
35
|
+
"kind": "SNAPSHOT_KIND_CAS_MANIFEST"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
"fileChangeProgress": {
|
|
42
|
+
"changeSetId": "aex_hermetic_0001:0",
|
|
43
|
+
"capturedAt": "2026-01-01T00:00:01.000Z"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"messages": [
|
|
3
|
+
{
|
|
4
|
+
"type": "MESSAGE_AI",
|
|
5
|
+
"content": "Hello there, hermetic world.",
|
|
6
|
+
"timestamp": "2026-01-01T00:00:02.000Z"
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
"phase": "EXECUTION_COMPLETED",
|
|
10
|
+
"startedAt": "2026-01-01T00:00:00.000Z",
|
|
11
|
+
"completedAt": "2026-01-01T00:00:04.000Z",
|
|
12
|
+
"streamingUsage": {
|
|
13
|
+
"inputTokens": "1200",
|
|
14
|
+
"outputTokens": "40",
|
|
15
|
+
"totalTokens": "1240",
|
|
16
|
+
"turnCount": 1,
|
|
17
|
+
"estimatedCostUsd": 0.00136,
|
|
18
|
+
"model": "composer-2.5",
|
|
19
|
+
"observedAt": "2026-01-01T00:00:03.000Z",
|
|
20
|
+
"requestedServiceTier": "SERVICE_TIER_STANDARD",
|
|
21
|
+
"requestedModelParams": "[{\"id\":\"fast\",\"value\":\"false\"},{\"id\":\"thinking\",\"value\":\"false\"}]",
|
|
22
|
+
"requestedThinkingMode": "THINKING_MODE_DISABLED"
|
|
23
|
+
},
|
|
24
|
+
"fileReviewEventStream": {
|
|
25
|
+
"executionId": "aex_hermetic_0001",
|
|
26
|
+
"events": [
|
|
27
|
+
{
|
|
28
|
+
"eventId": "aex_hermetic_0001:0:aex_hermetic_0001:0:FILE_REVIEW_EVENT_TYPE_BASELINE_CAPTURED",
|
|
29
|
+
"changeSetId": "aex_hermetic_0001:0",
|
|
30
|
+
"eventType": "FILE_REVIEW_EVENT_TYPE_BASELINE_CAPTURED",
|
|
31
|
+
"timestamp": "2026-01-01T00:00:00.000Z",
|
|
32
|
+
"actor": "runner",
|
|
33
|
+
"baselineCaptured": {
|
|
34
|
+
"changeSetId": "aex_hermetic_0001:0",
|
|
35
|
+
"turnId": "aex_hermetic_0001:0",
|
|
36
|
+
"harnessId": "cursor",
|
|
37
|
+
"baselineSnapshot": {
|
|
38
|
+
"kind": "SNAPSHOT_KIND_CAS_MANIFEST"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
"fileChangeProgress": {
|
|
45
|
+
"changeSetId": "aex_hermetic_0001:0",
|
|
46
|
+
"capturedAt": "2026-01-01T00:00:01.000Z"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/activities/execute-cursor/__tests__/hermetic/goldens/recovery-fresh-agent.status.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"messages": [
|
|
3
|
+
{
|
|
4
|
+
"type": "MESSAGE_AI",
|
|
5
|
+
"content": "Let me look at the recent changes.",
|
|
6
|
+
"timestamp": "2026-01-01T00:00:02.000Z"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"type": "MESSAGE_AI",
|
|
10
|
+
"content": "Two files changed since yesterday: notes.md and README.md.",
|
|
11
|
+
"timestamp": "2026-01-01T00:00:06.000Z"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"phase": "EXECUTION_COMPLETED",
|
|
15
|
+
"startedAt": "2026-01-01T00:00:00.000Z",
|
|
16
|
+
"completedAt": "2026-01-01T00:00:08.000Z",
|
|
17
|
+
"streamingUsage": {
|
|
18
|
+
"inputTokens": "2800",
|
|
19
|
+
"outputTokens": "55",
|
|
20
|
+
"totalTokens": "2855",
|
|
21
|
+
"turnCount": 1,
|
|
22
|
+
"estimatedCostUsd": 0.00302,
|
|
23
|
+
"model": "composer-2.5",
|
|
24
|
+
"observedAt": "2026-01-01T00:00:07.000Z",
|
|
25
|
+
"requestedServiceTier": "SERVICE_TIER_STANDARD",
|
|
26
|
+
"requestedModelParams": "[{\"id\":\"fast\",\"value\":\"false\"},{\"id\":\"thinking\",\"value\":\"false\"}]",
|
|
27
|
+
"requestedThinkingMode": "THINKING_MODE_DISABLED"
|
|
28
|
+
},
|
|
29
|
+
"fileReviewEventStream": {
|
|
30
|
+
"executionId": "aex_hermetic_0001",
|
|
31
|
+
"events": [
|
|
32
|
+
{
|
|
33
|
+
"eventId": "aex_hermetic_0001:0:aex_hermetic_0001:0:FILE_REVIEW_EVENT_TYPE_BASELINE_CAPTURED",
|
|
34
|
+
"changeSetId": "aex_hermetic_0001:0",
|
|
35
|
+
"eventType": "FILE_REVIEW_EVENT_TYPE_BASELINE_CAPTURED",
|
|
36
|
+
"timestamp": "2026-01-01T00:00:00.000Z",
|
|
37
|
+
"actor": "runner",
|
|
38
|
+
"baselineCaptured": {
|
|
39
|
+
"changeSetId": "aex_hermetic_0001:0",
|
|
40
|
+
"turnId": "aex_hermetic_0001:0",
|
|
41
|
+
"harnessId": "cursor",
|
|
42
|
+
"baselineSnapshot": {
|
|
43
|
+
"kind": "SNAPSHOT_KIND_CAS_MANIFEST"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
"fileChangeProgress": {
|
|
50
|
+
"changeSetId": "aex_hermetic_0001:0",
|
|
51
|
+
"capturedAt": "2026-01-01T00:00:01.000Z"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"messages": [
|
|
3
|
+
{
|
|
4
|
+
"type": "MESSAGE_AI",
|
|
5
|
+
"content": "Let me read it.",
|
|
6
|
+
"timestamp": "2026-01-01T00:00:02.000Z",
|
|
7
|
+
"toolCalls": [
|
|
8
|
+
{
|
|
9
|
+
"id": "call-hermetic-read-0001",
|
|
10
|
+
"name": "read",
|
|
11
|
+
"args": {
|
|
12
|
+
"path": "README.md"
|
|
13
|
+
},
|
|
14
|
+
"result": "# Hermetic\n\nA fixture readme.\n",
|
|
15
|
+
"status": "TOOL_CALL_COMPLETED",
|
|
16
|
+
"startedAt": "2026-01-01T00:00:03.000Z",
|
|
17
|
+
"completedAt": "2026-01-01T00:00:04.000Z",
|
|
18
|
+
"argsPreview": "{\"path\":\"README.md\"}",
|
|
19
|
+
"toolKind": "TOOL_KIND_FILE_READ"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"type": "MESSAGE_AI",
|
|
25
|
+
"content": "README.md holds a one-line fixture description.",
|
|
26
|
+
"timestamp": "2026-01-01T00:00:05.000Z"
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"phase": "EXECUTION_COMPLETED",
|
|
30
|
+
"startedAt": "2026-01-01T00:00:00.000Z",
|
|
31
|
+
"completedAt": "2026-01-01T00:00:07.000Z",
|
|
32
|
+
"streamingUsage": {
|
|
33
|
+
"inputTokens": "2000",
|
|
34
|
+
"outputTokens": "90",
|
|
35
|
+
"totalTokens": "2090",
|
|
36
|
+
"turnCount": 1,
|
|
37
|
+
"estimatedCostUsd": 0.00236,
|
|
38
|
+
"model": "composer-2.5",
|
|
39
|
+
"observedAt": "2026-01-01T00:00:06.000Z",
|
|
40
|
+
"requestedServiceTier": "SERVICE_TIER_STANDARD",
|
|
41
|
+
"requestedModelParams": "[{\"id\":\"fast\",\"value\":\"false\"},{\"id\":\"thinking\",\"value\":\"false\"}]",
|
|
42
|
+
"requestedThinkingMode": "THINKING_MODE_DISABLED"
|
|
43
|
+
},
|
|
44
|
+
"fileReviewEventStream": {
|
|
45
|
+
"executionId": "aex_hermetic_0001",
|
|
46
|
+
"events": [
|
|
47
|
+
{
|
|
48
|
+
"eventId": "aex_hermetic_0001:0:aex_hermetic_0001:0:FILE_REVIEW_EVENT_TYPE_BASELINE_CAPTURED",
|
|
49
|
+
"changeSetId": "aex_hermetic_0001:0",
|
|
50
|
+
"eventType": "FILE_REVIEW_EVENT_TYPE_BASELINE_CAPTURED",
|
|
51
|
+
"timestamp": "2026-01-01T00:00:00.000Z",
|
|
52
|
+
"actor": "runner",
|
|
53
|
+
"baselineCaptured": {
|
|
54
|
+
"changeSetId": "aex_hermetic_0001:0",
|
|
55
|
+
"turnId": "aex_hermetic_0001:0",
|
|
56
|
+
"harnessId": "cursor",
|
|
57
|
+
"baselineSnapshot": {
|
|
58
|
+
"kind": "SNAPSHOT_KIND_CAS_MANIFEST"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
"fileChangeProgress": {
|
|
65
|
+
"changeSetId": "aex_hermetic_0001:0",
|
|
66
|
+
"capturedAt": "2026-01-01T00:00:01.000Z"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"messages": [
|
|
3
|
+
{
|
|
4
|
+
"type": "MESSAGE_AI",
|
|
5
|
+
"content": "Starting on the parser.",
|
|
6
|
+
"timestamp": "2026-01-01T00:00:02.000Z"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"type": "MESSAGE_SYSTEM",
|
|
10
|
+
"content": "Execution interrupted: the runner worker was shut down while the agent was still running. You can retry or resume.",
|
|
11
|
+
"timestamp": "2026-01-01T00:00:04.000Z"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"type": "MESSAGE_SYSTEM",
|
|
15
|
+
"content": "Execution interrupted: the runner worker was shut down while the agent was still running. You can retry or resume.",
|
|
16
|
+
"timestamp": "2026-01-01T00:00:04.000Z"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"phase": "EXECUTION_FAILED",
|
|
20
|
+
"error": "Execution interrupted: runner worker was shut down. Retry or resume.",
|
|
21
|
+
"startedAt": "2026-01-01T00:00:00.000Z",
|
|
22
|
+
"completedAt": "2026-01-01T00:00:04.000Z",
|
|
23
|
+
"fileReviewEventStream": {
|
|
24
|
+
"executionId": "aex_hermetic_0001",
|
|
25
|
+
"events": [
|
|
26
|
+
{
|
|
27
|
+
"eventId": "aex_hermetic_0001:0:aex_hermetic_0001:0:FILE_REVIEW_EVENT_TYPE_BASELINE_CAPTURED",
|
|
28
|
+
"changeSetId": "aex_hermetic_0001:0",
|
|
29
|
+
"eventType": "FILE_REVIEW_EVENT_TYPE_BASELINE_CAPTURED",
|
|
30
|
+
"timestamp": "2026-01-01T00:00:00.000Z",
|
|
31
|
+
"actor": "runner",
|
|
32
|
+
"baselineCaptured": {
|
|
33
|
+
"changeSetId": "aex_hermetic_0001:0",
|
|
34
|
+
"turnId": "aex_hermetic_0001:0",
|
|
35
|
+
"harnessId": "cursor",
|
|
36
|
+
"baselineSnapshot": {
|
|
37
|
+
"kind": "SNAPSHOT_KIND_CAS_MANIFEST"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"fileChangeProgress": {
|
|
44
|
+
"changeSetId": "aex_hermetic_0001:0",
|
|
45
|
+
"capturedAt": "2026-01-01T00:00:01.000Z"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hermetic goldens: USER PAUSE versus WORKER SHUTDOWN — the two interruptions
|
|
3
|
+
* that THROW, and the byte-pinned copy that tells them apart.
|
|
4
|
+
*
|
|
5
|
+
* Invariant pinned (the throw-vs-return table the runtime will own in S2; parent
|
|
6
|
+
* §5c): both interruptions end the activity with a thrown Temporal
|
|
7
|
+
* `CancelledFailure` (never a return), but they persist DIFFERENT terminal
|
|
8
|
+
* states the control plane keys on:
|
|
9
|
+
*
|
|
10
|
+
* - a user pause (Temporal delivers cancellation, no shutdown signal) persists
|
|
11
|
+
* EXECUTION_PAUSED with "Execution paused by user. Use resume to continue."
|
|
12
|
+
* and throws "Activity paused by orchestrator";
|
|
13
|
+
* - a worker shutdown (the runner's per-queue shutdown signal is aborted AND
|
|
14
|
+
* cancellation is delivered) persists EXECUTION_FAILED with the error copy
|
|
15
|
+
* "Execution interrupted: runner worker was shut down. Retry or resume." and
|
|
16
|
+
* throws "Activity cancelled (worker shutdown, not user pause)".
|
|
17
|
+
*
|
|
18
|
+
* The cloud channel decision table matches on that copy, so the goldens
|
|
19
|
+
* (`goldens/pause.status.json`, `goldens/worker-shutdown.status.json`) are the
|
|
20
|
+
* wire contract, character for character. Both interruptions are triggered from
|
|
21
|
+
* a script `effect` step — never a timer — so the instant is deterministic: the
|
|
22
|
+
* stream loop sees `isCancelled()` on its next iteration and never processes
|
|
23
|
+
* the event that follows.
|
|
24
|
+
*
|
|
25
|
+
* What the goldens ALSO pin, on purpose: both paths append their system message
|
|
26
|
+
* TWICE — once in `resolvePreBoundaryTerminal` before the throw, once again in
|
|
27
|
+
* the outer `catch (CancelledFailure)`, which re-stamps and re-persists. That is
|
|
28
|
+
* today's production behavior on the wire. S2 must reproduce it byte for byte;
|
|
29
|
+
* whether it is later collapsed to one message is a separate, deliberate change
|
|
30
|
+
* (recorded in the entry's execution log for the owner).
|
|
31
|
+
*
|
|
32
|
+
* Parent phase rows exercised: the stream loop's cancellation check; the
|
|
33
|
+
* post-stream `classifyTurnInterruption`; `resolvePreBoundaryTerminal`'s
|
|
34
|
+
* pause and worker-shutdown arms; the outer `CancelledFailure` catch; the
|
|
35
|
+
* `finally` teardown under a thrown exit.
|
|
36
|
+
*
|
|
37
|
+
* Regenerate ONLY after a deliberate behavior change:
|
|
38
|
+
* npx vitest run src/activities/execute-cursor/__tests__/hermetic -u
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
42
|
+
import { toJson } from "@bufbuild/protobuf";
|
|
43
|
+
import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
44
|
+
import { ExecutionPhase, MessageType } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
45
|
+
|
|
46
|
+
vi.mock("@cursor/sdk", async () =>
|
|
47
|
+
(await import("../../__test-utils__/scripted-sdk.js")).scriptedCursorSdkModule(),
|
|
48
|
+
);
|
|
49
|
+
vi.mock("../../../../client/stigmer-client.js", async () =>
|
|
50
|
+
(await import("../../../../__test-utils__/hermetic-activity.js")).hermeticStigmerClientModule(),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
import {
|
|
54
|
+
ScriptedClock,
|
|
55
|
+
createHermeticEnvironment,
|
|
56
|
+
threwCancelledFailure,
|
|
57
|
+
type HermeticEnvironment,
|
|
58
|
+
type InvocationControls,
|
|
59
|
+
} from "../../../../__test-utils__/hermetic-activity.js";
|
|
60
|
+
import { ScriptedCursorAgent, sdkEvents, step } from "../../__test-utils__/scripted-agent.js";
|
|
61
|
+
import {
|
|
62
|
+
SDK_CATALOG,
|
|
63
|
+
beginCursorScenario,
|
|
64
|
+
cursorExecutionRecord,
|
|
65
|
+
runCursorTurn,
|
|
66
|
+
stubRegistryFetch,
|
|
67
|
+
} from "../../__test-utils__/hermetic-cursor.js";
|
|
68
|
+
|
|
69
|
+
const USER_MESSAGE = "Refactor the parser module.";
|
|
70
|
+
const NEVER_SEEN = "This text must never reach the transcript.";
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A turn that is interrupted mid-stream by `interrupt(controls)`: one assistant
|
|
74
|
+
* message lands, then the interruption, then an event the loop must NOT process.
|
|
75
|
+
*/
|
|
76
|
+
function interruptedTurn(
|
|
77
|
+
agentId: string,
|
|
78
|
+
runId: string,
|
|
79
|
+
clock: ScriptedClock,
|
|
80
|
+
getControls: () => InvocationControls,
|
|
81
|
+
interrupt: (controls: InvocationControls) => void,
|
|
82
|
+
): ScriptedCursorAgent {
|
|
83
|
+
const ev = sdkEvents(agentId, runId);
|
|
84
|
+
return new ScriptedCursorAgent({
|
|
85
|
+
agentId,
|
|
86
|
+
runIds: [runId],
|
|
87
|
+
observeStep: () => clock.tick(),
|
|
88
|
+
turns: [
|
|
89
|
+
[
|
|
90
|
+
step.event(ev.init()),
|
|
91
|
+
step.event(ev.assistant("Starting on the parser.")),
|
|
92
|
+
step.effect("interrupt the activity from outside", () => interrupt(getControls())),
|
|
93
|
+
step.event(ev.assistant(NEVER_SEEN)),
|
|
94
|
+
step.finished({ result: NEVER_SEEN }),
|
|
95
|
+
],
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
describe("ExecuteCursor hermetic — user pause vs worker shutdown", () => {
|
|
101
|
+
let env: HermeticEnvironment;
|
|
102
|
+
let registry: ReturnType<typeof stubRegistryFetch>;
|
|
103
|
+
const clock = new ScriptedClock();
|
|
104
|
+
|
|
105
|
+
beforeAll(() => {
|
|
106
|
+
env = createHermeticEnvironment();
|
|
107
|
+
registry = stubRegistryFetch();
|
|
108
|
+
clock.install();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
afterAll(() => {
|
|
112
|
+
clock.uninstall();
|
|
113
|
+
registry.restore();
|
|
114
|
+
env.dispose();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("a user pause persists PAUSED and throws CancelledFailure", async () => {
|
|
118
|
+
// ── Arrange ──────────────────────────────────────────────────────────────
|
|
119
|
+
clock.reset(); // both goldens read from :00
|
|
120
|
+
let controls: InvocationControls | undefined;
|
|
121
|
+
const agent = interruptedTurn(
|
|
122
|
+
"agent-hermetic-pause-0001",
|
|
123
|
+
"run-hermetic-pause-0001",
|
|
124
|
+
clock,
|
|
125
|
+
() => controls!,
|
|
126
|
+
(c) => c.cancel(),
|
|
127
|
+
);
|
|
128
|
+
const record = cursorExecutionRecord({ message: USER_MESSAGE });
|
|
129
|
+
const scenario = beginCursorScenario({ env, clock, record, sdk: { agents: [agent], catalog: SDK_CATALOG } });
|
|
130
|
+
|
|
131
|
+
// ── Act ──────────────────────────────────────────────────────────────────
|
|
132
|
+
const invocation = await runCursorTurn(scenario, { onControls: (c) => (controls = c) });
|
|
133
|
+
|
|
134
|
+
// ── Assert: the throw and the persisted state ────────────────────────────
|
|
135
|
+
expect(threwCancelledFailure(invocation.outcome), "a pause THROWS CancelledFailure").toBe(true);
|
|
136
|
+
if (!threwCancelledFailure(invocation.outcome)) throw new Error("unreachable");
|
|
137
|
+
expect(invocation.outcome.error.message).toBe("Activity paused by orchestrator");
|
|
138
|
+
expect(record.persistedPhases).toEqual([
|
|
139
|
+
ExecutionPhase.EXECUTION_IN_PROGRESS,
|
|
140
|
+
ExecutionPhase.EXECUTION_PAUSED,
|
|
141
|
+
]);
|
|
142
|
+
const final = record.lastFullStatus!;
|
|
143
|
+
expect(final.error, "a pause is not an error").toBe("");
|
|
144
|
+
expect(final.completedAt, "a paused turn is not complete").toBe("");
|
|
145
|
+
expect(
|
|
146
|
+
final.messages.filter((m) => m.type === MessageType.MESSAGE_SYSTEM).map((m) => m.content),
|
|
147
|
+
).toEqual([
|
|
148
|
+
"Execution paused by user. Use resume to continue.",
|
|
149
|
+
"Execution paused by user. Use resume to continue.",
|
|
150
|
+
]);
|
|
151
|
+
expect(final.messages.some((m) => m.content === NEVER_SEEN), "nothing after the cancel is processed").toBe(false);
|
|
152
|
+
|
|
153
|
+
const json = JSON.stringify(toJson(AgentExecutionStatusSchema, final), null, 2) + "\n";
|
|
154
|
+
await expect(json).toMatchFileSnapshot("./goldens/pause.status.json");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("a worker shutdown persists FAILED with the shutdown copy and throws CancelledFailure", async () => {
|
|
158
|
+
// ── Arrange ──────────────────────────────────────────────────────────────
|
|
159
|
+
clock.reset();
|
|
160
|
+
let controls: InvocationControls | undefined;
|
|
161
|
+
const agent = interruptedTurn(
|
|
162
|
+
"agent-hermetic-shutdown-0001",
|
|
163
|
+
"run-hermetic-shutdown-0001",
|
|
164
|
+
clock,
|
|
165
|
+
() => controls!,
|
|
166
|
+
(c) => {
|
|
167
|
+
// The runner-manager drains: the queue's shutdown signal aborts, then
|
|
168
|
+
// Temporal delivers the cancellation.
|
|
169
|
+
c.signalWorkerShutdown();
|
|
170
|
+
c.cancel();
|
|
171
|
+
},
|
|
172
|
+
);
|
|
173
|
+
const record = cursorExecutionRecord({ message: USER_MESSAGE });
|
|
174
|
+
const scenario = beginCursorScenario({ env, clock, record, sdk: { agents: [agent], catalog: SDK_CATALOG } });
|
|
175
|
+
|
|
176
|
+
// ── Act ──────────────────────────────────────────────────────────────────
|
|
177
|
+
const invocation = await runCursorTurn(scenario, { onControls: (c) => (controls = c) });
|
|
178
|
+
|
|
179
|
+
// ── Assert: the throw and the persisted state ────────────────────────────
|
|
180
|
+
expect(threwCancelledFailure(invocation.outcome), "a shutdown THROWS CancelledFailure").toBe(true);
|
|
181
|
+
if (!threwCancelledFailure(invocation.outcome)) throw new Error("unreachable");
|
|
182
|
+
expect(invocation.outcome.error.message).toBe("Activity cancelled (worker shutdown, not user pause)");
|
|
183
|
+
expect(record.persistedPhases).toEqual([
|
|
184
|
+
ExecutionPhase.EXECUTION_IN_PROGRESS,
|
|
185
|
+
ExecutionPhase.EXECUTION_FAILED,
|
|
186
|
+
]);
|
|
187
|
+
const final = record.lastFullStatus!;
|
|
188
|
+
expect(final.error).toBe("Execution interrupted: runner worker was shut down. Retry or resume.");
|
|
189
|
+
expect(final.completedAt).not.toBe("");
|
|
190
|
+
expect(
|
|
191
|
+
final.messages.filter((m) => m.type === MessageType.MESSAGE_SYSTEM).map((m) => m.content),
|
|
192
|
+
).toEqual([
|
|
193
|
+
"Execution interrupted: the runner worker was shut down while the agent was still running. You can retry or resume.",
|
|
194
|
+
"Execution interrupted: the runner worker was shut down while the agent was still running. You can retry or resume.",
|
|
195
|
+
]);
|
|
196
|
+
expect(final.messages.some((m) => m.content === NEVER_SEEN)).toBe(false);
|
|
197
|
+
|
|
198
|
+
const json = JSON.stringify(toJson(AgentExecutionStatusSchema, final), null, 2) + "\n";
|
|
199
|
+
await expect(json).toMatchFileSnapshot("./goldens/worker-shutdown.status.json");
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hermetic golden: a PLAIN TURN through the whole `ExecuteCursor` activity.
|
|
3
|
+
*
|
|
4
|
+
* Invariant pinned: an execution with no tools, no workspace entries, no MCP
|
|
5
|
+
* servers and no skills — the user asks, the agent answers — runs the full
|
|
6
|
+
* setup pipeline and ends COMPLETED with exactly the transcript, usage and
|
|
7
|
+
* timestamps recorded in `goldens/plain-turn.status.json`, RETURNING a slim
|
|
8
|
+
* status to the workflow (never throwing). This is the baseline every other
|
|
9
|
+
* scenario is a delta from, and the first proof that the activity is byte-
|
|
10
|
+
* stable under the hermetic driver.
|
|
11
|
+
*
|
|
12
|
+
* Parent phase rows exercised (20260908.02 `T01_0_plan.md` §3a): normalize
|
|
13
|
+
* input / heartbeat; artifact storage + the `persist` chokepoint; fetch
|
|
14
|
+
* execution, session, agent, blueprint; resolve environment (NOT_FOUND ->
|
|
15
|
+
* empty); provision workspace (per-session dir); workspace turn lock; MCP
|
|
16
|
+
* resolve (none); skills (none); attachments (none); HITL gate install;
|
|
17
|
+
* pricing + model id + variant params; create the engine session; write
|
|
18
|
+
* `harness_state_id` back; prompt composition; usage accumulation; run the
|
|
19
|
+
* turn; terminal mapping COMPLETED -> return; cleanup.
|
|
20
|
+
*
|
|
21
|
+
* Nothing here is a live Cursor agent: `@cursor/sdk` is the scripted double
|
|
22
|
+
* (`__test-utils__/scripted-sdk.ts`) and the control plane is the in-memory
|
|
23
|
+
* execution record (`__test-utils__/hermetic-activity.ts`). No network, no
|
|
24
|
+
* `CURSOR_API_KEY`, no Temporal worker.
|
|
25
|
+
*
|
|
26
|
+
* Regenerate the golden ONLY after a deliberate behavior change:
|
|
27
|
+
* npx vitest run src/activities/execute-cursor/__tests__/hermetic -u
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
31
|
+
import { toJson } from "@bufbuild/protobuf";
|
|
32
|
+
import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
33
|
+
import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
34
|
+
|
|
35
|
+
// Both mocks are hoisted by vitest; they must live in this file. Each factory
|
|
36
|
+
// imports the reusable module and returns its mock surface.
|
|
37
|
+
vi.mock("@cursor/sdk", async () =>
|
|
38
|
+
(await import("../../__test-utils__/scripted-sdk.js")).scriptedCursorSdkModule(),
|
|
39
|
+
);
|
|
40
|
+
vi.mock("../../../../client/stigmer-client.js", async () =>
|
|
41
|
+
(await import("../../../../__test-utils__/hermetic-activity.js")).hermeticStigmerClientModule(),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
import {
|
|
45
|
+
ScriptedClock,
|
|
46
|
+
createHermeticEnvironment,
|
|
47
|
+
type HermeticEnvironment,
|
|
48
|
+
} from "../../../../__test-utils__/hermetic-activity.js";
|
|
49
|
+
import { ScriptedCursorAgent, step } from "../../__test-utils__/scripted-agent.js";
|
|
50
|
+
import {
|
|
51
|
+
FIXTURE,
|
|
52
|
+
SDK_CATALOG,
|
|
53
|
+
beginCursorScenario,
|
|
54
|
+
cursorExecutionRecord,
|
|
55
|
+
runCursorTurn,
|
|
56
|
+
stubRegistryFetch,
|
|
57
|
+
} from "../../__test-utils__/hermetic-cursor.js";
|
|
58
|
+
|
|
59
|
+
const AGENT_ID = "agent-hermetic-plain-0001";
|
|
60
|
+
const RUN_ID = "run-hermetic-plain-0001";
|
|
61
|
+
const USER_MESSAGE = "Say hello in five words or fewer.";
|
|
62
|
+
const ASSISTANT_TEXT = "Hello there, hermetic world.";
|
|
63
|
+
|
|
64
|
+
describe("ExecuteCursor hermetic — plain turn", () => {
|
|
65
|
+
let env: HermeticEnvironment;
|
|
66
|
+
let registry: ReturnType<typeof stubRegistryFetch>;
|
|
67
|
+
const clock = new ScriptedClock();
|
|
68
|
+
|
|
69
|
+
beforeAll(() => {
|
|
70
|
+
env = createHermeticEnvironment();
|
|
71
|
+
registry = stubRegistryFetch();
|
|
72
|
+
clock.install();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterAll(() => {
|
|
76
|
+
clock.uninstall();
|
|
77
|
+
registry.restore();
|
|
78
|
+
env.dispose();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("completes with the golden transcript and returns a slim COMPLETED status", async () => {
|
|
82
|
+
// ── Arrange ──────────────────────────────────────────────────────────────
|
|
83
|
+
const agent = new ScriptedCursorAgent({
|
|
84
|
+
agentId: AGENT_ID,
|
|
85
|
+
runIds: [RUN_ID],
|
|
86
|
+
observeStep: () => clock.tick(),
|
|
87
|
+
turns: [
|
|
88
|
+
[
|
|
89
|
+
step.event({ type: "system", subtype: "init", agent_id: AGENT_ID, run_id: RUN_ID }),
|
|
90
|
+
step.event({
|
|
91
|
+
type: "assistant",
|
|
92
|
+
agent_id: AGENT_ID,
|
|
93
|
+
run_id: RUN_ID,
|
|
94
|
+
message: { role: "assistant", content: [{ type: "text", text: ASSISTANT_TEXT }] },
|
|
95
|
+
}),
|
|
96
|
+
step.turnEnded({ inputTokens: 1_200, outputTokens: 40 }),
|
|
97
|
+
step.finished({
|
|
98
|
+
result: ASSISTANT_TEXT,
|
|
99
|
+
model: {
|
|
100
|
+
id: FIXTURE.model,
|
|
101
|
+
params: [
|
|
102
|
+
{ id: "fast", value: "false" },
|
|
103
|
+
{ id: "thinking", value: "false" },
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
107
|
+
],
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
const record = cursorExecutionRecord({ message: USER_MESSAGE });
|
|
111
|
+
const scenario = beginCursorScenario({
|
|
112
|
+
env,
|
|
113
|
+
clock,
|
|
114
|
+
record,
|
|
115
|
+
sdk: { agents: [agent], catalog: SDK_CATALOG },
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ── Act ──────────────────────────────────────────────────────────────────
|
|
119
|
+
const invocation = await runCursorTurn(scenario);
|
|
120
|
+
|
|
121
|
+
// ── Assert: the outcome the workflow sees ────────────────────────────────
|
|
122
|
+
expect(invocation.outcome.kind, "a completed turn RETURNS, never throws").toBe("returned");
|
|
123
|
+
const slim = (invocation.outcome as { value: Record<string, unknown> }).value;
|
|
124
|
+
expect(slim.phase).toBe("EXECUTION_COMPLETED");
|
|
125
|
+
expect(slim.final_text).toBe(ASSISTANT_TEXT);
|
|
126
|
+
expect(slim, "the slim return carries no transcript").not.toHaveProperty("messages");
|
|
127
|
+
|
|
128
|
+
// ── Assert: what the control plane received, in order ────────────────────
|
|
129
|
+
expect(record.persistedPhases).toEqual([
|
|
130
|
+
ExecutionPhase.EXECUTION_IN_PROGRESS,
|
|
131
|
+
ExecutionPhase.EXECUTION_COMPLETED,
|
|
132
|
+
]);
|
|
133
|
+
expect(record.setupProgress, "the setup pipeline's phase labels, in order").toEqual([
|
|
134
|
+
"Fetching execution",
|
|
135
|
+
"Resolving agent blueprint",
|
|
136
|
+
"Resolving environment",
|
|
137
|
+
"Provisioning workspace",
|
|
138
|
+
"Resolving MCP servers",
|
|
139
|
+
"Resolving skills",
|
|
140
|
+
"Initializing Cursor agent",
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
// ── Assert: what the SDK was asked ───────────────────────────────────────
|
|
144
|
+
expect(scenario.sdk.resolutions).toHaveLength(1);
|
|
145
|
+
expect(scenario.sdk.resolutions[0].kind, "a fresh session creates, never resumes").toBe("create");
|
|
146
|
+
expect(scenario.sdk.resolutions[0].options?.model).toEqual({
|
|
147
|
+
id: FIXTURE.model,
|
|
148
|
+
params: [
|
|
149
|
+
{ id: "fast", value: "false" },
|
|
150
|
+
{ id: "thinking", value: "false" },
|
|
151
|
+
],
|
|
152
|
+
});
|
|
153
|
+
expect(agent.sends).toHaveLength(1);
|
|
154
|
+
expect(String(agent.sends[0].message), "the prompt carries the user's message").toContain(USER_MESSAGE);
|
|
155
|
+
expect(agent.sends[0].hasOnDelta).toBe(true);
|
|
156
|
+
|
|
157
|
+
// ── Assert: the harness_state_id write-back ─────────────────────────────
|
|
158
|
+
expect(record.sessionUpdates).toHaveLength(1);
|
|
159
|
+
expect(record.sessionUpdates[0].spec?.harnessStateId).toBe(AGENT_ID);
|
|
160
|
+
|
|
161
|
+
// ── Assert: hermeticity ──────────────────────────────────────────────────
|
|
162
|
+
expect(invocation.heartbeats.length, "the whole-activity heartbeat ran").toBeGreaterThan(0);
|
|
163
|
+
expect(registry.urls.every((u) => u.includes("/model-registry")), "no network beyond the registry").toBe(true);
|
|
164
|
+
|
|
165
|
+
// ── Assert: the golden ───────────────────────────────────────────────────
|
|
166
|
+
const finalStatus = record.lastFullStatus;
|
|
167
|
+
expect(finalStatus).toBeDefined();
|
|
168
|
+
const json = JSON.stringify(toJson(AgentExecutionStatusSchema, finalStatus!), null, 2) + "\n";
|
|
169
|
+
await expect(json).toMatchFileSnapshot("./goldens/plain-turn.status.json");
|
|
170
|
+
});
|
|
171
|
+
});
|