agentic-workflow-manager 9.19.0 → 9.20.0

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 (59) hide show
  1. package/dist/src/commands/change/index.js +492 -0
  2. package/dist/src/commands/changes/index.js +84 -0
  3. package/dist/src/commands/sensors/index.js +9 -0
  4. package/dist/src/commands/sensors/run.js +1 -0
  5. package/dist/src/core/changes/command.js +38 -0
  6. package/dist/src/core/changes/derive.js +120 -0
  7. package/dist/src/core/changes/fs-safe.js +47 -0
  8. package/dist/src/core/changes/git-env.js +68 -0
  9. package/dist/src/core/changes/git.js +216 -0
  10. package/dist/src/core/changes/list-parse.js +67 -0
  11. package/dist/src/core/changes/list.js +408 -0
  12. package/dist/src/core/changes/parse.js +72 -0
  13. package/dist/src/core/changes/range.js +53 -0
  14. package/dist/src/core/changes/receipt.js +274 -0
  15. package/dist/src/core/changes/record-io.js +149 -0
  16. package/dist/src/core/changes/render-list.js +109 -0
  17. package/dist/src/core/changes/render.js +178 -0
  18. package/dist/src/core/changes/text-safety.js +116 -0
  19. package/dist/src/core/changes/trailers.js +76 -0
  20. package/dist/src/core/changes/types.js +26 -0
  21. package/dist/src/core/changes/validate-shape.js +219 -0
  22. package/dist/src/core/changes/worktree.js +53 -0
  23. package/dist/src/index.js +4 -0
  24. package/dist/src/release/orchestrator.js +3 -1
  25. package/dist/tests/commands/change/new.test.js +1126 -0
  26. package/dist/tests/commands/change/validate.test.js +1145 -0
  27. package/dist/tests/commands/changes/list.test.js +423 -0
  28. package/dist/tests/commands/sensors/index.test.js +1 -1
  29. package/dist/tests/core/changes/derive.test.js +455 -0
  30. package/dist/tests/core/changes/fs-safe.test.js +82 -0
  31. package/dist/tests/core/changes/git-env.test.js +143 -0
  32. package/dist/tests/core/changes/git.test.js +442 -0
  33. package/dist/tests/core/changes/helpers.js +269 -0
  34. package/dist/tests/core/changes/list-fixtures.js +122 -0
  35. package/dist/tests/core/changes/list-long-history.test.js +52 -0
  36. package/dist/tests/core/changes/list-paging.test.js +511 -0
  37. package/dist/tests/core/changes/list-parse.test.js +120 -0
  38. package/dist/tests/core/changes/list-tree.test.js +44 -0
  39. package/dist/tests/core/changes/list.test.js +821 -0
  40. package/dist/tests/core/changes/parse.test.js +142 -0
  41. package/dist/tests/core/changes/predicates.test.js +83 -0
  42. package/dist/tests/core/changes/range.test.js +92 -0
  43. package/dist/tests/core/changes/receipt.test.js +947 -0
  44. package/dist/tests/core/changes/record-io.test.js +307 -0
  45. package/dist/tests/core/changes/record.test.js +549 -0
  46. package/dist/tests/core/changes/render-list.test.js +212 -0
  47. package/dist/tests/core/changes/render.test.js +202 -0
  48. package/dist/tests/core/changes/text-safety.test.js +226 -0
  49. package/dist/tests/core/changes/trailers.test.js +181 -0
  50. package/dist/tests/core/changes/worktree.test.js +259 -0
  51. package/dist/tests/release/orchestrator-trailer.test.js +72 -0
  52. package/dist/tests/release/orchestrator.test.js +1 -0
  53. package/dist/tests/structural/change-commands-are-registered.test.js +29 -0
  54. package/dist/tests/structural/change-record-git-and-platform-conventions.test.js +77 -0
  55. package/package.json +1 -1
  56. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  57. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  58. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  59. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const parse_1 = require("../../../src/core/changes/parse");
4
+ describe('isValidSlug', () => {
5
+ test.each(['a', 'a1', 'a-b-1', '1-2', 'a'.repeat(64)])('accepts %s', (slug) => {
6
+ expect((0, parse_1.isValidSlug)(slug)).toBe(true);
7
+ });
8
+ test.each(['', 'A', 'a_b', 'a--b', '-a', 'a-', 'a b', 'a/b', 'á', 'a'.repeat(65)])('rejects %j', (slug) => {
9
+ expect((0, parse_1.isValidSlug)(slug)).toBe(false);
10
+ });
11
+ test.each([undefined, null, 7, {}, ['a']])('rejects the non-string %j', (slug) => {
12
+ expect((0, parse_1.isValidSlug)(slug)).toBe(false);
13
+ });
14
+ });
15
+ describe('isValidRecordFileName', () => {
16
+ test('accepts date + slug + .md', () => {
17
+ expect((0, parse_1.isValidRecordFileName)('2026-10-17-change-record.md')).toBe(true);
18
+ });
19
+ test.each(['2026-10-17-x.txt', '2026-02-30-x.md', 'x.md', '2026-10-17-.md', '2026-10-17-X.md'])('rejects %s', (name) => {
20
+ expect((0, parse_1.isValidRecordFileName)(name)).toBe(false);
21
+ });
22
+ });
23
+ describe('splitRecord', () => {
24
+ test('splits frontmatter from body', () => {
25
+ expect((0, parse_1.splitRecord)('---\na: 1\n---\nbody\n')).toEqual({ ok: true, frontmatterText: 'a: 1', body: 'body\n' });
26
+ });
27
+ test('normalizes CRLF', () => {
28
+ expect((0, parse_1.splitRecord)('---\r\na: 1\r\n---\r\nbody\r\n')).toEqual({ ok: true, frontmatterText: 'a: 1', body: 'body\n' });
29
+ });
30
+ test('an empty frontmatter block yields empty text', () => {
31
+ expect((0, parse_1.splitRecord)('---\n---\nbody')).toEqual({ ok: true, frontmatterText: '', body: 'body' });
32
+ });
33
+ test.each([['empty input', ''], ['no leading delimiter', 'a: 1\n---\nbody'], ['leading blank line', '\n---\na: 1\n---\nbody']])('%s is frontmatter-missing', (_l, raw) => {
34
+ const r = (0, parse_1.splitRecord)(raw);
35
+ expect(r.ok).toBe(false);
36
+ expect(!r.ok && r.diagnostic.code).toBe('frontmatter-missing');
37
+ });
38
+ test('an unclosed frontmatter block is frontmatter-missing', () => {
39
+ const r = (0, parse_1.splitRecord)('---\na: 1\nbody');
40
+ expect(r.ok).toBe(false);
41
+ expect(!r.ok && r.diagnostic.code).toBe('frontmatter-missing');
42
+ });
43
+ test('only the first closing delimiter ends the frontmatter', () => {
44
+ const r = (0, parse_1.splitRecord)('---\na: 1\n---\nx\n---\ny\n');
45
+ expect(r).toEqual({ ok: true, frontmatterText: 'a: 1', body: 'x\n---\ny\n' });
46
+ });
47
+ });
48
+ describe('loadFrontmatter', () => {
49
+ test('parses a mapping and keeps timestamps as strings', () => {
50
+ expect((0, parse_1.loadFrontmatter)('a: 1\nd: 2026-10-17\nt: 2026-10-17T10:00:00Z')).toEqual({ ok: true, value: { a: 1, d: '2026-10-17', t: '2026-10-17T10:00:00Z' } });
51
+ });
52
+ test('empty text loads as undefined and comment-only text as null (both are not mappings)', () => {
53
+ expect((0, parse_1.loadFrontmatter)('')).toEqual({ ok: true, value: undefined });
54
+ expect((0, parse_1.loadFrontmatter)('# only a comment')).toEqual({ ok: true, value: null });
55
+ });
56
+ test('more than one document is yaml-invalid', () => {
57
+ const r = (0, parse_1.loadFrontmatter)('a: 1\n---\nb: 2');
58
+ expect(!r.ok && r.diagnostic.code).toBe('yaml-invalid');
59
+ });
60
+ test('custom tags are yaml-invalid', () => {
61
+ const r = (0, parse_1.loadFrontmatter)('a: !!js/function "f"');
62
+ expect(!r.ok && r.diagnostic.code).toBe('yaml-invalid');
63
+ });
64
+ test('duplicate keys are yaml-invalid', () => {
65
+ const r = (0, parse_1.loadFrontmatter)('a: 1\na: 2');
66
+ expect(!r.ok && r.diagnostic.code).toBe('yaml-invalid');
67
+ });
68
+ test('merge keys are not expanded (CORE_SCHEMA)', () => {
69
+ expect((0, parse_1.loadFrontmatter)('a: 1\n<<: {b: 2}')).toEqual({ ok: true, value: { a: 1, '<<': { b: 2 } } });
70
+ });
71
+ test('the yaml-invalid reason is JSON-escaped and single-line', () => {
72
+ const r = (0, parse_1.loadFrontmatter)('a: *x"\\y');
73
+ expect(r.ok).toBe(false);
74
+ const d = !r.ok ? r.diagnostic : undefined;
75
+ expect(d?.code).toBe('yaml-invalid');
76
+ expect(d?.message.startsWith('frontmatter is not valid YAML:')).toBe(true);
77
+ expect(d?.message).toContain('alias \\"x\\"\\\\y\\"');
78
+ expect(d?.message).not.toContain('alias "x"\\y"');
79
+ expect(d?.message).not.toMatch(/[\n\r]/);
80
+ });
81
+ test('an alias without an anchor is yaml-invalid', () => {
82
+ const r = (0, parse_1.loadFrontmatter)('a: *x');
83
+ expect(!r.ok && r.diagnostic.code).toBe('yaml-invalid');
84
+ });
85
+ // Pins the js-yaml listener contract (`state.anchor` on `close`). If a js-yaml upgrade changes it, these fail loudly
86
+ // instead of silently allowing anchors.
87
+ describe('anchor detection (js-yaml listener contract)', () => {
88
+ test.each([
89
+ ['a scalar anchor', 'a: &x 1'],
90
+ ['an unused anchor', 'a: &x 1\nb: 2'],
91
+ ['a collection anchor', 'a: &x [1, 2]'],
92
+ ['a mapping anchor', 'a: &x\n b: 1'],
93
+ ['a key anchor', '&k a: 1'],
94
+ ['an anchor and its alias', 'a: &x 1\nb: *x'],
95
+ ['an anchored top-level mapping', '&top\na: 1'],
96
+ ['an empty anchored node', 'a: &x'],
97
+ ])('%s is yaml-alias', (_label, text) => {
98
+ const r = (0, parse_1.loadFrontmatter)(text);
99
+ expect(!r.ok && r.diagnostic.code).toBe('yaml-alias');
100
+ });
101
+ test.each([['double-quoted', 'a: "&x *x"'], ['single-quoted', "a: '&x *x'"], ['block scalar', 'a: |\n &x *x'], ['inside a word', 'a: x&y']])('%s look-alike text is accepted', (_label, text) => {
102
+ expect((0, parse_1.loadFrontmatter)(text).ok).toBe(true);
103
+ });
104
+ });
105
+ });
106
+ describe('exported helpers never throw on non-string input', () => {
107
+ const hostile = [['undefined', undefined], ['null', null], ['a number', 42], ['an object', {}], ['a Buffer', Buffer.from('---\na: 1\n---\nb')], ['an array', []]];
108
+ test.each(hostile)('splitRecord(%s) is input-invalid', (_label, value) => {
109
+ let r;
110
+ expect(() => { r = (0, parse_1.splitRecord)(value); }).not.toThrow();
111
+ expect(r && !r.ok && r.diagnostic.code).toBe('input-invalid');
112
+ });
113
+ test.each(hostile)('loadFrontmatter(%s) is input-invalid', (_label, value) => {
114
+ let r;
115
+ expect(() => { r = (0, parse_1.loadFrontmatter)(value); }).not.toThrow();
116
+ expect(r && !r.ok && r.diagnostic.code).toBe('input-invalid');
117
+ });
118
+ test.each([['an array', ['2026-10-17-x.md']], ['a number', 20261017], ['null', null], ['undefined', undefined], ['an object', { toString: () => '2026-10-17-x.md' }]])('isValidRecordFileName(%s) is false', (_label, value) => {
119
+ expect((0, parse_1.isValidRecordFileName)(value)).toBe(false);
120
+ });
121
+ test('loadFrontmatter bounds the message of a huge YAML error', () => {
122
+ const r = (0, parse_1.loadFrontmatter)(`a: *${'x'.repeat(60000)}`);
123
+ expect(!r.ok && r.diagnostic.code).toBe('yaml-invalid');
124
+ expect(!r.ok && r.diagnostic.message.length).toBeLessThanOrEqual(300);
125
+ });
126
+ test('loadFrontmatter keeps a surrogate pair whole when it clips', () => {
127
+ for (const pad of ['', 'x']) {
128
+ const r = (0, parse_1.loadFrontmatter)(`a: *${pad}${'😀'.repeat(300)}`);
129
+ const message = !r.ok ? r.diagnostic.message : '';
130
+ expect(message.length).toBeLessThanOrEqual(300);
131
+ expect(message).not.toMatch(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/);
132
+ }
133
+ });
134
+ });
135
+ describe('isRecordPath (the one copy, used by change validate and changes list)', () => {
136
+ test.each(['docs/changes/2026-10-17-rec.md', 'docs/changes/2026-10-17-a-b-c.md'])('accepts %s', (rel) => expect((0, parse_1.isRecordPath)(rel)).toBe(true));
137
+ test.each(['docs/changes/sub/2026-10-17-rec.md', 'docs/changes', 'docs/changes/notes.md', 'docs/changes/2026-10-17-Rec.md', 'other/2026-10-17-rec.md', '/docs/changes/2026-10-17-rec.md', '', 'docs/changes/2026-10-17-rec.md/x'])('rejects %j', (rel) => expect((0, parse_1.isRecordPath)(rel)).toBe(false));
138
+ test('never throws on non-string input', () => {
139
+ for (const v of [undefined, null, 5, {}])
140
+ expect((0, parse_1.isRecordPath)(v)).toBe(false);
141
+ });
142
+ });
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const types_1 = require("../../../src/core/changes/types");
7
+ const receipt_1 = require("../../../src/core/changes/receipt");
8
+ const validate_shape_1 = require("../../../src/core/changes/validate-shape");
9
+ const git_1 = require("../../../src/core/changes/git");
10
+ const helpers_1 = require("./helpers");
11
+ const fs_1 = __importDefault(require("fs"));
12
+ const path_1 = __importDefault(require("path"));
13
+ // One set of predicates for what a receipt, a record and git may hold (types.ts); receipt.ts, validate-shape.ts and git.ts use these.
14
+ const SHA1 = '0123456789abcdef0123456789abcdef01234567';
15
+ const SHA256 = '0123456789abcdef'.repeat(4);
16
+ describe('shared receipt and record predicates', () => {
17
+ test('a commit id is 40 or 64 lowercase hex characters, and nothing else', () => {
18
+ expect((0, types_1.isCommitId)(SHA1)).toBe(true);
19
+ expect((0, types_1.isCommitId)(SHA256)).toBe(true);
20
+ expect(types_1.COMMIT_ID.test(SHA256)).toBe(true);
21
+ for (const bad of ['', 'a'.repeat(39), 'a'.repeat(41), 'a'.repeat(63), 'a'.repeat(65), 'A'.repeat(40), 'g'.repeat(40), `${SHA1}\n`, undefined, null, 5])
22
+ expect((0, types_1.isCommitId)(bad)).toBe(false);
23
+ });
24
+ test('the verdicts, the sensor statuses and the sensor names', () => {
25
+ expect(types_1.OVERALLS).toEqual(['pass', 'fail', 'skipped', 'not_certified']);
26
+ expect(types_1.SENSOR_STATUSES).toEqual(['pass', 'fail', 'inconclusive', 'skipped']);
27
+ for (const ok of types_1.OVERALLS)
28
+ expect((0, types_1.isOverall)(ok)).toBe(true);
29
+ for (const ok of types_1.SENSOR_STATUSES)
30
+ expect((0, types_1.isSensorStatus)(ok)).toBe(true);
31
+ for (const bad of ['great', 'PASS', '', undefined, 1]) {
32
+ expect((0, types_1.isOverall)(bad)).toBe(false);
33
+ expect((0, types_1.isSensorStatus)(bad)).toBe(false);
34
+ }
35
+ expect((0, types_1.isOverall)('inconclusive')).toBe(false);
36
+ expect((0, types_1.isSensorStatus)('not_certified')).toBe(false);
37
+ expect((0, types_1.isSensorName)('lint')).toBe(true);
38
+ expect((0, types_1.isSensorName)('\u{1F600}'.repeat(types_1.MAX_SENSOR_NAME))).toBe(true);
39
+ for (const bad of ['', 'a\nb', '\u{1F600}'.repeat(types_1.MAX_SENSOR_NAME + 1), 'a
b', 5, undefined])
40
+ expect((0, types_1.isSensorName)(bad)).toBe(false);
41
+ });
42
+ test('a receipt and a record accept the same commit ids (a sha-256 repository works)', () => {
43
+ const sensors = [{ name: 'lint', status: 'pass', errors: [] }];
44
+ for (const commit of [SHA1, SHA256]) {
45
+ const receipt = (0, receipt_1.buildReceipt)({ commit, ranAt: '2026-10-17T10:00:00.000Z', cliVersion: '1', output: { overall: 'pass', sensors } });
46
+ expect(receipt.commit).toBe(commit);
47
+ }
48
+ for (const bad of ['a'.repeat(63), 'a'.repeat(65)]) {
49
+ expect(() => (0, receipt_1.buildReceipt)({ commit: bad, ranAt: '2026-10-17T10:00:00.000Z', cliVersion: '1', output: { overall: 'pass', sensors } })).toThrow(/invalid receipt: commit/);
50
+ }
51
+ });
52
+ test('a receipt for a 64-hex commit round-trips through the file', () => {
53
+ const repo = (0, helpers_1.makeRepo)();
54
+ try {
55
+ const receipt = (0, receipt_1.buildReceipt)({ commit: SHA256, ranAt: '2026-10-17T10:00:00.000Z', cliVersion: '1', output: { overall: 'pass', sensors: [{ name: 'lint', status: 'pass', errors: [] }] } });
56
+ (0, receipt_1.writeReceipt)(repo.root, receipt);
57
+ expect(fs_1.default.existsSync(path_1.default.join(repo.root, '.awm', 'sensors', 'runs', `${SHA256}.json`))).toBe(true);
58
+ const read = (0, receipt_1.readReceipt)(repo.root, SHA256);
59
+ expect(read.ok && read.receipt.commit).toBe(SHA256);
60
+ }
61
+ finally {
62
+ repo.cleanup();
63
+ }
64
+ });
65
+ test('validate-shape accepts a 64-hex verification.commit and rejects 63 and 65', () => {
66
+ const record = (commit) => ['---', 'schema: awm.change-record/v1', 'id: 2026-10-17-x', 'title: t', 'summary_short: s', 'type: feat', 'scope: null', 'semver: minor', 'breaking: false', 'modules: []',
67
+ 'verification:', ` commit: "${commit}"`, ' overall: pass', ' sensors: {}', ` receipt_digest: "sha256:${'a'.repeat(64)}"`, 'manual_validation:', ' none: n/a', 'risks: []', 'rollback: git revert', '---', '', '## Qué se hizo', 'x', '## Qué se probó', 'x', '## Qué falta validar', 'x', '## Riesgos y rollback', 'x', ''].join('\n');
68
+ const commitDiagnostics = (commit) => (0, validate_shape_1.validateRecordShape)('2026-10-17-x.md', record(commit)).diagnostics.filter((d) => d.field === 'verification.commit');
69
+ expect(commitDiagnostics(SHA1)).toEqual([]);
70
+ expect(commitDiagnostics(SHA256)).toEqual([]);
71
+ expect(commitDiagnostics('a'.repeat(63))).toHaveLength(1);
72
+ expect(commitDiagnostics('a'.repeat(65))).toHaveLength(1);
73
+ });
74
+ test('git.ts answers commit ids that satisfy the shared predicate', () => {
75
+ const repo = (0, helpers_1.makeRepo)();
76
+ try {
77
+ expect((0, types_1.isCommitId)((0, git_1.commitSha)(repo.root, 'HEAD'))).toBe(true);
78
+ }
79
+ finally {
80
+ repo.cleanup();
81
+ }
82
+ });
83
+ });
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const helpers_1 = require("./helpers");
4
+ const command_1 = require("../../../src/core/changes/command");
5
+ const range_1 = require("../../../src/core/changes/range");
6
+ let repo;
7
+ /** A repository for the groups that need one (the pure recordDirProblem tests need none). */
8
+ function withRepo() {
9
+ let template;
10
+ beforeAll(() => { template = (0, helpers_1.makeRepo)(); });
11
+ afterAll(() => template.cleanup());
12
+ beforeEach(() => { repo = (0, helpers_1.cloneRepo)(template); });
13
+ afterEach(() => repo.cleanup());
14
+ }
15
+ describe('resolveRange', () => {
16
+ withRepo();
17
+ test('returns the pinned HEAD and the merge base of the default base', () => {
18
+ const main = (0, helpers_1.branchOff)(repo);
19
+ const head = repo.commit({ 'a.ts': '1' }, 'feat: a');
20
+ expect((0, range_1.resolveRange)(repo.root, undefined)).toEqual({ ok: true, head, mergeBase: main });
21
+ });
22
+ test('an explicit base is honoured', () => {
23
+ const main = (0, helpers_1.branchOff)(repo);
24
+ repo.commit({ 'a.ts': '1' }, 'feat: a');
25
+ repo.git('branch', 'other', main);
26
+ expect((0, range_1.resolveRange)(repo.root, 'other')).toMatchObject({ ok: true, mergeBase: main });
27
+ });
28
+ test('an unsafe base is invalid-input, and the value is escaped', () => {
29
+ const r = (0, range_1.resolveRange)(repo.root, '--x\u001b[0m');
30
+ expect(r).toMatchObject({ ok: false, failure: { code: 'invalid-input' } });
31
+ expect(JSON.stringify(r)).not.toContain('\u001b');
32
+ });
33
+ test('an unknown base is base-unresolved with the remedy', () => {
34
+ expect((0, range_1.resolveRange)(repo.root, 'nope')).toEqual({ ok: false, failure: { code: 'base-unresolved', message: 'could not resolve a base branch', remedy: 'pass --base <ref>' } });
35
+ });
36
+ test('histories without a common ancestor are base-unresolved (no merge base)', () => {
37
+ repo.git('checkout', '-q', '--orphan', 'island');
38
+ repo.git('rm', '-rfq', '.');
39
+ repo.commit({ 'island.ts': '1' }, 'feat: island');
40
+ expect((0, range_1.resolveRange)(repo.root, 'main')).toMatchObject({ ok: false, failure: { code: 'base-unresolved', remedy: 'pass --base <ref>' } });
41
+ expect((0, range_1.resolveRange)(repo.root, 'main').failure.message).toContain('no merge base');
42
+ });
43
+ test('an unreadable HEAD is git-failed', () => {
44
+ expect((0, range_1.resolveRange)((0, helpers_1.nonRepoDir)(repo), undefined)).toMatchObject({ ok: false, failure: { code: 'git-failed' } });
45
+ expect((0, range_1.resolveRange)((0, helpers_1.bad)(undefined), undefined)).toMatchObject({ ok: false, failure: { code: 'git-failed' } });
46
+ });
47
+ });
48
+ describe('headMovedFailure', () => {
49
+ withRepo();
50
+ test('HEAD moving has its own code, and it is an operational failure (exit 2) like the others', () => {
51
+ expect(range_1.HEAD_MOVED.code).toBe('head-moved');
52
+ expect((0, command_1.failed)(range_1.HEAD_MOVED)).toMatchObject({ code: 2, stdout: '' });
53
+ expect((0, command_1.failed)(range_1.HEAD_MOVED).stderr).toMatch(/^head-moved: HEAD moved during derivation\n$/);
54
+ });
55
+ test('is null while HEAD is the pinned commit and HEAD_MOVED once it moved', () => {
56
+ (0, helpers_1.branchOff)(repo);
57
+ const head = repo.commit({ 'a.ts': '1' }, 'feat: a');
58
+ expect((0, range_1.headMovedFailure)(repo.root, head)).toBeNull();
59
+ repo.commit({ 'b.ts': '1' }, 'feat: b');
60
+ expect((0, range_1.headMovedFailure)(repo.root, head)).toEqual(range_1.HEAD_MOVED);
61
+ });
62
+ test('an unreadable HEAD counts as moved', () => {
63
+ expect((0, range_1.headMovedFailure)((0, helpers_1.nonRepoDir)(repo), 'a'.repeat(40))).toEqual(range_1.HEAD_MOVED);
64
+ });
65
+ });
66
+ describe('recordDirProblem', () => {
67
+ const nonOk = (kind) => ({ kind });
68
+ const dir = (kind) => (kind === 'ok' ? { kind, dir: '/x' } : { kind });
69
+ test('a usable or absent directory is no problem for reading and writing', () => {
70
+ for (const purpose of ['read', 'write']) {
71
+ expect((0, range_1.recordDirProblem)(dir('ok'), purpose)).toBeNull();
72
+ expect((0, range_1.recordDirProblem)(dir('absent'), purpose)).toBeNull();
73
+ }
74
+ });
75
+ test('a link or a non-directory is unsafe-path, worded for the purpose', () => {
76
+ expect((0, range_1.recordDirProblem)(dir('unsafe'), 'read')).toEqual({ code: 'unsafe-path', message: 'docs/changes or its parent is a symlink or not a directory; refusing to read' });
77
+ expect((0, range_1.recordDirProblem)(dir('unsafe'), 'write')).toEqual({ code: 'unsafe-path', message: 'docs/changes or its parent is a symlink or not a directory; refusing to write' });
78
+ expect((0, range_1.recordDirProblem)(nonOk('unsafe'), 'create')).toMatchObject({ code: 'unsafe-path' });
79
+ });
80
+ test('an unreadable directory is io-failed', () => {
81
+ expect((0, range_1.recordDirProblem)(dir('error'), 'read')).toEqual({ code: 'io-failed', message: 'could not inspect docs/changes' });
82
+ expect((0, range_1.recordDirProblem)(nonOk('error'), 'create')).toEqual({ code: 'io-failed', message: 'could not create docs/changes' });
83
+ });
84
+ test.each([[null], [undefined], ['ok'], [7], [[]], [{}], [{ kind: 'weird' }], [{ kind: 7 }]])('a value that is not a directory state (%p) is invalid-input, never a throw', (value) => {
85
+ for (const purpose of ['read', 'write', 'create']) {
86
+ expect((0, range_1.recordDirProblem)((0, helpers_1.bad)(value), purpose)).toEqual({ code: 'invalid-input', message: 'the record directory state is not valid' });
87
+ }
88
+ });
89
+ test('after a create attempt an absent directory is io-failed too', () => {
90
+ expect((0, range_1.recordDirProblem)(nonOk('absent'), 'create')).toEqual({ code: 'io-failed', message: 'could not create docs/changes' });
91
+ });
92
+ });