agentic-workflow-manager 9.21.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 (71) hide show
  1. package/dist/src/commands/change/index.js +16 -5
  2. package/dist/src/commands/init.js +8 -4
  3. package/dist/src/commands/preflight/checks.js +22 -0
  4. package/dist/src/commands/registry/add.js +9 -3
  5. package/dist/src/commands/sensors/compatibility/contract.js +56 -3
  6. package/dist/src/commands/sensors/compatibility/manifest.js +16 -2
  7. package/dist/src/commands/sensors/index.js +10 -1
  8. package/dist/src/commands/sensors/inputs-digest.js +315 -0
  9. package/dist/src/commands/sensors/prepare.js +10 -0
  10. package/dist/src/commands/sensors/result-cache.js +278 -0
  11. package/dist/src/commands/sensors/result.js +31 -12
  12. package/dist/src/commands/sensors/run.js +265 -8
  13. package/dist/src/commands/sensors/status.js +66 -7
  14. package/dist/src/commands/sync.js +11 -5
  15. package/dist/src/commands/update.js +23 -12
  16. package/dist/src/core/changes/derive.js +6 -3
  17. package/dist/src/core/changes/git.js +1 -2
  18. package/dist/src/core/changes/receipt.js +219 -9
  19. package/dist/src/core/changes/worktree.js +15 -3
  20. package/dist/src/core/init/steps.js +3 -8
  21. package/dist/src/core/registries.js +20 -15
  22. package/dist/src/core/registry-git-errors.js +107 -0
  23. package/dist/src/core/registry-git.js +93 -0
  24. package/dist/src/core/registry-sync-report.js +74 -0
  25. package/dist/src/core/versioning.js +3 -6
  26. package/dist/src/index.js +5 -8
  27. package/dist/tests/commands/change/new.test.js +104 -58
  28. package/dist/tests/commands/change/validate-unmeasured.test.js +246 -0
  29. package/dist/tests/commands/change/validate.test.js +164 -108
  30. package/dist/tests/commands/changes/list.test.js +41 -22
  31. package/dist/tests/commands/job/gate-reconcile.test.js +13 -2
  32. package/dist/tests/commands/preflight/sensors-inputs.test.js +110 -0
  33. package/dist/tests/commands/registry-sync-honesty.test.js +175 -0
  34. package/dist/tests/commands/sensors/compatibility/contract.test.js +60 -0
  35. package/dist/tests/commands/sensors/compatibility/manifest.test.js +94 -0
  36. package/dist/tests/commands/sensors/exec.test.js +3 -1
  37. package/dist/tests/commands/sensors/inputs-digest.test.js +373 -0
  38. package/dist/tests/commands/sensors/result-cache.test.js +352 -0
  39. package/dist/tests/commands/sensors/run-cache.test.js +434 -0
  40. package/dist/tests/commands/sensors/run-slice.test.js +171 -0
  41. package/dist/tests/commands/sensors/status-inputs.test.js +271 -0
  42. package/dist/tests/commands/update.test.js +8 -8
  43. package/dist/tests/commands/watch/supervisor-loop.test.js +21 -5
  44. package/dist/tests/core/changes/helpers.js +15 -3
  45. package/dist/tests/core/changes/list-paging.test.js +131 -110
  46. package/dist/tests/core/changes/list.test.js +133 -83
  47. package/dist/tests/core/changes/receipt.test.js +279 -8
  48. package/dist/tests/core/diagnostics/provider-checks.test.js +3 -0
  49. package/dist/tests/core/init/steps-registry-sync.test.js +6 -8
  50. package/dist/tests/core/registry-git.test.js +153 -0
  51. package/dist/tests/helpers/commit-seeded-registry.js +18 -0
  52. package/dist/tests/integration/codex-provider-isolated.test.js +7 -2
  53. package/dist/tests/integration/copilot-init-isolated.test.js +7 -4
  54. package/dist/tests/structural/active-documentation.test.js +0 -41
  55. package/dist/tests/structural/bundle-skill-reference-contract.test.js +0 -12
  56. package/dist/tests/structural/cli-reference-matches-commander.test.js +94 -0
  57. package/dist/tests/structural/native-release-artifacts.test.js +116 -107
  58. package/dist/tests/structural/sensor-documentation-contract.test.js +0 -47
  59. package/dist/tests/structural/sensor-portability-contract.test.js +0 -7
  60. package/dist/tests/structural/simple-git-v4-pin.test.js +49 -0
  61. package/package.json +3 -2
  62. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  63. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  64. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  65. package/prebuilds/win32-x64/secure_fs.node +0 -0
  66. package/dist/tests/structural/cloud-admission-docs-contract.test.js +0 -54
  67. package/dist/tests/structural/documentation-phase-is-mechanized.test.js +0 -29
  68. package/dist/tests/structural/operator-environments-docs-contract.test.js +0 -70
  69. package/dist/tests/structural/process-model-description-contract.test.js +0 -34
  70. package/dist/tests/structural/project-declared-sensors-docs-contract.test.js +0 -104
  71. package/dist/tests/structural/r4-plan-currentness-docs.test.js +0 -37
@@ -15,14 +15,42 @@ const change_1 = require("../../../src/commands/change");
15
15
  const types_1 = require("../../../src/core/changes/types");
16
16
  let repo;
17
17
  let template;
18
- beforeAll(() => { template = (0, helpers_1.makeRepo)(); });
19
- afterAll(() => template.cleanup());
18
+ /** The template plus `adoptIn`, and the template plus `historyIn`: prebuilt once, copied per test with no git process. */
19
+ let adopted;
20
+ let historic;
21
+ beforeAll(() => {
22
+ template = (0, helpers_1.makeRepo)();
23
+ adopted = (0, helpers_1.cloneRepo)(template);
24
+ (0, list_fixtures_1.adoptIn)(adopted);
25
+ historic = (0, helpers_1.cloneRepo)(template);
26
+ (0, list_fixtures_1.historyIn)(historic);
27
+ });
28
+ afterAll(() => { adopted.cleanup(); historic.cleanup(); template.cleanup(); });
20
29
  beforeEach(() => { repo = (0, helpers_1.cloneRepo)(template); });
21
30
  afterEach(() => { jest.restoreAllMocks(); repo.cleanup(); });
22
31
  const list = (opts = {}) => (0, list_fixtures_1.listIn)(repo, opts);
23
32
  const listed = (opts = {}) => (0, list_fixtures_1.ok)(list(opts));
24
- const adopt = (id) => (0, list_fixtures_1.adoptIn)(repo, id);
25
- const history = () => (0, list_fixtures_1.historyIn)(repo);
33
+ const startFrom = (state) => { repo.cleanup(); repo = (0, helpers_1.cloneRepo)(state); };
34
+ /** The repository after `adoptIn(repo)` (a copy of the prebuilt one). */
35
+ const adopt = () => startFrom(adopted);
36
+ /** The repository after `historyIn(repo)` (a copy of the prebuilt one). */
37
+ const history = () => startFrom(historic);
38
+ /** The history of `historyIn`, listed once under a git spy for the tests that only read that listing (the state is never changed). */
39
+ let historyListing;
40
+ function listedHistory() {
41
+ if (historyListing === undefined) {
42
+ const spy = (0, helpers_1.spyOnGit)();
43
+ try {
44
+ historyListing = { result: (0, list_fixtures_1.ok)((0, list_fixtures_1.listIn)(historic)), calls: spy.calls };
45
+ }
46
+ finally {
47
+ spy.restore();
48
+ }
49
+ }
50
+ return historyListing;
51
+ }
52
+ /** The commits `repo.commit(files, message, date)` would make, in order, written by one `git fast-import` (importCommits). */
53
+ const commitMany = (planned) => (0, helpers_1.importCommits)(repo, planned.map(([files, message, date]) => ({ files, message: `${message}\n`, date })));
26
54
  describe('listChanges: input and base (R5.1, R3.1, R3.2, R3.5)', () => {
27
55
  test('resolves the local main by default and reports it', () => {
28
56
  adopt();
@@ -95,11 +123,7 @@ describe('listChanges: input and base (R5.1, R3.1, R3.2, R3.5)', () => {
95
123
  expect(r.changes.find((c) => c.subject === 'feat: first (#10)')?.classification).toBe('recorded');
96
124
  });
97
125
  test('a base that moves during the run cannot change the answer: every git call after the pin uses commit ids', () => {
98
- history();
99
- const spy = (0, helpers_1.spyOnGit)();
100
- listed();
101
- spy.restore();
102
- const afterPin = spy.calls.filter((a) => a.includes('log') || a.includes('diff') || a.includes('ls-tree') || a.includes('for-each-ref'));
126
+ const afterPin = listedHistory().calls.filter((a) => a.includes('log') || a.includes('diff') || a.includes('ls-tree') || a.includes('for-each-ref'));
103
127
  expect(afterPin.length).toBeGreaterThan(0);
104
128
  for (const call of afterPin)
105
129
  expect(call).not.toContain('main');
@@ -111,15 +135,16 @@ describe('listChanges: adoption point (R5.4)', () => {
111
135
  expect(listed()).toMatchObject({ adopted: false, changes: [], truncated: false, omitted: 0 });
112
136
  });
113
137
  test('R5.4 a docs/changes/README.md alone does not count: the repo has not adopted Change Records', () => {
114
- repo.commit({ 'docs/changes/README.md': 'hello' }, 'docs: readme', '2026-10-16T09:00:00Z');
115
- repo.commit({ 'a.ts': '1' }, 'feat: a', '2026-10-16T10:00:00Z');
138
+ commitMany([[{ 'docs/changes/README.md': 'hello' }, 'docs: readme', '2026-10-16T09:00:00Z'], [{ 'a.ts': '1' }, 'feat: a', '2026-10-16T10:00:00Z']]);
116
139
  expect(listed()).toMatchObject({ adopted: false, changes: [], truncated: false, omitted: 0 });
117
140
  });
118
141
  test('R5.4 a README first and a record later: the adoption is the commit that added the record', () => {
119
- repo.commit({ 'docs/changes/README.md': 'hello' }, 'docs: readme', '2026-10-16T09:00:00Z');
120
- repo.commit({ 'a.ts': '1' }, 'feat: before the first record', '2026-10-16T10:00:00Z');
121
- repo.commit({ [(0, list_fixtures_1.recordPath)(list_fixtures_1.FIRST)]: (0, list_fixtures_1.record)(list_fixtures_1.FIRST) }, 'feat: first record', '2026-10-16T11:00:00Z');
122
- repo.commit({ 'b.ts': '1' }, 'fix: after', '2026-10-16T12:00:00Z');
142
+ commitMany([
143
+ [{ 'docs/changes/README.md': 'hello' }, 'docs: readme', '2026-10-16T09:00:00Z'],
144
+ [{ 'a.ts': '1' }, 'feat: before the first record', '2026-10-16T10:00:00Z'],
145
+ [{ [(0, list_fixtures_1.recordPath)(list_fixtures_1.FIRST)]: (0, list_fixtures_1.record)(list_fixtures_1.FIRST) }, 'feat: first record', '2026-10-16T11:00:00Z'],
146
+ [{ 'b.ts': '1' }, 'fix: after', '2026-10-16T12:00:00Z'],
147
+ ]);
123
148
  const r = listed();
124
149
  expect(r.adopted).toBe(true);
125
150
  expect((0, list_fixtures_1.subjects)(r)).toEqual(['fix: after', 'feat: first record']);
@@ -130,8 +155,7 @@ describe('listChanges: adoption point (R5.4)', () => {
130
155
  ['a non-markdown file', 'docs/changes/2026-10-10-x.txt'],
131
156
  ['a record-shaped name outside docs/changes', 'docs/other/2026-10-10-x.md'],
132
157
  ])('R5.4 %s does not mark the adoption', (_label, file) => {
133
- repo.commit({ [file]: 'x' }, 'docs: not a record', '2026-10-16T09:00:00Z');
134
- repo.commit({ 'a.ts': '1' }, 'feat: a', '2026-10-16T10:00:00Z');
158
+ commitMany([[{ [file]: 'x' }, 'docs: not a record', '2026-10-16T09:00:00Z'], [{ 'a.ts': '1' }, 'feat: a', '2026-10-16T10:00:00Z']]);
135
159
  expect(listed()).toMatchObject({ adopted: false, changes: [] });
136
160
  });
137
161
  test('a record that was added and later deleted still marks the adoption', () => {
@@ -141,15 +165,17 @@ describe('listChanges: adoption point (R5.4)', () => {
141
165
  expect(listed().adopted).toBe(true);
142
166
  });
143
167
  test('commits older than the first one that added a file under docs/changes are omitted; the adopting commit itself is kept', () => {
144
- history();
145
- expect((0, list_fixtures_1.subjects)(listed())).not.toContain('feat: before adoption');
146
- expect((0, list_fixtures_1.subjects)(listed())).not.toContain('chore: init');
147
- expect((0, list_fixtures_1.subjects)(listed())).toContain('feat: first (#10)');
168
+ const { result } = listedHistory();
169
+ expect((0, list_fixtures_1.subjects)(result)).not.toContain('feat: before adoption');
170
+ expect((0, list_fixtures_1.subjects)(result)).not.toContain('chore: init');
171
+ expect((0, list_fixtures_1.subjects)(result)).toContain('feat: first (#10)');
148
172
  });
149
173
  test('modifying a file under docs/changes is not an adoption: only the commit that added the first file is', () => {
150
- repo.commit({ 'a.ts': '1' }, 'feat: before', '2026-10-16T09:00:00Z');
151
- repo.commit({ [(0, list_fixtures_1.recordPath)(list_fixtures_1.FIRST)]: (0, list_fixtures_1.record)(list_fixtures_1.FIRST) }, 'docs: first file', '2026-10-16T10:00:00Z');
152
- repo.commit({ [(0, list_fixtures_1.recordPath)(list_fixtures_1.FIRST)]: (0, list_fixtures_1.record)(list_fixtures_1.FIRST, { summary: 'Edit.' }) }, 'docs: edit', '2026-10-16T11:00:00Z');
174
+ commitMany([
175
+ [{ 'a.ts': '1' }, 'feat: before', '2026-10-16T09:00:00Z'],
176
+ [{ [(0, list_fixtures_1.recordPath)(list_fixtures_1.FIRST)]: (0, list_fixtures_1.record)(list_fixtures_1.FIRST) }, 'docs: first file', '2026-10-16T10:00:00Z'],
177
+ [{ [(0, list_fixtures_1.recordPath)(list_fixtures_1.FIRST)]: (0, list_fixtures_1.record)(list_fixtures_1.FIRST, { summary: 'Edit.' }) }, 'docs: edit', '2026-10-16T11:00:00Z'],
178
+ ]);
153
179
  expect((0, list_fixtures_1.subjects)(listed())).toEqual(['docs: edit', 'docs: first file']);
154
180
  });
155
181
  test('an adopted repo whose window holds no commit lists nothing', () => {
@@ -180,24 +206,20 @@ describe('listChanges: adoption point (R5.4)', () => {
180
206
  });
181
207
  describe('listChanges: classification (R5.3)', () => {
182
208
  test('recorded, missing, exempt and invalid-record', () => {
183
- history();
184
- expect((0, list_fixtures_1.byClass)(listed())).toEqual({ 'feat: first (#10)': 'recorded', 'fix: no record': 'missing', 'chore(release): v1.0.0 [skip ci]': 'exempt', 'feat: bad record': 'invalid-record' });
209
+ expect((0, list_fixtures_1.byClass)(listedHistory().result)).toEqual({ 'feat: first (#10)': 'recorded', 'fix: no record': 'missing', 'chore(release): v1.0.0 [skip ci]': 'exempt', 'feat: bad record': 'invalid-record' });
185
210
  });
186
211
  test('entries come newest first, in first-parent order', () => {
187
- history();
188
- expect((0, list_fixtures_1.subjects)(listed())).toEqual(['feat: bad record', 'chore(release): v1.0.0 [skip ci]', 'fix: no record', 'feat: first (#10)']);
212
+ expect((0, list_fixtures_1.subjects)(listedHistory().result)).toEqual(['feat: bad record', 'chore(release): v1.0.0 [skip ci]', 'fix: no record', 'feat: first (#10)']);
189
213
  });
190
214
  test('a recorded entry holds the path and the full frontmatter, nothing else', () => {
191
- history();
192
- const first = listed().changes.find((c) => c.subject === 'feat: first (#10)');
215
+ const first = listedHistory().result.changes.find((c) => c.subject === 'feat: first (#10)');
193
216
  expect(first?.records).toHaveLength(1);
194
217
  expect(first?.records[0]).toMatchObject({ path: (0, list_fixtures_1.recordPath)(list_fixtures_1.FIRST), schema: 'awm.change-record/v1', id: list_fixtures_1.FIRST, title: `T ${list_fixtures_1.FIRST}`, summary_short: 'Primer cambio.', type: 'feat', modules: ['cli'] });
195
218
  expect(first?.records[0]).not.toHaveProperty('diagnostics');
196
219
  expect(first?.records[0]).toHaveProperty('verification.overall', 'pass');
197
220
  });
198
221
  test('an invalid-record entry holds the path and its diagnostics, no frontmatter', () => {
199
- history();
200
- const bad = listed().changes.find((c) => c.subject === 'feat: bad record');
222
+ const bad = listedHistory().result.changes.find((c) => c.subject === 'feat: bad record');
201
223
  expect(bad?.records).toHaveLength(1);
202
224
  expect(bad?.records[0]).toMatchObject({ path: (0, list_fixtures_1.recordPath)('2026-10-17-bad') });
203
225
  expect(bad?.records[0]).not.toHaveProperty('schema');
@@ -258,8 +280,7 @@ describe('listChanges: classification (R5.3)', () => {
258
280
  expect(entry.records[0].diagnostics[0].message).toMatch(/not a regular file/);
259
281
  });
260
282
  test('exempt and missing entries have no records', () => {
261
- history();
262
- const r = listed();
283
+ const r = listedHistory().result;
263
284
  expect(r.changes.find((c) => c.classification === 'exempt')?.records).toEqual([]);
264
285
  expect(r.changes.find((c) => c.classification === 'missing')?.records).toEqual([]);
265
286
  });
@@ -275,8 +296,7 @@ describe('listChanges: classification (R5.3)', () => {
275
296
  });
276
297
  test('the opt-out must be a trailer of the final paragraph: a mention in the subject or the body does not exempt', () => {
277
298
  adopt();
278
- repo.commit({ 'a.ts': '1' }, 'feat: Change-Record: none', '2026-10-16T20:00:00Z');
279
- repo.commit({ 'b.ts': '1' }, 'feat: b\n\nChange-Record: none\n\nand more text', '2026-10-16T21:00:00Z');
299
+ commitMany([[{ 'a.ts': '1' }, 'feat: Change-Record: none', '2026-10-16T20:00:00Z'], [{ 'b.ts': '1' }, 'feat: b\n\nChange-Record: none\n\nand more text', '2026-10-16T21:00:00Z']]);
280
300
  expect((0, list_fixtures_1.byClass)(listed())).toEqual({ 'feat: Change-Record: none': 'missing', 'feat: b': 'missing' });
281
301
  });
282
302
  test('a commit adding two records, one of them invalid, is invalid-record and lists both', () => {
@@ -444,8 +464,7 @@ describe('listChanges: size and paths of what git prints (R5.7, R1.8)', () => {
444
464
  });
445
465
  test('a failing ls-tree makes the entry invalid-record with git-failed, and the others are still read', () => {
446
466
  adopt();
447
- repo.commit({ [(0, list_fixtures_1.recordPath)('2026-10-16-a')]: (0, list_fixtures_1.record)('2026-10-16-a') }, 'feat: a', '2026-10-16T10:00:00Z');
448
- repo.commit({ [(0, list_fixtures_1.recordPath)('2026-10-16-b')]: (0, list_fixtures_1.record)('2026-10-16-b') }, 'feat: b', '2026-10-16T11:00:00Z');
467
+ commitMany([[{ [(0, list_fixtures_1.recordPath)('2026-10-16-a')]: (0, list_fixtures_1.record)('2026-10-16-a') }, 'feat: a', '2026-10-16T10:00:00Z'], [{ [(0, list_fixtures_1.recordPath)('2026-10-16-b')]: (0, list_fixtures_1.record)('2026-10-16-b') }, 'feat: b', '2026-10-16T11:00:00Z']]);
449
468
  let fails = 1;
450
469
  const spy = (0, helpers_1.spyOnGit)((args) => { if (args.includes('ls-tree') && fails-- > 0)
451
470
  throw new Error('boom'); });
@@ -456,8 +475,7 @@ describe('listChanges: size and paths of what git prints (R5.7, R1.8)', () => {
456
475
  });
457
476
  test('a failing read of one blob makes that record git-failed only', () => {
458
477
  adopt();
459
- repo.commit({ [(0, list_fixtures_1.recordPath)('2026-10-16-a')]: (0, list_fixtures_1.record)('2026-10-16-a') }, 'feat: a', '2026-10-16T10:00:00Z');
460
- repo.commit({ [(0, list_fixtures_1.recordPath)('2026-10-16-b')]: (0, list_fixtures_1.record)('2026-10-16-b') }, 'feat: b', '2026-10-16T11:00:00Z');
478
+ commitMany([[{ [(0, list_fixtures_1.recordPath)('2026-10-16-a')]: (0, list_fixtures_1.record)('2026-10-16-a') }, 'feat: a', '2026-10-16T10:00:00Z'], [{ [(0, list_fixtures_1.recordPath)('2026-10-16-b')]: (0, list_fixtures_1.record)('2026-10-16-b') }, 'feat: b', '2026-10-16T11:00:00Z']]);
461
479
  let fails = 1;
462
480
  const spy = (0, helpers_1.spyOnGit)((args) => { if (args.includes('show') && fails-- > 0)
463
481
  throw new Error('boom'); });
@@ -510,8 +528,7 @@ describe('listChanges: derived fields (R5.5)', () => {
510
528
  });
511
529
  test('a tag on a later commit contains the earlier ones', () => {
512
530
  adopt();
513
- repo.commit({ 'a.ts': '1' }, 'fix: a', '2026-10-16T10:00:00Z');
514
- repo.commit({ 'b.ts': '1' }, 'fix: b', '2026-10-16T11:00:00Z');
531
+ commitMany([[{ 'a.ts': '1' }, 'fix: a', '2026-10-16T10:00:00Z'], [{ 'b.ts': '1' }, 'fix: b', '2026-10-16T11:00:00Z']]);
515
532
  repo.gitAt('2026-10-16T12:00:00Z', 'tag', '-a', 'v2.0.0', '-m', 'v2.0.0');
516
533
  expect(listed().changes.map((c) => c.release)).toEqual(['v2.0.0', 'v2.0.0']);
517
534
  });
@@ -558,26 +575,41 @@ describe('listChanges: derived fields (R5.5)', () => {
558
575
  });
559
576
  describe('R5.5 the tag lookup is bounded: only the 500 earliest-dated containing tags are examined per entry', () => {
560
577
  /** Lightweight tags pointing at `sha`, created with one fast-import process. */
561
- const lightTags = (sha, names) => {
562
- repo.gitInput(names.map((n) => `reset refs/tags/${n}\nfrom ${sha}\n`).join('\n'), 'fast-import', '--quiet');
578
+ const lightTags = (sha, names, into = repo) => {
579
+ into.gitInput(names.map((n) => `reset refs/tags/${n}\nfrom ${sha}\n`).join('\n'), 'fast-import', '--quiet');
563
580
  };
564
581
  const NAMES = Array.from({ length: 600 }, (_, i) => `t${String(i).padStart(4, '0')}`);
582
+ // One fix commit carrying the 600 lightweight tags, listed once under a git spy: the next two tests only read that listing.
583
+ let shared;
584
+ afterAll(() => shared?.state.cleanup());
585
+ function sharedTags() {
586
+ if (shared === undefined) {
587
+ const state = (0, helpers_1.cloneRepo)(adopted);
588
+ try {
589
+ state.commit({ 'a.ts': '1' }, 'fix: a', '2026-10-16T10:00:00Z');
590
+ lightTags(state.git('rev-parse', 'HEAD'), NAMES, state);
591
+ const spy = (0, helpers_1.spyOnGit)();
592
+ try {
593
+ shared = { state, result: (0, list_fixtures_1.ok)((0, list_fixtures_1.listIn)(state)), calls: spy.calls };
594
+ }
595
+ finally {
596
+ spy.restore();
597
+ }
598
+ }
599
+ catch (error) {
600
+ state.cleanup();
601
+ throw error;
602
+ }
603
+ }
604
+ return shared;
605
+ }
565
606
  test('the for-each-ref call asks for at most 500 tags', () => {
566
- adopt();
567
- repo.commit({ 'a.ts': '1' }, 'fix: a', '2026-10-16T10:00:00Z');
568
- lightTags(repo.git('rev-parse', 'HEAD'), NAMES);
569
- const spy = (0, helpers_1.spyOnGit)();
570
- listed();
571
- spy.restore();
572
- const lookups = spy.calls.filter((a) => a.includes('for-each-ref') && a.includes('refs/tags/')); // the tag lookup, not the base's
607
+ const lookups = sharedTags().calls.filter((a) => a.includes('for-each-ref') && a.includes('refs/tags/')); // the tag lookup, not the base's
573
608
  expect(lookups).toHaveLength(1);
574
609
  expect(lookups[0]).toContain('--count=500');
575
610
  });
576
611
  test('600 lightweight tags on the commit: the earliest by date then name is the release', () => {
577
- adopt();
578
- repo.commit({ 'a.ts': '1' }, 'fix: a', '2026-10-16T10:00:00Z');
579
- lightTags(repo.git('rev-parse', 'HEAD'), NAMES);
580
- expect(listed().changes[0].release).toBe('t0000');
612
+ expect(sharedTags().result.changes[0].release).toBe('t0000');
581
613
  });
582
614
  test('the earliest annotated tag among the first 500 still wins over 600 lightweight tags', () => {
583
615
  adopt();
@@ -711,8 +743,7 @@ describe('listChanges: shallow clones and the default base (R5.1)', () => {
711
743
  }
712
744
  test('a shallow clone fails with shallow-history and the remedy', () => {
713
745
  adopt();
714
- repo.commit({ 'a.ts': '1' }, 'fix: a', '2026-10-16T10:00:00Z');
715
- repo.commit({ 'b.ts': '1' }, 'fix: b', '2026-10-16T11:00:00Z');
746
+ commitMany([[{ 'a.ts': '1' }, 'fix: a', '2026-10-16T10:00:00Z'], [{ 'b.ts': '1' }, 'fix: b', '2026-10-16T11:00:00Z']]);
716
747
  const dest = clone(1);
717
748
  expect(repo.git('-C', dest, 'rev-parse', '--is-shallow-repository')).toBe('true');
718
749
  expect((0, list_1.listChanges)(dest, { since: list_fixtures_1.SINCE_48H })).toMatchObject({ ok: false, failure: { code: 'shallow-history', remedy: 'git fetch --unshallow' } });
@@ -751,46 +782,66 @@ describe('listChanges: shallow clones and the default base (R5.1)', () => {
751
782
  });
752
783
  });
753
784
  describe('listChanges: hostile text from git is escaped and clipped', () => {
785
+ // The subjects of the next six tests are commits of ONE history after the adoption point, listed once (each entry is found by its
786
+ // own commit date); that state is never changed. The commits are made as each test made its own: repo.commit, or git commit -m.
787
+ const SUBJECTS = {
788
+ hostile: 'feat: \u001b[31mred\u001b[0m \u202eevil\u202c \u2028 \u0085 \u007f end (#5)',
789
+ long: `feat: ${'x'.repeat(5000)}`,
790
+ atCap: `feat: ${'x'.repeat(list_1.MAX_SUBJECT - 6)}`,
791
+ surrogates: `feat: ${'x'.repeat(list_1.MAX_SUBJECT - 7)}\u{1F600}\u{1F600}`,
792
+ };
793
+ const ORDER = ['hostile', 'long', 'atCap', 'surrogates', 'controls', 'empty'];
794
+ const dateOf = (key) => new Date(Date.UTC(2026, 9, 16, 10, 0, 0) + ORDER.indexOf(key) * 60_000).toISOString();
795
+ let shared;
796
+ afterAll(() => shared?.state.cleanup());
797
+ function entry(key) {
798
+ if (shared === undefined) {
799
+ const state = (0, helpers_1.cloneRepo)(adopted);
800
+ try {
801
+ for (const k of ['hostile', 'long', 'atCap', 'surrogates'])
802
+ state.commit({ [`${k}.ts`]: '1' }, SUBJECTS[k], dateOf(k));
803
+ state.gitAt(dateOf('controls'), 'commit', '--allow-empty', '-q', '--allow-empty-message', '-m', '\u001b\u001b');
804
+ state.gitAt(dateOf('empty'), 'commit', '--allow-empty', '-q', '--allow-empty-message', '-m', '');
805
+ shared = { state, result: (0, list_fixtures_1.ok)((0, list_fixtures_1.listIn)(state)) };
806
+ }
807
+ catch (error) {
808
+ state.cleanup();
809
+ throw error;
810
+ }
811
+ }
812
+ expect(shared.result.changes).toHaveLength(ORDER.length);
813
+ const found = shared.result.changes.find((c) => c.merged_at === dateOf(key));
814
+ if (found === undefined)
815
+ throw new Error(`no entry for ${key}`);
816
+ return found;
817
+ }
754
818
  test('control characters, line separators and bidi marks in a subject are escaped', () => {
755
- adopt();
756
- repo.commit({ 'a.ts': '1' }, 'feat: \u001b[31mred\u001b[0m \u202eevil\u202c \u2028 \u0085 \u007f end (#5)', '2026-10-16T10:00:00Z');
757
- const entry = listed().changes[0];
758
- expect(entry.subject).not.toMatch(list_fixtures_1.UNSAFE);
759
- expect(entry.subject).toContain('\\u001b[31mred');
760
- expect(entry.subject).toContain('\\u202e');
761
- expect(entry.pr).toBe(5);
819
+ const e = entry('hostile');
820
+ expect(e.subject).not.toMatch(list_fixtures_1.UNSAFE);
821
+ expect(e.subject).toContain('\\u001b[31mred');
822
+ expect(e.subject).toContain('\\u202e');
823
+ expect(e.pr).toBe(5);
762
824
  });
763
825
  test('a subject is clipped to the subject cap with an ellipsis', () => {
764
- adopt();
765
- repo.commit({ 'a.ts': '1' }, `feat: ${'x'.repeat(5000)}`, '2026-10-16T10:00:00Z');
766
- const subject = listed().changes[0].subject;
826
+ const subject = entry('long').subject;
767
827
  expect(list_1.MAX_SUBJECT).toBe(1024);
768
828
  expect(subject.length).toBe(list_1.MAX_SUBJECT);
769
829
  expect(subject.endsWith('…')).toBe(true);
770
830
  });
771
831
  test('a subject exactly at the cap is not clipped', () => {
772
- adopt();
773
- const subject = `feat: ${'x'.repeat(list_1.MAX_SUBJECT - 6)}`;
774
- repo.commit({ 'a.ts': '1' }, subject, '2026-10-16T10:00:00Z');
775
- expect(listed().changes[0].subject).toBe(subject);
832
+ expect(entry('atCap').subject).toBe(SUBJECTS.atCap);
776
833
  });
777
834
  test('the cut never splits a surrogate pair', () => {
778
- adopt();
779
- repo.commit({ 'a.ts': '1' }, `feat: ${'x'.repeat(list_1.MAX_SUBJECT - 7)}\u{1F600}\u{1F600}`, '2026-10-16T10:00:00Z');
780
- const subject = listed().changes[0].subject;
835
+ const subject = entry('surrogates').subject;
781
836
  expect(Buffer.from(subject, 'utf8').toString('utf8')).toBe(subject);
782
837
  expect(subject.length).toBeLessThanOrEqual(list_1.MAX_SUBJECT);
783
838
  });
784
839
  test('a subject made only of controls is still one clean line', () => {
785
- adopt();
786
- repo.gitAt('2026-10-16T10:00:00Z', 'commit', '--allow-empty', '-q', '--allow-empty-message', '-m', '\u001b\u001b');
787
- const subject = listed().changes[0].subject;
840
+ const subject = entry('controls').subject;
788
841
  expect(subject).not.toMatch(list_fixtures_1.UNSAFE);
789
842
  });
790
843
  test('a commit with an empty message has an empty subject', () => {
791
- adopt();
792
- repo.gitAt('2026-10-16T10:00:00Z', 'commit', '--allow-empty', '-q', '--allow-empty-message', '-m', '');
793
- expect(listed().changes[0].subject).toBe('');
844
+ expect(entry('empty').subject).toBe('');
794
845
  });
795
846
  test('bidi marks inside a valid record are escaped in what list returns', () => {
796
847
  adopt();
@@ -815,8 +866,7 @@ describe('listChanges: hostile text from git is escaped and clipped', () => {
815
866
  expect(r.changes[0].sha).toMatch(/^[0-9a-f]{40}$/);
816
867
  });
817
868
  test('GIT_LITERAL_PATHSPECS and GIT_GLOB_PATHSPECS in the environment do not change the answer', () => {
818
- history();
819
- const plain = (0, list_fixtures_1.byClass)(listed());
820
- expect((0, helpers_1.withEnv)({ GIT_LITERAL_PATHSPECS: '1', GIT_GLOB_PATHSPECS: '1' }, () => (0, list_fixtures_1.byClass)(listed()))).toEqual(plain);
869
+ const plain = (0, list_fixtures_1.byClass)(listedHistory().result);
870
+ expect((0, helpers_1.withEnv)({ GIT_LITERAL_PATHSPECS: '1', GIT_GLOB_PATHSPECS: '1' }, () => (0, list_fixtures_1.byClass)((0, list_fixtures_1.ok)((0, list_fixtures_1.listIn)(historic))))).toEqual(plain);
821
871
  });
822
872
  });