agentic-workflow-manager 8.1.3 → 8.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/preflight/checks.js +70 -1
- package/dist/src/commands/preflight/index.js +6 -5
- package/dist/src/commands/sensors/changed.js +15 -0
- package/dist/src/commands/sensors/compatibility/contract.js +17 -3
- package/dist/src/commands/sensors/compatibility/manifest.js +16 -4
- package/dist/src/commands/sensors/compatibility/timeout.js +21 -0
- package/dist/src/commands/sensors/exec.js +3 -1
- package/dist/src/commands/sensors/index.js +5 -13
- package/dist/src/commands/sensors/init.js +1 -0
- package/dist/src/commands/sensors/prepare.js +136 -0
- package/dist/src/commands/sensors/result.js +140 -0
- package/dist/src/commands/sensors/run.js +56 -335
- package/dist/src/commands/sensors/status.js +93 -21
- package/dist/src/commands/sensors/verdict.js +27 -0
- package/dist/src/release/index.js +13 -0
- package/dist/src/release/orchestrator.js +2 -1
- package/dist/tests/commands/preflight/preflight.test.js +50 -0
- package/dist/tests/commands/sensors/baseline.test.js +14 -0
- package/dist/tests/commands/sensors/changed-windows.test.js +3 -0
- package/dist/tests/commands/sensors/compatibility/contract.test.js +51 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +27 -11
- package/dist/tests/commands/sensors/compatibility/probe.test.js +13 -3
- package/dist/tests/commands/sensors/exec-fixtures.js +1 -1
- package/dist/tests/commands/sensors/exec.test.js +17 -0
- package/dist/tests/commands/sensors/index.test.js +46 -8
- package/dist/tests/commands/sensors/init.test.js +30 -1
- package/dist/tests/commands/sensors/prepare.test.js +88 -0
- package/dist/tests/commands/sensors/router.test.js +15 -1
- package/dist/tests/commands/sensors/run-changed.test.js +4 -4
- package/dist/tests/commands/sensors/run.test.js +29 -0
- package/dist/tests/commands/sensors/status-windows.test.js +1 -1
- package/dist/tests/commands/sensors/status.test.js +52 -20
- package/dist/tests/integration/preflight-json-pipe.e2e.test.js +79 -3
- package/dist/tests/integration/sensor-compatibility.e2e.test.js +40 -1
- package/dist/tests/release/orchestrator.test.js +25 -1
- package/dist/tests/structural/sensor-documentation-contract.test.js +33 -5
- package/dist/tests/structural/support-matrix-is-current.test.js +25 -0
- package/package.json +1 -1
|
@@ -40,6 +40,7 @@ function parseArgs(argv) {
|
|
|
40
40
|
}
|
|
41
41
|
function realIO(repoRoot, cliDir) {
|
|
42
42
|
const pkgPath = path_1.default.join(cliDir, 'package.json');
|
|
43
|
+
const lockPath = path_1.default.join(cliDir, 'package-lock.json');
|
|
43
44
|
const changelogPath = path_1.default.join(repoRoot, 'CHANGELOG.md');
|
|
44
45
|
const npmrcPath = path_1.default.join(cliDir, '.npmrc');
|
|
45
46
|
return {
|
|
@@ -52,6 +53,18 @@ function realIO(repoRoot, cliDir) {
|
|
|
52
53
|
pkg.version = v;
|
|
53
54
|
fs_1.default.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
54
55
|
},
|
|
56
|
+
// El round-trip JSON.parse/stringify preserva el orden de claves y el formato que
|
|
57
|
+
// npm escribe (2 espacios + newline final), asi que el diff de un bump son las dos
|
|
58
|
+
// lineas de version y nada mas — verificado sobre el lockfile real de este repo.
|
|
59
|
+
writeLockVersion: (v) => {
|
|
60
|
+
if (!fs_1.default.existsSync(lockPath))
|
|
61
|
+
return;
|
|
62
|
+
const lock = JSON.parse(fs_1.default.readFileSync(lockPath, 'utf8'));
|
|
63
|
+
lock.version = v;
|
|
64
|
+
if (lock.packages?.[''])
|
|
65
|
+
lock.packages[''].version = v;
|
|
66
|
+
fs_1.default.writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n');
|
|
67
|
+
},
|
|
55
68
|
readChangelog: () => (fs_1.default.existsSync(changelogPath) ? fs_1.default.readFileSync(changelogPath, 'utf8') : ''),
|
|
56
69
|
writeChangelog: (c) => fs_1.default.writeFileSync(changelogPath, c),
|
|
57
70
|
writeNpmrc: (token) => fs_1.default.writeFileSync(npmrcPath, `//registry.npmjs.org/:_authToken=${token}\n`),
|
|
@@ -65,9 +65,10 @@ function release(opts, io) {
|
|
|
65
65
|
}
|
|
66
66
|
// aplicar
|
|
67
67
|
io.writePackageVersion(version);
|
|
68
|
+
io.writeLockVersion(version);
|
|
68
69
|
const section = (0, core_1.renderChangelog)(version, io.today(), commits);
|
|
69
70
|
io.writeChangelog(section + '\n' + io.readChangelog());
|
|
70
|
-
io.run('git', ['add', 'cli/package.json', 'CHANGELOG.md']);
|
|
71
|
+
io.run('git', ['add', 'cli/package.json', 'cli/package-lock.json', 'CHANGELOG.md']);
|
|
71
72
|
io.run('git', ['commit', '-m', `chore(release): v${version} [skip ci]`]);
|
|
72
73
|
io.run('git', ['tag', '-a', `v${version}`, '-m', `v${version}`]);
|
|
73
74
|
// OIDC mode: GitHub inyecta ACTIONS_ID_TOKEN_REQUEST_URL con id-token:write
|
|
@@ -9,6 +9,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const child_process_1 = require("child_process");
|
|
10
10
|
const checks_1 = require("../../../src/commands/preflight/checks");
|
|
11
11
|
const preflight_1 = require("../../../src/commands/preflight");
|
|
12
|
+
const run_1 = require("../../../src/commands/sensors/run");
|
|
12
13
|
// Only `execSync` (used by `resolveOnPath` to check for `gh`/`glab`) is mocked — `git
|
|
13
14
|
// remote get-url origin` runs for real via `execFileSync` against real tmpdir git repos,
|
|
14
15
|
// same as every other check in this file exercises the real filesystem.
|
|
@@ -16,7 +17,12 @@ jest.mock('child_process', () => ({
|
|
|
16
17
|
...jest.requireActual('child_process'),
|
|
17
18
|
execSync: jest.fn(),
|
|
18
19
|
}));
|
|
20
|
+
jest.mock('../../../src/commands/sensors/run', () => ({
|
|
21
|
+
...jest.requireActual('../../../src/commands/sensors/run'),
|
|
22
|
+
runSensors: jest.fn(),
|
|
23
|
+
}));
|
|
19
24
|
const mockExecSync = child_process_1.execSync;
|
|
25
|
+
const mockRunSensors = run_1.runSensors;
|
|
20
26
|
/** Turn a tmpdir into a real git repo with (optionally) an `origin` remote. */
|
|
21
27
|
function gitRepo(dir, remoteUrl) {
|
|
22
28
|
(0, child_process_1.execFileSync)('git', ['init'], { cwd: dir, stdio: 'pipe' });
|
|
@@ -47,6 +53,50 @@ const make = (o) => { const d = project(o); dirs.push(d); return d; };
|
|
|
47
53
|
afterAll(() => dirs.forEach(d => fs_1.default.rmSync(d, { recursive: true, force: true })));
|
|
48
54
|
const check = (r, id) => r.checks.find(c => c.id === id);
|
|
49
55
|
describe('preflight', () => {
|
|
56
|
+
afterEach(() => mockRunSensors.mockReset());
|
|
57
|
+
it('keeps default preflight static and does not dispatch sensors', async () => {
|
|
58
|
+
const dir = make({
|
|
59
|
+
manifest: { pack: 'generic', sensors: { security: { enabled: false } } },
|
|
60
|
+
});
|
|
61
|
+
await (0, checks_1.preflight)(dir);
|
|
62
|
+
expect(mockRunSensors).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
it('rejects an array passed as public preflight options before filesystem work', async () => {
|
|
65
|
+
await expect((0, checks_1.preflight)(process.cwd(), []))
|
|
66
|
+
.rejects.toThrow('preflight options must contain an optional boolean verifySensors');
|
|
67
|
+
});
|
|
68
|
+
it('requires an empirical sensor pass when verification is requested', async () => {
|
|
69
|
+
const dir = make({
|
|
70
|
+
manifest: { pack: 'generic', sensors: { security: { enabled: false } } },
|
|
71
|
+
});
|
|
72
|
+
mockRunSensors.mockResolvedValue({
|
|
73
|
+
overall: 'not_certified',
|
|
74
|
+
sensors: [{
|
|
75
|
+
name: 'lint', status: 'inconclusive', errors: [], skipReason: 'timeout after 30000ms',
|
|
76
|
+
execution: { timeoutMs: 30000, timeoutSource: 'project', elapsedMs: 30012, requestedScope: 'full', effectiveScope: 'full' },
|
|
77
|
+
}],
|
|
78
|
+
});
|
|
79
|
+
const report = await (0, checks_1.preflight)(dir, { verifySensors: true });
|
|
80
|
+
expect(mockRunSensors).toHaveBeenCalledWith({ cwd: dir, all: true });
|
|
81
|
+
expect(report.status).toBe('degraded');
|
|
82
|
+
expect(check(report, 'sensors-execution')).toMatchObject({
|
|
83
|
+
ok: false,
|
|
84
|
+
detail: expect.stringMatching(/lint.*30000ms.*elapsed.*timeout/i),
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
it('does not invent a sensor or timeout when empirical verification has no executed sensors', async () => {
|
|
88
|
+
const dir = make();
|
|
89
|
+
mockRunSensors.mockResolvedValue({ overall: 'not_certified', sensors: [] });
|
|
90
|
+
const report = await (0, checks_1.preflight)(dir, { verifySensors: true });
|
|
91
|
+
const execution = check(report, 'sensors-execution');
|
|
92
|
+
expect(execution).toMatchObject({
|
|
93
|
+
ok: false,
|
|
94
|
+
detail: 'sensor verdict was not_certified; no sensor established an empirical pass',
|
|
95
|
+
});
|
|
96
|
+
expect(execution.detail).not.toMatch(/timeout|elapsed|named sensor/i);
|
|
97
|
+
expect(execution.remedy).toContain('awm sensors init');
|
|
98
|
+
expect(execution.remedy).not.toContain('named sensor');
|
|
99
|
+
});
|
|
50
100
|
it('reports not_configured when no sensor manifest exists', async () => {
|
|
51
101
|
// The team-rollout case: a developer clones the repo and never runs
|
|
52
102
|
// `awm sensors init`. Today nothing notices until an unattended run is already
|
|
@@ -7,6 +7,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
const os_1 = __importDefault(require("os"));
|
|
9
9
|
const baseline_1 = require("../../../src/commands/sensors/baseline");
|
|
10
|
+
const result_1 = require("../../../src/commands/sensors/result");
|
|
10
11
|
const err = (over = {}) => ({
|
|
11
12
|
file: 'lib/a.ts', rule: 'TS2345', message: 'Argument of type X', ...over,
|
|
12
13
|
});
|
|
@@ -76,6 +77,19 @@ describe('partition', () => {
|
|
|
76
77
|
expect(newErrors).toHaveLength(0);
|
|
77
78
|
});
|
|
78
79
|
});
|
|
80
|
+
describe('applyBaseline', () => {
|
|
81
|
+
it('applies the same baseline to structured findings (R2)', () => {
|
|
82
|
+
const finding = { file: 'src/a.ts', line: 1, message: 'x' };
|
|
83
|
+
const accepted = [(0, baseline_1.fingerprint)('lint', finding)];
|
|
84
|
+
expect((0, result_1.applyBaseline)({ name: 'lint', status: 'fail', errors: [finding] }, accepted))
|
|
85
|
+
.toMatchObject({ status: 'pass', baselineCount: 1, newCount: 0 });
|
|
86
|
+
});
|
|
87
|
+
test.each(['inconclusive', 'skipped'])('baseline never changes %s to pass (R2.1)', status => {
|
|
88
|
+
const finding = { file: 'src/a.ts', line: 1, message: 'fixture finding' };
|
|
89
|
+
const accepted = [(0, baseline_1.fingerprint)('lint', finding)];
|
|
90
|
+
expect((0, result_1.applyBaseline)({ name: 'lint', status, errors: [finding], skipReason: 'fixture' }, accepted).status).toBe(status);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
79
93
|
describe('readBaseline / writeBaseline', () => {
|
|
80
94
|
let cwd;
|
|
81
95
|
beforeEach(() => { cwd = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-bl-')); });
|
|
@@ -41,4 +41,7 @@ describe('applyChangedCmd — Windows quoting', () => {
|
|
|
41
41
|
expect((0, changed_1.applyChangedCmd)('eslint {files}', ['report\\']))
|
|
42
42
|
.toBe(`eslint "report\\\\"`);
|
|
43
43
|
});
|
|
44
|
+
it('refuses unsafe filenames for legacy shell interpolation', () => {
|
|
45
|
+
expect((0, changed_1.changedScopeError)({ files: ['src/a&b.ts'] })).toMatch(/cmd\.exe metacharacter/);
|
|
46
|
+
});
|
|
44
47
|
});
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const contract_1 = require("../../../../src/commands/sensors/compatibility/contract");
|
|
7
|
+
const timeout_1 = require("../../../../src/commands/sensors/compatibility/timeout");
|
|
7
8
|
const fs_1 = __importDefault(require("fs"));
|
|
8
9
|
const os_1 = __importDefault(require("os"));
|
|
9
10
|
const path_1 = __importDefault(require("path"));
|
|
@@ -43,6 +44,22 @@ function validPack() {
|
|
|
43
44
|
};
|
|
44
45
|
}
|
|
45
46
|
describe('sensor pack v2 contract', () => {
|
|
47
|
+
it('exports bounded timeout validation and resolution (R3.1, R3.4)', () => {
|
|
48
|
+
expect((0, timeout_1.positiveTimeout)(1, 'sensor timeout')).toBe(1);
|
|
49
|
+
expect((0, timeout_1.resolveTimeout)({ project: 90_000, pack: 30_000, fast: true })).toEqual({ timeoutMs: 90_000, source: 'project' });
|
|
50
|
+
expect((0, timeout_1.resolveTimeout)({ pack: 30_000, fast: true })).toEqual({ timeoutMs: 30_000, source: 'pack' });
|
|
51
|
+
expect((0, timeout_1.resolveTimeout)({ fast: true })).toEqual({ timeoutMs: 10_000, source: 'fallback' });
|
|
52
|
+
expect((0, timeout_1.resolveTimeout)({ fast: false })).toEqual({ timeoutMs: 120_000, source: 'fallback' });
|
|
53
|
+
});
|
|
54
|
+
test.each([0, -1, 1.5, Number.MAX_SAFE_INTEGER + 1, '1000'])('rejects invalid resolved timeout %p (R3.3)', timeout => {
|
|
55
|
+
expect(() => (0, timeout_1.positiveTimeout)(timeout, 'sensor timeout')).toThrow(/sensor timeout.*positive safe integer/);
|
|
56
|
+
expect(() => (0, timeout_1.resolveTimeout)({ project: timeout, fast: true })).toThrow(/project timeout.*positive safe integer/);
|
|
57
|
+
});
|
|
58
|
+
it('rejects malformed timeout helper inputs loudly (R3.4)', () => {
|
|
59
|
+
expect(() => (0, timeout_1.positiveTimeout)(1000, '')).toThrow('timeout location must be a nonempty string');
|
|
60
|
+
expect(() => (0, timeout_1.resolveTimeout)(null)).toThrow('timeout resolution input is invalid');
|
|
61
|
+
expect(() => (0, timeout_1.resolveTimeout)({ fast: 'true' })).toThrow('timeout resolution input is invalid');
|
|
62
|
+
});
|
|
46
63
|
it('derives Semgrep compatibility from a contained shared policy reference', () => {
|
|
47
64
|
const sensorPacks = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-semgrep-policy-'));
|
|
48
65
|
const packDir = path_1.default.join(sensorPacks, 'python');
|
|
@@ -79,6 +96,40 @@ describe('sensor pack v2 contract', () => {
|
|
|
79
96
|
it('parses a valid versioned pack', () => {
|
|
80
97
|
expect((0, contract_1.parseSensorPack)(validPack(), 'pack.json')).toMatchObject({ kind: 'v2', pack: validPack() });
|
|
81
98
|
});
|
|
99
|
+
it('accepts one standalone files placeholder in changedCommand (R4)', () => {
|
|
100
|
+
const pack = validPack();
|
|
101
|
+
pack.sensors.lint.variants[0].changedCommand = {
|
|
102
|
+
executable: 'eslint', resolution: 'node-modules-bin',
|
|
103
|
+
args: ['--format', 'json', '{files}'],
|
|
104
|
+
fileInput: { placeholder: '{files}', extensions: ['.js', '.ts'] },
|
|
105
|
+
};
|
|
106
|
+
expect((0, contract_1.parseSensorPack)(pack, '/registry/sensor-packs/js-ts/pack.json')).toMatchObject({
|
|
107
|
+
kind: 'v2', pack: { sensors: { lint: { variants: [{ changedCommand: { args: ['--format', 'json', '{files}'] } }] } } },
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
test.each([
|
|
111
|
+
{ args: ['{files}', '{files}'], fileInput: { placeholder: '{files}', extensions: ['.ts'] } },
|
|
112
|
+
{ args: ['prefix-{files}'], fileInput: { placeholder: '{files}', extensions: ['.ts'] } },
|
|
113
|
+
{ args: ['{files}'], fileInput: { placeholder: '{files}', extensions: [] } },
|
|
114
|
+
])('rejects unsafe changedCommand %# (R4)', changedCommand => {
|
|
115
|
+
const pack = validPack();
|
|
116
|
+
pack.sensors.lint.variants[0].changedCommand = {
|
|
117
|
+
executable: 'eslint', resolution: 'node-modules-bin', ...changedCommand,
|
|
118
|
+
};
|
|
119
|
+
expect(() => (0, contract_1.parseSensorPack)(pack, '/registry/sensor-packs/js-ts/pack.json')).toThrow(/changedCommand|fileInput|\{files\}/);
|
|
120
|
+
});
|
|
121
|
+
test.each([0, -1, 1.5, Number.MAX_SAFE_INTEGER + 1, '1000'])('rejects pack sensor timeout %p (R3.3)', timeout => {
|
|
122
|
+
const pack = validPack();
|
|
123
|
+
pack.sensors.lint.timeout = timeout;
|
|
124
|
+
expect(() => (0, contract_1.parseSensorPack)(pack, '/registry/sensor-packs/js-ts/pack.json')).toThrow(/timeout.*positive safe integer/);
|
|
125
|
+
});
|
|
126
|
+
it('accepts an optional positive pack sensor timeout (R3.1)', () => {
|
|
127
|
+
const pack = validPack();
|
|
128
|
+
pack.sensors.lint.timeout = 30_000;
|
|
129
|
+
expect((0, contract_1.parseSensorPack)(pack, '/registry/sensor-packs/js-ts/pack.json')).toMatchObject({
|
|
130
|
+
kind: 'v2', pack: { sensors: { lint: { timeout: 30_000 } } },
|
|
131
|
+
});
|
|
132
|
+
});
|
|
82
133
|
it('accepts an opt-in hardening asset while variants may require no assets', () => {
|
|
83
134
|
const pack = {
|
|
84
135
|
...validPack(),
|
|
@@ -6,6 +6,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const manifest_1 = require("../../../../src/commands/sensors/compatibility/manifest");
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
|
+
function validV2Manifest() {
|
|
10
|
+
return {
|
|
11
|
+
schemaVersion: 2,
|
|
12
|
+
pack: 'js-ts',
|
|
13
|
+
sensors: {
|
|
14
|
+
lint: {
|
|
15
|
+
enabled: true, variantId: 'eslint-9',
|
|
16
|
+
command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.', '--format', 'json'] },
|
|
17
|
+
initializedCompatibility: { state: 'certified', reason: 'range-and-probe', variantId: 'eslint-9', toolVersion: '9.0.0', runtimeVersion: '24.0.0', certifiedRange: '>=9 <10', evidence: [] },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
9
22
|
describe('sensor manifest contract', () => {
|
|
10
23
|
it('keeps compatibility contracts on an acyclic import boundary', () => {
|
|
11
24
|
const source = (relative) => fs_1.default.readFileSync(path_1.default.join(__dirname, '../../../../src/commands/sensors', relative), 'utf8');
|
|
@@ -21,20 +34,23 @@ describe('sensor manifest contract', () => {
|
|
|
21
34
|
} });
|
|
22
35
|
});
|
|
23
36
|
it('accepts a v2 selected variant and structured command', () => {
|
|
24
|
-
const manifest =
|
|
25
|
-
schemaVersion: 2,
|
|
26
|
-
pack: 'js-ts',
|
|
27
|
-
sensors: {
|
|
28
|
-
lint: {
|
|
29
|
-
enabled: true, variantId: 'eslint-9',
|
|
30
|
-
command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.', '--format', 'json'] },
|
|
31
|
-
initializedCompatibility: { state: 'certified', reason: 'range-and-probe', variantId: 'eslint-9', toolVersion: '9.0.0', runtimeVersion: '24.0.0', certifiedRange: '>=9 <10', evidence: [] },
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
};
|
|
37
|
+
const manifest = validV2Manifest();
|
|
35
38
|
expect((0, manifest_1.parseSensorManifest)(manifest, 'sensors.json')).toMatchObject({ kind: 'v2', pack: manifest });
|
|
36
39
|
expect(JSON.parse((0, manifest_1.serializeManifestV2)(manifest))).toEqual(manifest);
|
|
37
40
|
});
|
|
41
|
+
it('accepts and serializes a positive v2 project timeout (R3)', () => {
|
|
42
|
+
const manifest = validV2Manifest();
|
|
43
|
+
manifest.sensors.lint.timeout = 45_000;
|
|
44
|
+
expect((0, manifest_1.parseSensorManifest)(manifest, '/project/.awm/sensors.json')).toMatchObject({
|
|
45
|
+
kind: 'v2', pack: { sensors: { lint: { timeout: 45_000 } } },
|
|
46
|
+
});
|
|
47
|
+
expect(JSON.parse((0, manifest_1.serializeManifestV2)(manifest))).toEqual(manifest);
|
|
48
|
+
});
|
|
49
|
+
test.each([0, -1, 1.5, Number.MAX_SAFE_INTEGER + 1, '1000'])('rejects v2 timeout %p before execution (R3.3)', timeout => {
|
|
50
|
+
const manifest = validV2Manifest();
|
|
51
|
+
manifest.sensors.lint.timeout = timeout;
|
|
52
|
+
expect(() => (0, manifest_1.parseSensorManifest)(manifest, '/project/.awm/sensors.json')).toThrow(/timeout.*positive safe integer/);
|
|
53
|
+
});
|
|
38
54
|
it('persists only an explicit v2 pack selection as applicability provenance', () => {
|
|
39
55
|
const manifest = {
|
|
40
56
|
schemaVersion: 2, pack: 'generic', packSelection: 'explicit',
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const probe_1 = require("../../../../src/commands/sensors/compatibility/probe");
|
|
4
|
-
const
|
|
4
|
+
const execResult = (overrides = {}) => ({
|
|
5
|
+
code: 0,
|
|
6
|
+
signal: null,
|
|
7
|
+
timedOut: false,
|
|
8
|
+
overflowed: false,
|
|
9
|
+
elapsedMs: 0,
|
|
10
|
+
stdout: 'SECRET_VALUE\nmatched',
|
|
11
|
+
stderr: '',
|
|
12
|
+
...overrides,
|
|
13
|
+
});
|
|
14
|
+
const fakeExecutor = jest.fn(async () => execResult());
|
|
5
15
|
const evidence = { cwd: process.cwd(), configFiles: ['eslint.config.js'], scripts: ['lint'] };
|
|
6
16
|
describe('runCompatibilityProbe', () => {
|
|
7
17
|
beforeEach(() => fakeExecutor.mockClear());
|
|
@@ -11,9 +21,9 @@ describe('runCompatibilityProbe', () => {
|
|
|
11
21
|
expect(JSON.stringify(result)).not.toContain('SECRET_VALUE');
|
|
12
22
|
});
|
|
13
23
|
it('never treats a timeout or overflow as a match', async () => {
|
|
14
|
-
fakeExecutor.mockResolvedValueOnce({ code: null, signal: 'SIGKILL', timedOut: true,
|
|
24
|
+
fakeExecutor.mockResolvedValueOnce(execResult({ code: null, signal: 'SIGKILL', timedOut: true, stdout: '' }));
|
|
15
25
|
await expect((0, probe_1.runCompatibilityProbe)({ kind: 'version' }, evidence, fakeExecutor)).resolves.toMatchObject({ status: 'unverifiable' });
|
|
16
|
-
fakeExecutor.mockResolvedValueOnce({
|
|
26
|
+
fakeExecutor.mockResolvedValueOnce(execResult({ overflowed: true, stdout: 'ok' }));
|
|
17
27
|
await expect((0, probe_1.runCompatibilityProbe)({ kind: 'version' }, evidence, fakeExecutor)).resolves.toMatchObject({ status: 'unverifiable' });
|
|
18
28
|
});
|
|
19
29
|
it('binds tool probes to the project node_modules executable instead of PATH', async () => {
|
|
@@ -6,7 +6,7 @@ exports.spawnFailed = exports.overflowed = exports.timedOut = exports.exited = e
|
|
|
6
6
|
* mock the exec boundary rather than `child_process` directly: `runCommand`
|
|
7
7
|
* never throws, so a mocked run is a value, not an exception.
|
|
8
8
|
*/
|
|
9
|
-
const base = { stdout: '', stderr: '', code: null, signal: null, timedOut: false, overflowed: false };
|
|
9
|
+
const base = { stdout: '', stderr: '', code: null, signal: null, timedOut: false, overflowed: false, elapsedMs: 0 };
|
|
10
10
|
/** Clean run: exit 0. */
|
|
11
11
|
const ok = (stdout = '') => ({ ...base, stdout, code: 0 });
|
|
12
12
|
exports.ok = ok;
|
|
@@ -7,6 +7,17 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
7
7
|
const os_1 = __importDefault(require("os"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const exec_1 = require("../../../src/commands/sensors/exec");
|
|
10
|
+
const result_1 = require("../../../src/commands/sensors/result");
|
|
11
|
+
const sensor = (overrides = {}) => ({
|
|
12
|
+
name: 'lint',
|
|
13
|
+
command: { kind: 'legacy', value: 'node -e "setTimeout(() => {}, 1000)"' },
|
|
14
|
+
formatter: 'generic',
|
|
15
|
+
timeoutMs: 5_000,
|
|
16
|
+
timeoutSource: 'fallback',
|
|
17
|
+
requestedScope: 'full',
|
|
18
|
+
effectiveScope: 'full',
|
|
19
|
+
...overrides,
|
|
20
|
+
});
|
|
10
21
|
const onPosix = process.platform !== 'win32' ? describe : describe.skip;
|
|
11
22
|
const itPosix = process.platform !== 'win32' ? it : it.skip;
|
|
12
23
|
/** Poll until `fn()` is true or the budget runs out. Avoids fixed sleeps. */
|
|
@@ -20,6 +31,12 @@ async function until(fn, budgetMs = 4000) {
|
|
|
20
31
|
return fn();
|
|
21
32
|
}
|
|
22
33
|
describe('runCommand — exit codes and output', () => {
|
|
34
|
+
it('records bounded execution evidence on timeout (R3.2,R3.4,R7.1)', async () => {
|
|
35
|
+
const result = await (0, result_1.executePrepared)(sensor({ timeoutMs: 25, timeoutSource: 'project' }));
|
|
36
|
+
expect(result.status).toBe('inconclusive');
|
|
37
|
+
expect(result.execution).toMatchObject({ timeoutMs: 25, timeoutSource: 'project', effectiveScope: 'full' });
|
|
38
|
+
expect(result.execution.elapsedMs).toBeGreaterThanOrEqual(0);
|
|
39
|
+
});
|
|
23
40
|
it('passes structured metacharacters literally without a shell', async () => {
|
|
24
41
|
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-exec-argv-'));
|
|
25
42
|
const marker = path_1.default.join(dir, 'must-not-exist');
|
|
@@ -15,18 +15,56 @@ const prompts_1 = require("@clack/prompts");
|
|
|
15
15
|
const coverage_1 = require("../../../src/commands/sensors/coverage");
|
|
16
16
|
const render_1 = require("../../../src/commands/sensors/coverage/render");
|
|
17
17
|
const index_1 = require("../../../src/commands/sensors/index");
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
it
|
|
23
|
-
|
|
18
|
+
const verdict_1 = require("../../../src/commands/sensors/verdict");
|
|
19
|
+
const processExit = jest.spyOn(process, 'exit').mockImplementation((() => undefined));
|
|
20
|
+
const stdoutWrite = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
21
|
+
describe('exitCodeForVerdict — sensor run verdict → exit code', () => {
|
|
22
|
+
it.each([
|
|
23
|
+
['pass', 0],
|
|
24
|
+
['fail', 1],
|
|
25
|
+
['skipped', 1],
|
|
26
|
+
['not_certified', 1],
|
|
27
|
+
])('%s → %i', (overall, code) => {
|
|
28
|
+
expect((0, verdict_1.exitCodeForVerdict)(overall)).toBe(code);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe('sensors run Commander wiring', () => {
|
|
32
|
+
const originalExitCode = process.exitCode;
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
jest.clearAllMocks();
|
|
35
|
+
process.exitCode = undefined;
|
|
36
|
+
});
|
|
37
|
+
afterAll(() => {
|
|
38
|
+
process.exitCode = originalExitCode;
|
|
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.each([
|
|
47
|
+
['pass', 0],
|
|
48
|
+
['fail', 1],
|
|
49
|
+
['skipped', 1],
|
|
50
|
+
['not_certified', 1],
|
|
51
|
+
])('writes full %s JSON before assigning exit code %i without process.exit', async (overall, code) => {
|
|
52
|
+
require('../../../src/commands/sensors/run').runSensors.mockResolvedValue({ sensors: [], overall });
|
|
53
|
+
const calls = [];
|
|
54
|
+
stdoutWrite.mockImplementation(() => {
|
|
55
|
+
calls.push(`stdout:${process.exitCode ?? 0}`);
|
|
56
|
+
return true;
|
|
57
|
+
});
|
|
58
|
+
await programWithSensors().parseAsync(['node', 'awm', 'sensors', 'run']);
|
|
59
|
+
expect(JSON.parse(String(stdoutWrite.mock.calls[0][0]))).toEqual({ sensors: [], overall });
|
|
60
|
+
expect(calls).toEqual(['stdout:0']);
|
|
61
|
+
expect(process.exitCode).toBe(code);
|
|
62
|
+
expect(processExit).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
24
64
|
});
|
|
25
65
|
describe('sensors coverage Commander wiring', () => {
|
|
26
66
|
const report = { schemaVersion: 1, pack: 'js-ts', registry: 'baseline', overall: 'gaps',
|
|
27
67
|
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
68
|
beforeEach(() => {
|
|
31
69
|
jest.clearAllMocks();
|
|
32
70
|
coverage_1.runCoverage.mockReturnValue(report);
|
|
@@ -310,7 +310,7 @@ describe('initSensors', () => {
|
|
|
310
310
|
const written = JSON.parse(fs_1.default.readFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), 'utf8'));
|
|
311
311
|
expect(explicit.manifest).toMatchObject({ schemaVersion: 2, pack: 'generic', packSelection: 'explicit', sensors: { security: { variantId: 'eslint-10' } } });
|
|
312
312
|
expect(written.packSelection).toBe('explicit');
|
|
313
|
-
await expect((0, status_1.computeSensorStatus)(tmpDir)).resolves.toMatchObject({ overall: '
|
|
313
|
+
await expect((0, status_1.computeSensorStatus)(tmpDir)).resolves.toMatchObject({ overall: 'DEGRADED', checks: { security: { ok: false } } });
|
|
314
314
|
}
|
|
315
315
|
finally {
|
|
316
316
|
fs_1.default.rmSync(v2Registry, { recursive: true, force: true });
|
|
@@ -370,6 +370,35 @@ describe('initSensors', () => {
|
|
|
370
370
|
fs_1.default.rmSync(v2Registry, { recursive: true, force: true });
|
|
371
371
|
}
|
|
372
372
|
});
|
|
373
|
+
it('preserves only the project timeout override, never prior executable authority (R3, R10)', async () => {
|
|
374
|
+
const v2Registry = makeV2Registry();
|
|
375
|
+
try {
|
|
376
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'package.json'), JSON.stringify({ devDependencies: { eslint: '^10.0.0' } }));
|
|
377
|
+
fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'node_modules', 'eslint'), { recursive: true });
|
|
378
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version: '10.0.0' }));
|
|
379
|
+
await (0, init_1.initSensors)({ cwd: tmpDir, registryRoot: v2Registry });
|
|
380
|
+
const manifestPath = path_1.default.join(tmpDir, '.awm', 'sensors.json');
|
|
381
|
+
const selected = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf8'));
|
|
382
|
+
selected.sensors.lint.timeout = 45_000;
|
|
383
|
+
selected.sensors.lint.command = {
|
|
384
|
+
executable: 'otherlint', resolution: 'path', args: ['--custom'],
|
|
385
|
+
environment: { ESLINT_USE_FLAT_CONFIG: 'false' },
|
|
386
|
+
};
|
|
387
|
+
selected.sensors.lint.assets = ['prior-owned.config'];
|
|
388
|
+
fs_1.default.writeFileSync(manifestPath, JSON.stringify(selected));
|
|
389
|
+
await (0, init_1.initSensors)({ cwd: tmpDir, registryRoot: v2Registry });
|
|
390
|
+
const written = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf8'));
|
|
391
|
+
expect(written.sensors.lint).toMatchObject({
|
|
392
|
+
timeout: 45_000,
|
|
393
|
+
command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
|
|
394
|
+
assets: ['eslint.config.awm.mjs'],
|
|
395
|
+
});
|
|
396
|
+
expect(written.sensors.lint.command.environment).toBeUndefined();
|
|
397
|
+
}
|
|
398
|
+
finally {
|
|
399
|
+
fs_1.default.rmSync(v2Registry, { recursive: true, force: true });
|
|
400
|
+
}
|
|
401
|
+
});
|
|
373
402
|
it('rejects a symlinked v2 pack source instead of reading it through init', async () => {
|
|
374
403
|
const v2Registry = makeV2Registry();
|
|
375
404
|
try {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const prepare_1 = require("../../../src/commands/sensors/prepare");
|
|
4
|
+
const fullCommand = { executable: 'manifest-eslint', resolution: 'path', args: ['.'] };
|
|
5
|
+
const changedCommand = {
|
|
6
|
+
executable: 'live-eslint',
|
|
7
|
+
resolution: 'path',
|
|
8
|
+
args: ['--format', 'json', '{files}'],
|
|
9
|
+
fileInput: { placeholder: '{files}', extensions: ['.ts'] },
|
|
10
|
+
};
|
|
11
|
+
function v2Input(overrides = {}) {
|
|
12
|
+
const liveVariant = {
|
|
13
|
+
id: 'eslint-live', priority: 1, certifiedRange: '>=1.0.0', requirements: { tool: 'eslint', toolRange: '>=1.0.0', runtime: 'node', runtimeRange: '>=1.0.0' }, assets: [],
|
|
14
|
+
probe: { kind: 'version' }, command: { executable: 'live-eslint', resolution: 'path', args: ['.'] }, changedCommand, formatter: 'eslint-llm',
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
name: 'lint',
|
|
18
|
+
sensor: {
|
|
19
|
+
enabled: true, fast: false, variantId: 'eslint-live', command: fullCommand,
|
|
20
|
+
initializedCompatibility: { state: 'certified', reason: 'test', variantId: 'eslint-live', toolVersion: '1.0.0', runtimeVersion: '1.0.0', certifiedRange: '>=1.0.0', evidence: [] },
|
|
21
|
+
},
|
|
22
|
+
liveSensor: { applicability: {}, fast: false, timeout: 30_000, variants: [liveVariant] },
|
|
23
|
+
liveState: { state: 'certified', reason: 'test', variantId: 'eslint-live', toolVersion: '1.0.0', runtimeVersion: '1.0.0', certifiedRange: '>=1.0.0', evidence: [] },
|
|
24
|
+
changed: { files: ['src/a.ts'] },
|
|
25
|
+
requestedScope: 'changed',
|
|
26
|
+
projectTimeout: 90_000,
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
describe('prepareV2Sensor', () => {
|
|
31
|
+
test('v2 uses the live command and project > pack > fallback timeout (R1.1, R3.1)', () => {
|
|
32
|
+
const prepared = (0, prepare_1.prepareV2Sensor)(v2Input());
|
|
33
|
+
expect(prepared.command).toEqual({ kind: 'structured', value: { ...changedCommand, args: ['--format', 'json', 'src/a.ts'] } });
|
|
34
|
+
expect(prepared.timeoutMs).toBe(90_000);
|
|
35
|
+
expect(prepared.timeoutSource).toBe('project');
|
|
36
|
+
expect((0, prepare_1.prepareV2Sensor)(v2Input({ projectTimeout: undefined })).timeoutSource).toBe('pack');
|
|
37
|
+
const fallback = v2Input();
|
|
38
|
+
fallback.liveSensor.timeout = undefined;
|
|
39
|
+
fallback.liveSensor.fast = true;
|
|
40
|
+
fallback.sensor.fast = undefined;
|
|
41
|
+
expect((0, prepare_1.prepareV2Sensor)({ ...fallback, projectTimeout: undefined })).toMatchObject({ timeoutMs: 10_000, timeoutSource: 'fallback' });
|
|
42
|
+
});
|
|
43
|
+
test('expands changed paths as literal argv entries (R4.1, R10.2)', () => {
|
|
44
|
+
const prepared = (0, prepare_1.prepareV2Sensor)(v2Input({ changed: { files: ['src/a b.ts', 'src/$x.ts'] } }));
|
|
45
|
+
expect(prepared.command).toEqual({ kind: 'structured', value: { ...changedCommand, args: ['--format', 'json', 'src/a b.ts', 'src/$x.ts'] } });
|
|
46
|
+
expect(prepared.effectiveScope).toBe('changed');
|
|
47
|
+
});
|
|
48
|
+
test('falls back full with an explicit reason without changedCommand (R4.2)', () => {
|
|
49
|
+
const input = v2Input();
|
|
50
|
+
input.liveSensor.variants[0].changedCommand = undefined;
|
|
51
|
+
const prepared = (0, prepare_1.prepareV2Sensor)(input);
|
|
52
|
+
expect(prepared.command).toEqual({ kind: 'structured', value: input.liveSensor.variants[0].command });
|
|
53
|
+
expect(prepared.effectiveScope).toBe('full');
|
|
54
|
+
expect(prepared.scopeReason).toMatch(/does not support changed scope/);
|
|
55
|
+
});
|
|
56
|
+
test('uses the full command with an explicit reason when the diff cannot resolve', () => {
|
|
57
|
+
const prepared = (0, prepare_1.prepareV2Sensor)(v2Input({ changed: { files: [], error: 'git failed' } }));
|
|
58
|
+
expect(prepared.effectiveScope).toBe('full');
|
|
59
|
+
expect(prepared.scopeReason).toMatch(/could not be resolved: git failed/);
|
|
60
|
+
});
|
|
61
|
+
test('returns zero-file pass plan without a process (R4.4)', () => {
|
|
62
|
+
const prepared = (0, prepare_1.prepareV2Sensor)(v2Input({ changed: { files: ['README.md'] } }));
|
|
63
|
+
expect(prepared).toMatchObject({ effectiveScope: 'changed', files: 0, syntheticStatus: 'pass' });
|
|
64
|
+
expect(prepared.command).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
test('rejects an invalid requested scope before preparing a command', () => {
|
|
67
|
+
expect(() => (0, prepare_1.prepareV2Sensor)({ ...v2Input(), requestedScope: 'sideways' }))
|
|
68
|
+
.toThrow('requested scope must be "full" or "changed"');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('prepareLegacySensor', () => {
|
|
72
|
+
const originalPlatform = process.platform;
|
|
73
|
+
afterEach(() => Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true }));
|
|
74
|
+
test('falls back to the full command for an unsafe Windows filename', () => {
|
|
75
|
+
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
76
|
+
const prepared = (0, prepare_1.prepareLegacySensor)({
|
|
77
|
+
name: 'lint', config: { cmd: 'eslint .', changedCmd: 'eslint {files}' }, requestedScope: 'changed', changed: { files: ['src/a&b.ts'] },
|
|
78
|
+
});
|
|
79
|
+
expect(prepared).toMatchObject({ command: { kind: 'legacy', value: 'eslint .' }, effectiveScope: 'full' });
|
|
80
|
+
expect(prepared.scopeReason).toMatch(/cmd\.exe metacharacter/);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe('validateRunOptions', () => {
|
|
84
|
+
test('rejects changed baseline capture before any scope preparation (R4.6)', () => {
|
|
85
|
+
expect(() => (0, prepare_1.validateRunOptions)({ changed: true, ignoreBaseline: true }))
|
|
86
|
+
.toThrow(/refusing to combine --changed with a baseline capture/);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -4,7 +4,7 @@ jest.mock('@clack/prompts', () => ({ log: { success: jest.fn(), info: jest.fn()
|
|
|
4
4
|
jest.mock('picocolors', () => ({ green: (s) => s, yellow: (s) => s, red: (s) => s }));
|
|
5
5
|
jest.mock('../../../src/commands/sensors/run', () => ({ runSensors: jest.fn().mockReturnValue({ sensors: [], overall: 'pass' }) }));
|
|
6
6
|
jest.mock('../../../src/commands/sensors/init', () => ({ initSensors: jest.fn().mockReturnValue({ detection: { pack: 'js-ts', indicators: [] }, manifest: { sensors: {} }, configured: [] }) }));
|
|
7
|
-
jest.mock('../../../src/commands/sensors/status', () => ({ computeSensorStatus: jest.fn().mockReturnValue({ overall: '
|
|
7
|
+
jest.mock('../../../src/commands/sensors/status', () => ({ computeSensorStatus: jest.fn().mockReturnValue({ overall: 'READY', pack: 'js-ts', checks: {} }) }));
|
|
8
8
|
jest.mock('../../../src/commands/sensors/install', () => ({ installSensorHook: jest.fn().mockReturnValue({ status: 'installed' }) }));
|
|
9
9
|
const commander_1 = require("commander");
|
|
10
10
|
const index_1 = require("../../../src/commands/sensors/index");
|
|
@@ -21,4 +21,18 @@ describe('registerSensorsCommand', () => {
|
|
|
21
21
|
expect(subNames).toContain('install');
|
|
22
22
|
expect(subNames).toContain('coverage');
|
|
23
23
|
});
|
|
24
|
+
it('renders READY without claiming sensor execution, HEALTHY, or project certification', async () => {
|
|
25
|
+
const program = new commander_1.Command();
|
|
26
|
+
const output = jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
27
|
+
try {
|
|
28
|
+
(0, index_1.registerSensorsCommand)(program);
|
|
29
|
+
await program.parseAsync(['node', 'awm', 'sensors', 'status']);
|
|
30
|
+
const rendered = output.mock.calls.flat().join('\n');
|
|
31
|
+
expect(rendered).toContain('READY');
|
|
32
|
+
expect(rendered).not.toMatch(/HEALTHY|certif/i);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
output.mockRestore();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
24
38
|
});
|
|
@@ -84,12 +84,12 @@ describe('runSensors --changed', () => {
|
|
|
84
84
|
expect(out.sensors[0].scope).toBeUndefined();
|
|
85
85
|
expect(out.changedScope).toEqual({ files: 0, error: 'not a git repository' });
|
|
86
86
|
});
|
|
87
|
-
it('
|
|
87
|
+
it('records a clean synthetic pass when an opted-in sensor has no changed files, without touching the others', async () => {
|
|
88
88
|
dir = project({ lint: LINT, typecheck: TYPECHECK });
|
|
89
89
|
mockChangedFiles.mockReturnValue({ files: [] });
|
|
90
90
|
const out = await load().runSensors({ cwd: dir, changed: true });
|
|
91
91
|
const lint = out.sensors.find((s) => s.name === 'lint');
|
|
92
|
-
expect(lint.status).toBe('
|
|
92
|
+
expect(lint.status).toBe('pass');
|
|
93
93
|
expect(lint.skipReason).toBe('no changed files in scope');
|
|
94
94
|
expect(cmds()).toEqual(['tsc --noEmit']);
|
|
95
95
|
});
|
|
@@ -101,13 +101,13 @@ describe('runSensors --changed', () => {
|
|
|
101
101
|
await load().runSensors({ cwd: dir, changed: true });
|
|
102
102
|
expect(cmds()).toEqual([`eslint --format json 'src/a.ts'`]);
|
|
103
103
|
});
|
|
104
|
-
it('
|
|
104
|
+
it('records a clean synthetic pass when the filter empties the scope, rather than running repo-wide', async () => {
|
|
105
105
|
// A docs-only commit means the lint sensor has nothing to say. Falling back to
|
|
106
106
|
// the full command here would reintroduce exactly the cost --changed removes.
|
|
107
107
|
dir = project({ lint: { ...LINT, changedExtensions: ['.ts'] }, typecheck: TYPECHECK });
|
|
108
108
|
mockChangedFiles.mockReturnValue({ files: ['README.md'] });
|
|
109
109
|
const out = await load().runSensors({ cwd: dir, changed: true });
|
|
110
|
-
expect(out.sensors.find((s) => s.name === 'lint').status).toBe('
|
|
110
|
+
expect(out.sensors.find((s) => s.name === 'lint').status).toBe('pass');
|
|
111
111
|
expect(cmds()).toEqual(['tsc --noEmit']);
|
|
112
112
|
});
|
|
113
113
|
it('refuses a changedCmd without a {files} placeholder instead of running it repo-wide', async () => {
|