agentic-workflow-manager 3.9.1 → 3.11.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/preflight/checks.js +115 -0
- package/dist/src/commands/sensors/formatters/mypy.js +30 -0
- package/dist/src/commands/sensors/formatters/ruff.js +45 -0
- package/dist/src/commands/sensors/formatters/shellcheck.js +45 -0
- package/dist/src/commands/sensors/index.js +11 -4
- package/dist/src/commands/sensors/init.js +80 -15
- package/dist/src/commands/sensors/run.js +36 -5
- package/dist/src/commands/sensors/status.js +9 -14
- package/dist/src/core/paths.js +13 -0
- package/dist/tests/commands/preflight/preflight.test.js +200 -0
- package/dist/tests/commands/sensors/formatters/mypy.test.js +60 -0
- package/dist/tests/commands/sensors/formatters/ruff.test.js +92 -0
- package/dist/tests/commands/sensors/formatters/shellcheck.test.js +65 -0
- package/dist/tests/commands/sensors/init.test.js +159 -4
- package/dist/tests/commands/sensors/run.test.js +91 -0
- package/dist/tests/commands/sensors/status.test.js +29 -0
- package/dist/tests/core/paths.test.js +34 -0
- package/package.json +1 -1
|
@@ -86,6 +86,79 @@ describe('runSensors', () => {
|
|
|
86
86
|
expect(sec.status).toBe('skipped');
|
|
87
87
|
expect(sec.skipReason).toBe('disabled');
|
|
88
88
|
});
|
|
89
|
+
it('dispatches by the formatter field, not the sensor name — ruff on a `lint` sensor', async () => {
|
|
90
|
+
// The whole point of the `formatter` field: a `lint` sensor is eslint for
|
|
91
|
+
// js-ts but ruff for python. Old name-based dispatch (lint -> eslint parser)
|
|
92
|
+
// would choke on ruff's flat JSON array (no `.messages` — TypeError) instead
|
|
93
|
+
// of parsing it. Real captured `ruff --output-format json` shape.
|
|
94
|
+
const pythonManifest = {
|
|
95
|
+
pack: 'python',
|
|
96
|
+
sensors: {
|
|
97
|
+
lint: { cmd: 'ruff check . --output-format json', fast: true, formatter: 'ruff' },
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
fs_1.default.writeFileSync(path.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify(pythonManifest));
|
|
101
|
+
const ruffJson = JSON.stringify([{
|
|
102
|
+
code: 'F401',
|
|
103
|
+
filename: path.join(tmpDir, 'bad.py'),
|
|
104
|
+
location: { row: 1, column: 8 },
|
|
105
|
+
message: '`os` imported but unused',
|
|
106
|
+
}]);
|
|
107
|
+
mockRunCommand.mockResolvedValueOnce(exited(1, ruffJson));
|
|
108
|
+
const { runSensors } = load();
|
|
109
|
+
const result = await runSensors({ fast: true, cwd: tmpDir });
|
|
110
|
+
const lint = result.sensors.find((s) => s.name === 'lint');
|
|
111
|
+
expect(lint.status).toBe('fail');
|
|
112
|
+
expect(lint.errors[0].rule).toBe('F401');
|
|
113
|
+
expect(lint.errors[0].message).toMatch('SENSOR[lint]');
|
|
114
|
+
expect(lint.errors[0].message).toMatch('imported but unused');
|
|
115
|
+
});
|
|
116
|
+
it('falls back to name-based dispatch when formatter is absent (pre-existing manifest)', async () => {
|
|
117
|
+
// A manifest written before the `formatter` field existed must keep working
|
|
118
|
+
// exactly as before: `lint` -> eslint parser, no `formatter` key anywhere.
|
|
119
|
+
mockRunCommand
|
|
120
|
+
.mockResolvedValueOnce(exited(1, 'src/a.ts(1,1): error TS0001: Bad type.'))
|
|
121
|
+
.mockResolvedValueOnce(exited(1, JSON.stringify([{ filePath: '/x/a.js', messages: [{ ruleId: 'no-unused-vars', severity: 2, message: 'unused', line: 1, column: 1 }] }])));
|
|
122
|
+
const { runSensors } = load();
|
|
123
|
+
const result = await runSensors({ fast: true, cwd: tmpDir });
|
|
124
|
+
const lint = result.sensors.find((s) => s.name === 'lint');
|
|
125
|
+
expect(lint.status).toBe('fail');
|
|
126
|
+
expect(lint.errors[0].rule).toBe('no-unused-vars');
|
|
127
|
+
});
|
|
128
|
+
it('degrades to the generic formatter (never a wrong-shape misparse) for an unrecognized formatter value', async () => {
|
|
129
|
+
// Regression for Finding 5: `formatter: 'bandit'` is present but not one of the
|
|
130
|
+
// 8 known values. The OLD behavior fell through to name-based dispatch — sensor
|
|
131
|
+
// name 'security' -> parseSemgrepOutput — which would silently misparse
|
|
132
|
+
// bandit's differently-shaped `results` entries (no `path`/`start`/`check_id`
|
|
133
|
+
// keys) into garbage findings (`file: undefined, line: 0`). The fix must use
|
|
134
|
+
// parseGenericOutput instead: an honest raw-wrap, never a wrong-shape guess.
|
|
135
|
+
const banditManifest = {
|
|
136
|
+
pack: 'python',
|
|
137
|
+
sensors: {
|
|
138
|
+
security: { cmd: 'bandit -r . -f json', fast: false, formatter: 'bandit' },
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
fs_1.default.writeFileSync(path.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify(banditManifest));
|
|
142
|
+
// Real bandit JSON shape (fields semgrep's parser does not know how to read).
|
|
143
|
+
const banditJson = JSON.stringify({
|
|
144
|
+
results: [
|
|
145
|
+
{ filename: 'app.py', line_number: 12, test_id: 'B105', issue_text: 'hardcoded password' },
|
|
146
|
+
],
|
|
147
|
+
});
|
|
148
|
+
mockRunCommand.mockResolvedValueOnce(exited(1, banditJson));
|
|
149
|
+
const { runSensors } = load();
|
|
150
|
+
const result = await runSensors({ all: true, cwd: tmpDir });
|
|
151
|
+
const sec = result.sensors.find((s) => s.name === 'security');
|
|
152
|
+
expect(sec.status).toBe('fail');
|
|
153
|
+
expect(sec.errors).toHaveLength(1);
|
|
154
|
+
// Never the semgrep-misparse shape (file: undefined, line: 0, rule: undefined).
|
|
155
|
+
expect(sec.errors[0].file).toBeUndefined();
|
|
156
|
+
expect(sec.errors[0].line).toBeUndefined();
|
|
157
|
+
expect(sec.errors[0].rule).toBeUndefined();
|
|
158
|
+
// The honest generic raw-wrap: the raw JSON, verbatim, inside one message.
|
|
159
|
+
expect(sec.errors[0].message).toMatch('SENSOR[raw]');
|
|
160
|
+
expect(sec.errors[0].message).toContain('B105');
|
|
161
|
+
});
|
|
89
162
|
const tcError = () => exited(1, 'src/a.ts(1,1): error TS0001: Bad type.');
|
|
90
163
|
it('baseline suppresses accepted findings — sensor passes on no NEW findings', async () => {
|
|
91
164
|
const { runSensors } = load();
|
|
@@ -160,6 +233,24 @@ describe('runSensors — missing tool is a fail, not a skip', () => {
|
|
|
160
233
|
expect(out.overall).toBe('fail');
|
|
161
234
|
});
|
|
162
235
|
});
|
|
236
|
+
describe('runSensors — sensors: null in a hand-edited manifest', () => {
|
|
237
|
+
// Regression for Finding 3: `checkManifest` (preflight) already guards
|
|
238
|
+
// `manifest.sensors ?? {}` — runSensors' `Object.entries(activeManifest.sensors)`
|
|
239
|
+
// needs the same guard, or a corrupted/hand-edited `.awm/sensors.json` with
|
|
240
|
+
// `"sensors": null` crashes `Object.entries(null)`, taking down the whole run.
|
|
241
|
+
it('degrades gracefully instead of throwing when sensors is null', async () => {
|
|
242
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-null-sensors-'));
|
|
243
|
+
fs_1.default.mkdirSync(path_1.default.join(dir, '.awm'), { recursive: true });
|
|
244
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, '.awm', 'sensors.json'), JSON.stringify({ pack: 'python', sensors: null }));
|
|
245
|
+
try {
|
|
246
|
+
const result = await (0, run_1.runSensors)({ cwd: dir, all: true });
|
|
247
|
+
expect(result.sensors).toEqual([]);
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
fs_1.default.rmSync(dir, { recursive: true });
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
});
|
|
163
254
|
describe('runSensors — not_certified + auto-discovery', () => {
|
|
164
255
|
let tmpDir;
|
|
165
256
|
beforeEach(() => {
|
|
@@ -109,6 +109,35 @@ describe('computeSensorStatus', () => {
|
|
|
109
109
|
expect(result.checks.security.ok).toBe(true);
|
|
110
110
|
});
|
|
111
111
|
});
|
|
112
|
+
it('is DEGRADED (never HEALTHY) when the manifest has zero sensor entries', () => {
|
|
113
|
+
// `Object.values({}).every(...)` is vacuously true — guard against reading an
|
|
114
|
+
// empty manifest (the honest floor when the registry had no pack.json for the
|
|
115
|
+
// detected stack) as a clean run that found nothing wrong.
|
|
116
|
+
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
117
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
118
|
+
pack: 'python',
|
|
119
|
+
sensors: {},
|
|
120
|
+
}));
|
|
121
|
+
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
122
|
+
expect(result.overall).toBe('DEGRADED');
|
|
123
|
+
expect(result.pack).toBe('python');
|
|
124
|
+
});
|
|
125
|
+
it('degrades gracefully (never throws) when sensors is null in a hand-edited manifest', () => {
|
|
126
|
+
// Regression for Finding 3: `checkManifest` (preflight) already guards
|
|
127
|
+
// `manifest.sensors ?? {}` — computeSensorStatus needs the same guard, or a
|
|
128
|
+
// corrupted/hand-edited manifest with `"sensors": null` crashes
|
|
129
|
+
// `Object.entries(null)`.
|
|
130
|
+
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
131
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
132
|
+
pack: 'python',
|
|
133
|
+
sensors: null,
|
|
134
|
+
}));
|
|
135
|
+
expect(() => (0, status_1.computeSensorStatus)(tmpDir)).not.toThrow();
|
|
136
|
+
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
137
|
+
expect(result.overall).toBe('DEGRADED');
|
|
138
|
+
expect(result.pack).toBe('python');
|
|
139
|
+
expect(result.checks).toEqual({});
|
|
140
|
+
});
|
|
112
141
|
it('marks disabled sensors as ok', () => {
|
|
113
142
|
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
114
143
|
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
@@ -5,7 +5,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const os_1 = __importDefault(require("os"));
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
8
9
|
const paths_1 = require("../../src/core/paths");
|
|
10
|
+
jest.mock('child_process', () => ({ execSync: jest.fn() }));
|
|
11
|
+
const mockExecSync = child_process_1.execSync;
|
|
9
12
|
describe('core/paths', () => {
|
|
10
13
|
let origHome;
|
|
11
14
|
let origAwmHome;
|
|
@@ -13,6 +16,7 @@ describe('core/paths', () => {
|
|
|
13
16
|
beforeEach(() => {
|
|
14
17
|
origHome = process.env.HOME;
|
|
15
18
|
origAwmHome = process.env.AWM_HOME;
|
|
19
|
+
mockExecSync.mockReset();
|
|
16
20
|
});
|
|
17
21
|
afterEach(() => {
|
|
18
22
|
if (origHome === undefined)
|
|
@@ -75,4 +79,34 @@ describe('core/paths', () => {
|
|
|
75
79
|
(0, paths_1.warnIfUnsupportedPlatform)(log);
|
|
76
80
|
expect(calls).toEqual([paths_1.WINDOWS_NATIVE_WARNING]);
|
|
77
81
|
});
|
|
82
|
+
describe('resolveOnPath', () => {
|
|
83
|
+
it('uses `command -v` on POSIX and returns true when the binary resolves', () => {
|
|
84
|
+
setPlatform('linux');
|
|
85
|
+
mockExecSync.mockImplementation(((cmd) => {
|
|
86
|
+
if (cmd === 'command -v semgrep')
|
|
87
|
+
return Buffer.from('/usr/bin/semgrep');
|
|
88
|
+
throw new Error(`not found: ${cmd}`);
|
|
89
|
+
}));
|
|
90
|
+
expect((0, paths_1.resolveOnPath)('semgrep')).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
it('returns false on POSIX when `command -v` fails to resolve the binary', () => {
|
|
93
|
+
setPlatform('linux');
|
|
94
|
+
mockExecSync.mockImplementation(() => { throw new Error('not found'); });
|
|
95
|
+
expect((0, paths_1.resolveOnPath)('semgrep')).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
it('uses `where` on win32, not `command -v`', () => {
|
|
98
|
+
setPlatform('win32');
|
|
99
|
+
mockExecSync.mockImplementation(((cmd) => {
|
|
100
|
+
if (cmd === 'where semgrep')
|
|
101
|
+
return Buffer.from('C:\\tools\\semgrep.exe');
|
|
102
|
+
throw new Error(`not found: ${cmd}`);
|
|
103
|
+
}));
|
|
104
|
+
expect((0, paths_1.resolveOnPath)('semgrep')).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
it('returns false on win32 when `where` cannot find the binary', () => {
|
|
107
|
+
setPlatform('win32');
|
|
108
|
+
mockExecSync.mockImplementation(() => { throw new Error('not found'); });
|
|
109
|
+
expect((0, paths_1.resolveOnPath)('semgrep')).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
78
112
|
});
|