agentic-workflow-manager 3.5.0 → 3.7.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.
@@ -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([]);
@@ -11,10 +11,11 @@ function mkTmp() {
11
11
  return fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-sensors-'));
12
12
  }
13
13
  // Define stable mock before jest.mock hoisting
14
- const mockExecSyncFn = jest.fn();
15
- jest.mock('child_process', () => ({
16
- execSync: (...args) => mockExecSyncFn(...args),
14
+ const mockRunCommand = jest.fn();
15
+ jest.mock('../../../src/commands/sensors/exec', () => ({
16
+ runCommand: (...args) => mockRunCommand(...args),
17
17
  }));
18
+ const { ok, exited, timedOut } = require('./exec-fixtures');
18
19
  const MANIFEST = {
19
20
  pack: 'js-ts',
20
21
  sensors: {
@@ -33,15 +34,15 @@ describe('runSensors', () => {
33
34
  tmpDir = fs_1.default.mkdtempSync(path.join(os.tmpdir(), 'awm-run-test-'));
34
35
  fs_1.default.mkdirSync(path.join(tmpDir, '.awm'), { recursive: true });
35
36
  fs_1.default.writeFileSync(path.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify(MANIFEST));
36
- mockExecSyncFn.mockReset();
37
+ mockRunCommand.mockReset();
37
38
  });
38
39
  afterEach(() => { fs_1.default.rmSync(tmpDir, { recursive: true }); });
39
40
  const load = () => require('../../../src/commands/sensors/run');
40
- it('returns not_certified output when manifest does not exist', () => {
41
+ it('returns not_certified output when manifest does not exist', async () => {
41
42
  const emptyDir = fs_1.default.mkdtempSync(path.join(os.tmpdir(), 'awm-empty-'));
42
43
  try {
43
44
  const { runSensors } = load();
44
- const result = runSensors({ fast: true, cwd: emptyDir });
45
+ const result = await runSensors({ fast: true, cwd: emptyDir });
45
46
  expect(result.overall).toBe('not_certified');
46
47
  expect(result.sensors).toHaveLength(0);
47
48
  }
@@ -49,80 +50,80 @@ describe('runSensors', () => {
49
50
  fs_1.default.rmSync(emptyDir, { recursive: true });
50
51
  }
51
52
  });
52
- it('runs only fast sensors with --fast flag', () => {
53
- mockExecSyncFn.mockReturnValue('');
53
+ it('runs only fast sensors with --fast flag', async () => {
54
+ mockRunCommand.mockResolvedValue(ok());
54
55
  const { runSensors } = load();
55
- const result = runSensors({ fast: true, cwd: tmpDir });
56
- expect(mockExecSyncFn).toHaveBeenCalledTimes(2); // typecheck + lint (security disabled, mutation disabled)
56
+ const result = await runSensors({ fast: true, cwd: tmpDir });
57
+ expect(mockRunCommand).toHaveBeenCalledTimes(2); // typecheck + lint (security disabled, mutation disabled)
57
58
  expect(result.sensors.some((s) => s.name === 'security')).toBe(false);
58
59
  expect(result.overall).toBe('pass');
59
60
  });
60
- it('returns fail when a fast sensor has errors', () => {
61
- mockExecSyncFn
62
- .mockImplementationOnce(() => { throw Object.assign(new Error(), { stdout: "src/a.ts(1,1): error TS0001: Bad type.", stderr: '', status: 1 }); })
63
- .mockReturnValueOnce('');
61
+ it('returns fail when a fast sensor has errors', async () => {
62
+ mockRunCommand
63
+ .mockResolvedValueOnce(exited(1, 'src/a.ts(1,1): error TS0001: Bad type.'))
64
+ .mockResolvedValueOnce(ok());
64
65
  const { runSensors } = load();
65
- const result = runSensors({ fast: true, cwd: tmpDir });
66
+ const result = await runSensors({ fast: true, cwd: tmpDir });
66
67
  expect(result.overall).toBe('fail');
67
68
  const tc = result.sensors.find((s) => s.name === 'typecheck');
68
69
  expect(tc.status).toBe('fail');
69
70
  expect(tc.errors[0].message).toMatch('SENSOR[typecheck]');
70
71
  });
71
- it('marks sensor as inconclusive on timeout', () => {
72
- mockExecSyncFn.mockImplementationOnce(() => { throw Object.assign(new Error('killed'), { code: 'ETIMEDOUT' }); });
73
- mockExecSyncFn.mockReturnValueOnce('');
72
+ it('marks sensor as inconclusive on timeout', async () => {
73
+ mockRunCommand.mockResolvedValueOnce(timedOut());
74
+ mockRunCommand.mockResolvedValueOnce(ok());
74
75
  const { runSensors } = load();
75
- const result = runSensors({ fast: true, cwd: tmpDir });
76
+ const result = await runSensors({ fast: true, cwd: tmpDir });
76
77
  const tc = result.sensors.find((s) => s.name === 'typecheck');
77
78
  expect(tc.status).toBe('inconclusive');
78
79
  expect(tc.skipReason).toMatch('timeout');
79
80
  });
80
- it('skips disabled sensors', () => {
81
- mockExecSyncFn.mockReturnValue('');
81
+ it('skips disabled sensors', async () => {
82
+ mockRunCommand.mockResolvedValue(ok());
82
83
  const { runSensors } = load();
83
- const result = runSensors({ all: true, cwd: tmpDir });
84
+ const result = await runSensors({ all: true, cwd: tmpDir });
84
85
  const sec = result.sensors.find((s) => s.name === 'security');
85
86
  expect(sec.status).toBe('skipped');
86
87
  expect(sec.skipReason).toBe('disabled');
87
88
  });
88
- const tcError = () => { throw Object.assign(new Error(), { stdout: 'src/a.ts(1,1): error TS0001: Bad type.', stderr: '', status: 1 }); };
89
- it('baseline suppresses accepted findings — sensor passes on no NEW findings', () => {
89
+ const tcError = () => exited(1, 'src/a.ts(1,1): error TS0001: Bad type.');
90
+ it('baseline suppresses accepted findings — sensor passes on no NEW findings', async () => {
90
91
  const { runSensors } = load();
91
92
  const { buildBaseline, writeBaseline } = require('../../../src/commands/sensors/baseline');
92
93
  // Run 1 (no baseline): typecheck reports a TS error → fail.
93
- mockExecSyncFn.mockImplementationOnce(tcError).mockReturnValueOnce('');
94
- const first = runSensors({ fast: true, cwd: tmpDir });
94
+ mockRunCommand.mockResolvedValueOnce(tcError()).mockResolvedValueOnce(ok());
95
+ const first = await runSensors({ fast: true, cwd: tmpDir });
95
96
  expect(first.overall).toBe('fail');
96
97
  // Accept the current findings as baseline.
97
98
  writeBaseline(tmpDir, buildBaseline(first.sensors.map((s) => ({ name: s.name, errors: s.errors }))));
98
99
  // Run 2 (same finding): baseline-suppressed → pass.
99
- mockExecSyncFn.mockImplementationOnce(tcError).mockReturnValueOnce('');
100
- const second = runSensors({ fast: true, cwd: tmpDir });
100
+ mockRunCommand.mockResolvedValueOnce(tcError()).mockResolvedValueOnce(ok());
101
+ const second = await runSensors({ fast: true, cwd: tmpDir });
101
102
  const tc = second.sensors.find((s) => s.name === 'typecheck');
102
103
  expect(tc.status).toBe('pass');
103
104
  expect(tc.baselineCount).toBe(1);
104
105
  expect(second.overall).toBe('pass');
105
106
  });
106
- it('baseline lets NEW findings through (still fails)', () => {
107
+ it('baseline lets NEW findings through (still fails)', async () => {
107
108
  const { runSensors } = load();
108
109
  const { writeBaseline } = require('../../../src/commands/sensors/baseline');
109
110
  writeBaseline(tmpDir, { typecheck: ['some-unrelated-fingerprint'] });
110
- mockExecSyncFn.mockImplementationOnce(tcError).mockReturnValueOnce('');
111
- const result = runSensors({ fast: true, cwd: tmpDir });
111
+ mockRunCommand.mockResolvedValueOnce(tcError()).mockResolvedValueOnce(ok());
112
+ const result = await runSensors({ fast: true, cwd: tmpDir });
112
113
  const tc = result.sensors.find((s) => s.name === 'typecheck');
113
114
  expect(tc.status).toBe('fail');
114
115
  expect(result.overall).toBe('fail');
115
116
  });
116
- it('--ignore-baseline reports all findings even when a baseline exists', () => {
117
+ it('--ignore-baseline reports all findings even when a baseline exists', async () => {
117
118
  const { runSensors } = load();
118
119
  const { buildBaseline, writeBaseline } = require('../../../src/commands/sensors/baseline');
119
120
  // First capture + accept the finding.
120
- mockExecSyncFn.mockImplementationOnce(tcError).mockReturnValueOnce('');
121
- const first = runSensors({ fast: true, cwd: tmpDir });
121
+ mockRunCommand.mockResolvedValueOnce(tcError()).mockResolvedValueOnce(ok());
122
+ const first = await runSensors({ fast: true, cwd: tmpDir });
122
123
  writeBaseline(tmpDir, buildBaseline(first.sensors.map((s) => ({ name: s.name, errors: s.errors }))));
123
124
  // With ignoreBaseline, the accepted finding still counts → fail.
124
- mockExecSyncFn.mockImplementationOnce(tcError).mockReturnValueOnce('');
125
- const result = runSensors({ fast: true, cwd: tmpDir, ignoreBaseline: true });
125
+ mockRunCommand.mockResolvedValueOnce(tcError()).mockResolvedValueOnce(ok());
126
+ const result = await runSensors({ fast: true, cwd: tmpDir, ignoreBaseline: true });
126
127
  expect(result.overall).toBe('fail');
127
128
  });
128
129
  });
@@ -133,32 +134,26 @@ describe('runSensors — missing tool is a fail, not a skip', () => {
133
134
  fs_1.default.rmSync(root, { recursive: true, force: true });
134
135
  });
135
136
  beforeEach(() => {
136
- mockExecSyncFn.mockReset();
137
+ mockRunCommand.mockReset();
137
138
  // What Node's execSync actually throws when the binary is absent and `/bin/sh`
138
139
  // is dash (Debian/Ubuntu, hence most CI runners): status 127, `not found`
139
140
  // rather than bash's `command not found`, and no `code` — ENOENT is set only
140
141
  // when spawning the shell itself fails, not the command inside it.
141
- mockExecSyncFn.mockImplementation(() => {
142
- throw Object.assign(new Error('Command failed: awm-nonexistent-binary-xyz .'), {
143
- stdout: '',
144
- stderr: '/bin/sh: 1: awm-nonexistent-binary-xyz: not found\n',
145
- status: 127,
146
- });
147
- });
142
+ mockRunCommand.mockResolvedValue(exited(127, '', '/bin/sh: 1: awm-nonexistent-binary-xyz: not found\n'));
148
143
  });
149
144
  // The sensor is named `security` so it uses the semgrep formatter, which returns
150
145
  // zero findings for unparseable shell noise and lets execution reach the
151
146
  // tool-missing branch. Under the generic formatter any stderr becomes a finding,
152
147
  // so a sensor named `ghost` would report `fail` without that branch ever running —
153
148
  // green for a reason unrelated to what this test claims to cover.
154
- it('marks a sensor whose binary is missing as fail', () => {
149
+ it('marks a sensor whose binary is missing as fail', async () => {
155
150
  root = mkTmp();
156
151
  fs_1.default.mkdirSync(path_1.default.join(root, '.awm'));
157
152
  fs_1.default.writeFileSync(path_1.default.join(root, '.awm', 'sensors.json'), JSON.stringify({
158
153
  pack: 'js-ts',
159
154
  sensors: { security: { cmd: 'awm-nonexistent-binary-xyz .', fast: true } },
160
155
  }));
161
- const out = (0, run_1.runSensors)({ cwd: root });
156
+ const out = await (0, run_1.runSensors)({ cwd: root });
162
157
  const security = out.sensors.find((s) => s.name === 'security');
163
158
  expect(security?.status).toBe('fail');
164
159
  expect(security?.errors[0].message).toMatch(/not available/i);
@@ -168,8 +163,8 @@ describe('runSensors — missing tool is a fail, not a skip', () => {
168
163
  describe('runSensors — not_certified + auto-discovery', () => {
169
164
  let tmpDir;
170
165
  beforeEach(() => {
171
- mockExecSyncFn.mockReset();
172
- mockExecSyncFn.mockReturnValue('');
166
+ mockRunCommand.mockReset();
167
+ mockRunCommand.mockResolvedValue(ok());
173
168
  });
174
169
  afterEach(() => {
175
170
  if (tmpDir) {
@@ -177,19 +172,19 @@ describe('runSensors — not_certified + auto-discovery', () => {
177
172
  tmpDir = undefined;
178
173
  }
179
174
  });
180
- it('returns not_certified when no manifest exists anywhere up the tree', () => {
175
+ it('returns not_certified when no manifest exists anywhere up the tree', async () => {
181
176
  tmpDir = mkTmp();
182
- const out = (0, run_1.runSensors)({ cwd: tmpDir });
177
+ const out = await (0, run_1.runSensors)({ cwd: tmpDir });
183
178
  expect(out.overall).toBe('not_certified');
184
179
  expect(out.sensors).toEqual([]);
185
180
  });
186
- it('discovers .awm/sensors.json in a parent directory (walk-up)', () => {
181
+ it('discovers .awm/sensors.json in a parent directory (walk-up)', async () => {
187
182
  tmpDir = mkTmp();
188
183
  fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'));
189
184
  fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({ pack: 'test', sensors: { noop: { cmd: 'echo ok', fast: true } } }));
190
185
  const nested = path_1.default.join(tmpDir, 'a', 'b');
191
186
  fs_1.default.mkdirSync(nested, { recursive: true });
192
- const out = (0, run_1.runSensors)({ cwd: nested });
187
+ const out = await (0, run_1.runSensors)({ cwd: nested });
193
188
  expect(out.overall).toBe('pass');
194
189
  expect(out.sensors.length).toBe(1);
195
190
  });
@@ -234,7 +229,7 @@ describe('reconcilePack', () => {
234
229
  fs_1.default.writeFileSync(path_1.default.join(dir, 'package.json'), '{}');
235
230
  return dir;
236
231
  }
237
- it('upgrades generic→js-ts when package.json is present', () => {
232
+ it('upgrades generic→js-ts when package.json is present', async () => {
238
233
  const { reconcilePack } = require('../../../src/commands/sensors/run');
239
234
  const dir = tmpProject('generic', true);
240
235
  try {
@@ -253,7 +248,7 @@ describe('reconcilePack', () => {
253
248
  fs_1.default.rmSync(dir, { recursive: true });
254
249
  }
255
250
  });
256
- it('is a no-op when pack is already real (idempotent)', () => {
251
+ it('is a no-op when pack is already real (idempotent)', async () => {
257
252
  const { reconcilePack } = require('../../../src/commands/sensors/run');
258
253
  const dir = tmpProject('js-ts', true);
259
254
  try {
@@ -266,7 +261,7 @@ describe('reconcilePack', () => {
266
261
  fs_1.default.rmSync(dir, { recursive: true });
267
262
  }
268
263
  });
269
- it('does not upgrade a truly generic project (no indicators)', () => {
264
+ it('does not upgrade a truly generic project (no indicators)', async () => {
270
265
  const { reconcilePack } = require('../../../src/commands/sensors/run');
271
266
  const dir = tmpProject('generic', false);
272
267
  try {
@@ -288,44 +283,32 @@ describe('runSensors — test sensor (exit-code)', () => {
288
283
  jest.resetModules();
289
284
  tmpDir = fs_1.default.mkdtempSync(path.join(os.tmpdir(), 'awm-run-test-'));
290
285
  fs_1.default.mkdirSync(path.join(tmpDir, '.awm'), { recursive: true });
291
- mockExecSyncFn.mockReset();
286
+ mockRunCommand.mockReset();
292
287
  });
293
288
  afterEach(() => { fs_1.default.rmSync(tmpDir, { recursive: true }); });
294
289
  const load = () => require('../../../src/commands/sensors/run');
295
- it('test sensor: passing run (exit 0 with output) is pass, not fail', () => {
290
+ it('test sensor: passing run (exit 0 with output) is pass, not fail', async () => {
296
291
  fs_1.default.writeFileSync(path.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({ pack: 'js-ts', sensors: { test: { cmd: 'npm test', fast: false } } }));
297
- mockExecSyncFn.mockReturnValue('Tests: 6 passed, 6 total\n'); // runner prints on success
292
+ mockRunCommand.mockResolvedValue(ok('Tests: 6 passed, 6 total\n')); // runner prints on success
298
293
  const { runSensors } = load();
299
- const result = runSensors({ all: true, cwd: tmpDir });
294
+ const result = await runSensors({ all: true, cwd: tmpDir });
300
295
  const test = result.sensors.find((s) => s.name === 'test');
301
296
  expect(test.status).toBe('pass');
302
297
  });
303
- it('test sensor: failing run (non-zero exit) is fail, not skipped', () => {
298
+ it('test sensor: failing run (non-zero exit) is fail, not skipped', async () => {
304
299
  fs_1.default.writeFileSync(path.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({ pack: 'js-ts', sensors: { test: { cmd: 'npm test', fast: false } } }));
305
- mockExecSyncFn.mockImplementation(() => {
306
- const err = new Error('jest failed');
307
- err.status = 1;
308
- err.stdout = 'Tests: 1 failed, 5 passed\n';
309
- err.stderr = '';
310
- throw err;
311
- });
300
+ mockRunCommand.mockResolvedValue(exited(1, 'Tests: 1 failed, 5 passed\n'));
312
301
  const { runSensors } = load();
313
- const result = runSensors({ all: true, cwd: tmpDir });
302
+ const result = await runSensors({ all: true, cwd: tmpDir });
314
303
  const test = result.sensors.find((s) => s.name === 'test');
315
304
  expect(test.status).toBe('fail');
316
305
  expect(result.overall).toBe('fail');
317
306
  });
318
- it('test sensor: missing npm test script exits non-zero → fail, not skipped', () => {
307
+ it('test sensor: missing npm test script exits non-zero → fail, not skipped', async () => {
319
308
  fs_1.default.writeFileSync(path.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({ pack: 'js-ts', sensors: { test: { cmd: 'npm test', fast: false } } }));
320
- mockExecSyncFn.mockImplementation(() => {
321
- const err = new Error('npm test: missing script');
322
- err.status = 1;
323
- err.stdout = 'npm error Missing script: test\n';
324
- err.stderr = '';
325
- throw err;
326
- });
309
+ mockRunCommand.mockResolvedValue(exited(1, 'npm error Missing script: test\n'));
327
310
  const { runSensors } = load();
328
- const result = runSensors({ all: true, cwd: tmpDir });
311
+ const result = await runSensors({ all: true, cwd: tmpDir });
329
312
  const test = result.sensors.find((s) => s.name === 'test');
330
313
  expect(test.status).toBe('fail');
331
314
  expect(result.overall).toBe('fail');
@@ -335,10 +318,10 @@ describe('runSensors — honest floor (not_certified over real stack)', () => {
335
318
  const path = require('path');
336
319
  const os = require('os');
337
320
  beforeEach(() => {
338
- mockExecSyncFn.mockReset();
339
- mockExecSyncFn.mockReturnValue('');
321
+ mockRunCommand.mockReset();
322
+ mockRunCommand.mockResolvedValue(ok());
340
323
  });
341
- it('returns not_certified (not skipped) for a generic manifest over a real stack', () => {
324
+ it('returns not_certified (not skipped) for a generic manifest over a real stack', async () => {
342
325
  const dir = fs_1.default.mkdtempSync(path.join(os.tmpdir(), 'awm-floor-'));
343
326
  fs_1.default.mkdirSync(path.join(dir, '.awm'), { recursive: true });
344
327
  fs_1.default.writeFileSync(path.join(dir, '.awm', 'sensors.json'), JSON.stringify({ pack: 'generic', sensors: { security: { cmd: 'semgrep .', fast: false } } }));
@@ -349,7 +332,7 @@ describe('runSensors — honest floor (not_certified over real stack)', () => {
349
332
  try {
350
333
  jest.resetModules();
351
334
  const { runSensors } = require('../../../src/commands/sensors/run');
352
- const result = runSensors({ fast: true, cwd: dir }); // --fast filters the fast:false security sensor → empty
335
+ const result = await runSensors({ fast: true, cwd: dir }); // --fast filters the fast:false security sensor → empty
353
336
  expect(result.overall).toBe('not_certified');
354
337
  }
355
338
  finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-workflow-manager",
3
- "version": "3.5.0",
3
+ "version": "3.7.0",
4
4
  "main": "dist/src/index.js",
5
5
  "bin": {
6
6
  "awm": "./dist/src/index.js"