@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,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hermetic golden: the DENY-AND-RETRY approval round trip through two real
|
|
3
|
+
* `ExecuteCursor` invocations, with the REAL bash hook as the out-of-process
|
|
4
|
+
* half.
|
|
5
|
+
*
|
|
6
|
+
* Invariant pinned (the Cursor pause primitive, parent §4a `pausePrimitive:
|
|
7
|
+
* "deny-and-retry"`): the model proposes a gated built-in (`shell`); the
|
|
8
|
+
* workspace hook the activity installed DENIES it and appends to the denial
|
|
9
|
+
* ledger; the stream loop reads the ledger on the next `tool_call` event, cancels
|
|
10
|
+
* the run, and the turn boundary overlays the row as WAITING_APPROVAL; the
|
|
11
|
+
* activity persists WAITING_FOR_APPROVAL, parks the agent, and RETURNS a slim
|
|
12
|
+
* status. The user approves (the server writes `approval_action` on the row —
|
|
13
|
+
* here the record does, per field ownership). The reinvocation (`thread_id` set,
|
|
14
|
+
* `turn_seq` 1) seeds its transcript from the persisted execution, derives a
|
|
15
|
+
* grant from the approved row, reinstalls the gate, and the SAME hook now ALLOWS
|
|
16
|
+
* the re-issued call — which the model emits under a FRESH `call_id`, and which
|
|
17
|
+
* the transcript reconciles onto the committed row by canonical identity (a
|
|
18
|
+
* superset, never a duplicate, never a second gate). The turn ends COMPLETED.
|
|
19
|
+
*
|
|
20
|
+
* Two goldens, one per invocation: `goldens/deny-and-retry.turn1.status.json`
|
|
21
|
+
* (the paused transcript) and `goldens/deny-and-retry.turn2.status.json` (the
|
|
22
|
+
* resumed, completed transcript). S2 must reproduce both byte for byte.
|
|
23
|
+
*
|
|
24
|
+
* Parent phase rows exercised beyond `tool-call`: reinvocation detection and
|
|
25
|
+
* transcript seeding (Phase 3); reading approval decisions from the persisted
|
|
26
|
+
* transcript (`reconstructAdjudicatedApprovals`); grants → approval state →
|
|
27
|
+
* the gate (Phase 5c); the first-denial early stop in the stream loop; the
|
|
28
|
+
* turn boundary's denial overlay; `enterApprovalPause` (WAITING_FOR_APPROVAL,
|
|
29
|
+
* park, return); the parked-agent checkout on the resume (#215, no `Agent.resume`
|
|
30
|
+
* call); the resume prompt.
|
|
31
|
+
*
|
|
32
|
+
* The hook is the runner's own generated bash script, found the way the SDK
|
|
33
|
+
* finds it (`.cursor/hooks.json` in the workspace) and run with the SDK's real
|
|
34
|
+
* preToolUse input shape (`cursor-hook-harness.ts` builders). Skipped where
|
|
35
|
+
* `bash` is unavailable — reported as SKIPPED, never a silent pass.
|
|
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 {
|
|
45
|
+
ApprovalAction,
|
|
46
|
+
ExecutionPhase,
|
|
47
|
+
ToolCallStatus,
|
|
48
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
49
|
+
|
|
50
|
+
vi.mock("@cursor/sdk", async () =>
|
|
51
|
+
(await import("../../__test-utils__/scripted-sdk.js")).scriptedCursorSdkModule(),
|
|
52
|
+
);
|
|
53
|
+
vi.mock("../../../../client/stigmer-client.js", async () =>
|
|
54
|
+
(await import("../../../../__test-utils__/hermetic-activity.js")).hermeticStigmerClientModule(),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
import {
|
|
58
|
+
ScriptedClock,
|
|
59
|
+
createHermeticEnvironment,
|
|
60
|
+
type HermeticEnvironment,
|
|
61
|
+
} from "../../../../__test-utils__/hermetic-activity.js";
|
|
62
|
+
import { hasBash, hookShell } from "../../__test-utils__/cursor-hook-harness.js";
|
|
63
|
+
import { ScriptedCursorAgent, sdkEvents, step } from "../../__test-utils__/scripted-agent.js";
|
|
64
|
+
import {
|
|
65
|
+
FIXTURE,
|
|
66
|
+
SDK_CATALOG,
|
|
67
|
+
beginCursorScenario,
|
|
68
|
+
cursorExecutionRecord,
|
|
69
|
+
runCursorTurn,
|
|
70
|
+
runWorkspaceHook,
|
|
71
|
+
sessionWorkspaceDir,
|
|
72
|
+
stubRegistryFetch,
|
|
73
|
+
} from "../../__test-utils__/hermetic-cursor.js";
|
|
74
|
+
|
|
75
|
+
const AGENT_ID = "agent-hermetic-deny-0001";
|
|
76
|
+
const RUN_1 = "run-hermetic-deny-0001";
|
|
77
|
+
const RUN_2 = "run-hermetic-deny-0002";
|
|
78
|
+
const CALL_TURN_1 = "call-hermetic-shell-0001";
|
|
79
|
+
const CALL_TURN_2 = "call-hermetic-shell-0002";
|
|
80
|
+
const USER_MESSAGE = "Run the build and tell me if it passes.";
|
|
81
|
+
const COMMAND = "npm run build";
|
|
82
|
+
const SHELL_ARGS = { command: COMMAND };
|
|
83
|
+
const BUILD_OUTPUT = "build ok (hermetic)\n";
|
|
84
|
+
const FINAL_TEXT = "The build passes.";
|
|
85
|
+
|
|
86
|
+
describe.skipIf(!hasBash)("ExecuteCursor hermetic — deny-and-retry approval round trip", () => {
|
|
87
|
+
let env: HermeticEnvironment;
|
|
88
|
+
let registry: ReturnType<typeof stubRegistryFetch>;
|
|
89
|
+
const clock = new ScriptedClock();
|
|
90
|
+
|
|
91
|
+
beforeAll(() => {
|
|
92
|
+
env = createHermeticEnvironment();
|
|
93
|
+
registry = stubRegistryFetch();
|
|
94
|
+
clock.install();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
afterAll(() => {
|
|
98
|
+
clock.uninstall();
|
|
99
|
+
registry.restore();
|
|
100
|
+
env.dispose();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("pauses on the hook's deny, then completes on the approved re-issue", async () => {
|
|
104
|
+
// ── Arrange ──────────────────────────────────────────────────────────────
|
|
105
|
+
const workspaceRoot = sessionWorkspaceDir(env);
|
|
106
|
+
const hookDecisions: string[] = [];
|
|
107
|
+
const runHook = (label: string) =>
|
|
108
|
+
step.effect(label, () => {
|
|
109
|
+
hookDecisions.push(runWorkspaceHook(workspaceRoot, hookShell(COMMAND)).permission);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const ev1 = sdkEvents(AGENT_ID, RUN_1);
|
|
113
|
+
const ev2 = sdkEvents(AGENT_ID, RUN_2);
|
|
114
|
+
const agent = new ScriptedCursorAgent({
|
|
115
|
+
agentId: AGENT_ID,
|
|
116
|
+
runIds: [RUN_1, RUN_2],
|
|
117
|
+
observeStep: () => clock.tick(),
|
|
118
|
+
turns: [
|
|
119
|
+
// Turn 1: the SDK runs the hook before executing the gated tool; the
|
|
120
|
+
// hook denies and writes the ledger; the SDK reports the call blocked.
|
|
121
|
+
[
|
|
122
|
+
step.event(ev1.init()),
|
|
123
|
+
step.event(ev1.assistant("I'll run the build now.")),
|
|
124
|
+
step.event(ev1.toolCall(CALL_TURN_1, "shell", "running", SHELL_ARGS)),
|
|
125
|
+
runHook("hook: turn 1 (expect deny)"),
|
|
126
|
+
step.event(ev1.toolCall(CALL_TURN_1, "shell", "error", SHELL_ARGS, "Blocked by hook")),
|
|
127
|
+
// Never reached: the loop detects the denial on the event above and
|
|
128
|
+
// cancels the run. Present so a regression that does NOT cancel is
|
|
129
|
+
// caught by the golden, not by a missing-result throw.
|
|
130
|
+
step.event(ev1.assistant("The build was blocked.")),
|
|
131
|
+
step.finished({ result: "The build was blocked." }),
|
|
132
|
+
],
|
|
133
|
+
// Turn 2 (resumed): the hook runs again for the re-issued call and now
|
|
134
|
+
// allows it (the grant); the tool runs under a FRESH call id.
|
|
135
|
+
[
|
|
136
|
+
runHook("hook: turn 2 (expect allow)"),
|
|
137
|
+
step.event(ev2.toolCall(CALL_TURN_2, "shell", "running", SHELL_ARGS)),
|
|
138
|
+
step.event(ev2.toolCall(CALL_TURN_2, "shell", "completed", SHELL_ARGS, BUILD_OUTPUT)),
|
|
139
|
+
step.event(ev2.assistant(FINAL_TEXT)),
|
|
140
|
+
step.turnEnded({ inputTokens: 3_100, outputTokens: 60 }),
|
|
141
|
+
step.finished({ result: FINAL_TEXT, model: { id: FIXTURE.model, params: [] } }),
|
|
142
|
+
],
|
|
143
|
+
],
|
|
144
|
+
});
|
|
145
|
+
const record = cursorExecutionRecord({ message: USER_MESSAGE });
|
|
146
|
+
const scenario = beginCursorScenario({
|
|
147
|
+
env,
|
|
148
|
+
clock,
|
|
149
|
+
record,
|
|
150
|
+
sdk: { agents: [agent], catalog: SDK_CATALOG },
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ── Act 1: the first invocation ──────────────────────────────────────────
|
|
154
|
+
const turn1 = await runCursorTurn(scenario, { threadId: "", turnSeq: 0 });
|
|
155
|
+
|
|
156
|
+
// ── Assert 1: paused for approval, returned, agent parked ────────────────
|
|
157
|
+
expect(hookDecisions, "the real hook denied the gated shell call").toEqual(["deny"]);
|
|
158
|
+
expect(turn1.outcome.kind, "an approval pause RETURNS to the workflow").toBe("returned");
|
|
159
|
+
expect((turn1.outcome as { value: Record<string, unknown> }).value.phase).toBe(
|
|
160
|
+
"EXECUTION_WAITING_FOR_APPROVAL",
|
|
161
|
+
);
|
|
162
|
+
expect(record.persistedPhases).toEqual([
|
|
163
|
+
ExecutionPhase.EXECUTION_IN_PROGRESS,
|
|
164
|
+
ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL,
|
|
165
|
+
]);
|
|
166
|
+
expect(agent.runs[0].cancelCalls, "the first denial cancels the run exactly once").toHaveLength(1);
|
|
167
|
+
expect(agent.closeCalls, "the agent is parked for the resume, not closed").toBe(0);
|
|
168
|
+
|
|
169
|
+
const waiting = record.waitingToolCalls();
|
|
170
|
+
expect(waiting, "exactly one gate row").toHaveLength(1);
|
|
171
|
+
expect(waiting[0].id).toBe(CALL_TURN_1);
|
|
172
|
+
expect(waiting[0].name).toBe("shell");
|
|
173
|
+
expect(waiting[0].requiresApproval).toBe(true);
|
|
174
|
+
// The gate row carries what the HOOK saw (the ledger's captured tool_input:
|
|
175
|
+
// command, cwd, timeout), overlaid on the stream's args — the golden pins
|
|
176
|
+
// the full shape; here only the shared salient field is asserted.
|
|
177
|
+
expect(waiting[0].args).toMatchObject(SHELL_ARGS);
|
|
178
|
+
// The model's post-denial narration never reaches the user as fact.
|
|
179
|
+
expect(record.status?.messages.some((m) => m.content === "The build was blocked.")).toBe(false);
|
|
180
|
+
|
|
181
|
+
const turn1Json = JSON.stringify(toJson(AgentExecutionStatusSchema, record.lastFullStatus!), null, 2) + "\n";
|
|
182
|
+
await expect(turn1Json).toMatchFileSnapshot("./goldens/deny-and-retry.turn1.status.json");
|
|
183
|
+
|
|
184
|
+
// ── The user approves (the server's SubmitApproval effect on the row) ────
|
|
185
|
+
clock.tick();
|
|
186
|
+
expect(record.decideWaitingToolCalls(ApprovalAction.APPROVE, new Date().toISOString())).toBe(1);
|
|
187
|
+
|
|
188
|
+
// ── Act 2: the reinvocation ──────────────────────────────────────────────
|
|
189
|
+
const turn2 = await runCursorTurn(scenario, { threadId: AGENT_ID, turnSeq: 1 });
|
|
190
|
+
|
|
191
|
+
// ── Assert 2: allowed, reconciled, completed ─────────────────────────────
|
|
192
|
+
expect(hookDecisions, "the SAME hook allows the approved re-issue").toEqual(["deny", "allow"]);
|
|
193
|
+
expect(turn2.outcome.kind).toBe("returned");
|
|
194
|
+
const slim2 = (turn2.outcome as { value: Record<string, unknown> }).value;
|
|
195
|
+
expect(slim2.phase).toBe("EXECUTION_COMPLETED");
|
|
196
|
+
expect(slim2.final_text).toBe(FINAL_TEXT);
|
|
197
|
+
expect(record.persistedPhases).toEqual([
|
|
198
|
+
ExecutionPhase.EXECUTION_IN_PROGRESS,
|
|
199
|
+
ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL,
|
|
200
|
+
ExecutionPhase.EXECUTION_IN_PROGRESS,
|
|
201
|
+
ExecutionPhase.EXECUTION_COMPLETED,
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
// #215: the resume checks the parked agent out of the session cache; the
|
|
205
|
+
// SDK is asked to create once for the whole round trip and never to resume.
|
|
206
|
+
expect(scenario.sdk.resolutions.map((r) => r.kind)).toEqual(["create"]);
|
|
207
|
+
expect(agent.sends).toHaveLength(2);
|
|
208
|
+
|
|
209
|
+
// Each tool gates exactly once: the committed row survives under its
|
|
210
|
+
// ORIGINAL id, now COMPLETED with the tool's result; the fresh call id the
|
|
211
|
+
// model emitted is reconciled onto it, never a second row.
|
|
212
|
+
const rows = record.toolCalls();
|
|
213
|
+
expect(rows.filter((tc) => tc.name === "shell")).toHaveLength(1);
|
|
214
|
+
const shell = rows.find((tc) => tc.name === "shell")!;
|
|
215
|
+
expect(shell.id).toBe(CALL_TURN_1);
|
|
216
|
+
expect(shell.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
|
|
217
|
+
expect(shell.result).toBe(BUILD_OUTPUT);
|
|
218
|
+
expect(shell.approvalAction, "the decision survives on the row").toBe(ApprovalAction.APPROVE);
|
|
219
|
+
expect(rows.some((tc) => tc.id === CALL_TURN_2)).toBe(false);
|
|
220
|
+
expect(record.waitingToolCalls()).toHaveLength(0);
|
|
221
|
+
|
|
222
|
+
// ── Assert: hermeticity ──────────────────────────────────────────────────
|
|
223
|
+
expect(registry.urls.every((u) => u.includes("/model-registry"))).toBe(true);
|
|
224
|
+
|
|
225
|
+
const turn2Json = JSON.stringify(toJson(AgentExecutionStatusSchema, record.lastFullStatus!), null, 2) + "\n";
|
|
226
|
+
await expect(turn2Json).toMatchFileSnapshot("./goldens/deny-and-retry.turn2.status.json");
|
|
227
|
+
});
|
|
228
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hermetic golden: a FILE-REVIEW CAPTURE turn on a git workspace through the
|
|
3
|
+
* whole `ExecuteCursor` activity.
|
|
4
|
+
*
|
|
5
|
+
* Invariant pinned (apply-then-review, the universal file-review model): on a
|
|
6
|
+
* git work tree the file edit FLOWS — the hook the activity installed ALLOWS
|
|
7
|
+
* the write (capture mode gates only gitignored paths, shell and MCP) — and the
|
|
8
|
+
* turn boundary captures the net change from the git diff against the baseline
|
|
9
|
+
* tree pinned before the agent ran, authors CANDIDATE_CAPTURED to the
|
|
10
|
+
* file_review ledger with one card for the edited file, stamps the flowed edit
|
|
11
|
+
* row with the change-set id, and the activity persists WAITING_FOR_APPROVAL
|
|
12
|
+
* and RETURNS without consulting `run.wait()`. The change-set id is the
|
|
13
|
+
* deterministic `executionId:turnSeq`. The golden
|
|
14
|
+
* (`goldens/file-review-capture.status.json`) pins the ledger shape S2 must
|
|
15
|
+
* reproduce.
|
|
16
|
+
*
|
|
17
|
+
* Parent phase rows exercised beyond `deny-and-retry`: provision a `local_path`
|
|
18
|
+
* workspace entry (mounted as-is in local mode); capture-mode derivation on a
|
|
19
|
+
* git tree; the baseline pin (`captureBaselineToLedger`, git snapshot); the
|
|
20
|
+
* turn boundary's candidate capture (`captureTurnToLedger`) and row stamping;
|
|
21
|
+
* the pause for review with file cards.
|
|
22
|
+
*
|
|
23
|
+
* Determinism: the fixture repo's commit and tree ids are byte-stable because
|
|
24
|
+
* `initGitWorkspace` pins author, committer and both dates. The stream's edit
|
|
25
|
+
* args use the REPO-RELATIVE path so no temp directory can reach the golden;
|
|
26
|
+
* the hook input carries the absolute path the SDK would pass (the hook's
|
|
27
|
+
* `git check-ignore` resolves it against the baked workspace root).
|
|
28
|
+
*
|
|
29
|
+
* Skipped where `bash` is unavailable — reported as SKIPPED, never a silent pass.
|
|
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 { readFileSync, writeFileSync } from "node:fs";
|
|
37
|
+
import { join } from "node:path";
|
|
38
|
+
import { toJson } from "@bufbuild/protobuf";
|
|
39
|
+
import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
40
|
+
import { ExecutionPhase, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
41
|
+
|
|
42
|
+
vi.mock("@cursor/sdk", async () =>
|
|
43
|
+
(await import("../../__test-utils__/scripted-sdk.js")).scriptedCursorSdkModule(),
|
|
44
|
+
);
|
|
45
|
+
vi.mock("../../../../client/stigmer-client.js", async () =>
|
|
46
|
+
(await import("../../../../__test-utils__/hermetic-activity.js")).hermeticStigmerClientModule(),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
import {
|
|
50
|
+
ScriptedClock,
|
|
51
|
+
createHermeticEnvironment,
|
|
52
|
+
type HermeticEnvironment,
|
|
53
|
+
} from "../../../../__test-utils__/hermetic-activity.js";
|
|
54
|
+
import { hasBash, hookWrite } from "../../__test-utils__/cursor-hook-harness.js";
|
|
55
|
+
import { ScriptedCursorAgent, sdkEvents, step } from "../../__test-utils__/scripted-agent.js";
|
|
56
|
+
import {
|
|
57
|
+
FIXTURE,
|
|
58
|
+
SDK_CATALOG,
|
|
59
|
+
beginCursorScenario,
|
|
60
|
+
cursorExecutionRecord,
|
|
61
|
+
initGitWorkspace,
|
|
62
|
+
localPathEntry,
|
|
63
|
+
runCursorTurn,
|
|
64
|
+
runWorkspaceHook,
|
|
65
|
+
stubRegistryFetch,
|
|
66
|
+
} from "../../__test-utils__/hermetic-cursor.js";
|
|
67
|
+
|
|
68
|
+
const AGENT_ID = "agent-hermetic-capture-0001";
|
|
69
|
+
const RUN_ID = "run-hermetic-capture-0001";
|
|
70
|
+
const CALL_ID = "call-hermetic-edit-0001";
|
|
71
|
+
const USER_MESSAGE = "Add a line to notes.md saying the fixture was reviewed.";
|
|
72
|
+
const FILE = "notes.md";
|
|
73
|
+
const BEFORE = "# Notes\n\nA fixture file.\n";
|
|
74
|
+
const AFTER = "# Notes\n\nA fixture file.\n\nReviewed by the hermetic run.\n";
|
|
75
|
+
const CHANGE_SET_ID = `${FIXTURE.executionId}:0`;
|
|
76
|
+
|
|
77
|
+
describe.skipIf(!hasBash)("ExecuteCursor hermetic — file-review capture on a git workspace", () => {
|
|
78
|
+
let env: HermeticEnvironment;
|
|
79
|
+
let registry: ReturnType<typeof stubRegistryFetch>;
|
|
80
|
+
const clock = new ScriptedClock();
|
|
81
|
+
|
|
82
|
+
beforeAll(() => {
|
|
83
|
+
env = createHermeticEnvironment();
|
|
84
|
+
registry = stubRegistryFetch();
|
|
85
|
+
clock.install();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
afterAll(() => {
|
|
89
|
+
clock.uninstall();
|
|
90
|
+
registry.restore();
|
|
91
|
+
env.dispose();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("lets the edit flow, captures it at the boundary, and pauses for review", async () => {
|
|
95
|
+
// ── Arrange ──────────────────────────────────────────────────────────────
|
|
96
|
+
const repo = join(env.workspaceRootDir, "repos", "notes");
|
|
97
|
+
initGitWorkspace(repo, { [FILE]: BEFORE });
|
|
98
|
+
const absFile = join(repo, FILE);
|
|
99
|
+
const hookDecisions: string[] = [];
|
|
100
|
+
|
|
101
|
+
const ev = sdkEvents(AGENT_ID, RUN_ID);
|
|
102
|
+
const agent = new ScriptedCursorAgent({
|
|
103
|
+
agentId: AGENT_ID,
|
|
104
|
+
runIds: [RUN_ID],
|
|
105
|
+
observeStep: () => clock.tick(),
|
|
106
|
+
turns: [
|
|
107
|
+
[
|
|
108
|
+
step.event(ev.init()),
|
|
109
|
+
step.event(ev.assistant("Appending the review line.")),
|
|
110
|
+
// The SDK runs the hook before the write tool executes...
|
|
111
|
+
step.effect("hook: Write (expect allow in capture mode)", () => {
|
|
112
|
+
hookDecisions.push(runWorkspaceHook(repo, hookWrite(absFile, AFTER)).permission);
|
|
113
|
+
}),
|
|
114
|
+
// ...then performs the write itself...
|
|
115
|
+
step.effect("write notes.md", () => writeFileSync(absFile, AFTER, "utf-8")),
|
|
116
|
+
// ...and streams the tool call.
|
|
117
|
+
step.event(ev.toolCall(CALL_ID, "edit", "running", { path: FILE, content: AFTER })),
|
|
118
|
+
step.event(ev.toolCall(CALL_ID, "edit", "completed", { path: FILE, content: AFTER }, "ok")),
|
|
119
|
+
step.event(ev.assistant("Done — notes.md now records the review.")),
|
|
120
|
+
step.turnEnded({ inputTokens: 2_400, outputTokens: 70 }),
|
|
121
|
+
step.finished({ result: "Done", model: { id: FIXTURE.model, params: [] } }),
|
|
122
|
+
],
|
|
123
|
+
],
|
|
124
|
+
});
|
|
125
|
+
const record = cursorExecutionRecord({
|
|
126
|
+
message: USER_MESSAGE,
|
|
127
|
+
workspaceEntries: [localPathEntry("notes", repo)],
|
|
128
|
+
});
|
|
129
|
+
const scenario = beginCursorScenario({
|
|
130
|
+
env,
|
|
131
|
+
clock,
|
|
132
|
+
record,
|
|
133
|
+
sdk: { agents: [agent], catalog: SDK_CATALOG },
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ── Act ──────────────────────────────────────────────────────────────────
|
|
137
|
+
const invocation = await runCursorTurn(scenario);
|
|
138
|
+
|
|
139
|
+
// ── Assert: the edit flowed and the turn paused for review ───────────────
|
|
140
|
+
expect(hookDecisions, "capture mode lets the write flow").toEqual(["allow"]);
|
|
141
|
+
expect(readFileSync(absFile, "utf-8"), "the working tree holds the candidate").toBe(AFTER);
|
|
142
|
+
expect(invocation.outcome.kind, "a review pause RETURNS to the workflow").toBe("returned");
|
|
143
|
+
expect((invocation.outcome as { value: Record<string, unknown> }).value.phase).toBe(
|
|
144
|
+
"EXECUTION_WAITING_FOR_APPROVAL",
|
|
145
|
+
);
|
|
146
|
+
expect(record.persistedPhases).toEqual([
|
|
147
|
+
ExecutionPhase.EXECUTION_IN_PROGRESS,
|
|
148
|
+
ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL,
|
|
149
|
+
]);
|
|
150
|
+
expect(agent.runs[0].cancelCalls, "a flowed edit is not a denial; nothing is cancelled").toHaveLength(0);
|
|
151
|
+
|
|
152
|
+
// ── Assert: the file_review ledger ───────────────────────────────────────
|
|
153
|
+
const events = record.status?.fileReviewEventStream?.events ?? [];
|
|
154
|
+
const types = events.map((e) => e.payload.case);
|
|
155
|
+
expect(types).toEqual(["baselineCaptured", "candidateCaptured"]);
|
|
156
|
+
const candidate = events[1].payload;
|
|
157
|
+
if (candidate.case !== "candidateCaptured") throw new Error("unreachable");
|
|
158
|
+
expect(candidate.value.changeSetId).toBe(CHANGE_SET_ID);
|
|
159
|
+
expect(candidate.value.changes.map((c) => c.pathAfter)).toEqual([FILE]);
|
|
160
|
+
|
|
161
|
+
// The flowed edit row: the built-in policy still classifies `edit` as a
|
|
162
|
+
// gated category (`requiresApproval: true` is the policy's verdict, stamped
|
|
163
|
+
// at fold time), but capture mode let it FLOW — so the row is COMPLETED,
|
|
164
|
+
// never WAITING_APPROVAL, and the review happens on the file card instead.
|
|
165
|
+
const rows = record.toolCalls();
|
|
166
|
+
expect(rows).toHaveLength(1);
|
|
167
|
+
expect(rows[0].name).toBe("edit");
|
|
168
|
+
expect(rows[0].status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
|
|
169
|
+
expect(rows[0].requiresApproval, "policy verdict, not the gate outcome").toBe(true);
|
|
170
|
+
expect(record.waitingToolCalls(), "no gate row — the review is on the file card").toHaveLength(0);
|
|
171
|
+
|
|
172
|
+
// ── Assert: hermeticity ──────────────────────────────────────────────────
|
|
173
|
+
expect(registry.urls.every((u) => u.includes("/model-registry"))).toBe(true);
|
|
174
|
+
|
|
175
|
+
// ── Assert: the golden ───────────────────────────────────────────────────
|
|
176
|
+
const json = JSON.stringify(toJson(AgentExecutionStatusSchema, record.lastFullStatus!), null, 2) + "\n";
|
|
177
|
+
expect(json, "no temp path may reach the golden").not.toContain(env.workspaceRootDir);
|
|
178
|
+
await expect(json).toMatchFileSnapshot("./goldens/file-review-capture.status.json");
|
|
179
|
+
});
|
|
180
|
+
});
|
package/src/activities/execute-cursor/__tests__/hermetic/goldens/deny-and-retry.turn1.status.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"messages": [
|
|
3
|
+
{
|
|
4
|
+
"type": "MESSAGE_AI",
|
|
5
|
+
"content": "I'll run the build now.",
|
|
6
|
+
"timestamp": "2026-01-01T00:00:02.000Z",
|
|
7
|
+
"toolCalls": [
|
|
8
|
+
{
|
|
9
|
+
"id": "call-hermetic-shell-0001",
|
|
10
|
+
"name": "shell",
|
|
11
|
+
"args": {
|
|
12
|
+
"command": "npm run build",
|
|
13
|
+
"cwd": "/x",
|
|
14
|
+
"timeout": 30000
|
|
15
|
+
},
|
|
16
|
+
"status": "TOOL_CALL_WAITING_APPROVAL",
|
|
17
|
+
"startedAt": "2026-01-01T00:00:03.000Z",
|
|
18
|
+
"requiresApproval": true,
|
|
19
|
+
"approvalMessage": "Run command: npm run build",
|
|
20
|
+
"approvalRequestedAt": "2026-01-01T00:00:05.000Z",
|
|
21
|
+
"argsPreview": "{\"command\":\"npm run build\",\"cwd\":\"/x\",\"timeout\":30000}",
|
|
22
|
+
"toolKind": "TOOL_KIND_SHELL",
|
|
23
|
+
"approvalPolicySource": "APPROVAL_POLICY_SOURCE_BUILTIN_CATEGORY",
|
|
24
|
+
"policyEngineVersion": "phase-7"
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"phase": "EXECUTION_WAITING_FOR_APPROVAL",
|
|
30
|
+
"startedAt": "2026-01-01T00:00:00.000Z",
|
|
31
|
+
"fileReviewEventStream": {
|
|
32
|
+
"executionId": "aex_hermetic_0001",
|
|
33
|
+
"events": [
|
|
34
|
+
{
|
|
35
|
+
"eventId": "aex_hermetic_0001:0:aex_hermetic_0001:0:FILE_REVIEW_EVENT_TYPE_BASELINE_CAPTURED",
|
|
36
|
+
"changeSetId": "aex_hermetic_0001:0",
|
|
37
|
+
"eventType": "FILE_REVIEW_EVENT_TYPE_BASELINE_CAPTURED",
|
|
38
|
+
"timestamp": "2026-01-01T00:00:00.000Z",
|
|
39
|
+
"actor": "runner",
|
|
40
|
+
"baselineCaptured": {
|
|
41
|
+
"changeSetId": "aex_hermetic_0001:0",
|
|
42
|
+
"turnId": "aex_hermetic_0001:0",
|
|
43
|
+
"harnessId": "cursor",
|
|
44
|
+
"baselineSnapshot": {
|
|
45
|
+
"kind": "SNAPSHOT_KIND_CAS_MANIFEST"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
"fileChangeProgress": {
|
|
52
|
+
"changeSetId": "aex_hermetic_0001:0",
|
|
53
|
+
"capturedAt": "2026-01-01T00:00:01.000Z"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/activities/execute-cursor/__tests__/hermetic/goldens/deny-and-retry.turn2.status.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"messages": [
|
|
3
|
+
{
|
|
4
|
+
"type": "MESSAGE_AI",
|
|
5
|
+
"content": "I'll run the build now.",
|
|
6
|
+
"timestamp": "2026-01-01T00:00:02.000Z",
|
|
7
|
+
"toolCalls": [
|
|
8
|
+
{
|
|
9
|
+
"id": "call-hermetic-shell-0001",
|
|
10
|
+
"name": "shell",
|
|
11
|
+
"args": {
|
|
12
|
+
"command": "npm run build",
|
|
13
|
+
"cwd": "/x",
|
|
14
|
+
"timeout": 30000
|
|
15
|
+
},
|
|
16
|
+
"result": "build ok (hermetic)\n",
|
|
17
|
+
"status": "TOOL_CALL_COMPLETED",
|
|
18
|
+
"startedAt": "2026-01-01T00:00:03.000Z",
|
|
19
|
+
"completedAt": "2026-01-01T00:00:09.000Z",
|
|
20
|
+
"requiresApproval": true,
|
|
21
|
+
"approvalMessage": "Run command: npm run build",
|
|
22
|
+
"approvalRequestedAt": "2026-01-01T00:00:05.000Z",
|
|
23
|
+
"approvalDecidedAt": "2026-01-01T00:00:06.000Z",
|
|
24
|
+
"approvalAction": "APPROVAL_ACTION_APPROVE",
|
|
25
|
+
"argsPreview": "{\"command\":\"npm run build\",\"cwd\":\"/x\",\"timeout\":30000}",
|
|
26
|
+
"toolKind": "TOOL_KIND_SHELL",
|
|
27
|
+
"approvalPolicySource": "APPROVAL_POLICY_SOURCE_BUILTIN_CATEGORY",
|
|
28
|
+
"policyEngineVersion": "phase-7"
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"type": "MESSAGE_AI",
|
|
34
|
+
"content": "The build passes.",
|
|
35
|
+
"timestamp": "2026-01-01T00:00:10.000Z"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"phase": "EXECUTION_COMPLETED",
|
|
39
|
+
"startedAt": "2026-01-01T00:00:06.000Z",
|
|
40
|
+
"completedAt": "2026-01-01T00:00:12.000Z",
|
|
41
|
+
"streamingUsage": {
|
|
42
|
+
"inputTokens": "3100",
|
|
43
|
+
"outputTokens": "60",
|
|
44
|
+
"totalTokens": "3160",
|
|
45
|
+
"turnCount": 1,
|
|
46
|
+
"estimatedCostUsd": 0.00334,
|
|
47
|
+
"model": "composer-2.5",
|
|
48
|
+
"observedAt": "2026-01-01T00:00:11.000Z",
|
|
49
|
+
"requestedServiceTier": "SERVICE_TIER_STANDARD",
|
|
50
|
+
"requestedModelParams": "[{\"id\":\"fast\",\"value\":\"false\"},{\"id\":\"thinking\",\"value\":\"false\"}]",
|
|
51
|
+
"requestedThinkingMode": "THINKING_MODE_DISABLED"
|
|
52
|
+
},
|
|
53
|
+
"fileReviewEventStream": {
|
|
54
|
+
"executionId": "aex_hermetic_0001",
|
|
55
|
+
"events": [
|
|
56
|
+
{
|
|
57
|
+
"eventId": "aex_hermetic_0001:1:aex_hermetic_0001:1:FILE_REVIEW_EVENT_TYPE_BASELINE_CAPTURED",
|
|
58
|
+
"changeSetId": "aex_hermetic_0001:1",
|
|
59
|
+
"eventType": "FILE_REVIEW_EVENT_TYPE_BASELINE_CAPTURED",
|
|
60
|
+
"timestamp": "2026-01-01T00:00:06.000Z",
|
|
61
|
+
"actor": "runner",
|
|
62
|
+
"baselineCaptured": {
|
|
63
|
+
"changeSetId": "aex_hermetic_0001:1",
|
|
64
|
+
"turnId": "aex_hermetic_0001:1",
|
|
65
|
+
"harnessId": "cursor",
|
|
66
|
+
"baselineSnapshot": {
|
|
67
|
+
"kind": "SNAPSHOT_KIND_CAS_MANIFEST"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
"fileChangeProgress": {
|
|
74
|
+
"changeSetId": "aex_hermetic_0001:1",
|
|
75
|
+
"capturedAt": "2026-01-01T00:00:08.000Z"
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/activities/execute-cursor/__tests__/hermetic/goldens/file-review-capture.status.json
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
{
|
|
2
|
+
"messages": [
|
|
3
|
+
{
|
|
4
|
+
"type": "MESSAGE_AI",
|
|
5
|
+
"content": "Appending the review line.",
|
|
6
|
+
"timestamp": "2026-01-01T00:00:02.000Z",
|
|
7
|
+
"toolCalls": [
|
|
8
|
+
{
|
|
9
|
+
"id": "call-hermetic-edit-0001",
|
|
10
|
+
"name": "edit",
|
|
11
|
+
"args": {
|
|
12
|
+
"path": "notes.md",
|
|
13
|
+
"content": "# Notes\n\nA fixture file.\n\nReviewed by the hermetic run.\n"
|
|
14
|
+
},
|
|
15
|
+
"result": "ok",
|
|
16
|
+
"status": "TOOL_CALL_COMPLETED",
|
|
17
|
+
"startedAt": "2026-01-01T00:00:05.000Z",
|
|
18
|
+
"completedAt": "2026-01-01T00:00:06.000Z",
|
|
19
|
+
"requiresApproval": true,
|
|
20
|
+
"approvalMessage": "Write file: notes.md",
|
|
21
|
+
"argsPreview": "{\"path\":\"notes.md\",\"content\":\"# Notes\\n\\nA fixture file.\\n\\nReviewed by the hermetic run.\\n\"}",
|
|
22
|
+
"toolKind": "TOOL_KIND_FILE_EDIT",
|
|
23
|
+
"approvalPolicySource": "APPROVAL_POLICY_SOURCE_BUILTIN_CATEGORY",
|
|
24
|
+
"policyEngineVersion": "phase-7",
|
|
25
|
+
"fileChangeSetId": "aex_hermetic_0001:0"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"type": "MESSAGE_AI",
|
|
31
|
+
"content": "Done — notes.md now records the review.",
|
|
32
|
+
"timestamp": "2026-01-01T00:00:07.000Z"
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"phase": "EXECUTION_WAITING_FOR_APPROVAL",
|
|
36
|
+
"startedAt": "2026-01-01T00:00:00.000Z",
|
|
37
|
+
"streamingUsage": {
|
|
38
|
+
"inputTokens": "2400",
|
|
39
|
+
"outputTokens": "70",
|
|
40
|
+
"totalTokens": "2470",
|
|
41
|
+
"turnCount": 1,
|
|
42
|
+
"estimatedCostUsd": 0.00268,
|
|
43
|
+
"model": "composer-2.5",
|
|
44
|
+
"observedAt": "2026-01-01T00:00:08.000Z",
|
|
45
|
+
"requestedServiceTier": "SERVICE_TIER_STANDARD",
|
|
46
|
+
"requestedModelParams": "[{\"id\":\"fast\",\"value\":\"false\"},{\"id\":\"thinking\",\"value\":\"false\"}]",
|
|
47
|
+
"requestedThinkingMode": "THINKING_MODE_DISABLED"
|
|
48
|
+
},
|
|
49
|
+
"fileReviewEventStream": {
|
|
50
|
+
"executionId": "aex_hermetic_0001",
|
|
51
|
+
"events": [
|
|
52
|
+
{
|
|
53
|
+
"eventId": "aex_hermetic_0001:0:aex_hermetic_0001:0:FILE_REVIEW_EVENT_TYPE_BASELINE_CAPTURED",
|
|
54
|
+
"changeSetId": "aex_hermetic_0001:0",
|
|
55
|
+
"eventType": "FILE_REVIEW_EVENT_TYPE_BASELINE_CAPTURED",
|
|
56
|
+
"timestamp": "2026-01-01T00:00:00.000Z",
|
|
57
|
+
"actor": "runner",
|
|
58
|
+
"baselineCaptured": {
|
|
59
|
+
"changeSetId": "aex_hermetic_0001:0",
|
|
60
|
+
"turnId": "aex_hermetic_0001:0",
|
|
61
|
+
"harnessId": "cursor",
|
|
62
|
+
"baselineSnapshot": {
|
|
63
|
+
"kind": "SNAPSHOT_KIND_GIT_TREE_REF",
|
|
64
|
+
"git": {
|
|
65
|
+
"treeOid": "06669a7bfb7a3e46bda2c295d6851e52e8f778c6",
|
|
66
|
+
"ref": "refs/stigmer/baseline/aex_hermetic_0001"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"eventId": "aex_hermetic_0001:0:aex_hermetic_0001:0:FILE_REVIEW_EVENT_TYPE_CANDIDATE_CAPTURED",
|
|
73
|
+
"changeSetId": "aex_hermetic_0001:0",
|
|
74
|
+
"eventType": "FILE_REVIEW_EVENT_TYPE_CANDIDATE_CAPTURED",
|
|
75
|
+
"timestamp": "2026-01-01T00:00:09.000Z",
|
|
76
|
+
"actor": "runner",
|
|
77
|
+
"candidateCaptured": {
|
|
78
|
+
"changeSetId": "aex_hermetic_0001:0",
|
|
79
|
+
"candidateSnapshot": {
|
|
80
|
+
"kind": "SNAPSHOT_KIND_GIT_TREE_REF",
|
|
81
|
+
"git": {
|
|
82
|
+
"treeOid": "2bb46c28a7498d578d76b52fa1cd9c941032e787",
|
|
83
|
+
"ref": "refs/stigmer/capture/aex_hermetic_0001"
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"changes": [
|
|
87
|
+
{
|
|
88
|
+
"id": "aex_hermetic_0001:0:notes.md",
|
|
89
|
+
"pathBefore": "notes.md",
|
|
90
|
+
"pathAfter": "notes.md",
|
|
91
|
+
"kind": "FILE_CHANGE_KIND_MODIFY",
|
|
92
|
+
"captureClass": "FILE_CAPTURE_CLASS_GIT_TRACKED",
|
|
93
|
+
"before": {
|
|
94
|
+
"inline": "# Notes\n\nA fixture file.\n"
|
|
95
|
+
},
|
|
96
|
+
"after": {
|
|
97
|
+
"inline": "# Notes\n\nA fixture file.\n\nReviewed by the hermetic run.\n"
|
|
98
|
+
},
|
|
99
|
+
"beforeSha256": "0db28d643539d6a16370a30971d73a2ee0f9509dec6cbb5a143373d5ad52b83f",
|
|
100
|
+
"afterSha256": "96e76ecee9e8af5ef051ad2edaeeed6fbc989a50850a3ef1bcf518115fd1c097",
|
|
101
|
+
"diffComplete": true,
|
|
102
|
+
"fileDigest": "10056ad414223572125b6f8a148e79dbed10b58e18957d213992e79f475bedef",
|
|
103
|
+
"linesAdded": 2
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
"aggregateDigest": "0a2af1c9c6e6f36e81beb770fdeea30860686096a409eb6df47d34d8e46737b9",
|
|
107
|
+
"diffCompleteness": "DIFF_COMPLETENESS_COMPLETE"
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
},
|
|
112
|
+
"fileChangeProgress": {
|
|
113
|
+
"changeSetId": "aex_hermetic_0001:0",
|
|
114
|
+
"filesChanged": 1,
|
|
115
|
+
"linesAdded": 2,
|
|
116
|
+
"entries": [
|
|
117
|
+
{
|
|
118
|
+
"pathBefore": "notes.md",
|
|
119
|
+
"pathAfter": "notes.md",
|
|
120
|
+
"kind": "FILE_CHANGE_KIND_MODIFY",
|
|
121
|
+
"linesAdded": 2
|
|
122
|
+
}
|
|
123
|
+
],
|
|
124
|
+
"capturedAt": "2026-01-01T00:00:05.000Z"
|
|
125
|
+
}
|
|
126
|
+
}
|