agentic-workflow-manager 9.22.0 → 9.22.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.
Files changed (53) hide show
  1. package/dist/src/commands/change/index.js +2 -2
  2. package/dist/src/commands/init.js +8 -4
  3. package/dist/src/commands/registry/add.js +9 -3
  4. package/dist/src/commands/sync.js +11 -5
  5. package/dist/src/commands/update.js +23 -12
  6. package/dist/src/core/changes/derive.js +6 -3
  7. package/dist/src/core/changes/git.js +1 -2
  8. package/dist/src/core/changes/worktree.js +15 -3
  9. package/dist/src/core/init/steps.js +3 -8
  10. package/dist/src/core/registries.js +20 -15
  11. package/dist/src/core/registry-git-errors.js +107 -0
  12. package/dist/src/core/registry-git.js +93 -0
  13. package/dist/src/core/registry-sync-report.js +74 -0
  14. package/dist/src/core/versioning.js +3 -6
  15. package/dist/src/index.js +5 -8
  16. package/dist/tests/commands/change/new.test.js +103 -57
  17. package/dist/tests/commands/change/validate.test.js +164 -108
  18. package/dist/tests/commands/changes/list.test.js +41 -22
  19. package/dist/tests/commands/job/gate-reconcile.test.js +13 -2
  20. package/dist/tests/commands/registry-sync-honesty.test.js +175 -0
  21. package/dist/tests/commands/sensors/exec.test.js +3 -1
  22. package/dist/tests/commands/update.test.js +8 -8
  23. package/dist/tests/commands/watch/supervisor-loop.test.js +21 -5
  24. package/dist/tests/core/changes/helpers.js +15 -3
  25. package/dist/tests/core/changes/list-paging.test.js +131 -110
  26. package/dist/tests/core/changes/list.test.js +133 -83
  27. package/dist/tests/core/changes/receipt.test.js +18 -6
  28. package/dist/tests/core/diagnostics/provider-checks.test.js +3 -0
  29. package/dist/tests/core/init/steps-registry-sync.test.js +6 -8
  30. package/dist/tests/core/registry-git.test.js +153 -0
  31. package/dist/tests/helpers/commit-seeded-registry.js +18 -0
  32. package/dist/tests/integration/codex-provider-isolated.test.js +7 -2
  33. package/dist/tests/integration/copilot-init-isolated.test.js +7 -4
  34. package/dist/tests/structural/active-documentation.test.js +0 -41
  35. package/dist/tests/structural/bundle-skill-reference-contract.test.js +0 -12
  36. package/dist/tests/structural/cli-reference-matches-commander.test.js +94 -0
  37. package/dist/tests/structural/native-release-artifacts.test.js +116 -107
  38. package/dist/tests/structural/sensor-documentation-contract.test.js +0 -47
  39. package/dist/tests/structural/sensor-portability-contract.test.js +0 -7
  40. package/dist/tests/structural/simple-git-v4-pin.test.js +49 -0
  41. package/package.json +2 -2
  42. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  43. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  44. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  45. package/prebuilds/win32-x64/secure_fs.node +0 -0
  46. package/dist/tests/structural/cloud-admission-docs-contract.test.js +0 -54
  47. package/dist/tests/structural/documentation-phase-is-mechanized.test.js +0 -29
  48. package/dist/tests/structural/operator-environments-docs-contract.test.js +0 -70
  49. package/dist/tests/structural/process-model-description-contract.test.js +0 -34
  50. package/dist/tests/structural/project-declared-sensors-docs-contract.test.js +0 -104
  51. package/dist/tests/structural/r4-plan-currentness-docs.test.js +0 -37
  52. package/dist/tests/structural/sensor-execution-optimization-docs-contract.test.js +0 -102
  53. package/dist/tests/structural/sensor-slice-decision-contract.test.js +0 -63
@@ -11,20 +11,73 @@ const list_fixtures_1 = require("./list-fixtures");
11
11
  const list_1 = require("../../../src/core/changes/list");
12
12
  let repo;
13
13
  let template;
14
- beforeAll(() => { template = (0, helpers_1.makeRepo)(); });
15
- afterAll(() => template.cleanup());
14
+ /** The template plus the default adoption commit of `adoptIn`: `adopt()` starts a test from a copy of it (no git process). */
15
+ let adopted;
16
+ beforeAll(() => { template = (0, helpers_1.makeRepo)(); adopted = (0, helpers_1.cloneRepo)(template); (0, list_fixtures_1.adoptIn)(adopted); });
17
+ afterAll(() => { adopted.cleanup(); template.cleanup(); });
16
18
  beforeEach(() => { repo = (0, helpers_1.cloneRepo)(template); });
17
19
  afterEach(() => { jest.restoreAllMocks(); repo.cleanup(); });
18
20
  const list = (opts = {}) => (0, list_fixtures_1.listIn)(repo, opts);
19
21
  const listed = (opts = {}) => (0, list_fixtures_1.ok)(list(opts));
20
- const adopt = (id) => (0, list_fixtures_1.adoptIn)(repo, id);
22
+ /** The default adoption commit (a copy of the prebuilt `adopted` repository), or `adoptIn(repo, id)` for another id. */
23
+ const adopt = (id) => {
24
+ if (id !== undefined) {
25
+ (0, list_fixtures_1.adoptIn)(repo, id);
26
+ return;
27
+ }
28
+ repo.cleanup();
29
+ repo = (0, helpers_1.cloneRepo)(adopted);
30
+ };
21
31
  const history = () => (0, list_fixtures_1.historyIn)(repo);
32
+ /** The commits `repo.commit(files, message, date)` would make, in order, written by one `git fast-import` (importCommits). */
33
+ const commitMany = (planned) => (0, helpers_1.importCommits)(repo, planned.map(([files, message, date]) => ({ files, message: `${message}\n`, date })));
34
+ /** `n` commits `${prefix}${i}` (i from `from`), each adding `${file}${i}.ts`, all at `date`. */
35
+ const series = (n, prefix, file, date, from = 1) => Array.from({ length: n }, (_, k) => [{ [`${file}${from + k}.ts`]: '1' }, `${prefix}${from + k}`, date]);
36
+ /**
37
+ * A state for read-only tests, built once (lazily, by `build` acting on `repo`) and listed once under a git spy; the tests that
38
+ * share it only read the result and the recorded git calls, and never change the state.
39
+ */
40
+ function sharedListing(build, opts = {}) {
41
+ let state;
42
+ let memo;
43
+ afterAll(() => state?.cleanup());
44
+ return () => {
45
+ if (memo === undefined) {
46
+ const own = repo;
47
+ repo = (0, helpers_1.cloneRepo)(template);
48
+ try {
49
+ build();
50
+ state = repo;
51
+ const spy = (0, helpers_1.spyOnGit)();
52
+ try {
53
+ memo = { result: listed(opts), calls: spy.calls };
54
+ }
55
+ finally {
56
+ spy.restore();
57
+ }
58
+ }
59
+ catch (error) {
60
+ if (state === undefined)
61
+ repo.cleanup();
62
+ throw error;
63
+ }
64
+ finally {
65
+ repo = own;
66
+ }
67
+ }
68
+ return memo;
69
+ };
70
+ }
71
+ /** One `fix: a` commit after the adoption point, listed once. */
72
+ const oneFix = sharedListing(() => { adopt(); repo.commit({ 'a.ts': '1' }, 'fix: a', '2026-10-16T10:00:00Z'); });
22
73
  describe('listChanges: window and bounds (R5.2, R5.5, R5.7)', () => {
23
74
  test('the window start is inclusive and a commit one second older is out', () => {
24
75
  adopt();
25
- repo.commit({ 'a.ts': '1' }, 'fix: before', '2026-10-15T11:59:59Z');
26
- repo.commit({ 'b.ts': '1' }, 'fix: at start', '2026-10-15T12:00:00Z');
27
- repo.commit({ 'c.ts': '1' }, 'fix: after', '2026-10-15T12:00:01Z');
76
+ commitMany([
77
+ [{ 'a.ts': '1' }, 'fix: before', '2026-10-15T11:59:59Z'],
78
+ [{ 'b.ts': '1' }, 'fix: at start', '2026-10-15T12:00:00Z'],
79
+ [{ 'c.ts': '1' }, 'fix: after', '2026-10-15T12:00:01Z'],
80
+ ]);
28
81
  expect((0, list_fixtures_1.subjects)(listed())).toEqual(['fix: after', 'fix: at start']);
29
82
  });
30
83
  test('a future-dated commit is included', () => {
@@ -36,9 +89,11 @@ describe('listChanges: window and bounds (R5.2, R5.5, R5.7)', () => {
36
89
  });
37
90
  test('each commit is tested individually: an old-dated commit between two in-window ones does not stop the walk', () => {
38
91
  adopt();
39
- repo.commit({ 'a.ts': '1' }, 'fix: x', '2026-10-16T10:00:00Z');
40
- repo.commit({ 'b.ts': '1' }, 'fix: y old', '2026-10-01T10:00:00Z');
41
- repo.commit({ 'c.ts': '1' }, 'fix: z', '2026-10-16T12:00:00Z');
92
+ commitMany([
93
+ [{ 'a.ts': '1' }, 'fix: x', '2026-10-16T10:00:00Z'],
94
+ [{ 'b.ts': '1' }, 'fix: y old', '2026-10-01T10:00:00Z'],
95
+ [{ 'c.ts': '1' }, 'fix: z', '2026-10-16T12:00:00Z'],
96
+ ]);
42
97
  expect((0, list_fixtures_1.subjects)(listed())).toEqual(['fix: z', 'fix: x']);
43
98
  });
44
99
  test('merged_at is the committer date, not the author date', () => {
@@ -54,22 +109,19 @@ describe('listChanges: window and bounds (R5.2, R5.5, R5.7)', () => {
54
109
  });
55
110
  test('beyond the limit the newest entries are kept and the rest are counted', () => {
56
111
  adopt();
57
- for (let i = 1; i <= 5; i++)
58
- repo.commit({ [`f${i}.ts`]: '1' }, `fix: n${i}`, `2026-10-16T0${i}:00:00Z`);
112
+ commitMany(Array.from({ length: 5 }, (_, k) => [{ [`f${k + 1}.ts`]: '1' }, `fix: n${k + 1}`, `2026-10-16T0${k + 1}:00:00Z`]));
59
113
  const r = listed({ limit: 3 });
60
114
  expect((0, list_fixtures_1.subjects)(r)).toEqual(['fix: n5', 'fix: n4', 'fix: n3']);
61
115
  expect(r).toMatchObject({ truncated: true, omitted: 2 });
62
116
  });
63
117
  test('exactly at the limit is not truncated', () => {
64
118
  adopt();
65
- for (let i = 1; i <= 3; i++)
66
- repo.commit({ [`f${i}.ts`]: '1' }, `fix: n${i}`, `2026-10-16T0${i}:00:00Z`);
119
+ commitMany(Array.from({ length: 3 }, (_, k) => [{ [`f${k + 1}.ts`]: '1' }, `fix: n${k + 1}`, `2026-10-16T0${k + 1}:00:00Z`]));
67
120
  expect(listed({ limit: 3 })).toMatchObject({ truncated: false, omitted: 0 });
68
121
  });
69
122
  test('the limit counts window entries only, not older commits', () => {
70
123
  adopt();
71
- repo.commit({ 'a.ts': '1' }, 'fix: old', '2026-10-01T00:00:00Z');
72
- repo.commit({ 'b.ts': '1' }, 'fix: new', '2026-10-16T00:00:00Z');
124
+ commitMany([[{ 'a.ts': '1' }, 'fix: old', '2026-10-01T00:00:00Z'], [{ 'b.ts': '1' }, 'fix: new', '2026-10-16T00:00:00Z']]);
73
125
  expect(listed({ limit: 1 })).toMatchObject({ truncated: false, omitted: 0 });
74
126
  });
75
127
  test('a limit above 200 is capped at 200: 205 window entries give 200 listed and 5 omitted', () => {
@@ -115,25 +167,25 @@ describe('listChanges: window and bounds (R5.2, R5.5, R5.7)', () => {
115
167
  });
116
168
  test('the per-run budget fails closed on the commits that come after it is spent', () => {
117
169
  adopt();
118
- for (let i = 1; i <= 3; i++)
119
- repo.commit({ [(0, list_fixtures_1.recordPath)(`2026-10-16-r${i}`)]: (0, list_fixtures_1.record)(`2026-10-16-r${i}`) }, `feat: r${i}`, `2026-10-16T0${i}:00:00Z`);
170
+ commitMany(Array.from({ length: 3 }, (_, k) => [{ [(0, list_fixtures_1.recordPath)(`2026-10-16-r${k + 1}`)]: (0, list_fixtures_1.record)(`2026-10-16-r${k + 1}`) }, `feat: r${k + 1}`, `2026-10-16T0${k + 1}:00:00Z`]));
120
171
  const r = listed({ maxRecords: 2 });
121
172
  expect((0, list_fixtures_1.byClass)(r)).toEqual({ 'feat: r3': 'recorded', 'feat: r2': 'recorded', 'feat: r1': 'invalid-record' });
122
173
  expect(r.changes[2].records).toEqual([{ path: 'docs/changes', diagnostics: [expect.objectContaining({ code: 'too-many-records' })] }]);
123
174
  });
124
175
  test('the per-run budget is spent on records actually read: a commit refused for being too large does not use it up', () => {
125
176
  adopt();
126
- repo.commit(files(1, 100), 'feat: small', '2026-10-16T01:00:00Z');
127
- repo.commit(files(3, 200), 'feat: big', '2026-10-16T02:00:00Z');
128
- repo.commit(files(2, 300), 'feat: medium', '2026-10-16T03:00:00Z');
177
+ commitMany([
178
+ [files(1, 100), 'feat: small', '2026-10-16T01:00:00Z'],
179
+ [files(3, 200), 'feat: big', '2026-10-16T02:00:00Z'],
180
+ [files(2, 300), 'feat: medium', '2026-10-16T03:00:00Z'],
181
+ ]);
129
182
  const r = listed({ maxRecords: 3 });
130
183
  expect((0, list_fixtures_1.byClass)(r)).toEqual({ 'feat: medium': 'recorded', 'feat: big': 'invalid-record', 'feat: small': 'recorded' });
131
184
  expect(r.changes.find((c) => c.subject === 'feat: big')?.records).toHaveLength(1);
132
185
  });
133
186
  test('a budget of exactly the records of the run reads them all', () => {
134
187
  adopt();
135
- repo.commit(files(2, 100), 'feat: a', '2026-10-16T01:00:00Z');
136
- repo.commit(files(1, 200), 'feat: b', '2026-10-16T02:00:00Z');
188
+ commitMany([[files(2, 100), 'feat: a', '2026-10-16T01:00:00Z'], [files(1, 200), 'feat: b', '2026-10-16T02:00:00Z']]);
137
189
  expect(Object.values((0, list_fixtures_1.byClass)(listed({ maxRecords: 3 })))).toEqual(['recorded', 'recorded']);
138
190
  expect(Object.values((0, list_fixtures_1.byClass)(listed({ maxRecords: 2 })))).toEqual(['recorded', 'invalid-record']);
139
191
  });
@@ -151,19 +203,12 @@ describe('listChanges: subjects and tags beyond their caps are clipped, never an
151
203
  expect(r.changes[1].subject.endsWith('…')).toBe(true);
152
204
  }, 120_000);
153
205
  test('the subject is asked from git bounded, so that git never prints all of it', () => {
154
- adopt();
155
- repo.commit({ 'a.ts': '1' }, 'fix: a', '2026-10-16T10:00:00Z');
156
- const spy = (0, helpers_1.spyOnGit)();
157
- listed();
158
- spy.restore();
159
- const call = spy.calls.find((a) => a.some((x) => x.startsWith('--format=%H%x1f%<(')));
206
+ const call = oneFix().calls.find((a) => a.some((x) => x.startsWith('--format=%H%x1f%<(')));
160
207
  expect(call).toBeDefined();
161
208
  expect(call?.some((x) => /%<\(\d+,trunc\)%s/.test(x))).toBe(true);
162
209
  });
163
210
  test('a short subject is returned without the padding git adds', () => {
164
- adopt();
165
- repo.commit({ 'a.ts': '1' }, 'fix: a', '2026-10-16T10:00:00Z');
166
- expect((0, list_fixtures_1.subjects)(listed())).toEqual(['fix: a']);
211
+ expect((0, list_fixtures_1.subjects)(oneFix().result)).toEqual(['fix: a']);
167
212
  });
168
213
  test('R5.5 a commit whose subject cannot be read still lists: its pr is null (the tail fallback), never a failure', () => {
169
214
  adopt();
@@ -176,11 +221,9 @@ describe('listChanges: subjects and tags beyond their caps are clipped, never an
176
221
  });
177
222
  test('one bad subject among many is found by halving the batch: a logarithmic number of calls, not one per commit', () => {
178
223
  adopt();
179
- for (let i = 1; i <= 7; i++)
180
- repo.commit({ [`f${i}.ts`]: '1' }, `fix: n${i}`, '2026-10-16T10:00:00Z');
224
+ commitMany(series(7, 'fix: n', 'f', '2026-10-16T10:00:00Z'));
181
225
  repo.gitAt('2026-10-16T10:00:00Z', 'commit', '--allow-empty', '-q', '-F', bigMessage(`fix: ${'\u0301'.repeat(9_000_000)}`));
182
- for (let i = 8; i <= 15; i++)
183
- repo.commit({ [`f${i}.ts`]: '1' }, `fix: n${i}`, '2026-10-16T11:00:00Z');
226
+ commitMany(series(8, 'fix: n', 'f', '2026-10-16T11:00:00Z', 8));
184
227
  const spy = (0, helpers_1.spyOnGit)();
185
228
  const r = listed();
186
229
  spy.restore();
@@ -206,8 +249,12 @@ describe('listChanges: the first-parent scan is read in pages (R5.7)', () => {
206
249
  const isScan = (a) => a.includes('log') && a.some((x) => x === '--format=%H%x1f%ct%x1f%P');
207
250
  const isAdoptionPage = (a) => a.includes('log') && a.some((x) => x.startsWith(':(top,glob)docs/changes/'));
208
251
  /** `n` commits in the window, `fix: n1` oldest. */
209
- const commits = (n, from = 1) => { for (let i = from; i < from + n; i++)
210
- repo.commit({ [`f${i}.ts`]: '1' }, `fix: n${i}`, '2026-10-16T10:00:00Z'); };
252
+ const inWindow = (n, from = 1) => series(n, 'fix: n', 'f', '2026-10-16T10:00:00Z', from);
253
+ const commits = (n, from = 1) => commitMany(inWindow(n, from));
254
+ /** `n` commits `fix: old${i}`, each adding `o${i}.ts`, dated before the window. */
255
+ const old = (n) => series(n, 'fix: old', 'o', '2026-10-01T10:00:00Z');
256
+ /** `n` commits `docs: note ${i}`, each adding `docs/changes/2026-10-16-n${i}.md` (a record-like name: they touch docs/changes). */
257
+ const notes = (n, from = 1) => Array.from({ length: n }, (_, k) => [{ [`docs/changes/2026-10-16-n${from + k}.md`]: '1' }, `docs: note ${from + k}`, '2026-10-16T10:00:00Z']);
211
258
  const countCalls = (pred, opts) => { const spy = (0, helpers_1.spyOnGit)(); listed(opts); spy.restore(); return spy.calls.filter(pred).length; };
212
259
  test('the real page size is 10 000 commits and is what a default run asks git for', () => {
213
260
  expect(list_1.PAGE_SIZE).toBe(10_000);
@@ -243,8 +290,7 @@ describe('listChanges: the first-parent scan is read in pages (R5.7)', () => {
243
290
  expect(listed({ pageSize: 1 })).toEqual(listed());
244
291
  });
245
292
  test('the scan stops at the adoption point: no page is read beyond the one holding it', () => {
246
- repo.commit({ 'old.ts': '1' }, 'feat: before', '2026-10-16T09:00:00Z');
247
- repo.commit({ 'old2.ts': '1' }, 'feat: before too', '2026-10-16T09:30:00Z');
293
+ commitMany([[{ 'old.ts': '1' }, 'feat: before', '2026-10-16T09:00:00Z'], [{ 'old2.ts': '1' }, 'feat: before too', '2026-10-16T09:30:00Z']]);
248
294
  adopt('2026-10-16-first');
249
295
  commits(5);
250
296
  const r = listed({ pageSize: 2 });
@@ -253,40 +299,31 @@ describe('listChanges: the first-parent scan is read in pages (R5.7)', () => {
253
299
  });
254
300
  test('the scan stops at the first page without a commit in the window', () => {
255
301
  adopt();
256
- for (let i = 1; i <= 6; i++)
257
- repo.commit({ [`o${i}.ts`]: '1' }, `fix: old${i}`, '2026-10-01T10:00:00Z');
258
- commits(2);
302
+ commitMany([...old(6), ...inWindow(2)]);
259
303
  expect((0, list_fixtures_1.subjects)(listed({ pageSize: 2 }))).toEqual(['fix: n2', 'fix: n1']);
260
304
  expect(countCalls(isScan, { pageSize: 2 })).toBe(2);
261
305
  });
262
306
  test('a window with no commit in it reads one page only', () => {
263
307
  adopt();
264
- for (let i = 1; i <= 6; i++)
265
- repo.commit({ [`o${i}.ts`]: '1' }, `fix: old${i}`, '2026-10-01T10:00:00Z');
308
+ commitMany(old(6));
266
309
  expect(listed({ pageSize: 2 })).toMatchObject({ adopted: true, changes: [], truncated: false, omitted: 0 });
267
310
  expect(countCalls(isScan, { pageSize: 2 })).toBe(1);
268
311
  });
269
312
  test('a page that holds some commits of the window and some older ones is followed by one more page', () => {
270
313
  adopt();
271
- for (let i = 1; i <= 4; i++)
272
- repo.commit({ [`o${i}.ts`]: '1' }, `fix: old${i}`, '2026-10-01T10:00:00Z');
273
- commits(1);
314
+ commitMany([...old(4), ...inWindow(1)]);
274
315
  expect((0, list_fixtures_1.subjects)(listed({ pageSize: 2 }))).toEqual(['fix: n1']);
275
316
  expect(countCalls(isScan, { pageSize: 2 })).toBe(2);
276
317
  });
277
318
  test('a commit dated inside the window beyond a page with none is not listed (dates are assumed not to decrease)', () => {
278
319
  adopt();
279
- commits(1);
280
- for (let i = 1; i <= 3; i++)
281
- repo.commit({ [`o${i}.ts`]: '1' }, `fix: old${i}`, '2026-10-01T10:00:00Z');
282
- commits(1, 2);
320
+ commitMany([...inWindow(1), ...old(3), ...inWindow(1, 2)]);
283
321
  expect((0, list_fixtures_1.subjects)(listed({ pageSize: 2 }))).toEqual(['fix: n2']);
284
322
  expect((0, list_fixtures_1.subjects)(listed({ pageSize: 10 }))).toEqual(['fix: n2', 'fix: n1']);
285
323
  });
286
324
  test('an unusable date counts as inside the window when it decides whether to read on', () => {
287
325
  adopt();
288
- for (let i = 1; i <= 4; i++)
289
- repo.commit({ [`o${i}.ts`]: '1' }, `fix: old${i}`, '2026-10-01T10:00:00Z');
326
+ commitMany(old(4));
290
327
  (0, list_fixtures_1.commitLiterally)(repo, '');
291
328
  repo.commit({ 'o5.ts': '1' }, 'fix: old5', '2026-10-01T10:00:00Z');
292
329
  expect((0, list_fixtures_1.subjects)(listed({ pageSize: 2 }))).toEqual(['fix: odd date']);
@@ -314,25 +351,19 @@ describe('listChanges: the first-parent scan is read in pages (R5.7)', () => {
314
351
  expect(paged).toEqual(listed());
315
352
  });
316
353
  test('the adoption search is paged too: the oldest commit that touched docs/changes is found across pages', () => {
317
- repo.commit({ 'old.ts': '1' }, 'feat: before', '2026-10-16T09:00:00Z');
318
- for (let i = 1; i <= 7; i++)
319
- repo.commit({ [`docs/changes/2026-10-16-n${i}.md`]: '1' }, `docs: note ${i}`, '2026-10-16T10:00:00Z');
354
+ commitMany([[{ 'old.ts': '1' }, 'feat: before', '2026-10-16T09:00:00Z'], ...notes(7)]);
320
355
  const paged = listed({ pageSize: 3 });
321
356
  expect((0, list_fixtures_1.subjects)(paged)).toEqual(Array.from({ length: 7 }, (_, i) => `docs: note ${7 - i}`));
322
357
  expect(paged).toEqual(listed());
323
358
  expect(countCalls(isAdoptionPage, { pageSize: 3 })).toBe(3);
324
359
  });
325
360
  test('the adoption search reads one more, empty, page when the last one was full', () => {
326
- for (let i = 1; i <= 6; i++)
327
- repo.commit({ [`docs/changes/2026-10-16-n${i}.md`]: '1' }, `docs: note ${i}`, '2026-10-16T10:00:00Z');
361
+ commitMany(notes(6));
328
362
  expect((0, list_fixtures_1.subjects)(listed({ pageSize: 3 }))).toHaveLength(6);
329
363
  expect(countCalls(isAdoptionPage, { pageSize: 3 })).toBe(3);
330
364
  });
331
365
  test('the adoption search skips commits that did not touch docs/changes between two pages', () => {
332
- for (let i = 1; i <= 3; i++) {
333
- repo.commit({ [`docs/changes/2026-10-16-n${i}.md`]: '1' }, `docs: note ${i}`, '2026-10-16T10:00:00Z');
334
- commits(2, i * 10);
335
- }
366
+ commitMany([1, 2, 3].flatMap((i) => [...notes(1, i), ...inWindow(2, i * 10)]));
336
367
  const r = listed({ pageSize: 2 });
337
368
  expect(r.changes.map((c) => c.subject).filter((x) => x.startsWith('docs:'))).toEqual(['docs: note 3', 'docs: note 2', 'docs: note 1']);
338
369
  expect(r).toEqual(listed());
@@ -356,8 +387,7 @@ describe('listChanges: the first-parent scan is read in pages (R5.7)', () => {
356
387
  expect(outcome).toMatchObject({ ok: false, failure: { code: 'git-failed' } });
357
388
  });
358
389
  test('a git failure on a later page of the adoption search is a git-failed failure', () => {
359
- for (let i = 1; i <= 6; i++)
360
- repo.commit({ [`docs/changes/2026-10-16-n${i}.md`]: '1' }, `docs: note ${i}`, '2026-10-16T10:00:00Z');
390
+ commitMany(notes(6));
361
391
  let seen = 0;
362
392
  const spy = (0, helpers_1.spyOnGit)((args) => { if (isAdoptionPage(args) && ++seen === 2)
363
393
  throw new Error('boom'); });
@@ -367,12 +397,7 @@ describe('listChanges: the first-parent scan is read in pages (R5.7)', () => {
367
397
  });
368
398
  });
369
399
  describe('listChanges: the pr comes from the true end of the full subject (R5.5)', () => {
370
- const prOf = (subject) => {
371
- adopt();
372
- repo.commit({ 'a.ts': '1' }, subject, '2026-10-16T10:00:00Z');
373
- return listed().changes[0].pr;
374
- };
375
- test.each([
400
+ const CASES = [
376
401
  ['5000 characters long', `fix: ${'a'.repeat(5000)} (#12)`, 12],
377
402
  ['1030 characters long (just beyond the subject cap)', `fix: ${'a'.repeat(1030 - 12)} (#12)`, 12],
378
403
  ['exactly the width of the tail', `${'x'.repeat(64 - 6)} (#12)`, 12],
@@ -388,28 +413,39 @@ describe('listChanges: the pr comes from the true end of the full subject (R5.5)
388
413
  ['with accented characters at the cut', `${'\u00e9'.repeat(100)} (#12)`, 12],
389
414
  ['with astral characters at the cut', `${'\u{1F600}'.repeat(100)} (#12)`, 12],
390
415
  ['with two (#n) at the end', `fix: ${'a'.repeat(300)} (#1) (#2)`, 2],
391
- ])('a subject %s', (_label, subject, pr) => {
392
- expect(prOf(subject)).toBe(pr);
416
+ ];
417
+ const CLIPPED = `fix: ${'a'.repeat(5000)} (#12)`;
418
+ const SHORT = 'fix: short (#4)';
419
+ // Every subject below is its own commit of one shared history, listed once; an entry is found by its (unique) commit date.
420
+ const SUBJECTS = [...CASES.map(([, subject]) => subject), CLIPPED, SHORT];
421
+ const dateOf = (i) => new Date(Date.UTC(2026, 9, 16, 10, 0, 0) + i * 60_000).toISOString();
422
+ const shared = sharedListing(() => {
423
+ adopt();
424
+ commitMany(SUBJECTS.map((subject, i) => [{ [`a${i}.ts`]: '1' }, subject, dateOf(i)]));
425
+ });
426
+ const entryOf = (subject) => {
427
+ const { changes } = shared().result;
428
+ expect(changes).toHaveLength(SUBJECTS.length);
429
+ const i = SUBJECTS.indexOf(subject);
430
+ expect(i).toBeGreaterThan(-1);
431
+ const entry = changes.find((c) => c.merged_at === dateOf(i));
432
+ if (entry === undefined)
433
+ throw new Error(`no entry for subject ${i}`);
434
+ return entry;
435
+ };
436
+ test.each(CASES)('a subject %s', (_label, subject, pr) => {
437
+ expect(entryOf(subject).pr).toBe(pr);
393
438
  });
394
439
  test('the end of the subject is asked from git bounded to the tail width', () => {
395
- adopt();
396
- repo.commit({ 'a.ts': '1' }, 'fix: a', '2026-10-16T10:00:00Z');
397
- const spy = (0, helpers_1.spyOnGit)();
398
- listed();
399
- spy.restore();
400
- expect(spy.calls.some((a) => a.includes('--format=%H%x1f%<(64,ltrunc)%s'))).toBe(true);
440
+ expect(oneFix().calls.some((a) => a.includes('--format=%H%x1f%<(64,ltrunc)%s'))).toBe(true);
401
441
  });
402
442
  test('the subject itself is still clipped while the pr is read from its end', () => {
403
- adopt();
404
- repo.commit({ 'a.ts': '1' }, `fix: ${'a'.repeat(5000)} (#12)`, '2026-10-16T10:00:00Z');
405
- const entry = listed().changes[0];
443
+ const entry = entryOf(CLIPPED);
406
444
  expect(entry.subject.length).toBe(list_1.MAX_SUBJECT);
407
445
  expect(entry.pr).toBe(12);
408
446
  });
409
447
  test('the tail never leaks into the subject', () => {
410
- adopt();
411
- repo.commit({ 'a.ts': '1' }, 'fix: short (#4)', '2026-10-16T10:00:00Z');
412
- expect(listed().changes[0].subject).toBe('fix: short (#4)');
448
+ expect(entryOf(SHORT).subject).toBe(SHORT);
413
449
  });
414
450
  });
415
451
  describe('listChanges: read-only and bounded (R5.7)', () => {
@@ -444,22 +480,16 @@ describe('listChanges: read-only and bounded (R5.7)', () => {
444
480
  expect((0, list_fixtures_1.byClass)(r)).toEqual({ 'fix: next': 'missing', 'feat: first': 'invalid-record' });
445
481
  expect(r.changes[1].records).toEqual([{ path: (0, list_fixtures_1.recordPath)(list_fixtures_1.FIRST), diagnostics: [expect.objectContaining({ code: 'git-failed' })] }]);
446
482
  });
483
+ // The history of `historyIn`, listed once under a git spy: the next four tests only read the calls it made.
484
+ const historyCalls = (() => { const shared = sharedListing(history); return () => shared().calls; })();
447
485
  test('every git call is read-only', () => {
448
- history();
449
- const spy = (0, helpers_1.spyOnGit)();
450
- listed();
451
- spy.restore();
452
- const subcommands = new Set(spy.calls.map((a) => a.find((x) => !x.startsWith('-') && x !== 'diff.relative=false') ?? ''));
486
+ const subcommands = new Set(historyCalls().map((a) => a.find((x) => !x.startsWith('-') && x !== 'diff.relative=false') ?? ''));
453
487
  expect([...subcommands].sort()).toEqual(expect.arrayContaining(['log']));
454
488
  for (const name of subcommands)
455
489
  expect(['rev-parse', 'log', 'diff', 'ls-tree', 'show', 'for-each-ref', 'config']).toContain(name);
456
490
  });
457
491
  test('every git log carries the safety flags', () => {
458
- history();
459
- const spy = (0, helpers_1.spyOnGit)();
460
- listed();
461
- spy.restore();
462
- const logs = spy.calls.filter((a) => a.includes('log'));
492
+ const logs = historyCalls().filter((a) => a.includes('log'));
463
493
  expect(logs.length).toBeGreaterThanOrEqual(3);
464
494
  for (const call of logs) {
465
495
  expect(call).toContain('--no-show-signature');
@@ -468,11 +498,7 @@ describe('listChanges: read-only and bounded (R5.7)', () => {
468
498
  }
469
499
  });
470
500
  test('pathspecs are literal and follow a --', () => {
471
- history();
472
- const spy = (0, helpers_1.spyOnGit)();
473
- listed();
474
- spy.restore();
475
- const withPath = spy.calls.filter((a) => a.some((x) => x.includes('docs/changes')));
501
+ const withPath = historyCalls().filter((a) => a.some((x) => x.includes('docs/changes')));
476
502
  expect(withPath.length).toBeGreaterThan(0);
477
503
  for (const call of withPath) {
478
504
  const dash = call.indexOf('--');
@@ -481,11 +507,7 @@ describe('listChanges: read-only and bounded (R5.7)', () => {
481
507
  }
482
508
  });
483
509
  test('the history is read along the first parent only, in pages', () => {
484
- history();
485
- const spy = (0, helpers_1.spyOnGit)();
486
- listed();
487
- spy.restore();
488
- const scans = spy.calls.filter((a) => a.includes('log') && a.some((x) => x.startsWith('--max-count=')));
510
+ const scans = historyCalls().filter((a) => a.includes('log') && a.some((x) => x.startsWith('--max-count=')));
489
511
  expect(scans.length).toBeGreaterThanOrEqual(2);
490
512
  for (const call of scans) {
491
513
  expect(call).toContain('--first-parent');
@@ -494,12 +516,11 @@ describe('listChanges: read-only and bounded (R5.7)', () => {
494
516
  });
495
517
  test('git calls per entry are constant: recorded entries cost the same however long the history', () => {
496
518
  adopt();
497
- const add = (from, to) => {
498
- for (let i = from; i <= to; i++) {
499
- const id = `2026-10-16-r${String(i).padStart(3, '0')}`;
500
- repo.commit({ [(0, list_fixtures_1.recordPath)(id)]: (0, list_fixtures_1.record)(id) }, `feat: r${i}`, '2026-10-16T10:00:00Z');
501
- }
502
- };
519
+ const add = (from, to) => commitMany(Array.from({ length: to - from + 1 }, (_, k) => {
520
+ const i = from + k;
521
+ const id = `2026-10-16-r${String(i).padStart(3, '0')}`;
522
+ return [{ [(0, list_fixtures_1.recordPath)(id)]: (0, list_fixtures_1.record)(id) }, `feat: r${i}`, '2026-10-16T10:00:00Z'];
523
+ }));
503
524
  const count = () => { const spy = (0, helpers_1.spyOnGit)(); const r = listed(); spy.restore(); expect(r.changes.length).toBeGreaterThan(0); return spy.calls.length; };
504
525
  add(1, 5);
505
526
  const small = count();