agentic-workflow-manager 3.4.0 → 3.6.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.
- package/dist/src/commands/job/exec-wrapper.js +136 -0
- package/dist/src/commands/job/export.js +94 -0
- package/dist/src/commands/job/gate.js +118 -0
- package/dist/src/commands/job/heartbeat.js +15 -0
- package/dist/src/commands/job/index.js +246 -0
- package/dist/src/commands/job/query.js +37 -0
- package/dist/src/commands/job/reap.js +24 -0
- package/dist/src/commands/job/reconcile.js +112 -0
- package/dist/src/commands/job/request.js +27 -0
- package/dist/src/commands/sensors/exec.js +121 -0
- package/dist/src/commands/sensors/index.js +4 -4
- package/dist/src/commands/sensors/run.js +124 -71
- package/dist/src/commands/watch/apply.js +352 -0
- package/dist/src/commands/watch/generations.js +249 -0
- package/dist/src/commands/watch/index.js +49 -0
- package/dist/src/commands/watch/init.js +72 -0
- package/dist/src/commands/watch/lock.js +89 -0
- package/dist/src/commands/watch/runner.js +191 -0
- package/dist/src/commands/watch/supervisor.js +266 -0
- package/dist/src/core/atomic-file.js +31 -0
- package/dist/src/core/export/pack.js +7 -1
- package/dist/src/core/journal/adapter.js +27 -0
- package/dist/src/core/journal/fingerprint.js +80 -0
- package/dist/src/core/journal/paths.js +56 -0
- package/dist/src/core/journal/process.js +284 -0
- package/dist/src/core/journal/redact.js +142 -0
- package/dist/src/core/journal/requests.js +132 -0
- package/dist/src/core/journal/store.js +107 -0
- package/dist/src/core/journal/types.js +165 -0
- package/dist/src/index.js +4 -0
- package/dist/tests/commands/job/exec-wrapper.test.js +85 -0
- package/dist/tests/commands/job/export.test.js +76 -0
- package/dist/tests/commands/job/gate-reconcile.test.js +297 -0
- package/dist/tests/commands/job/reap-cli.test.js +101 -0
- package/dist/tests/commands/job/verbs.test.js +56 -0
- package/dist/tests/commands/job/verdict-determinism.test.js +138 -0
- package/dist/tests/commands/sensors/exec-fixtures.js +24 -0
- package/dist/tests/commands/sensors/exec.test.js +91 -0
- package/dist/tests/commands/sensors/run-inconclusive.test.js +55 -66
- package/dist/tests/commands/sensors/run-partial.test.js +225 -0
- package/dist/tests/commands/sensors/run-tool-missing.test.js +6 -6
- package/dist/tests/commands/sensors/run.test.js +64 -81
- package/dist/tests/commands/watch/apply.test.js +397 -0
- package/dist/tests/commands/watch/e2e-crash.test.js +157 -0
- package/dist/tests/commands/watch/generations.test.js +115 -0
- package/dist/tests/commands/watch/integration.test.js +124 -0
- package/dist/tests/commands/watch/lock.test.js +60 -0
- package/dist/tests/commands/watch/runner.test.js +239 -0
- package/dist/tests/commands/watch/supervisor-loop.test.js +203 -0
- package/dist/tests/commands/watch/watch-init.test.js +43 -0
- package/dist/tests/core/atomic-file-durable.test.js +42 -0
- package/dist/tests/core/journal/adapter.test.js +27 -0
- package/dist/tests/core/journal/fingerprint.test.js +164 -0
- package/dist/tests/core/journal/paths.test.js +35 -0
- package/dist/tests/core/journal/process.test.js +213 -0
- package/dist/tests/core/journal/redact.test.js +59 -0
- package/dist/tests/core/journal/requests.test.js +134 -0
- package/dist/tests/core/journal/store.test.js +88 -0
- package/dist/tests/core/journal/types.test.js +78 -0
- package/dist/tests/structural/exec-invocation-explicit-stdio.test.js +94 -0
- package/package.json +1 -1
|
@@ -0,0 +1,91 @@
|
|
|
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 exec_1 = require("../../../src/commands/sensors/exec");
|
|
10
|
+
const onPosix = process.platform !== 'win32' ? describe : describe.skip;
|
|
11
|
+
/** Poll until `fn()` is true or the budget runs out. Avoids fixed sleeps. */
|
|
12
|
+
async function until(fn, budgetMs = 4000) {
|
|
13
|
+
const deadline = Date.now() + budgetMs;
|
|
14
|
+
while (Date.now() < deadline) {
|
|
15
|
+
if (fn())
|
|
16
|
+
return true;
|
|
17
|
+
await new Promise(r => setTimeout(r, 25));
|
|
18
|
+
}
|
|
19
|
+
return fn();
|
|
20
|
+
}
|
|
21
|
+
describe('runCommand — exit codes and output', () => {
|
|
22
|
+
it('returns stdout and code 0 for a clean command', async () => {
|
|
23
|
+
const r = await (0, exec_1.runCommand)('echo hello', { timeout: 5000, cwd: process.cwd() });
|
|
24
|
+
expect(r.code).toBe(0);
|
|
25
|
+
expect(r.stdout.trim()).toBe('hello');
|
|
26
|
+
expect(r.timedOut).toBe(false);
|
|
27
|
+
expect(r.overflowed).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
it('captures stderr and a non-zero exit code without throwing', async () => {
|
|
30
|
+
const r = await (0, exec_1.runCommand)('echo oops 1>&2; exit 3', { timeout: 5000, cwd: process.cwd() });
|
|
31
|
+
expect(r.code).toBe(3);
|
|
32
|
+
expect(r.stderr).toMatch(/oops/);
|
|
33
|
+
expect(r.timedOut).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
it('reports 127 for a command that does not exist', async () => {
|
|
36
|
+
const r = await (0, exec_1.runCommand)('awm-definitely-not-a-real-binary-xyz', { timeout: 5000, cwd: process.cwd() });
|
|
37
|
+
expect(r.code).toBe(127);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('runCommand — output cap', () => {
|
|
41
|
+
it('stops at maxBuffer, flags overflow, and keeps what it read', async () => {
|
|
42
|
+
// 200 lines of ~50 bytes each, capped at 1KB.
|
|
43
|
+
const r = await (0, exec_1.runCommand)(`for i in $(seq 1 200); do echo "line-$i-padding-padding-padding-padding"; done`, {
|
|
44
|
+
timeout: 10_000, cwd: process.cwd(), maxBuffer: 1024,
|
|
45
|
+
});
|
|
46
|
+
expect(r.overflowed).toBe(true);
|
|
47
|
+
expect(r.stdout.length).toBeLessThanOrEqual(1024);
|
|
48
|
+
// The point of the cap change: what was read is still usable, not discarded.
|
|
49
|
+
expect(r.stdout).toMatch(/line-1-/);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
onPosix('runCommand — timeout', () => {
|
|
53
|
+
it('flags the timeout and returns the output produced before the deadline', async () => {
|
|
54
|
+
const r = await (0, exec_1.runCommand)('echo partial-finding; sleep 30', { timeout: 700, cwd: process.cwd() });
|
|
55
|
+
expect(r.timedOut).toBe(true);
|
|
56
|
+
// This is the whole point of dropping execSync: 700ms of work is not thrown away.
|
|
57
|
+
expect(r.stdout).toMatch(/partial-finding/);
|
|
58
|
+
});
|
|
59
|
+
it('kills the grandchild process, not just the shell it spawned', async () => {
|
|
60
|
+
// Models `npx tsc --noEmit`: the sensor command is a wrapper that spawns the
|
|
61
|
+
// real tool. execSync SIGTERMs only the shell it started, leaving the tool
|
|
62
|
+
// running and reparented to init — the leak that compounds across retries.
|
|
63
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-exec-group-'));
|
|
64
|
+
const beat = path_1.default.join(dir, 'beat');
|
|
65
|
+
const worker = path_1.default.join(dir, 'worker.js');
|
|
66
|
+
fs_1.default.writeFileSync(worker, `
|
|
67
|
+
const fs = require('fs');
|
|
68
|
+
setInterval(() => fs.writeFileSync(${JSON.stringify(beat)}, String(Date.now())), 30);
|
|
69
|
+
setTimeout(() => {}, 60000);
|
|
70
|
+
`);
|
|
71
|
+
try {
|
|
72
|
+
const r = await (0, exec_1.runCommand)(`sh -c "node ${worker} & wait"`, { timeout: 800, cwd: dir });
|
|
73
|
+
expect(r.timedOut).toBe(true);
|
|
74
|
+
// The worker must have been alive before the kill, or the test proves nothing.
|
|
75
|
+
expect(await until(() => fs_1.default.existsSync(beat))).toBe(true);
|
|
76
|
+
const atKill = fs_1.default.readFileSync(beat, 'utf-8');
|
|
77
|
+
const stillBeating = await until(() => fs_1.default.readFileSync(beat, 'utf-8') !== atKill, 1000);
|
|
78
|
+
expect(stillBeating).toBe(false);
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
}, 15_000);
|
|
84
|
+
});
|
|
85
|
+
describe('runCommand — spawn failure', () => {
|
|
86
|
+
it('surfaces a spawn error instead of hanging', async () => {
|
|
87
|
+
const r = await (0, exec_1.runCommand)('echo hi', { timeout: 5000, cwd: path_1.default.join(os_1.default.tmpdir(), 'awm-no-such-dir-xyz') });
|
|
88
|
+
expect(r.spawnError).toBeDefined();
|
|
89
|
+
expect(r.code).not.toBe(0);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -6,10 +6,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const fs_1 = __importDefault(require("fs"));
|
|
7
7
|
const os_1 = __importDefault(require("os"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
|
-
const
|
|
10
|
-
jest.mock('
|
|
11
|
-
|
|
9
|
+
const mockRunCommand = jest.fn();
|
|
10
|
+
jest.mock('../../../src/commands/sensors/exec', () => ({
|
|
11
|
+
runCommand: (...args) => mockRunCommand(...args),
|
|
12
12
|
}));
|
|
13
|
+
const { ok, exited, timedOut, overflowed } = require('./exec-fixtures');
|
|
13
14
|
/** Sensors run in manifest insertion order, so mocks are queued in that order. */
|
|
14
15
|
const MANIFEST = {
|
|
15
16
|
pack: 'js-ts',
|
|
@@ -18,14 +19,14 @@ const MANIFEST = {
|
|
|
18
19
|
security: { cmd: 'semgrep .', fast: false },
|
|
19
20
|
},
|
|
20
21
|
};
|
|
21
|
-
const timeoutError = () =>
|
|
22
|
+
const timeoutError = () => timedOut();
|
|
22
23
|
describe('runSensors — inconclusive: a sensor that could not certify is never green', () => {
|
|
23
24
|
let root;
|
|
24
25
|
let fakeAwmHome;
|
|
25
26
|
let prevAwmHome;
|
|
26
27
|
beforeEach(() => {
|
|
27
28
|
jest.resetModules();
|
|
28
|
-
|
|
29
|
+
mockRunCommand.mockReset();
|
|
29
30
|
root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-inconclusive-'));
|
|
30
31
|
fs_1.default.mkdirSync(path_1.default.join(root, '.awm'), { recursive: true });
|
|
31
32
|
fs_1.default.writeFileSync(path_1.default.join(root, '.awm', 'sensors.json'), JSON.stringify(MANIFEST));
|
|
@@ -40,55 +41,51 @@ describe('runSensors — inconclusive: a sensor that could not certify is never
|
|
|
40
41
|
fs_1.default.rmSync(fakeAwmHome, { recursive: true, force: true });
|
|
41
42
|
});
|
|
42
43
|
const load = () => require('../../../src/commands/sensors/run');
|
|
43
|
-
it('reports a timed-out sensor as inconclusive, keeping its reason', () => {
|
|
44
|
-
|
|
45
|
-
.
|
|
46
|
-
.
|
|
44
|
+
it('reports a timed-out sensor as inconclusive, keeping its reason', async () => {
|
|
45
|
+
mockRunCommand
|
|
46
|
+
.mockResolvedValueOnce(ok()) // typecheck: clean
|
|
47
|
+
.mockResolvedValueOnce(timeoutError()); // security: times out
|
|
47
48
|
const { runSensors } = load();
|
|
48
|
-
const out = runSensors({ cwd: root });
|
|
49
|
+
const out = await runSensors({ cwd: root });
|
|
49
50
|
const security = out.sensors.find((s) => s.name === 'security');
|
|
50
51
|
expect(security.status).toBe('inconclusive');
|
|
51
52
|
expect(security.skipReason).toMatch(/timeout/);
|
|
52
53
|
});
|
|
53
|
-
it('does not let a healthy sensor carry the run to pass while another could not certify', () => {
|
|
54
|
-
|
|
55
|
-
.
|
|
56
|
-
.
|
|
54
|
+
it('does not let a healthy sensor carry the run to pass while another could not certify', async () => {
|
|
55
|
+
mockRunCommand
|
|
56
|
+
.mockResolvedValueOnce(ok())
|
|
57
|
+
.mockResolvedValueOnce(timeoutError());
|
|
57
58
|
const { runSensors } = load();
|
|
58
|
-
const out = runSensors({ cwd: root });
|
|
59
|
+
const out = await runSensors({ cwd: root });
|
|
59
60
|
expect(out.sensors.find((s) => s.name === 'typecheck').status).toBe('pass');
|
|
60
61
|
expect(out.overall).toBe('not_certified');
|
|
61
62
|
});
|
|
62
|
-
it('reports a sensor whose output was truncated as inconclusive', () => {
|
|
63
|
-
|
|
64
|
-
.
|
|
65
|
-
.
|
|
63
|
+
it('reports a sensor whose output was truncated as inconclusive', async () => {
|
|
64
|
+
mockRunCommand
|
|
65
|
+
.mockResolvedValueOnce(ok())
|
|
66
|
+
.mockResolvedValueOnce(overflowed());
|
|
66
67
|
const { runSensors } = load();
|
|
67
|
-
const out = runSensors({ cwd: root });
|
|
68
|
+
const out = await runSensors({ cwd: root });
|
|
68
69
|
const security = out.sensors.find((s) => s.name === 'security');
|
|
69
70
|
expect(security.status).toBe('inconclusive');
|
|
70
71
|
expect(security.skipReason).toMatch(/exceeded/);
|
|
71
72
|
expect(out.overall).toBe('not_certified');
|
|
72
73
|
});
|
|
73
|
-
it('reports an uninterpretable non-zero exit as inconclusive', () => {
|
|
74
|
-
|
|
75
|
-
.
|
|
76
|
-
.mockImplementationOnce(() => {
|
|
74
|
+
it('reports an uninterpretable non-zero exit as inconclusive', async () => {
|
|
75
|
+
mockRunCommand
|
|
76
|
+
.mockResolvedValueOnce(ok())
|
|
77
77
|
// semgrep formatter yields no findings for non-JSON output, the
|
|
78
78
|
// tool is present (exit 2, not 127), and `security` is not an
|
|
79
79
|
// exit-code sensor — the residual "I don't know" case.
|
|
80
|
-
|
|
81
|
-
stdout: '', stderr: 'internal error: rule engine crashed\n', status: 2,
|
|
82
|
-
});
|
|
83
|
-
});
|
|
80
|
+
.mockResolvedValueOnce(exited(2, '', 'internal error: rule engine crashed\n'));
|
|
84
81
|
const { runSensors } = load();
|
|
85
|
-
const out = runSensors({ cwd: root });
|
|
82
|
+
const out = await runSensors({ cwd: root });
|
|
86
83
|
const security = out.sensors.find((s) => s.name === 'security');
|
|
87
84
|
expect(security.status).toBe('inconclusive');
|
|
88
85
|
expect(security.skipReason).toMatch(/exit 2/);
|
|
89
86
|
expect(out.overall).toBe('not_certified');
|
|
90
87
|
});
|
|
91
|
-
it('reports an enabled sensor with no cmd as inconclusive', () => {
|
|
88
|
+
it('reports an enabled sensor with no cmd as inconclusive', async () => {
|
|
92
89
|
fs_1.default.writeFileSync(path_1.default.join(root, '.awm', 'sensors.json'), JSON.stringify({
|
|
93
90
|
pack: 'js-ts',
|
|
94
91
|
sensors: {
|
|
@@ -96,29 +93,25 @@ describe('runSensors — inconclusive: a sensor that could not certify is never
|
|
|
96
93
|
depcheck: { fast: false }, // enabled, but nothing to run
|
|
97
94
|
},
|
|
98
95
|
}));
|
|
99
|
-
|
|
96
|
+
mockRunCommand.mockResolvedValueOnce(ok()); // typecheck: clean
|
|
100
97
|
const { runSensors } = load();
|
|
101
|
-
const out = runSensors({ cwd: root });
|
|
98
|
+
const out = await runSensors({ cwd: root });
|
|
102
99
|
const depcheck = out.sensors.find((s) => s.name === 'depcheck');
|
|
103
100
|
expect(depcheck.status).toBe('inconclusive');
|
|
104
101
|
expect(depcheck.skipReason).toBe('no cmd configured');
|
|
105
102
|
expect(out.overall).toBe('not_certified');
|
|
106
103
|
});
|
|
107
|
-
it('reports fail, not not_certified, when something is broken and something could not run', () => {
|
|
108
|
-
|
|
109
|
-
.
|
|
110
|
-
|
|
111
|
-
stdout: 'src/a.ts(1,1): error TS0001: Bad type.', stderr: '', status: 1,
|
|
112
|
-
});
|
|
113
|
-
})
|
|
114
|
-
.mockImplementationOnce(timeoutError); // security: times out
|
|
104
|
+
it('reports fail, not not_certified, when something is broken and something could not run', async () => {
|
|
105
|
+
mockRunCommand
|
|
106
|
+
.mockResolvedValueOnce(exited(1, 'src/a.ts(1,1): error TS0001: Bad type.')) // typecheck: real findings
|
|
107
|
+
.mockResolvedValueOnce(timeoutError()); // security: times out
|
|
115
108
|
const { runSensors } = load();
|
|
116
|
-
const out = runSensors({ cwd: root });
|
|
109
|
+
const out = await runSensors({ cwd: root });
|
|
117
110
|
expect(out.sensors.find((s) => s.name === 'typecheck').status).toBe('fail');
|
|
118
111
|
expect(out.sensors.find((s) => s.name === 'security').status).toBe('inconclusive');
|
|
119
112
|
expect(out.overall).toBe('fail');
|
|
120
113
|
});
|
|
121
|
-
it('never emits an overall value outside the published domain', () => {
|
|
114
|
+
it('never emits an overall value outside the published domain', async () => {
|
|
122
115
|
// `inconclusive` is a per-sensor status only. External consumers (the
|
|
123
116
|
// registry skills) read `overall`, whose domain must not grow — this
|
|
124
117
|
// pins that invariant at runtime on a three-sensor pass+fail+inconclusive
|
|
@@ -137,20 +130,16 @@ describe('runSensors — inconclusive: a sensor that could not certify is never
|
|
|
137
130
|
},
|
|
138
131
|
}));
|
|
139
132
|
const DOMAIN = ['pass', 'fail', 'skipped', 'not_certified'];
|
|
140
|
-
|
|
141
|
-
.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
});
|
|
145
|
-
})
|
|
146
|
-
.mockReturnValueOnce('') // lint: clean → pass
|
|
147
|
-
.mockImplementationOnce(timeoutError); // security: times out → inconclusive
|
|
133
|
+
mockRunCommand
|
|
134
|
+
.mockResolvedValueOnce(exited(1, 'src/a.ts(1,1): error TS0001: Bad type.')) // typecheck: real findings → fail
|
|
135
|
+
.mockResolvedValueOnce(ok()) // lint: clean → pass
|
|
136
|
+
.mockResolvedValueOnce(timeoutError()); // security: times out → inconclusive
|
|
148
137
|
const { runSensors } = load();
|
|
149
|
-
const out = runSensors({ cwd: root });
|
|
138
|
+
const out = await runSensors({ cwd: root });
|
|
150
139
|
expect(DOMAIN).toContain(out.overall);
|
|
151
140
|
expect(out.overall).not.toBe('inconclusive');
|
|
152
141
|
});
|
|
153
|
-
it('keeps a deliberately disabled sensor apart from one that could not certify', () => {
|
|
142
|
+
it('keeps a deliberately disabled sensor apart from one that could not certify', async () => {
|
|
154
143
|
fs_1.default.writeFileSync(path_1.default.join(root, '.awm', 'sensors.json'), JSON.stringify({
|
|
155
144
|
pack: 'js-ts',
|
|
156
145
|
sensors: {
|
|
@@ -158,17 +147,17 @@ describe('runSensors — inconclusive: a sensor that could not certify is never
|
|
|
158
147
|
mutation: { cmd: 'npx stryker run', enabled: false },
|
|
159
148
|
},
|
|
160
149
|
}));
|
|
161
|
-
|
|
150
|
+
mockRunCommand.mockResolvedValueOnce(timeoutError()); // security: times out
|
|
162
151
|
// mutation: never invoked
|
|
163
152
|
const { runSensors } = load();
|
|
164
|
-
const out = runSensors({ cwd: root });
|
|
153
|
+
const out = await runSensors({ cwd: root });
|
|
165
154
|
// Same run, two different meanings — the whole point of the split.
|
|
166
155
|
expect(out.sensors.find((s) => s.name === 'mutation').status).toBe('skipped');
|
|
167
156
|
expect(out.sensors.find((s) => s.name === 'mutation').skipReason).toBe('disabled');
|
|
168
157
|
expect(out.sensors.find((s) => s.name === 'security').status).toBe('inconclusive');
|
|
169
158
|
expect(out.overall).toBe('not_certified');
|
|
170
159
|
});
|
|
171
|
-
it('does not degrade the verdict for a disabled sensor alongside healthy ones', () => {
|
|
160
|
+
it('does not degrade the verdict for a disabled sensor alongside healthy ones', async () => {
|
|
172
161
|
fs_1.default.writeFileSync(path_1.default.join(root, '.awm', 'sensors.json'), JSON.stringify({
|
|
173
162
|
pack: 'js-ts',
|
|
174
163
|
sensors: {
|
|
@@ -176,12 +165,12 @@ describe('runSensors — inconclusive: a sensor that could not certify is never
|
|
|
176
165
|
mutation: { cmd: 'npx stryker run', enabled: false },
|
|
177
166
|
},
|
|
178
167
|
}));
|
|
179
|
-
|
|
168
|
+
mockRunCommand.mockResolvedValueOnce(ok());
|
|
180
169
|
const { runSensors } = load();
|
|
181
|
-
const out = runSensors({ cwd: root });
|
|
170
|
+
const out = await runSensors({ cwd: root });
|
|
182
171
|
expect(out.overall).toBe('pass');
|
|
183
172
|
});
|
|
184
|
-
it('still refuses to certify a tree whose sensors are all disabled', () => {
|
|
173
|
+
it('still refuses to certify a tree whose sensors are all disabled', async () => {
|
|
185
174
|
fs_1.default.writeFileSync(path_1.default.join(root, '.awm', 'sensors.json'), JSON.stringify({
|
|
186
175
|
pack: 'js-ts',
|
|
187
176
|
sensors: {
|
|
@@ -191,25 +180,25 @@ describe('runSensors — inconclusive: a sensor that could not certify is never
|
|
|
191
180
|
}));
|
|
192
181
|
fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}'); // real stack indicator
|
|
193
182
|
const { runSensors } = load();
|
|
194
|
-
const out = runSensors({ cwd: root });
|
|
183
|
+
const out = await runSensors({ cwd: root });
|
|
195
184
|
expect(out.sensors.every((s) => s.status === 'skipped')).toBe(true);
|
|
196
185
|
expect(out.overall).toBe('not_certified');
|
|
197
|
-
expect(
|
|
186
|
+
expect(mockRunCommand).not.toHaveBeenCalled();
|
|
198
187
|
});
|
|
199
|
-
it('leaves an inconclusive result untouched when a baseline is applied', () => {
|
|
188
|
+
it('leaves an inconclusive result untouched when a baseline is applied', async () => {
|
|
200
189
|
const { writeBaseline } = require('../../../src/commands/sensors/baseline');
|
|
201
190
|
writeBaseline(root, { security: ['some-accepted-fingerprint'] });
|
|
202
|
-
|
|
203
|
-
.
|
|
204
|
-
.
|
|
191
|
+
mockRunCommand
|
|
192
|
+
.mockResolvedValueOnce(ok())
|
|
193
|
+
.mockResolvedValueOnce(timeoutError());
|
|
205
194
|
const { runSensors } = load();
|
|
206
|
-
const out = runSensors({ cwd: root });
|
|
195
|
+
const out = await runSensors({ cwd: root });
|
|
207
196
|
const security = out.sensors.find((s) => s.name === 'security');
|
|
208
197
|
expect(security.status).toBe('inconclusive');
|
|
209
198
|
expect(security.baselineCount).toBeUndefined();
|
|
210
199
|
expect(out.overall).toBe('not_certified');
|
|
211
200
|
});
|
|
212
|
-
it('applyBaseline leaves an inconclusive result untouched even if it somehow carried findings', () => {
|
|
201
|
+
it('applyBaseline leaves an inconclusive result untouched even if it somehow carried findings', async () => {
|
|
213
202
|
// Every current `inconclusive` producer sets `errors: []`, so a test built
|
|
214
203
|
// on the public `runSensors()` API can't tell "the explicit guard fired"
|
|
215
204
|
// apart from "fell through to partition() and incidentally suppressed 0
|
|
@@ -0,0 +1,225 @@
|
|
|
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 mockRunCommand = jest.fn();
|
|
10
|
+
jest.mock('../../../src/commands/sensors/exec', () => ({
|
|
11
|
+
runCommand: (...args) => mockRunCommand(...args),
|
|
12
|
+
}));
|
|
13
|
+
const { ok, timedOut, overflowed } = require('./exec-fixtures');
|
|
14
|
+
const TS_FINDING = 'src/a.ts(1,1): error TS0001: Bad type.';
|
|
15
|
+
function project(sensors) {
|
|
16
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-partial-'));
|
|
17
|
+
fs_1.default.mkdirSync(path_1.default.join(dir, '.awm'), { recursive: true });
|
|
18
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, '.awm', 'sensors.json'), JSON.stringify({ pack: 'js-ts', sensors }));
|
|
19
|
+
return dir;
|
|
20
|
+
}
|
|
21
|
+
describe('runSensors — a cut-short run keeps the findings it did produce', () => {
|
|
22
|
+
let dir;
|
|
23
|
+
let prevAwmHome;
|
|
24
|
+
let fakeAwmHome;
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
jest.resetModules();
|
|
27
|
+
mockRunCommand.mockReset();
|
|
28
|
+
// CLAUDE.md: no test may reach the real ~/.awm.
|
|
29
|
+
fakeAwmHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-home-'));
|
|
30
|
+
prevAwmHome = process.env.AWM_HOME;
|
|
31
|
+
process.env.AWM_HOME = fakeAwmHome;
|
|
32
|
+
});
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
process.env.AWM_HOME = prevAwmHome;
|
|
35
|
+
if (dir)
|
|
36
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
37
|
+
fs_1.default.rmSync(fakeAwmHome, { recursive: true, force: true });
|
|
38
|
+
});
|
|
39
|
+
const load = () => require('../../../src/commands/sensors/run');
|
|
40
|
+
it('reports findings from partial output as fail instead of discarding them', async () => {
|
|
41
|
+
// The regression this guards: the old runner threw away everything a
|
|
42
|
+
// timed-out sensor had printed, so a 60s lint run that had already found
|
|
43
|
+
// real errors reported zero — and the caller re-ran it by hand to learn
|
|
44
|
+
// what it had just paid for.
|
|
45
|
+
dir = project({ typecheck: { cmd: 'npx tsc --noEmit', fast: true } });
|
|
46
|
+
mockRunCommand.mockResolvedValueOnce(timedOut(TS_FINDING));
|
|
47
|
+
const { runSensors } = load();
|
|
48
|
+
const out = await runSensors({ cwd: dir });
|
|
49
|
+
const tc = out.sensors.find((s) => s.name === 'typecheck');
|
|
50
|
+
expect(tc.status).toBe('fail');
|
|
51
|
+
expect(tc.errors).toHaveLength(1);
|
|
52
|
+
expect(tc.errors[0].message).toMatch(/Bad type/);
|
|
53
|
+
expect(out.overall).toBe('fail');
|
|
54
|
+
});
|
|
55
|
+
it('marks the partial fail as incomplete so absence of findings is not read as coverage', async () => {
|
|
56
|
+
dir = project({ typecheck: { cmd: 'npx tsc --noEmit', fast: true, timeout: 30000 } });
|
|
57
|
+
mockRunCommand.mockResolvedValueOnce(timedOut(TS_FINDING));
|
|
58
|
+
const { runSensors } = load();
|
|
59
|
+
const out = await runSensors({ cwd: dir });
|
|
60
|
+
const tc = out.sensors.find((s) => s.name === 'typecheck');
|
|
61
|
+
expect(tc.incomplete).toMatch(/timeout after 30000ms/);
|
|
62
|
+
expect(tc.incomplete).toMatch(/did not finish/);
|
|
63
|
+
});
|
|
64
|
+
it('still refuses to certify when the partial output is clean', async () => {
|
|
65
|
+
// A clean partial proves nothing — the findings could all be in the part
|
|
66
|
+
// that never ran. This must stay inconclusive, never pass.
|
|
67
|
+
dir = project({ typecheck: { cmd: 'npx tsc --noEmit', fast: true } });
|
|
68
|
+
mockRunCommand.mockResolvedValueOnce(timedOut('Checking 400 files...\n'));
|
|
69
|
+
const { runSensors } = load();
|
|
70
|
+
const out = await runSensors({ cwd: dir });
|
|
71
|
+
const tc = out.sensors.find((s) => s.name === 'typecheck');
|
|
72
|
+
expect(tc.status).toBe('inconclusive');
|
|
73
|
+
expect(tc.skipReason).toMatch(/timeout/);
|
|
74
|
+
expect(tc.incomplete).toBeUndefined();
|
|
75
|
+
expect(out.overall).toBe('not_certified');
|
|
76
|
+
});
|
|
77
|
+
it('applies the same rule to output-cap overflow', async () => {
|
|
78
|
+
dir = project({ typecheck: { cmd: 'npx tsc --noEmit', fast: true } });
|
|
79
|
+
mockRunCommand.mockResolvedValueOnce(overflowed(TS_FINDING));
|
|
80
|
+
const { runSensors } = load();
|
|
81
|
+
const out = await runSensors({ cwd: dir });
|
|
82
|
+
const tc = out.sensors.find((s) => s.name === 'typecheck');
|
|
83
|
+
expect(tc.status).toBe('fail');
|
|
84
|
+
expect(tc.incomplete).toMatch(/exceeded/);
|
|
85
|
+
});
|
|
86
|
+
it('fails the sensor when the shell could not be started', async () => {
|
|
87
|
+
const { spawnFailed } = require('./exec-fixtures');
|
|
88
|
+
dir = project({ typecheck: { cmd: 'npx tsc --noEmit', fast: true } });
|
|
89
|
+
mockRunCommand.mockResolvedValueOnce(spawnFailed('spawn /bin/sh ENOENT'));
|
|
90
|
+
const { runSensors } = load();
|
|
91
|
+
const out = await runSensors({ cwd: dir });
|
|
92
|
+
const tc = out.sensors.find((s) => s.name === 'typecheck');
|
|
93
|
+
expect(tc.status).toBe('fail');
|
|
94
|
+
expect(tc.errors[0].message).toMatch(/could not be started/);
|
|
95
|
+
expect(out.overall).toBe('fail');
|
|
96
|
+
});
|
|
97
|
+
it('lets the baseline suppress a finding that came from partial output', async () => {
|
|
98
|
+
dir = project({ typecheck: { cmd: 'npx tsc --noEmit', fast: true } });
|
|
99
|
+
const { buildBaseline, writeBaseline } = require('../../../src/commands/sensors/baseline');
|
|
100
|
+
const { parseTscOutput } = require('../../../src/commands/sensors/formatters/tsc');
|
|
101
|
+
writeBaseline(dir, buildBaseline([{ name: 'typecheck', errors: parseTscOutput(TS_FINDING) }]));
|
|
102
|
+
mockRunCommand.mockResolvedValueOnce(timedOut(TS_FINDING));
|
|
103
|
+
const { runSensors } = load();
|
|
104
|
+
const out = await runSensors({ cwd: dir });
|
|
105
|
+
const tc = out.sensors.find((s) => s.name === 'typecheck');
|
|
106
|
+
expect(tc.status).toBe('pass');
|
|
107
|
+
expect(tc.baselineCount).toBe(1);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe('runSensors — sensors run concurrently', () => {
|
|
111
|
+
let dir;
|
|
112
|
+
let prevAwmHome;
|
|
113
|
+
let prevConcurrency;
|
|
114
|
+
let fakeAwmHome;
|
|
115
|
+
beforeEach(() => {
|
|
116
|
+
jest.resetModules();
|
|
117
|
+
mockRunCommand.mockReset();
|
|
118
|
+
fakeAwmHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-home-'));
|
|
119
|
+
prevAwmHome = process.env.AWM_HOME;
|
|
120
|
+
prevConcurrency = process.env.AWM_SENSORS_CONCURRENCY;
|
|
121
|
+
process.env.AWM_HOME = fakeAwmHome;
|
|
122
|
+
});
|
|
123
|
+
afterEach(() => {
|
|
124
|
+
process.env.AWM_HOME = prevAwmHome;
|
|
125
|
+
if (prevConcurrency === undefined)
|
|
126
|
+
delete process.env.AWM_SENSORS_CONCURRENCY;
|
|
127
|
+
else
|
|
128
|
+
process.env.AWM_SENSORS_CONCURRENCY = prevConcurrency;
|
|
129
|
+
if (dir)
|
|
130
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
131
|
+
fs_1.default.rmSync(fakeAwmHome, { recursive: true, force: true });
|
|
132
|
+
});
|
|
133
|
+
const load = () => require('../../../src/commands/sensors/run');
|
|
134
|
+
/** Resolves after `ms`, recording when it started and finished. */
|
|
135
|
+
const timed = (log, label, ms) => () => new Promise((resolve) => {
|
|
136
|
+
log.push([`${label}:start`, Date.now()]);
|
|
137
|
+
setTimeout(() => { log.push([`${label}:end`, Date.now()]); resolve(ok()); }, ms);
|
|
138
|
+
});
|
|
139
|
+
const THREE = {
|
|
140
|
+
typecheck: { cmd: 'a', fast: true },
|
|
141
|
+
lint: { cmd: 'b', fast: true },
|
|
142
|
+
security: { cmd: 'c', fast: true },
|
|
143
|
+
};
|
|
144
|
+
it('starts every sensor before the first one finishes', async () => {
|
|
145
|
+
process.env.AWM_SENSORS_CONCURRENCY = '3';
|
|
146
|
+
dir = project(THREE);
|
|
147
|
+
const log = [];
|
|
148
|
+
mockRunCommand
|
|
149
|
+
.mockImplementationOnce(timed(log, 'typecheck', 120))
|
|
150
|
+
.mockImplementationOnce(timed(log, 'lint', 120))
|
|
151
|
+
.mockImplementationOnce(timed(log, 'security', 120));
|
|
152
|
+
const { runSensors } = load();
|
|
153
|
+
await runSensors({ cwd: dir });
|
|
154
|
+
const order = log.map(([label]) => label);
|
|
155
|
+
// All three starts precede the first end — that is what serial execution
|
|
156
|
+
// could not do, and the reason wall clock stops being the sum.
|
|
157
|
+
expect(order.slice(0, 3)).toEqual(['typecheck:start', 'lint:start', 'security:start']);
|
|
158
|
+
expect(order[3]).toMatch(/:end$/);
|
|
159
|
+
});
|
|
160
|
+
it('honours a concurrency of 1 by running them strictly one at a time', async () => {
|
|
161
|
+
process.env.AWM_SENSORS_CONCURRENCY = '1';
|
|
162
|
+
dir = project(THREE);
|
|
163
|
+
const log = [];
|
|
164
|
+
mockRunCommand
|
|
165
|
+
.mockImplementationOnce(timed(log, 'typecheck', 30))
|
|
166
|
+
.mockImplementationOnce(timed(log, 'lint', 30))
|
|
167
|
+
.mockImplementationOnce(timed(log, 'security', 30));
|
|
168
|
+
const { runSensors } = load();
|
|
169
|
+
await runSensors({ cwd: dir });
|
|
170
|
+
expect(log.map(([label]) => label)).toEqual([
|
|
171
|
+
'typecheck:start', 'typecheck:end',
|
|
172
|
+
'lint:start', 'lint:end',
|
|
173
|
+
'security:start', 'security:end',
|
|
174
|
+
]);
|
|
175
|
+
});
|
|
176
|
+
it('reports results in manifest order regardless of which sensor finishes first', async () => {
|
|
177
|
+
process.env.AWM_SENSORS_CONCURRENCY = '3';
|
|
178
|
+
dir = project(THREE);
|
|
179
|
+
const log = [];
|
|
180
|
+
// Deliberately inverted durations: security finishes first, typecheck last.
|
|
181
|
+
mockRunCommand
|
|
182
|
+
.mockImplementationOnce(timed(log, 'typecheck', 90))
|
|
183
|
+
.mockImplementationOnce(timed(log, 'lint', 50))
|
|
184
|
+
.mockImplementationOnce(timed(log, 'security', 10));
|
|
185
|
+
const { runSensors } = load();
|
|
186
|
+
const out = await runSensors({ cwd: dir });
|
|
187
|
+
expect(out.sensors.map((s) => s.name)).toEqual(['typecheck', 'lint', 'security']);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
describe('resolveConcurrency', () => {
|
|
191
|
+
const load = () => require('../../../src/commands/sensors/run');
|
|
192
|
+
let prev;
|
|
193
|
+
beforeEach(() => { jest.resetModules(); prev = process.env.AWM_SENSORS_CONCURRENCY; });
|
|
194
|
+
afterEach(() => {
|
|
195
|
+
if (prev === undefined)
|
|
196
|
+
delete process.env.AWM_SENSORS_CONCURRENCY;
|
|
197
|
+
else
|
|
198
|
+
process.env.AWM_SENSORS_CONCURRENCY = prev;
|
|
199
|
+
});
|
|
200
|
+
it('never exceeds the number of sensors to run', () => {
|
|
201
|
+
delete process.env.AWM_SENSORS_CONCURRENCY;
|
|
202
|
+
const { resolveConcurrency } = load();
|
|
203
|
+
expect(resolveConcurrency({ pack: 'js-ts', sensors: {} }, 1)).toBe(1);
|
|
204
|
+
});
|
|
205
|
+
it('caps at 4 even on a large box', () => {
|
|
206
|
+
delete process.env.AWM_SENSORS_CONCURRENCY;
|
|
207
|
+
const { resolveConcurrency } = load();
|
|
208
|
+
expect(resolveConcurrency({ pack: 'js-ts', sensors: {} }, 32)).toBeLessThanOrEqual(4);
|
|
209
|
+
});
|
|
210
|
+
it('lets the manifest pin it', () => {
|
|
211
|
+
delete process.env.AWM_SENSORS_CONCURRENCY;
|
|
212
|
+
const { resolveConcurrency } = load();
|
|
213
|
+
expect(resolveConcurrency({ pack: 'js-ts', sensors: {}, concurrency: 2 }, 8)).toBe(2);
|
|
214
|
+
});
|
|
215
|
+
it('lets the environment override the manifest', () => {
|
|
216
|
+
process.env.AWM_SENSORS_CONCURRENCY = '1';
|
|
217
|
+
const { resolveConcurrency } = load();
|
|
218
|
+
expect(resolveConcurrency({ pack: 'js-ts', sensors: {}, concurrency: 4 }, 8)).toBe(1);
|
|
219
|
+
});
|
|
220
|
+
it('ignores nonsense and falls back to the derived cap', () => {
|
|
221
|
+
process.env.AWM_SENSORS_CONCURRENCY = 'banana';
|
|
222
|
+
const { resolveConcurrency } = load();
|
|
223
|
+
expect(resolveConcurrency({ pack: 'js-ts', sensors: {} }, 8)).toBeGreaterThanOrEqual(1);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -36,23 +36,23 @@ describe('runSensors — an absent tool never reads as green (real /bin/sh)', ()
|
|
|
36
36
|
roots.push(root);
|
|
37
37
|
return root;
|
|
38
38
|
};
|
|
39
|
-
it('marks a sensor whose binary is absent as fail, not skipped', () => {
|
|
39
|
+
it('marks a sensor whose binary is absent as fail, not skipped', async () => {
|
|
40
40
|
const root = project({ security: { cmd: `${MISSING_BIN} .`, fast: true } });
|
|
41
|
-
const out = (0, run_1.runSensors)({ cwd: root });
|
|
41
|
+
const out = await (0, run_1.runSensors)({ cwd: root });
|
|
42
42
|
const security = out.sensors.find(s => s.name === 'security');
|
|
43
43
|
expect(security.status).toBe('fail');
|
|
44
44
|
expect(security.errors[0].message).toMatch(/not available/i);
|
|
45
45
|
});
|
|
46
|
-
it('does not let a healthy sensor carry the run to pass while another tool is absent', () => {
|
|
46
|
+
it('does not let a healthy sensor carry the run to pass while another tool is absent', async () => {
|
|
47
47
|
const root = project({
|
|
48
48
|
typecheck: { cmd: 'node -e ""', fast: true },
|
|
49
49
|
security: { cmd: `${MISSING_BIN} .`, fast: true },
|
|
50
50
|
});
|
|
51
|
-
const out = (0, run_1.runSensors)({ cwd: root });
|
|
51
|
+
const out = await (0, run_1.runSensors)({ cwd: root });
|
|
52
52
|
expect(out.sensors.find(s => s.name === 'typecheck').status).toBe('pass');
|
|
53
53
|
expect(out.overall).toBe('fail');
|
|
54
54
|
});
|
|
55
|
-
it('does not misread a tool that ran and merely printed "not found" as an absent tool', () => {
|
|
55
|
+
it('does not misread a tool that ran and merely printed "not found" as an absent tool', async () => {
|
|
56
56
|
// Exits 1, not 127: the binary existed and reported something of its own.
|
|
57
57
|
// Classifying this as a missing tool would be a false accusation. It also
|
|
58
58
|
// must not read as a benign 'skipped': the formatter parsed no findings
|
|
@@ -61,7 +61,7 @@ describe('runSensors — an absent tool never reads as green (real /bin/sh)', ()
|
|
|
61
61
|
const root = project({
|
|
62
62
|
security: { cmd: `node -e "console.error('rule pack not found'); process.exit(1)"`, fast: true },
|
|
63
63
|
});
|
|
64
|
-
const out = (0, run_1.runSensors)({ cwd: root });
|
|
64
|
+
const out = await (0, run_1.runSensors)({ cwd: root });
|
|
65
65
|
const security = out.sensors.find(s => s.name === 'security');
|
|
66
66
|
expect(security.status).toBe('inconclusive');
|
|
67
67
|
expect(security.errors).toEqual([]);
|