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.
- package/dist/src/commands/change/index.js +16 -5
- package/dist/src/commands/init.js +8 -4
- package/dist/src/commands/preflight/checks.js +22 -0
- package/dist/src/commands/registry/add.js +9 -3
- package/dist/src/commands/sensors/compatibility/contract.js +56 -3
- package/dist/src/commands/sensors/compatibility/manifest.js +16 -2
- package/dist/src/commands/sensors/index.js +10 -1
- package/dist/src/commands/sensors/inputs-digest.js +315 -0
- package/dist/src/commands/sensors/prepare.js +10 -0
- package/dist/src/commands/sensors/result-cache.js +278 -0
- package/dist/src/commands/sensors/result.js +31 -12
- package/dist/src/commands/sensors/run.js +265 -8
- package/dist/src/commands/sensors/status.js +66 -7
- 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/receipt.js +219 -9
- 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 +104 -58
- package/dist/tests/commands/change/validate-unmeasured.test.js +246 -0
- 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/preflight/sensors-inputs.test.js +110 -0
- package/dist/tests/commands/registry-sync-honesty.test.js +175 -0
- package/dist/tests/commands/sensors/compatibility/contract.test.js +60 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +94 -0
- package/dist/tests/commands/sensors/exec.test.js +3 -1
- package/dist/tests/commands/sensors/inputs-digest.test.js +373 -0
- package/dist/tests/commands/sensors/result-cache.test.js +352 -0
- package/dist/tests/commands/sensors/run-cache.test.js +434 -0
- package/dist/tests/commands/sensors/run-slice.test.js +171 -0
- package/dist/tests/commands/sensors/status-inputs.test.js +271 -0
- 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 +279 -8
- 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 +3 -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
|
@@ -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
|
-
|
|
19
|
-
|
|
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
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
145
|
-
expect((0, list_fixtures_1.subjects)(
|
|
146
|
-
expect((0, list_fixtures_1.subjects)(
|
|
147
|
-
expect((0, list_fixtures_1.subjects)(
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
expect(
|
|
759
|
-
expect(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
819
|
-
|
|
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
|
});
|