agentic-workflow-manager 6.5.2 → 8.0.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 (93) hide show
  1. package/dist/scripts/sensor-support-matrix.js +103 -0
  2. package/dist/scripts/support-matrix.js +40 -17
  3. package/dist/src/commands/init.js +1 -3
  4. package/dist/src/commands/ledger/index.js +6 -0
  5. package/dist/src/commands/preflight/checks.js +4 -4
  6. package/dist/src/commands/preflight/index.js +2 -2
  7. package/dist/src/commands/registry/add.js +2 -2
  8. package/dist/src/commands/registry/index.js +5 -0
  9. package/dist/src/commands/registry/status.js +7 -0
  10. package/dist/src/commands/sensors/compatibility/contract.js +261 -0
  11. package/dist/src/commands/sensors/compatibility/discovery.js +207 -0
  12. package/dist/src/commands/sensors/compatibility/live.js +59 -0
  13. package/dist/src/commands/sensors/compatibility/manifest.js +197 -0
  14. package/dist/src/commands/sensors/compatibility/materialize.js +126 -0
  15. package/dist/src/commands/sensors/compatibility/pack-source.js +78 -0
  16. package/dist/src/commands/sensors/compatibility/probe.js +47 -0
  17. package/dist/src/commands/sensors/compatibility/resolve.js +96 -0
  18. package/dist/src/commands/sensors/compatibility/types.js +2 -0
  19. package/dist/src/commands/sensors/coverage/contract.js +0 -50
  20. package/dist/src/commands/sensors/coverage/empirical.js +131 -0
  21. package/dist/src/commands/sensors/coverage/evaluate.js +85 -7
  22. package/dist/src/commands/sensors/coverage/evidence.js +5 -1
  23. package/dist/src/commands/sensors/coverage/index.js +44 -7
  24. package/dist/src/commands/sensors/coverage/render.js +214 -14
  25. package/dist/src/commands/sensors/coverage/resolve.js +24 -38
  26. package/dist/src/commands/sensors/exec.js +150 -9
  27. package/dist/src/commands/sensors/index.js +18 -6
  28. package/dist/src/commands/sensors/init.js +92 -6
  29. package/dist/src/commands/sensors/run.js +71 -0
  30. package/dist/src/commands/sensors/status.js +48 -4
  31. package/dist/src/commands/sync.js +7 -0
  32. package/dist/src/core/bundles.js +38 -3
  33. package/dist/src/core/init/steps.js +7 -4
  34. package/dist/src/core/ledger/scan.js +228 -0
  35. package/dist/src/core/ledger/store.js +89 -25
  36. package/dist/src/core/ledger/types.js +54 -0
  37. package/dist/src/core/paths.js +10 -9
  38. package/dist/src/core/registries.js +134 -14
  39. package/dist/src/index.js +24 -1
  40. package/dist/tests/commands/ledger/index.test.js +11 -0
  41. package/dist/tests/commands/preflight/preflight.test.js +64 -64
  42. package/dist/tests/commands/registry/add.test.js +69 -2
  43. package/dist/tests/commands/registry/status.test.js +14 -0
  44. package/dist/tests/commands/sensors/compatibility/contract.test.js +98 -0
  45. package/dist/tests/commands/sensors/compatibility/discovery.test.js +119 -0
  46. package/dist/tests/commands/sensors/compatibility/manifest.test.js +68 -0
  47. package/dist/tests/commands/sensors/compatibility/materialize.test.js +53 -0
  48. package/dist/tests/commands/sensors/compatibility/pack-source.test.js +55 -0
  49. package/dist/tests/commands/sensors/compatibility/probe.test.js +27 -0
  50. package/dist/tests/commands/sensors/compatibility/resolve.test.js +75 -0
  51. package/dist/tests/commands/sensors/coverage/contract.test.js +8 -22
  52. package/dist/tests/commands/sensors/coverage/empirical.test.js +111 -0
  53. package/dist/tests/commands/sensors/coverage/evaluate.test.js +29 -0
  54. package/dist/tests/commands/sensors/coverage/evidence.test.js +9 -0
  55. package/dist/tests/commands/sensors/coverage/index.test.js +40 -13
  56. package/dist/tests/commands/sensors/coverage/render.test.js +187 -12
  57. package/dist/tests/commands/sensors/coverage/resolve.test.js +25 -4
  58. package/dist/tests/commands/sensors/exec-windows.test.js +92 -2
  59. package/dist/tests/commands/sensors/exec.test.js +67 -5
  60. package/dist/tests/commands/sensors/index.test.js +8 -0
  61. package/dist/tests/commands/sensors/init-pack-unavailable.test.js +12 -12
  62. package/dist/tests/commands/sensors/init.test.js +132 -48
  63. package/dist/tests/commands/sensors/run-inconclusive.test.js +9 -2
  64. package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
  65. package/dist/tests/commands/sensors/run.test.js +66 -3
  66. package/dist/tests/commands/sensors/status-windows.test.js +7 -7
  67. package/dist/tests/commands/sensors/status.test.js +74 -23
  68. package/dist/tests/commands/update.test.js +1 -1
  69. package/dist/tests/core/bundles.test.js +37 -0
  70. package/dist/tests/core/discovery.test.js +4 -0
  71. package/dist/tests/core/init/orchestrator.test.js +5 -1
  72. package/dist/tests/core/init/steps.test.js +20 -4
  73. package/dist/tests/core/ledger/scan.test.js +217 -0
  74. package/dist/tests/core/ledger/store.test.js +108 -0
  75. package/dist/tests/core/profile-pins.test.js +2 -1
  76. package/dist/tests/core/project-skill-links.test.js +22 -0
  77. package/dist/tests/core/registries-capability.test.js +10 -0
  78. package/dist/tests/core/registries-sync.test.js +16 -0
  79. package/dist/tests/core/registries.test.js +129 -1
  80. package/dist/tests/core/registry-manifest.test.js +7 -0
  81. package/dist/tests/core/same-existing-path.test.js +35 -0
  82. package/dist/tests/core/sync-gates.test.js +2 -1
  83. package/dist/tests/integration/copilot-init-isolated.test.js +18 -0
  84. package/dist/tests/integration/sensor-compatibility.e2e.test.js +141 -0
  85. package/dist/tests/integration/sensor-coverage.e2e.test.js +35 -6
  86. package/dist/tests/integration/sensor-structured-run.e2e.test.js +127 -0
  87. package/dist/tests/structural/active-documentation.test.js +128 -0
  88. package/dist/tests/structural/async-entrypoint.test.js +15 -0
  89. package/dist/tests/structural/jest-environment-is-isolated.test.js +44 -0
  90. package/dist/tests/structural/r3-cli-major-version.test.js +37 -0
  91. package/dist/tests/structural/sensor-documentation-contract.test.js +77 -0
  92. package/dist/tests/structural/support-matrix-is-current.test.js +69 -2
  93. package/package.json +8 -2
@@ -1,20 +1,45 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
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',
4
+ test('not configured is explicit, actionable and exit-0 data', async () => {
5
+ await expect((0, coverage_1.runCoverage)('/fixture', { resolve: () => ({ kind: 'not_configured' }), scan: () => ({
6
+ entries: [], sources: { activeFiles: 0, archivedFiles: 0, validEntries: 0, validFindings: 0, skippedFindings: 0, skippedByReason: {} }, omittedEvidenceRefs: 0,
7
+ }) })).resolves.toEqual({
8
+ schemaVersion: 2, pack: null, registry: null, overall: 'inconclusive',
7
9
  static: { status: 'inconclusive', reason: 'not_configured', classes: [] },
10
+ empirical: expect.objectContaining({ status: 'no-evidence' }),
8
11
  });
9
12
  });
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: [] } });
13
+ test('unavailable static contracts make empirical findings coverage-unverifiable', async () => {
14
+ const scan = {
15
+ entries: [{ entry: { ts: 'x', branch: 'main', phase: 'qa', source_skill: 'test', polarity: 'finding',
16
+ class: 'structural', signature: 'private', severity: 'minor', desc: 'secret', defectClass: 'lint-errors' }, source: '.awm/ledger/main.jsonl:1', evidenceRef: '.awm/ledger/main.jsonl:1' }],
17
+ sources: { activeFiles: 1, archivedFiles: 0, validEntries: 1, validFindings: 1, skippedFindings: 0, skippedByReason: {} }, omittedEvidenceRefs: 0,
18
+ };
19
+ const notConfigured = await (0, coverage_1.runCoverage)('/fixture', { resolve: () => ({ kind: 'not_configured' }), scan: () => scan });
20
+ expect(notConfigured.empirical?.classes[0].outcome).toBe('coverage-unverifiable');
21
+ const manifest = { kind: 'legacy', pack: { pack: 'legacy', sensors: {}, compatibility: {
22
+ state: 'compatible-unverified', reason: 'legacy', variantId: null, toolVersion: null, runtimeVersion: null, certifiedRange: null, evidence: [],
23
+ } } };
24
+ const noReference = await (0, coverage_1.runCoverage)('/fixture', { resolve: () => ({ kind: 'no_reference', projectRoot: '/fixture', pack: 'legacy', registry: 'baseline', manifest }), scan: () => scan });
25
+ expect(noReference.empirical?.classes[0].outcome).toBe('coverage-unverifiable');
15
26
  });
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 .' } } };
27
+ test('old pack is no_reference and preserves pack and registry', async () => {
28
+ const manifest = { kind: 'legacy', pack: { pack: 'legacy', sensors: {}, compatibility: {
29
+ state: 'compatible-unverified', reason: 'legacy', variantId: null, toolVersion: null, runtimeVersion: null, certifiedRange: null, evidence: [],
30
+ } } };
31
+ await expect((0, coverage_1.runCoverage)('/fixture', { resolve: () => ({ kind: 'no_reference', projectRoot: '/fixture', pack: 'legacy', registry: 'baseline', manifest }), scan: () => ({
32
+ entries: [{ entry: { ts: 'x', branch: 'main', phase: 'qa', source_skill: 'test', polarity: 'finding', class: 'structural', signature: 'private', severity: 'minor', desc: 'secret', defectClass: 'lint-errors' }, source: '.awm/ledger/main.jsonl:1', evidenceRef: '.awm/ledger/main.jsonl:1' }],
33
+ sources: { activeFiles: 1, archivedFiles: 0, validEntries: 1, validFindings: 1, skippedFindings: 0, skippedByReason: {} }, omittedEvidenceRefs: 0,
34
+ }) }))
35
+ .resolves.toEqual({ schemaVersion: 2, pack: 'legacy', registry: 'baseline', overall: 'inconclusive',
36
+ static: { status: 'inconclusive', reason: 'no_reference', classes: [] },
37
+ empirical: expect.objectContaining({ status: 'evidence', classes: [expect.objectContaining({ outcome: 'coverage-unverifiable' })] }) });
38
+ });
39
+ test('ready input observes every declared detector and evaluates once', async () => {
40
+ const manifest = { kind: 'legacy', pack: { pack: 'js-ts', sensors: { lint: { cmd: 'eslint .' }, format: { cmd: 'prettier --check .' } }, compatibility: {
41
+ state: 'compatible-unverified', reason: 'legacy', variantId: null, toolVersion: null, runtimeVersion: null, certifiedRange: null, evidence: [],
42
+ } } };
18
43
  const contract = { schemaVersion: 1, classes: {
19
44
  formatting: { description: 'Formatting', detectors: [{ sensor: 'format' }], remedy: { summary: 'Add format', command: 'npm i -D prettier' } },
20
45
  linting: { description: 'Linting', detectors: [{ sensor: 'lint' }], remedy: { summary: 'Add lint', command: 'npm i -D eslint' } },
@@ -22,9 +47,11 @@ test('ready input observes every declared detector and evaluates once', () => {
22
47
  const observe = jest.fn((_root, classId, detectorIndex, detector) => ({
23
48
  classId, detectorIndex, sensor: detector.sensor, status: 'covered', evidence: [],
24
49
  }));
25
- const out = (0, coverage_1.runCoverage)('/fixture', { resolve: () => ({ kind: 'ready', projectRoot: '/fixture', pack: 'js-ts', registry: 'baseline', manifest, contract }), observe });
50
+ const out = await (0, coverage_1.runCoverage)('/fixture', { resolve: () => ({ kind: 'ready', projectRoot: '/fixture', pack: 'js-ts', registry: 'baseline', manifest, contract }), observe, scan: () => ({
51
+ entries: [], sources: { activeFiles: 0, archivedFiles: 0, validEntries: 0, validFindings: 0, skippedFindings: 0, skippedByReason: {} }, omittedEvidenceRefs: 0,
52
+ }) });
26
53
  expect(observe.mock.calls.map((call) => [call[1], call[2]])).toEqual([['formatting', 0], ['linting', 0]]);
27
54
  expect(out.static.classes.map((item) => item.id)).toEqual(['formatting', 'linting']);
28
- expect(out.overall).toBe('covered');
29
- expect(out).not.toHaveProperty('empirical');
55
+ expect(out.overall).toBe('inconclusive');
56
+ expect(out.empirical).toMatchObject({ status: 'no-evidence' });
30
57
  });
@@ -1,40 +1,206 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const render_1 = require("../../../../src/commands/sensors/coverage/render");
4
+ const empirical_1 = require("../../../../src/commands/sensors/coverage/empirical");
4
5
  const report = {
5
- schemaVersion: 1, pack: 'js-ts', registry: 'baseline', overall: 'gaps',
6
+ schemaVersion: 2, pack: 'js-ts', registry: 'baseline', overall: 'gaps',
6
7
  static: { status: 'gaps', reason: null, classes: [
7
- { id: 'formatting', description: 'Formatting', status: 'missing',
8
- detectors: [{ sensor: 'format', status: 'missing', evidence: [] }],
8
+ { id: 'lint-errors', description: 'Formatting', status: 'missing',
9
+ detectors: [{ sensor: 'format', status: 'missing', evidence: [], compatibility: { state: 'missing-tool', reason: 'fixture', variantId: null, toolVersion: null, runtimeVersion: null, certifiedRange: null, evidence: [] } }],
9
10
  remedy: { summary: 'Add formatter', command: 'npm i -D prettier' } },
10
11
  { id: 'style', description: 'Style', status: 'unverifiable',
11
- detectors: [{ sensor: 'lint', status: 'unverifiable', evidence: [{ kind: 'command', status: 'custom' }] }],
12
+ detectors: [{ sensor: 'lint', status: 'unverifiable', evidence: [{ kind: 'command', status: 'custom' }], compatibility: { state: 'unverifiable', reason: 'fixture', variantId: null, toolVersion: null, runtimeVersion: null, certifiedRange: null, evidence: [] } }],
12
13
  remedy: { summary: 'Declare evidence', command: 'awm sensors init' } },
13
14
  ] },
14
15
  };
16
+ const redactedCluster = {
17
+ occurrences: 1,
18
+ recurrent: false,
19
+ severity: 'important',
20
+ kind: 'exact',
21
+ signatures: [],
22
+ omittedSignatures: 1,
23
+ evidenceRefs: ['src/a.ts:1'],
24
+ omittedEvidenceRefs: 0,
25
+ };
26
+ const empiricalEvidence = {
27
+ recurrenceThreshold: 2, status: 'evidence',
28
+ classes: [{ defectClass: 'lint-errors', occurrences: 1, recurrent: false, severity: 'important', outcome: 'gap', evidenceRefs: ['src/a.ts:1'], omittedEvidenceRefs: 0, clusters: [redactedCluster] }],
29
+ unclassified: { occurrences: 0, evidenceRefs: [], omittedEvidenceRefs: 0 },
30
+ sources: { activeFiles: 1, archivedFiles: 0, validEntries: 1, validFindings: 1, skippedFindings: 0, skippedByReason: {} },
31
+ omittedEvidenceRefs: 0,
32
+ };
15
33
  test('human output shows every non-green class, remedy and totals without raw evidence (R2.8)', () => {
16
34
  const human = (0, render_1.renderCoverageHuman)(report);
17
35
  expect(human).toBe([
18
36
  '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', '',
37
+ 'missing lint-errors — Formatting', ' detector: format (missing)', ' compatibility: missing-tool — fixture', ' remedy: Add formatter', ' command: npm i -D prettier',
38
+ 'unverifiable style — Style', ' detector: lint (unverifiable)', ' compatibility: unverifiable — fixture', ' remedy: Declare evidence', ' command: awm sensors init', '',
39
+ 'Summary: 0 covered, 1 missing, 1 unverifiable, 0 not applicable', '',
22
40
  ].join('\n'));
23
41
  expect(human).not.toContain('commandIncludes');
24
42
  expect(human).not.toContain('custom');
25
43
  });
44
+ test('human output explains an incompatible detector even when its structural evidence is covered (R3.3)', () => {
45
+ const compatibility = {
46
+ state: 'incompatible',
47
+ reason: 'installed eslint version is outside the certified range',
48
+ variantId: 'eslint-v9',
49
+ toolVersion: '10.0.0',
50
+ runtimeVersion: null,
51
+ certifiedRange: '^9.0.0',
52
+ evidence: [],
53
+ };
54
+ const incompatible = {
55
+ ...report,
56
+ static: {
57
+ ...report.static,
58
+ classes: [{
59
+ ...report.static.classes[0],
60
+ detectors: [{
61
+ ...report.static.classes[0].detectors[0],
62
+ status: 'covered',
63
+ compatibility,
64
+ }],
65
+ }],
66
+ },
67
+ };
68
+ const human = (0, render_1.renderCoverageHuman)(incompatible);
69
+ expect(human).toContain(' detector: format (covered)');
70
+ expect(human).toContain(' compatibility: incompatible — installed eslint version is outside the certified range');
71
+ });
26
72
  test('json is the exact versioned envelope and ends in newline (R2.8, R2.14)', () => {
27
73
  expect((0, render_1.renderCoverageJson)(report)).toBe(`${JSON.stringify(report, null, 2)}\n`);
28
74
  });
29
75
  test('keeps the R2 static shape when an optional empirical section is added (R2.14)', () => {
30
- const extended = { ...report, empirical: { status: 'no_evidence' } };
76
+ const extended = { ...report, empirical: {
77
+ recurrenceThreshold: 2, status: 'partial', classes: [{ defectClass: 'lint-errors', occurrences: 1, recurrent: false, severity: 'important', outcome: 'gap', evidenceRefs: ['src/a.ts:1'], omittedEvidenceRefs: 0, clusters: [redactedCluster] }],
78
+ unclassified: { occurrences: 1, evidenceRefs: [], omittedEvidenceRefs: 1 },
79
+ sources: { activeFiles: 1, archivedFiles: 0, validEntries: 2, validFindings: 2, skippedFindings: 1, skippedByReason: { 'invalid-json': 1 } }, omittedEvidenceRefs: 1,
80
+ } };
31
81
  const parsed = JSON.parse((0, render_1.renderCoverageJson)(extended));
32
82
  expect(parsed.static).toEqual(report.static);
33
- expect(parsed.empirical).toEqual({ status: 'no_evidence' });
34
- expect(parsed.schemaVersion).toBe(1);
83
+ expect(parsed.empirical).toEqual(extended.empirical);
84
+ expect(parsed.schemaVersion).toBe(2);
85
+ });
86
+ test('renders sanitized empirical outcomes without ledger descriptions or unsafe signatures', () => {
87
+ const empirical = {
88
+ recurrenceThreshold: 2, status: 'partial', classes: [{ defectClass: 'lint-errors', occurrences: 1, recurrent: false, severity: 'important', outcome: 'gap', evidenceRefs: ['PR #2'], omittedEvidenceRefs: 0, clusters: [redactedCluster] }],
89
+ unclassified: { occurrences: 1, evidenceRefs: [], omittedEvidenceRefs: 1 },
90
+ sources: { activeFiles: 1, archivedFiles: 0, validEntries: 2, validFindings: 2, skippedFindings: 1, skippedByReason: { 'invalid-json': 1 } }, omittedEvidenceRefs: 1,
91
+ };
92
+ const human = (0, render_1.renderCoverageHuman)({ ...report, empirical });
93
+ expect(human).toContain('Empirical coverage: partial');
94
+ expect(human).toContain('gap lint-errors — 1 occurrence');
95
+ expect(human).toContain('below recurrence threshold (2)');
96
+ expect(human).toContain('PR #2');
97
+ expect(human).toContain('omitted cluster signatures: 1');
98
+ expect(human).not.toContain('desc');
99
+ });
100
+ test('renders a report generated with both invalid and scanner-omitted evidence refs', () => {
101
+ const empirical = (0, empirical_1.evaluateEmpiricalCoverage)({
102
+ entries: [{ entry: { ts: '2026-08-14', branch: 'main', phase: 'qa', source_skill: 'test', polarity: 'finding', class: 'structural', signature: 'private', severity: 'important', desc: 'secret', defectClass: 'lint-errors' }, source: '.awm/ledger/main.jsonl:1', evidenceRef: 'not an allowed ref' }],
103
+ sources: { activeFiles: 1, archivedFiles: 0, validEntries: 1, validFindings: 1, skippedFindings: 3, skippedByReason: { 'evidence-ref-limit': 3 } }, omittedEvidenceRefs: 3,
104
+ }, { 'lint-errors': 'missing' }, 2);
105
+ expect(() => (0, render_1.renderCoverageJson)({ ...report, empirical })).not.toThrow();
106
+ expect((0, render_1.renderCoverageHuman)({ ...report, empirical })).toContain('omitted evidence refs: 4');
107
+ });
108
+ test('renders recurrence emphasis and rejects the retired complete empirical state', () => {
109
+ const empirical = {
110
+ recurrenceThreshold: 2, status: 'evidence', classes: [{ defectClass: 'lint-errors', occurrences: 2, recurrent: true, severity: 'important', outcome: 'gap', evidenceRefs: [], omittedEvidenceRefs: 0, clusters: [{ ...redactedCluster, occurrences: 2, recurrent: true, evidenceRefs: [] }] }],
111
+ unclassified: { occurrences: 0, evidenceRefs: [], omittedEvidenceRefs: 0 },
112
+ sources: { activeFiles: 1, archivedFiles: 0, validEntries: 2, validFindings: 2, skippedFindings: 0, skippedByReason: {} }, omittedEvidenceRefs: 0,
113
+ };
114
+ expect((0, render_1.renderCoverageHuman)({ ...report, empirical })).toContain('recurrent at threshold 2');
115
+ expect(() => (0, render_1.renderCoverageJson)({ ...report, empirical: { ...empirical, status: 'complete' } })).toThrow('invalid report');
116
+ });
117
+ test.each([
118
+ ['a recurrence flag that disagrees with its threshold', {
119
+ ...empiricalEvidence,
120
+ classes: [{ ...empiricalEvidence.classes[0], recurrent: true }],
121
+ }],
122
+ ['a skipped finding total that disagrees with its reason totals', {
123
+ ...empiricalEvidence,
124
+ status: 'partial',
125
+ sources: { ...empiricalEvidence.sources, skippedFindings: 2, skippedByReason: { 'invalid-json': 1 } },
126
+ }],
127
+ ['an omitted evidence total that disagrees with its component omissions', {
128
+ ...empiricalEvidence,
129
+ status: 'partial',
130
+ sources: { ...empiricalEvidence.sources, skippedFindings: 1, skippedByReason: { 'evidence-ref-limit': 1 } },
131
+ }],
132
+ ['evidence status when findings were skipped', {
133
+ ...empiricalEvidence,
134
+ sources: { ...empiricalEvidence.sources, skippedFindings: 1, skippedByReason: { 'invalid-json': 1 } },
135
+ }],
136
+ ['partial status without any incomplete evidence', {
137
+ ...empiricalEvidence,
138
+ status: 'partial',
139
+ }],
140
+ ['no-evidence status with a valid finding', {
141
+ ...empiricalEvidence,
142
+ status: 'no-evidence',
143
+ }],
144
+ ['inconclusive status with a valid finding', {
145
+ ...empiricalEvidence,
146
+ status: 'inconclusive',
147
+ }],
148
+ ])('renderers reject an empirical envelope with %s', (_case, empirical) => {
149
+ for (const render of [render_1.renderCoverageJson, render_1.renderCoverageHuman]) {
150
+ expect(() => render({ ...report, empirical })).toThrow(/^renderCoverage(?:Json|Human): invalid report/);
151
+ }
152
+ });
153
+ test.each([
154
+ ['a covered class reported as a gap', {
155
+ static: { ...report.static, status: 'covered', classes: [{
156
+ ...report.static.classes[0], status: 'covered',
157
+ detectors: [{ ...report.static.classes[0].detectors[0], status: 'covered',
158
+ compatibility: { ...report.static.classes[0].detectors[0].compatibility, state: 'certified' } }],
159
+ }] },
160
+ empirical: { ...empiricalEvidence, classes: [{ ...empiricalEvidence.classes[0], outcome: 'gap' }] },
161
+ }],
162
+ ['a missing-tool class reported as covered', {
163
+ static: report.static,
164
+ empirical: { ...empiricalEvidence, classes: [{ ...empiricalEvidence.classes[0], outcome: 'covered-by-sensor' }] },
165
+ }],
166
+ ['an unverifiable class reported as a gap', {
167
+ static: { ...report.static, status: 'inconclusive', classes: [{ ...report.static.classes[1], id: 'lint-errors' }] },
168
+ empirical: { ...empiricalEvidence, classes: [{ ...empiricalEvidence.classes[0], outcome: 'gap' }] },
169
+ }],
170
+ ['a not-applicable class with evidence reported without a contradiction', {
171
+ static: { ...report.static, status: 'inconclusive', classes: [{
172
+ ...report.static.classes[0], status: 'not-applicable',
173
+ detectors: [{ ...report.static.classes[0].detectors[0], status: 'covered',
174
+ compatibility: { ...report.static.classes[0].detectors[0].compatibility, state: 'not-applicable' } }],
175
+ }] },
176
+ empirical: { ...empiricalEvidence, classes: [{ ...empiricalEvidence.classes[0], outcome: 'coverage-unverifiable' }] },
177
+ }],
178
+ ['an unmapped class reported as a gap while the catalog is available', {
179
+ static: report.static,
180
+ empirical: { ...empiricalEvidence, classes: [{ ...empiricalEvidence.classes[0], defectClass: 'unknown-class', outcome: 'gap' }] },
181
+ }],
182
+ ['a mapped class reported as unmapped', {
183
+ static: report.static,
184
+ empirical: { ...empiricalEvidence, classes: [{ ...empiricalEvidence.classes[0], outcome: 'unmapped-class' }] },
185
+ }],
186
+ ])('renderers reject an empirical outcome incompatible with static coverage: %s', (_case, invalid) => {
187
+ const envelope = { ...report, overall: invalid.static.status, static: invalid.static, empirical: invalid.empirical };
188
+ for (const render of [render_1.renderCoverageJson, render_1.renderCoverageHuman]) {
189
+ expect(() => render(envelope)).toThrow(/^renderCoverage(?:Json|Human): invalid report/);
190
+ }
191
+ });
192
+ test('renderers reject non-unverifiable empirical outcomes when the catalog is unavailable', () => {
193
+ const unavailable = {
194
+ schemaVersion: 2, pack: null, registry: null, overall: 'inconclusive',
195
+ static: { status: 'inconclusive', reason: 'not_configured', classes: [] },
196
+ empirical: { ...empiricalEvidence, classes: [{ ...empiricalEvidence.classes[0], outcome: 'gap' }] },
197
+ };
198
+ for (const render of [render_1.renderCoverageJson, render_1.renderCoverageHuman]) {
199
+ expect(() => render(unavailable)).toThrow(/^renderCoverage(?:Json|Human): invalid report/);
200
+ }
35
201
  });
36
202
  test('not_configured names the remedy and no_reference stays distinct (R2.6)', () => {
37
- const notConfigured = { schemaVersion: 1, pack: null, registry: null, overall: 'inconclusive',
203
+ const notConfigured = { schemaVersion: 2, pack: null, registry: null, overall: 'inconclusive',
38
204
  static: { status: 'inconclusive', reason: 'not_configured', classes: [] } };
39
205
  expect((0, render_1.renderCoverageHuman)(notConfigured)).toContain('Run: awm sensors init');
40
206
  expect((0, render_1.renderCoverageHuman)({ ...notConfigured, pack: 'legacy', registry: 'baseline', static: { ...notConfigured.static, reason: 'no_reference' } }))
@@ -52,6 +218,7 @@ test('human output never renders structured detector evidence', () => {
52
218
  { kind: 'file', path: '.prettierrc', status: 'matched' },
53
219
  { kind: 'marker', path: '.prettierrc', ordinal: 1, status: 'missing' },
54
220
  ],
221
+ compatibility: report.static.classes[0].detectors[0].compatibility,
55
222
  }] }],
56
223
  },
57
224
  };
@@ -71,12 +238,20 @@ test('human output removes OSC controls from pack-provided text while retaining
71
238
  ...report.static.classes[0],
72
239
  description: `${osc8Open}Formatting${osc8Close}`,
73
240
  remedy: { summary: `Add ${osc8Open}formatter${osc8Close}`, command: 'npm i -D prettier' },
241
+ detectors: [{
242
+ ...report.static.classes[0].detectors[0],
243
+ compatibility: {
244
+ ...report.static.classes[0].detectors[0].compatibility,
245
+ reason: `tool ${osc8Open}version${osc8Close} is unsupported`,
246
+ },
247
+ }],
74
248
  }],
75
249
  },
76
250
  };
77
251
  const human = (0, render_1.renderCoverageHuman)(hostile);
78
- expect(human).toContain('missing formatting — Formatting');
252
+ expect(human).toContain('missing lint-errors — Formatting');
79
253
  expect(human).toContain(' remedy: Add formatter');
254
+ expect(human).toContain(' compatibility: missing-tool — tool version is unsupported');
80
255
  expect(human.replace(/\n/g, '')).not.toMatch(/[\u0000-\u001F\u007F-\u009F]/);
81
256
  });
82
257
  test.each([
@@ -41,7 +41,19 @@ test('no manifest returns not_configured without reading registries', () => {
41
41
  fs_1.default.rmSync(path_1.default.join(project, '.awm', 'sensors.json'), { force: true });
42
42
  fs_1.default.mkdirSync(awmHome, { recursive: true });
43
43
  fs_1.default.writeFileSync(path_1.default.join(awmHome, 'registries.json'), '{malformed');
44
- expect((0, resolve_1.resolveCoverageInputs)(project)).toEqual({ kind: 'not_configured' });
44
+ const originalLstat = fs_1.default.lstatSync;
45
+ const lstat = jest.spyOn(fs_1.default, 'lstatSync').mockImplementation(((file, ...args) => {
46
+ if (typeof file === 'string' && file.endsWith(path_1.default.join('.awm', 'sensors.json'))) {
47
+ throw Object.assign(new Error('missing manifest'), { code: 'ENOENT' });
48
+ }
49
+ return originalLstat(file, ...args);
50
+ }));
51
+ try {
52
+ expect((0, resolve_1.resolveCoverageInputs)(project)).toEqual({ kind: 'not_configured' });
53
+ }
54
+ finally {
55
+ lstat.mockRestore();
56
+ }
45
57
  });
46
58
  test('selects the first configured registry containing the exact pack', () => {
47
59
  writeManifest({ pack: 'js-ts', sensors: {} });
@@ -98,7 +110,7 @@ test.each([
98
110
  });
99
111
  test.each([
100
112
  ['pack-malformed', '{broken', /Invalid JSON.*pack\.json/],
101
- ['pack-oversize', Buffer.alloc(1024 * 1024 + 1), /pack\.json.*exceeds 1 MiB/],
113
+ ['pack-oversize', Buffer.alloc(1024 * 1024 + 1), /pack.*pack\.json.*exceeds the 1 MiB limit/],
102
114
  ])('rejects %s', (_name, body, expected) => {
103
115
  writeManifest({ pack: 'js-ts', sensors: {} });
104
116
  configure(['baseline']);
@@ -121,10 +133,19 @@ test('rejects a JSON object that is not a valid pack', () => {
121
133
  writeManifest({ pack: 'js-ts', sensors: {} });
122
134
  configure(['baseline']);
123
135
  writePack('baseline', 'js-ts', { coverage });
124
- expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(noFollowUnavailableOnNativeWindows ? safetyError : /Invalid pack.*name/);
136
+ expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(noFollowUnavailableOnNativeWindows ? safetyError : /Invalid sensor pack.*name/);
125
137
  });
126
138
  test('rejects registry names that are not safe path components', () => {
127
139
  writeManifest({ pack: 'js-ts', sensors: {} });
128
140
  configure(['']);
129
- expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(noFollowUnavailableOnNativeWindows ? safetyError : /Invalid registry name/);
141
+ expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(noFollowUnavailableOnNativeWindows ? safetyError : /malformed entry name|Invalid registry name/);
142
+ });
143
+ test.each([
144
+ [{ schemaVersion: 3, name: 'js-ts', sensors: {}, coverage: {} }, /supported: legacy, 2/],
145
+ [{ schemaVersion: 2, name: 'js-ts', sensors: {}, coverage: {} }, /sensors must be nonempty/],
146
+ ])('fails closed before reading coverage from invalid versioned packs', (pack, expected) => {
147
+ writeManifest({ pack: 'js-ts', sensors: {} });
148
+ configure(['baseline']);
149
+ writePack('baseline', 'js-ts', pack);
150
+ expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(noFollowUnavailableOnNativeWindows ? safetyError : expected);
130
151
  });
@@ -1,6 +1,12 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  const events_1 = require("events");
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const os_1 = __importDefault(require("os"));
9
+ const path_1 = __importDefault(require("path"));
4
10
  const child_process_1 = require("child_process");
5
11
  const exec_1 = require("../../../src/commands/sensors/exec");
6
12
  jest.mock('child_process', () => ({
@@ -21,12 +27,12 @@ function fakeChild(pid = 4242) {
21
27
  describe('runCommand — win32', () => {
22
28
  const originalPlatform = process.platform;
23
29
  beforeEach(() => {
24
- Object.defineProperty(process, 'platform', { value: 'win32' });
30
+ Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
25
31
  mockSpawn.mockReset();
26
32
  mockExecFile.mockReset();
27
33
  });
28
34
  afterEach(() => {
29
- Object.defineProperty(process, 'platform', { value: originalPlatform });
35
+ Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true });
30
36
  jest.useRealTimers();
31
37
  });
32
38
  it('spawns with detached: false — win32 has no POSIX process groups to detach into', async () => {
@@ -38,6 +44,90 @@ describe('runCommand — win32', () => {
38
44
  const r = await pending;
39
45
  expect(r.code).toBe(0);
40
46
  });
47
+ it('resolves a PATHEXT executable and keeps structured argv shell-free', async () => {
48
+ const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-structured-win-'));
49
+ const savedPath = process.env.PATH;
50
+ const savedPathExt = process.env.PATHEXT;
51
+ try {
52
+ fs_1.default.writeFileSync(path_1.default.join(dir, 'tool.exe'), 'fixture');
53
+ process.env.PATH = dir;
54
+ process.env.PATHEXT = '.CMD;.EXE';
55
+ const child = fakeChild();
56
+ mockSpawn.mockReturnValue(child);
57
+ const pending = (0, exec_1.runStructuredCommand)({ executable: 'tool', resolution: 'path', args: ['literal;&'] }, { timeout: 5000, cwd: dir });
58
+ expect(mockSpawn).toHaveBeenCalledWith(path_1.default.join(dir, 'tool.exe'), ['literal;&'], expect.objectContaining({ shell: false, detached: false }));
59
+ child.emit('close', 0, null);
60
+ await expect(pending).resolves.toMatchObject({ code: 0 });
61
+ expect(() => (0, exec_1.runStructuredCommand)({ executable: 'tool.cmd', resolution: 'path', args: [] }, { timeout: 5000, cwd: dir })).toThrow(/wrappers/);
62
+ }
63
+ finally {
64
+ process.env.PATH = savedPath;
65
+ process.env.PATHEXT = savedPathExt;
66
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
67
+ }
68
+ });
69
+ it('uses the project node_modules executable instead of a same-named global command', async () => {
70
+ const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-local-bin-win-'));
71
+ const global = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-global-bin-win-'));
72
+ const savedPath = process.env.PATH;
73
+ const savedPathExt = process.env.PATHEXT;
74
+ try {
75
+ const localBin = path_1.default.join(dir, 'node_modules', '.bin');
76
+ fs_1.default.mkdirSync(localBin, { recursive: true });
77
+ fs_1.default.writeFileSync(path_1.default.join(localBin, 'tool.exe'), 'local');
78
+ fs_1.default.writeFileSync(path_1.default.join(global, 'tool.exe'), 'global');
79
+ process.env.PATH = global;
80
+ process.env.PATHEXT = '.EXE';
81
+ const child = fakeChild();
82
+ mockSpawn.mockReturnValue(child);
83
+ const pending = (0, exec_1.runStructuredCommand)({ executable: 'tool', resolution: 'node-modules-bin', args: [] }, { timeout: 5000, cwd: dir });
84
+ expect(mockSpawn).toHaveBeenCalledWith(path_1.default.join(localBin, 'tool.exe'), [], expect.objectContaining({ shell: false }));
85
+ child.emit('close', 0, null);
86
+ await expect(pending).resolves.toMatchObject({ code: 0 });
87
+ }
88
+ finally {
89
+ process.env.PATH = savedPath;
90
+ process.env.PATHEXT = savedPathExt;
91
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
92
+ fs_1.default.rmSync(global, { recursive: true, force: true });
93
+ }
94
+ });
95
+ it('rejects a missing local executable instead of falling back to a same-named global command', () => {
96
+ const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-local-bin-missing-win-'));
97
+ const global = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-global-bin-missing-win-'));
98
+ const savedPath = process.env.PATH;
99
+ try {
100
+ fs_1.default.writeFileSync(path_1.default.join(global, 'tool.exe'), 'global');
101
+ process.env.PATH = global;
102
+ mockSpawn.mockReturnValue(fakeChild());
103
+ expect(() => (0, exec_1.runStructuredCommand)({ executable: 'tool', resolution: 'node-modules-bin', args: [] }, { timeout: 5000, cwd: dir }))
104
+ .toThrow(/node_modules.*not found locally/i);
105
+ expect(mockSpawn).not.toHaveBeenCalled();
106
+ }
107
+ finally {
108
+ process.env.PATH = savedPath;
109
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
110
+ fs_1.default.rmSync(global, { recursive: true, force: true });
111
+ }
112
+ });
113
+ it('rejects a missing Python environment executable instead of falling back to a same-named global command', () => {
114
+ const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-python-env-missing-win-'));
115
+ const global = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-python-env-global-win-'));
116
+ const savedPath = process.env.PATH;
117
+ try {
118
+ fs_1.default.writeFileSync(path_1.default.join(global, 'semgrep.exe'), 'global');
119
+ process.env.PATH = global;
120
+ mockSpawn.mockReturnValue(fakeChild());
121
+ expect(() => (0, exec_1.runStructuredCommand)({ executable: 'semgrep', resolution: 'python-environment', args: ['--validate'] }, { timeout: 5000, cwd: dir }))
122
+ .toThrow(/python.*environment.*local|contained/i);
123
+ expect(mockSpawn).not.toHaveBeenCalled();
124
+ }
125
+ finally {
126
+ process.env.PATH = savedPath;
127
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
128
+ fs_1.default.rmSync(global, { recursive: true, force: true });
129
+ }
130
+ });
41
131
  it('propagates cmd.exe\'s own exit code for a command that does not exist (1, not the POSIX 127)', async () => {
42
132
  // Mirrors exec.test.ts's POSIX "reports 127" case. cmd.exe has no
43
133
  // equivalent 127 convention — it reports 1 (sometimes 9009) for
@@ -20,6 +20,25 @@ async function until(fn, budgetMs = 4000) {
20
20
  return fn();
21
21
  }
22
22
  describe('runCommand — exit codes and output', () => {
23
+ it('passes structured metacharacters literally without a shell', async () => {
24
+ const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-exec-argv-'));
25
+ const marker = path_1.default.join(dir, 'must-not-exist');
26
+ try {
27
+ const literal = `;touch ${marker}`;
28
+ const received = path_1.default.join(dir, 'received');
29
+ const result = await (0, exec_1.runStructuredCommand)({
30
+ executable: process.execPath,
31
+ resolution: 'path',
32
+ args: ['-e', "require('fs').writeFileSync(process.argv[1], process.argv[2])", received, literal],
33
+ }, { timeout: 5000, cwd: dir });
34
+ expect(result.code).toBe(0);
35
+ expect(fs_1.default.readFileSync(received, 'utf8')).toBe(literal);
36
+ expect(fs_1.default.existsSync(marker)).toBe(false);
37
+ }
38
+ finally {
39
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
40
+ }
41
+ });
23
42
  it('returns stdout and code 0 for a clean command', async () => {
24
43
  const r = await (0, exec_1.runCommand)('echo hello', { timeout: 5000, cwd: process.cwd() });
25
44
  expect(r.code).toBe(0);
@@ -27,7 +46,7 @@ describe('runCommand — exit codes and output', () => {
27
46
  expect(r.timedOut).toBe(false);
28
47
  expect(r.overflowed).toBe(false);
29
48
  });
30
- it('captures stderr and a non-zero exit code without throwing', async () => {
49
+ itPosix('captures stderr and a non-zero exit code without throwing', async () => {
31
50
  // Portable by construction: `node -e "..."` is invoked identically by
32
51
  // `spawn(cmd, {shell:true})` on both `/bin/sh -c` (POSIX) and
33
52
  // `cmd.exe /d /s /c` (win32) — the shell only tokenizes the outer
@@ -38,7 +57,7 @@ describe('runCommand — exit codes and output', () => {
38
57
  // separator there, so the whole string became literal arguments to
39
58
  // `echo` and `exit 3` never ran as its own command — the run
40
59
  // "succeeded" with code 0 instead of 3 on windows-latest CI.
41
- const r = await (0, exec_1.runCommand)(`node -e "process.stderr.write('oops'); process.exit(3)"`, { timeout: 5000, cwd: process.cwd() });
60
+ const r = await (0, exec_1.runCommand)('printf oops >&2; exit 3', { timeout: 5000, cwd: process.cwd() });
42
61
  expect(r.code).toBe(3);
43
62
  expect(r.stderr).toMatch(/oops/);
44
63
  expect(r.timedOut).toBe(false);
@@ -53,7 +72,7 @@ describe('runCommand — exit codes and output', () => {
53
72
  expect(r.code).toBe(127);
54
73
  });
55
74
  });
56
- describe('runCommand — output cap', () => {
75
+ onPosix('runCommand — output cap', () => {
57
76
  it('stops at maxBuffer, flags overflow, and keeps what it read', async () => {
58
77
  // 200 lines of ~50 bytes each, capped at 1KB. A `for i in $(seq ...); do
59
78
  // ... done` POSIX shell loop silently no-ops under cmd.exe (win32's
@@ -64,11 +83,11 @@ describe('runCommand — output cap', () => {
64
83
  // r.overflowed came back false). A `node -e` one-liner is invoked
65
84
  // identically by both shells (same portability reasoning as the
66
85
  // exit-code test above).
67
- const r = await (0, exec_1.runCommand)(`node -e "for(let i=1;i<=200;i++){console.log('line-'+i+'-padding-padding-padding-padding')}"`, { timeout: 10_000, cwd: process.cwd(), maxBuffer: 1024 });
86
+ const r = await (0, exec_1.runCommand)("yes 'line-padding-padding-padding-padding'", { timeout: 10_000, cwd: process.cwd(), maxBuffer: 1024 });
68
87
  expect(r.overflowed).toBe(true);
69
88
  expect(r.stdout.length).toBeLessThanOrEqual(1024);
70
89
  // The point of the cap change: what was read is still usable, not discarded.
71
- expect(r.stdout).toMatch(/line-1-/);
90
+ expect(r.stdout).toMatch(/line-padding/);
72
91
  });
73
92
  });
74
93
  onPosix('runCommand — timeout', () => {
@@ -111,3 +130,46 @@ describe('runCommand — spawn failure', () => {
111
130
  expect(r.code).not.toBe(0);
112
131
  });
113
132
  });
133
+ onPosix('runStructuredCommand — local node_modules binaries', () => {
134
+ it('follows an npm-style contained shim but rejects one escaping node_modules', async () => {
135
+ const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-local-bin-symlink-'));
136
+ const modules = path_1.default.join(dir, 'node_modules');
137
+ const bin = path_1.default.join(modules, '.bin');
138
+ const contained = path_1.default.join(modules, 'tool', 'bin', 'tool');
139
+ const outside = path_1.default.join(dir, 'outside-tool');
140
+ try {
141
+ fs_1.default.mkdirSync(path_1.default.dirname(contained), { recursive: true });
142
+ fs_1.default.writeFileSync(contained, '#!/bin/sh\necho contained\n', { mode: 0o755 });
143
+ fs_1.default.writeFileSync(outside, '#!/bin/sh\necho outside\n', { mode: 0o755 });
144
+ fs_1.default.mkdirSync(bin, { recursive: true });
145
+ fs_1.default.symlinkSync('../tool/bin/tool', path_1.default.join(bin, 'tool'));
146
+ await expect((0, exec_1.runStructuredCommand)({ executable: 'tool', resolution: 'node-modules-bin', args: [] }, { timeout: 5000, cwd: dir }))
147
+ .resolves.toMatchObject({ code: 0, stdout: 'contained\n' });
148
+ fs_1.default.unlinkSync(path_1.default.join(bin, 'tool'));
149
+ fs_1.default.symlinkSync(outside, path_1.default.join(bin, 'tool'));
150
+ expect(() => (0, exec_1.runStructuredCommand)({ executable: 'tool', resolution: 'node-modules-bin', args: [] }, { timeout: 5000, cwd: dir }))
151
+ .toThrow(/node_modules.*contain|local/i);
152
+ }
153
+ finally {
154
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
155
+ }
156
+ });
157
+ });
158
+ onPosix('runStructuredCommand — Python environments', () => {
159
+ it('rejects a missing project Python executable instead of falling back to PATH', () => {
160
+ const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-python-env-missing-'));
161
+ const global = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-python-env-global-'));
162
+ const savedPath = process.env.PATH;
163
+ try {
164
+ fs_1.default.writeFileSync(path_1.default.join(global, 'semgrep'), '#!/bin/sh\necho global\n', { mode: 0o755 });
165
+ process.env.PATH = global;
166
+ expect(() => (0, exec_1.runStructuredCommand)({ executable: 'semgrep', resolution: 'python-environment', args: ['--validate'] }, { timeout: 5000, cwd: dir }))
167
+ .toThrow(/python.*environment.*local|contained/i);
168
+ }
169
+ finally {
170
+ process.env.PATH = savedPath;
171
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
172
+ fs_1.default.rmSync(global, { recursive: true, force: true });
173
+ }
174
+ });
175
+ });
@@ -52,6 +52,14 @@ describe('sensors coverage Commander wiring', () => {
52
52
  expect(render_1.renderCoverageHuman).toHaveBeenCalledWith(report);
53
53
  expect(stdoutWrite).toHaveBeenCalledWith('human\n');
54
54
  });
55
+ it.each(['0', '-1', '1.5', '2x', 'Infinity', 'NaN', '9007199254740992'])('Commander rejects unsafe --min %s before coverage I/O (R5.5)', async (value) => {
56
+ await expect(programWithSensors().parseAsync(['node', 'awm', 'sensors', 'coverage', '--min', value])).rejects.toThrow('--min must be a positive safe integer');
57
+ expect(coverage_1.runCoverage).not.toHaveBeenCalled();
58
+ });
59
+ it('passes --min as a positive integer to coverage', async () => {
60
+ await programWithSensors().parseAsync(['node', 'awm', 'sensors', 'coverage', '--min', '3']);
61
+ expect(coverage_1.runCoverage).toHaveBeenCalledWith(process.cwd(), {}, { min: 3 });
62
+ });
55
63
  it('emits JSON for --json and does not exit for gaps or inconclusive (R2.9)', async () => {
56
64
  for (const overall of ['gaps', 'inconclusive']) {
57
65
  coverage_1.runCoverage.mockReturnValue({ ...report, overall, static: { ...report.static, status: overall } });