agentic-workflow-manager 8.1.4 → 8.1.6
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/preflight/checks.js +70 -1
- package/dist/src/commands/preflight/index.js +6 -5
- package/dist/src/commands/sensors/changed.js +15 -0
- package/dist/src/commands/sensors/compatibility/contract.js +17 -3
- package/dist/src/commands/sensors/compatibility/manifest.js +16 -4
- package/dist/src/commands/sensors/compatibility/timeout.js +21 -0
- package/dist/src/commands/sensors/exec.js +3 -1
- package/dist/src/commands/sensors/index.js +5 -13
- package/dist/src/commands/sensors/init.js +1 -0
- package/dist/src/commands/sensors/prepare.js +140 -0
- package/dist/src/commands/sensors/result.js +140 -0
- package/dist/src/commands/sensors/run.js +56 -335
- package/dist/src/commands/sensors/status.js +93 -21
- package/dist/src/commands/sensors/verdict.js +27 -0
- package/dist/tests/commands/preflight/preflight.test.js +50 -0
- package/dist/tests/commands/sensors/baseline.test.js +14 -0
- package/dist/tests/commands/sensors/changed-windows.test.js +3 -0
- package/dist/tests/commands/sensors/compatibility/contract.test.js +51 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +27 -11
- package/dist/tests/commands/sensors/compatibility/probe.test.js +13 -3
- package/dist/tests/commands/sensors/exec-fixtures.js +1 -1
- package/dist/tests/commands/sensors/exec.test.js +36 -0
- package/dist/tests/commands/sensors/index.test.js +46 -8
- package/dist/tests/commands/sensors/init.test.js +30 -1
- package/dist/tests/commands/sensors/prepare.test.js +88 -0
- package/dist/tests/commands/sensors/router.test.js +15 -1
- package/dist/tests/commands/sensors/run-changed.test.js +4 -4
- package/dist/tests/commands/sensors/run.test.js +29 -0
- package/dist/tests/commands/sensors/status-windows.test.js +1 -1
- package/dist/tests/commands/sensors/status.test.js +52 -20
- package/dist/tests/integration/preflight-json-pipe.e2e.test.js +79 -3
- package/dist/tests/integration/sensor-compatibility.e2e.test.js +40 -1
- package/dist/tests/structural/sensor-documentation-contract.test.js +33 -5
- package/dist/tests/structural/support-matrix-is-current.test.js +25 -0
- package/package.json +1 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const prepare_1 = require("../../../src/commands/sensors/prepare");
|
|
4
|
+
const fullCommand = { executable: 'manifest-eslint', resolution: 'path', args: ['.'] };
|
|
5
|
+
const changedCommand = {
|
|
6
|
+
executable: 'live-eslint',
|
|
7
|
+
resolution: 'path',
|
|
8
|
+
args: ['--format', 'json', '{files}'],
|
|
9
|
+
fileInput: { placeholder: '{files}', extensions: ['.ts'] },
|
|
10
|
+
};
|
|
11
|
+
function v2Input(overrides = {}) {
|
|
12
|
+
const liveVariant = {
|
|
13
|
+
id: 'eslint-live', priority: 1, certifiedRange: '>=1.0.0', requirements: { tool: 'eslint', toolRange: '>=1.0.0', runtime: 'node', runtimeRange: '>=1.0.0' }, assets: [],
|
|
14
|
+
probe: { kind: 'version' }, command: { executable: 'live-eslint', resolution: 'path', args: ['.'] }, changedCommand, formatter: 'eslint-llm',
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
name: 'lint',
|
|
18
|
+
sensor: {
|
|
19
|
+
enabled: true, fast: false, variantId: 'eslint-live', command: fullCommand,
|
|
20
|
+
initializedCompatibility: { state: 'certified', reason: 'test', variantId: 'eslint-live', toolVersion: '1.0.0', runtimeVersion: '1.0.0', certifiedRange: '>=1.0.0', evidence: [] },
|
|
21
|
+
},
|
|
22
|
+
liveSensor: { applicability: {}, fast: false, timeout: 30_000, variants: [liveVariant] },
|
|
23
|
+
liveState: { state: 'certified', reason: 'test', variantId: 'eslint-live', toolVersion: '1.0.0', runtimeVersion: '1.0.0', certifiedRange: '>=1.0.0', evidence: [] },
|
|
24
|
+
changed: { files: ['src/a.ts'] },
|
|
25
|
+
requestedScope: 'changed',
|
|
26
|
+
projectTimeout: 90_000,
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
describe('prepareV2Sensor', () => {
|
|
31
|
+
test('v2 uses the live command and project > pack > fallback timeout (R1.1, R3.1)', () => {
|
|
32
|
+
const prepared = (0, prepare_1.prepareV2Sensor)(v2Input());
|
|
33
|
+
expect(prepared.command).toEqual({ kind: 'structured', value: { executable: 'live-eslint', resolution: 'path', args: ['--format', 'json', 'src/a.ts'] } });
|
|
34
|
+
expect(prepared.timeoutMs).toBe(90_000);
|
|
35
|
+
expect(prepared.timeoutSource).toBe('project');
|
|
36
|
+
expect((0, prepare_1.prepareV2Sensor)(v2Input({ projectTimeout: undefined })).timeoutSource).toBe('pack');
|
|
37
|
+
const fallback = v2Input();
|
|
38
|
+
fallback.liveSensor.timeout = undefined;
|
|
39
|
+
fallback.liveSensor.fast = true;
|
|
40
|
+
fallback.sensor.fast = undefined;
|
|
41
|
+
expect((0, prepare_1.prepareV2Sensor)({ ...fallback, projectTimeout: undefined })).toMatchObject({ timeoutMs: 10_000, timeoutSource: 'fallback' });
|
|
42
|
+
});
|
|
43
|
+
test('expands changed paths as literal argv entries (R4.1, R10.2)', () => {
|
|
44
|
+
const prepared = (0, prepare_1.prepareV2Sensor)(v2Input({ changed: { files: ['src/a b.ts', 'src/$x.ts'] } }));
|
|
45
|
+
expect(prepared.command).toEqual({ kind: 'structured', value: { executable: 'live-eslint', resolution: 'path', args: ['--format', 'json', 'src/a b.ts', 'src/$x.ts'] } });
|
|
46
|
+
expect(prepared.effectiveScope).toBe('changed');
|
|
47
|
+
});
|
|
48
|
+
test('falls back full with an explicit reason without changedCommand (R4.2)', () => {
|
|
49
|
+
const input = v2Input();
|
|
50
|
+
input.liveSensor.variants[0].changedCommand = undefined;
|
|
51
|
+
const prepared = (0, prepare_1.prepareV2Sensor)(input);
|
|
52
|
+
expect(prepared.command).toEqual({ kind: 'structured', value: input.liveSensor.variants[0].command });
|
|
53
|
+
expect(prepared.effectiveScope).toBe('full');
|
|
54
|
+
expect(prepared.scopeReason).toMatch(/does not support changed scope/);
|
|
55
|
+
});
|
|
56
|
+
test('uses the full command with an explicit reason when the diff cannot resolve', () => {
|
|
57
|
+
const prepared = (0, prepare_1.prepareV2Sensor)(v2Input({ changed: { files: [], error: 'git failed' } }));
|
|
58
|
+
expect(prepared.effectiveScope).toBe('full');
|
|
59
|
+
expect(prepared.scopeReason).toMatch(/could not be resolved: git failed/);
|
|
60
|
+
});
|
|
61
|
+
test('returns zero-file pass plan without a process (R4.4)', () => {
|
|
62
|
+
const prepared = (0, prepare_1.prepareV2Sensor)(v2Input({ changed: { files: ['README.md'] } }));
|
|
63
|
+
expect(prepared).toMatchObject({ effectiveScope: 'changed', files: 0, syntheticStatus: 'pass' });
|
|
64
|
+
expect(prepared.command).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
test('rejects an invalid requested scope before preparing a command', () => {
|
|
67
|
+
expect(() => (0, prepare_1.prepareV2Sensor)({ ...v2Input(), requestedScope: 'sideways' }))
|
|
68
|
+
.toThrow('requested scope must be "full" or "changed"');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('prepareLegacySensor', () => {
|
|
72
|
+
const originalPlatform = process.platform;
|
|
73
|
+
afterEach(() => Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true }));
|
|
74
|
+
test('falls back to the full command for an unsafe Windows filename', () => {
|
|
75
|
+
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
76
|
+
const prepared = (0, prepare_1.prepareLegacySensor)({
|
|
77
|
+
name: 'lint', config: { cmd: 'eslint .', changedCmd: 'eslint {files}' }, requestedScope: 'changed', changed: { files: ['src/a&b.ts'] },
|
|
78
|
+
});
|
|
79
|
+
expect(prepared).toMatchObject({ command: { kind: 'legacy', value: 'eslint .' }, effectiveScope: 'full' });
|
|
80
|
+
expect(prepared.scopeReason).toMatch(/cmd\.exe metacharacter/);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe('validateRunOptions', () => {
|
|
84
|
+
test('rejects changed baseline capture before any scope preparation (R4.6)', () => {
|
|
85
|
+
expect(() => (0, prepare_1.validateRunOptions)({ changed: true, ignoreBaseline: true }))
|
|
86
|
+
.toThrow(/refusing to combine --changed with a baseline capture/);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -4,7 +4,7 @@ jest.mock('@clack/prompts', () => ({ log: { success: jest.fn(), info: jest.fn()
|
|
|
4
4
|
jest.mock('picocolors', () => ({ green: (s) => s, yellow: (s) => s, red: (s) => s }));
|
|
5
5
|
jest.mock('../../../src/commands/sensors/run', () => ({ runSensors: jest.fn().mockReturnValue({ sensors: [], overall: 'pass' }) }));
|
|
6
6
|
jest.mock('../../../src/commands/sensors/init', () => ({ initSensors: jest.fn().mockReturnValue({ detection: { pack: 'js-ts', indicators: [] }, manifest: { sensors: {} }, configured: [] }) }));
|
|
7
|
-
jest.mock('../../../src/commands/sensors/status', () => ({ computeSensorStatus: jest.fn().mockReturnValue({ overall: '
|
|
7
|
+
jest.mock('../../../src/commands/sensors/status', () => ({ computeSensorStatus: jest.fn().mockReturnValue({ overall: 'READY', pack: 'js-ts', checks: {} }) }));
|
|
8
8
|
jest.mock('../../../src/commands/sensors/install', () => ({ installSensorHook: jest.fn().mockReturnValue({ status: 'installed' }) }));
|
|
9
9
|
const commander_1 = require("commander");
|
|
10
10
|
const index_1 = require("../../../src/commands/sensors/index");
|
|
@@ -21,4 +21,18 @@ describe('registerSensorsCommand', () => {
|
|
|
21
21
|
expect(subNames).toContain('install');
|
|
22
22
|
expect(subNames).toContain('coverage');
|
|
23
23
|
});
|
|
24
|
+
it('renders READY without claiming sensor execution, HEALTHY, or project certification', async () => {
|
|
25
|
+
const program = new commander_1.Command();
|
|
26
|
+
const output = jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
27
|
+
try {
|
|
28
|
+
(0, index_1.registerSensorsCommand)(program);
|
|
29
|
+
await program.parseAsync(['node', 'awm', 'sensors', 'status']);
|
|
30
|
+
const rendered = output.mock.calls.flat().join('\n');
|
|
31
|
+
expect(rendered).toContain('READY');
|
|
32
|
+
expect(rendered).not.toMatch(/HEALTHY|certif/i);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
output.mockRestore();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
24
38
|
});
|
|
@@ -84,12 +84,12 @@ describe('runSensors --changed', () => {
|
|
|
84
84
|
expect(out.sensors[0].scope).toBeUndefined();
|
|
85
85
|
expect(out.changedScope).toEqual({ files: 0, error: 'not a git repository' });
|
|
86
86
|
});
|
|
87
|
-
it('
|
|
87
|
+
it('records a clean synthetic pass when an opted-in sensor has no changed files, without touching the others', async () => {
|
|
88
88
|
dir = project({ lint: LINT, typecheck: TYPECHECK });
|
|
89
89
|
mockChangedFiles.mockReturnValue({ files: [] });
|
|
90
90
|
const out = await load().runSensors({ cwd: dir, changed: true });
|
|
91
91
|
const lint = out.sensors.find((s) => s.name === 'lint');
|
|
92
|
-
expect(lint.status).toBe('
|
|
92
|
+
expect(lint.status).toBe('pass');
|
|
93
93
|
expect(lint.skipReason).toBe('no changed files in scope');
|
|
94
94
|
expect(cmds()).toEqual(['tsc --noEmit']);
|
|
95
95
|
});
|
|
@@ -101,13 +101,13 @@ describe('runSensors --changed', () => {
|
|
|
101
101
|
await load().runSensors({ cwd: dir, changed: true });
|
|
102
102
|
expect(cmds()).toEqual([`eslint --format json 'src/a.ts'`]);
|
|
103
103
|
});
|
|
104
|
-
it('
|
|
104
|
+
it('records a clean synthetic pass when the filter empties the scope, rather than running repo-wide', async () => {
|
|
105
105
|
// A docs-only commit means the lint sensor has nothing to say. Falling back to
|
|
106
106
|
// the full command here would reintroduce exactly the cost --changed removes.
|
|
107
107
|
dir = project({ lint: { ...LINT, changedExtensions: ['.ts'] }, typecheck: TYPECHECK });
|
|
108
108
|
mockChangedFiles.mockReturnValue({ files: ['README.md'] });
|
|
109
109
|
const out = await load().runSensors({ cwd: dir, changed: true });
|
|
110
|
-
expect(out.sensors.find((s) => s.name === 'lint').status).toBe('
|
|
110
|
+
expect(out.sensors.find((s) => s.name === 'lint').status).toBe('pass');
|
|
111
111
|
expect(cmds()).toEqual(['tsc --noEmit']);
|
|
112
112
|
});
|
|
113
113
|
it('refuses a changedCmd without a {files} placeholder instead of running it repo-wide', async () => {
|
|
@@ -7,6 +7,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
7
7
|
const os_1 = __importDefault(require("os"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const run_1 = require("../../../src/commands/sensors/run");
|
|
10
|
+
const verdict_1 = require("../../../src/commands/sensors/verdict");
|
|
10
11
|
function mkTmp() {
|
|
11
12
|
return fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-sensors-'));
|
|
12
13
|
}
|
|
@@ -53,6 +54,17 @@ describe('runSensors', () => {
|
|
|
53
54
|
fs_1.default.rmSync(emptyDir, { recursive: true });
|
|
54
55
|
}
|
|
55
56
|
});
|
|
57
|
+
it.each([
|
|
58
|
+
['fast', { fast: 'yes' }, 'run option fast must be a boolean'],
|
|
59
|
+
['slow', { slow: 1 }, 'run option slow must be a boolean'],
|
|
60
|
+
['all', { all: null }, 'run option all must be a boolean'],
|
|
61
|
+
['cwd', { cwd: ' ' }, 'run option cwd must be a nonempty string'],
|
|
62
|
+
])('rejects invalid public %s options before selecting or dispatching sensors', async (_name, options, message) => {
|
|
63
|
+
const { runSensors } = load();
|
|
64
|
+
await expect(runSensors(options)).rejects.toThrow(message);
|
|
65
|
+
expect(mockRunCommand).not.toHaveBeenCalled();
|
|
66
|
+
expect(mockRunStructuredCommand).not.toHaveBeenCalled();
|
|
67
|
+
});
|
|
56
68
|
it('runs only fast sensors with --fast flag', async () => {
|
|
57
69
|
mockRunCommand.mockResolvedValue(ok());
|
|
58
70
|
const { runSensors } = load();
|
|
@@ -61,6 +73,12 @@ describe('runSensors', () => {
|
|
|
61
73
|
expect(result.sensors.some((s) => s.name === 'security')).toBe(false);
|
|
62
74
|
expect(result.overall).toBe('not_certified');
|
|
63
75
|
});
|
|
76
|
+
it('runs both fast and slow sensors when --fast and --slow are combined', async () => {
|
|
77
|
+
mockRunCommand.mockResolvedValue(ok());
|
|
78
|
+
const { runSensors } = load();
|
|
79
|
+
const result = await runSensors({ fast: true, slow: true, cwd: tmpDir });
|
|
80
|
+
expect(result.sensors.map((sensor) => sensor.name)).toEqual(['typecheck', 'lint', 'security', 'mutation']);
|
|
81
|
+
});
|
|
64
82
|
it('returns fail when a fast sensor has errors', async () => {
|
|
65
83
|
mockRunCommand
|
|
66
84
|
.mockResolvedValueOnce(exited(1, 'src/a.ts(1,1): error TS0001: Bad type.'))
|
|
@@ -203,6 +221,17 @@ describe('runSensors', () => {
|
|
|
203
221
|
expect(result.overall).toBe('fail');
|
|
204
222
|
});
|
|
205
223
|
});
|
|
224
|
+
describe('reduceVerdict', () => {
|
|
225
|
+
it('lets a failing full sensor outrank an empty changed-scope synthetic pass', () => {
|
|
226
|
+
expect((0, verdict_1.reduceVerdict)([
|
|
227
|
+
{ name: 'lint', status: 'pass', errors: [], scope: 'changed' },
|
|
228
|
+
{ name: 'typecheck', status: 'fail', errors: [{ message: 'broken' }] },
|
|
229
|
+
])).toBe('fail');
|
|
230
|
+
});
|
|
231
|
+
it('rejects malformed result statuses instead of treating them as skipped', () => {
|
|
232
|
+
expect(() => (0, verdict_1.reduceVerdict)([{ name: 'lint', status: 'bogus', errors: [] }])).toThrow('sensor result status is invalid');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
206
235
|
describe('runSensors v2 lifecycle contract', () => {
|
|
207
236
|
let project;
|
|
208
237
|
let home;
|
|
@@ -53,7 +53,7 @@ describe('computeSensorStatus — Windows PATH resolution', () => {
|
|
|
53
53
|
// En win32 el usuario escribe `semgrep` y en disco existe `semgrep.cmd`.
|
|
54
54
|
fs_1.default.writeFileSync(path_1.default.join(pathDir, 'semgrep.cmd'), '@echo off\r\n');
|
|
55
55
|
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
56
|
-
expect(result.overall).toBe('
|
|
56
|
+
expect(result.overall).toBe('READY');
|
|
57
57
|
expect(result.checks.security.ok).toBe(true);
|
|
58
58
|
});
|
|
59
59
|
it('reporta ok:false en win32 cuando el binario no esta en PATH', async () => {
|
|
@@ -7,8 +7,11 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
const os_1 = __importDefault(require("os"));
|
|
9
9
|
const status_1 = require("../../../src/commands/sensors/status");
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
jest.mock('../../../src/commands/sensors/exec', () => ({
|
|
11
|
+
runCommand: jest.fn(),
|
|
12
|
+
runStructuredCommand: jest.fn(),
|
|
13
|
+
}));
|
|
14
|
+
const { runCommand, runStructuredCommand } = require('../../../src/commands/sensors/exec');
|
|
12
15
|
// `resolveOnPath` resuelve PATH en proceso (ya no invoca un shell — ver
|
|
13
16
|
// core/paths.ts). Por eso estos tests controlan un PATH aislado en vez de
|
|
14
17
|
// mockear `execSync`: ademas de reflejar el mecanismo real, los vuelve
|
|
@@ -43,13 +46,35 @@ describe('computeSensorStatus', () => {
|
|
|
43
46
|
expect(result.overall).toBe('NOT_CONFIGURED');
|
|
44
47
|
expect(result.pack).toBeNull();
|
|
45
48
|
});
|
|
49
|
+
it.each([
|
|
50
|
+
['valid manifest', 'READY', () => {
|
|
51
|
+
installLocalBin('eslint');
|
|
52
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'eslint.config.awm.mjs'), 'export default []');
|
|
53
|
+
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
54
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
55
|
+
pack: 'js-ts', sensors: { lint: { cmd: 'npx eslint . --config eslint.config.awm.mjs' } },
|
|
56
|
+
}));
|
|
57
|
+
}],
|
|
58
|
+
['missing tool', 'DEGRADED', () => {
|
|
59
|
+
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
60
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
61
|
+
pack: 'js-ts', sensors: { lint: { cmd: 'npx eslint .' } },
|
|
62
|
+
}));
|
|
63
|
+
}],
|
|
64
|
+
['absent manifest', 'NOT_CONFIGURED', () => { }],
|
|
65
|
+
])('reports %s as %s without dispatching sensor commands', async (_case, overall, setup) => {
|
|
66
|
+
setup();
|
|
67
|
+
await expect((0, status_1.computeSensorStatus)(tmpDir)).resolves.toMatchObject({ overall });
|
|
68
|
+
expect(runCommand).not.toHaveBeenCalled();
|
|
69
|
+
expect(runStructuredCommand).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
46
71
|
// Helper: simulate a tool installed locally (node_modules/.bin/<tool>)
|
|
47
72
|
function installLocalBin(tool) {
|
|
48
73
|
const binDir = path_1.default.join(tmpDir, 'node_modules', '.bin');
|
|
49
74
|
fs_1.default.mkdirSync(binDir, { recursive: true });
|
|
50
75
|
fs_1.default.writeFileSync(path_1.default.join(binDir, tool), '');
|
|
51
76
|
}
|
|
52
|
-
it('
|
|
77
|
+
it('reports READY for an operational legacy manifest when its declared npx tool is installed locally', async () => {
|
|
53
78
|
installLocalBin('tsc');
|
|
54
79
|
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
55
80
|
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
@@ -57,7 +82,7 @@ describe('computeSensorStatus', () => {
|
|
|
57
82
|
sensors: { typecheck: { cmd: 'npx tsc --noEmit', fast: true } }
|
|
58
83
|
}));
|
|
59
84
|
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
60
|
-
expect(result.overall).toBe('
|
|
85
|
+
expect(result.overall).toBe('READY');
|
|
61
86
|
expect(result.pack).toBe('js-ts');
|
|
62
87
|
expect(result.checks.typecheck.ok).toBe(true);
|
|
63
88
|
});
|
|
@@ -84,7 +109,7 @@ describe('computeSensorStatus', () => {
|
|
|
84
109
|
expect(result.checks.lint.ok).toBe(false);
|
|
85
110
|
expect(result.checks.lint.detail).toMatch(/missing config/i);
|
|
86
111
|
});
|
|
87
|
-
it('is
|
|
112
|
+
it('is READY when the declared npx tool and config are present without running a sensor command', async () => {
|
|
88
113
|
installLocalBin('eslint');
|
|
89
114
|
fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'eslint.config.awm.mjs'), 'export default []');
|
|
90
115
|
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
@@ -93,7 +118,10 @@ describe('computeSensorStatus', () => {
|
|
|
93
118
|
sensors: { lint: { cmd: 'npx eslint . --config eslint.config.awm.mjs --format json', fast: true } }
|
|
94
119
|
}));
|
|
95
120
|
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
121
|
+
expect(result.overall).toBe('READY');
|
|
96
122
|
expect(result.checks.lint.ok).toBe(true);
|
|
123
|
+
expect(runCommand).not.toHaveBeenCalled();
|
|
124
|
+
expect(runStructuredCommand).not.toHaveBeenCalled();
|
|
97
125
|
});
|
|
98
126
|
it('returns DEGRADED when a binary is missing', async () => {
|
|
99
127
|
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
@@ -122,7 +150,7 @@ describe('computeSensorStatus', () => {
|
|
|
122
150
|
}));
|
|
123
151
|
installOnPath('semgrep');
|
|
124
152
|
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
125
|
-
expect(result.overall).toBe('
|
|
153
|
+
expect(result.overall).toBe('READY');
|
|
126
154
|
expect(result.checks.security.ok).toBe(true);
|
|
127
155
|
});
|
|
128
156
|
});
|
|
@@ -164,10 +192,10 @@ describe('computeSensorStatus', () => {
|
|
|
164
192
|
expect(result.checks.mutation.ok).toBe(true);
|
|
165
193
|
expect(result.checks.mutation.detail).toBe('disabled');
|
|
166
194
|
});
|
|
167
|
-
it('
|
|
168
|
-
// The manifest
|
|
169
|
-
//
|
|
170
|
-
//
|
|
195
|
+
it('degrades a v2 manifest whose live static compatibility has drifted', async () => {
|
|
196
|
+
// The manifest's initialization evidence remains durable diagnostic context.
|
|
197
|
+
// Status only checks its declared local executable; it never re-runs a project
|
|
198
|
+
// compatibility probe or calls a sensor command.
|
|
171
199
|
const previousHome = process.env.AWM_HOME;
|
|
172
200
|
const home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-home-'));
|
|
173
201
|
try {
|
|
@@ -178,7 +206,7 @@ describe('computeSensorStatus', () => {
|
|
|
178
206
|
const variant = (id, range) => ({
|
|
179
207
|
id, priority: 10, certifiedRange: range,
|
|
180
208
|
requirements: { tool: 'eslint', toolRange: range, runtime: 'node', runtimeRange: '>=0.0.0' },
|
|
181
|
-
assets: ['eslint.config.awm.mjs'], formatter: 'eslint-llm', probe: { kind: '
|
|
209
|
+
assets: ['eslint.config.awm.mjs'], formatter: 'eslint-llm', probe: { kind: 'package-script-present' },
|
|
182
210
|
command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
|
|
183
211
|
});
|
|
184
212
|
fs_1.default.writeFileSync(path_1.default.join(registry, 'sensor-packs', 'js-ts', 'pack.json'), JSON.stringify({
|
|
@@ -188,6 +216,8 @@ describe('computeSensorStatus', () => {
|
|
|
188
216
|
fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'package.json'), JSON.stringify({ devDependencies: { eslint: '^10.0.0' } }));
|
|
189
217
|
fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'node_modules', 'eslint'), { recursive: true });
|
|
190
218
|
fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version: '10.0.0' }));
|
|
219
|
+
fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'node_modules', '.bin'), { recursive: true });
|
|
220
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', '.bin', 'eslint'), '');
|
|
191
221
|
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
192
222
|
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
193
223
|
schemaVersion: 2, pack: 'js-ts', sensors: { lint: {
|
|
@@ -196,15 +226,17 @@ describe('computeSensorStatus', () => {
|
|
|
196
226
|
} },
|
|
197
227
|
}));
|
|
198
228
|
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
199
|
-
expect(result
|
|
200
|
-
expect(result.checks.lint
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
expect(
|
|
207
|
-
expect(
|
|
229
|
+
expect(result).toMatchObject({ overall: 'DEGRADED', checks: { lint: { ok: false } } });
|
|
230
|
+
expect(result.checks.lint.detail).toMatch(/drift|eslint-10/i);
|
|
231
|
+
// Once the installed tool matches the initialized variant, a failed
|
|
232
|
+
// static probe is still evidence of degraded readiness — it is not
|
|
233
|
+
// a runtime probe and cannot be waived as merely inconclusive.
|
|
234
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version: '9.0.0' }));
|
|
235
|
+
const staticProbeFailure = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
236
|
+
expect(staticProbeFailure).toMatchObject({ overall: 'DEGRADED', checks: { lint: { ok: false } } });
|
|
237
|
+
expect(staticProbeFailure.checks.lint.detail).toMatch(/probe-not-matched|unverifiable/i);
|
|
238
|
+
expect(runCommand).not.toHaveBeenCalled();
|
|
239
|
+
expect(runStructuredCommand).not.toHaveBeenCalled();
|
|
208
240
|
}
|
|
209
241
|
finally {
|
|
210
242
|
if (previousHome === undefined)
|
|
@@ -7,20 +7,96 @@ const child_process_1 = require("child_process");
|
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const os_1 = __importDefault(require("os"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
10
11
|
const cliRoot = path_1.default.resolve(__dirname, '../..');
|
|
11
12
|
const bin = path_1.default.join(cliRoot, 'dist', 'src', 'index.js');
|
|
12
|
-
test('preserves
|
|
13
|
+
test('preserves actionable no-manifest preflight JSON when verify-sensors is piped', () => {
|
|
13
14
|
const project = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-preflight-pipe-'));
|
|
14
15
|
try {
|
|
15
|
-
const result = (0, child_process_1.spawnSync)(process.execPath, [bin, 'preflight', '--json'], {
|
|
16
|
+
const result = (0, child_process_1.spawnSync)(process.execPath, [bin, 'preflight', '--verify-sensors', '--json'], {
|
|
16
17
|
cwd: project,
|
|
17
18
|
encoding: 'utf8',
|
|
18
19
|
env: { ...process.env, AWM_HOME: path_1.default.join(project, 'awm-home'), AWM_NO_UPDATE_CHECK: '1' },
|
|
19
20
|
});
|
|
20
21
|
expect(result.status).toBe(1);
|
|
21
|
-
expect(JSON.parse(result.stdout)).toMatchObject({
|
|
22
|
+
expect(JSON.parse(result.stdout)).toMatchObject({
|
|
23
|
+
status: 'not_configured',
|
|
24
|
+
checks: expect.arrayContaining([expect.objectContaining({
|
|
25
|
+
id: 'sensors-execution', ok: false,
|
|
26
|
+
detail: 'sensor verdict was not_certified; no sensor established an empirical pass',
|
|
27
|
+
remedy: expect.stringContaining('awm sensors init'),
|
|
28
|
+
})]),
|
|
29
|
+
});
|
|
22
30
|
}
|
|
23
31
|
finally {
|
|
24
32
|
fs_1.default.rmSync(project, { recursive: true, force: true });
|
|
25
33
|
}
|
|
26
34
|
});
|
|
35
|
+
function writeJson(file, value) {
|
|
36
|
+
fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
|
|
37
|
+
fs_1.default.writeFileSync(file, JSON.stringify(value, null, 2));
|
|
38
|
+
}
|
|
39
|
+
function hashTree(root) {
|
|
40
|
+
const hash = crypto_1.default.createHash('sha256');
|
|
41
|
+
const walk = (dir) => {
|
|
42
|
+
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
43
|
+
const file = path_1.default.join(dir, entry.name);
|
|
44
|
+
hash.update(path_1.default.relative(root, file));
|
|
45
|
+
if (entry.isDirectory())
|
|
46
|
+
walk(file);
|
|
47
|
+
else if (entry.isFile())
|
|
48
|
+
hash.update(fs_1.default.readFileSync(file));
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
walk(root);
|
|
52
|
+
return hash.digest('hex');
|
|
53
|
+
}
|
|
54
|
+
test('verify-sensors degrades parseably for a v2 sensor that exits 2 without findings and does not write the project', () => {
|
|
55
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-preflight-verify-'));
|
|
56
|
+
const project = path_1.default.join(root, 'project');
|
|
57
|
+
const awmHome = path_1.default.join(root, 'awm-home');
|
|
58
|
+
const registry = path_1.default.join(awmHome, 'registries', 'baseline');
|
|
59
|
+
try {
|
|
60
|
+
fs_1.default.mkdirSync(project, { recursive: true });
|
|
61
|
+
fs_1.default.writeFileSync(path_1.default.join(project, 'AGENTS.md'), '# context\n', { encoding: 'utf8', flag: 'w' });
|
|
62
|
+
writeJson(path_1.default.join(project, 'package.json'), { name: 'preflight-verify-fixture', private: true });
|
|
63
|
+
writeJson(path_1.default.join(project, 'node_modules', 'fixture-sensor', 'package.json'), { name: 'fixture-sensor', version: '1.0.0' });
|
|
64
|
+
fs_1.default.writeFileSync(path_1.default.join(project, 'fixture-sensor.config.mjs'), 'export default {};\n');
|
|
65
|
+
fs_1.default.writeFileSync(path_1.default.join(project, 'fixture-sensor.mjs'), 'process.exit(2);\n');
|
|
66
|
+
writeJson(path_1.default.join(registry, 'sensor-packs', 'fixture', 'pack.json'), {
|
|
67
|
+
schemaVersion: 2, name: 'fixture', description: 'preflight exit-2 fixture', detects: ['package.json'],
|
|
68
|
+
sensors: { lint: { applicability: { allFiles: ['package.json'] }, variants: [{
|
|
69
|
+
id: 'fixture-v1', priority: 100,
|
|
70
|
+
requirements: { tool: 'fixture-sensor', toolRange: '>=1 <2', runtime: 'node', runtimeRange: '>=20', configFiles: ['fixture-sensor.config.mjs'] },
|
|
71
|
+
certifiedRange: '>=1 <2', command: { executable: 'node', resolution: 'path', args: ['fixture-sensor.mjs'] },
|
|
72
|
+
assets: ['fixture-sensor.config.mjs'], formatter: 'generic', probe: { kind: 'config-present' },
|
|
73
|
+
}] } },
|
|
74
|
+
coverage: { schemaVersion: 1, classes: {
|
|
75
|
+
'fixture-output': {
|
|
76
|
+
description: 'fixture output', detectors: [{ sensor: 'lint' }],
|
|
77
|
+
remedy: { summary: 'run fixture', command: 'awm sensors init --pack fixture' },
|
|
78
|
+
},
|
|
79
|
+
} },
|
|
80
|
+
});
|
|
81
|
+
writeJson(path_1.default.join(awmHome, 'registries.json'), [{ name: 'baseline', remote: 'fixture' }]);
|
|
82
|
+
writeJson(path_1.default.join(project, '.awm', 'sensors.json'), {
|
|
83
|
+
schemaVersion: 2, pack: 'fixture', packSelection: 'explicit', sensors: { lint: {
|
|
84
|
+
enabled: true, fast: true, variantId: 'fixture-v1', command: { executable: 'node', resolution: 'path', args: ['fixture-sensor.mjs'] }, assets: ['fixture-sensor.config.mjs'],
|
|
85
|
+
initializedCompatibility: { state: 'certified', reason: 'fixture', variantId: 'fixture-v1', toolVersion: '1.0.0', runtimeVersion: process.versions.node, certifiedRange: '>=1 <2', evidence: [] },
|
|
86
|
+
} },
|
|
87
|
+
});
|
|
88
|
+
const before = hashTree(project);
|
|
89
|
+
const result = (0, child_process_1.spawnSync)(process.execPath, [bin, 'preflight', '--verify-sensors', '--json', '--cwd', project], {
|
|
90
|
+
cwd: project, encoding: 'utf8', env: { ...process.env, AWM_HOME: awmHome, AWM_NO_UPDATE_CHECK: '1' },
|
|
91
|
+
});
|
|
92
|
+
expect(result.status).toBe(1);
|
|
93
|
+
expect(JSON.parse(result.stdout)).toMatchObject({
|
|
94
|
+
status: 'degraded',
|
|
95
|
+
checks: expect.arrayContaining([expect.objectContaining({ id: 'sensors-execution', ok: false, detail: expect.stringMatching(/lint.*exit 2/i) })]),
|
|
96
|
+
});
|
|
97
|
+
expect(hashTree(project)).toBe(before);
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
@@ -58,6 +58,13 @@ function runCli(fixture, ...args) {
|
|
|
58
58
|
env: { ...process.env, AWM_HOME: fixture.awmHome, AWM_NO_UPDATE_CHECK: '1' },
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
|
+
function runAwm(fixture, ...args) {
|
|
62
|
+
return (0, child_process_1.spawnSync)(process.execPath, [bin, ...args], {
|
|
63
|
+
cwd: fixture.project,
|
|
64
|
+
encoding: 'utf8',
|
|
65
|
+
env: { ...process.env, AWM_HOME: fixture.awmHome, AWM_NO_UPDATE_CHECK: '1' },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
61
68
|
function json(result) {
|
|
62
69
|
expect(result.status).toBe(0);
|
|
63
70
|
if (!(result.stdout ?? '').trim())
|
|
@@ -81,7 +88,7 @@ test.each(['linux', 'darwin', 'win32'])('keeps injected resolver semantics consi
|
|
|
81
88
|
throw new Error('fixture must be a v2 pack');
|
|
82
89
|
const discovered = (0, discovery_1.discoverProjectEvidence)(fixture.project, parsed.pack, { platform: () => platform });
|
|
83
90
|
const probe = await (0, probe_1.runCompatibilityProbe)({ kind: 'version' }, { cwd: fixture.project, toolExecutable: 'eslint' }, async () => ({
|
|
84
|
-
code: 0, signal: null, timedOut: false, overflowed: false, stdout: 'eslint v10.4.1', stderr: '',
|
|
91
|
+
code: 0, signal: null, timedOut: false, overflowed: false, elapsedMs: 0, stdout: 'eslint v10.4.1', stderr: '',
|
|
85
92
|
}));
|
|
86
93
|
const result = (0, resolve_1.resolveSensorCompatibility)(parsed.pack.sensors.lint, { ...discovered, probe }, { pack: 'js-ts', sensor: 'lint' });
|
|
87
94
|
expect(result).toMatchObject({ state: 'certified', variantId: 'eslint-10', toolVersion: '10.4.1' });
|
|
@@ -100,6 +107,38 @@ testWithNoFollow('compiled binary dispatches coverage and emits parseable JSON o
|
|
|
100
107
|
fs_1.default.rmSync(fixture.root, { recursive: true, force: true });
|
|
101
108
|
}
|
|
102
109
|
});
|
|
110
|
+
testWithNoFollow('compiled sensors run returns nonzero while preserving parseable not_certified JSON', () => {
|
|
111
|
+
const fixture = createFixture();
|
|
112
|
+
try {
|
|
113
|
+
fs_1.default.rmSync(path_1.default.join(fixture.project, '.awm', 'sensors.json'));
|
|
114
|
+
const result = runCli(fixture, 'run', '--fast');
|
|
115
|
+
expect(result.status).toBe(1);
|
|
116
|
+
expect(JSON.parse(result.stdout ?? '')).toMatchObject({ sensors: [], overall: 'not_certified' });
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
fs_1.default.rmSync(fixture.root, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
testWithNoFollow('compiled status reports static READY without writing or executing the project sensor (R6, R6.2)', () => {
|
|
123
|
+
const fixture = createFixture();
|
|
124
|
+
try {
|
|
125
|
+
const localBin = path_1.default.join(fixture.project, 'node_modules', '.bin', 'eslint');
|
|
126
|
+
fs_1.default.mkdirSync(path_1.default.dirname(localBin), { recursive: true });
|
|
127
|
+
fs_1.default.writeFileSync(localBin, 'this fixture must never be executed by status\n');
|
|
128
|
+
fs_1.default.copyFileSync(path_1.default.join(fixture.registryRoot, 'sensor-packs', 'js-ts', 'eslint.fixture.mjs'), path_1.default.join(fixture.project, 'eslint.fixture.mjs'));
|
|
129
|
+
const initialized = runCli(fixture, 'init', '--registry-root', fixture.registryRoot, '--pack', 'js-ts', '--no-configure');
|
|
130
|
+
expect(initialized.status).toBe(0);
|
|
131
|
+
const before = hashTree(fixture.project);
|
|
132
|
+
const result = runAwm(fixture, 'sensors', 'status');
|
|
133
|
+
expect(result.status).toBe(0);
|
|
134
|
+
expect(result.stdout).toContain('READY');
|
|
135
|
+
expect(result.stderr).toBe('');
|
|
136
|
+
expect(hashTree(fixture.project)).toBe(before);
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
fs_1.default.rmSync(fixture.root, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
103
142
|
testWithNoFollow('legacy coverage stays unverified, init migrates explicitly, and version drift is visible (R7.2, R7.8)', () => {
|
|
104
143
|
const fixture = createFixture();
|
|
105
144
|
try {
|
|
@@ -15,7 +15,25 @@ const sensors_1 = require("../../src/commands/sensors");
|
|
|
15
15
|
const ROOT = path_1.default.resolve(__dirname, '../../..');
|
|
16
16
|
const read = (file) => fs_1.default.readFileSync(path_1.default.join(ROOT, file), 'utf8');
|
|
17
17
|
function documentedJson(files) {
|
|
18
|
-
return files.flatMap((file) =>
|
|
18
|
+
return files.flatMap((file) => {
|
|
19
|
+
const source = read(file);
|
|
20
|
+
return Array.from(source.matchAll(/```json\s*\n([\s\S]*?)```/g), (match) => {
|
|
21
|
+
const sectionStart = source.lastIndexOf('###', match.index ?? 0);
|
|
22
|
+
const leadIn = source.slice(Math.max(0, sectionStart), match.index ?? 0);
|
|
23
|
+
return {
|
|
24
|
+
value: JSON.parse(match[1]),
|
|
25
|
+
intentionallyIncomplete: /intentionally incomplete fragment/.test(leadIn),
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function documentedV2Manifests(files) {
|
|
31
|
+
return documentedJson(files)
|
|
32
|
+
.filter((example) => !example.intentionallyIncomplete
|
|
33
|
+
&& typeof example.value === 'object'
|
|
34
|
+
&& example.value !== null
|
|
35
|
+
&& example.value.schemaVersion === 2)
|
|
36
|
+
.map(example => example.value);
|
|
19
37
|
}
|
|
20
38
|
function helpFor(command) {
|
|
21
39
|
const program = new commander_1.Command().name('awm');
|
|
@@ -38,17 +56,27 @@ describe('R3 canonical sensor documentation', () => {
|
|
|
38
56
|
phrases.forEach((phrase) => expect(text).toContain(phrase));
|
|
39
57
|
});
|
|
40
58
|
test('parses every documented v2 sensor manifest example with the production parser', () => {
|
|
41
|
-
const examples =
|
|
42
|
-
.filter((value) => typeof value === 'object' && value !== null && value.schemaVersion === 2);
|
|
59
|
+
const examples = documentedV2Manifests(['docs/configuration.md', 'docs/cli-reference.md']);
|
|
43
60
|
expect(examples.length).toBeGreaterThan(0);
|
|
44
61
|
examples.forEach((example) => expect(() => (0, manifest_1.parseSensorManifest)(example, 'documented example')).not.toThrow());
|
|
45
62
|
});
|
|
46
63
|
test('keeps documented v2 sensor manifests portable across native platforms', () => {
|
|
47
|
-
const examples =
|
|
48
|
-
.filter((value) => typeof value === 'object' && value !== null && value.schemaVersion === 2);
|
|
64
|
+
const examples = documentedV2Manifests(['docs/configuration.md', 'docs/cli-reference.md']);
|
|
49
65
|
expect(examples.length).toBeGreaterThan(0);
|
|
50
66
|
examples.forEach((example) => expect(example).not.toHaveProperty('registryRoot'));
|
|
51
67
|
});
|
|
68
|
+
test('excludes only an explicitly incomplete timeout fragment, not invalid full v2 manifests', () => {
|
|
69
|
+
const examples = documentedJson(['docs/configuration.md']);
|
|
70
|
+
const incomplete = examples.find(example => example.intentionallyIncomplete);
|
|
71
|
+
const complete = documentedV2Manifests(['docs/configuration.md']);
|
|
72
|
+
expect(incomplete?.value).toMatchObject({ schemaVersion: 2, sensors: { test: { timeout: 600000 } } });
|
|
73
|
+
expect(() => (0, manifest_1.parseSensorManifest)(incomplete?.value, 'incomplete timeout fragment')).toThrow();
|
|
74
|
+
expect(complete).toHaveLength(1);
|
|
75
|
+
expect(() => (0, manifest_1.parseSensorManifest)(complete[0], 'complete documented manifest')).not.toThrow();
|
|
76
|
+
expect(() => (0, manifest_1.parseSensorManifest)({
|
|
77
|
+
schemaVersion: 2, pack: 'js-ts', sensors: { test: { enabled: true, variantId: 'npm-script' } },
|
|
78
|
+
}, 'invalid full manifest')).toThrow();
|
|
79
|
+
});
|
|
52
80
|
test('documents exact Commander flags', () => {
|
|
53
81
|
expect(helpFor('sensors coverage')).toContain('--min <count>');
|
|
54
82
|
expect(helpFor('ledger add')).toContain('--defect-class <id>');
|
|
@@ -24,6 +24,31 @@ const sensor_support_matrix_1 = require("../../scripts/sensor-support-matrix");
|
|
|
24
24
|
const SENSOR_FIXTURE_REGISTRY = path_1.default.join(__dirname, '..', 'fixtures', 'sensor-support-matrix', 'registry');
|
|
25
25
|
const CI_WORKFLOW_PATH = path_1.default.resolve(__dirname, '../../..', '.github', 'workflows', 'ci.yml');
|
|
26
26
|
describe('docs/support-matrix.md refleja el codigo', () => {
|
|
27
|
+
it('documents the bounded, empirical sensor gate contract (R3-R7, R10)', () => {
|
|
28
|
+
const root = path_1.default.resolve(__dirname, '../../..');
|
|
29
|
+
const cliReference = fs_1.default.readFileSync(path_1.default.join(root, 'docs', 'cli-reference.md'), 'utf8');
|
|
30
|
+
const configuration = fs_1.default.readFileSync(path_1.default.join(root, 'docs', 'configuration.md'), 'utf8');
|
|
31
|
+
const acceptance = fs_1.default.readFileSync(path_1.default.join(root, 'docs', 'testing', 'core-acceptance.md'), 'utf8');
|
|
32
|
+
const osMatrix = fs_1.default.readFileSync(path_1.default.join(root, 'docs', 'testing', 'os-matrix.md'), 'utf8');
|
|
33
|
+
for (const expected of [
|
|
34
|
+
'`execution.timeoutMs`', '`timeoutSource`', '`elapsedMs`',
|
|
35
|
+
'`requestedScope`', '`effectiveScope`', '`files`', '`scopeReason`',
|
|
36
|
+
'`project` → `pack` → `fallback`', '10,000 ms', '120,000 ms',
|
|
37
|
+
'`pass`', '`fail`', '`not_certified`', '`skipped`',
|
|
38
|
+
'`awm preflight --verify-sensors`', 'read-only', 'READY', 'not a health or certification claim',
|
|
39
|
+
])
|
|
40
|
+
expect(cliReference + configuration + acceptance).toContain(expected);
|
|
41
|
+
expect(acceptance).toContain('legacy manifest');
|
|
42
|
+
expect(acceptance).toContain('v2 manifest without new fields');
|
|
43
|
+
expect(acceptance).toContain('supported, unsupported, empty, and Git-error');
|
|
44
|
+
expect(acceptance).toContain('project, pack, and fallback');
|
|
45
|
+
expect(cliReference).toContain('`not_certified` with an empty sensor list');
|
|
46
|
+
expect(cliReference).toContain('`awm sensors init`');
|
|
47
|
+
expect(acceptance).toContain('established (for example `not_certified` and an empty list)');
|
|
48
|
+
expect(acceptance).toMatch(/must not fabricate a\s+sensor name, timeout, source, or elapsed time/);
|
|
49
|
+
expect(osMatrix).toContain('Ubuntu, macOS, and native Windows');
|
|
50
|
+
expect(osMatrix).toContain('shell-free');
|
|
51
|
+
});
|
|
27
52
|
it('el bloque generado esta al dia', () => {
|
|
28
53
|
const doc = fs_1.default.readFileSync(support_matrix_1.DOC_PATH, 'utf-8');
|
|
29
54
|
const expected = (0, support_matrix_1.spliceGenerated)(doc, (0, support_matrix_1.renderProviderTables)());
|