agentic-workflow-manager 8.0.0 → 8.1.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/sensors/compatibility/contract.js +142 -21
- package/dist/src/commands/sensors/compatibility/discovery.js +1 -1
- package/dist/src/commands/sensors/compatibility/live.js +40 -7
- package/dist/src/commands/sensors/compatibility/manifest.js +13 -3
- package/dist/src/commands/sensors/compatibility/materialize.js +9 -1
- package/dist/src/commands/sensors/compatibility/probe.js +9 -9
- package/dist/src/commands/sensors/compatibility/resolve.js +3 -0
- package/dist/src/commands/sensors/exec.js +106 -12
- package/dist/src/commands/sensors/init.js +6 -3
- package/dist/src/commands/sensors/run.js +1 -1
- package/dist/src/commands/sensors/status.js +1 -1
- package/dist/tests/commands/sensors/compatibility/contract.test.js +95 -0
- package/dist/tests/commands/sensors/compatibility/live.test.js +124 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +9 -0
- package/dist/tests/commands/sensors/compatibility/materialize.test.js +19 -0
- package/dist/tests/commands/sensors/compatibility/probe.test.js +10 -0
- package/dist/tests/commands/sensors/compatibility/python-venv-fixture.js +29 -0
- package/dist/tests/commands/sensors/compatibility/resolve.test.js +8 -0
- package/dist/tests/commands/sensors/exec-windows.test.js +58 -10
- package/dist/tests/commands/sensors/exec.test.js +51 -3
- package/dist/tests/commands/sensors/init.test.js +72 -0
- package/dist/tests/structural/r3-cli-major-version.test.js +3 -3
- package/package.json +1 -1
|
@@ -90,6 +90,7 @@ function collectSpawn(input, opts) {
|
|
|
90
90
|
shell: false,
|
|
91
91
|
cwd: opts.cwd,
|
|
92
92
|
detached: !(0, paths_1.isWindowsNative)(),
|
|
93
|
+
...(input.environment ? { env: { ...process.env, ...input.environment } } : {}),
|
|
93
94
|
// stdin closed: a sensor must never block waiting for input, and the
|
|
94
95
|
// EOF also tells watch-mode-capable tools (vitest, jest) to run once.
|
|
95
96
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -144,14 +145,72 @@ function collectSpawn(input, opts) {
|
|
|
144
145
|
});
|
|
145
146
|
}
|
|
146
147
|
function validateStructuredCommand(command) {
|
|
147
|
-
if (!command || typeof command !== 'object' || typeof command.executable !== 'string' ||
|
|
148
|
+
if (!command || typeof command !== 'object' || typeof command.executable !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(command.executable)) {
|
|
148
149
|
throw new Error('structured command executable must be a safe executable name');
|
|
149
150
|
}
|
|
150
|
-
|
|
151
|
-
|
|
151
|
+
const normalizedExecutable = command.executable.toLowerCase().replace(/\.exe$/, '');
|
|
152
|
+
if (new Set(['sh', 'bash', 'cmd', 'powershell']).has(normalizedExecutable)) {
|
|
153
|
+
throw new Error('structured command executable must not be a shell');
|
|
154
|
+
}
|
|
155
|
+
if (!Array.isArray(command.args) || command.args.length === 0) {
|
|
156
|
+
throw new Error('structured command args must be a nonempty array');
|
|
157
|
+
}
|
|
158
|
+
for (const [index, arg] of command.args.entries()) {
|
|
159
|
+
if (typeof arg !== 'string' || arg.trim() === '' || /[\0\r\n]/.test(arg)) {
|
|
160
|
+
throw new Error(`structured command args[${index}] must be a nonempty single-line string without NUL`);
|
|
161
|
+
}
|
|
162
|
+
if (arg.includes('{files}') && arg !== '{files}') {
|
|
163
|
+
throw new Error(`structured command args[${index}] must not embed {files}`);
|
|
164
|
+
}
|
|
152
165
|
}
|
|
153
166
|
if (!['node-modules-bin', 'python-environment', 'path'].includes(command.resolution))
|
|
154
167
|
throw new Error('structured command resolution is unsupported');
|
|
168
|
+
if (command.pythonEnvironmentRoot !== undefined && (command.resolution !== 'python-environment' || (command.pythonEnvironmentRoot !== '.venv' && command.pythonEnvironmentRoot !== 'venv'))) {
|
|
169
|
+
throw new Error('structured command pythonEnvironmentRoot must name the selected .venv or venv Python environment');
|
|
170
|
+
}
|
|
171
|
+
if (command.resolution === 'python-environment' && command.pythonEnvironmentRoot === undefined)
|
|
172
|
+
throw new Error('python environment command requires a discovery-bound contained local environment root');
|
|
173
|
+
const packageManagers = new Set(['npm', 'pnpm', 'yarn', 'bun']);
|
|
174
|
+
const normalizedPackageManager = typeof command.packageManager === 'string' ? command.packageManager.toLowerCase().replace(/\.exe$/, '') : undefined;
|
|
175
|
+
if (packageManagers.has(normalizedExecutable) && normalizedPackageManager !== normalizedExecutable)
|
|
176
|
+
throw new Error('structured command packageManager must explicitly match its executable');
|
|
177
|
+
if (normalizedPackageManager !== undefined && !packageManagers.has(normalizedPackageManager))
|
|
178
|
+
throw new Error('structured command packageManager is unsupported');
|
|
179
|
+
if (normalizedPackageManager !== undefined && normalizedPackageManager !== normalizedExecutable)
|
|
180
|
+
throw new Error('structured command packageManager must explicitly match its executable');
|
|
181
|
+
if (command.environment !== undefined) {
|
|
182
|
+
const environment = command.environment;
|
|
183
|
+
if (!environment || typeof environment !== 'object' || Array.isArray(environment) || Object.keys(environment).length !== 1 || !Object.prototype.hasOwnProperty.call(environment, 'ESLINT_USE_FLAT_CONFIG')) {
|
|
184
|
+
throw new Error('structured command environment must be the exact allowlisted ESLINT_USE_FLAT_CONFIG=true or false mapping');
|
|
185
|
+
}
|
|
186
|
+
const flatConfig = environment.ESLINT_USE_FLAT_CONFIG;
|
|
187
|
+
if (flatConfig !== 'true' && flatConfig !== 'false')
|
|
188
|
+
throw new Error('structured command environment must be the exact allowlisted ESLINT_USE_FLAT_CONFIG=true or false mapping');
|
|
189
|
+
}
|
|
190
|
+
const fileArguments = command.args.filter(arg => arg === '{files}').length;
|
|
191
|
+
if (command.fileInput === undefined) {
|
|
192
|
+
if (fileArguments !== 0)
|
|
193
|
+
throw new Error('structured command {files} argument requires fileInput');
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const fileInput = command.fileInput;
|
|
197
|
+
if (!fileInput || typeof fileInput !== 'object' || Array.isArray(fileInput) || Object.keys(fileInput).length !== 2 || !Object.prototype.hasOwnProperty.call(fileInput, 'placeholder') || !Object.prototype.hasOwnProperty.call(fileInput, 'extensions')) {
|
|
198
|
+
throw new Error('structured command fileInput must contain only placeholder and extensions');
|
|
199
|
+
}
|
|
200
|
+
const { placeholder, extensions } = fileInput;
|
|
201
|
+
if (placeholder !== '{files}')
|
|
202
|
+
throw new Error('structured command fileInput.placeholder must be {files}');
|
|
203
|
+
if (!Array.isArray(extensions) || extensions.length === 0 || extensions.some(extension => typeof extension !== 'string' || extension.trim() === '' || /[\0\r\n]/.test(extension) || !/^\.[A-Za-z0-9]+$/.test(extension))) {
|
|
204
|
+
throw new Error('structured command fileInput.extensions must be a nonempty array of extensions');
|
|
205
|
+
}
|
|
206
|
+
if (fileArguments !== 1)
|
|
207
|
+
throw new Error('structured command fileInput requires exactly one {files} argument');
|
|
208
|
+
}
|
|
209
|
+
function safeWindowsExecutableExtensions() {
|
|
210
|
+
return (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD')
|
|
211
|
+
.split(';')
|
|
212
|
+
.map(ext => ext.toLowerCase())
|
|
213
|
+
.filter(ext => ext === '.exe' || ext === '.com');
|
|
155
214
|
}
|
|
156
215
|
function regularFile(candidate) {
|
|
157
216
|
try {
|
|
@@ -162,6 +221,25 @@ function regularFile(candidate) {
|
|
|
162
221
|
return false;
|
|
163
222
|
}
|
|
164
223
|
}
|
|
224
|
+
/** Resolve a Python environment executable only when every selected path
|
|
225
|
+
* component is a non-symbolic-link local entry. A leaf-only lstat follows a
|
|
226
|
+
* linked .venv/venv ancestor and can otherwise execute outside the project. */
|
|
227
|
+
function localPythonEnvironmentExecutable(cwd, environmentRoot, executable) {
|
|
228
|
+
const parts = [environmentRoot, (0, paths_1.isWindowsNative)() ? 'Scripts' : 'bin', executable];
|
|
229
|
+
let candidate = cwd;
|
|
230
|
+
try {
|
|
231
|
+
for (const part of parts) {
|
|
232
|
+
candidate = path_1.default.join(candidate, part);
|
|
233
|
+
const stat = fs_1.default.lstatSync(candidate);
|
|
234
|
+
if (stat.isSymbolicLink())
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
return fs_1.default.lstatSync(candidate).isFile() ? candidate : null;
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
165
243
|
function containedPath(root, candidate) {
|
|
166
244
|
const relative = path_1.default.relative(root, candidate);
|
|
167
245
|
return relative !== '' && !relative.startsWith(`..${path_1.default.sep}`) && relative !== '..' && !path_1.default.isAbsolute(relative);
|
|
@@ -202,7 +280,9 @@ function resolveStructuredExecutable(command, cwd) {
|
|
|
202
280
|
return local;
|
|
203
281
|
throw new Error('node_modules executable is not a contained local file');
|
|
204
282
|
}
|
|
205
|
-
const extensions = (
|
|
283
|
+
const extensions = safeWindowsExecutableExtensions();
|
|
284
|
+
if (extensions.length === 0)
|
|
285
|
+
throw new Error('PATHEXT contains no safe Windows executable extension for structured commands');
|
|
206
286
|
const candidates = [path_1.default.join(bin, command.executable), ...extensions.map(extension => path_1.default.join(bin, command.executable + extension))];
|
|
207
287
|
for (const candidate of candidates) {
|
|
208
288
|
const lower = candidate.toLowerCase();
|
|
@@ -218,8 +298,24 @@ function resolveStructuredExecutable(command, cwd) {
|
|
|
218
298
|
if (command.resolution === 'python-environment') {
|
|
219
299
|
if (path_1.default.isAbsolute(command.executable))
|
|
220
300
|
throw new Error('python environment executable must be a contained local name');
|
|
221
|
-
|
|
222
|
-
|
|
301
|
+
if (!(0, paths_1.isWindowsNative)()) {
|
|
302
|
+
const local = localPythonEnvironmentExecutable(cwd, command.pythonEnvironmentRoot, command.executable);
|
|
303
|
+
if (local)
|
|
304
|
+
return local;
|
|
305
|
+
throw new Error('python environment executable is not a contained local regular file');
|
|
306
|
+
}
|
|
307
|
+
const extensions = safeWindowsExecutableExtensions();
|
|
308
|
+
if (extensions.length === 0)
|
|
309
|
+
throw new Error('PATHEXT contains no safe Windows executable extension for structured commands');
|
|
310
|
+
for (const executable of [command.executable, ...extensions.map(extension => command.executable + extension)]) {
|
|
311
|
+
const lower = executable.toLowerCase();
|
|
312
|
+
if (lower.endsWith('.cmd') || lower.endsWith('.bat'))
|
|
313
|
+
throw new Error('structured commands cannot execute Windows command wrappers');
|
|
314
|
+
const local = localPythonEnvironmentExecutable(cwd, command.pythonEnvironmentRoot, executable);
|
|
315
|
+
if (local && extensions.some(extension => lower.endsWith(extension)))
|
|
316
|
+
return local;
|
|
317
|
+
}
|
|
318
|
+
throw new Error('python environment executable is not a contained local regular file');
|
|
223
319
|
}
|
|
224
320
|
else if (path_1.default.isAbsolute(command.executable))
|
|
225
321
|
candidates.push(command.executable);
|
|
@@ -231,11 +327,11 @@ function resolveStructuredExecutable(command, cwd) {
|
|
|
231
327
|
const found = candidates.find(regularFile);
|
|
232
328
|
if (found)
|
|
233
329
|
return found;
|
|
234
|
-
if (command.resolution === 'python-environment')
|
|
235
|
-
throw new Error('python environment executable is not a contained local regular file');
|
|
236
330
|
return command.executable;
|
|
237
331
|
}
|
|
238
|
-
const extensions = (
|
|
332
|
+
const extensions = safeWindowsExecutableExtensions();
|
|
333
|
+
if (extensions.length === 0)
|
|
334
|
+
throw new Error('PATHEXT contains no safe Windows executable extension for structured commands');
|
|
239
335
|
for (const candidate of candidates) {
|
|
240
336
|
const lower = candidate.toLowerCase();
|
|
241
337
|
if (lower.endsWith('.cmd') || lower.endsWith('.bat'))
|
|
@@ -246,14 +342,12 @@ function resolveStructuredExecutable(command, cwd) {
|
|
|
246
342
|
if (regularFile(candidate + extension))
|
|
247
343
|
return candidate + extension;
|
|
248
344
|
}
|
|
249
|
-
if (command.resolution === 'python-environment')
|
|
250
|
-
throw new Error('python environment executable is not a contained local regular file');
|
|
251
345
|
return command.executable;
|
|
252
346
|
}
|
|
253
347
|
/** Execute a v2 command as an executable plus literal argv; it never starts a shell. */
|
|
254
348
|
function runStructuredCommand(command, opts) {
|
|
255
349
|
validateStructuredCommand(command);
|
|
256
|
-
return collectSpawn({ executable: resolveStructuredExecutable(command, opts.cwd), args: command.args, shell: false }, opts);
|
|
350
|
+
return collectSpawn({ executable: resolveStructuredExecutable(command, opts.cwd), args: command.args, shell: false, environment: command.environment }, opts);
|
|
257
351
|
}
|
|
258
352
|
/** Legacy sensor strings intentionally retain their documented shell semantics. */
|
|
259
353
|
function runCommand(cmd, opts) {
|
|
@@ -253,9 +253,11 @@ async function initSensors(opts = {}) {
|
|
|
253
253
|
if (opts.registryRoot) {
|
|
254
254
|
const resolvedV2 = readV2Pack(resolvedPack, opts.registryRoot);
|
|
255
255
|
if (resolvedV2) {
|
|
256
|
-
const
|
|
256
|
+
const packSelection = opts.pack ? 'explicit' : undefined;
|
|
257
|
+
const live = await (0, live_1.resolveParsedPackCompatibility)(cwd, resolvedV2.pack, { packSelection });
|
|
258
|
+
const compatibility = live.sensors;
|
|
257
259
|
const sensors = {};
|
|
258
|
-
for (const [name, sensor] of Object.entries(
|
|
260
|
+
for (const [name, sensor] of Object.entries(live.pack.sensors)) {
|
|
259
261
|
const resolved = compatibility[name];
|
|
260
262
|
const variant = resolved.variantId === null ? null : sensor.variants.find(candidate => candidate.id === resolved.variantId) ?? null;
|
|
261
263
|
// Unresolved states do not receive an arbitrary command. They remain
|
|
@@ -273,6 +275,7 @@ async function initSensors(opts = {}) {
|
|
|
273
275
|
variantId: variant.id,
|
|
274
276
|
command: variant.command,
|
|
275
277
|
assets: variant.assets,
|
|
278
|
+
...(variant.policyRef ? { policyRef: variant.policyRef } : {}),
|
|
276
279
|
initializedCompatibility: migratedOverride
|
|
277
280
|
? { ...resolved, state: 'compatible-unverified', reason: 'legacy custom command requires explicit v2 migration' }
|
|
278
281
|
: resolved,
|
|
@@ -281,7 +284,7 @@ async function initSensors(opts = {}) {
|
|
|
281
284
|
}
|
|
282
285
|
const materialized = (0, materialize_1.materializeResolvedSensors)({
|
|
283
286
|
projectRoot: cwd, packRoot: resolvedV2.packRoot,
|
|
284
|
-
pack: resolvedPack, registryRoot: opts.registryRoot, sensors, configure,
|
|
287
|
+
pack: resolvedPack, ...(packSelection ? { packSelection } : {}), registryRoot: opts.registryRoot, sensors, configure,
|
|
285
288
|
});
|
|
286
289
|
return { detection, ...materialized,
|
|
287
290
|
compatibility, ...(unavailablePack ? { unavailablePack } : {}) };
|
|
@@ -73,7 +73,7 @@ async function resolveLiveV2(cwd, manifest) {
|
|
|
73
73
|
if (manifest.kind !== 'v2')
|
|
74
74
|
return null;
|
|
75
75
|
try {
|
|
76
|
-
return await (0, live_1.resolveLiveCompatibility)(cwd, manifest.pack.pack, manifest.pack.registryRoot);
|
|
76
|
+
return await (0, live_1.resolveLiveCompatibility)(cwd, manifest.pack.pack, manifest.pack.registryRoot, { packSelection: manifest.pack.packSelection });
|
|
77
77
|
}
|
|
78
78
|
catch {
|
|
79
79
|
return null;
|
|
@@ -68,7 +68,7 @@ async function computeSensorStatus(cwd = process.cwd()) {
|
|
|
68
68
|
const checks = {};
|
|
69
69
|
let live;
|
|
70
70
|
try {
|
|
71
|
-
live = await (0, live_1.resolveLiveCompatibility)(cwd, parsed.pack.pack, parsed.pack.registryRoot);
|
|
71
|
+
live = await (0, live_1.resolveLiveCompatibility)(cwd, parsed.pack.pack, parsed.pack.registryRoot, { packSelection: parsed.pack.packSelection });
|
|
72
72
|
}
|
|
73
73
|
catch (error) {
|
|
74
74
|
const detail = `compatibility revalidation failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
const contract_1 = require("../../../../src/commands/sensors/compatibility/contract");
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
4
10
|
const coverage = {
|
|
5
11
|
schemaVersion: 1,
|
|
6
12
|
classes: {
|
|
@@ -37,9 +43,92 @@ function validPack() {
|
|
|
37
43
|
};
|
|
38
44
|
}
|
|
39
45
|
describe('sensor pack v2 contract', () => {
|
|
46
|
+
it('derives Semgrep compatibility from a contained shared policy reference', () => {
|
|
47
|
+
const sensorPacks = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-semgrep-policy-'));
|
|
48
|
+
const packDir = path_1.default.join(sensorPacks, 'python');
|
|
49
|
+
try {
|
|
50
|
+
fs_1.default.mkdirSync(path_1.default.join(sensorPacks, 'shared'), { recursive: true });
|
|
51
|
+
fs_1.default.mkdirSync(packDir);
|
|
52
|
+
fs_1.default.writeFileSync(path_1.default.join(sensorPacks, 'shared', 'semgrep-policy.json'), JSON.stringify({
|
|
53
|
+
tool: 'semgrep', toolRange: '>=1.0.0', runtime: 'python', runtimeRange: '>=3.9.0', probe: 'semgrep-validate',
|
|
54
|
+
}));
|
|
55
|
+
const pack = validPack();
|
|
56
|
+
pack.sensors.lint.variants[0] = {
|
|
57
|
+
id: 'semgrep-python', priority: 10, certifiedRange: '>=1.0.0', policyRef: 'shared/semgrep-policy.json',
|
|
58
|
+
command: { executable: 'semgrep', resolution: 'path', args: ['--config', '.semgrep.awm.yml', '--json', '.'] },
|
|
59
|
+
assets: ['.semgrep.awm.yml'], formatter: 'semgrep',
|
|
60
|
+
};
|
|
61
|
+
const parsed = (0, contract_1.parseSensorPack)(pack, path_1.default.join(packDir, 'pack.json'));
|
|
62
|
+
expect(parsed).toMatchObject({ kind: 'v2', pack: { sensors: { lint: { variants: [{
|
|
63
|
+
policyRef: 'shared/semgrep-policy.json', requirements: { tool: 'semgrep', runtime: 'python' }, probe: { kind: 'semgrep-validate' },
|
|
64
|
+
}] } } } });
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
fs_1.default.rmSync(sensorPacks, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
it('fails closed when a policy reference is not the AWM-owned shared Semgrep policy', () => {
|
|
71
|
+
const pack = validPack();
|
|
72
|
+
pack.sensors.lint.variants[0] = {
|
|
73
|
+
id: 'semgrep-python', priority: 10, certifiedRange: '>=1.0.0', policyRef: '../secret.json',
|
|
74
|
+
command: { executable: 'semgrep', resolution: 'path', args: ['--config', '.semgrep.awm.yml', '--json', '.'] },
|
|
75
|
+
assets: ['.semgrep.awm.yml'], formatter: 'semgrep',
|
|
76
|
+
};
|
|
77
|
+
expect(() => (0, contract_1.parseSensorPack)(pack, '/tmp/sensor-packs/python/pack.json')).toThrow('policyRef');
|
|
78
|
+
});
|
|
40
79
|
it('parses a valid versioned pack', () => {
|
|
41
80
|
expect((0, contract_1.parseSensorPack)(validPack(), 'pack.json')).toMatchObject({ kind: 'v2', pack: validPack() });
|
|
42
81
|
});
|
|
82
|
+
it('accepts an opt-in hardening asset while variants may require no assets', () => {
|
|
83
|
+
const pack = {
|
|
84
|
+
...validPack(),
|
|
85
|
+
hardening: { 'typescript-strict': { assets: ['tsconfig.awm.json'] } },
|
|
86
|
+
sensors: {
|
|
87
|
+
lint: {
|
|
88
|
+
...validPack().sensors.lint,
|
|
89
|
+
variants: [{ ...validPack().sensors.lint.variants[0], assets: [] }],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
expect((0, contract_1.parseSensorPack)(pack, 'pack.json')).toMatchObject({
|
|
94
|
+
kind: 'v2',
|
|
95
|
+
pack: { hardening: { 'typescript-strict': { assets: ['tsconfig.awm.json'] } }, sensors: { lint: { variants: [{ assets: [] }] } } },
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
it.each(['true', 'false'])('accepts the exact ESLINT_USE_FLAT_CONFIG=%s environment mapping', flatConfig => {
|
|
99
|
+
const command = {
|
|
100
|
+
executable: 'npm',
|
|
101
|
+
resolution: 'path',
|
|
102
|
+
args: ['run', 'lint'],
|
|
103
|
+
packageManager: 'npm',
|
|
104
|
+
environment: { ESLINT_USE_FLAT_CONFIG: flatConfig },
|
|
105
|
+
};
|
|
106
|
+
expect((0, contract_1.parseSensorPack)({ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...validPack().sensors.lint.variants[0], command }] } } }, 'pack.json'))
|
|
107
|
+
.toMatchObject({ kind: 'v2', pack: { sensors: { lint: { variants: [{ command }] } } } });
|
|
108
|
+
});
|
|
109
|
+
it.each([
|
|
110
|
+
{ ESLINT_USE_FLAT_CONFIG: 'yes' },
|
|
111
|
+
{ ESLINT_USE_FLAT_CONFIG: 'true', OTHER: 'value' },
|
|
112
|
+
])('rejects an unknown or expanded ESLint environment mapping', environment => {
|
|
113
|
+
const command = { executable: 'npm', resolution: 'path', args: ['run', 'lint'], packageManager: 'npm', environment };
|
|
114
|
+
expect(() => (0, contract_1.parseSensorPack)({ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...validPack().sensors.lint.variants[0], command }] } } }, 'pack.json'))
|
|
115
|
+
.toThrow('command.environment');
|
|
116
|
+
});
|
|
117
|
+
it('normalizes package-manager executable spelling before enforcing its explicit selection', () => {
|
|
118
|
+
const variant = validPack().sensors.lint.variants[0];
|
|
119
|
+
const withExecutable = (executable, packageManager) => ({
|
|
120
|
+
...validPack(),
|
|
121
|
+
sensors: { lint: { ...validPack().sensors.lint, variants: [{
|
|
122
|
+
...variant,
|
|
123
|
+
command: { executable, resolution: 'path', args: ['run', 'lint'], ...(packageManager === undefined ? {} : { packageManager }) },
|
|
124
|
+
}] } },
|
|
125
|
+
});
|
|
126
|
+
expect(() => (0, contract_1.parseSensorPack)(withExecutable('NPM'), 'pack.json')).toThrow('packageManager');
|
|
127
|
+
expect((0, contract_1.parseSensorPack)(withExecutable('npm.exe', 'NPM'), 'pack.json')).toMatchObject({
|
|
128
|
+
kind: 'v2', pack: { sensors: { lint: { variants: [{ command: { executable: 'npm.exe', packageManager: 'npm' } }] } } },
|
|
129
|
+
});
|
|
130
|
+
expect(() => (0, contract_1.parseSensorPack)(withExecutable('NPM', 'pnpm'), 'pack.json')).toThrow('match executable');
|
|
131
|
+
});
|
|
43
132
|
it('keeps an unversioned pack on the legacy compatibility path', () => {
|
|
44
133
|
const legacy = { name: 'legacy', sensors: {} };
|
|
45
134
|
expect((0, contract_1.parseSensorPack)(legacy, 'pack.json')).toMatchObject({ kind: 'legacy', pack: { ...legacy, compatibility: { state: 'compatible-unverified' } } });
|
|
@@ -90,8 +179,14 @@ describe('sensor pack v2 contract', () => {
|
|
|
90
179
|
[{ ...validPack(), sensors: { lint: { variants: [validPack().sensors.lint.variants[0], { ...validPack().sensors.lint.variants[0], id: 'eslint-9-next', certifiedRange: '>=9.1.0 <10.0.0' }] } } }, 'overlap'],
|
|
91
180
|
[{ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...validPack().sensors.lint.variants[0], assets: ['C:/secret'] }] } } }, 'asset'],
|
|
92
181
|
[{ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...validPack().sensors.lint.variants[0], command: { executable: 'cmd.exe', resolution: 'path', args: ['x'] } }] } } }, 'executable'],
|
|
182
|
+
[{ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...validPack().sensors.lint.variants[0], command: { executable: 'npm', resolution: 'path', args: ['run', 'lint'] } }] } } }, 'packageManager'],
|
|
183
|
+
[{ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...validPack().sensors.lint.variants[0], command: { executable: 'npm', resolution: 'path', args: ['run', 'lint'], packageManager: 'pnpm' } }] } } }, 'match executable'],
|
|
184
|
+
[{ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...validPack().sensors.lint.variants[0], command: { ...validPack().sensors.lint.variants[0].command, environment: { NODE_OPTIONS: '--require unsafe' } } }] } } }, 'environment'],
|
|
93
185
|
[{ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...validPack().sensors.lint.variants[0], command: { ...validPack().sensors.lint.variants[0].command, args: ['prefix{files}'] } }] } } }, 'embed'],
|
|
94
186
|
[{ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...validPack().sensors.lint.variants[0], formatter: 'bad\nformat' }] } } }, 'formatter'],
|
|
187
|
+
[{ ...validPack(), hardening: { 'typescript-strict': { assets: ['../tsconfig.awm.json'] } } }, 'asset'],
|
|
188
|
+
[{ ...validPack(), hardening: { 'typescript-strict': { assets: [] } } }, 'hardening'],
|
|
189
|
+
[{ ...validPack(), hardening: { 'typescript-strict': { assets: ['tsconfig.awm.json'], command: {} } } }, 'unknown field'],
|
|
95
190
|
])('rejects malformed pack %j', (input, message) => {
|
|
96
191
|
expect(() => (0, contract_1.parseSensorPack)(input, 'pack.json')).toThrow(message);
|
|
97
192
|
});
|
|
@@ -0,0 +1,124 @@
|
|
|
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 live_1 = require("../../../../src/commands/sensors/compatibility/live");
|
|
10
|
+
const exec_1 = require("../../../../src/commands/sensors/exec");
|
|
11
|
+
const python_venv_fixture_1 = require("./python-venv-fixture");
|
|
12
|
+
const itPosix = process.platform !== 'win32' ? it : it.skip;
|
|
13
|
+
describe('resolveParsedPackCompatibility — contained Python commands', () => {
|
|
14
|
+
test.each([
|
|
15
|
+
['linux', ['.venv', 'bin', 'semgrep']],
|
|
16
|
+
['win32', ['.venv', 'Scripts', 'semgrep.exe']],
|
|
17
|
+
])('creates a local Semgrep executable in the %s virtual-environment layout', (targetPlatform, executableParts) => {
|
|
18
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-semgrep-venv-layout-'));
|
|
19
|
+
try {
|
|
20
|
+
(0, python_venv_fixture_1.createSemgrepVenvFixture)(root, targetPlatform);
|
|
21
|
+
expect(fs_1.default.lstatSync(path_1.default.join(root, ...executableParts)).isFile()).toBe(true);
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
it('resolves the Windows virtual-environment executable from Scripts instead of PATH', async () => {
|
|
28
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-semgrep-windows-resolution-'));
|
|
29
|
+
const global = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-semgrep-windows-global-'));
|
|
30
|
+
const originalPlatform = process.platform;
|
|
31
|
+
const savedPath = process.env.PATH;
|
|
32
|
+
const savedPathExt = process.env.PATHEXT;
|
|
33
|
+
try {
|
|
34
|
+
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
35
|
+
process.env.PATH = global;
|
|
36
|
+
process.env.PATHEXT = '.EXE';
|
|
37
|
+
(0, python_venv_fixture_1.createSemgrepVenvFixture)(root, 'win32');
|
|
38
|
+
fs_1.default.writeFileSync(path_1.default.join(global, 'semgrep.exe'), 'must-not-run');
|
|
39
|
+
await expect((0, exec_1.runStructuredCommand)({
|
|
40
|
+
executable: 'semgrep', resolution: 'python-environment', pythonEnvironmentRoot: '.venv', args: ['--version'],
|
|
41
|
+
}, { cwd: root, timeout: 5_000 })).resolves.toMatchObject({
|
|
42
|
+
code: 0,
|
|
43
|
+
stdout: expect.stringContaining(process.version),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true });
|
|
48
|
+
if (savedPath === undefined)
|
|
49
|
+
delete process.env.PATH;
|
|
50
|
+
else
|
|
51
|
+
process.env.PATH = savedPath;
|
|
52
|
+
if (savedPathExt === undefined)
|
|
53
|
+
delete process.env.PATHEXT;
|
|
54
|
+
else
|
|
55
|
+
process.env.PATHEXT = savedPathExt;
|
|
56
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
57
|
+
fs_1.default.rmSync(global, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
itPosix('binds Semgrep discovery, validation probe, and execution to the same local virtual-environment executable', async () => {
|
|
61
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-semgrep-contained-'));
|
|
62
|
+
const global = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-semgrep-global-'));
|
|
63
|
+
const savedPath = process.env.PATH;
|
|
64
|
+
try {
|
|
65
|
+
(0, python_venv_fixture_1.createSemgrepVenvFixture)(root);
|
|
66
|
+
fs_1.default.writeFileSync(path_1.default.join(root, '.venv', 'bin', 'semgrep'), '#!/bin/sh\nif [ "$1" = "--validate" ]; then echo local-semgrep; exit 0; fi\nexit 1\n', { mode: 0o755 });
|
|
67
|
+
fs_1.default.writeFileSync(path_1.default.join(global, 'semgrep'), '#!/bin/sh\necho global-semgrep\n', { mode: 0o755 });
|
|
68
|
+
process.env.PATH = global;
|
|
69
|
+
const pack = {
|
|
70
|
+
schemaVersion: 2,
|
|
71
|
+
name: 'python',
|
|
72
|
+
description: 'test',
|
|
73
|
+
detects: ['pyproject.toml'],
|
|
74
|
+
coverage: {},
|
|
75
|
+
sensors: {
|
|
76
|
+
security: {
|
|
77
|
+
applicability: { allFiles: ['pyproject.toml'] },
|
|
78
|
+
variants: [{
|
|
79
|
+
id: 'semgrep-1', priority: 1, certifiedRange: '>=1 <2',
|
|
80
|
+
requirements: { tool: 'semgrep', toolRange: '>=1 <2', runtime: 'python', runtimeRange: '>=3.12 <4' },
|
|
81
|
+
assets: [], formatter: 'semgrep', probe: { kind: 'semgrep-validate' },
|
|
82
|
+
command: { executable: 'semgrep', resolution: 'path', args: ['--validate'] },
|
|
83
|
+
}],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'pyproject.toml'), '[project]\nname = "sample"\n');
|
|
88
|
+
const live = await (0, live_1.resolveParsedPackCompatibility)(root, pack);
|
|
89
|
+
const command = live.pack.sensors.security.variants[0].command;
|
|
90
|
+
expect(live.sensors.security).toMatchObject({ state: 'certified', toolVersion: '1.91.0' });
|
|
91
|
+
expect(command).toMatchObject({ executable: 'semgrep', resolution: 'python-environment' });
|
|
92
|
+
await expect((0, exec_1.runStructuredCommand)(command, { cwd: root, timeout: 5_000 })).resolves.toMatchObject({ code: 0, stdout: 'local-semgrep\n' });
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
process.env.PATH = savedPath;
|
|
96
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
97
|
+
fs_1.default.rmSync(global, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
it('never falls back from the discovered .venv to a sibling venv executable', async () => {
|
|
101
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-semgrep-environment-identity-'));
|
|
102
|
+
try {
|
|
103
|
+
(0, python_venv_fixture_1.createSemgrepVenvFixture)(root, process.platform, '.venv', { executable: false });
|
|
104
|
+
(0, python_venv_fixture_1.createSemgrepVenvFixture)(root, process.platform, 'venv');
|
|
105
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'pyproject.toml'), '[project]\nname = "sample"\n');
|
|
106
|
+
const pack = {
|
|
107
|
+
schemaVersion: 2, name: 'python', description: 'test', detects: ['pyproject.toml'], coverage: {},
|
|
108
|
+
sensors: { security: { applicability: { allFiles: ['pyproject.toml'] }, variants: [{
|
|
109
|
+
id: 'semgrep-1', priority: 1, certifiedRange: '>=1 <2',
|
|
110
|
+
requirements: { tool: 'semgrep', toolRange: '>=1 <2', runtime: 'python', runtimeRange: '>=3.12 <4' },
|
|
111
|
+
assets: [], formatter: 'semgrep', probe: { kind: 'semgrep-validate' },
|
|
112
|
+
command: { executable: 'semgrep', resolution: 'path', args: ['--validate'] },
|
|
113
|
+
}] } },
|
|
114
|
+
};
|
|
115
|
+
const live = await (0, live_1.resolveParsedPackCompatibility)(root, pack);
|
|
116
|
+
expect(live.sensors.security).toMatchObject({ state: 'unverifiable', reason: 'probe-inconclusive', toolVersion: '1.91.0' });
|
|
117
|
+
expect(() => (0, exec_1.runStructuredCommand)(live.pack.sensors.security.variants[0].command, { cwd: root, timeout: 5_000 }))
|
|
118
|
+
.toThrow('python environment executable is not a contained local regular file');
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -35,6 +35,15 @@ describe('sensor manifest contract', () => {
|
|
|
35
35
|
expect((0, manifest_1.parseSensorManifest)(manifest, 'sensors.json')).toMatchObject({ kind: 'v2', pack: manifest });
|
|
36
36
|
expect(JSON.parse((0, manifest_1.serializeManifestV2)(manifest))).toEqual(manifest);
|
|
37
37
|
});
|
|
38
|
+
it('persists only an explicit v2 pack selection as applicability provenance', () => {
|
|
39
|
+
const manifest = {
|
|
40
|
+
schemaVersion: 2, pack: 'generic', packSelection: 'explicit',
|
|
41
|
+
sensors: {},
|
|
42
|
+
};
|
|
43
|
+
expect((0, manifest_1.parseSensorManifest)(manifest, 'sensors.json')).toMatchObject({ kind: 'v2', pack: { packSelection: 'explicit' } });
|
|
44
|
+
expect(JSON.parse((0, manifest_1.serializeManifestV2)(manifest))).toEqual(manifest);
|
|
45
|
+
expect(() => (0, manifest_1.parseSensorManifest)({ ...manifest, packSelection: 'detected' }, 'sensors.json')).toThrow('packSelection');
|
|
46
|
+
});
|
|
38
47
|
it('accepts optional contained assets and rejects traversal', () => {
|
|
39
48
|
const sensor = { enabled: true, variantId: 'eslint-9', assets: ['eslint.config.awm.mjs'], command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] }, initializedCompatibility: { state: 'certified', reason: 'ok', variantId: 'eslint-9', toolVersion: '9.0.0', runtimeVersion: '24.0.0', certifiedRange: '>=9 <10', evidence: [] } };
|
|
40
49
|
expect((0, manifest_1.parseSensorManifest)({ schemaVersion: 2, pack: 'js-ts', sensors: { lint: sensor } }, 'sensors.json')).toMatchObject({ kind: 'v2' });
|
|
@@ -38,6 +38,25 @@ describe('materializeResolvedSensors', () => {
|
|
|
38
38
|
expect(result.preserved).toEqual(['eslint.config.awm.mjs']);
|
|
39
39
|
expect(fs_1.default.readFileSync(path_1.default.join(projectRoot, 'eslint.config.awm.mjs'), 'utf8')).toBe('owner content');
|
|
40
40
|
});
|
|
41
|
+
it('revalidates a shared Semgrep policy reference without materializing the policy itself', () => {
|
|
42
|
+
const sensorPacks = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-materialize-policy-'));
|
|
43
|
+
const semgrepPack = path_1.default.join(sensorPacks, 'python');
|
|
44
|
+
try {
|
|
45
|
+
fs_1.default.mkdirSync(path_1.default.join(sensorPacks, 'shared'), { recursive: true });
|
|
46
|
+
fs_1.default.mkdirSync(semgrepPack);
|
|
47
|
+
fs_1.default.writeFileSync(path_1.default.join(semgrepPack, 'pack.json'), '{}');
|
|
48
|
+
fs_1.default.writeFileSync(path_1.default.join(semgrepPack, '.semgrep.awm.yml'), 'rules: []\n');
|
|
49
|
+
fs_1.default.writeFileSync(path_1.default.join(sensorPacks, 'shared', 'semgrep-policy.json'), JSON.stringify({ tool: 'semgrep', toolRange: '>=1.0.0', runtime: 'python', runtimeRange: '>=3.9.0', probe: 'semgrep-validate' }));
|
|
50
|
+
const result = (0, materialize_1.materializeResolvedSensors)({ projectRoot, packRoot: semgrepPack, pack: 'python', sensors: {
|
|
51
|
+
security: { enabled: true, variantId: 'semgrep-python', command: { executable: 'semgrep', resolution: 'path', args: ['--config', '.semgrep.awm.yml', '--json', '.'] }, assets: ['.semgrep.awm.yml'], policyRef: 'shared/semgrep-policy.json', initializedCompatibility: { ...evidence, variantId: 'semgrep-python' } },
|
|
52
|
+
} });
|
|
53
|
+
expect(result.configured).toEqual(['.semgrep.awm.yml']);
|
|
54
|
+
expect(fs_1.default.existsSync(path_1.default.join(projectRoot, 'shared', 'semgrep-policy.json'))).toBe(false);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
fs_1.default.rmSync(sensorPacks, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
41
60
|
it('reports previous AWM assets as orphaned and never deletes them', () => {
|
|
42
61
|
fs_1.default.mkdirSync(path_1.default.join(projectRoot, '.awm'));
|
|
43
62
|
fs_1.default.writeFileSync(path_1.default.join(projectRoot, '.awm', 'sensors.json'), JSON.stringify({ schemaVersion: 2, pack: 'js-ts', sensors: {
|
|
@@ -24,4 +24,14 @@ describe('runCompatibilityProbe', () => {
|
|
|
24
24
|
await (0, probe_1.runCompatibilityProbe)({ kind: 'semgrep-validate' }, evidence, fakeExecutor);
|
|
25
25
|
expect(fakeExecutor).toHaveBeenCalledWith(expect.objectContaining({ executable: 'semgrep', resolution: 'python-environment' }), expect.any(Object));
|
|
26
26
|
});
|
|
27
|
+
it('keeps Windows Semgrep validation on the discovered .venv executable', async () => {
|
|
28
|
+
await (0, probe_1.runCompatibilityProbe)({ kind: 'semgrep-validate' }, {
|
|
29
|
+
cwd: process.cwd(), toolExecutable: 'semgrep', toolResolution: 'python-environment', pythonEnvironmentRoot: '.venv',
|
|
30
|
+
}, fakeExecutor);
|
|
31
|
+
expect(fakeExecutor).toHaveBeenCalledWith({ executable: 'semgrep', resolution: 'python-environment', pythonEnvironmentRoot: '.venv', args: ['--validate'] }, expect.objectContaining({ cwd: process.cwd() }));
|
|
32
|
+
});
|
|
33
|
+
it.each(['mypy', 'ruff', 'pytest'])('probes a %s variant through the contained Python environment', async (toolExecutable) => {
|
|
34
|
+
await (0, probe_1.runCompatibilityProbe)({ kind: 'version' }, { ...evidence, toolExecutable, toolResolution: 'python-environment' }, fakeExecutor);
|
|
35
|
+
expect(fakeExecutor).toHaveBeenCalledWith(expect.objectContaining({ executable: toolExecutable, resolution: 'python-environment', args: ['--version'] }), expect.any(Object));
|
|
36
|
+
});
|
|
27
37
|
});
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
exports.createSemgrepVenvFixture = createSemgrepVenvFixture;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
/** Creates contained Semgrep metadata and the host-appropriate virtualenv executable. */
|
|
10
|
+
function createSemgrepVenvFixture(root, targetPlatform = process.platform, environmentRoot = '.venv', options = {}) {
|
|
11
|
+
if (typeof root !== 'string' || root.trim() === '')
|
|
12
|
+
throw new Error('fixture root must be a non-empty path');
|
|
13
|
+
const windows = targetPlatform === 'win32';
|
|
14
|
+
const sitePackages = windows
|
|
15
|
+
? [environmentRoot, 'Lib', 'site-packages']
|
|
16
|
+
: [environmentRoot, 'lib', 'python3.12', 'site-packages'];
|
|
17
|
+
const metadata = path_1.default.join(root, ...sitePackages, 'semgrep-1.91.0.dist-info');
|
|
18
|
+
fs_1.default.mkdirSync(metadata, { recursive: true });
|
|
19
|
+
fs_1.default.writeFileSync(path_1.default.join(root, environmentRoot, 'pyvenv.cfg'), 'version = 3.12.4\n');
|
|
20
|
+
fs_1.default.writeFileSync(path_1.default.join(metadata, 'METADATA'), 'Name: semgrep\nVersion: 1.91.0\n');
|
|
21
|
+
if (options.executable === false)
|
|
22
|
+
return;
|
|
23
|
+
const executable = path_1.default.join(root, environmentRoot, windows ? 'Scripts' : 'bin', windows ? 'semgrep.exe' : 'semgrep');
|
|
24
|
+
fs_1.default.mkdirSync(path_1.default.dirname(executable), { recursive: true });
|
|
25
|
+
if (windows)
|
|
26
|
+
fs_1.default.copyFileSync(process.execPath, executable);
|
|
27
|
+
else
|
|
28
|
+
fs_1.default.writeFileSync(executable, '#!/bin/sh\necho local-semgrep\n', { mode: 0o755 });
|
|
29
|
+
}
|
|
@@ -12,6 +12,14 @@ const sensor = { applicability: { allFiles: ['package.json'] }, variants: [varia
|
|
|
12
12
|
const evidence = (input = {}) => ({ paths: ['package.json'], applicable: true, packageManagerConflict: false, toolVersion: '10.4.1', runtimeVersion: '22.0.0', probe: { status: 'matched' }, ...input });
|
|
13
13
|
const context = { pack: 'js-ts', sensor: 'lint' };
|
|
14
14
|
describe('resolveSensorCompatibility', () => {
|
|
15
|
+
it('keeps an automatically selected generic pack not-applicable despite language markers', () => {
|
|
16
|
+
const resolved = (0, resolve_1.resolveSensorCompatibility)({ applicability: { kind: 'explicit-or-supported-language' }, variants: [variant('semgrep')] }, evidence({ applicable: undefined, paths: ['pyproject.toml'], toolVersion: '1.0.0', runtimeVersion: '3.12.0', probe: { status: 'matched' } }), { pack: 'generic', sensor: 'security' });
|
|
17
|
+
expect(resolved).toMatchObject({ state: 'not-applicable', reason: 'applicability-not-met' });
|
|
18
|
+
});
|
|
19
|
+
it('treats the persisted explicit generic pack selection as positive capability in project resolution', () => {
|
|
20
|
+
const resolved = (0, resolve_1.resolveProjectCompatibility)({ schemaVersion: 2, name: 'generic', sensors: { security: { applicability: { kind: 'explicit-or-supported-language' }, variants: [variant('semgrep')] } } }, evidence({ applicable: undefined, paths: [], packSelection: 'explicit' }));
|
|
21
|
+
expect(resolved.sensors.security.state).not.toBe('not-applicable');
|
|
22
|
+
});
|
|
15
23
|
test.each([
|
|
16
24
|
['certified', evidence(), 'eslint-main'],
|
|
17
25
|
['compatible-unverified', evidence({ toolVersion: '11.0.0' }), 'eslint-main'],
|