agentic-workflow-manager 6.4.1 → 6.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/src/commands/add.js +18 -1
  2. package/dist/src/commands/init.js +11 -2
  3. package/dist/src/commands/sensors/coverage/contract.js +156 -0
  4. package/dist/src/commands/sensors/coverage/evaluate.js +107 -0
  5. package/dist/src/commands/sensors/coverage/evidence.js +118 -0
  6. package/dist/src/commands/sensors/coverage/index.js +31 -0
  7. package/dist/src/commands/sensors/coverage/render.js +152 -0
  8. package/dist/src/commands/sensors/coverage/resolve.js +126 -0
  9. package/dist/src/commands/sensors/index.js +16 -0
  10. package/dist/src/core/provider-version.js +26 -2
  11. package/dist/src/index.js +10 -5
  12. package/dist/src/utils/config.js +17 -0
  13. package/dist/tests/commands/add.test.js +42 -0
  14. package/dist/tests/commands/init.test.js +23 -0
  15. package/dist/tests/commands/sensors/coverage/contract.test.js +101 -0
  16. package/dist/tests/commands/sensors/coverage/evaluate.test.js +82 -0
  17. package/dist/tests/commands/sensors/coverage/evidence.test.js +251 -0
  18. package/dist/tests/commands/sensors/coverage/index.test.js +30 -0
  19. package/dist/tests/commands/sensors/coverage/render.test.js +149 -0
  20. package/dist/tests/commands/sensors/coverage/resolve.test.js +130 -0
  21. package/dist/tests/commands/sensors/index.test.js +52 -1
  22. package/dist/tests/commands/sensors/router.test.js +2 -1
  23. package/dist/tests/core/provider-version.test.js +41 -0
  24. package/dist/tests/integration/codex-provider-isolated.test.js +15 -1
  25. package/dist/tests/integration/copilot-init-isolated.test.js +1 -0
  26. package/dist/tests/integration/sensor-coverage-provider-evidence.test.js +88 -0
  27. package/dist/tests/integration/sensor-coverage.e2e.test.js +113 -0
  28. package/dist/tests/structural/jest-environment-is-isolated.test.js +14 -0
  29. package/dist/tests/structural/sensor-configs-are-present.test.js +10 -0
  30. package/dist/tests/utils/config.test.js +21 -0
  31. package/package.json +1 -1
@@ -0,0 +1,251 @@
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 tmp_1 = require("../../../support/tmp");
10
+ const evidence_1 = require("../../../../src/commands/sensors/coverage/evidence");
11
+ const contract_1 = require("../../../../src/commands/sensors/coverage/contract");
12
+ let root;
13
+ let externalRoot;
14
+ beforeEach(() => {
15
+ root = (0, tmp_1.mkCanonicalTmpDir)('awm-coverage-evidence-');
16
+ externalRoot = undefined;
17
+ });
18
+ afterEach(() => {
19
+ fs_1.default.rmSync(root, { recursive: true, force: true });
20
+ if (externalRoot)
21
+ fs_1.default.rmSync(externalRoot, { recursive: true, force: true });
22
+ });
23
+ const detector = {
24
+ sensor: 'lint',
25
+ evidence: {
26
+ commandIncludes: ['eslint', '--config'],
27
+ files: [{ path: 'eslint.config.js', containsAll: ['no-unreachable'] }],
28
+ },
29
+ };
30
+ const noFollowUnavailableOnNativeWindows = process.platform === 'win32'
31
+ && typeof fs_1.default.constants.O_NOFOLLOW !== 'number';
32
+ const regularFileStatus = (posixStatus) => noFollowUnavailableOnNativeWindows ? 'unverifiable' : posixStatus;
33
+ test('active matching sensor with all AND evidence is covered (R2.2)', () => {
34
+ fs_1.default.writeFileSync(path_1.default.join(root, 'eslint.config.js'), "rules: { 'no-unreachable': 'error' }");
35
+ const observed = (0, evidence_1.observeDetector)(root, 'style', 0, detector, {
36
+ cmd: 'npx eslint . --config eslint.config.js', enabled: true,
37
+ });
38
+ if (noFollowUnavailableOnNativeWindows) {
39
+ expect(observed).toMatchObject({
40
+ status: 'unverifiable',
41
+ evidence: expect.arrayContaining([{ kind: 'file', path: 'eslint.config.js', status: 'unverifiable' }]),
42
+ });
43
+ return;
44
+ }
45
+ expect(observed).toEqual({
46
+ classId: 'style',
47
+ detectorIndex: 0,
48
+ sensor: 'lint',
49
+ status: 'covered',
50
+ evidence: [
51
+ { kind: 'command', status: 'matched' },
52
+ { kind: 'file', path: 'eslint.config.js', status: 'matched' },
53
+ { kind: 'marker', path: 'eslint.config.js', ordinal: 1, status: 'matched' },
54
+ ],
55
+ });
56
+ });
57
+ test.each([
58
+ [undefined, 'missing'],
59
+ [{ cmd: 'npx eslint .', enabled: false }, 'disabled'],
60
+ [{ cmd: 'custom-linter .' }, 'unverifiable'],
61
+ [{ enabled: true }, 'unverifiable'],
62
+ ])('maps sensor availability/config %# to %s (R2.3, R2.5)', (sensor, expected) => {
63
+ expect((0, evidence_1.observeDetector)(root, 'style', 0, detector, sensor)).toMatchObject({ status: expected });
64
+ });
65
+ test('records absent and custom required commands without exposing their text (R2.5)', () => {
66
+ const absent = (0, evidence_1.observeDetector)(root, 'style', 0, detector, { enabled: true });
67
+ const custom = (0, evidence_1.observeDetector)(root, 'style', 0, detector, { cmd: 'private-linter --private-flag' });
68
+ expect(absent).toMatchObject({
69
+ status: 'unverifiable',
70
+ evidence: [{ kind: 'command', status: 'missing' }],
71
+ });
72
+ expect(custom).toMatchObject({
73
+ status: 'unverifiable',
74
+ evidence: [{ kind: 'command', status: 'custom' }],
75
+ });
76
+ expect(JSON.stringify({ absent, custom })).not.toContain('private-linter');
77
+ expect(JSON.stringify({ absent, custom })).not.toContain('private-flag');
78
+ });
79
+ test('recognized command plus missing file is ineffective (R2.4)', () => {
80
+ const out = (0, evidence_1.observeDetector)(root, 'style', 0, detector, { cmd: 'eslint --config eslint.config.js' });
81
+ expect(out.status).toBe('ineffective');
82
+ expect(out.evidence).toContainEqual({ kind: 'file', path: 'eslint.config.js', status: 'missing' });
83
+ });
84
+ test('recognized command plus missing literal marker is ineffective (R2.4)', () => {
85
+ fs_1.default.writeFileSync(path_1.default.join(root, 'eslint.config.js'), 'export default []');
86
+ const out = (0, evidence_1.observeDetector)(root, 'style', 0, detector, { cmd: 'eslint --config eslint.config.js' });
87
+ expect(out.status).toBe(regularFileStatus('ineffective'));
88
+ if (noFollowUnavailableOnNativeWindows) {
89
+ expect(out.evidence).toContainEqual({ kind: 'file', path: 'eslint.config.js', status: 'unverifiable' });
90
+ }
91
+ else {
92
+ expect(out.evidence).toContainEqual({ kind: 'marker', path: 'eslint.config.js', ordinal: 1, status: 'missing' });
93
+ }
94
+ });
95
+ test('native Windows without no-follow support never certifies regular evidence from its contents', () => {
96
+ fs_1.default.writeFileSync(path_1.default.join(root, 'eslint.config.js'), 'no-unreachable');
97
+ const observed = (0, evidence_1.observeDetector)(root, 'style', 0, detector, { cmd: 'eslint --config eslint.config.js' });
98
+ if (noFollowUnavailableOnNativeWindows) {
99
+ expect(observed.status).toBe('unverifiable');
100
+ expect(observed.status).not.toBe('covered');
101
+ expect(observed.status).not.toBe('ineffective');
102
+ }
103
+ else {
104
+ expect(observed.status).toBe('covered');
105
+ }
106
+ });
107
+ test('unverifiable file evidence dominates missing evidence in the detector result (R2.5a)', () => {
108
+ const mixed = {
109
+ sensor: 'lint',
110
+ evidence: {
111
+ files: [
112
+ { path: 'missing.js', containsAll: [] },
113
+ { path: 'linked.js', containsAll: ['no-unreachable'] },
114
+ ],
115
+ },
116
+ };
117
+ const target = path_1.default.join(root, 'target.js');
118
+ fs_1.default.writeFileSync(target, 'no-unreachable');
119
+ fs_1.default.symlinkSync(target, path_1.default.join(root, 'linked.js'));
120
+ const out = (0, evidence_1.observeDetector)(root, 'style', 0, mixed, { cmd: 'eslint' });
121
+ expect(out.status).toBe('unverifiable');
122
+ expect(out.evidence).toEqual(expect.arrayContaining([
123
+ { kind: 'file', path: 'missing.js', status: 'missing' },
124
+ { kind: 'file', path: 'linked.js', status: 'unverifiable' },
125
+ ]));
126
+ });
127
+ test.each(['symlink', 'oversize'])('%s evidence is unverifiable, never green or missing (R2.5a, R2.11)', (kind) => {
128
+ const target = path_1.default.join(root, 'target.js');
129
+ fs_1.default.writeFileSync(target, 'no-unreachable');
130
+ const file = path_1.default.join(root, 'eslint.config.js');
131
+ if (kind === 'symlink')
132
+ fs_1.default.symlinkSync(target, file);
133
+ if (kind === 'oversize')
134
+ fs_1.default.writeFileSync(file, Buffer.alloc(contract_1.MAX_COVERAGE_FILE_BYTES + 1));
135
+ expect((0, evidence_1.observeDetector)(root, 'style', 0, detector, { cmd: 'eslint --config eslint.config.js' })).toMatchObject({
136
+ status: 'unverifiable',
137
+ evidence: expect.arrayContaining([{ kind: 'file', path: 'eslint.config.js', status: 'unverifiable' }]),
138
+ });
139
+ });
140
+ test('does not trust injected lstat semantics to make symlink evidence regular (R2.11)', () => {
141
+ const target = path_1.default.join(root, 'target.js');
142
+ const file = path_1.default.join(root, 'eslint.config.js');
143
+ fs_1.default.writeFileSync(target, 'no-unreachable');
144
+ fs_1.default.symlinkSync(target, file);
145
+ const command = { cmd: 'eslint --config eslint.config.js' };
146
+ const observedWithLstat = (0, evidence_1.observeDetector)(root, 'style', 0, detector, command, {
147
+ lstatSync: fs_1.default.lstatSync,
148
+ });
149
+ const simulatedStatRegression = (0, evidence_1.observeDetector)(root, 'style', 0, detector, command, {
150
+ lstatSync: fs_1.default.statSync,
151
+ });
152
+ expect(observedWithLstat.status).toBe('unverifiable');
153
+ expect(simulatedStatRegression.status).toBe('unverifiable');
154
+ });
155
+ test('rejects a file swapped to a symlink after lstat without reading its target (R2.11)', () => {
156
+ const file = path_1.default.join(root, 'eslint.config.js');
157
+ externalRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-coverage-external-'));
158
+ const outsideTarget = path_1.default.join(externalRoot, 'outside-target.js');
159
+ fs_1.default.writeFileSync(file, 'no-unreachable');
160
+ fs_1.default.writeFileSync(outsideTarget, 'no-unreachable private-target-content');
161
+ const reads = [];
162
+ const out = (0, evidence_1.observeDetector)(root, 'style', 0, detector, { cmd: 'eslint --config eslint.config.js' }, {
163
+ lstatSync: (input) => {
164
+ const stat = fs_1.default.lstatSync(input);
165
+ fs_1.default.rmSync(input);
166
+ fs_1.default.symlinkSync(outsideTarget, input);
167
+ return stat;
168
+ },
169
+ readSync: (fd, buffer, offset, length, position) => {
170
+ reads.push(fd);
171
+ return fs_1.default.readSync(fd, buffer, offset, length, position);
172
+ },
173
+ });
174
+ expect(out).toMatchObject({
175
+ status: 'unverifiable',
176
+ evidence: expect.arrayContaining([{ kind: 'file', path: 'eslint.config.js', status: 'unverifiable' }]),
177
+ });
178
+ expect(reads).toEqual([]);
179
+ });
180
+ test('rejects a non-regular descriptor before reading it (R2.11)', () => {
181
+ const file = path_1.default.join(root, 'eslint.config.js');
182
+ fs_1.default.writeFileSync(file, 'no-unreachable');
183
+ const reads = [];
184
+ const out = (0, evidence_1.observeDetector)(root, 'style', 0, detector, { cmd: 'eslint --config eslint.config.js' }, {
185
+ lstatSync: fs_1.default.lstatSync,
186
+ openSync: fs_1.default.openSync,
187
+ fstatSync: () => fs_1.default.statSync(root),
188
+ readSync: (fd, buffer, offset, length, position) => {
189
+ reads.push(fd);
190
+ return fs_1.default.readSync(fd, buffer, offset, length, position);
191
+ },
192
+ closeSync: fs_1.default.closeSync,
193
+ });
194
+ expect(out).toMatchObject({
195
+ status: 'unverifiable',
196
+ evidence: expect.arrayContaining([{ kind: 'file', path: 'eslint.config.js', status: 'unverifiable' }]),
197
+ });
198
+ expect(reads).toEqual([]);
199
+ });
200
+ test('rejects evidence that grows beyond the byte cap after descriptor validation (R2.11)', () => {
201
+ const file = path_1.default.join(root, 'eslint.config.js');
202
+ fs_1.default.writeFileSync(file, 'no-unreachable');
203
+ const out = (0, evidence_1.observeDetector)(root, 'style', 0, detector, { cmd: 'eslint --config eslint.config.js' }, {
204
+ lstatSync: fs_1.default.lstatSync,
205
+ openSync: fs_1.default.openSync,
206
+ fstatSync: (fd) => {
207
+ const stat = fs_1.default.fstatSync(fd);
208
+ fs_1.default.appendFileSync(file, Buffer.alloc(contract_1.MAX_COVERAGE_FILE_BYTES + 1));
209
+ return stat;
210
+ },
211
+ closeSync: fs_1.default.closeSync,
212
+ });
213
+ expect(out).toMatchObject({
214
+ status: 'unverifiable',
215
+ evidence: expect.arrayContaining([{ kind: 'file', path: 'eslint.config.js', status: 'unverifiable' }]),
216
+ });
217
+ });
218
+ test('read errors are unverifiable independently of host permissions (R2.5a)', () => {
219
+ fs_1.default.writeFileSync(path_1.default.join(root, 'eslint.config.js'), 'no-unreachable');
220
+ const io = {
221
+ lstatSync: fs_1.default.lstatSync,
222
+ readSync: () => { throw Object.assign(new Error('permission denied'), { code: 'EACCES' }); },
223
+ };
224
+ expect((0, evidence_1.observeDetector)(root, 'style', 0, detector, { cmd: 'eslint --config eslint.config.js' }, io).status).toBe('unverifiable');
225
+ });
226
+ test('rejects an evidence path that escapes the project root (R2.11)', () => {
227
+ const escaped = {
228
+ sensor: 'lint',
229
+ evidence: { files: [{ path: '../outside.txt', containsAll: [] }] },
230
+ };
231
+ expect(() => (0, evidence_1.observeDetector)(root, 'style', 0, escaped, { cmd: 'eslint' }))
232
+ .toThrow('evidence path escaped project root: ../outside.txt');
233
+ });
234
+ test.each([
235
+ [{}, 'style', 0, 'root must be a non-empty string'],
236
+ [' ', 'style', 0, 'root must be a non-empty string'],
237
+ ['/valid-root', '', 0, 'classId must be a non-empty string'],
238
+ ['/valid-root', ' ', 0, 'classId must be a non-empty string'],
239
+ ['/valid-root', 'style', -1, 'detectorIndex must be a non-negative integer'],
240
+ ['/valid-root', 'style', 0.5, 'detectorIndex must be a non-negative integer'],
241
+ ])('rejects malformed public arguments %#', (inputRoot, classId, detectorIndex, message) => {
242
+ expect(() => (0, evidence_1.observeDetector)(inputRoot, classId, detectorIndex, detector, { cmd: 'eslint --config eslint.config.js' }))
243
+ .toThrow(`observeDetector: ${message}`);
244
+ });
245
+ test('reports only ordinal/path/status and never leaks command or marker text (RF-1.4)', () => {
246
+ fs_1.default.writeFileSync(path_1.default.join(root, 'eslint.config.js'), 'secret-marker');
247
+ const serialized = JSON.stringify((0, evidence_1.observeDetector)(root, 'style', 0, detector, { cmd: 'private-command eslint --config' }));
248
+ expect(serialized).not.toContain('private-command');
249
+ expect(serialized).not.toContain('no-unreachable');
250
+ expect(serialized).not.toContain('secret-marker');
251
+ });
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const coverage_1 = require("../../../../src/commands/sensors/coverage");
4
+ test('not configured is explicit, actionable and exit-0 data', () => {
5
+ expect((0, coverage_1.runCoverage)('/fixture', { resolve: () => ({ kind: 'not_configured' }) })).toEqual({
6
+ schemaVersion: 1, pack: null, registry: null, overall: 'inconclusive',
7
+ static: { status: 'inconclusive', reason: 'not_configured', classes: [] },
8
+ });
9
+ });
10
+ test('old pack is no_reference and preserves pack and registry', () => {
11
+ const manifest = { pack: 'legacy', sensors: {} };
12
+ expect((0, coverage_1.runCoverage)('/fixture', { resolve: () => ({ kind: 'no_reference', projectRoot: '/fixture', pack: 'legacy', registry: 'baseline', manifest }) }))
13
+ .toEqual({ schemaVersion: 1, pack: 'legacy', registry: 'baseline', overall: 'inconclusive',
14
+ static: { status: 'inconclusive', reason: 'no_reference', classes: [] } });
15
+ });
16
+ test('ready input observes every declared detector and evaluates once', () => {
17
+ const manifest = { pack: 'js-ts', sensors: { lint: { cmd: 'eslint .' }, format: { cmd: 'prettier --check .' } } };
18
+ const contract = { schemaVersion: 1, classes: {
19
+ formatting: { description: 'Formatting', detectors: [{ sensor: 'format' }], remedy: { summary: 'Add format', command: 'npm i -D prettier' } },
20
+ linting: { description: 'Linting', detectors: [{ sensor: 'lint' }], remedy: { summary: 'Add lint', command: 'npm i -D eslint' } },
21
+ } };
22
+ const observe = jest.fn((_root, classId, detectorIndex, detector) => ({
23
+ classId, detectorIndex, sensor: detector.sensor, status: 'covered', evidence: [],
24
+ }));
25
+ const out = (0, coverage_1.runCoverage)('/fixture', { resolve: () => ({ kind: 'ready', projectRoot: '/fixture', pack: 'js-ts', registry: 'baseline', manifest, contract }), observe });
26
+ expect(observe.mock.calls.map((call) => [call[1], call[2]])).toEqual([['formatting', 0], ['linting', 0]]);
27
+ expect(out.static.classes.map((item) => item.id)).toEqual(['formatting', 'linting']);
28
+ expect(out.overall).toBe('covered');
29
+ expect(out).not.toHaveProperty('empirical');
30
+ });
@@ -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('registers sensors command with 4 subcommands', () => {
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
  });