agentic-workflow-manager 9.20.0 → 9.20.2
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/changes/index.js +2 -1
- package/dist/src/core/changes/derive.js +21 -8
- package/dist/src/core/changes/git.js +37 -6
- package/dist/src/core/changes/range.js +28 -13
- package/dist/src/core/process/model.js +153 -10
- package/dist/tests/commands/change/new.test.js +28 -1
- package/dist/tests/commands/change/validate.test.js +3 -3
- package/dist/tests/commands/changes/list.test.js +1 -0
- package/dist/tests/commands/process.test.js +4 -0
- package/dist/tests/core/changes/derive.test.js +75 -2
- package/dist/tests/core/changes/git.test.js +83 -1
- package/dist/tests/core/changes/helpers.js +2 -2
- package/dist/tests/core/changes/list.test.js +6 -5
- package/dist/tests/core/changes/range.test.js +89 -0
- package/dist/tests/core/dashboard/write-html.test.js +2 -3
- package/dist/tests/core/evidence/store.test.js +2 -3
- package/dist/tests/core/ledger/store.test.js +3 -9
- package/dist/tests/core/process/discover.test.js +21 -0
- package/dist/tests/core/process/model.test.js +338 -0
- package/dist/tests/helpers/platform.js +27 -0
- package/dist/tests/structural/change-record-git-and-platform-conventions.test.js +6 -5
- package/dist/tests/structural/native-release-artifacts.test.js +295 -46
- package/dist/tests/structural/process-model-description-contract.test.js +34 -0
- package/package.json +1 -1
- 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
|
@@ -129,6 +129,18 @@ describe('deriveFromCommits and deriveModules (no repository)', () => {
|
|
|
129
129
|
expect(result.diagnostic.message).toContain('abc\\ndef');
|
|
130
130
|
expect(result.diagnostic.message.length).toBeLessThanOrEqual(300);
|
|
131
131
|
});
|
|
132
|
+
test('R3.3 (amended) type and scope come from the focus commits; semver and breaking from all', () => {
|
|
133
|
+
const all = [c('docs', 'plans'), c('ci', 'release'), c('docs', 'plans'), c('feat', 'plans', true)];
|
|
134
|
+
const focus = [c('ci', 'release')];
|
|
135
|
+
expect((0, derive_1.deriveFromCommits)(all, focus)).toEqual({ type: 'ci', scope: 'release', semver: 'major', breaking: true });
|
|
136
|
+
});
|
|
137
|
+
test('R3.3 (amended) an empty, unusable or non-array focus falls back to all commits', () => {
|
|
138
|
+
const all = [c('docs', 'plans'), c('ci', 'release')];
|
|
139
|
+
const expected = (0, derive_1.deriveFromCommits)(all);
|
|
140
|
+
for (const focus of [[], [{ type: 'x y', scope: null, breaking: false, subject: '' }], undefined, null, 'x']) {
|
|
141
|
+
expect((0, derive_1.deriveFromCommits)(all, focus)).toEqual(expected);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
132
144
|
});
|
|
133
145
|
describe('deriveFields in a repository', () => {
|
|
134
146
|
let repo;
|
|
@@ -293,11 +305,29 @@ describe('deriveFields in a repository', () => {
|
|
|
293
305
|
const result = (0, derive_1.deriveFields)(nonRepoDir(), 'a'.repeat(40));
|
|
294
306
|
expect(result).toEqual({ ok: false, diagnostic: expect.objectContaining({ code: 'git-failed' }) });
|
|
295
307
|
});
|
|
296
|
-
test('deriveFields
|
|
308
|
+
test('deriveFields reports git-failed when only the focus log (the one that drops docs/plans) fails', () => {
|
|
297
309
|
const base = branch();
|
|
310
|
+
repo.commit({ 'cli/src/a.ts': '1' }, 'feat(c): a');
|
|
311
|
+
const mb = (0, git_1.mergeBase)(repo.root, base);
|
|
312
|
+
// Only the second `git log` carries the docs/plans exclusion: the full log, HEAD and the diff all succeed.
|
|
313
|
+
const git = (0, helpers_1.spyOnGit)((args) => { if (args.includes(':(top,exclude,literal)docs/plans'))
|
|
314
|
+
throw new Error('focus log failed'); });
|
|
315
|
+
try {
|
|
316
|
+
expect((0, derive_1.deriveFields)(repo.root, mb)).toEqual({ ok: false, diagnostic: expect.objectContaining({ code: 'git-failed' }) });
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
git.restore();
|
|
320
|
+
}
|
|
321
|
+
expect(git.calls.filter((a) => a[0] === 'log')).toHaveLength(2);
|
|
322
|
+
});
|
|
323
|
+
test('deriveFields works from a subdirectory of the repository, and still drops the top-level docs/plans there', () => {
|
|
324
|
+
const base = branch();
|
|
325
|
+
// Two plan-only commits: were docs/plans read relative to cli/ (no `top`), they would choose the scope.
|
|
326
|
+
repo.commit({ 'docs/plans/p.md': '1' }, 'docs(plans): plan it');
|
|
327
|
+
repo.commit({ 'docs/plans/p.md': '2' }, 'docs(plans): amend it');
|
|
298
328
|
repo.commit({ 'cli/src/a.ts': '1', 'top.ts': '1' }, 'feat(c): top and sub');
|
|
299
329
|
const sub = path_1.default.join(repo.root, 'cli');
|
|
300
|
-
expect((0, derive_1.deriveFields)(sub, (0, git_1.mergeBase)(repo.root, base))).toEqual({ ok: true, fields: { type: 'feat', scope: 'c', semver: 'minor', breaking: false, modules: ['.', 'cli/src'] } });
|
|
330
|
+
expect((0, derive_1.deriveFields)(sub, (0, git_1.mergeBase)(repo.root, base))).toEqual({ ok: true, fields: { type: 'feat', scope: 'c', semver: 'minor', breaking: false, modules: ['.', 'cli/src', 'docs/plans'] } });
|
|
301
331
|
});
|
|
302
332
|
test('R3.3 a signed breaking commit is derived even with log.showSignature on', () => {
|
|
303
333
|
const base = branch();
|
|
@@ -432,6 +462,49 @@ describe('deriveFields in a repository', () => {
|
|
|
432
462
|
}
|
|
433
463
|
expect(moved).toBe(true);
|
|
434
464
|
});
|
|
465
|
+
test('R3.3 (amended) plan-only commits do not choose type or scope (the #228 shape)', () => {
|
|
466
|
+
const base = branch();
|
|
467
|
+
repo.commit({ 'docs/plans/p.md': '1' }, 'docs(plans): plan it');
|
|
468
|
+
repo.commit({ '.github/workflows/ci.yml': '1' }, 'ci(release): shard');
|
|
469
|
+
repo.commit({ 'docs/plans/p.md': '2' }, 'docs(plans): amend A1');
|
|
470
|
+
repo.commit({ 'docs/plans/p.md': '3' }, 'docs(plans): amend A2');
|
|
471
|
+
repo.commit({ '.github/workflows/release.yml': '1' }, 'ci(release): gate it');
|
|
472
|
+
repo.commit({ 'docs/decisions.md': '1' }, 'docs(decisions): record it');
|
|
473
|
+
expect((0, derive_1.deriveFields)(repo.root, (0, git_1.mergeBase)(repo.root, base))).toEqual({ ok: true, fields: { type: 'ci', scope: 'release', semver: 'none', breaking: false, modules: ['.github/workflows', 'docs', 'docs/plans'] } });
|
|
474
|
+
});
|
|
475
|
+
test('R3.3 (amended) a plan-only commit still counts for semver and breaking', () => {
|
|
476
|
+
const base = branch();
|
|
477
|
+
repo.commit({ 'docs/plans/p.md': '1' }, 'feat(plans)!: plan a breaking change');
|
|
478
|
+
repo.commit({ 'cli/src/a.ts': '1' }, 'fix(process): code');
|
|
479
|
+
expect((0, derive_1.deriveFields)(repo.root, (0, git_1.mergeBase)(repo.root, base))).toEqual({ ok: true, fields: { type: 'fix', scope: 'process', semver: 'major', breaking: true, modules: ['cli/src', 'docs/plans'] } });
|
|
480
|
+
});
|
|
481
|
+
test('R3.3 (amended) a commit touching docs/plans and code counts for type and scope', () => {
|
|
482
|
+
const base = branch();
|
|
483
|
+
repo.commit({ 'cli/src/a.ts': '1' }, 'chore(x): first');
|
|
484
|
+
repo.commit({ 'docs/plans/p.md': '1', 'cli/src/b.ts': '1' }, 'docs(plans): mixed');
|
|
485
|
+
repo.commit({ 'docs/plans/p.md': '2', 'cli/src/c.ts': '1' }, 'docs(plans): mixed again');
|
|
486
|
+
expect((0, derive_1.deriveFields)(repo.root, (0, git_1.mergeBase)(repo.root, base))).toMatchObject({ ok: true, fields: { type: 'chore', scope: 'plans' } });
|
|
487
|
+
});
|
|
488
|
+
test('R3.3 (amended) a pure planning branch keeps type and scope from its plan commits', () => {
|
|
489
|
+
const base = branch();
|
|
490
|
+
repo.commit({ 'docs/plans/p.md': '1' }, 'docs(plans): plan only');
|
|
491
|
+
expect((0, derive_1.deriveFields)(repo.root, (0, git_1.mergeBase)(repo.root, base))).toEqual({ ok: true, fields: { type: 'docs', scope: 'plans', semver: 'none', breaking: false, modules: ['docs/plans'] } });
|
|
492
|
+
});
|
|
493
|
+
test('R3.3 (amended) only the top-level docs/plans is excluded, not similarly named directories', () => {
|
|
494
|
+
const base = branch();
|
|
495
|
+
// docs/plans-old decides the type (the only feat) and cli/docs/plans the scope (nested 2 against archive 1): excluding
|
|
496
|
+
// either directory as well would change the result (chore, or the archive/nested tie broken to archive).
|
|
497
|
+
repo.commit({ 'docs/plans-old/p.md': '1' }, 'feat(archive): old plans');
|
|
498
|
+
repo.commit({ 'cli/docs/plans/p.md': '1' }, 'chore(nested): nested plans');
|
|
499
|
+
repo.commit({ 'cli/src/a.ts': '1' }, 'chore(nested): code');
|
|
500
|
+
expect((0, derive_1.deriveFields)(repo.root, (0, git_1.mergeBase)(repo.root, base))).toMatchObject({ ok: true, fields: { type: 'feat', scope: 'nested' } });
|
|
501
|
+
});
|
|
502
|
+
test('R3.3 (amended) non-conventional code commits leave type and scope to every conventional commit, here the plan', () => {
|
|
503
|
+
const base = branch();
|
|
504
|
+
repo.commit({ 'cli/x.ts': '1' }, 'Update the code');
|
|
505
|
+
repo.commit({ 'docs/plans/p.md': '1' }, 'docs(plans): plan it');
|
|
506
|
+
expect((0, derive_1.deriveFields)(repo.root, (0, git_1.mergeBase)(repo.root, base))).toEqual({ ok: true, fields: { type: 'docs', scope: 'plans', semver: 'none', breaking: false, modules: ['cli', 'docs/plans'] } });
|
|
507
|
+
});
|
|
435
508
|
});
|
|
436
509
|
describe('commitsFromRecords: hostile subjects are never handed to the release parser (R3.3), in linear time', () => {
|
|
437
510
|
// The release parser's header regex backtracks quadratically on blanks followed by a line terminator its `.` cannot cross, so a
|
|
@@ -68,7 +68,7 @@ describe('git functions in a repository', () => {
|
|
|
68
68
|
test('returns null for a directory that does not exist', () => {
|
|
69
69
|
expect((0, git_1.runGit)(path_1.default.join(repo.home, 'missing'), ['status'])).toBeNull();
|
|
70
70
|
});
|
|
71
|
-
test('R3.1 resolves base: explicit, origin/HEAD, main, master', () => {
|
|
71
|
+
test('R3.1 resolves base: explicit, origin/HEAD, origin/main, origin/master, main, master', () => {
|
|
72
72
|
branch();
|
|
73
73
|
expect(baseOf(repo.root)).toBe('main');
|
|
74
74
|
expect(baseOf(repo.root, 'main')).toBe('main');
|
|
@@ -76,6 +76,39 @@ describe('git functions in a repository', () => {
|
|
|
76
76
|
repo.git('symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/main');
|
|
77
77
|
expect(baseOf(repo.root)).toBe('refs/remotes/origin/HEAD');
|
|
78
78
|
});
|
|
79
|
+
test('R3.1 without origin/HEAD, origin/main wins over a local main', () => {
|
|
80
|
+
branch();
|
|
81
|
+
repo.git('update-ref', 'refs/remotes/origin/main', 'main');
|
|
82
|
+
expect(baseOf(repo.root)).toBe('refs/remotes/origin/main');
|
|
83
|
+
});
|
|
84
|
+
test('R3.1 origin/master is used when there is no origin/HEAD nor origin/main, before a local main', () => {
|
|
85
|
+
branch();
|
|
86
|
+
repo.git('update-ref', 'refs/remotes/origin/master', 'main');
|
|
87
|
+
expect(baseOf(repo.root)).toBe('refs/remotes/origin/master');
|
|
88
|
+
});
|
|
89
|
+
test('R3.1 with origin/main and origin/master on different commits and no origin/HEAD, origin/main is the base', () => {
|
|
90
|
+
branch();
|
|
91
|
+
repo.commit({ 'a.ts': '1' }, 'feat: a');
|
|
92
|
+
repo.git('update-ref', 'refs/remotes/origin/master', 'main');
|
|
93
|
+
repo.git('update-ref', 'refs/remotes/origin/main', 'HEAD');
|
|
94
|
+
expect(repo.git('rev-parse', 'refs/remotes/origin/main')).not.toBe(repo.git('rev-parse', 'refs/remotes/origin/master'));
|
|
95
|
+
expect((0, git_1.resolveBaseCommit)(repo.root)).toEqual({ base: 'refs/remotes/origin/main', sha: repo.git('rev-parse', 'refs/remotes/origin/main') });
|
|
96
|
+
});
|
|
97
|
+
test('R3.1 origin/HEAD still wins over origin/main', () => {
|
|
98
|
+
branch();
|
|
99
|
+
repo.git('update-ref', 'refs/remotes/origin/main', 'main');
|
|
100
|
+
repo.git('update-ref', 'refs/remotes/origin/trunk', 'main');
|
|
101
|
+
repo.git('symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/trunk');
|
|
102
|
+
expect(baseOf(repo.root)).toBe('refs/remotes/origin/HEAD');
|
|
103
|
+
});
|
|
104
|
+
test('R3.1 a local branch named origin/main is not taken for the remote candidate', () => {
|
|
105
|
+
repo.git('branch', 'origin/main');
|
|
106
|
+
expect(baseOf(repo.root)).toBe('main');
|
|
107
|
+
});
|
|
108
|
+
test('R3.1 an explicit base never falls back to origin/main', () => {
|
|
109
|
+
repo.git('update-ref', 'refs/remotes/origin/main', 'main');
|
|
110
|
+
expect(baseOf(repo.root, 'nope')).toBeNull();
|
|
111
|
+
});
|
|
79
112
|
test('R3.1 an explicit base wins over origin/HEAD and main', () => {
|
|
80
113
|
branch();
|
|
81
114
|
repo.git('branch', 'release');
|
|
@@ -89,6 +122,55 @@ describe('git functions in a repository', () => {
|
|
|
89
122
|
repo.git('branch', '-m', 'main', 'trunk');
|
|
90
123
|
expect(baseOf(repo.root)).toBe('master');
|
|
91
124
|
});
|
|
125
|
+
test.each(['main', 'master'])('R3.1 (Q10) a tag named %s does not redirect the local default, and the base keeps its short name', (name) => {
|
|
126
|
+
branch();
|
|
127
|
+
if (name === 'master')
|
|
128
|
+
repo.git('branch', '-m', 'main', 'master');
|
|
129
|
+
const tagged = repo.commit({ 'tagged.ts': '1' }, 'feat: tagged');
|
|
130
|
+
repo.git('tag', name, tagged);
|
|
131
|
+
expect(repo.git('rev-parse', name)).toBe(tagged); // git alone prefers the tag
|
|
132
|
+
expect((0, git_1.resolveBaseCommit)(repo.root)).toEqual({ base: name, sha: repo.git('rev-parse', `refs/heads/${name}`) });
|
|
133
|
+
});
|
|
134
|
+
test('R3.1 a tag literally named refs/heads/main is not the absent branch main: the base is master', () => {
|
|
135
|
+
branch();
|
|
136
|
+
repo.git('branch', '-m', 'main', 'master');
|
|
137
|
+
const tagged = repo.commit({ 'tagged.ts': '1' }, 'feat: tagged');
|
|
138
|
+
repo.git('tag', 'refs/heads/main', tagged);
|
|
139
|
+
expect(repo.git('rev-parse', 'refs/heads/main')).toBe(tagged); // git alone falls through to refs/tags/refs/heads/main
|
|
140
|
+
expect((0, git_1.resolveBaseCommit)(repo.root)).toEqual({ base: 'master', sha: repo.git('rev-parse', 'refs/heads/master') });
|
|
141
|
+
});
|
|
142
|
+
test('R3.1 a tag literally named refs/remotes/origin/main is not the absent remote branch: the base is main', () => {
|
|
143
|
+
branch();
|
|
144
|
+
const tagged = repo.commit({ 'tagged.ts': '1' }, 'feat: tagged');
|
|
145
|
+
repo.git('tag', 'refs/remotes/origin/main', tagged);
|
|
146
|
+
expect(repo.git('rev-parse', 'refs/remotes/origin/main')).toBe(tagged);
|
|
147
|
+
expect((0, git_1.resolveBaseCommit)(repo.root)).toEqual({ base: 'main', sha: repo.git('rev-parse', 'refs/heads/main') });
|
|
148
|
+
});
|
|
149
|
+
test('R3.1 only an exact refname counts: a branch main/x does not make refs/heads/main exist (a tag of that name is not it)', () => {
|
|
150
|
+
branch();
|
|
151
|
+
repo.git('branch', '-m', 'main', 'master');
|
|
152
|
+
repo.git('branch', 'main/x', 'master'); // `for-each-ref refs/heads/main` lists refs/heads/main/x
|
|
153
|
+
const tagged = repo.commit({ 'tagged.ts': '1' }, 'feat: tagged');
|
|
154
|
+
repo.git('tag', 'refs/heads/main', tagged);
|
|
155
|
+
expect(repo.git('for-each-ref', '--format=%(refname)', 'refs/heads/main')).toBe('refs/heads/main/x');
|
|
156
|
+
expect((0, git_1.resolveBaseCommit)(repo.root)).toEqual({ base: 'master', sha: repo.git('rev-parse', 'refs/heads/master') });
|
|
157
|
+
});
|
|
158
|
+
test('R3.1 when git cannot list the default refs, no default is taken (fail closed)', () => {
|
|
159
|
+
const git = (0, helpers_1.spyOnGit)((args) => { if (args.includes('for-each-ref'))
|
|
160
|
+
throw new Error('boom'); });
|
|
161
|
+
try {
|
|
162
|
+
expect((0, git_1.resolveBaseCommit)(repo.root)).toBeNull();
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
git.restore();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
test('R3.1 an explicit base keeps git\'s own resolution (a tag counts)', () => {
|
|
169
|
+
branch();
|
|
170
|
+
const tagged = repo.commit({ 'tagged.ts': '1' }, 'feat: tagged');
|
|
171
|
+
repo.git('tag', 'v1', tagged);
|
|
172
|
+
expect((0, git_1.resolveBaseCommit)(repo.root, 'v1')).toEqual({ base: 'v1', sha: tagged });
|
|
173
|
+
});
|
|
92
174
|
test('R3.1 origin/HEAD that points nowhere is skipped', () => {
|
|
93
175
|
repo.git('symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/gone');
|
|
94
176
|
expect(baseOf(repo.root)).toBe('main');
|
|
@@ -135,8 +135,8 @@ function gitAtLeast(major, minor) {
|
|
|
135
135
|
const v = gitVersion();
|
|
136
136
|
return v !== null && (v[0] > major || (v[0] === major && v[1] >= minor));
|
|
137
137
|
}
|
|
138
|
-
|
|
139
|
-
exports
|
|
138
|
+
var platform_1 = require("../../helpers/platform");
|
|
139
|
+
Object.defineProperty(exports, "posixOnly", { enumerable: true, get: function () { return platform_1.posixOnly; } });
|
|
140
140
|
/**
|
|
141
141
|
* A copy of `template` in its own directory (same throwaway home, same git isolation): a few milliseconds instead of the
|
|
142
142
|
* several git processes `makeRepo` spawns. For suites that need a fresh repository per test. `cleanup` removes only the copy;
|
|
@@ -28,14 +28,15 @@ describe('listChanges: input and base (R5.1, R3.1, R3.2, R3.5)', () => {
|
|
|
28
28
|
adopt();
|
|
29
29
|
expect(listed().base).toBe('main');
|
|
30
30
|
});
|
|
31
|
-
test('the default base costs one rev-parse per candidate
|
|
31
|
+
test('the default base costs one for-each-ref for all candidates and one rev-parse per existing candidate, not a second one to pin it', () => {
|
|
32
32
|
adopt();
|
|
33
33
|
const spy = (0, helpers_1.spyOnGit)();
|
|
34
34
|
listed();
|
|
35
35
|
spy.restore();
|
|
36
36
|
const asked = (ref) => spy.calls.filter((a) => a.includes(`${ref}^{commit}`)).length;
|
|
37
|
-
expect(
|
|
38
|
-
expect(asked('
|
|
37
|
+
expect(spy.calls.filter((a) => a.includes('for-each-ref') && a.includes('refs/heads/main'))).toHaveLength(1);
|
|
38
|
+
expect(asked('refs/remotes/origin/HEAD')).toBe(0); // absent: never handed to rev-parse
|
|
39
|
+
expect(asked('refs/heads/main')).toBe(1); // the local default resolves as the full ref (Q10)
|
|
39
40
|
});
|
|
40
41
|
test('an explicit base is used and reported', () => {
|
|
41
42
|
adopt();
|
|
@@ -568,7 +569,7 @@ describe('listChanges: derived fields (R5.5)', () => {
|
|
|
568
569
|
const spy = (0, helpers_1.spyOnGit)();
|
|
569
570
|
listed();
|
|
570
571
|
spy.restore();
|
|
571
|
-
const lookups = spy.calls.filter((a) => a.includes('for-each-ref'));
|
|
572
|
+
const lookups = spy.calls.filter((a) => a.includes('for-each-ref') && a.includes('refs/tags/')); // the tag lookup, not the base's
|
|
572
573
|
expect(lookups).toHaveLength(1);
|
|
573
574
|
expect(lookups[0]).toContain('--count=500');
|
|
574
575
|
});
|
|
@@ -610,7 +611,7 @@ describe('listChanges: derived fields (R5.5)', () => {
|
|
|
610
611
|
test('a git failure while looking for the tags is git-failed, never a release of null', () => {
|
|
611
612
|
adopt();
|
|
612
613
|
repo.commit({ 'a.ts': '1' }, 'fix: a', '2026-10-16T10:00:00Z');
|
|
613
|
-
const spy = (0, helpers_1.spyOnGit)((args) => { if (args.includes('for-each-ref'))
|
|
614
|
+
const spy = (0, helpers_1.spyOnGit)((args) => { if (args.includes('for-each-ref') && args.includes('refs/tags/'))
|
|
614
615
|
throw new Error('boom'); });
|
|
615
616
|
const outcome = list();
|
|
616
617
|
spy.restore();
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
3
8
|
const helpers_1 = require("./helpers");
|
|
4
9
|
const command_1 = require("../../../src/core/changes/command");
|
|
10
|
+
const git_1 = require("../../../src/core/changes/git");
|
|
5
11
|
const range_1 = require("../../../src/core/changes/range");
|
|
6
12
|
let repo;
|
|
7
13
|
/** A repository for the groups that need one (the pure recordDirProblem tests need none). */
|
|
@@ -40,11 +46,94 @@ describe('resolveRange', () => {
|
|
|
40
46
|
expect((0, range_1.resolveRange)(repo.root, 'main')).toMatchObject({ ok: false, failure: { code: 'base-unresolved', remedy: 'pass --base <ref>' } });
|
|
41
47
|
expect((0, range_1.resolveRange)(repo.root, 'main').failure.message).toContain('no merge base');
|
|
42
48
|
});
|
|
49
|
+
/** A commit on its own branch from main, made shallow (no parents as far as git can tell): it shares no merge base with feat/x. */
|
|
50
|
+
function shallowCommit() {
|
|
51
|
+
repo.git('checkout', '-q', '-b', 'up', 'main');
|
|
52
|
+
const up = repo.commit({ 'up.ts': '1' }, 'feat: upstream');
|
|
53
|
+
repo.git('checkout', '-q', 'feat/x');
|
|
54
|
+
repo.git('branch', '-D', 'up');
|
|
55
|
+
fs_1.default.writeFileSync(path_1.default.join(repo.root, '.git', 'shallow'), `${up}\n`);
|
|
56
|
+
return up;
|
|
57
|
+
}
|
|
58
|
+
test('R3.1 (Q7) without --base, a shallow origin/main with no merge base gives way to a connected local main', () => {
|
|
59
|
+
const main = (0, helpers_1.branchOff)(repo);
|
|
60
|
+
const head = repo.commit({ 'a.ts': '1' }, 'feat: a');
|
|
61
|
+
const up = shallowCommit();
|
|
62
|
+
repo.git('update-ref', 'refs/remotes/origin/main', up);
|
|
63
|
+
expect((0, range_1.resolveRange)(repo.root, undefined)).toEqual({ ok: true, head, mergeBase: main });
|
|
64
|
+
// `changes list` needs no merge base: it keeps the first candidate that names a commit.
|
|
65
|
+
expect((0, range_1.resolveBaseRef)(repo.root, undefined)).toEqual({ ok: true, base: 'refs/remotes/origin/main', sha: up });
|
|
66
|
+
});
|
|
67
|
+
test('R3.1 (Q7) an explicit --base without a merge base is base-unresolved: no fallback to the defaults', () => {
|
|
68
|
+
(0, helpers_1.branchOff)(repo);
|
|
69
|
+
repo.commit({ 'a.ts': '1' }, 'feat: a');
|
|
70
|
+
repo.git('branch', 'shallow', shallowCommit());
|
|
71
|
+
expect((0, range_1.resolveRange)(repo.root, 'shallow')).toEqual({ ok: false, failure: { code: 'base-unresolved', message: 'no merge base between "shallow" and HEAD', remedy: 'pass --base <ref>' } });
|
|
72
|
+
});
|
|
73
|
+
test('R3.1 (Q7) without --base, when no default that names a commit has a merge base, the failure names the first of them', () => {
|
|
74
|
+
(0, helpers_1.branchOff)(repo);
|
|
75
|
+
repo.commit({ 'a.ts': '1' }, 'feat: a');
|
|
76
|
+
const up = shallowCommit();
|
|
77
|
+
repo.git('update-ref', 'refs/remotes/origin/main', up);
|
|
78
|
+
repo.git('update-ref', 'refs/heads/main', up);
|
|
79
|
+
expect((0, range_1.resolveRange)(repo.root, undefined)).toEqual({ ok: false, failure: { code: 'base-unresolved', message: 'no merge base between "refs/remotes/origin/main" and HEAD', remedy: 'pass --base <ref>' } });
|
|
80
|
+
});
|
|
81
|
+
test('R3.1 without --base, a tag literally named refs/heads/main is not taken for the absent branch main', () => {
|
|
82
|
+
(0, helpers_1.branchOff)(repo);
|
|
83
|
+
const head = repo.commit({ 'a.ts': '1' }, 'feat: a');
|
|
84
|
+
const master = repo.git('rev-parse', 'main');
|
|
85
|
+
repo.git('branch', '-m', 'main', 'master');
|
|
86
|
+
repo.git('tag', 'refs/heads/main', head);
|
|
87
|
+
expect((0, range_1.resolveRange)(repo.root, undefined)).toEqual({ ok: true, head, mergeBase: master });
|
|
88
|
+
});
|
|
89
|
+
test('R3.1 (Q9) the merge base comes from the commit id the base resolved to, not from the base name read again', () => {
|
|
90
|
+
const main = (0, helpers_1.branchOff)(repo);
|
|
91
|
+
const head = repo.commit({ 'a.ts': '1' }, 'feat: a');
|
|
92
|
+
const elsewhere = shallowCommit();
|
|
93
|
+
let moved = false;
|
|
94
|
+
const git = (0, helpers_1.spyOnGit)((args) => {
|
|
95
|
+
if (!moved && args[0] === 'merge-base') {
|
|
96
|
+
moved = true;
|
|
97
|
+
repo.git('update-ref', 'refs/heads/main', elsewhere);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
try {
|
|
101
|
+
expect((0, range_1.resolveRange)(repo.root, undefined)).toEqual({ ok: true, head, mergeBase: main });
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
git.restore();
|
|
105
|
+
}
|
|
106
|
+
expect(moved).toBe(true);
|
|
107
|
+
});
|
|
43
108
|
test('an unreadable HEAD is git-failed', () => {
|
|
44
109
|
expect((0, range_1.resolveRange)((0, helpers_1.nonRepoDir)(repo), undefined)).toMatchObject({ ok: false, failure: { code: 'git-failed' } });
|
|
45
110
|
expect((0, range_1.resolveRange)((0, helpers_1.bad)(undefined), undefined)).toMatchObject({ ok: false, failure: { code: 'git-failed' } });
|
|
46
111
|
});
|
|
47
112
|
});
|
|
113
|
+
describe('BASE_OPTION_HELP (R3.1, Q6)', () => {
|
|
114
|
+
test('names the default candidates in order', () => {
|
|
115
|
+
expect(range_1.BASE_OPTION_HELP).toBe('base branch (default: origin/HEAD, origin/main, origin/master, main, master)');
|
|
116
|
+
});
|
|
117
|
+
test('is built from the candidate list, not written by hand', () => {
|
|
118
|
+
jest.isolateModules(() => {
|
|
119
|
+
jest.doMock('../../../src/core/changes/git', () => ({
|
|
120
|
+
...jest.requireActual('../../../src/core/changes/git'),
|
|
121
|
+
DEFAULT_BASES: [{ ref: 'refs/remotes/origin/trunk', name: 'refs/remotes/origin/trunk' }, { ref: 'refs/heads/dev', name: 'dev' }],
|
|
122
|
+
}));
|
|
123
|
+
expect(require('../../../src/core/changes/range').BASE_OPTION_HELP).toBe('base branch (default: origin/trunk, dev)');
|
|
124
|
+
});
|
|
125
|
+
jest.dontMock('../../../src/core/changes/git');
|
|
126
|
+
});
|
|
127
|
+
test('the default candidates are the remote ones, then the local branches as full refs that report their short name', () => {
|
|
128
|
+
expect(git_1.DEFAULT_BASES).toEqual([
|
|
129
|
+
{ ref: 'refs/remotes/origin/HEAD', name: 'refs/remotes/origin/HEAD' },
|
|
130
|
+
{ ref: 'refs/remotes/origin/main', name: 'refs/remotes/origin/main' },
|
|
131
|
+
{ ref: 'refs/remotes/origin/master', name: 'refs/remotes/origin/master' },
|
|
132
|
+
{ ref: 'refs/heads/main', name: 'main' },
|
|
133
|
+
{ ref: 'refs/heads/master', name: 'master' },
|
|
134
|
+
]);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
48
137
|
describe('headMovedFailure', () => {
|
|
49
138
|
withRepo();
|
|
50
139
|
test('HEAD moving has its own code, and it is an operational failure (exit 2) like the others', () => {
|
|
@@ -7,6 +7,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
7
7
|
const os_1 = __importDefault(require("os"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const write_html_1 = require("../../../src/core/dashboard/write-html");
|
|
10
|
+
const platform_1 = require("../../helpers/platform");
|
|
10
11
|
describe('writeHtmlAtomically', () => {
|
|
11
12
|
let root;
|
|
12
13
|
beforeEach(() => { root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-html-')); });
|
|
@@ -85,9 +86,7 @@ describe('writeHtmlAtomically', () => {
|
|
|
85
86
|
open.mockRestore();
|
|
86
87
|
}
|
|
87
88
|
});
|
|
88
|
-
|
|
89
|
-
if (process.platform === 'win32')
|
|
90
|
-
return;
|
|
89
|
+
(0, platform_1.posixOnly)('uses mode 0600 for a POSIX adjacent temporary file', () => {
|
|
91
90
|
const target = path_1.default.join(root, 'posix.html');
|
|
92
91
|
const open = jest.spyOn(fs_1.default, 'openSync');
|
|
93
92
|
try {
|
|
@@ -8,6 +8,7 @@ const os_1 = __importDefault(require("os"));
|
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const evidence_fixtures_1 = require("../../helpers/evidence-fixtures");
|
|
10
10
|
const store_1 = require("../../../src/core/evidence/store");
|
|
11
|
+
const platform_1 = require("../../helpers/platform");
|
|
11
12
|
describe('cycle evidence store', () => {
|
|
12
13
|
let root;
|
|
13
14
|
beforeEach(() => { root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-')); });
|
|
@@ -20,9 +21,7 @@ describe('cycle evidence store', () => {
|
|
|
20
21
|
expect(fs_1.default.readdirSync(dir)).toEqual([`${evidence.cycleId}.json`]);
|
|
21
22
|
expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, `${evidence.cycleId}.json`), 'utf8')).qa.fixes).toBe(0);
|
|
22
23
|
});
|
|
23
|
-
|
|
24
|
-
if (process.platform === 'win32')
|
|
25
|
-
return;
|
|
24
|
+
(0, platform_1.posixOnly)('rejects symlinked evidence ancestors without writing outside root', () => {
|
|
26
25
|
const outside = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-outside-'));
|
|
27
26
|
fs_1.default.symlinkSync(outside, path_1.default.join(root, '.awm'));
|
|
28
27
|
expect(() => (0, store_1.writeCycleEvidence)(root, (0, evidence_fixtures_1.cycleEvidenceFixture)())).toThrow(/symlink/);
|
|
@@ -8,6 +8,7 @@ const os_1 = __importDefault(require("os"));
|
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const store_1 = require("../../../src/core/ledger/store");
|
|
10
10
|
const types_1 = require("../../../src/core/ledger/types");
|
|
11
|
+
const platform_1 = require("../../helpers/platform");
|
|
11
12
|
function mkTmp() {
|
|
12
13
|
return fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-ledger-'));
|
|
13
14
|
}
|
|
@@ -191,18 +192,11 @@ describe('ledger store — archive', () => {
|
|
|
191
192
|
expect((0, store_1.listEntries)(cwd, 'feat-x')).toEqual([expect.objectContaining({ signature: 'active-evidence' })]);
|
|
192
193
|
expect(fs_1.default.readFileSync(archivePath, 'utf-8')).toContain('archived-evidence');
|
|
193
194
|
});
|
|
194
|
-
|
|
195
|
+
(0, platform_1.symlinkTest)('refuses a dangling symlink archive destination and preserves the active ledger', () => {
|
|
195
196
|
(0, store_1.addEntry)(cwd, entry({ signature: 'active-evidence' }));
|
|
196
197
|
const archivePath = path_1.default.join(cwd, '.awm', 'ledger', 'archive', 'feat-x-20260606T000000.jsonl');
|
|
197
198
|
fs_1.default.mkdirSync(path_1.default.dirname(archivePath), { recursive: true });
|
|
198
|
-
|
|
199
|
-
fs_1.default.symlinkSync(path_1.default.join(cwd, 'missing-target.jsonl'), archivePath);
|
|
200
|
-
}
|
|
201
|
-
catch (error) {
|
|
202
|
-
if (process.platform === 'win32')
|
|
203
|
-
return;
|
|
204
|
-
throw error;
|
|
205
|
-
}
|
|
199
|
+
fs_1.default.symlinkSync(path_1.default.join(cwd, 'missing-target.jsonl'), archivePath);
|
|
206
200
|
expect(() => (0, store_1.archiveLedger)(cwd, 'feat-x', '20260606T000000')).toThrow(/archive.*already exists/i);
|
|
207
201
|
expect(fs_1.default.lstatSync(archivePath).isSymbolicLink()).toBe(true);
|
|
208
202
|
expect((0, store_1.listEntries)(cwd, 'feat-x')).toEqual([expect.objectContaining({ signature: 'active-evidence' })]);
|
|
@@ -66,6 +66,27 @@ describe('discoverProcessModels', () => {
|
|
|
66
66
|
expect(r.models.map((m) => m.name)).toEqual(['mi-proceso']);
|
|
67
67
|
expect(r.diagnostics).toEqual([]);
|
|
68
68
|
});
|
|
69
|
+
it('description sobrevive parse → discover, tal cual la lee YAML', () => {
|
|
70
|
+
const saved = { HOME: process.env.HOME, AWM_HOME: process.env.AWM_HOME };
|
|
71
|
+
process.env.HOME = path_1.default.join(tmp, 'home');
|
|
72
|
+
process.env.AWM_HOME = path_1.default.join(tmp, 'home', '.awm');
|
|
73
|
+
try {
|
|
74
|
+
const root = registry(path_1.default.join(tmp, 'rd'), {
|
|
75
|
+
'con-desc': FIXTURE.replace('NAME', 'con-desc').replace('created:', 'description: "Use when: tracing\\u00A0"\ncreated:'),
|
|
76
|
+
});
|
|
77
|
+
const r = (0, discover_1.discoverProcessModels)([root]);
|
|
78
|
+
expect(r.diagnostics).toEqual([]);
|
|
79
|
+
expect(r.models.map((m) => m.description)).toEqual(['Use when: tracing ']);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
for (const [k, v] of Object.entries(saved)) {
|
|
83
|
+
if (v === undefined)
|
|
84
|
+
delete process.env[k];
|
|
85
|
+
else
|
|
86
|
+
process.env[k] = v;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
69
90
|
it('un modelo roto no impide descubrir los sanos', () => {
|
|
70
91
|
const root = registry(path_1.default.join(tmp, 'r2'), {
|
|
71
92
|
'sano': FIXTURE.replace('NAME', 'sano'),
|