agentic-workflow-manager 9.18.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.
- package/dist/src/commands/change/index.js +492 -0
- package/dist/src/commands/changes/index.js +84 -0
- package/dist/src/commands/plan/index.js +1 -1
- package/dist/src/commands/sensors/index.js +9 -0
- package/dist/src/commands/sensors/read-only-observation.js +305 -0
- package/dist/src/commands/sensors/run.js +24 -60
- package/dist/src/core/admission/index.js +11 -1
- package/dist/src/core/changes/command.js +38 -0
- package/dist/src/core/changes/derive.js +120 -0
- package/dist/src/core/changes/fs-safe.js +47 -0
- package/dist/src/core/changes/git-env.js +68 -0
- package/dist/src/core/changes/git.js +216 -0
- package/dist/src/core/changes/list-parse.js +67 -0
- package/dist/src/core/changes/list.js +408 -0
- package/dist/src/core/changes/parse.js +72 -0
- package/dist/src/core/changes/range.js +53 -0
- package/dist/src/core/changes/receipt.js +274 -0
- package/dist/src/core/changes/record-io.js +149 -0
- package/dist/src/core/changes/render-list.js +109 -0
- package/dist/src/core/changes/render.js +178 -0
- package/dist/src/core/changes/text-safety.js +116 -0
- package/dist/src/core/changes/trailers.js +76 -0
- package/dist/src/core/changes/types.js +26 -0
- package/dist/src/core/changes/validate-shape.js +219 -0
- package/dist/src/core/changes/worktree.js +53 -0
- package/dist/src/core/currentness/check.js +5 -2
- package/dist/src/index.js +4 -0
- package/dist/src/release/orchestrator.js +3 -1
- package/dist/tests/commands/change/new.test.js +1126 -0
- package/dist/tests/commands/change/validate.test.js +1145 -0
- package/dist/tests/commands/changes/list.test.js +423 -0
- package/dist/tests/commands/sensors/index.test.js +1 -1
- package/dist/tests/commands/sensors/read-only-observation.test.js +537 -0
- package/dist/tests/commands/sensors/run-read-only.test.js +151 -0
- package/dist/tests/core/admission/index.test.js +60 -0
- package/dist/tests/core/changes/derive.test.js +455 -0
- package/dist/tests/core/changes/fs-safe.test.js +82 -0
- package/dist/tests/core/changes/git-env.test.js +143 -0
- package/dist/tests/core/changes/git.test.js +442 -0
- package/dist/tests/core/changes/helpers.js +269 -0
- package/dist/tests/core/changes/list-fixtures.js +122 -0
- package/dist/tests/core/changes/list-long-history.test.js +52 -0
- package/dist/tests/core/changes/list-paging.test.js +511 -0
- package/dist/tests/core/changes/list-parse.test.js +120 -0
- package/dist/tests/core/changes/list-tree.test.js +44 -0
- package/dist/tests/core/changes/list.test.js +821 -0
- package/dist/tests/core/changes/parse.test.js +142 -0
- package/dist/tests/core/changes/predicates.test.js +83 -0
- package/dist/tests/core/changes/range.test.js +92 -0
- package/dist/tests/core/changes/receipt.test.js +947 -0
- package/dist/tests/core/changes/record-io.test.js +307 -0
- package/dist/tests/core/changes/record.test.js +549 -0
- package/dist/tests/core/changes/render-list.test.js +212 -0
- package/dist/tests/core/changes/render.test.js +202 -0
- package/dist/tests/core/changes/text-safety.test.js +226 -0
- package/dist/tests/core/changes/trailers.test.js +181 -0
- package/dist/tests/core/changes/worktree.test.js +259 -0
- package/dist/tests/core/currentness/check-insteadof.test.js +120 -0
- package/dist/tests/core/currentness/check.test.js +75 -14
- package/dist/tests/release/orchestrator-trailer.test.js +72 -0
- package/dist/tests/release/orchestrator.test.js +1 -0
- package/dist/tests/structural/change-commands-are-registered.test.js +29 -0
- package/dist/tests/structural/change-record-git-and-platform-conventions.test.js +77 -0
- package/dist/tests/structural/cloud-admission-docs-contract.test.js +54 -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
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const fs_1 = __importDefault(require("fs"));
|
|
40
|
+
const os_1 = __importDefault(require("os"));
|
|
41
|
+
const path_1 = __importDefault(require("path"));
|
|
42
|
+
const child_process_1 = __importStar(require("child_process"));
|
|
43
|
+
const helpers_1 = require("./helpers");
|
|
44
|
+
const worktree_1 = require("../../../src/core/changes/worktree");
|
|
45
|
+
const ENV_KEYS = ['HOME', 'AWM_HOME', 'GIT_CONFIG_GLOBAL', 'GIT_CONFIG_NOSYSTEM'];
|
|
46
|
+
const ORIGINAL = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]));
|
|
47
|
+
let saved = {};
|
|
48
|
+
let root;
|
|
49
|
+
let home;
|
|
50
|
+
// Jest's sandboxed `process.env` is a copy that children do not see, so the child's environment is passed explicitly.
|
|
51
|
+
const gitEnv = () => ({ ...process.env, HOME: home, GIT_CONFIG_GLOBAL: path_1.default.join(home, 'gitconfig'), GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: 't@t', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: 't@t', GIT_CONFIG_NOSYSTEM: '1' });
|
|
52
|
+
const g = (...args) => (0, child_process_1.execFileSync)('git', ['-c', 'commit.gpgsign=false', ...args], { cwd: root, encoding: 'utf8', env: gitEnv() }).trim();
|
|
53
|
+
const real = child_process_1.default.execFileSync;
|
|
54
|
+
let spy;
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
saved = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]));
|
|
57
|
+
home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-home-'));
|
|
58
|
+
Object.assign(process.env, { HOME: home, AWM_HOME: path_1.default.join(home, '.awm'), GIT_CONFIG_GLOBAL: (0, helpers_1.emptyGitConfig)(home), GIT_CONFIG_NOSYSTEM: '1' });
|
|
59
|
+
root = fs_1.default.realpathSync.native(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-wt-')));
|
|
60
|
+
g('init', '-q', '-b', 'main');
|
|
61
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'a.txt'), 'a');
|
|
62
|
+
g('add', '.');
|
|
63
|
+
g('commit', '-q', '-m', 'feat: a');
|
|
64
|
+
});
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
spy?.mockRestore();
|
|
67
|
+
spy = undefined;
|
|
68
|
+
for (const [k, v] of Object.entries(saved)) {
|
|
69
|
+
if (v === undefined)
|
|
70
|
+
delete process.env[k];
|
|
71
|
+
else
|
|
72
|
+
process.env[k] = v;
|
|
73
|
+
}
|
|
74
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
75
|
+
fs_1.default.rmSync(home, { recursive: true, force: true });
|
|
76
|
+
});
|
|
77
|
+
afterAll(() => {
|
|
78
|
+
// The environment the suite started with is back, so nothing leaks into the next test file.
|
|
79
|
+
for (const k of ENV_KEYS)
|
|
80
|
+
expect(process.env[k]).toBe(ORIGINAL[k]);
|
|
81
|
+
});
|
|
82
|
+
/** Intercepts git invocations whose first argument is `first`; every other call reaches the real git. */
|
|
83
|
+
const intercept = (first, handler) => {
|
|
84
|
+
spy = jest.spyOn(child_process_1.default, 'execFileSync').mockImplementation(((cmd, args, opts) => {
|
|
85
|
+
if (cmd === 'git' && args[0] === first)
|
|
86
|
+
return handler(opts, args);
|
|
87
|
+
return real(cmd, args, opts);
|
|
88
|
+
}));
|
|
89
|
+
};
|
|
90
|
+
const fail = (code) => () => { throw Object.assign(new Error(`spawnSync git ${code}`), { code }); };
|
|
91
|
+
describe('worktree state', () => {
|
|
92
|
+
test('R2.x isolation: HOME and AWM_HOME point at a throwaway directory, never the real ones', () => {
|
|
93
|
+
expect(process.env.HOME).toBe(home);
|
|
94
|
+
expect(process.env.HOME).not.toBe(ORIGINAL.HOME);
|
|
95
|
+
expect(process.env.AWM_HOME).toBe(path_1.default.join(home, '.awm'));
|
|
96
|
+
expect(process.env.AWM_HOME).not.toBe(ORIGINAL.AWM_HOME);
|
|
97
|
+
expect(process.env.GIT_CONFIG_GLOBAL).toBe(path_1.default.join(home, 'gitconfig'));
|
|
98
|
+
expect(fs_1.default.readFileSync(path_1.default.join(home, 'gitconfig'), 'utf8')).toBe('');
|
|
99
|
+
expect(process.env.GIT_CONFIG_NOSYSTEM).toBe('1');
|
|
100
|
+
});
|
|
101
|
+
test('the runtime-state exclusion list is exactly the local state paths', () => {
|
|
102
|
+
expect([...worktree_1.RUNTIME_STATE_EXCLUDES]).toEqual(['.awm/sensors/runs', '.awm/journal', '.awm/ledger', '.awm/evidence', '.awm/track.json']);
|
|
103
|
+
});
|
|
104
|
+
test('a clean repository reports its root, HEAD and a clean tree', () => {
|
|
105
|
+
const state = (0, worktree_1.gitState)(root, true);
|
|
106
|
+
expect(state.kind).toBe('ok');
|
|
107
|
+
if (state.kind === 'ok') {
|
|
108
|
+
expect(fs_1.default.realpathSync.native(state.root)).toBe(fs_1.default.realpathSync.native(root));
|
|
109
|
+
expect(state.commit).toBe(g('rev-parse', 'HEAD'));
|
|
110
|
+
expect(state.dirty).toBe(false);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
test('the root is normalized with native separators', () => {
|
|
114
|
+
intercept('rev-parse', (opts, args) => (args[1] === '--show-toplevel' ? `${root}${path_1.default.sep}.${path_1.default.sep}\n` : real('git', args, opts)));
|
|
115
|
+
const state = (0, worktree_1.gitState)(root, true);
|
|
116
|
+
expect(state.kind === 'ok' && state.root).toBe(path_1.default.resolve(root));
|
|
117
|
+
});
|
|
118
|
+
test('an untracked file is dirty for the pre-run measure and clean for the post-run recheck', () => {
|
|
119
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'new.ts'), 'x');
|
|
120
|
+
const pre = (0, worktree_1.gitState)(root, true);
|
|
121
|
+
const post = (0, worktree_1.gitState)(root, false);
|
|
122
|
+
expect(pre.kind === 'ok' && pre.dirty).toBe(true);
|
|
123
|
+
expect(post.kind === 'ok' && post.dirty).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
test('the pre-run measure sees untracked files even when status.showUntrackedFiles is no', () => {
|
|
126
|
+
g('config', 'status.showUntrackedFiles', 'no');
|
|
127
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'new.ts'), 'x');
|
|
128
|
+
const pre = (0, worktree_1.gitState)(root, true);
|
|
129
|
+
expect(pre.kind === 'ok' && pre.dirty).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
test('a tracked change is dirty for both measures', () => {
|
|
132
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'a.txt'), 'changed');
|
|
133
|
+
const pre = (0, worktree_1.gitState)(root, true);
|
|
134
|
+
const post = (0, worktree_1.gitState)(root, false);
|
|
135
|
+
expect(pre.kind === 'ok' && pre.dirty).toBe(true);
|
|
136
|
+
expect(post.kind === 'ok' && post.dirty).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
test.each(worktree_1.RUNTIME_STATE_EXCLUDES.map((p) => [p]))('an untracked file at %s is not a change', (rel) => {
|
|
139
|
+
fs_1.default.mkdirSync(path_1.default.dirname(path_1.default.join(root, rel)), { recursive: true });
|
|
140
|
+
fs_1.default.writeFileSync(path_1.default.join(root, rel), 'x');
|
|
141
|
+
const pre = (0, worktree_1.gitState)(root, true);
|
|
142
|
+
expect(pre.kind === 'ok' && pre.dirty).toBe(false);
|
|
143
|
+
});
|
|
144
|
+
test('extra exclusions hide changes under them, on top of the runtime-state ones', () => {
|
|
145
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'docs/changes'), { recursive: true });
|
|
146
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'docs/changes/r.md'), 'r');
|
|
147
|
+
const without = (0, worktree_1.gitState)(root, true);
|
|
148
|
+
const withExtra = (0, worktree_1.gitState)(root, true, ['docs/changes']);
|
|
149
|
+
expect(without.kind === 'ok' && without.dirty).toBe(true);
|
|
150
|
+
expect(withExtra.kind === 'ok' && withExtra.dirty).toBe(false);
|
|
151
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'other.ts'), 'x');
|
|
152
|
+
const stillDirty = (0, worktree_1.gitState)(root, true, ['docs/changes']);
|
|
153
|
+
expect(stillDirty.kind === 'ok' && stillDirty.dirty).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
test('extra exclusions keep the runtime-state exclusions', () => {
|
|
156
|
+
fs_1.default.mkdirSync(path_1.default.join(root, '.awm/journal'), { recursive: true });
|
|
157
|
+
fs_1.default.writeFileSync(path_1.default.join(root, '.awm/journal/j.json'), 'x');
|
|
158
|
+
const state = (0, worktree_1.gitState)(root, true, ['docs/changes']);
|
|
159
|
+
expect(state.kind === 'ok' && state.dirty).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
test.each([['docs/changes'], [[7]], [['../out']], [['a\nb']], [['/abs']], [[' pad']], [Array.from({ length: 65 }, (_, i) => `d${i}`)]])('unsafe extra exclusions %j are a failure, never a throw', (extra) => {
|
|
162
|
+
expect((0, worktree_1.gitState)(root, true, extra)).toEqual({ kind: 'failed' });
|
|
163
|
+
});
|
|
164
|
+
test('GIT_*_PATHSPECS variables in the environment do not change the exclusions', () => {
|
|
165
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'docs/changes'), { recursive: true });
|
|
166
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'docs/changes/r.md'), 'r');
|
|
167
|
+
fs_1.default.mkdirSync(path_1.default.join(root, '.awm/journal'), { recursive: true });
|
|
168
|
+
fs_1.default.writeFileSync(path_1.default.join(root, '.awm/journal/j.json'), 'x');
|
|
169
|
+
const saved = process.env.GIT_LITERAL_PATHSPECS;
|
|
170
|
+
process.env.GIT_LITERAL_PATHSPECS = '1';
|
|
171
|
+
try {
|
|
172
|
+
const state = (0, worktree_1.gitState)(root, true, ['docs/changes']);
|
|
173
|
+
expect(state.kind === 'ok' && state.dirty).toBe(false);
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
if (saved === undefined)
|
|
177
|
+
delete process.env.GIT_LITERAL_PATHSPECS;
|
|
178
|
+
else
|
|
179
|
+
process.env.GIT_LITERAL_PATHSPECS = saved;
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
describe('a submodule change is a change whatever the configuration says', () => {
|
|
183
|
+
// Offline: vendor/sm is a nested repository registered as a gitlink, hidden by .gitmodules `ignore = all` and diff.ignoreSubmodules=all.
|
|
184
|
+
let sm = '';
|
|
185
|
+
let commitInSm = () => '';
|
|
186
|
+
const registerHiddenSubmodule = () => {
|
|
187
|
+
const hidden = (0, helpers_1.makeHiddenSubmodule)(root, (cwd, ...args) => (0, child_process_1.execFileSync)('git', ['-c', 'commit.gpgsign=false', ...args], { cwd, encoding: 'utf8', env: gitEnv() }).trim());
|
|
188
|
+
sm = hidden.dir;
|
|
189
|
+
commitInSm = hidden.commitInSm;
|
|
190
|
+
};
|
|
191
|
+
test('a clean tree holding a submodule is clean', () => {
|
|
192
|
+
registerHiddenSubmodule();
|
|
193
|
+
const state = (0, worktree_1.gitState)(root, true);
|
|
194
|
+
expect(state.kind === 'ok' && state.dirty).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
test('an untracked file inside a clean submodule: the post-run recheck ignores it, the pre-run measure counts it', () => {
|
|
197
|
+
registerHiddenSubmodule();
|
|
198
|
+
fs_1.default.writeFileSync(path_1.default.join(sm, 'coverage.out'), 'x'); // what a sensor leaves behind
|
|
199
|
+
const post = (0, worktree_1.gitState)(root, false);
|
|
200
|
+
const pre = (0, worktree_1.gitState)(root, true);
|
|
201
|
+
expect(post.kind === 'ok' && post.dirty).toBe(false);
|
|
202
|
+
// git reports the submodule itself as modified (untracked content) in the normal-untracked mode, so the pre-run measure is dirty.
|
|
203
|
+
expect(pre.kind === 'ok' && pre.dirty).toBe(true);
|
|
204
|
+
});
|
|
205
|
+
test.each([[true], [false]])('modified tracked content inside a submodule makes the tree dirty (untracked=%p)', (untracked) => {
|
|
206
|
+
registerHiddenSubmodule();
|
|
207
|
+
fs_1.default.writeFileSync(path_1.default.join(sm, 'one'), 'changed');
|
|
208
|
+
const state = (0, worktree_1.gitState)(root, untracked);
|
|
209
|
+
expect(state.kind === 'ok' && state.dirty).toBe(true);
|
|
210
|
+
});
|
|
211
|
+
test.each([[true], [false]])('a submodule checked out at another commit than the gitlink makes the tree dirty (untracked=%p)', (untracked) => {
|
|
212
|
+
registerHiddenSubmodule();
|
|
213
|
+
commitInSm('two');
|
|
214
|
+
const state = (0, worktree_1.gitState)(root, untracked);
|
|
215
|
+
expect(state.kind === 'ok' && state.dirty).toBe(true);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
test('a non-string or empty cwd is a failure, never a throw', () => {
|
|
219
|
+
expect((0, worktree_1.gitState)(undefined, true)).toEqual({ kind: 'failed' });
|
|
220
|
+
expect((0, worktree_1.gitState)('', true)).toEqual({ kind: 'failed' });
|
|
221
|
+
});
|
|
222
|
+
test('a directory outside git is not-repo', () => {
|
|
223
|
+
const plain = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-plain-'));
|
|
224
|
+
try {
|
|
225
|
+
expect((0, worktree_1.gitState)(plain, true)).toEqual({ kind: 'not-repo' });
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
fs_1.default.rmSync(plain, { recursive: true, force: true });
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
test('a repository without a commit is not-repo', () => {
|
|
232
|
+
const empty = fs_1.default.realpathSync.native(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-empty-')));
|
|
233
|
+
try {
|
|
234
|
+
(0, child_process_1.execFileSync)('git', ['init', '-q', '-b', 'main'], { cwd: empty, env: gitEnv() });
|
|
235
|
+
expect((0, worktree_1.gitState)(empty, true)).toEqual({ kind: 'not-repo' });
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
fs_1.default.rmSync(empty, { recursive: true, force: true });
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
test.each([['ENOBUFS'], ['ETIMEDOUT']])('a %s from git status is a failure, not a missing repository', (code) => {
|
|
242
|
+
intercept('status', fail(code));
|
|
243
|
+
expect((0, worktree_1.gitState)(root, true)).toEqual({ kind: 'failed' });
|
|
244
|
+
});
|
|
245
|
+
test('git missing from PATH is a failure', () => {
|
|
246
|
+
intercept('rev-parse', fail('ENOENT'));
|
|
247
|
+
expect((0, worktree_1.gitState)(root, true)).toEqual({ kind: 'failed' });
|
|
248
|
+
});
|
|
249
|
+
test('git status gets a 64 MiB buffer', () => {
|
|
250
|
+
let seen;
|
|
251
|
+
intercept('status', (opts) => { seen = opts.maxBuffer; throw Object.assign(new Error('stop'), { code: 'ENOBUFS' }); });
|
|
252
|
+
(0, worktree_1.gitState)(root, true);
|
|
253
|
+
expect(seen).toBeGreaterThanOrEqual(64 * 1024 * 1024);
|
|
254
|
+
});
|
|
255
|
+
test('the reasons are the stable user-facing strings', () => {
|
|
256
|
+
expect(worktree_1.REASON_NOT_REPO).toBe('not inside a git repository with a commit');
|
|
257
|
+
expect(worktree_1.REASON_GIT_FAILED).toBe('git command failed or timed out');
|
|
258
|
+
});
|
|
259
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
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 fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const util_1 = require("util");
|
|
11
|
+
const url_1 = require("url");
|
|
12
|
+
/**
|
|
13
|
+
* A-R1 / A-R2 / V-R3(a), real git. AWM's own cloud recipe (D-7) installs a global
|
|
14
|
+
* `url.<base>.insteadOf` rewrite; `git remote get-url` then reports the rewritten
|
|
15
|
+
* transport URL while `.git/config` keeps the configured one. The rewrite here points
|
|
16
|
+
* at a local bare repository so the test stays offline.
|
|
17
|
+
*/
|
|
18
|
+
describe('checkCurrentness under a global insteadOf rewrite (real git)', () => {
|
|
19
|
+
const CONFIGURED = 'https://example.test/team/repo.git';
|
|
20
|
+
const ENV_KEYS = ['HOME', 'AWM_HOME', 'GIT_CONFIG_GLOBAL', 'GIT_CONFIG_NOSYSTEM', 'GIT_CEILING_DIRECTORIES'];
|
|
21
|
+
const saved = {};
|
|
22
|
+
let root;
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-insteadof-'));
|
|
25
|
+
for (const key of ENV_KEYS)
|
|
26
|
+
saved[key] = process.env[key];
|
|
27
|
+
const home = path_1.default.join(root, 'home');
|
|
28
|
+
fs_1.default.mkdirSync(home, { recursive: true });
|
|
29
|
+
process.env.HOME = home;
|
|
30
|
+
process.env.AWM_HOME = path_1.default.join(home, '.awm');
|
|
31
|
+
const globalConfig = path_1.default.join(home, 'gitconfig');
|
|
32
|
+
fs_1.default.writeFileSync(globalConfig, [
|
|
33
|
+
'[user]',
|
|
34
|
+
'\temail = test@example.test',
|
|
35
|
+
'\tname = test',
|
|
36
|
+
`[url "${(0, url_1.pathToFileURL)(path_1.default.join(root, 'remote')).href}/"]`,
|
|
37
|
+
'\tinsteadOf = https://example.test/',
|
|
38
|
+
'',
|
|
39
|
+
].join('\n'));
|
|
40
|
+
process.env.GIT_CONFIG_GLOBAL = globalConfig;
|
|
41
|
+
process.env.GIT_CONFIG_NOSYSTEM = '1';
|
|
42
|
+
process.env.GIT_CEILING_DIRECTORIES = root;
|
|
43
|
+
jest.resetModules();
|
|
44
|
+
});
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
for (const key of ENV_KEYS) {
|
|
47
|
+
if (saved[key] === undefined)
|
|
48
|
+
delete process.env[key];
|
|
49
|
+
else
|
|
50
|
+
process.env[key] = saved[key];
|
|
51
|
+
}
|
|
52
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
53
|
+
});
|
|
54
|
+
// Jest's sandboxed `process.env` is a copy: assignments never reach a spawned child
|
|
55
|
+
// by themselves, so every git invocation (setup and the CLI transport) gets it explicitly.
|
|
56
|
+
const git = (cwd, args) => (0, child_process_1.execFileSync)('git', args, { cwd, env: { ...process.env }, stdio: 'pipe', encoding: 'utf8' });
|
|
57
|
+
const realGit = async (cwd, args, options) => (await (0, util_1.promisify)(child_process_1.execFile)('git', args, { cwd, env: { ...process.env }, timeout: options.timeoutMs, maxBuffer: options.maxOutputBytes })).stdout;
|
|
58
|
+
function cloneTaggedRegistry() {
|
|
59
|
+
const seed = path_1.default.join(root, 'seed');
|
|
60
|
+
fs_1.default.mkdirSync(seed);
|
|
61
|
+
git(seed, ['init', '-q', '-b', 'main']);
|
|
62
|
+
fs_1.default.writeFileSync(path_1.default.join(seed, 'README.md'), 'registry\n');
|
|
63
|
+
git(seed, ['add', '.']);
|
|
64
|
+
git(seed, ['commit', '-qm', 'initial']);
|
|
65
|
+
git(seed, ['tag', 'v1.0.0']);
|
|
66
|
+
const bare = path_1.default.join(root, 'remote', 'team', 'repo.git');
|
|
67
|
+
fs_1.default.mkdirSync(path_1.default.dirname(bare), { recursive: true });
|
|
68
|
+
git(root, ['clone', '-q', '--bare', seed, bare]);
|
|
69
|
+
const checkout = path_1.default.join(root, 'checkout');
|
|
70
|
+
git(root, ['clone', '-q', CONFIGURED, checkout]);
|
|
71
|
+
git(checkout, ['checkout', '-q', 'v1.0.0']);
|
|
72
|
+
return checkout;
|
|
73
|
+
}
|
|
74
|
+
async function check(checkout) {
|
|
75
|
+
const { checkCurrentness } = require('../../../src/core/currentness/check');
|
|
76
|
+
return checkCurrentness(root, {
|
|
77
|
+
cliVersion: () => '1.2.3',
|
|
78
|
+
now: () => 1_700_000_000_000,
|
|
79
|
+
fetch: jest.fn().mockResolvedValue({ ok: true, text: async () => JSON.stringify({ version: '1.2.3' }) }),
|
|
80
|
+
readPreferences: () => ({ pins: {} }),
|
|
81
|
+
listRegistries: () => [{ name: 'baseline', remote: CONFIGURED, contentRoot: checkout }],
|
|
82
|
+
git: realGit,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
it('reports the registry current when only the transport URL is rewritten', async () => {
|
|
86
|
+
const checkout = cloneTaggedRegistry();
|
|
87
|
+
expect(git(checkout, ['remote', 'get-url', 'origin']).trim()).not.toBe(CONFIGURED);
|
|
88
|
+
expect(git(checkout, ['config', '--get', 'remote.origin.url']).trim()).toBe(CONFIGURED);
|
|
89
|
+
const report = await check(checkout);
|
|
90
|
+
expect(report.components[1]).toEqual(expect.objectContaining({
|
|
91
|
+
component: 'registry:baseline', status: 'current', installed: 'v1.0.0', latest: 'v1.0.0',
|
|
92
|
+
}));
|
|
93
|
+
});
|
|
94
|
+
it('still reports an origin stored with a different path as unverifiable', async () => {
|
|
95
|
+
const checkout = cloneTaggedRegistry();
|
|
96
|
+
git(checkout, ['remote', 'set-url', 'origin', 'https://example.test/team/other.git']);
|
|
97
|
+
const report = await check(checkout);
|
|
98
|
+
expect(report.components[1]).toEqual(expect.objectContaining({
|
|
99
|
+
status: 'unverifiable', detail: expect.stringMatching(/origin does not match configured remote/i),
|
|
100
|
+
}));
|
|
101
|
+
});
|
|
102
|
+
it('does not let a global remote.origin.url stand in for a checkout without its own origin', async () => {
|
|
103
|
+
const checkout = cloneTaggedRegistry();
|
|
104
|
+
git(checkout, ['remote', 'remove', 'origin']);
|
|
105
|
+
fs_1.default.appendFileSync(process.env.GIT_CONFIG_GLOBAL, `[remote "origin"]\n\turl = ${CONFIGURED}\n`);
|
|
106
|
+
expect(git(checkout, ['config', '--get', 'remote.origin.url']).trim()).toBe(CONFIGURED);
|
|
107
|
+
const report = await check(checkout);
|
|
108
|
+
expect(report.components[1]).toEqual(expect.objectContaining({
|
|
109
|
+
status: 'unverifiable', detail: 'Could not read local registry origin.',
|
|
110
|
+
}));
|
|
111
|
+
});
|
|
112
|
+
it('reports an empty stored origin as an unreadable origin', async () => {
|
|
113
|
+
const checkout = cloneTaggedRegistry();
|
|
114
|
+
git(checkout, ['config', 'remote.origin.url', '']);
|
|
115
|
+
const report = await check(checkout);
|
|
116
|
+
expect(report.components[1]).toEqual(expect.objectContaining({
|
|
117
|
+
status: 'unverifiable', detail: 'Could not read local registry origin.',
|
|
118
|
+
}));
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -29,7 +29,7 @@ describe('checkCurrentness', () => {
|
|
|
29
29
|
git: jest.fn().mockImplementation(async (_cwd, args) => {
|
|
30
30
|
if (args[0] === 'ls-remote')
|
|
31
31
|
return `${'a'.repeat(40)}\trefs/tags/v1.2.3\n`;
|
|
32
|
-
if (args[0] === '
|
|
32
|
+
if (args[0] === 'config')
|
|
33
33
|
return 'https://example.test/team/repo.git\n';
|
|
34
34
|
if (args[0] === 'rev-parse')
|
|
35
35
|
return `${'a'.repeat(40)}\n`;
|
|
@@ -67,7 +67,7 @@ describe('checkCurrentness', () => {
|
|
|
67
67
|
git: jest.fn().mockImplementation(async (_cwd, args) => {
|
|
68
68
|
if (args[0] === 'ls-remote')
|
|
69
69
|
return `${'b'.repeat(40)}\trefs/tags/v1.1.0\n${'a'.repeat(40)}\trefs/tags/v1.0.0\n`;
|
|
70
|
-
if (args[0] === '
|
|
70
|
+
if (args[0] === 'config')
|
|
71
71
|
return 'https://example.test/team/repo.git\n';
|
|
72
72
|
if (args[0] === 'rev-parse')
|
|
73
73
|
return `${'a'.repeat(40)}\n`;
|
|
@@ -118,7 +118,7 @@ describe('checkCurrentness', () => {
|
|
|
118
118
|
git: jest.fn().mockImplementation(async (_cwd, args) => {
|
|
119
119
|
if (args[0] === 'ls-remote')
|
|
120
120
|
return `${'b'.repeat(40)}\trefs/tags/${latest}\n${'a'.repeat(40)}\trefs/tags/${installed}\n`;
|
|
121
|
-
if (args[0] === '
|
|
121
|
+
if (args[0] === 'config')
|
|
122
122
|
return 'https://example.test/team/repo.git\n';
|
|
123
123
|
if (args[0] === 'rev-parse')
|
|
124
124
|
return `${'a'.repeat(40)}\n`;
|
|
@@ -135,7 +135,7 @@ describe('checkCurrentness', () => {
|
|
|
135
135
|
git: jest.fn().mockImplementation(async (_cwd, args) => {
|
|
136
136
|
if (args[0] === 'ls-remote')
|
|
137
137
|
return `${'a'.repeat(40)}\trefs/tags/1.2.4\n${'b'.repeat(40)}\trefs/tags/1.2.5-rc.1\n`;
|
|
138
|
-
if (args[0] === '
|
|
138
|
+
if (args[0] === 'config')
|
|
139
139
|
return 'https://example.test/team/repo.git\n';
|
|
140
140
|
if (args[0] === 'rev-parse')
|
|
141
141
|
return `${'a'.repeat(40)}\n`;
|
|
@@ -155,7 +155,7 @@ describe('checkCurrentness', () => {
|
|
|
155
155
|
git: jest.fn().mockImplementation(async (_cwd, args) => {
|
|
156
156
|
if (args[0] === 'ls-remote')
|
|
157
157
|
return `${sharedSha}\trefs/tags/v1.2.4\n`;
|
|
158
|
-
if (args[0] === '
|
|
158
|
+
if (args[0] === 'config')
|
|
159
159
|
return 'https://example.test/team/repo.git\n';
|
|
160
160
|
if (args[0] === 'rev-parse')
|
|
161
161
|
return `${sharedSha}\n`;
|
|
@@ -169,7 +169,7 @@ describe('checkCurrentness', () => {
|
|
|
169
169
|
}));
|
|
170
170
|
});
|
|
171
171
|
it.each([
|
|
172
|
-
['branch provenance', async (_cwd, args) => args[0] === 'describe' ? Promise.reject(new Error('not a tag')) : args[0] === 'ls-remote' ? `${'a'.repeat(40)}\trefs/tags/v1.2.3\n` : args[0] === '
|
|
172
|
+
['branch provenance', async (_cwd, args) => args[0] === 'describe' ? Promise.reject(new Error('not a tag')) : args[0] === 'ls-remote' ? `${'a'.repeat(40)}\trefs/tags/v1.2.3\n` : args[0] === 'config' ? 'https://example.test/team/repo.git\n' : `${'a'.repeat(40)}\n`],
|
|
173
173
|
['malformed npm semver', undefined],
|
|
174
174
|
])('fails closed for %s', async (name, git) => {
|
|
175
175
|
const { checkCurrentness } = require('../../../src/core/currentness/check');
|
|
@@ -186,7 +186,7 @@ describe('checkCurrentness', () => {
|
|
|
186
186
|
throw new Error('fatal: no tag exactly matches HEAD');
|
|
187
187
|
if (args[0] === 'ls-remote')
|
|
188
188
|
return `${'a'.repeat(40)}\trefs/tags/v1.2.3\n`;
|
|
189
|
-
if (args[0] === '
|
|
189
|
+
if (args[0] === 'config')
|
|
190
190
|
return 'https://example.test/team/repo.git\n';
|
|
191
191
|
return `${'a'.repeat(40)}\n`;
|
|
192
192
|
}),
|
|
@@ -202,7 +202,7 @@ describe('checkCurrentness', () => {
|
|
|
202
202
|
const { checkCurrentness } = require('../../../src/core/currentness/check');
|
|
203
203
|
const result = await checkCurrentness(root, deps({
|
|
204
204
|
git: jest.fn().mockImplementation(async (_cwd, args) => {
|
|
205
|
-
if (args[0] === '
|
|
205
|
+
if (args[0] === 'config')
|
|
206
206
|
return 'https://user:secret@example.test/team/other.git\n';
|
|
207
207
|
if (args[0] === 'ls-remote')
|
|
208
208
|
return `${'a'.repeat(40)}\trefs/tags/v1.2.3\n`;
|
|
@@ -225,7 +225,7 @@ describe('checkCurrentness', () => {
|
|
|
225
225
|
const result = await checkCurrentness(root, deps({
|
|
226
226
|
listRegistries: () => [{ name: 'baseline', remote: configured, contentRoot: '/registry' }],
|
|
227
227
|
git: jest.fn().mockImplementation(async (_cwd, args) => {
|
|
228
|
-
if (args[0] === '
|
|
228
|
+
if (args[0] === 'config')
|
|
229
229
|
return 'ssh://git@github.example/team/repo.git\n';
|
|
230
230
|
if (args[0] === 'ls-remote')
|
|
231
231
|
return `${'a'.repeat(40)}\trefs/tags/v1.2.3\n`;
|
|
@@ -246,7 +246,7 @@ describe('checkCurrentness', () => {
|
|
|
246
246
|
git: jest.fn().mockImplementation(async (_cwd, args) => {
|
|
247
247
|
if (args[0] === 'ls-remote')
|
|
248
248
|
throw new Error('remote unavailable');
|
|
249
|
-
if (args[0] === '
|
|
249
|
+
if (args[0] === 'config')
|
|
250
250
|
return 'https://example.test/team/repo.git\n';
|
|
251
251
|
if (args[0] === 'describe')
|
|
252
252
|
return 'v1.2.3\n';
|
|
@@ -265,7 +265,7 @@ describe('checkCurrentness', () => {
|
|
|
265
265
|
git: jest.fn().mockImplementation(async (_cwd, args) => {
|
|
266
266
|
if (args[0] === 'ls-remote')
|
|
267
267
|
return `${'a'.repeat(40)}\trefs/tags/v1.2.4-rc.1\n`;
|
|
268
|
-
if (args[0] === '
|
|
268
|
+
if (args[0] === 'config')
|
|
269
269
|
return 'https://example.test/team/repo.git\n';
|
|
270
270
|
if (args[0] === 'describe')
|
|
271
271
|
return 'v1.2.3\n';
|
|
@@ -321,7 +321,7 @@ describe('checkCurrentness', () => {
|
|
|
321
321
|
const git = jest.fn().mockImplementation(async (_cwd, args) => {
|
|
322
322
|
if (args[0] === 'ls-remote')
|
|
323
323
|
return new Promise((_resolve, reject) => { rejectGit = reject; });
|
|
324
|
-
if (args[0] === '
|
|
324
|
+
if (args[0] === 'config')
|
|
325
325
|
return 'https://example.test/team/repo.git\n';
|
|
326
326
|
if (args[0] === 'rev-parse')
|
|
327
327
|
return `${'a'.repeat(40)}\n`;
|
|
@@ -380,7 +380,7 @@ describe('checkCurrentness', () => {
|
|
|
380
380
|
git: jest.fn().mockImplementation(async (_cwd, args) => {
|
|
381
381
|
if (args[0] === 'ls-remote')
|
|
382
382
|
return `${'a'.repeat(40)}\trefs/tags/v1.2.3\n`;
|
|
383
|
-
if (args[0] === '
|
|
383
|
+
if (args[0] === 'config')
|
|
384
384
|
return `${remote}\n`;
|
|
385
385
|
if (args[0] === 'rev-parse')
|
|
386
386
|
return `${'a'.repeat(40)}\n`;
|
|
@@ -402,7 +402,7 @@ describe('checkCurrentness', () => {
|
|
|
402
402
|
git: jest.fn().mockImplementation(async (_cwd, args) => {
|
|
403
403
|
if (args[0] === 'ls-remote')
|
|
404
404
|
return `${tagObject}\trefs/tags/v1.2.3\n${peeledCommit}\trefs/tags/v1.2.3^{}\n`;
|
|
405
|
-
if (args[0] === '
|
|
405
|
+
if (args[0] === 'config')
|
|
406
406
|
return 'https://example.test/team/repo.git\n';
|
|
407
407
|
if (args[0] === 'rev-parse')
|
|
408
408
|
return `${peeledCommit}\n`;
|
|
@@ -413,4 +413,65 @@ describe('checkCurrentness', () => {
|
|
|
413
413
|
}));
|
|
414
414
|
expect(result.components[1]).toEqual(expect.objectContaining({ status: 'current', latest: 'v1.2.3' }));
|
|
415
415
|
});
|
|
416
|
+
it('reads the literal origin config value, never the insteadOf-rewritten URL', async () => {
|
|
417
|
+
const configured = 'https://github.example/team/repo.git';
|
|
418
|
+
const git = jest.fn().mockImplementation(async (_cwd, args) => {
|
|
419
|
+
if (args[0] === 'config')
|
|
420
|
+
return `${configured}\n`;
|
|
421
|
+
if (args[0] === 'remote')
|
|
422
|
+
return 'https://x-access-token:ghs_SECRET@github.example/team/repo.git\n';
|
|
423
|
+
if (args[0] === 'ls-remote')
|
|
424
|
+
return `${'a'.repeat(40)}\trefs/tags/v1.2.3\n`;
|
|
425
|
+
if (args[0] === 'describe')
|
|
426
|
+
return 'v1.2.3\n';
|
|
427
|
+
return `${'a'.repeat(40)}\n`;
|
|
428
|
+
});
|
|
429
|
+
const { checkCurrentness } = require('../../../src/core/currentness/check');
|
|
430
|
+
const result = await checkCurrentness(root, deps({
|
|
431
|
+
listRegistries: () => [{ name: 'baseline', remote: configured, contentRoot: '/registry' }],
|
|
432
|
+
git,
|
|
433
|
+
}));
|
|
434
|
+
expect(result.components[1]).toEqual(expect.objectContaining({ status: 'current', latest: 'v1.2.3' }));
|
|
435
|
+
expect(git).toHaveBeenCalledWith('/registry', ['config', '--local', '--get', 'remote.origin.url'], expect.any(Object));
|
|
436
|
+
expect(git).not.toHaveBeenCalledWith('/registry', ['remote', 'get-url', 'origin'], expect.any(Object));
|
|
437
|
+
expect(git).toHaveBeenCalledWith('/registry', ['ls-remote', '--tags', configured], expect.any(Object));
|
|
438
|
+
expect(JSON.stringify(result)).not.toContain('x-access-token');
|
|
439
|
+
expect(JSON.stringify(result)).not.toContain('SECRET');
|
|
440
|
+
});
|
|
441
|
+
it.each([['an empty', ''], ['a whitespace-only', ' \n']])('reports %s origin config value as an unreadable origin', async (_label, output) => {
|
|
442
|
+
const { checkCurrentness } = require('../../../src/core/currentness/check');
|
|
443
|
+
const result = await checkCurrentness(root, deps({
|
|
444
|
+
git: jest.fn().mockImplementation(async (_cwd, args) => {
|
|
445
|
+
if (args[0] === 'config')
|
|
446
|
+
return output;
|
|
447
|
+
if (args[0] === 'ls-remote')
|
|
448
|
+
return `${'a'.repeat(40)}\trefs/tags/v1.2.3\n`;
|
|
449
|
+
if (args[0] === 'describe')
|
|
450
|
+
return 'v1.2.3\n';
|
|
451
|
+
return `${'a'.repeat(40)}\n`;
|
|
452
|
+
}),
|
|
453
|
+
}));
|
|
454
|
+
expect(result.components[1]).toEqual(expect.objectContaining({
|
|
455
|
+
status: 'unverifiable',
|
|
456
|
+
detail: 'Could not read local registry origin.',
|
|
457
|
+
}));
|
|
458
|
+
});
|
|
459
|
+
it('reports a missing origin config value as an unreadable origin', async () => {
|
|
460
|
+
const { checkCurrentness } = require('../../../src/core/currentness/check');
|
|
461
|
+
const result = await checkCurrentness(root, deps({
|
|
462
|
+
git: jest.fn().mockImplementation(async (_cwd, args) => {
|
|
463
|
+
if (args[0] === 'config')
|
|
464
|
+
throw Object.assign(new Error('exit 1'), { code: 1 });
|
|
465
|
+
if (args[0] === 'ls-remote')
|
|
466
|
+
return `${'a'.repeat(40)}\trefs/tags/v1.2.3\n`;
|
|
467
|
+
if (args[0] === 'describe')
|
|
468
|
+
return 'v1.2.3\n';
|
|
469
|
+
return `${'a'.repeat(40)}\n`;
|
|
470
|
+
}),
|
|
471
|
+
}));
|
|
472
|
+
expect(result.components[1]).toEqual(expect.objectContaining({
|
|
473
|
+
status: 'unverifiable',
|
|
474
|
+
detail: 'Could not read local registry origin.',
|
|
475
|
+
}));
|
|
476
|
+
});
|
|
416
477
|
});
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
// cli/tests/release/orchestrator-trailer.test.ts
|
|
7
|
+
//
|
|
8
|
+
// R6.1 (#217): the bump commit must be read as a Change Record opt-out by the code that reads it
|
|
9
|
+
// (`hasOptOut`), not just contain the right text in a mocked argv. The orchestrator runs against a
|
|
10
|
+
// real tmp git repo: `git add` and `git commit` execute for real with the argv the orchestrator
|
|
11
|
+
// passes (never retyped here); every other command is faked.
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const child_process_1 = require("child_process");
|
|
15
|
+
const orchestrator_1 = require("../../src/release/orchestrator");
|
|
16
|
+
const core_1 = require("../../src/release/core");
|
|
17
|
+
const trailers_1 = require("../../src/core/changes/trailers");
|
|
18
|
+
const helpers_1 = require("../core/changes/helpers");
|
|
19
|
+
describe('release — the bump commit opts out of the Change Record (R6.1)', () => {
|
|
20
|
+
let repo;
|
|
21
|
+
beforeEach(() => { repo = (0, helpers_1.makeRepo)(); });
|
|
22
|
+
afterEach(() => repo.cleanup());
|
|
23
|
+
it('the commit the orchestrator creates is read as an opt-out and keeps its subject byte-identical', () => {
|
|
24
|
+
// Explicit environment for the child git: identity and config isolation never come from process.env alone.
|
|
25
|
+
const gitEnv = {
|
|
26
|
+
...process.env, HOME: repo.home, GIT_CONFIG_GLOBAL: path_1.default.join(repo.home, 'gitconfig'), GIT_CONFIG_NOSYSTEM: '1',
|
|
27
|
+
GIT_AUTHOR_NAME: 't', GIT_AUTHOR_EMAIL: 't@t', GIT_COMMITTER_NAME: 't', GIT_COMMITTER_EMAIL: 't@t',
|
|
28
|
+
};
|
|
29
|
+
repo.commit({
|
|
30
|
+
'cli/package.json': '{"version":"2.1.1"}\n',
|
|
31
|
+
'cli/package-lock.json': '{"version":"2.1.1"}\n',
|
|
32
|
+
'CHANGELOG.md': '# Changelog\n',
|
|
33
|
+
}, 'chore: seed release files');
|
|
34
|
+
repo.git('config', 'commit.gpgsign', 'false');
|
|
35
|
+
expect((0, trailers_1.hasOptOut)(repo.root, ['-1', 'HEAD'])).toBe(false);
|
|
36
|
+
let pkgVersion = '2.1.1';
|
|
37
|
+
let changelog = '# Changelog\n';
|
|
38
|
+
const write = (rel, content) => fs_1.default.writeFileSync(path_1.default.join(repo.root, rel), content);
|
|
39
|
+
const commitArgv = [];
|
|
40
|
+
const io = {
|
|
41
|
+
run(cmd, args) {
|
|
42
|
+
if (cmd === 'git' && (args[0] === 'add' || args[0] === 'commit')) {
|
|
43
|
+
if (args[0] === 'commit')
|
|
44
|
+
commitArgv.push(args);
|
|
45
|
+
return (0, child_process_1.execFileSync)('git', args, { cwd: repo.root, encoding: 'utf8', env: gitEnv });
|
|
46
|
+
}
|
|
47
|
+
if (cmd === 'git' && args[0] === 'rev-parse')
|
|
48
|
+
return 'main';
|
|
49
|
+
if (cmd === 'git' && args[0] === 'log')
|
|
50
|
+
return `feat: nueva${core_1.US}${core_1.RS}`;
|
|
51
|
+
return ''; // status, tag, npm view/publish: nothing there, nothing dirty
|
|
52
|
+
},
|
|
53
|
+
readPackageVersion: () => pkgVersion,
|
|
54
|
+
writePackageVersion: (v) => { pkgVersion = v; write('cli/package.json', `{"version":"${v}"}\n`); },
|
|
55
|
+
writeLockVersion: (v) => write('cli/package-lock.json', `{"version":"${v}"}\n`),
|
|
56
|
+
readChangelog: () => changelog,
|
|
57
|
+
writeChangelog: (c) => { changelog = c; write('CHANGELOG.md', c); },
|
|
58
|
+
writeNpmrc: () => { },
|
|
59
|
+
removeNpmrc: () => { },
|
|
60
|
+
today: () => '2026-06-25',
|
|
61
|
+
log: () => { },
|
|
62
|
+
env: { NPM_TOKEN: 'tok' },
|
|
63
|
+
};
|
|
64
|
+
const res = (0, orchestrator_1.release)({ dryRun: false, force: null, push: false, branch: 'main', cliDir: '/cli' }, io);
|
|
65
|
+
expect(res).toEqual({ released: true, version: '2.2.0' });
|
|
66
|
+
expect(commitArgv).toHaveLength(1); // the argv under test was really executed, once
|
|
67
|
+
expect(repo.git('log', '-1', '--format=%s')).toBe('chore(release): v2.2.0 [skip ci]');
|
|
68
|
+
expect((0, trailers_1.hasOptOut)(repo.root, ['-1', 'HEAD'])).toBe(true);
|
|
69
|
+
// The opt-out is a trailer paragraph of its own: the full message is subject, blank line, trailer.
|
|
70
|
+
expect(repo.git('log', '-1', '--format=%B')).toBe('chore(release): v2.2.0 [skip ci]\n\nChange-Record: none');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -51,6 +51,7 @@ describe('release — happy path', () => {
|
|
|
51
51
|
expect(calls).toContain('WRITE_PKG 2.2.0');
|
|
52
52
|
const joined = calls.join('\n');
|
|
53
53
|
expect(joined).toMatch(/git commit .*chore\(release\): v2.2.0/);
|
|
54
|
+
expect(joined).toMatch(/git commit .*chore\(release\): v2\.2\.0 \[skip ci\] -m Change-Record: none/);
|
|
54
55
|
expect(joined).toMatch(/git tag -a v2.2.0/);
|
|
55
56
|
expect(joined).toMatch(/npm publish/);
|
|
56
57
|
expect(joined).toMatch(/git push origin main/);
|