agentic-workflow-manager 6.4.2 → 6.5.1
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/sensors/coverage/contract.js +164 -0
- package/dist/src/commands/sensors/coverage/evaluate.js +107 -0
- package/dist/src/commands/sensors/coverage/evidence.js +118 -0
- package/dist/src/commands/sensors/coverage/index.js +31 -0
- package/dist/src/commands/sensors/coverage/render.js +152 -0
- package/dist/src/commands/sensors/coverage/resolve.js +126 -0
- package/dist/src/commands/sensors/index.js +16 -0
- package/dist/tests/commands/sensors/coverage/contract.test.js +114 -0
- package/dist/tests/commands/sensors/coverage/evaluate.test.js +82 -0
- package/dist/tests/commands/sensors/coverage/evidence.test.js +251 -0
- package/dist/tests/commands/sensors/coverage/index.test.js +30 -0
- package/dist/tests/commands/sensors/coverage/render.test.js +149 -0
- package/dist/tests/commands/sensors/coverage/resolve.test.js +130 -0
- package/dist/tests/commands/sensors/index.test.js +52 -1
- package/dist/tests/commands/sensors/router.test.js +2 -1
- package/dist/tests/integration/codex-provider-isolated.test.js +15 -1
- package/dist/tests/integration/copilot-init-isolated.test.js +1 -0
- package/dist/tests/integration/sensor-coverage-provider-evidence.test.js +92 -0
- package/dist/tests/integration/sensor-coverage.e2e.test.js +113 -0
- package/dist/tests/structural/jest-environment-is-isolated.test.js +14 -0
- package/dist/tests/structural/sensor-configs-are-present.test.js +10 -0
- package/package.json +1 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const render_1 = require("../../../../src/commands/sensors/coverage/render");
|
|
4
|
+
const report = {
|
|
5
|
+
schemaVersion: 1, pack: 'js-ts', registry: 'baseline', overall: 'gaps',
|
|
6
|
+
static: { status: 'gaps', reason: null, classes: [
|
|
7
|
+
{ id: 'formatting', description: 'Formatting', status: 'missing',
|
|
8
|
+
detectors: [{ sensor: 'format', status: 'missing', evidence: [] }],
|
|
9
|
+
remedy: { summary: 'Add formatter', command: 'npm i -D prettier' } },
|
|
10
|
+
{ id: 'style', description: 'Style', status: 'unverifiable',
|
|
11
|
+
detectors: [{ sensor: 'lint', status: 'unverifiable', evidence: [{ kind: 'command', status: 'custom' }] }],
|
|
12
|
+
remedy: { summary: 'Declare evidence', command: 'awm sensors init' } },
|
|
13
|
+
] },
|
|
14
|
+
};
|
|
15
|
+
test('human output shows every non-green class, remedy and totals without raw evidence (R2.8)', () => {
|
|
16
|
+
const human = (0, render_1.renderCoverageHuman)(report);
|
|
17
|
+
expect(human).toBe([
|
|
18
|
+
'Sensor coverage', 'Pack: js-ts', 'Registry: baseline', 'Overall: gaps', '',
|
|
19
|
+
'missing formatting — Formatting', ' detector: format (missing)', ' remedy: Add formatter', ' command: npm i -D prettier',
|
|
20
|
+
'unverifiable style — Style', ' detector: lint (unverifiable)', ' remedy: Declare evidence', ' command: awm sensors init', '',
|
|
21
|
+
'Summary: 0 covered, 1 missing, 1 unverifiable', '',
|
|
22
|
+
].join('\n'));
|
|
23
|
+
expect(human).not.toContain('commandIncludes');
|
|
24
|
+
expect(human).not.toContain('custom');
|
|
25
|
+
});
|
|
26
|
+
test('json is the exact versioned envelope and ends in newline (R2.8, R2.14)', () => {
|
|
27
|
+
expect((0, render_1.renderCoverageJson)(report)).toBe(`${JSON.stringify(report, null, 2)}\n`);
|
|
28
|
+
});
|
|
29
|
+
test('keeps the R2 static shape when an optional empirical section is added (R2.14)', () => {
|
|
30
|
+
const extended = { ...report, empirical: { status: 'no_evidence' } };
|
|
31
|
+
const parsed = JSON.parse((0, render_1.renderCoverageJson)(extended));
|
|
32
|
+
expect(parsed.static).toEqual(report.static);
|
|
33
|
+
expect(parsed.empirical).toEqual({ status: 'no_evidence' });
|
|
34
|
+
expect(parsed.schemaVersion).toBe(1);
|
|
35
|
+
});
|
|
36
|
+
test('not_configured names the remedy and no_reference stays distinct (R2.6)', () => {
|
|
37
|
+
const notConfigured = { schemaVersion: 1, pack: null, registry: null, overall: 'inconclusive',
|
|
38
|
+
static: { status: 'inconclusive', reason: 'not_configured', classes: [] } };
|
|
39
|
+
expect((0, render_1.renderCoverageHuman)(notConfigured)).toContain('Run: awm sensors init');
|
|
40
|
+
expect((0, render_1.renderCoverageHuman)({ ...notConfigured, pack: 'legacy', registry: 'baseline', static: { ...notConfigured.static, reason: 'no_reference' } }))
|
|
41
|
+
.toContain('No coverage reference');
|
|
42
|
+
});
|
|
43
|
+
test('human output never renders structured detector evidence', () => {
|
|
44
|
+
const evidenceReport = {
|
|
45
|
+
...report,
|
|
46
|
+
static: {
|
|
47
|
+
...report.static,
|
|
48
|
+
classes: [{ ...report.static.classes[0], detectors: [{
|
|
49
|
+
sensor: 'format', status: 'missing',
|
|
50
|
+
evidence: [
|
|
51
|
+
{ kind: 'command', status: 'matched' },
|
|
52
|
+
{ kind: 'file', path: '.prettierrc', status: 'matched' },
|
|
53
|
+
{ kind: 'marker', path: '.prettierrc', ordinal: 1, status: 'missing' },
|
|
54
|
+
],
|
|
55
|
+
}] }],
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
const human = (0, render_1.renderCoverageHuman)(evidenceReport);
|
|
59
|
+
expect(human).not.toContain('.prettierrc');
|
|
60
|
+
expect(human).not.toContain('marker');
|
|
61
|
+
expect(human).not.toContain('matched');
|
|
62
|
+
});
|
|
63
|
+
test('human output removes OSC controls from pack-provided text while retaining printable text', () => {
|
|
64
|
+
const osc8Open = '\x1B]8;;https://attacker.invalid\x07';
|
|
65
|
+
const osc8Close = '\x1B]8;;\x07';
|
|
66
|
+
const hostile = {
|
|
67
|
+
...report,
|
|
68
|
+
static: {
|
|
69
|
+
...report.static,
|
|
70
|
+
classes: [{
|
|
71
|
+
...report.static.classes[0],
|
|
72
|
+
description: `${osc8Open}Formatting${osc8Close}`,
|
|
73
|
+
remedy: { summary: `Add ${osc8Open}formatter${osc8Close}`, command: 'npm i -D prettier' },
|
|
74
|
+
}],
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
const human = (0, render_1.renderCoverageHuman)(hostile);
|
|
78
|
+
expect(human).toContain('missing formatting — Formatting');
|
|
79
|
+
expect(human).toContain(' remedy: Add formatter');
|
|
80
|
+
expect(human.replace(/\n/g, '')).not.toMatch(/[\u0000-\u001F\u007F-\u009F]/);
|
|
81
|
+
});
|
|
82
|
+
test.each([
|
|
83
|
+
['non-string pack', { ...report, pack: 42 }],
|
|
84
|
+
['malformed class detectors', { ...report, static: { ...report.static, classes: [{ ...report.static.classes[0], detectors: null }] } }],
|
|
85
|
+
['unknown static reason', { ...report, static: { ...report.static, reason: 'other' } }],
|
|
86
|
+
['unknown top-level field', { ...report, extra: true }],
|
|
87
|
+
['unknown class field', { ...report, static: { ...report.static, classes: [{ ...report.static.classes[0], extra: true }] } }],
|
|
88
|
+
['unknown evidence field', { ...report, static: { ...report.static, classes: [{ ...report.static.classes[0], detectors: [{
|
|
89
|
+
...report.static.classes[0].detectors[0], evidence: [{ kind: 'command', status: 'matched', command: 'secret' }],
|
|
90
|
+
}] }] } }],
|
|
91
|
+
['malformed marker evidence', { ...report, static: { ...report.static, classes: [{ ...report.static.classes[0], detectors: [{
|
|
92
|
+
...report.static.classes[0].detectors[0], evidence: [{ kind: 'marker', path: '.prettierrc', ordinal: 0, status: 'matched' }],
|
|
93
|
+
}] }] } }],
|
|
94
|
+
['mismatched static and overall status', { ...report, static: { ...report.static, status: 'covered' } }],
|
|
95
|
+
['covered class whose detectors are all missing', {
|
|
96
|
+
...report,
|
|
97
|
+
static: { ...report.static, classes: [{ ...report.static.classes[0], status: 'covered' }] },
|
|
98
|
+
}],
|
|
99
|
+
['missing class with an unverifiable detector and no covered detector', {
|
|
100
|
+
...report,
|
|
101
|
+
static: { ...report.static, classes: [{
|
|
102
|
+
...report.static.classes[0],
|
|
103
|
+
detectors: [{ ...report.static.classes[0].detectors[0], status: 'unverifiable' }],
|
|
104
|
+
}] },
|
|
105
|
+
}],
|
|
106
|
+
['gaps overall when every class is covered', {
|
|
107
|
+
...report,
|
|
108
|
+
static: {
|
|
109
|
+
...report.static,
|
|
110
|
+
classes: report.static.classes.map((coverageClass) => ({
|
|
111
|
+
...coverageClass,
|
|
112
|
+
status: 'covered',
|
|
113
|
+
detectors: coverageClass.detectors.map((detector) => ({ ...detector, status: 'covered' })),
|
|
114
|
+
})),
|
|
115
|
+
},
|
|
116
|
+
}],
|
|
117
|
+
['not_configured with resolved pack and registry', {
|
|
118
|
+
schemaVersion: 1, pack: 'js-ts', registry: 'baseline', overall: 'inconclusive',
|
|
119
|
+
static: { status: 'inconclusive', reason: 'not_configured', classes: [] },
|
|
120
|
+
}],
|
|
121
|
+
['not_configured with classes', {
|
|
122
|
+
schemaVersion: 1, pack: null, registry: null, overall: 'inconclusive',
|
|
123
|
+
static: { status: 'inconclusive', reason: 'not_configured', classes: [report.static.classes[0]] },
|
|
124
|
+
}],
|
|
125
|
+
['no_reference without resolved registry', {
|
|
126
|
+
schemaVersion: 1, pack: 'js-ts', registry: null, overall: 'inconclusive',
|
|
127
|
+
static: { status: 'inconclusive', reason: 'no_reference', classes: [] },
|
|
128
|
+
}],
|
|
129
|
+
['no_reference with classes', {
|
|
130
|
+
schemaVersion: 1, pack: 'js-ts', registry: 'baseline', overall: 'inconclusive',
|
|
131
|
+
static: { status: 'inconclusive', reason: 'no_reference', classes: [report.static.classes[0]] },
|
|
132
|
+
}],
|
|
133
|
+
['normal coverage without a pack', { ...report, pack: null }],
|
|
134
|
+
['normal coverage without classes', { ...report, static: { ...report.static, classes: [] } }],
|
|
135
|
+
['blank pack', { ...report, pack: ' ' }],
|
|
136
|
+
['blank registry', { ...report, registry: '' }],
|
|
137
|
+
['blank class id', { ...report, static: { ...report.static, classes: [{ ...report.static.classes[0], id: ' ' }] } }],
|
|
138
|
+
['blank class description', { ...report, static: { ...report.static, classes: [{ ...report.static.classes[0], description: '' }] } }],
|
|
139
|
+
['blank remedy summary', { ...report, static: { ...report.static, classes: [{ ...report.static.classes[0], remedy: { ...report.static.classes[0].remedy, summary: ' ' } }] } }],
|
|
140
|
+
['blank remedy command', { ...report, static: { ...report.static, classes: [{ ...report.static.classes[0], remedy: { ...report.static.classes[0].remedy, command: '' } }] } }],
|
|
141
|
+
['empty class detectors', { ...report, static: { ...report.static, classes: [{ ...report.static.classes[0], detectors: [] }] } }],
|
|
142
|
+
['blank detector sensor', { ...report, static: { ...report.static, classes: [{ ...report.static.classes[0], detectors: [{ ...report.static.classes[0].detectors[0], sensor: ' ' }] }] } }],
|
|
143
|
+
['duplicate class ids', { ...report, static: { ...report.static, classes: [report.static.classes[0], { ...report.static.classes[1], id: report.static.classes[0].id }] } }],
|
|
144
|
+
['unsorted class ids', { ...report, static: { ...report.static, classes: [...report.static.classes].reverse() } }],
|
|
145
|
+
])('renderers reject a malformed envelope with %s before emitting or dereferencing fields', (_case, malformed) => {
|
|
146
|
+
for (const render of [render_1.renderCoverageJson, render_1.renderCoverageHuman]) {
|
|
147
|
+
expect(() => render(malformed)).toThrow(/^renderCoverage(?:Json|Human): invalid report/);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
const tmp_1 = require("../../../support/tmp");
|
|
9
|
+
const resolve_1 = require("../../../../src/commands/sensors/coverage/resolve");
|
|
10
|
+
let root;
|
|
11
|
+
let awmHome;
|
|
12
|
+
let project;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
root = (0, tmp_1.mkCanonicalTmpDir)('awm-coverage-resolve-');
|
|
15
|
+
awmHome = path_1.default.join(root, 'home');
|
|
16
|
+
project = path_1.default.join(root, 'project');
|
|
17
|
+
fs_1.default.mkdirSync(path_1.default.join(project, '.awm'), { recursive: true });
|
|
18
|
+
process.env.AWM_HOME = awmHome;
|
|
19
|
+
});
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
delete process.env.AWM_HOME;
|
|
22
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
const configure = (names) => {
|
|
25
|
+
fs_1.default.mkdirSync(awmHome, { recursive: true });
|
|
26
|
+
fs_1.default.writeFileSync(path_1.default.join(awmHome, 'registries.json'), JSON.stringify(names.map((name) => ({ name, remote: 'fixture' }))));
|
|
27
|
+
};
|
|
28
|
+
const writeManifest = (body) => fs_1.default.writeFileSync(path_1.default.join(project, '.awm', 'sensors.json'), typeof body === 'string' || Buffer.isBuffer(body) ? body : JSON.stringify(body));
|
|
29
|
+
const coverage = { schemaVersion: 1, classes: {
|
|
30
|
+
formatting: { description: 'Formatting', detectors: [{ sensor: 'format' }], remedy: { summary: 'Add formatter', command: 'npm i -D prettier' } },
|
|
31
|
+
} };
|
|
32
|
+
const noFollowUnavailableOnNativeWindows = process.platform === 'win32'
|
|
33
|
+
&& typeof fs_1.default.constants.O_NOFOLLOW !== 'number';
|
|
34
|
+
const safetyError = /platform cannot guarantee no symlink dereference/;
|
|
35
|
+
const writePack = (registry, pack, body) => {
|
|
36
|
+
const dir = path_1.default.join(awmHome, 'registries', registry, 'sensor-packs', pack);
|
|
37
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
38
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'pack.json'), typeof body === 'string' || Buffer.isBuffer(body) ? body : JSON.stringify(body));
|
|
39
|
+
};
|
|
40
|
+
test('no manifest returns not_configured without reading registries', () => {
|
|
41
|
+
fs_1.default.rmSync(path_1.default.join(project, '.awm', 'sensors.json'), { force: true });
|
|
42
|
+
fs_1.default.mkdirSync(awmHome, { recursive: true });
|
|
43
|
+
fs_1.default.writeFileSync(path_1.default.join(awmHome, 'registries.json'), '{malformed');
|
|
44
|
+
expect((0, resolve_1.resolveCoverageInputs)(project)).toEqual({ kind: 'not_configured' });
|
|
45
|
+
});
|
|
46
|
+
test('selects the first configured registry containing the exact pack', () => {
|
|
47
|
+
writeManifest({ pack: 'js-ts', sensors: {} });
|
|
48
|
+
configure(['first', 'second']);
|
|
49
|
+
writePack('first', 'generic', { name: 'generic', sensors: {} });
|
|
50
|
+
writePack('second', 'js-ts', { name: 'js-ts', sensors: {}, coverage });
|
|
51
|
+
if (noFollowUnavailableOnNativeWindows) {
|
|
52
|
+
expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(safetyError);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
expect((0, resolve_1.resolveCoverageInputs)(project)).toMatchObject({ kind: 'ready', pack: 'js-ts', registry: 'second' });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
test('registry ordering chooses the earlier exact pack when both registries contain it', () => {
|
|
59
|
+
writeManifest({ pack: 'js-ts', sensors: {} });
|
|
60
|
+
configure(['first', 'second']);
|
|
61
|
+
writePack('first', 'js-ts', { name: 'js-ts', sensors: {}, coverage });
|
|
62
|
+
writePack('second', 'js-ts', { name: 'js-ts', sensors: {}, coverage });
|
|
63
|
+
if (noFollowUnavailableOnNativeWindows) {
|
|
64
|
+
expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(safetyError);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
expect((0, resolve_1.resolveCoverageInputs)(project)).toMatchObject({ kind: 'ready', registry: 'first' });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
test('old pack without coverage is no_reference, not covered', () => {
|
|
71
|
+
writeManifest({ pack: 'js-ts', sensors: {} });
|
|
72
|
+
configure(['baseline']);
|
|
73
|
+
writePack('baseline', 'js-ts', { name: 'js-ts', sensors: {} });
|
|
74
|
+
if (noFollowUnavailableOnNativeWindows) {
|
|
75
|
+
expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(safetyError);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
expect((0, resolve_1.resolveCoverageInputs)(project)).toMatchObject({ kind: 'no_reference', pack: 'js-ts', registry: 'baseline' });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
test('native Windows without no-follow support fails closed before resolving normal coverage inputs', () => {
|
|
82
|
+
writeManifest({ pack: 'js-ts', sensors: {} });
|
|
83
|
+
configure(['baseline']);
|
|
84
|
+
writePack('baseline', 'js-ts', { name: 'js-ts', sensors: {}, coverage });
|
|
85
|
+
if (noFollowUnavailableOnNativeWindows) {
|
|
86
|
+
expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(safetyError);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
expect((0, resolve_1.resolveCoverageInputs)(project)).toMatchObject({ kind: 'ready' });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
test.each([
|
|
93
|
+
['manifest-malformed', '{broken', /Invalid JSON.*sensors\.json/],
|
|
94
|
+
['manifest-oversize', Buffer.alloc(1024 * 1024 + 1), /sensors\.json.*exceeds 1 MiB/],
|
|
95
|
+
])('rejects %s', (_name, body, expected) => {
|
|
96
|
+
writeManifest(body);
|
|
97
|
+
expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(noFollowUnavailableOnNativeWindows && _name !== 'manifest-oversize' ? safetyError : expected);
|
|
98
|
+
});
|
|
99
|
+
test.each([
|
|
100
|
+
['pack-malformed', '{broken', /Invalid JSON.*pack\.json/],
|
|
101
|
+
['pack-oversize', Buffer.alloc(1024 * 1024 + 1), /pack\.json.*exceeds 1 MiB/],
|
|
102
|
+
])('rejects %s', (_name, body, expected) => {
|
|
103
|
+
writeManifest({ pack: 'js-ts', sensors: {} });
|
|
104
|
+
configure(['baseline']);
|
|
105
|
+
writePack('baseline', 'js-ts', body);
|
|
106
|
+
expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(noFollowUnavailableOnNativeWindows ? safetyError : expected);
|
|
107
|
+
});
|
|
108
|
+
test('rejects a symlinked manifest without dereferencing it', () => {
|
|
109
|
+
const manifest = path_1.default.join(project, '.awm', 'sensors.json');
|
|
110
|
+
const target = path_1.default.join(root, 'manifest.json');
|
|
111
|
+
fs_1.default.writeFileSync(target, JSON.stringify({ pack: 'js-ts', sensors: {} }));
|
|
112
|
+
fs_1.default.symlinkSync(target, manifest);
|
|
113
|
+
expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(/sensors\.json.*regular file/);
|
|
114
|
+
});
|
|
115
|
+
test('rejects a dangling symlinked manifest without reporting not_configured', () => {
|
|
116
|
+
const manifest = path_1.default.join(project, '.awm', 'sensors.json');
|
|
117
|
+
fs_1.default.symlinkSync(path_1.default.join(root, 'missing-manifest.json'), manifest);
|
|
118
|
+
expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(/sensors\.json.*regular file/);
|
|
119
|
+
});
|
|
120
|
+
test('rejects a JSON object that is not a valid pack', () => {
|
|
121
|
+
writeManifest({ pack: 'js-ts', sensors: {} });
|
|
122
|
+
configure(['baseline']);
|
|
123
|
+
writePack('baseline', 'js-ts', { coverage });
|
|
124
|
+
expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(noFollowUnavailableOnNativeWindows ? safetyError : /Invalid pack.*name/);
|
|
125
|
+
});
|
|
126
|
+
test('rejects registry names that are not safe path components', () => {
|
|
127
|
+
writeManifest({ pack: 'js-ts', sensors: {} });
|
|
128
|
+
configure(['']);
|
|
129
|
+
expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(noFollowUnavailableOnNativeWindows ? safetyError : /Invalid registry name/);
|
|
130
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
jest.mock('@clack/prompts', () => ({ log: { success: jest.fn(), info: jest.fn() } }));
|
|
3
|
+
jest.mock('@clack/prompts', () => ({ log: { success: jest.fn(), info: jest.fn(), error: 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() }));
|
|
6
6
|
jest.mock('../../../src/commands/sensors/init', () => ({ initSensors: jest.fn() }));
|
|
@@ -8,6 +8,12 @@ jest.mock('../../../src/commands/sensors/status', () => ({ computeSensorStatus:
|
|
|
8
8
|
jest.mock('../../../src/commands/sensors/install', () => ({ installSensorHook: jest.fn() }));
|
|
9
9
|
jest.mock('../../../src/commands/sensors/baseline', () => ({ buildBaseline: jest.fn(), writeBaseline: jest.fn() }));
|
|
10
10
|
jest.mock('../../../src/core/registries', () => ({ capabilityRoot: jest.fn(() => '/mock/registry') }));
|
|
11
|
+
jest.mock('../../../src/commands/sensors/coverage', () => ({ runCoverage: jest.fn() }));
|
|
12
|
+
jest.mock('../../../src/commands/sensors/coverage/render', () => ({ renderCoverageHuman: jest.fn(), renderCoverageJson: jest.fn() }));
|
|
13
|
+
const commander_1 = require("commander");
|
|
14
|
+
const prompts_1 = require("@clack/prompts");
|
|
15
|
+
const coverage_1 = require("../../../src/commands/sensors/coverage");
|
|
16
|
+
const render_1 = require("../../../src/commands/sensors/coverage/render");
|
|
11
17
|
const index_1 = require("../../../src/commands/sensors/index");
|
|
12
18
|
describe('exitCodeFor — sensor run verdict → exit code', () => {
|
|
13
19
|
const base = (overall) => ({ sensors: [], overall });
|
|
@@ -16,3 +22,48 @@ describe('exitCodeFor — sensor run verdict → exit code', () => {
|
|
|
16
22
|
it('not_certified → 0 (signal is in overall, not exit code)', () => expect((0, index_1.exitCodeFor)(base('not_certified'))).toBe(0));
|
|
17
23
|
it('fail → 1', () => expect((0, index_1.exitCodeFor)(base('fail'))).toBe(1));
|
|
18
24
|
});
|
|
25
|
+
describe('sensors coverage Commander wiring', () => {
|
|
26
|
+
const report = { schemaVersion: 1, pack: 'js-ts', registry: 'baseline', overall: 'gaps',
|
|
27
|
+
static: { status: 'gaps', reason: null, classes: [] } };
|
|
28
|
+
const stdoutWrite = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
29
|
+
const processExit = jest.spyOn(process, 'exit').mockImplementation((() => undefined));
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
jest.clearAllMocks();
|
|
32
|
+
coverage_1.runCoverage.mockReturnValue(report);
|
|
33
|
+
render_1.renderCoverageHuman.mockReturnValue('human\n');
|
|
34
|
+
render_1.renderCoverageJson.mockReturnValue('json\n');
|
|
35
|
+
});
|
|
36
|
+
afterAll(() => {
|
|
37
|
+
stdoutWrite.mockRestore();
|
|
38
|
+
processExit.mockRestore();
|
|
39
|
+
});
|
|
40
|
+
const programWithSensors = () => {
|
|
41
|
+
const program = new commander_1.Command();
|
|
42
|
+
program.exitOverride();
|
|
43
|
+
(0, index_1.registerSensorsCommand)(program);
|
|
44
|
+
return program;
|
|
45
|
+
};
|
|
46
|
+
it('registers sensors coverage with --json and emits human output by default (R2.8)', async () => {
|
|
47
|
+
const program = programWithSensors();
|
|
48
|
+
const sensors = program.commands.find((command) => command.name() === 'sensors');
|
|
49
|
+
const coverage = sensors.commands.find((command) => command.name() === 'coverage');
|
|
50
|
+
expect(coverage.options.some((option) => option.long === '--json')).toBe(true);
|
|
51
|
+
await program.parseAsync(['node', 'awm', 'sensors', 'coverage']);
|
|
52
|
+
expect(render_1.renderCoverageHuman).toHaveBeenCalledWith(report);
|
|
53
|
+
expect(stdoutWrite).toHaveBeenCalledWith('human\n');
|
|
54
|
+
});
|
|
55
|
+
it('emits JSON for --json and does not exit for gaps or inconclusive (R2.9)', async () => {
|
|
56
|
+
for (const overall of ['gaps', 'inconclusive']) {
|
|
57
|
+
coverage_1.runCoverage.mockReturnValue({ ...report, overall, static: { ...report.static, status: overall } });
|
|
58
|
+
await programWithSensors().parseAsync(['node', 'awm', 'sensors', 'coverage', '--json']);
|
|
59
|
+
}
|
|
60
|
+
expect(render_1.renderCoverageJson).toHaveBeenCalledTimes(2);
|
|
61
|
+
expect(processExit).not.toHaveBeenCalled();
|
|
62
|
+
});
|
|
63
|
+
it('prints an actionable contract error and exits 1 (R2.7)', async () => {
|
|
64
|
+
coverage_1.runCoverage.mockImplementation(() => { throw new Error('Invalid coverage contract at pack.json: schemaVersion expected 1'); });
|
|
65
|
+
await programWithSensors().parseAsync(['node', 'awm', 'sensors', 'coverage']);
|
|
66
|
+
expect(prompts_1.log.error).toHaveBeenCalledWith(expect.stringContaining('schemaVersion'));
|
|
67
|
+
expect(processExit).toHaveBeenCalledWith(1);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -9,7 +9,7 @@ jest.mock('../../../src/commands/sensors/install', () => ({ installSensorHook: j
|
|
|
9
9
|
const commander_1 = require("commander");
|
|
10
10
|
const index_1 = require("../../../src/commands/sensors/index");
|
|
11
11
|
describe('registerSensorsCommand', () => {
|
|
12
|
-
it('
|
|
12
|
+
it('keeps existing sensor subcommands and adds coverage', () => {
|
|
13
13
|
const program = new commander_1.Command();
|
|
14
14
|
(0, index_1.registerSensorsCommand)(program);
|
|
15
15
|
const cmd = program.commands.find(c => c.name() === 'sensors');
|
|
@@ -19,5 +19,6 @@ describe('registerSensorsCommand', () => {
|
|
|
19
19
|
expect(subNames).toContain('init');
|
|
20
20
|
expect(subNames).toContain('status');
|
|
21
21
|
expect(subNames).toContain('install');
|
|
22
|
+
expect(subNames).toContain('coverage');
|
|
22
23
|
});
|
|
23
24
|
});
|
|
@@ -223,10 +223,24 @@ describe('codex provider — isolated home E2E (Task 9)', () => {
|
|
|
223
223
|
it('doctor reports the isolated Codex install as supported/healthy, not against the real ~/.awm', () => {
|
|
224
224
|
seedPublicRegistryFixture(path_1.default.join(tmpHome, '.awm/registries/baseline'));
|
|
225
225
|
writePrefs(prefsWith(['claude-code', 'codex']));
|
|
226
|
+
const childProcess = require('child_process');
|
|
227
|
+
const realExecFileSync = childProcess.execFileSync;
|
|
228
|
+
const execSpy = jest.spyOn(childProcess, 'execFileSync').mockImplementation(((command, ...args) => {
|
|
229
|
+
if (command === 'codex') {
|
|
230
|
+
throw Object.assign(new Error('spawnSync codex ENOENT'), { code: 'ENOENT' });
|
|
231
|
+
}
|
|
232
|
+
return realExecFileSync(command, ...args);
|
|
233
|
+
}));
|
|
234
|
+
jest.resetModules();
|
|
226
235
|
const { runDoctor } = require('../../src/commands/doctor');
|
|
227
236
|
// R20: doctor resolves a single explicit provider without needing every
|
|
228
237
|
// enabled agent to be independently initialized first.
|
|
229
|
-
|
|
238
|
+
try {
|
|
239
|
+
runDoctor({ cwd: tmpWork, json: true, agent: 'codex' });
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
execSpy.mockRestore();
|
|
243
|
+
}
|
|
230
244
|
const report = JSON.parse(stdout());
|
|
231
245
|
expect(report.providers.map((p) => p.id)).toEqual(['codex']);
|
|
232
246
|
const codexReport = report.providers[0];
|
|
@@ -49,6 +49,7 @@ describe('copilot provider — isolated home E2E (devCore global-scope guard reg
|
|
|
49
49
|
originalAwmHome = process.env.AWM_HOME;
|
|
50
50
|
process.env.HOME = tmpHome;
|
|
51
51
|
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
52
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpWork, 'package.json'), JSON.stringify({ name: 'copilot-e2e-fixture' }));
|
|
52
53
|
jest.resetModules();
|
|
53
54
|
writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
54
55
|
});
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const evidenceDir = path_1.default.resolve(__dirname, '../../../docs/research/r2/evidence');
|
|
11
|
+
const runner = path_1.default.resolve(__dirname, '../../../docs/research/r2/provider-run.mjs');
|
|
12
|
+
const providers = ['claude-code', 'codex'];
|
|
13
|
+
function read(provider) {
|
|
14
|
+
const file = path_1.default.join(evidenceDir, `${provider}.json`);
|
|
15
|
+
return fs_1.default.existsSync(file) ? JSON.parse(fs_1.default.readFileSync(file, 'utf8')) : null;
|
|
16
|
+
}
|
|
17
|
+
function expectEvidenceIntegrity(provider, value) {
|
|
18
|
+
expect(value).toMatchObject({ schema: 1, provider });
|
|
19
|
+
expect(['pass', 'partial', 'fail']).toContain(value.result);
|
|
20
|
+
// This identifies the evidence's source without assuming CI has fetched that object.
|
|
21
|
+
expect(value.sourceHead).toMatch(/^[0-9a-f]{40}$/);
|
|
22
|
+
expect(value.command).toBe('node cli/dist/src/index.js sensors coverage --json');
|
|
23
|
+
expect(value.providerVersion).toMatch(/^\S[^\r\n]*$/);
|
|
24
|
+
expect(value.providerIdentity).toEqual({
|
|
25
|
+
requestedProvider: provider,
|
|
26
|
+
executable: provider === 'claude-code' ? 'claude' : 'codex',
|
|
27
|
+
attestedBy: 'executable--version',
|
|
28
|
+
});
|
|
29
|
+
const serialized = JSON.stringify(value);
|
|
30
|
+
expect(serialized).not.toMatch(/\/home\/[^/"\s]+|\/Users\/[^/"\s]+|\b(?:token|secret|password|api[-_]?key)\b"?\s*[:=]/i);
|
|
31
|
+
if (value.result === 'pass') {
|
|
32
|
+
expect(value.reportSha256).toMatch(/^[0-9a-f]{64}$/);
|
|
33
|
+
expect(value.semanticContract).toBeDefined();
|
|
34
|
+
expect(value.report).toBeDefined();
|
|
35
|
+
expect(crypto_1.default.createHash('sha256').update(`${JSON.stringify(value.report)}\n`).digest('hex'))
|
|
36
|
+
.toBe(value.reportSha256);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
describe('R2 provider evidence integrity', () => {
|
|
40
|
+
it('locates the repository root from docs/research/r2 before invoking the compiled CLI', () => {
|
|
41
|
+
expect(fs_1.default.readFileSync(runner, 'utf8')).toContain("fileURLToPath(import.meta.url)), '../../..')");
|
|
42
|
+
});
|
|
43
|
+
it.each(providers)('%s evidence, when recorded, is a sanitized attested partial (RNF-T.2)', (provider) => {
|
|
44
|
+
const value = read(provider);
|
|
45
|
+
if (value === null) {
|
|
46
|
+
expect({ certification: 'partial', missingProviders: [provider] }).toEqual({
|
|
47
|
+
certification: 'partial',
|
|
48
|
+
missingProviders: ['claude-code'],
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
expectEvidenceIntegrity(provider, value);
|
|
53
|
+
});
|
|
54
|
+
it('accepts a valid but locally unavailable source SHA and rejects malformed source SHAs', () => {
|
|
55
|
+
const value = read('codex');
|
|
56
|
+
if (value === null)
|
|
57
|
+
throw new Error('Codex evidence fixture is required for integrity validation');
|
|
58
|
+
expectEvidenceIntegrity('codex', { ...value, sourceHead: 'f'.repeat(40) });
|
|
59
|
+
expect(() => expectEvidenceIntegrity('codex', { ...value, sourceHead: 'not-a-commit' }))
|
|
60
|
+
.toThrow();
|
|
61
|
+
});
|
|
62
|
+
it('reports RNF-T.2 as partial until two real pass envelopes can be compared', () => {
|
|
63
|
+
const claude = read('claude-code');
|
|
64
|
+
const codex = read('codex');
|
|
65
|
+
const missingPassEvidence = providers.filter((provider) => read(provider)?.result !== 'pass');
|
|
66
|
+
if (missingPassEvidence.length > 0) {
|
|
67
|
+
expect({ certification: 'partial', missingPassEvidence }).toEqual({
|
|
68
|
+
certification: 'partial',
|
|
69
|
+
missingPassEvidence: ['claude-code'],
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (claude === null || codex === null)
|
|
74
|
+
throw new Error('pass evidence must be present before certification');
|
|
75
|
+
expect(claude.sourceHead).toBe(codex.sourceHead);
|
|
76
|
+
expect(claude.semanticContract).toEqual(codex.semanticContract);
|
|
77
|
+
});
|
|
78
|
+
it('does not write Claude evidence when its required executable is unavailable', () => {
|
|
79
|
+
// Certified evidence is now committed at this path (RNF-T.2), so this test cannot
|
|
80
|
+
// assume the file is absent. Instead, force the attestation to fail by hiding
|
|
81
|
+
// `claude` from PATH and assert the file is left byte-for-byte untouched.
|
|
82
|
+
const output = path_1.default.join(evidenceDir, 'claude-code.json');
|
|
83
|
+
const before = fs_1.default.existsSync(output) ? fs_1.default.readFileSync(output, 'utf8') : null;
|
|
84
|
+
const result = (0, child_process_1.spawnSync)(process.execPath, [runner, 'claude-code', path_1.default.resolve(__dirname, '../..')], {
|
|
85
|
+
cwd: path_1.default.resolve(__dirname, '../../..'), encoding: 'utf8', env: { ...process.env, PATH: '' },
|
|
86
|
+
});
|
|
87
|
+
expect(result.status).not.toBe(0);
|
|
88
|
+
expect(`${result.stdout}${result.stderr}`).toContain('claude');
|
|
89
|
+
const after = fs_1.default.existsSync(output) ? fs_1.default.readFileSync(output, 'utf8') : null;
|
|
90
|
+
expect(after).toBe(before);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
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 crypto_1 = __importDefault(require("crypto"));
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const cliDir = path_1.default.resolve(__dirname, '../..');
|
|
12
|
+
const bin = path_1.default.join(cliDir, 'dist/src/index.js');
|
|
13
|
+
const fixture = path_1.default.join(cliDir, 'tests/fixtures/sensor-coverage/js-ts-gap');
|
|
14
|
+
const registryFixture = path_1.default.join(cliDir, 'tests/fixtures/sensor-coverage/registry');
|
|
15
|
+
const noFollowUnavailableOnNativeWindows = process.platform === 'win32'
|
|
16
|
+
&& typeof fs_1.default.constants.O_NOFOLLOW !== 'number';
|
|
17
|
+
const hashTree = (root) => {
|
|
18
|
+
const hash = crypto_1.default.createHash('sha256');
|
|
19
|
+
const walk = (directory) => {
|
|
20
|
+
for (const entry of fs_1.default.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
21
|
+
const file = path_1.default.join(directory, entry.name);
|
|
22
|
+
hash.update(path_1.default.relative(root, file));
|
|
23
|
+
if (entry.isDirectory())
|
|
24
|
+
walk(file);
|
|
25
|
+
else if (entry.isFile())
|
|
26
|
+
hash.update(fs_1.default.readFileSync(file));
|
|
27
|
+
else
|
|
28
|
+
hash.update(`link:${fs_1.default.readlinkSync(file)}`);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
walk(root);
|
|
32
|
+
return hash.digest('hex');
|
|
33
|
+
};
|
|
34
|
+
beforeAll(() => {
|
|
35
|
+
if (!fs_1.default.existsSync(bin)) {
|
|
36
|
+
throw new Error(`Coverage E2E requires the compiled CLI at ${bin}; run npm run build before this test.`);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
test('js-ts gap fixture tracks the manifest copied into the coverage project', () => {
|
|
40
|
+
const manifest = path_1.default.join(fixture, '.awm', 'sensors.json');
|
|
41
|
+
expect(fs_1.default.existsSync(manifest)).toBe(true);
|
|
42
|
+
expect(JSON.parse(fs_1.default.readFileSync(manifest, 'utf8'))).toMatchObject({
|
|
43
|
+
sensors: expect.any(Object),
|
|
44
|
+
});
|
|
45
|
+
expect((0, child_process_1.execFileSync)('git', ['ls-files', '--error-unmatch', 'tests/fixtures/sensor-coverage/js-ts-gap/.awm/sensors.json'], {
|
|
46
|
+
cwd: cliDir, encoding: 'utf8',
|
|
47
|
+
}).trim()).toBe('tests/fixtures/sensor-coverage/js-ts-gap/.awm/sensors.json');
|
|
48
|
+
});
|
|
49
|
+
function runWithFixture(mutatePack) {
|
|
50
|
+
const tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-coverage-e2e-'));
|
|
51
|
+
const project = path_1.default.join(tmp, 'project');
|
|
52
|
+
const awmHome = path_1.default.join(tmp, 'awm-home');
|
|
53
|
+
const registry = path_1.default.join(awmHome, 'registries', 'baseline');
|
|
54
|
+
fs_1.default.cpSync(fixture, project, { recursive: true });
|
|
55
|
+
fs_1.default.mkdirSync(path_1.default.dirname(registry), { recursive: true });
|
|
56
|
+
fs_1.default.cpSync(registryFixture, registry, { recursive: true });
|
|
57
|
+
fs_1.default.writeFileSync(path_1.default.join(awmHome, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'fixture' }]));
|
|
58
|
+
if (mutatePack) {
|
|
59
|
+
const packPath = path_1.default.join(registry, 'sensor-packs/js-ts/pack.json');
|
|
60
|
+
const pack = JSON.parse(fs_1.default.readFileSync(packPath, 'utf8'));
|
|
61
|
+
mutatePack(pack);
|
|
62
|
+
fs_1.default.writeFileSync(packPath, JSON.stringify(pack));
|
|
63
|
+
}
|
|
64
|
+
return { tmp, project, awmHome };
|
|
65
|
+
}
|
|
66
|
+
test('compiled CLI reports gaps or fails closed when no-follow safety is unavailable (RF-1.1, RF-1.4)', () => {
|
|
67
|
+
const { tmp, project, awmHome } = runWithFixture();
|
|
68
|
+
try {
|
|
69
|
+
const before = [hashTree(project), hashTree(awmHome)];
|
|
70
|
+
const result = (0, child_process_1.spawnSync)(process.execPath, [bin, 'sensors', 'coverage', '--json'], {
|
|
71
|
+
cwd: project, encoding: 'utf8', env: { ...process.env, AWM_HOME: awmHome, AWM_NO_UPDATE_CHECK: '1' },
|
|
72
|
+
});
|
|
73
|
+
if (noFollowUnavailableOnNativeWindows) {
|
|
74
|
+
expect(result.status).toBe(1);
|
|
75
|
+
expect(`${result.stdout ?? ''}${result.stderr ?? ''}`).toContain('platform cannot guarantee no symlink dereference');
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
expect(result.status).toBe(0);
|
|
79
|
+
const report = JSON.parse(result.stdout ?? '');
|
|
80
|
+
expect(report.static.classes).toEqual(expect.arrayContaining([
|
|
81
|
+
expect.objectContaining({ id: 'formatting', status: 'missing' }),
|
|
82
|
+
expect.objectContaining({ id: 'project-style-conventions', status: 'missing' }),
|
|
83
|
+
]));
|
|
84
|
+
}
|
|
85
|
+
expect([hashTree(project), hashTree(awmHome)]).toEqual(before);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
fs_1.default.rmSync(tmp, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
const testWithNoFollow = noFollowUnavailableOnNativeWindows ? test.skip : test;
|
|
92
|
+
testWithNoFollow('informative states exit zero; malformed contract exits non-zero (R2.7, R2.9)', () => {
|
|
93
|
+
const run = (mutatePack) => {
|
|
94
|
+
const { tmp, project, awmHome } = runWithFixture(mutatePack);
|
|
95
|
+
try {
|
|
96
|
+
return (0, child_process_1.spawnSync)(process.execPath, [bin, 'sensors', 'coverage', '--json'], {
|
|
97
|
+
cwd: project, encoding: 'utf8', env: { ...process.env, AWM_HOME: awmHome, AWM_NO_UPDATE_CHECK: '1' },
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
fs_1.default.rmSync(tmp, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const gaps = run(() => undefined);
|
|
105
|
+
const noReference = run((pack) => { delete pack.coverage; });
|
|
106
|
+
const malformed = run((pack) => { pack.coverage.schemaVersion = 2; });
|
|
107
|
+
expect(gaps.status).toBe(0);
|
|
108
|
+
expect(JSON.parse(gaps.stdout ?? '')).toMatchObject({ overall: 'gaps' });
|
|
109
|
+
expect(noReference.status).toBe(0);
|
|
110
|
+
expect(JSON.parse(noReference.stdout ?? '')).toMatchObject({ static: { reason: 'no_reference' } });
|
|
111
|
+
expect(malformed.status).toBe(1);
|
|
112
|
+
expect(`${malformed.stdout ?? ''}${malformed.stderr ?? ''}`).toContain('schemaVersion must be 1');
|
|
113
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
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 os_1 = __importDefault(require("os"));
|
|
7
|
+
describe('Jest environment isolation', () => {
|
|
8
|
+
it('does not inherit the operator Codex home', () => {
|
|
9
|
+
expect(process.env.CODEX_HOME).toBeUndefined();
|
|
10
|
+
});
|
|
11
|
+
it('uses a suite-owned temporary directory', () => {
|
|
12
|
+
expect(os_1.default.tmpdir()).toContain('awm-jest-');
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -18,6 +18,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
18
18
|
const path_1 = __importDefault(require("path"));
|
|
19
19
|
const CLI_ROOT = path_1.default.resolve(__dirname, '..', '..');
|
|
20
20
|
const SENSORS = path_1.default.join(CLI_ROOT, '.awm', 'sensors.json');
|
|
21
|
+
const BASELINE = path_1.default.join(CLI_ROOT, '.awm', 'sensors.baseline.json');
|
|
21
22
|
/** Extrae los archivos de config que un comando de sensor referencia. Se buscan por la
|
|
22
23
|
* convención `*.awm.*` que usan todos los packs, no por una lista de nombres — un pack
|
|
23
24
|
* nuevo con otro linter queda cubierto igual. */
|
|
@@ -46,4 +47,13 @@ describe('configs de sensores: declarados <=> presentes', () => {
|
|
|
46
47
|
// termina resuelto borrando el sensor, que es la salida equivocada.
|
|
47
48
|
expect(missing.join('\n') || 'todos presentes').toBe('todos presentes');
|
|
48
49
|
});
|
|
50
|
+
it('cada sensor conservado en el baseline sigue declarado en el manifiesto', () => {
|
|
51
|
+
const baseline = JSON.parse(fs_1.default.readFileSync(BASELINE, 'utf8'));
|
|
52
|
+
const missing = Object.keys(baseline).filter((name) => manifest?.sensors?.[name] === undefined);
|
|
53
|
+
expect(missing).toEqual([]);
|
|
54
|
+
});
|
|
55
|
+
it('el sensor de tests cubre la duración observada de la suite con margen de CI', () => {
|
|
56
|
+
const testSensor = manifest?.sensors?.test;
|
|
57
|
+
expect(testSensor?.timeout).toBeGreaterThanOrEqual(300_000);
|
|
58
|
+
});
|
|
49
59
|
});
|