agentic-workflow-manager 9.18.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.
- package/dist/src/commands/plan/index.js +1 -1
- package/dist/src/commands/sensors/read-only-observation.js +305 -0
- package/dist/src/commands/sensors/run.js +23 -60
- package/dist/src/core/admission/index.js +11 -1
- package/dist/src/core/currentness/check.js +5 -2
- 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/currentness/check-insteadof.test.js +120 -0
- package/dist/tests/core/currentness/check.test.js +75 -14
- 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,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
|
+
});
|
|
@@ -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] === '
|
|
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,54 @@
|
|
|
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 path_1 = __importDefault(require("path"));
|
|
8
|
+
/** CMD-DOCS — V-R4 (issue #215 documentation). */
|
|
9
|
+
const repoRoot = path_1.default.resolve(__dirname, '../../..');
|
|
10
|
+
/** Whitespace-normalized so re-wrapping the prose never breaks a pinned phrase. */
|
|
11
|
+
const read = (relative) => fs_1.default.readFileSync(path_1.default.join(repoRoot, ...relative.split('/')), 'utf8').replace(/\s+/g, ' ');
|
|
12
|
+
describe('issue #215 documentation contract (V-R4)', () => {
|
|
13
|
+
const configuration = read('docs/configuration.md');
|
|
14
|
+
it('documents .awm/admission.json with defaults, the git rule and the accepted risk', () => {
|
|
15
|
+
expect(configuration).toContain('## Read-only admission observation (`.awm/admission.json`)');
|
|
16
|
+
for (const name of ['node_modules', '.next', '.turbo', 'dist', 'build', 'coverage', '.venv', 'target'])
|
|
17
|
+
expect(configuration).toContain(`\`${name}\``);
|
|
18
|
+
expect(configuration).toContain('git-ignored and contains no tracked file');
|
|
19
|
+
expect(configuration).toContain('**Accepted risk:**');
|
|
20
|
+
expect(configuration).toContain('`admission-config-invalid`');
|
|
21
|
+
expect(configuration).toContain('`readOnlyObservation`');
|
|
22
|
+
expect(configuration).toContain('`awm plan admit` does not print it');
|
|
23
|
+
});
|
|
24
|
+
it.each([
|
|
25
|
+
'250000',
|
|
26
|
+
'1 GiB',
|
|
27
|
+
'`read-only-mutation-detected`',
|
|
28
|
+
'`read-only-observation-unavailable`',
|
|
29
|
+
'`not-ignored`',
|
|
30
|
+
'`contains-tracked`',
|
|
31
|
+
'`git-unavailable`',
|
|
32
|
+
'`.git` or `.awm`',
|
|
33
|
+
'never skipped',
|
|
34
|
+
'including any `.awm` or `.git` nested',
|
|
35
|
+
'regular file',
|
|
36
|
+
'65536 bytes',
|
|
37
|
+
])('documents the observation limit, reason code or reserved-name rule %s', literal => {
|
|
38
|
+
expect(configuration).toContain(literal);
|
|
39
|
+
});
|
|
40
|
+
it('points --verify-sensors in the CLI reference at the observation rules', () => {
|
|
41
|
+
const doc = read('docs/cli-reference.md');
|
|
42
|
+
expect(doc).toContain('configuration.md#read-only-admission-observation-awmadmissionjson');
|
|
43
|
+
expect(doc).toContain('`ADMISSION_SENSORS_BLOCKED`');
|
|
44
|
+
});
|
|
45
|
+
it('states that the D-7 insteadOf recipe passes strict currentness', () => {
|
|
46
|
+
const doc = read('docs/operations/cloud-and-platforms.md');
|
|
47
|
+
expect(doc).toContain('`awm preflight --require-current`');
|
|
48
|
+
expect(doc).toContain('`remote.origin.url`');
|
|
49
|
+
expect(doc).toContain('--local');
|
|
50
|
+
});
|
|
51
|
+
it('describes the snapshot exclusions in the plan admit help', () => {
|
|
52
|
+
expect(read('cli/src/commands/plan/index.ts')).toContain("'require an empirical sensor pass; read-only observation skips git-ignored build output (see .awm/admission.json)'");
|
|
53
|
+
});
|
|
54
|
+
});
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|