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.
- package/dist/src/commands/change/index.js +2 -2
- package/dist/src/commands/init.js +8 -4
- package/dist/src/commands/registry/add.js +9 -3
- package/dist/src/commands/sync.js +11 -5
- package/dist/src/commands/update.js +23 -12
- package/dist/src/core/changes/derive.js +6 -3
- package/dist/src/core/changes/git.js +1 -2
- package/dist/src/core/changes/worktree.js +15 -3
- package/dist/src/core/init/steps.js +3 -8
- package/dist/src/core/registries.js +20 -15
- package/dist/src/core/registry-git-errors.js +107 -0
- package/dist/src/core/registry-git.js +93 -0
- package/dist/src/core/registry-sync-report.js +74 -0
- package/dist/src/core/versioning.js +3 -6
- package/dist/src/index.js +5 -8
- package/dist/tests/commands/change/new.test.js +103 -57
- package/dist/tests/commands/change/validate.test.js +164 -108
- package/dist/tests/commands/changes/list.test.js +41 -22
- package/dist/tests/commands/job/gate-reconcile.test.js +13 -2
- package/dist/tests/commands/registry-sync-honesty.test.js +175 -0
- package/dist/tests/commands/sensors/exec.test.js +3 -1
- package/dist/tests/commands/update.test.js +8 -8
- package/dist/tests/commands/watch/supervisor-loop.test.js +21 -5
- package/dist/tests/core/changes/helpers.js +15 -3
- package/dist/tests/core/changes/list-paging.test.js +131 -110
- package/dist/tests/core/changes/list.test.js +133 -83
- package/dist/tests/core/changes/receipt.test.js +18 -6
- package/dist/tests/core/diagnostics/provider-checks.test.js +3 -0
- package/dist/tests/core/init/steps-registry-sync.test.js +6 -8
- package/dist/tests/core/registry-git.test.js +153 -0
- package/dist/tests/helpers/commit-seeded-registry.js +18 -0
- package/dist/tests/integration/codex-provider-isolated.test.js +7 -2
- package/dist/tests/integration/copilot-init-isolated.test.js +7 -4
- package/dist/tests/structural/active-documentation.test.js +0 -41
- package/dist/tests/structural/bundle-skill-reference-contract.test.js +0 -12
- package/dist/tests/structural/cli-reference-matches-commander.test.js +94 -0
- package/dist/tests/structural/native-release-artifacts.test.js +116 -107
- package/dist/tests/structural/sensor-documentation-contract.test.js +0 -47
- package/dist/tests/structural/sensor-portability-contract.test.js +0 -7
- package/dist/tests/structural/simple-git-v4-pin.test.js +49 -0
- package/package.json +2 -2
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
- package/dist/tests/structural/cloud-admission-docs-contract.test.js +0 -54
- package/dist/tests/structural/documentation-phase-is-mechanized.test.js +0 -29
- package/dist/tests/structural/operator-environments-docs-contract.test.js +0 -70
- package/dist/tests/structural/process-model-description-contract.test.js +0 -34
- package/dist/tests/structural/project-declared-sensors-docs-contract.test.js +0 -104
- package/dist/tests/structural/r4-plan-currentness-docs.test.js +0 -37
- package/dist/tests/structural/sensor-execution-optimization-docs-contract.test.js +0 -102
- 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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
]
|
|
392
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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();
|