agentic-workflow-manager 9.17.0 → 9.19.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 (36) hide show
  1. package/dist/src/commands/env/index.js +192 -0
  2. package/dist/src/commands/plan/index.js +1 -1
  3. package/dist/src/commands/registry/doctor.js +428 -0
  4. package/dist/src/commands/registry/index.js +52 -0
  5. package/dist/src/commands/registry/scaffold.js +140 -0
  6. package/dist/src/commands/sensors/read-only-observation.js +305 -0
  7. package/dist/src/commands/sensors/run.js +23 -60
  8. package/dist/src/commands/watch/index.js +3 -0
  9. package/dist/src/core/admission/index.js +11 -1
  10. package/dist/src/core/currentness/check.js +5 -2
  11. package/dist/src/core/environments/resolve.js +122 -0
  12. package/dist/src/core/environments/store.js +323 -0
  13. package/dist/src/core/facts/catalog.js +138 -0
  14. package/dist/src/core/facts/needs.js +57 -0
  15. package/dist/src/core/facts/types.js +21 -0
  16. package/dist/src/index.js +2 -0
  17. package/dist/tests/commands/env/index.test.js +261 -0
  18. package/dist/tests/commands/registry/doctor.test.js +283 -0
  19. package/dist/tests/commands/registry/scaffold.test.js +186 -0
  20. package/dist/tests/commands/sensors/read-only-observation.test.js +537 -0
  21. package/dist/tests/commands/sensors/run-read-only.test.js +151 -0
  22. package/dist/tests/commands/watch/reopened-196.test.js +9 -2
  23. package/dist/tests/core/admission/index.test.js +60 -0
  24. package/dist/tests/core/currentness/check-insteadof.test.js +120 -0
  25. package/dist/tests/core/currentness/check.test.js +75 -14
  26. package/dist/tests/core/environments/resolve.test.js +347 -0
  27. package/dist/tests/core/environments/store.test.js +248 -0
  28. package/dist/tests/core/facts/catalog.test.js +179 -0
  29. package/dist/tests/core/facts/needs.test.js +88 -0
  30. package/dist/tests/structural/cloud-admission-docs-contract.test.js +54 -0
  31. package/dist/tests/structural/operator-environments-docs-contract.test.js +70 -0
  32. package/package.json +1 -1
  33. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  34. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  35. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  36. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -0,0 +1,151 @@
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 mockRunCommand = jest.fn();
11
+ jest.mock('../../../src/commands/sensors/exec', () => ({
12
+ runCommand: (...args) => mockRunCommand(...args),
13
+ }));
14
+ const { ok } = require('./exec-fixtures');
15
+ describe('runSensors read-only observation (admission)', () => {
16
+ const ENV_KEYS = ['HOME', 'AWM_HOME', 'GIT_CONFIG_GLOBAL', 'GIT_CONFIG_NOSYSTEM', 'GIT_CEILING_DIRECTORIES'];
17
+ const saved = {};
18
+ let root;
19
+ let project;
20
+ beforeEach(() => {
21
+ jest.resetModules();
22
+ mockRunCommand.mockReset();
23
+ mockRunCommand.mockResolvedValue(ok(''));
24
+ root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-run-readonly-'));
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, '[user]\n\temail = test@example.test\n\tname = test\n');
33
+ process.env.GIT_CONFIG_GLOBAL = globalConfig;
34
+ process.env.GIT_CONFIG_NOSYSTEM = '1';
35
+ process.env.GIT_CEILING_DIRECTORIES = root;
36
+ project = path_1.default.join(root, 'project');
37
+ write('.awm/sensors.json', JSON.stringify({ pack: 'js-ts', sensors: { lint: { fast: true, cmd: 'eslint --format json .' } } }));
38
+ });
39
+ afterEach(() => {
40
+ jest.dontMock('../../../src/commands/sensors/read-only-observation');
41
+ for (const key of ENV_KEYS) {
42
+ if (saved[key] === undefined)
43
+ delete process.env[key];
44
+ else
45
+ process.env[key] = saved[key];
46
+ }
47
+ fs_1.default.rmSync(root, { recursive: true, force: true });
48
+ });
49
+ function write(relative, content = 'x') {
50
+ const file = path_1.default.join(project, ...relative.split('/'));
51
+ fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
52
+ fs_1.default.writeFileSync(file, content);
53
+ }
54
+ const git = (args) => (0, child_process_1.execFileSync)('git', args, { cwd: project, env: { ...process.env }, stdio: 'pipe', encoding: 'utf8' });
55
+ function commitProject(gitignore) {
56
+ write('.gitignore', gitignore);
57
+ git(['init', '-q', '-b', 'main']);
58
+ git(['add', '.']);
59
+ git(['commit', '-qm', 'initial']);
60
+ }
61
+ const load = () => require('../../../src/commands/sensors/run');
62
+ it('skips ignored default directories, including one a sensor creates, and reports them', async () => {
63
+ commitProject('node_modules/\ncoverage/\n');
64
+ write('node_modules/pkg/index.js', 'installed');
65
+ mockRunCommand.mockImplementation(async () => {
66
+ write('coverage/lcov.info', 'sensor output');
67
+ write('node_modules/pkg/index.js', 'rebuilt');
68
+ return ok('');
69
+ });
70
+ const out = await load().runSensors({ cwd: project, readOnly: true });
71
+ expect(out.reason).not.toBe('read-only-mutation-detected');
72
+ expect(out.reason).not.toBe('read-only-observation-unavailable');
73
+ expect(out.readOnlyObservation).toEqual({ defaults: true, excluded: ['coverage', 'node_modules'], rejected: [] });
74
+ });
75
+ it('still detects a mutation inside a matching directory that is not ignored', async () => {
76
+ commitProject('');
77
+ write('node_modules/pkg/index.js', 'installed');
78
+ mockRunCommand.mockImplementation(async () => {
79
+ write('node_modules/pkg/index.js', 'rebuilt');
80
+ return ok('');
81
+ });
82
+ const out = await load().runSensors({ cwd: project, readOnly: true });
83
+ expect(out).toMatchObject({ overall: 'not_certified', reason: 'read-only-mutation-detected' });
84
+ expect(out.readOnlyObservation).toEqual({ defaults: true, excluded: [], rejected: [{ path: 'node_modules', reason: 'not-ignored' }] });
85
+ });
86
+ it('observes everything and says so when the project is not a git worktree', async () => {
87
+ write('node_modules/pkg/index.js', 'installed');
88
+ const out = await load().runSensors({ cwd: project, readOnly: true });
89
+ expect(out.readOnlyObservation).toEqual({ defaults: true, excluded: [], rejected: [{ path: 'node_modules', reason: 'git-unavailable' }] });
90
+ });
91
+ it('blocks a read-only run on an invalid admission file before running sensors', async () => {
92
+ write('.awm/admission.json', JSON.stringify({ schemaVersion: 2 }));
93
+ const out = await load().runSensors({ cwd: project, readOnly: true });
94
+ expect(out).toMatchObject({
95
+ overall: 'not_certified', mode: 'invalid', reason: 'admission-config-invalid',
96
+ remedy: 'repair .awm/admission.json: schemaVersion must be 1',
97
+ });
98
+ expect(mockRunCommand).not.toHaveBeenCalled();
99
+ });
100
+ it('never reads the admission file outside read-only runs', async () => {
101
+ write('.awm/admission.json', JSON.stringify({ schemaVersion: 2 }));
102
+ const out = await load().runSensors({ cwd: project });
103
+ expect(out.reason).not.toBe('admission-config-invalid');
104
+ expect(out.readOnlyObservation).toBeUndefined();
105
+ expect(mockRunCommand).toHaveBeenCalled();
106
+ });
107
+ it('returns an actionable remedy with evidence when the snapshot exceeds a limit', async () => {
108
+ jest.doMock('../../../src/commands/sensors/read-only-observation', () => {
109
+ const actual = jest.requireActual('../../../src/commands/sensors/read-only-observation');
110
+ return { ...actual, filesystemSnapshot: () => ({ ok: false, failure: 'limit-bytes' }) };
111
+ });
112
+ const out = await load().runSensors({ cwd: project, readOnly: true });
113
+ expect(out).toMatchObject({ overall: 'not_certified', reason: 'read-only-observation-unavailable' });
114
+ expect(out.remedy).toMatch(/1 GiB/);
115
+ expect(out.remedy).toContain('.awm/admission.json');
116
+ expect(out.readOnlyObservation).toEqual({ defaults: true, excluded: [], rejected: [] });
117
+ expect(mockRunCommand).not.toHaveBeenCalled();
118
+ });
119
+ describe('the post-run snapshot', () => {
120
+ const DIGEST = 'a'.repeat(64);
121
+ const stubSnapshot = (second) => {
122
+ const calls = jest.fn()
123
+ .mockReturnValueOnce({ ok: true, digest: DIGEST })
124
+ .mockReturnValueOnce(second);
125
+ jest.doMock('../../../src/commands/sensors/read-only-observation', () => {
126
+ const actual = jest.requireActual('../../../src/commands/sensors/read-only-observation');
127
+ return { ...actual, filesystemSnapshot: (...args) => calls(...args) };
128
+ });
129
+ return calls;
130
+ };
131
+ it('never certifies a run whose worktree cannot be observed after the sensors ran', async () => {
132
+ const snapshot = stubSnapshot({ ok: false, failure: 'unreadable' });
133
+ const out = await load().runSensors({ cwd: project, readOnly: true });
134
+ expect(out).toMatchObject({
135
+ overall: 'not_certified', reason: 'read-only-mutation-detected',
136
+ remedy: 'sensor execution changed the worktree; repair it before using this evidence for admission',
137
+ });
138
+ expect(out.readOnlyObservation).toEqual({ defaults: true, excluded: expect.any(Array), rejected: expect.any(Array) });
139
+ expect(snapshot).toHaveBeenCalledTimes(2);
140
+ expect(mockRunCommand).toHaveBeenCalledTimes(1);
141
+ });
142
+ it('keeps the verdict when the post-run snapshot matches the first one', async () => {
143
+ const snapshot = stubSnapshot({ ok: true, digest: DIGEST });
144
+ const out = await load().runSensors({ cwd: project, readOnly: true });
145
+ expect(out.reason).not.toBe('read-only-mutation-detected');
146
+ expect(out.readOnlyObservation).toEqual({ defaults: true, excluded: expect.any(Array), rejected: expect.any(Array) });
147
+ expect(snapshot).toHaveBeenCalledTimes(2);
148
+ expect(mockRunCommand).toHaveBeenCalledTimes(1);
149
+ });
150
+ });
151
+ });
@@ -921,9 +921,16 @@ describe('issue #196 reopened: supervised S1 handoff', () => {
921
921
  }
922
922
  expect(stdout).toContain('supervisor activo');
923
923
  child.kill('SIGINT');
924
- const exit = await new Promise(resolve => child.once('close', code => resolve(code)));
925
- expect(exit).toBe(130);
924
+ const exit = await new Promise(resolve => {
925
+ child.once('exit', (code, signal) => resolve({ code, signal }));
926
+ });
927
+ // Contract: truthful interrupt report. Exit status is secondary —
928
+ // on Darwin, child_process often reports code null + SIGINT when the
929
+ // signal disposition races process.exitCode=130 (never use process.exit
930
+ // in watch/index: it kills in-process Jest callers). Either 130 or
931
+ // null+SIGINT is an interrupted stop; require the detenido line either way.
926
932
  expect(stdout).toMatch(/detenido; ciclo IN_PROGRESS/);
933
+ expect(exit.code === 130 || (exit.code === null && exit.signal === 'SIGINT')).toBe(true);
927
934
  expect(stdout).not.toContain('COMPLETE');
928
935
  expect(stderr).not.toContain('Error');
929
936
  }
@@ -258,6 +258,66 @@ describe('admitPlan', () => {
258
258
  expect(() => (0, admission_1.sanitizeAdmissionReport)({ ...report, sensorEvidence: 'opt-out' })).toThrow('invalid report');
259
259
  });
260
260
  });
261
+ describe('the sensor-blocked diagnostic', () => {
262
+ const base = () => ({ plan: valid, provider: 'codex', cwd: process.cwd(), enabledAgents: ['codex'], verifySensors: true });
263
+ it('carries the sensor reason and remedy when the verdict is not-certified', async () => {
264
+ const report = await (0, admission_1.admitPlan)({ ...base(), sensors: {
265
+ overall: 'not_certified', sensors: [], mode: 'invalid', reason: 'read-only-observation-unavailable',
266
+ remedy: 'declare heavy directories in .awm/admission.json.', projectRoot: '/r', manifestPath: '/r/.awm/sensors.json',
267
+ } });
268
+ expect(report.diagnostics[0]).toEqual({
269
+ code: 'ADMISSION_SENSORS_BLOCKED',
270
+ message: 'Sensor verdict is not-certified (read-only-observation-unavailable): declare heavy directories in .awm/admission.json.',
271
+ });
272
+ expect(report.diagnostics[0].message.match(/ADMISSION_[A-Z_]+/g)).toBeNull();
273
+ });
274
+ it('keeps the reason alone when the run has no remedy', async () => {
275
+ const report = await (0, admission_1.admitPlan)({ ...base(), sensors: {
276
+ overall: 'not_certified', sensors: [], mode: 'native-gate', reason: 'native-gate-declared', projectRoot: '/r', manifestPath: '/r/.awm/sensors.json',
277
+ } });
278
+ expect(report.diagnostics[0].message).toBe('Sensor verdict is not-certified (native-gate-declared).');
279
+ });
280
+ it.each([
281
+ ['remedy', { reason: 'admission-config-invalid', remedy: 'repair .awm/admission.json: ADMISSION_FOO is not a known field' }, 'ADMISSION-FOO'],
282
+ ['reason', { reason: 'ADMISSION_X-unavailable', remedy: 'fix it' }, 'ADMISSION-X'],
283
+ ])('leaves the diagnostic code as the only ADMISSION_ token when the %s echoes one', async (_name, fields, neutralized) => {
284
+ const report = await (0, admission_1.admitPlan)({ ...base(), sensors: {
285
+ overall: 'not_certified', sensors: [], mode: 'invalid', ...fields, projectRoot: '/r', manifestPath: '/r/.awm/sensors.json',
286
+ } });
287
+ expect(report.diagnostics[0].code).toBe('ADMISSION_SENSORS_BLOCKED');
288
+ expect(report.diagnostics[0].message).toContain(neutralized);
289
+ expect(`${report.diagnostics[0].code} ${report.diagnostics[0].message}`.match(/ADMISSION_[A-Z_]+/g)).toEqual(['ADMISSION_SENSORS_BLOCKED']);
290
+ });
291
+ it.each([
292
+ ['trims a padded reason and remedy', ' read-only-observation-unavailable ', ' declare it in .awm/admission.json. ', 'Sensor verdict is not-certified (read-only-observation-unavailable): declare it in .awm/admission.json.'],
293
+ ['drops a whitespace-only reason', ' ', 'declare it in .awm/admission.json.', 'Sensor verdict is not-certified.'],
294
+ ['drops a whitespace-only remedy', 'some-reason', ' ', 'Sensor verdict is not-certified (some-reason).'],
295
+ ])('%s', async (_name, reason, remedy, message) => {
296
+ const report = await (0, admission_1.admitPlan)({ ...base(), sensors: {
297
+ overall: 'not_certified', sensors: [], mode: 'invalid', reason, remedy, projectRoot: '/r', manifestPath: '/r/.awm/sensors.json',
298
+ } });
299
+ expect(report.diagnostics[0]).toEqual({ code: 'ADMISSION_SENSORS_BLOCKED', message });
300
+ });
301
+ it('neutralizes every ADMISSION_ token when several are echoed in the reason and the remedy', async () => {
302
+ const report = await (0, admission_1.admitPlan)({ ...base(), sensors: {
303
+ overall: 'not_certified', sensors: [], mode: 'invalid',
304
+ reason: 'admission-config-invalid ADMISSION_BAZ ADMISSION_QUX',
305
+ remedy: 'repair .awm/admission.json: ADMISSION_FOO is not allowed, ADMISSION_BAR neither',
306
+ projectRoot: '/r', manifestPath: '/r/.awm/sensors.json',
307
+ } });
308
+ const { code, message } = report.diagnostics[0];
309
+ expect(code).toBe('ADMISSION_SENSORS_BLOCKED');
310
+ for (const token of ['ADMISSION-FOO', 'ADMISSION-BAR', 'ADMISSION-BAZ', 'ADMISSION-QUX'])
311
+ expect(message).toContain(token);
312
+ expect(`${code} ${message}`.match(/ADMISSION_[A-Z_]+/g)).toEqual(['ADMISSION_SENSORS_BLOCKED']);
313
+ });
314
+ it('keeps the bare verdict for a failing run', async () => {
315
+ const report = await (0, admission_1.admitPlan)({ ...base(), sensors: {
316
+ overall: 'fail', sensors: [], mode: 'project-sensors', reason: 'configured-v3', projectRoot: '/r', manifestPath: '/r/.awm/sensors.json',
317
+ } });
318
+ expect(report.diagnostics[0]).toEqual({ code: 'ADMISSION_SENSORS_BLOCKED', message: 'Sensor verdict is fail.' });
319
+ });
320
+ });
261
321
  it('admits unattended work only with an exact schema-2 plan binding', async () => {
262
322
  const unattended = { ...valid, manifest: { ...valid.manifest, executionMode: 'desatendido' } };
263
323
  const journal = { ...(0, types_1.emptyState)('main'), schema: 2, planBinding: { path: 'docs/plan.md', digest: 'a'.repeat(64), schema: 'compact-slices/v1', executionMode: 'desatendido', boundAt: '2026-09-15T00:00:00.000Z' } };
@@ -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] === 'remote')
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] === 'remote')
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] === 'remote')
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] === 'remote')
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] === 'remote')
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] === 'remote' ? 'https://example.test/team/repo.git\n' : `${'a'.repeat(40)}\n`],
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] === 'remote')
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] === 'remote')
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] === 'remote')
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] === 'remote')
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] === 'remote')
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] === 'remote')
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] === 'remote')
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] === 'remote')
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
  });