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.
@@ -0,0 +1,126 @@
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
+ exports.readBoundedJson = readBoundedJson;
7
+ exports.resolveCoverageInputs = resolveCoverageInputs;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const registries_1 = require("../../../core/registries");
11
+ const contract_1 = require("./contract");
12
+ function readFailure(file, error) {
13
+ return new Error(`Cannot read ${file}: ${error instanceof Error ? error.message : String(error)}`);
14
+ }
15
+ /** Read JSON from a regular, bounded file without ever following a symlink. */
16
+ function readBoundedJson(file) {
17
+ if (typeof file !== 'string' || file.trim().length === 0)
18
+ throw new Error('readBoundedJson: file must be a non-empty string');
19
+ let listed;
20
+ try {
21
+ listed = fs_1.default.lstatSync(file);
22
+ }
23
+ catch (error) {
24
+ throw readFailure(file, error);
25
+ }
26
+ if (!listed.isFile() || listed.isSymbolicLink())
27
+ throw new Error(`Cannot read ${file}: expected a regular file`);
28
+ if (listed.size > contract_1.MAX_COVERAGE_FILE_BYTES)
29
+ throw new Error(`Cannot read ${file}: exceeds 1 MiB limit`);
30
+ const noFollow = fs_1.default.constants.O_NOFOLLOW;
31
+ if (typeof noFollow !== 'number')
32
+ throw new Error(`Cannot read ${file}: platform cannot guarantee no symlink dereference`);
33
+ let descriptor;
34
+ let content;
35
+ try {
36
+ descriptor = fs_1.default.openSync(file, fs_1.default.constants.O_RDONLY | noFollow);
37
+ const opened = fs_1.default.fstatSync(descriptor);
38
+ if (!opened.isFile() || opened.size > contract_1.MAX_COVERAGE_FILE_BYTES) {
39
+ throw new Error('expected a regular file within the 1 MiB limit');
40
+ }
41
+ const buffer = Buffer.allocUnsafe(contract_1.MAX_COVERAGE_FILE_BYTES + 1);
42
+ const count = fs_1.default.readSync(descriptor, buffer, 0, buffer.length, null);
43
+ if (!Number.isSafeInteger(count) || count < 0 || count > contract_1.MAX_COVERAGE_FILE_BYTES) {
44
+ throw new Error('exceeds 1 MiB limit');
45
+ }
46
+ content = buffer.subarray(0, count).toString('utf8');
47
+ }
48
+ catch (error) {
49
+ throw readFailure(file, error);
50
+ }
51
+ finally {
52
+ if (descriptor !== undefined)
53
+ fs_1.default.closeSync(descriptor);
54
+ }
55
+ try {
56
+ return JSON.parse(content);
57
+ }
58
+ catch (error) {
59
+ throw new Error(`Invalid JSON at ${file}: ${error instanceof Error ? error.message : String(error)}`);
60
+ }
61
+ }
62
+ function readPackEnvelope(input, file, expectedName) {
63
+ if (typeof input !== 'object' || input === null || Array.isArray(input))
64
+ throw new Error(`Invalid pack at ${file}: expected object`);
65
+ const pack = input;
66
+ if (typeof pack.name !== 'string' || pack.name !== expectedName) {
67
+ throw new Error(`Invalid pack at ${file}: name must equal '${expectedName}'`);
68
+ }
69
+ if (typeof pack.sensors !== 'object' || pack.sensors === null || Array.isArray(pack.sensors)) {
70
+ throw new Error(`Invalid pack at ${file}: sensors must be an object`);
71
+ }
72
+ return 'coverage' in pack ? { coverage: pack.coverage } : {};
73
+ }
74
+ function safeRegistryName(name) {
75
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) || name.includes('..')) {
76
+ throw new Error(`Invalid registry name '${name}': expected a safe path component`);
77
+ }
78
+ }
79
+ /** Finds the nearest manifest without following a symlink during discovery. */
80
+ function findManifestDirNoFollow(startCwd) {
81
+ let dir = path_1.default.resolve(startCwd);
82
+ while (true) {
83
+ const manifestPath = path_1.default.join(dir, '.awm', 'sensors.json');
84
+ try {
85
+ fs_1.default.lstatSync(manifestPath);
86
+ return dir;
87
+ }
88
+ catch (error) {
89
+ if (error.code !== 'ENOENT')
90
+ throw readFailure(manifestPath, error);
91
+ }
92
+ const parent = path_1.default.dirname(dir);
93
+ if (parent === dir)
94
+ return null;
95
+ dir = parent;
96
+ }
97
+ }
98
+ function resolveCoverageInputs(cwd) {
99
+ if (typeof cwd !== 'string' || cwd.trim().length === 0)
100
+ throw new Error('resolveCoverageInputs: cwd must be a non-empty string');
101
+ const projectRoot = findManifestDirNoFollow(cwd);
102
+ if (!projectRoot)
103
+ return { kind: 'not_configured' };
104
+ const manifestPath = path_1.default.join(projectRoot, '.awm', 'sensors.json');
105
+ const manifest = (0, contract_1.parseCoverageManifest)(readBoundedJson(manifestPath), manifestPath);
106
+ for (const registry of (0, registries_1.listRegistries)()) {
107
+ safeRegistryName(registry.name);
108
+ const packPath = path_1.default.join(registry.contentRoot, 'sensor-packs', manifest.pack, 'pack.json');
109
+ try {
110
+ fs_1.default.lstatSync(packPath);
111
+ }
112
+ catch (error) {
113
+ if (error.code === 'ENOENT')
114
+ continue;
115
+ throw readFailure(packPath, error);
116
+ }
117
+ const { coverage } = readPackEnvelope(readBoundedJson(packPath), packPath, manifest.pack);
118
+ if (coverage === undefined)
119
+ return { kind: 'no_reference', projectRoot, pack: manifest.pack, registry: registry.name, manifest };
120
+ return {
121
+ kind: 'ready', projectRoot, pack: manifest.pack, registry: registry.name,
122
+ manifest, contract: (0, contract_1.parseCoverageContract)(coverage, packPath),
123
+ };
124
+ }
125
+ throw new Error(`Pack '${manifest.pack}' was not found in configured registries`);
126
+ }
@@ -12,6 +12,8 @@ const init_1 = require("./init");
12
12
  const status_1 = require("./status");
13
13
  const install_1 = require("./install");
14
14
  const baseline_1 = require("./baseline");
15
+ const coverage_1 = require("./coverage");
16
+ const render_1 = require("./coverage/render");
15
17
  const registries_1 = require("../../core/registries");
16
18
  /** Map a sensor run verdict to a process exit code. fail → 1; everything else → 0.
17
19
  * not_certified intentionally exits 0: its signal lives in `overall`, because
@@ -21,6 +23,20 @@ function exitCodeFor(output) {
21
23
  }
22
24
  function registerSensorsCommand(program) {
23
25
  const sensors = program.command('sensors').description('manage computational sensors for the current project');
26
+ sensors
27
+ .command('coverage')
28
+ .description('report static gaps between configured sensors and the pack reference')
29
+ .option('--json', 'emit the versioned machine-readable envelope')
30
+ .action((opts) => {
31
+ try {
32
+ const report = (0, coverage_1.runCoverage)(process.cwd());
33
+ process.stdout.write(opts.json ? (0, render_1.renderCoverageJson)(report) : (0, render_1.renderCoverageHuman)(report));
34
+ }
35
+ catch (error) {
36
+ prompts_1.log.error(error instanceof Error ? error.message : String(error));
37
+ process.exit(1);
38
+ }
39
+ });
24
40
  sensors
25
41
  .command('run')
26
42
  .description('run sensors from .awm/sensors.json')
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const contract_1 = require("../../../../src/commands/sensors/coverage/contract");
4
+ describe('coverage contract v1', () => {
5
+ it('returns a complete valid contract unchanged', () => {
6
+ const input = {
7
+ schemaVersion: 1,
8
+ classes: {
9
+ 'runtime-validation': {
10
+ description: 'All durable coverage artifacts validate their inputs.',
11
+ detectors: [{
12
+ sensor: 'contract-test',
13
+ evidence: {
14
+ commandIncludes: ['coverage'],
15
+ files: [{ path: 'contract.ts', containsAll: ['parseCoverageContract'] }],
16
+ },
17
+ }],
18
+ remedy: { summary: 'Add a parser.', command: 'npm test -- contract.test.ts' },
19
+ },
20
+ },
21
+ };
22
+ expect((0, contract_1.parseCoverageContract)(input, 'coverage.json')).toEqual(input);
23
+ });
24
+ test.each([
25
+ [{ schemaVersion: 2, classes: {} }, 'schemaVersion'],
26
+ [{ schemaVersion: 1, classes: {}, extra: true }, 'unknown field'],
27
+ [{ schemaVersion: 1, classes: {} }, 'classes'],
28
+ [{ schemaVersion: 1, classes: { Bad: { description: 'x', detectors: [{ sensor: 'test' }], remedy: { summary: 'x', command: 'x' } } } }, 'class'],
29
+ [{ schemaVersion: 1, classes: { valid: { description: '', detectors: [{ sensor: 'test' }], remedy: { summary: 'x', command: 'x' } } } }, 'description'],
30
+ [{ schemaVersion: 1, classes: { valid: { description: ' \t', detectors: [{ sensor: 'test' }], remedy: { summary: 'x', command: 'x' } } } }, 'description'],
31
+ [{ schemaVersion: 1, classes: { valid: { description: 'x', detectors: [], remedy: { summary: 'x', command: 'x' } } } }, 'detectors'],
32
+ ])('rejects malformed contract %j', (input, message) => {
33
+ expect(() => (0, contract_1.parseCoverageContract)(input, 'coverage.json')).toThrow(message);
34
+ });
35
+ test.each(['', '.', '..', 'a..b', '../secret', 'a/../../secret', '/etc/passwd', 'C:\\secret', 'a\\..\\secret', ' report.txt', 'report!.txt', 'ñ.txt', '.env', '.gitignore'])('rejects hostile evidence path %p', (path) => {
36
+ const input = {
37
+ schemaVersion: 1,
38
+ classes: {
39
+ valid: {
40
+ description: 'x',
41
+ detectors: [{ sensor: 'test', evidence: { files: [{ path, containsAll: [] }] } }],
42
+ remedy: { summary: 'x', command: 'x' },
43
+ },
44
+ },
45
+ };
46
+ expect(() => (0, contract_1.parseCoverageContract)(input, 'coverage.json')).toThrow('path');
47
+ });
48
+ test.each(['.semgrep.awm.yml', '.dep-cruiser.awm.js'])('accepts safe dotfile evidence path %p', (path) => {
49
+ const input = {
50
+ schemaVersion: 1,
51
+ classes: {
52
+ valid: {
53
+ description: 'x',
54
+ detectors: [{ sensor: 'test', evidence: { files: [{ path, containsAll: [] }] } }],
55
+ remedy: { summary: 'x', command: 'x' },
56
+ },
57
+ },
58
+ };
59
+ expect((0, contract_1.parseCoverageContract)(input, 'coverage.json')).toEqual(input);
60
+ });
61
+ it('rejects whitespace-only evidence text', () => {
62
+ const input = {
63
+ schemaVersion: 1,
64
+ classes: {
65
+ valid: {
66
+ description: 'x',
67
+ detectors: [{ sensor: 'test', evidence: { files: [{ path: 'report.txt', containsAll: [' \n'] }] } }],
68
+ remedy: { summary: 'x', command: 'x' },
69
+ },
70
+ },
71
+ };
72
+ expect(() => (0, contract_1.parseCoverageContract)(input, 'coverage.json')).toThrow('containsAll');
73
+ });
74
+ it('rejects unknown nested evidence fields', () => {
75
+ const input = {
76
+ schemaVersion: 1,
77
+ classes: {
78
+ valid: {
79
+ description: 'x',
80
+ detectors: [{ sensor: 'test', evidence: { commandInclude: ['coverage'] } }],
81
+ remedy: { summary: 'x', command: 'x' },
82
+ },
83
+ },
84
+ };
85
+ expect(() => (0, contract_1.parseCoverageContract)(input, 'coverage.json')).toThrow('unknown field');
86
+ });
87
+ });
88
+ describe('coverage manifest boundary', () => {
89
+ it('accepts all legacy sensor fields', () => {
90
+ const input = {
91
+ pack: 'js-ts',
92
+ concurrency: 2,
93
+ sensors: {
94
+ lint: {
95
+ cmd: 'npm run lint', fast: true, enabled: true, timeout: 120, changedCmd: 'npm run lint -- {files}', changedExtensions: ['.ts'], formatter: 'eslint-llm',
96
+ },
97
+ },
98
+ };
99
+ expect((0, contract_1.parseCoverageManifest)(input, 'sensors.json')).toEqual(input);
100
+ });
101
+ test.each([
102
+ [null, 'object'],
103
+ [{}, 'pack'],
104
+ [{ pack: '', sensors: {} }, 'pack'],
105
+ [{ pack: ' js-ts', sensors: {} }, 'pack'],
106
+ [{ pack: 'js ts', sensors: {} }, 'pack'],
107
+ [{ pack: 'js@ts', sensors: {} }, 'pack'],
108
+ [{ pack: 'js-ts', sensors: null }, 'sensors'],
109
+ [{ pack: 'js-ts', sensors: { lint: { cmd: 3 } } }, 'cmd'],
110
+ [{ pack: 'js-ts', sensors: { 'lint!': {} } }, 'sensor name'],
111
+ ])('rejects malformed manifest %j', (input, message) => {
112
+ expect(() => (0, contract_1.parseCoverageManifest)(input, 'sensors.json')).toThrow(message);
113
+ });
114
+ });
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const evaluate_1 = require("../../../../src/commands/sensors/coverage/evaluate");
4
+ const contract = {
5
+ schemaVersion: 1,
6
+ classes: {
7
+ alpha: {
8
+ description: 'Alpha',
9
+ detectors: [{ sensor: 'one' }, { sensor: 'two' }],
10
+ remedy: { summary: 'Fix alpha', command: 'fix alpha' },
11
+ },
12
+ zeta: {
13
+ description: 'Zeta',
14
+ detectors: [{ sensor: 'three' }],
15
+ remedy: { summary: 'Fix zeta', command: 'fix zeta' },
16
+ },
17
+ },
18
+ };
19
+ const observation = (classId, detectorIndex, sensor, status) => ({ classId, detectorIndex, sensor, status, evidence: [] });
20
+ describe('coverage evaluation', () => {
21
+ test.each([
22
+ [[observation('alpha', 0, 'one', 'covered'), observation('alpha', 1, 'two', 'missing')], 'covered'],
23
+ [[observation('alpha', 0, 'one', 'missing'), observation('alpha', 1, 'two', 'disabled')], 'missing'],
24
+ [[observation('alpha', 0, 'one', 'ineffective'), observation('alpha', 1, 'two', 'missing')], 'missing'],
25
+ [[observation('alpha', 0, 'one', 'unverifiable'), observation('alpha', 1, 'two', 'missing')], 'unverifiable'],
26
+ ])('reduces detector alternatives %j to %s', (alpha, expected) => {
27
+ const result = (0, evaluate_1.evaluateCoverage)(contract, [...alpha, observation('zeta', 0, 'three', 'covered')]);
28
+ expect(result.classes.find((item) => item.id === 'alpha')?.status).toBe(expected);
29
+ });
30
+ it('makes global gaps outrank unverifiable while preserving both classes', () => {
31
+ const result = (0, evaluate_1.evaluateCoverage)(contract, [
32
+ observation('alpha', 0, 'one', 'unverifiable'),
33
+ observation('alpha', 1, 'two', 'missing'),
34
+ observation('zeta', 0, 'three', 'missing'),
35
+ ]);
36
+ expect(result.overall).toBe('gaps');
37
+ expect(result.classes.map((item) => [item.id, item.status])).toEqual([
38
+ ['alpha', 'unverifiable'],
39
+ ['zeta', 'missing'],
40
+ ]);
41
+ });
42
+ it('sorts classes by stable ID and is deterministic under reordered observations', () => {
43
+ const result = (0, evaluate_1.evaluateCoverage)(contract, [
44
+ observation('zeta', 0, 'three', 'covered'),
45
+ observation('alpha', 1, 'two', 'missing'),
46
+ observation('alpha', 0, 'one', 'covered'),
47
+ ]);
48
+ expect(result.classes.map((item) => item.id)).toEqual(['alpha', 'zeta']);
49
+ expect((0, evaluate_1.evaluateCoverage)(contract, [
50
+ observation('alpha', 0, 'one', 'covered'),
51
+ observation('zeta', 0, 'three', 'covered'),
52
+ observation('alpha', 1, 'two', 'missing'),
53
+ ])).toEqual(result);
54
+ });
55
+ it('fails loudly when an observation is missing or duplicated', () => {
56
+ expect(() => (0, evaluate_1.evaluateCoverage)(contract, [])).toThrow(/missing observation.*one/);
57
+ expect(() => (0, evaluate_1.evaluateCoverage)(contract, [
58
+ observation('alpha', 0, 'one', 'covered'),
59
+ observation('alpha', 0, 'one', 'covered'),
60
+ observation('alpha', 1, 'two', 'covered'),
61
+ observation('zeta', 0, 'three', 'covered'),
62
+ ])).toThrow(/duplicate observation.*alpha:0/);
63
+ });
64
+ it('keeps alternatives with the same sensor independent by detector index', () => {
65
+ const sameSensor = {
66
+ schemaVersion: 1,
67
+ classes: {
68
+ config: {
69
+ description: 'Project configuration',
70
+ detectors: [{ sensor: 'lint' }, { sensor: 'lint' }],
71
+ remedy: { summary: 'Add config', command: 'touch eslint.config.js' },
72
+ },
73
+ },
74
+ };
75
+ const result = (0, evaluate_1.evaluateCoverage)(sameSensor, [
76
+ observation('config', 0, 'lint', 'ineffective'),
77
+ observation('config', 1, 'lint', 'covered'),
78
+ ]);
79
+ expect(result.classes[0].status).toBe('covered');
80
+ expect(result.classes[0].detectors).toHaveLength(2);
81
+ });
82
+ });
@@ -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
+ });