@stigmer/runner 3.1.2 → 3.1.4
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/activities/execute-cursor/attachment-resolver.d.ts +1 -1
- package/dist/activities/execute-cursor/attachment-resolver.js +1 -1
- package/dist/activities/execute-cursor/capture-flow.d.ts +29 -0
- package/dist/activities/execute-cursor/capture-flow.js +59 -0
- package/dist/activities/execute-cursor/capture-flow.js.map +1 -1
- package/dist/activities/execute-cursor/command-provenance.d.ts +11 -25
- package/dist/activities/execute-cursor/command-provenance.js +25 -115
- package/dist/activities/execute-cursor/command-provenance.js.map +1 -1
- package/dist/activities/execute-cursor/index.js +358 -557
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/todo-tracker.d.ts +6 -1
- package/dist/activities/execute-cursor/todo-tracker.js +15 -43
- package/dist/activities/execute-cursor/todo-tracker.js.map +1 -1
- package/dist/activities/execute-cursor/turn-boundary.d.ts +108 -0
- package/dist/activities/execute-cursor/turn-boundary.js +163 -0
- package/dist/activities/execute-cursor/turn-boundary.js.map +1 -0
- package/dist/activities/execute-cursor/turn-stream.d.ts +141 -0
- package/dist/activities/execute-cursor/turn-stream.js +249 -0
- package/dist/activities/execute-cursor/turn-stream.js.map +1 -0
- package/dist/activities/execute-deep-agent/command-provenance.d.ts +61 -0
- package/dist/activities/execute-deep-agent/command-provenance.js +72 -0
- package/dist/activities/execute-deep-agent/command-provenance.js.map +1 -0
- package/dist/activities/execute-deep-agent/index.js +73 -4
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/status-builder.js +8 -1
- package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming-v3.js +6 -1
- package/dist/activities/execute-deep-agent/streaming-v3.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming.d.ts +7 -0
- package/dist/activities/execute-deep-agent/streaming.js +5 -1
- package/dist/activities/execute-deep-agent/streaming.js.map +1 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.js +12 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
- package/dist/otel.js +10 -0
- package/dist/otel.js.map +1 -1
- package/dist/shared/filereview/capture.d.ts +18 -1
- package/dist/shared/filereview/capture.js +105 -28
- package/dist/shared/filereview/capture.js.map +1 -1
- package/dist/shared/filereview/cas-progress.d.ts +63 -0
- package/dist/shared/filereview/cas-progress.js +128 -0
- package/dist/shared/filereview/cas-progress.js.map +1 -0
- package/dist/shared/filereview/cas-substrate.d.ts +38 -0
- package/dist/shared/filereview/cas-substrate.js +48 -15
- package/dist/shared/filereview/cas-substrate.js.map +1 -1
- package/dist/shared/filereview/command-provenance.d.ts +93 -0
- package/dist/shared/filereview/command-provenance.js +132 -0
- package/dist/shared/filereview/command-provenance.js.map +1 -0
- package/dist/shared/filereview/events.d.ts +16 -0
- package/dist/shared/filereview/events.js +37 -0
- package/dist/shared/filereview/events.js.map +1 -1
- package/dist/shared/filereview/git-substrate.d.ts +48 -0
- package/dist/shared/filereview/git-substrate.js +78 -0
- package/dist/shared/filereview/git-substrate.js.map +1 -1
- package/dist/shared/filereview/index.d.ts +5 -2
- package/dist/shared/filereview/index.js +5 -2
- package/dist/shared/filereview/index.js.map +1 -1
- package/dist/shared/filereview/line-counts.d.ts +48 -0
- package/dist/shared/filereview/line-counts.js +72 -0
- package/dist/shared/filereview/line-counts.js.map +1 -0
- package/dist/shared/filereview/progress.d.ts +167 -0
- package/dist/shared/filereview/progress.js +196 -0
- package/dist/shared/filereview/progress.js.map +1 -0
- package/dist/shared/implement-plan-prompt.d.ts +6 -4
- package/dist/shared/implement-plan-prompt.js +38 -6
- package/dist/shared/implement-plan-prompt.js.map +1 -1
- package/dist/shared/plan-artifact.d.ts +59 -11
- package/dist/shared/plan-artifact.js +144 -16
- package/dist/shared/plan-artifact.js.map +1 -1
- package/dist/shared/plan-mode-prompt.d.ts +15 -4
- package/dist/shared/plan-mode-prompt.js +28 -5
- package/dist/shared/plan-mode-prompt.js.map +1 -1
- package/dist/shared/todos.d.ts +56 -0
- package/dist/shared/todos.js +98 -0
- package/dist/shared/todos.js.map +1 -0
- package/dist/shared/tool-row.d.ts +16 -0
- package/dist/shared/tool-row.js +31 -0
- package/dist/shared/tool-row.js.map +1 -1
- package/package.json +3 -2
- package/src/__tests__/otel-turn-span.test.ts +61 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +7 -0
- package/src/activities/execute-cursor/__tests__/delta-enricher.test.ts +68 -0
- package/src/activities/execute-cursor/__tests__/progress-substrate.test.ts +169 -0
- package/src/activities/execute-cursor/__tests__/turn-boundary.test.ts +280 -0
- package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +349 -0
- package/src/activities/execute-cursor/attachment-resolver.ts +1 -1
- package/src/activities/execute-cursor/capture-flow.ts +78 -0
- package/src/activities/execute-cursor/command-provenance.ts +25 -120
- package/src/activities/execute-cursor/index.ts +419 -602
- package/src/activities/execute-cursor/todo-tracker.ts +17 -59
- package/src/activities/execute-cursor/turn-boundary.ts +274 -0
- package/src/activities/execute-cursor/turn-stream.ts +418 -0
- package/src/activities/execute-deep-agent/__tests__/command-provenance.test.ts +252 -0
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +11 -0
- package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +78 -0
- package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +105 -1
- package/src/activities/execute-deep-agent/command-provenance.ts +102 -0
- package/src/activities/execute-deep-agent/index.ts +91 -4
- package/src/activities/execute-deep-agent/status-builder.ts +9 -0
- package/src/activities/execute-deep-agent/streaming-v3.ts +6 -1
- package/src/activities/execute-deep-agent/streaming.ts +12 -1
- package/src/activities/execute-deep-agent/v3-status-builder.ts +13 -0
- package/src/otel.ts +8 -0
- package/src/shared/__tests__/implement-plan-prompt.test.ts +40 -2
- package/src/shared/__tests__/plan-artifact.test.ts +142 -9
- package/src/shared/__tests__/todos.test.ts +216 -0
- package/src/shared/filereview/__tests__/capture.test.ts +182 -1
- package/src/shared/filereview/__tests__/cas-progress.test.ts +228 -0
- package/src/shared/filereview/__tests__/cas-substrate.test.ts +100 -0
- package/src/shared/filereview/__tests__/command-provenance.test.ts +252 -0
- package/src/shared/filereview/__tests__/events.test.ts +105 -0
- package/src/shared/filereview/__tests__/line-counts.test.ts +73 -0
- package/src/shared/filereview/__tests__/progress.test.ts +338 -0
- package/src/shared/filereview/capture.ts +134 -30
- package/src/shared/filereview/cas-progress.ts +170 -0
- package/src/shared/filereview/cas-substrate.ts +79 -18
- package/src/shared/filereview/command-provenance.ts +180 -0
- package/src/shared/filereview/events.ts +50 -0
- package/src/shared/filereview/git-substrate.ts +114 -0
- package/src/shared/filereview/index.ts +36 -1
- package/src/shared/filereview/line-counts.ts +83 -0
- package/src/shared/filereview/progress.ts +310 -0
- package/src/shared/implement-plan-prompt.ts +39 -8
- package/src/shared/plan-artifact.ts +152 -16
- package/src/shared/plan-mode-prompt.ts +28 -5
- package/src/shared/todos.ts +126 -0
- package/src/shared/tool-row.ts +34 -0
|
@@ -138,6 +138,111 @@ describe("buildCapturedFileChange", () => {
|
|
|
138
138
|
}
|
|
139
139
|
});
|
|
140
140
|
|
|
141
|
+
it("stamps display line counts from inline sides", () => {
|
|
142
|
+
const modify = buildCapturedFileChange({
|
|
143
|
+
id: "fc-counts",
|
|
144
|
+
pathBefore: "src/a.ts",
|
|
145
|
+
pathAfter: "src/a.ts",
|
|
146
|
+
kind: FileChangeKind.MODIFY,
|
|
147
|
+
captureClass: FileCaptureClass.GIT_TRACKED,
|
|
148
|
+
before: "one\ntwo\n",
|
|
149
|
+
after: "one\nTWO\nthree\n",
|
|
150
|
+
});
|
|
151
|
+
expect(modify.linesAdded).toBe(2);
|
|
152
|
+
expect(modify.linesRemoved).toBe(1);
|
|
153
|
+
|
|
154
|
+
const add = buildCapturedFileChange({
|
|
155
|
+
id: "fc-add-counts",
|
|
156
|
+
pathBefore: "",
|
|
157
|
+
pathAfter: "src/new.ts",
|
|
158
|
+
kind: FileChangeKind.ADD,
|
|
159
|
+
captureClass: FileCaptureClass.GIT_TRACKED,
|
|
160
|
+
after: "a\nb\nc\n",
|
|
161
|
+
});
|
|
162
|
+
expect(add.linesAdded).toBe(3);
|
|
163
|
+
expect(add.linesRemoved).toBe(0);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("prefers explicit lineCounts (the CAS substrate's capture-time counts) over inline counting", () => {
|
|
167
|
+
const change = buildCapturedFileChange({
|
|
168
|
+
id: "fc-explicit",
|
|
169
|
+
pathBefore: "notes.txt",
|
|
170
|
+
pathAfter: "notes.txt",
|
|
171
|
+
kind: FileChangeKind.MODIFY,
|
|
172
|
+
captureClass: FileCaptureClass.NON_GIT_CAS,
|
|
173
|
+
before: {
|
|
174
|
+
kind: "ref", sha256: "s1", storageKey: "k1", sizeBytes: 10, isBinary: false,
|
|
175
|
+
},
|
|
176
|
+
after: {
|
|
177
|
+
kind: "ref", sha256: "s2", storageKey: "k2", sizeBytes: 12, isBinary: false,
|
|
178
|
+
},
|
|
179
|
+
lineCounts: { linesAdded: 7, linesRemoved: 4 },
|
|
180
|
+
});
|
|
181
|
+
expect(change.linesAdded).toBe(7);
|
|
182
|
+
expect(change.linesRemoved).toBe(4);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("leaves counts at zero when a side is not countable (binary / ref without explicit counts / withheld)", () => {
|
|
186
|
+
// Binary side: no text diff exists, so no count may claim one does.
|
|
187
|
+
const binary = binaryChange("bin");
|
|
188
|
+
expect(binary.linesAdded).toBe(0);
|
|
189
|
+
expect(binary.linesRemoved).toBe(0);
|
|
190
|
+
|
|
191
|
+
// Ref side without explicit counts: the bytes are not here to count.
|
|
192
|
+
const ref = buildCapturedFileChange({
|
|
193
|
+
id: "fc-ref",
|
|
194
|
+
pathBefore: "big.txt",
|
|
195
|
+
pathAfter: "big.txt",
|
|
196
|
+
kind: FileChangeKind.MODIFY,
|
|
197
|
+
captureClass: FileCaptureClass.NON_GIT_CAS,
|
|
198
|
+
before: { kind: "ref", sha256: "s1", storageKey: "k1", sizeBytes: 9, isBinary: false },
|
|
199
|
+
after: { kind: "ref", sha256: "s2", storageKey: "k2", sizeBytes: 9, isBinary: false },
|
|
200
|
+
});
|
|
201
|
+
expect(ref.linesAdded).toBe(0);
|
|
202
|
+
expect(ref.linesRemoved).toBe(0);
|
|
203
|
+
|
|
204
|
+
// Secret-withheld: content-less by design.
|
|
205
|
+
const withheld = unavailableChange("secret");
|
|
206
|
+
expect(withheld.linesAdded).toBe(0);
|
|
207
|
+
expect(withheld.linesRemoved).toBe(0);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("keeps line counts out of the enforcement digests (informational only)", () => {
|
|
211
|
+
const common = {
|
|
212
|
+
id: "fc-digest",
|
|
213
|
+
pathBefore: "a",
|
|
214
|
+
pathAfter: "a",
|
|
215
|
+
kind: FileChangeKind.MODIFY,
|
|
216
|
+
captureClass: FileCaptureClass.NON_GIT_CAS,
|
|
217
|
+
before: {
|
|
218
|
+
kind: "ref", sha256: "s1", storageKey: "k1", sizeBytes: 3, isBinary: false,
|
|
219
|
+
},
|
|
220
|
+
after: {
|
|
221
|
+
kind: "ref", sha256: "s2", storageKey: "k2", sizeBytes: 3, isBinary: false,
|
|
222
|
+
},
|
|
223
|
+
} as const;
|
|
224
|
+
// Identical content with and without counts must be digest-indistinguishable —
|
|
225
|
+
// structurally guaranteed (FileDigestInput excludes counts), locked here so a
|
|
226
|
+
// future refactor cannot silently fold display data into enforcement.
|
|
227
|
+
const withoutCounts = buildCapturedFileChange(common);
|
|
228
|
+
const withCounts = buildCapturedFileChange({
|
|
229
|
+
...common,
|
|
230
|
+
lineCounts: { linesAdded: 5, linesRemoved: 2 },
|
|
231
|
+
});
|
|
232
|
+
expect(withCounts.fileDigest).toBe(withoutCounts.fileDigest);
|
|
233
|
+
|
|
234
|
+
const aggWithout = buildCandidateCapturedEvent(ctx, undefined, [withoutCounts]);
|
|
235
|
+
const aggWith = buildCandidateCapturedEvent(ctx, undefined, [withCounts]);
|
|
236
|
+
if (
|
|
237
|
+
aggWithout.payload.case === "candidateCaptured" &&
|
|
238
|
+
aggWith.payload.case === "candidateCaptured"
|
|
239
|
+
) {
|
|
240
|
+
expect(aggWith.payload.value.aggregateDigest).toBe(
|
|
241
|
+
aggWithout.payload.value.aggregateDigest,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
141
246
|
it("omits the before side for an ADD and the after side for a DELETE", () => {
|
|
142
247
|
const add = buildCapturedFileChange({
|
|
143
248
|
id: "fc-add",
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture-time line counting: the `+N −M` semantics must mirror what the SDK's
|
|
3
|
+
* jsdiff-based diff renderer shows (same algorithm, same "\ No newline"
|
|
4
|
+
* exclusion), with absent sides treated as the empty document and a byte-size
|
|
5
|
+
* guard that skips counting rather than stalling capture.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import { countLineChanges, LINE_COUNT_MAX_BYTES } from "../line-counts.js";
|
|
10
|
+
|
|
11
|
+
describe("countLineChanges", () => {
|
|
12
|
+
it("counts a plain modify", () => {
|
|
13
|
+
expect(countLineChanges("a\nb\nc\n", "a\nB\nc\nd\n")).toEqual({
|
|
14
|
+
linesAdded: 2,
|
|
15
|
+
linesRemoved: 1,
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("counts an ADD (absent before) as all lines added", () => {
|
|
20
|
+
expect(countLineChanges(undefined, "one\ntwo\nthree\n")).toEqual({
|
|
21
|
+
linesAdded: 3,
|
|
22
|
+
linesRemoved: 0,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("counts a DELETE (absent after) as all lines removed", () => {
|
|
27
|
+
expect(countLineChanges("one\ntwo\n", undefined)).toEqual({
|
|
28
|
+
linesAdded: 0,
|
|
29
|
+
linesRemoved: 2,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("returns zero counts for identical sides", () => {
|
|
34
|
+
expect(countLineChanges("same\n", "same\n")).toEqual({
|
|
35
|
+
linesAdded: 0,
|
|
36
|
+
linesRemoved: 0,
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("returns undefined when both sides are absent", () => {
|
|
41
|
+
expect(countLineChanges(undefined, undefined)).toBeUndefined();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("excludes the no-trailing-newline marker from the counts", () => {
|
|
45
|
+
// "a" -> "a\n": jsdiff models this as the "a" line replaced (one removed,
|
|
46
|
+
// one added) plus "\ No newline" metadata, which must not be counted —
|
|
47
|
+
// exactly the population the SDK's mapPatchHunks renders.
|
|
48
|
+
expect(countLineChanges("a", "a\n")).toEqual({
|
|
49
|
+
linesAdded: 1,
|
|
50
|
+
linesRemoved: 1,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("counts an added file with no trailing newline by its real line count", () => {
|
|
55
|
+
expect(countLineChanges(undefined, "one\ntwo")).toEqual({
|
|
56
|
+
linesAdded: 2,
|
|
57
|
+
linesRemoved: 0,
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("skips counting when a side exceeds the byte guard", () => {
|
|
62
|
+
const huge = "x\n".repeat(LINE_COUNT_MAX_BYTES / 2 + 1);
|
|
63
|
+
expect(countLineChanges(huge, "small\n")).toBeUndefined();
|
|
64
|
+
expect(countLineChanges("small\n", huge)).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("guards on UTF-8 byte length, not character count", () => {
|
|
68
|
+
// Each "€" is 3 bytes; a string just over the guard in bytes while well
|
|
69
|
+
// under it in characters must still be skipped.
|
|
70
|
+
const wide = "€".repeat(Math.ceil(LINE_COUNT_MAX_BYTES / 3) + 1);
|
|
71
|
+
expect(countLineChanges(wide, "")).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for mid-run live capture (DD-32):
|
|
3
|
+
* - `captureProgressDelta` (git-substrate) against a REAL temp git repo — the
|
|
4
|
+
* only faithful test for git plumbing (kinds, counts, binary, rename→add+del,
|
|
5
|
+
* tree-unchanged short-circuit, excludePaths).
|
|
6
|
+
* - `buildFileChangeProgress` (pure) — secret zeroing, entry cap with honest
|
|
7
|
+
* totals, aggregate sums.
|
|
8
|
+
* - `shouldCaptureProgress` (pure) — the debounce floor.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { execFile } from "node:child_process";
|
|
12
|
+
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { promisify } from "node:util";
|
|
16
|
+
import { beforeEach, afterEach, describe, expect, it } from "vitest";
|
|
17
|
+
import { FileChangeKind, FileChangeType } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
18
|
+
import {
|
|
19
|
+
captureProgressDelta,
|
|
20
|
+
snapshotBaseline,
|
|
21
|
+
dropCaptureRefs,
|
|
22
|
+
type GitProgressEntry,
|
|
23
|
+
type GitProgressDelta,
|
|
24
|
+
} from "../git-substrate.js";
|
|
25
|
+
import {
|
|
26
|
+
buildFileChangeProgress,
|
|
27
|
+
createGitProgressSubstrate,
|
|
28
|
+
createHybridProgressSubstrate,
|
|
29
|
+
PROGRESS_MAX_ENTRIES,
|
|
30
|
+
shouldCaptureProgress,
|
|
31
|
+
type ProgressCapture,
|
|
32
|
+
type ProgressDelta,
|
|
33
|
+
type ProgressEntry,
|
|
34
|
+
type ProgressSubstrate,
|
|
35
|
+
} from "../progress.js";
|
|
36
|
+
|
|
37
|
+
const execFileAsync = promisify(execFile);
|
|
38
|
+
const EXEC_ID = "exec-progress-1";
|
|
39
|
+
const CHANGE_SET_ID = "exec-progress-1:0";
|
|
40
|
+
const RUNNER_OWNED_PATHS: readonly string[] = [".cursor/hooks.json"];
|
|
41
|
+
|
|
42
|
+
let repo: string;
|
|
43
|
+
|
|
44
|
+
async function git(args: string[]): Promise<void> {
|
|
45
|
+
await execFileAsync("git", args, { cwd: repo });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function write(rel: string, content: string): Promise<void> {
|
|
49
|
+
await mkdir(join(repo, rel, ".."), { recursive: true });
|
|
50
|
+
await writeFile(join(repo, rel), content, "utf-8");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function writeBytes(rel: string, bytes: Uint8Array): Promise<void> {
|
|
54
|
+
await mkdir(join(repo, rel, ".."), { recursive: true });
|
|
55
|
+
await writeFile(join(repo, rel), bytes);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function entryFor(delta: GitProgressDelta, path: string): GitProgressEntry | undefined {
|
|
59
|
+
return delta.entries.find((e) => e.pathAfter === path || e.pathBefore === path);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
beforeEach(async () => {
|
|
63
|
+
repo = await mkdtemp(join(tmpdir(), "stigmer-progress-"));
|
|
64
|
+
await git(["init", "-q"]);
|
|
65
|
+
await git(["config", "user.email", "t@t.local"]);
|
|
66
|
+
await git(["config", "user.name", "t"]);
|
|
67
|
+
await write("src/main.ts", "export const x = 1;\n");
|
|
68
|
+
await write("keep.txt", "line1\nline2\nline3\n");
|
|
69
|
+
await git(["add", "-A"]);
|
|
70
|
+
await git(["commit", "-q", "-m", "initial"]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
afterEach(async () => {
|
|
74
|
+
await dropCaptureRefs(repo, EXEC_ID).catch(() => {});
|
|
75
|
+
await rm(repo, { recursive: true, force: true });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("captureProgressDelta (real git repo)", () => {
|
|
79
|
+
it("reports ADD / MODIFY / DELETE with line counts and honest path sides", async () => {
|
|
80
|
+
const baseline = await snapshotBaseline(repo, EXEC_ID);
|
|
81
|
+
|
|
82
|
+
await write("src/new.ts", "a\nb\nc\n"); // ADD (3 added)
|
|
83
|
+
await write("src/main.ts", "export const x = 2;\nexport const y = 3;\n"); // MODIFY
|
|
84
|
+
await rm(join(repo, "keep.txt")); // DELETE (3 removed)
|
|
85
|
+
|
|
86
|
+
const delta = await captureProgressDelta(repo, EXEC_ID, baseline);
|
|
87
|
+
expect(delta).toBeDefined();
|
|
88
|
+
expect(delta!.entries).toHaveLength(3);
|
|
89
|
+
|
|
90
|
+
const added = entryFor(delta!, "src/new.ts")!;
|
|
91
|
+
expect(added.changeType).toBe(FileChangeType.CREATE);
|
|
92
|
+
expect(added.pathBefore).toBe("");
|
|
93
|
+
expect(added.pathAfter).toBe("src/new.ts");
|
|
94
|
+
expect(added.linesAdded).toBe(3);
|
|
95
|
+
expect(added.linesRemoved).toBe(0);
|
|
96
|
+
|
|
97
|
+
const modified = entryFor(delta!, "src/main.ts")!;
|
|
98
|
+
expect(modified.changeType).toBe(FileChangeType.MODIFY);
|
|
99
|
+
expect(modified.pathBefore).toBe("src/main.ts");
|
|
100
|
+
expect(modified.pathAfter).toBe("src/main.ts");
|
|
101
|
+
expect(modified.linesAdded).toBeGreaterThan(0);
|
|
102
|
+
|
|
103
|
+
const deleted = entryFor(delta!, "keep.txt")!;
|
|
104
|
+
expect(deleted.changeType).toBe(FileChangeType.DELETE);
|
|
105
|
+
expect(deleted.pathBefore).toBe("keep.txt");
|
|
106
|
+
expect(deleted.pathAfter).toBe("");
|
|
107
|
+
expect(deleted.linesRemoved).toBe(3);
|
|
108
|
+
expect(deleted.linesAdded).toBe(0);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("surfaces a binary change with zero counts (numstat reports '-')", async () => {
|
|
112
|
+
const baseline = await snapshotBaseline(repo, EXEC_ID);
|
|
113
|
+
await writeBytes("blob.bin", new Uint8Array([0, 1, 2, 0, 255, 0, 3]));
|
|
114
|
+
|
|
115
|
+
const delta = await captureProgressDelta(repo, EXEC_ID, baseline);
|
|
116
|
+
const bin = entryFor(delta!, "blob.bin")!;
|
|
117
|
+
expect(bin.changeType).toBe(FileChangeType.CREATE);
|
|
118
|
+
expect(bin.linesAdded).toBe(0);
|
|
119
|
+
expect(bin.linesRemoved).toBe(0);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("splits a rename into a delete + create (--no-renames)", async () => {
|
|
123
|
+
const baseline = await snapshotBaseline(repo, EXEC_ID);
|
|
124
|
+
await rm(join(repo, "keep.txt"));
|
|
125
|
+
await write("renamed.txt", "line1\nline2\nline3\n");
|
|
126
|
+
|
|
127
|
+
const delta = await captureProgressDelta(repo, EXEC_ID, baseline);
|
|
128
|
+
expect(entryFor(delta!, "keep.txt")!.changeType).toBe(FileChangeType.DELETE);
|
|
129
|
+
expect(entryFor(delta!, "renamed.txt")!.changeType).toBe(FileChangeType.CREATE);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("short-circuits (returns undefined) when the tree is unchanged since lastTreeSha", async () => {
|
|
133
|
+
const baseline = await snapshotBaseline(repo, EXEC_ID);
|
|
134
|
+
await write("src/new.ts", "a\nb\n");
|
|
135
|
+
|
|
136
|
+
const first = await captureProgressDelta(repo, EXEC_ID, baseline);
|
|
137
|
+
expect(first).toBeDefined();
|
|
138
|
+
|
|
139
|
+
const second = await captureProgressDelta(repo, EXEC_ID, baseline, [], first!.afterTree);
|
|
140
|
+
expect(second).toBeUndefined();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("recomputes (0 entries) after the agent reverts its own edits", async () => {
|
|
144
|
+
const baseline = await snapshotBaseline(repo, EXEC_ID);
|
|
145
|
+
await write("src/new.ts", "a\nb\n");
|
|
146
|
+
const first = await captureProgressDelta(repo, EXEC_ID, baseline);
|
|
147
|
+
|
|
148
|
+
await rm(join(repo, "src/new.ts")); // revert to clean
|
|
149
|
+
const second = await captureProgressDelta(repo, EXEC_ID, baseline, [], first!.afterTree);
|
|
150
|
+
expect(second).toBeDefined();
|
|
151
|
+
expect(second!.entries).toHaveLength(0);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("excludes runner-owned gate files from the delta", async () => {
|
|
155
|
+
const baseline = await snapshotBaseline(repo, EXEC_ID, RUNNER_OWNED_PATHS);
|
|
156
|
+
await write(".cursor/hooks.json", "{}\n");
|
|
157
|
+
await write("real.ts", "x\n");
|
|
158
|
+
|
|
159
|
+
const delta = await captureProgressDelta(repo, EXEC_ID, baseline, RUNNER_OWNED_PATHS);
|
|
160
|
+
expect(entryFor(delta!, ".cursor/hooks.json")).toBeUndefined();
|
|
161
|
+
expect(entryFor(delta!, "real.ts")).toBeDefined();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("buildFileChangeProgress (pure)", () => {
|
|
166
|
+
function delta(entries: ProgressEntry[], totalFilesChanged?: number): ProgressDelta {
|
|
167
|
+
return totalFilesChanged === undefined ? { entries } : { entries, totalFilesChanged };
|
|
168
|
+
}
|
|
169
|
+
function entry(
|
|
170
|
+
path: string,
|
|
171
|
+
added: number,
|
|
172
|
+
removed: number,
|
|
173
|
+
kind: FileChangeKind = FileChangeKind.MODIFY,
|
|
174
|
+
): ProgressEntry {
|
|
175
|
+
return {
|
|
176
|
+
pathBefore: kind === FileChangeKind.ADD ? "" : path,
|
|
177
|
+
pathAfter: path,
|
|
178
|
+
kind,
|
|
179
|
+
linesAdded: added,
|
|
180
|
+
linesRemoved: removed,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
it("sums aggregate counts and carries the per-entry kind through unchanged", () => {
|
|
185
|
+
const progress = buildFileChangeProgress(
|
|
186
|
+
delta([
|
|
187
|
+
entry("a.ts", 5, 0, FileChangeKind.ADD),
|
|
188
|
+
entry("b.ts", 2, 3),
|
|
189
|
+
]),
|
|
190
|
+
CHANGE_SET_ID,
|
|
191
|
+
);
|
|
192
|
+
expect(progress.changeSetId).toBe(CHANGE_SET_ID);
|
|
193
|
+
expect(progress.filesChanged).toBe(2);
|
|
194
|
+
expect(progress.linesAdded).toBe(7);
|
|
195
|
+
expect(progress.linesRemoved).toBe(3);
|
|
196
|
+
expect(progress.entries[0].kind).toBe(FileChangeKind.ADD);
|
|
197
|
+
expect(progress.entries[1].kind).toBe(FileChangeKind.MODIFY);
|
|
198
|
+
expect(progress.capturedAt).not.toBe("");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("reports totalFilesChanged when a substrate capped its reads, else the entry count", () => {
|
|
202
|
+
// git substrate leaves totalFilesChanged undefined -> filesChanged = entries.length.
|
|
203
|
+
const gitLike = buildFileChangeProgress(delta([entry("a.ts", 1, 0)]), CHANGE_SET_ID);
|
|
204
|
+
expect(gitLike.filesChanged).toBe(1);
|
|
205
|
+
|
|
206
|
+
// cas substrate read only a bounded prefix -> filesChanged reflects the honest
|
|
207
|
+
// total even though fewer entries were emitted.
|
|
208
|
+
const casLike = buildFileChangeProgress(delta([entry("a.ts", 1, 0)], 7), CHANGE_SET_ID);
|
|
209
|
+
expect(casLike.filesChanged).toBe(7);
|
|
210
|
+
expect(casLike.entries).toHaveLength(1);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("zeroes counts for secret-like paths (path visible, magnitude withheld)", () => {
|
|
214
|
+
const progress = buildFileChangeProgress(
|
|
215
|
+
delta([entry(".env", 10, 2), entry("app.ts", 4, 1)]),
|
|
216
|
+
CHANGE_SET_ID,
|
|
217
|
+
);
|
|
218
|
+
// The secret path is still counted as a changed file and its path is present…
|
|
219
|
+
expect(progress.filesChanged).toBe(2);
|
|
220
|
+
const secret = progress.entries.find((e) => e.pathAfter === ".env")!;
|
|
221
|
+
expect(secret.linesAdded).toBe(0);
|
|
222
|
+
expect(secret.linesRemoved).toBe(0);
|
|
223
|
+
// …but its magnitude is excluded from the aggregate.
|
|
224
|
+
expect(progress.linesAdded).toBe(4);
|
|
225
|
+
expect(progress.linesRemoved).toBe(1);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("caps entries but keeps files_changed and totals honest over all files", () => {
|
|
229
|
+
const entries: ProgressEntry[] = [];
|
|
230
|
+
for (let i = 0; i < PROGRESS_MAX_ENTRIES + 25; i++) {
|
|
231
|
+
entries.push(entry(`f${i}.ts`, 1, 1));
|
|
232
|
+
}
|
|
233
|
+
const progress = buildFileChangeProgress(delta(entries), CHANGE_SET_ID);
|
|
234
|
+
expect(progress.entries).toHaveLength(PROGRESS_MAX_ENTRIES);
|
|
235
|
+
expect(progress.filesChanged).toBe(PROGRESS_MAX_ENTRIES + 25);
|
|
236
|
+
expect(progress.linesAdded).toBe(PROGRESS_MAX_ENTRIES + 25);
|
|
237
|
+
expect(progress.linesRemoved).toBe(PROGRESS_MAX_ENTRIES + 25);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("produces a zero-file snapshot for an empty delta (revert-to-clean)", () => {
|
|
241
|
+
const progress = buildFileChangeProgress(delta([]), CHANGE_SET_ID);
|
|
242
|
+
expect(progress.filesChanged).toBe(0);
|
|
243
|
+
expect(progress.entries).toHaveLength(0);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe("shouldCaptureProgress (pure)", () => {
|
|
248
|
+
it("always captures on the first call (lastAtMs = 0, real clock)", () => {
|
|
249
|
+
// The caller starts a fresh turn with lastAtMs = 0 and now = Date.now(),
|
|
250
|
+
// which is always far past any floor.
|
|
251
|
+
expect(shouldCaptureProgress(0, Date.now(), 2000)).toBe(true);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("gates within the floor and allows once it elapses", () => {
|
|
255
|
+
expect(shouldCaptureProgress(1000, 2500, 2000)).toBe(false);
|
|
256
|
+
expect(shouldCaptureProgress(1000, 3000, 2000)).toBe(true);
|
|
257
|
+
expect(shouldCaptureProgress(1000, 3001, 2000)).toBe(true);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe("createGitProgressSubstrate (real git repo)", () => {
|
|
262
|
+
it("reports changed:true then reuses the cached full delta on an unchanged tree", async () => {
|
|
263
|
+
const baseline = await snapshotBaseline(repo, EXEC_ID);
|
|
264
|
+
const sub = createGitProgressSubstrate({
|
|
265
|
+
workspaceRoot: repo,
|
|
266
|
+
executionId: EXEC_ID,
|
|
267
|
+
baselineTree: baseline,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
await write("src/new.ts", "a\nb\n");
|
|
271
|
+
const first = await sub.capture();
|
|
272
|
+
expect(first.changed).toBe(true);
|
|
273
|
+
expect(first.delta.entries.length).toBeGreaterThan(0);
|
|
274
|
+
|
|
275
|
+
// Nothing moved since the last capture: the tree-sha short-circuits, but the
|
|
276
|
+
// full delta is still returned (changed:false) so a hybrid can merge it.
|
|
277
|
+
const second = await sub.capture();
|
|
278
|
+
expect(second.changed).toBe(false);
|
|
279
|
+
expect(second.delta.entries).toEqual(first.delta.entries);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
/** A fake substrate returning a fixed capture — for merge/short-circuit logic. */
|
|
284
|
+
function fakeSubstrate(capture: ProgressCapture): ProgressSubstrate {
|
|
285
|
+
return { capture: () => Promise.resolve(capture) };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function progressEntry(path: string): ProgressEntry {
|
|
289
|
+
return { pathBefore: "", pathAfter: path, kind: FileChangeKind.ADD, linesAdded: 1, linesRemoved: 0 };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
describe("createHybridProgressSubstrate", () => {
|
|
293
|
+
it("concatenates disjoint git + cas slices and sums the honest totals", async () => {
|
|
294
|
+
const git = fakeSubstrate({
|
|
295
|
+
delta: { entries: [progressEntry("tracked.ts")] }, // git: total = entries.length
|
|
296
|
+
changed: true,
|
|
297
|
+
});
|
|
298
|
+
const cas = fakeSubstrate({
|
|
299
|
+
delta: { entries: [progressEntry(".env.local")], totalFilesChanged: 3 }, // cas capped
|
|
300
|
+
changed: true,
|
|
301
|
+
});
|
|
302
|
+
const hybrid = createHybridProgressSubstrate(git, cas);
|
|
303
|
+
|
|
304
|
+
const { delta, changed } = await hybrid.capture();
|
|
305
|
+
expect(changed).toBe(true);
|
|
306
|
+
expect(delta.entries.map((e) => e.pathAfter)).toEqual(["tracked.ts", ".env.local"]);
|
|
307
|
+
// 1 (git entries) + 3 (cas honest total) = 4.
|
|
308
|
+
expect(delta.totalFilesChanged).toBe(4);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("still emits the full git slice when only the cas slice changed", async () => {
|
|
312
|
+
// Git unchanged returns its cached full delta with changed:false; the hybrid
|
|
313
|
+
// must keep it (a `ProgressDelta | undefined` contract would have dropped it).
|
|
314
|
+
const git = fakeSubstrate({
|
|
315
|
+
delta: { entries: [progressEntry("tracked.ts")] },
|
|
316
|
+
changed: false,
|
|
317
|
+
});
|
|
318
|
+
const cas = fakeSubstrate({
|
|
319
|
+
delta: { entries: [progressEntry("build/out.js")], totalFilesChanged: 1 },
|
|
320
|
+
changed: true,
|
|
321
|
+
});
|
|
322
|
+
const hybrid = createHybridProgressSubstrate(git, cas);
|
|
323
|
+
|
|
324
|
+
const { delta, changed } = await hybrid.capture();
|
|
325
|
+
expect(changed).toBe(true);
|
|
326
|
+
expect(delta.entries.map((e) => e.pathAfter)).toEqual(["tracked.ts", "build/out.js"]);
|
|
327
|
+
expect(delta.totalFilesChanged).toBe(2);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("reports changed:false only when NEITHER slice moved", async () => {
|
|
331
|
+
const git = fakeSubstrate({ delta: { entries: [] }, changed: false });
|
|
332
|
+
const cas = fakeSubstrate({ delta: { entries: [], totalFilesChanged: 0 }, changed: false });
|
|
333
|
+
const hybrid = createHybridProgressSubstrate(git, cas);
|
|
334
|
+
|
|
335
|
+
const { changed } = await hybrid.capture();
|
|
336
|
+
expect(changed).toBe(false);
|
|
337
|
+
});
|
|
338
|
+
});
|