agentic-workflow-manager 9.5.0 → 9.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/context/index.js +3 -1
- package/dist/src/commands/preflight/checks.js +2 -2
- package/dist/src/commands/sensors/bootstrap.js +171 -0
- package/dist/src/commands/sensors/compatibility/contract.js +18 -15
- package/dist/src/commands/sensors/compatibility/manifest.js +29 -1
- package/dist/src/commands/sensors/compatibility/materialize.js +256 -54
- package/dist/src/commands/sensors/compatibility/safe-file.js +240 -5
- package/dist/src/commands/sensors/coverage/index.js +2 -2
- package/dist/src/commands/sensors/coverage/resolve.js +20 -10
- package/dist/src/commands/sensors/detection.js +31 -0
- package/dist/src/commands/sensors/exec.js +116 -11
- package/dist/src/commands/sensors/index.js +51 -10
- package/dist/src/commands/sensors/init.js +17 -170
- package/dist/src/commands/sensors/migrate.js +209 -0
- package/dist/src/commands/sensors/run.js +2 -2
- package/dist/src/core/discovery.js +79 -4
- package/dist/src/core/init/steps.js +24 -11
- package/dist/src/core/orchestrators.js +34 -10
- package/dist/src/core/secure-fs/native-bridge.js +209 -0
- package/dist/src/core/text.js +7 -0
- package/dist/tests/commands/context.test.js +9 -1
- package/dist/tests/commands/hooks/install.test.js +4 -0
- package/dist/tests/commands/hooks/resync.test.js +2 -0
- package/dist/tests/commands/sensors/bootstrap-observational.test.js +24 -0
- package/dist/tests/commands/sensors/bootstrap.test.js +175 -0
- package/dist/tests/commands/sensors/compatibility/contract.test.js +38 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +67 -0
- package/dist/tests/commands/sensors/compatibility/materialize.test.js +410 -0
- package/dist/tests/commands/sensors/compatibility/pack-source.test.js +34 -1
- package/dist/tests/commands/sensors/compatibility/safe-file.test.js +52 -0
- package/dist/tests/commands/sensors/coverage/index.test.js +20 -0
- package/dist/tests/commands/sensors/coverage/resolve.test.js +10 -0
- package/dist/tests/commands/sensors/exec.test.js +86 -0
- package/dist/tests/commands/sensors/index.test.js +54 -0
- package/dist/tests/commands/sensors/init-pack-unavailable.test.js +21 -84
- package/dist/tests/commands/sensors/init.test.js +56 -490
- package/dist/tests/commands/sensors/migrate.test.js +513 -0
- package/dist/tests/core/context/orchestrator.test.js +2 -0
- package/dist/tests/core/discovery-multiroot.test.js +5 -0
- package/dist/tests/core/discovery-parent-race.test.js +95 -0
- package/dist/tests/core/discovery-safe-read.test.js +115 -0
- package/dist/tests/core/discovery.test.js +28 -1
- package/dist/tests/core/init/orchestrator.test.js +3 -1
- package/dist/tests/core/orchestrators.test.js +171 -14
- package/dist/tests/core/secure-fs/native-bridge.test.js +733 -0
- package/dist/tests/core/text.test.js +10 -0
- package/dist/tests/integration/context-orchestrators-e2e.test.js +57 -7
- package/dist/tests/integration/sensor-bootstrap.e2e.test.js +165 -0
- package/dist/tests/integration/sensor-compatibility.e2e.test.js +10 -2
- package/dist/tests/integration/sensor-structured-run.e2e.test.js +3 -1
- package/dist/tests/structural/native-release-artifacts.test.js +22 -0
- package/dist/tests/structural/release-flags-are-reachable-from-ci.test.js +10 -0
- package/dist/tests/structural/release-registry-provenance.test.js +4 -0
- package/dist/tests/structural/sensor-portability-contract.test.js +44 -0
- package/package.json +6 -2
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/linux-arm64/secure_fs.node +0 -0
- package/prebuilds/linux-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -8,6 +8,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
10
|
const result_1 = require("../../../src/commands/sensors/result");
|
|
11
|
+
const baseline_1 = require("../../../src/commands/sensors/baseline");
|
|
11
12
|
const prepare_1 = require("../../../src/commands/sensors/prepare");
|
|
12
13
|
const sensor = (overrides = {}) => ({
|
|
13
14
|
name: 'lint',
|
|
@@ -21,6 +22,7 @@ const sensor = (overrides = {}) => ({
|
|
|
21
22
|
});
|
|
22
23
|
const onPosix = process.platform !== 'win32' ? describe : describe.skip;
|
|
23
24
|
const itPosix = process.platform !== 'win32' ? it : it.skip;
|
|
25
|
+
const itLinux = process.platform === 'linux' ? it : it.skip;
|
|
24
26
|
/** Poll until `fn()` is true or the budget runs out. Avoids fixed sleeps. */
|
|
25
27
|
async function until(fn, budgetMs = 4000) {
|
|
26
28
|
const deadline = Date.now() + budgetMs;
|
|
@@ -57,6 +59,78 @@ describe('runCommand — exit codes and output', () => {
|
|
|
57
59
|
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
58
60
|
}
|
|
59
61
|
});
|
|
62
|
+
it('preserves a structured non-zero report for interpretation and baseline suppression', async () => {
|
|
63
|
+
const command = {
|
|
64
|
+
executable: 'node',
|
|
65
|
+
resolution: 'path',
|
|
66
|
+
args: ['-e', "console.log('structured finding'); process.exit(1)"],
|
|
67
|
+
};
|
|
68
|
+
const raw = await (0, exec_1.runStructuredCommand)(command, { timeout: 5_000, cwd: process.cwd() });
|
|
69
|
+
expect(raw).toMatchObject({ code: 1, stdout: 'structured finding\n', timedOut: false, overflowed: false });
|
|
70
|
+
const interpreted = (0, result_1.interpretResult)(sensor({ command: { kind: 'structured', value: command } }), raw);
|
|
71
|
+
expect(interpreted).toMatchObject({ status: 'fail', errors: [{ message: expect.stringContaining('structured finding') }] });
|
|
72
|
+
const suppressed = (0, result_1.applyBaseline)(interpreted, [(0, baseline_1.fingerprint)('lint', interpreted.errors[0])]);
|
|
73
|
+
expect(suppressed).toMatchObject({ status: 'pass', errors: [], baselineCount: 1 });
|
|
74
|
+
});
|
|
75
|
+
it('keeps an unrelated structured non-zero exit non-passing', async () => {
|
|
76
|
+
const raw = await (0, exec_1.runStructuredCommand)({
|
|
77
|
+
executable: 'node',
|
|
78
|
+
resolution: 'path',
|
|
79
|
+
args: ['-e', 'process.exit(3)'],
|
|
80
|
+
}, { timeout: 5_000, cwd: process.cwd() });
|
|
81
|
+
const interpreted = (0, result_1.interpretResult)(sensor({ command: { kind: 'structured', value: { executable: 'node', resolution: 'path', args: ['-e', 'process.exit(3)'] } } }), raw);
|
|
82
|
+
expect(interpreted.status).not.toBe('pass');
|
|
83
|
+
});
|
|
84
|
+
itLinux('keeps structured stderr separate from exit-0 stdout findings', async () => {
|
|
85
|
+
const command = {
|
|
86
|
+
executable: 'perl',
|
|
87
|
+
resolution: 'path',
|
|
88
|
+
args: ['-e', 'print STDERR "warning only\\n"'],
|
|
89
|
+
};
|
|
90
|
+
const raw = await (0, exec_1.runStructuredCommand)(command, { timeout: 5_000, cwd: process.cwd() });
|
|
91
|
+
expect(raw).toMatchObject({ code: 0, stdout: '', stderr: 'warning only\n' });
|
|
92
|
+
expect((0, result_1.interpretResult)(sensor({ command: { kind: 'structured', value: command } }), raw)).toMatchObject({ status: 'pass', errors: [] });
|
|
93
|
+
});
|
|
94
|
+
itLinux('never lets a structured capture file grow beyond maxBuffer', async () => {
|
|
95
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-exec-cap-'));
|
|
96
|
+
const marker = path_1.default.join(dir, 'written');
|
|
97
|
+
const existing = new Set(fs_1.default.readdirSync(os_1.default.tmpdir()).filter(name => name.startsWith('awm-sensor-output-')));
|
|
98
|
+
try {
|
|
99
|
+
const result = (0, exec_1.runStructuredCommand)({
|
|
100
|
+
executable: 'node',
|
|
101
|
+
resolution: 'path',
|
|
102
|
+
args: ['-e', `const fs = require('fs'); try { fs.writeSync(1, Buffer.alloc(8192, 'x')); } catch {} fs.writeFileSync(${JSON.stringify(marker)}, 'written'); setTimeout(() => {}, 1000);`],
|
|
103
|
+
}, { timeout: 5_000, cwd: dir, maxBuffer: 1_024 });
|
|
104
|
+
expect(await until(() => fs_1.default.existsSync(marker))).toBe(true);
|
|
105
|
+
const capture = fs_1.default.readdirSync(os_1.default.tmpdir()).find(name => name.startsWith('awm-sensor-output-') && !existing.has(name));
|
|
106
|
+
expect(capture).toBeDefined();
|
|
107
|
+
expect(fs_1.default.statSync(path_1.default.join(os_1.default.tmpdir(), capture, 'stdout')).size).toBeLessThanOrEqual(1_024);
|
|
108
|
+
await expect(result).resolves.toMatchObject({ overflowed: true });
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
itLinux('bounds combined structured stdout and stderr by maxBuffer', async () => {
|
|
115
|
+
const result = await (0, exec_1.runStructuredCommand)({
|
|
116
|
+
executable: 'node',
|
|
117
|
+
resolution: 'path',
|
|
118
|
+
args: ['-e', "process.stdout.write('o'.repeat(700)); process.stderr.write('e'.repeat(700)); setTimeout(() => {}, 1000);"],
|
|
119
|
+
}, { timeout: 5_000, cwd: process.cwd(), maxBuffer: 1_024 });
|
|
120
|
+
expect(result.overflowed).toBe(true);
|
|
121
|
+
expect(Buffer.byteLength(result.stdout) + Buffer.byteLength(result.stderr)).toBeLessThanOrEqual(1_024);
|
|
122
|
+
expect(result.stdout).toMatch(/^o+$/);
|
|
123
|
+
expect(result.stderr).toMatch(/^e+$/);
|
|
124
|
+
});
|
|
125
|
+
itLinux('allows one structured stream to use the full global budget', async () => {
|
|
126
|
+
const result = await (0, exec_1.runStructuredCommand)({
|
|
127
|
+
executable: 'perl',
|
|
128
|
+
resolution: 'path',
|
|
129
|
+
args: ['-e', "print 'o' x 700"],
|
|
130
|
+
}, { timeout: 5_000, cwd: process.cwd(), maxBuffer: 1_024 });
|
|
131
|
+
expect(result).toMatchObject({ code: 0, overflowed: false, stderr: '' });
|
|
132
|
+
expect(Buffer.byteLength(result.stdout)).toBe(700);
|
|
133
|
+
});
|
|
60
134
|
it('returns stdout and code 0 for a clean command', async () => {
|
|
61
135
|
const r = await (0, exec_1.runCommand)('echo hello', { timeout: 5000, cwd: process.cwd() });
|
|
62
136
|
expect(r.code).toBe(0);
|
|
@@ -107,6 +181,18 @@ onPosix('runCommand — output cap', () => {
|
|
|
107
181
|
// The point of the cap change: what was read is still usable, not discarded.
|
|
108
182
|
expect(r.stdout).toMatch(/line-padding/);
|
|
109
183
|
});
|
|
184
|
+
it('counts multibyte UTF-8 output against maxBuffer in bytes', async () => {
|
|
185
|
+
const r = await (0, exec_1.runCommand)("yes '€'", { timeout: 5_000, cwd: process.cwd(), maxBuffer: 1_024 });
|
|
186
|
+
expect(r.overflowed).toBe(true);
|
|
187
|
+
expect(Buffer.byteLength(r.stdout) + Buffer.byteLength(r.stderr)).toBeLessThanOrEqual(1_024);
|
|
188
|
+
});
|
|
189
|
+
it.each([
|
|
190
|
+
["perl -e 'print chr(255) x 1024'", 'invalid bytes'],
|
|
191
|
+
["perl -e 'print \"\\xE2\\x82\" x 512'", 'truncated UTF-8 sequences'],
|
|
192
|
+
])('never expands %s beyond maxBuffer while decoding %s', async (command) => {
|
|
193
|
+
const r = await (0, exec_1.runCommand)(command, { timeout: 5_000, cwd: process.cwd(), maxBuffer: 1_024 });
|
|
194
|
+
expect(Buffer.byteLength(r.stdout) + Buffer.byteLength(r.stderr)).toBeLessThanOrEqual(1_024);
|
|
195
|
+
});
|
|
110
196
|
});
|
|
111
197
|
onPosix('runCommand — timeout', () => {
|
|
112
198
|
it('flags the timeout and returns the output produced before the deadline', async () => {
|
|
@@ -10,12 +10,14 @@ jest.mock('../../../src/commands/sensors/baseline', () => ({ buildBaseline: jest
|
|
|
10
10
|
jest.mock('../../../src/core/registries', () => ({ capabilityRoot: jest.fn(() => '/mock/registry') }));
|
|
11
11
|
jest.mock('../../../src/commands/sensors/coverage', () => ({ runCoverage: jest.fn() }));
|
|
12
12
|
jest.mock('../../../src/commands/sensors/coverage/render', () => ({ renderCoverageHuman: jest.fn(), renderCoverageJson: jest.fn() }));
|
|
13
|
+
jest.mock('../../../src/commands/sensors/bootstrap', () => ({ planSensorBootstrap: jest.fn(), applySensorBootstrap: jest.fn() }));
|
|
13
14
|
const commander_1 = require("commander");
|
|
14
15
|
const prompts_1 = require("@clack/prompts");
|
|
15
16
|
const coverage_1 = require("../../../src/commands/sensors/coverage");
|
|
16
17
|
const render_1 = require("../../../src/commands/sensors/coverage/render");
|
|
17
18
|
const index_1 = require("../../../src/commands/sensors/index");
|
|
18
19
|
const verdict_1 = require("../../../src/commands/sensors/verdict");
|
|
20
|
+
const bootstrap_1 = require("../../../src/commands/sensors/bootstrap");
|
|
19
21
|
const processExit = jest.spyOn(process, 'exit').mockImplementation((() => undefined));
|
|
20
22
|
const stdoutWrite = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
21
23
|
describe('exitCodeForVerdict — sensor run verdict → exit code', () => {
|
|
@@ -28,6 +30,58 @@ describe('exitCodeForVerdict — sensor run verdict → exit code', () => {
|
|
|
28
30
|
expect((0, verdict_1.exitCodeForVerdict)(overall)).toBe(code);
|
|
29
31
|
});
|
|
30
32
|
});
|
|
33
|
+
describe('sensors bootstrap Commander wiring', () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
jest.clearAllMocks();
|
|
36
|
+
process.exitCode = undefined;
|
|
37
|
+
});
|
|
38
|
+
it('renders dry-run without calling the applier', async () => {
|
|
39
|
+
bootstrap_1.planSensorBootstrap.mockResolvedValue({
|
|
40
|
+
kind: 'create', dryRun: true, changes: [{ path: '.awm/sensors.json', action: 'create' }],
|
|
41
|
+
manifest: { schemaVersion: 3, mode: 'native-gate', reason: 'CI' },
|
|
42
|
+
});
|
|
43
|
+
const program = new commander_1.Command();
|
|
44
|
+
program.exitOverride();
|
|
45
|
+
(0, index_1.registerSensorsCommand)(program);
|
|
46
|
+
await program.parseAsync(['node', 'awm', 'sensors', 'bootstrap', '--mode', 'native-gate', '--reason', 'CI', '--dry-run']);
|
|
47
|
+
expect(bootstrap_1.planSensorBootstrap).toHaveBeenCalledWith(process.cwd(), { mode: 'native-gate', reason: 'CI', dryRun: true });
|
|
48
|
+
expect(bootstrap_1.applySensorBootstrap).not.toHaveBeenCalled();
|
|
49
|
+
expect(prompts_1.log.info).toHaveBeenCalledWith(expect.stringContaining('dry-run'));
|
|
50
|
+
});
|
|
51
|
+
it('renders the chosen project-sensors pack in dry-run output', async () => {
|
|
52
|
+
bootstrap_1.planSensorBootstrap.mockResolvedValue({
|
|
53
|
+
kind: 'create', dryRun: true, changes: [{ path: '.awm/sensors.json', action: 'create' }],
|
|
54
|
+
manifest: { schemaVersion: 3, mode: 'project-sensors', pack: 'js-ts' },
|
|
55
|
+
});
|
|
56
|
+
const program = new commander_1.Command();
|
|
57
|
+
program.exitOverride();
|
|
58
|
+
(0, index_1.registerSensorsCommand)(program);
|
|
59
|
+
await program.parseAsync(['node', 'awm', 'sensors', 'bootstrap', '--mode', 'project-sensors', '--dry-run']);
|
|
60
|
+
expect(prompts_1.log.info).toHaveBeenCalledWith(expect.stringContaining('pack=js-ts'));
|
|
61
|
+
expect(bootstrap_1.applySensorBootstrap).not.toHaveBeenCalled();
|
|
62
|
+
});
|
|
63
|
+
it('applies an executable plan and reports its result', async () => {
|
|
64
|
+
const plan = { kind: 'create', dryRun: false, changes: [{ path: '.awm/sensors.json', action: 'create' }], manifest: { schemaVersion: 3, mode: 'native-gate', reason: 'CI' } };
|
|
65
|
+
bootstrap_1.planSensorBootstrap.mockResolvedValue(plan);
|
|
66
|
+
bootstrap_1.applySensorBootstrap.mockReturnValue('created');
|
|
67
|
+
const program = new commander_1.Command();
|
|
68
|
+
program.exitOverride();
|
|
69
|
+
(0, index_1.registerSensorsCommand)(program);
|
|
70
|
+
await program.parseAsync(['node', 'awm', 'sensors', 'bootstrap', '--mode', 'native-gate', '--reason', 'CI']);
|
|
71
|
+
expect(bootstrap_1.applySensorBootstrap).toHaveBeenCalledWith(plan);
|
|
72
|
+
expect(prompts_1.log.success).toHaveBeenCalledWith(expect.stringContaining('created'));
|
|
73
|
+
});
|
|
74
|
+
it('returns a non-zero semantic exit for a blocked plan', async () => {
|
|
75
|
+
bootstrap_1.planSensorBootstrap.mockResolvedValue({ kind: 'blocked', dryRun: false, reason: 'mode-required', remedy: 'choose-mode' });
|
|
76
|
+
const program = new commander_1.Command();
|
|
77
|
+
program.exitOverride();
|
|
78
|
+
(0, index_1.registerSensorsCommand)(program);
|
|
79
|
+
await program.parseAsync(['node', 'awm', 'sensors', 'bootstrap']);
|
|
80
|
+
expect(process.exitCode).toBe(1);
|
|
81
|
+
expect(prompts_1.log.error).toHaveBeenCalledWith('mode-required: choose-mode');
|
|
82
|
+
expect(bootstrap_1.applySensorBootstrap).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
31
85
|
describe('sensors run Commander wiring', () => {
|
|
32
86
|
const originalExitCode = process.exitCode;
|
|
33
87
|
beforeEach(() => {
|
|
@@ -3,94 +3,31 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
// `awm sensors init` auto-detects the stack, then reads that pack's defaults out of
|
|
7
|
-
// the registry. When the registry has no such pack — a pinned or stale registry, or one
|
|
8
|
-
// that simply never shipped it — `readPackDefaults` returned null and the manifest was
|
|
9
|
-
// written EMPTY, with no error and no warning:
|
|
10
|
-
//
|
|
11
|
-
// $ awm sensors init # python project, registry predates the python pack
|
|
12
|
-
// ✔ Detected: python (pyproject.toml)
|
|
13
|
-
// ✔ Wrote .awm/sensors.json ← {"pack":"python","sensors":{}}
|
|
14
|
-
//
|
|
15
|
-
// Nothing in that output says the quality gate now checks nothing. The operator finds
|
|
16
|
-
// out later, from an unrelated command (`awm preflight` exits 1, `awm sensors status`
|
|
17
|
-
// reports DEGRADED), and the manifest is by then committed.
|
|
18
|
-
//
|
|
19
|
-
// `--pack <name>` has always validated against the registry and thrown (assertPackExists).
|
|
20
|
-
// Auto-detection reaching the same dead end must not be quieter than the explicit path:
|
|
21
|
-
// it now falls back to a pack the registry actually has, and says which pack was missing
|
|
22
|
-
// so the operator hears it at the moment the decision is made.
|
|
23
6
|
const fs_1 = __importDefault(require("fs"));
|
|
24
7
|
const os_1 = __importDefault(require("os"));
|
|
25
8
|
const path_1 = __importDefault(require("path"));
|
|
9
|
+
jest.mock('../../../src/commands/sensors/bootstrap', () => ({
|
|
10
|
+
planSensorBootstrap: jest.fn(),
|
|
11
|
+
applySensorBootstrap: jest.fn(),
|
|
12
|
+
}));
|
|
26
13
|
const init_1 = require("../../../src/commands/sensors/init");
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
fs_1.default.
|
|
33
|
-
|
|
34
|
-
return root;
|
|
35
|
-
}
|
|
36
|
-
function mkPythonProject() {
|
|
37
|
-
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-packproj-'));
|
|
38
|
-
fs_1.default.writeFileSync(path_1.default.join(dir, 'pyproject.toml'), '[project]\nname = "x"\n');
|
|
39
|
-
return dir;
|
|
40
|
-
}
|
|
41
|
-
const GENERIC = { security: { defaultCmd: 'semgrep .', fast: false } };
|
|
42
|
-
const PYTHON = { typecheck: { defaultCmd: 'mypy .', fast: true, formatter: 'mypy' } };
|
|
43
|
-
describe('awm sensors init — detected pack missing from the registry', () => {
|
|
44
|
-
const made = [];
|
|
45
|
-
const registry = (packs) => { const r = mkRegistry(packs); made.push(r); return r; };
|
|
46
|
-
const project = () => { const p = mkPythonProject(); made.push(p); return p; };
|
|
47
|
-
const manifestOf = (dir) => JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, '.awm', 'sensors.json'), 'utf-8'));
|
|
48
|
-
afterAll(() => { for (const d of made)
|
|
49
|
-
fs_1.default.rmSync(d, { recursive: true, force: true }); });
|
|
50
|
-
it('names the missing pack instead of writing an empty manifest silently', async () => {
|
|
51
|
-
const cwd = project();
|
|
52
|
-
const result = await (0, init_1.initSensors)({ cwd, registryRoot: registry({ 'js-ts': {}, generic: GENERIC }) });
|
|
53
|
-
expect(result.unavailablePack).toBe('python');
|
|
54
|
-
expect(result.detection.pack).toBe('python'); // what the tree actually is — unchanged
|
|
14
|
+
const bootstrap_1 = require("../../../src/commands/sensors/bootstrap");
|
|
15
|
+
describe('initSensors when a pack is unavailable', () => {
|
|
16
|
+
let project;
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
jest.clearAllMocks();
|
|
19
|
+
project = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-init-unavailable-'));
|
|
20
|
+
fs_1.default.writeFileSync(path_1.default.join(project, 'pyproject.toml'), '[project]\nname = "fixture"\n');
|
|
55
21
|
});
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const cwd = project();
|
|
67
|
-
const result = await (0, init_1.initSensors)({ cwd, registryRoot: registry({ 'js-ts': {} }) });
|
|
68
|
-
expect(result.unavailablePack).toBe('python');
|
|
69
|
-
expect(result.manifest.pack).toBe('python');
|
|
70
|
-
expect(result.manifest.sensors).toEqual({});
|
|
71
|
-
});
|
|
72
|
-
it('is quiet when the registry does have the detected pack', async () => {
|
|
73
|
-
const cwd = project();
|
|
74
|
-
const result = await (0, init_1.initSensors)({ cwd, registryRoot: registry({ python: PYTHON, generic: GENERIC }) });
|
|
75
|
-
expect(result.unavailablePack).toBeUndefined();
|
|
76
|
-
expect(result.manifest.pack).toBe('python');
|
|
77
|
-
expect(Object.keys(result.manifest.sensors)).toEqual(['typecheck']);
|
|
78
|
-
});
|
|
79
|
-
it('does not fall back when there is no registry to check against', async () => {
|
|
80
|
-
// No registryRoot means nothing to validate against, the same tolerance
|
|
81
|
-
// `--pack` already has — not evidence that the pack is missing.
|
|
82
|
-
const cwd = project();
|
|
83
|
-
const result = await (0, init_1.initSensors)({ cwd });
|
|
84
|
-
expect(result.unavailablePack).toBeUndefined();
|
|
85
|
-
expect(result.manifest.pack).toBe('python');
|
|
86
|
-
});
|
|
87
|
-
it('keeps sensors the user already had when falling back', async () => {
|
|
88
|
-
// The fallback must not be a way to lose hand-written configuration.
|
|
89
|
-
const cwd = project();
|
|
90
|
-
fs_1.default.mkdirSync(path_1.default.join(cwd, '.awm'), { recursive: true });
|
|
91
|
-
fs_1.default.writeFileSync(path_1.default.join(cwd, '.awm', 'sensors.json'), JSON.stringify({ pack: 'python', sensors: { test: { cmd: 'pytest -q', fast: false } } }));
|
|
92
|
-
const result = await (0, init_1.initSensors)({ cwd, registryRoot: registry({ generic: GENERIC }) });
|
|
93
|
-
expect(result.manifest.sensors.test?.cmd).toBe('pytest -q');
|
|
94
|
-
expect(Object.keys(result.manifest.sensors).sort()).toEqual(['security', 'test']);
|
|
22
|
+
afterEach(() => fs_1.default.rmSync(project, { recursive: true, force: true }));
|
|
23
|
+
it('fails closed with the portable bootstrap remedy rather than writing an empty or fallback v2 manifest', async () => {
|
|
24
|
+
bootstrap_1.planSensorBootstrap.mockResolvedValue({
|
|
25
|
+
kind: 'blocked', projectRoot: project, manifestPath: path_1.default.join(project, '.awm', 'sensors.json'), changes: [], dryRun: false,
|
|
26
|
+
reason: 'source-unavailable', remedy: 'install-registry-or-run-awm-update',
|
|
27
|
+
});
|
|
28
|
+
await expect((0, init_1.initSensors)({ cwd: project, registryRoot: '/stale/registry' }))
|
|
29
|
+
.rejects.toThrow('source-unavailable: install-registry-or-run-awm-update');
|
|
30
|
+
expect(bootstrap_1.applySensorBootstrap).not.toHaveBeenCalled();
|
|
31
|
+
expect(fs_1.default.existsSync(path_1.default.join(project, '.awm', 'sensors.json'))).toBe(false);
|
|
95
32
|
});
|
|
96
33
|
});
|