agentic-workflow-manager 8.1.6 → 8.2.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/sensors/compatibility/live.js +1 -0
- package/dist/src/commands/sensors/compatibility/manifest.js +3 -1
- package/dist/src/commands/sensors/compatibility/materialize.js +6 -2
- package/dist/src/commands/sensors/compatibility/probe.js +15 -3
- package/dist/src/commands/sensors/index.js +2 -1
- package/dist/src/commands/sensors/init.js +8 -3
- package/dist/src/commands/sensors/run.js +10 -2
- package/dist/src/commands/sensors/status.js +6 -2
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +12 -0
- package/dist/tests/commands/sensors/compatibility/probe.test.js +27 -0
- package/dist/tests/commands/sensors/init.test.js +53 -0
- package/dist/tests/commands/sensors/run.test.js +27 -0
- package/dist/tests/commands/sensors/status.test.js +58 -0
- package/dist/tests/integration/published-sensor-gate-matrix.e2e.test.js +237 -0
- package/dist/tests/structural/published-sensor-gate-acceptance.test.js +35 -0
- package/package.json +2 -2
|
@@ -85,6 +85,7 @@ async function resolveParsedPackCompatibility(cwd, pack, options = {}) {
|
|
|
85
85
|
environment: variant.command.environment,
|
|
86
86
|
configFiles: evidence.configFiles,
|
|
87
87
|
scripts: evidence.scripts,
|
|
88
|
+
variantArgs: variant.command.args,
|
|
88
89
|
})
|
|
89
90
|
: null;
|
|
90
91
|
sensors[name] = (0, resolve_1.resolveProjectCompatibility)({ ...executionPack, sensors: { [name]: sensor } }, { ...resolutionEvidence, probe: probe ?? undefined }).sensors[name];
|
|
@@ -184,7 +184,7 @@ function provenanceRoot(value, source) {
|
|
|
184
184
|
return parsed;
|
|
185
185
|
}
|
|
186
186
|
function parseV2Manifest(value, source) {
|
|
187
|
-
fields(value, ['schemaVersion', 'pack', 'packSelection', 'registryRoot', 'sensors', 'concurrency'], source, 'root');
|
|
187
|
+
fields(value, ['schemaVersion', 'pack', 'packSelection', 'registryRoot', 'packageRoot', 'sensors', 'concurrency'], source, 'root');
|
|
188
188
|
if (value.schemaVersion !== 2)
|
|
189
189
|
invalid(source, `unsupported manifest schemaVersion ${String(value.schemaVersion)}; supported: legacy, 2; upgrade or migrate the manifest`);
|
|
190
190
|
const pack = id(value.pack, source, 'pack');
|
|
@@ -200,6 +200,8 @@ function parseV2Manifest(value, source) {
|
|
|
200
200
|
}
|
|
201
201
|
if ('registryRoot' in value)
|
|
202
202
|
manifest.registryRoot = provenanceRoot(value.registryRoot, source);
|
|
203
|
+
if ('packageRoot' in value)
|
|
204
|
+
manifest.packageRoot = asset(value.packageRoot, source, 'packageRoot');
|
|
203
205
|
if ('concurrency' in value) {
|
|
204
206
|
if (typeof value.concurrency !== 'number' || !Number.isSafeInteger(value.concurrency) || value.concurrency <= 0)
|
|
205
207
|
invalid(source, 'concurrency must be a positive safe integer');
|
|
@@ -82,7 +82,11 @@ function materializeResolvedSensors(input) {
|
|
|
82
82
|
throw new Error('materialized sensor must be v2');
|
|
83
83
|
sensors[name] = parsed.pack.sensors[name];
|
|
84
84
|
}
|
|
85
|
-
const manifest = { schemaVersion: 2, pack, sensors, ...(input.packSelection === 'explicit' ? { packSelection: 'explicit' } : {}), ...(input.registryRoot ? { registryRoot: input.registryRoot } : {}) };
|
|
85
|
+
const manifest = { schemaVersion: 2, pack, sensors, ...(input.packSelection === 'explicit' ? { packSelection: 'explicit' } : {}), ...(input.registryRoot ? { registryRoot: input.registryRoot } : {}), ...(input.packageRoot ? { packageRoot: containedAsset(input.packageRoot, 'packageRoot') } : {}) };
|
|
86
|
+
// Assets land where sensors will actually run — projectRoot by default, or
|
|
87
|
+
// packageRoot underneath it for a monorepo. The manifest write below stays
|
|
88
|
+
// pinned to projectRoot either way (see atomicWrite call at the end).
|
|
89
|
+
const configRoot = manifest.packageRoot ? root(path_1.default.join(projectRoot, manifest.packageRoot), 'packageRoot') : projectRoot;
|
|
86
90
|
// A policy reference is deliberately not a materialized asset. Resolve it here
|
|
87
91
|
// from the registry-owned sibling only, so a manifest cannot turn arbitrary
|
|
88
92
|
// registry content into a project write through a cosmetic policy field.
|
|
@@ -99,7 +103,7 @@ function materializeResolvedSensors(input) {
|
|
|
99
103
|
if (input.configure !== false) {
|
|
100
104
|
for (const asset of selected) {
|
|
101
105
|
const source = path_1.default.join(packRoot, ...asset.split('/'));
|
|
102
|
-
const destination = path_1.default.join(
|
|
106
|
+
const destination = path_1.default.join(configRoot, ...asset.split('/'));
|
|
103
107
|
let stat;
|
|
104
108
|
try {
|
|
105
109
|
stat = fs_1.default.lstatSync(source);
|
|
@@ -24,11 +24,23 @@ function commandFor(kind, evidence) {
|
|
|
24
24
|
};
|
|
25
25
|
if (kind === 'version')
|
|
26
26
|
return { ...command, args: ['--version'] };
|
|
27
|
+
// A pack asset is never named the tool's default config filename (e.g.
|
|
28
|
+
// `eslint.config.awm.mjs`, not `eslint.config.js`), so the real command always
|
|
29
|
+
// pins it via an explicit `--config <file>`. Mirroring that same pair here — rather
|
|
30
|
+
// than probing with a bare invocation — is what lets the probe find the config the
|
|
31
|
+
// real run will actually use, for whatever name or tool version is in play.
|
|
27
32
|
if (kind === 'eslint-print-config')
|
|
28
|
-
return { ...command, args: ['--print-config', evidence.configFiles?.[0] ?? 'package.json'] };
|
|
33
|
+
return { ...command, args: [...configFlag(evidence.variantArgs), '--print-config', evidence.configFiles?.[0] ?? 'package.json'] };
|
|
29
34
|
if (kind === 'typescript-show-config')
|
|
30
|
-
return { ...command, args: ['--showConfig'] };
|
|
31
|
-
return { ...command, args: ['--validate'] };
|
|
35
|
+
return { ...command, args: [...configFlag(evidence.variantArgs), '--showConfig'] };
|
|
36
|
+
return { ...command, args: [...configFlag(evidence.variantArgs), '--validate'] };
|
|
37
|
+
}
|
|
38
|
+
/** The `--config <file>` pair from a variant's real command args, if it declares one. */
|
|
39
|
+
function configFlag(variantArgs) {
|
|
40
|
+
if (!variantArgs)
|
|
41
|
+
return [];
|
|
42
|
+
const i = variantArgs.indexOf('--config');
|
|
43
|
+
return i !== -1 && variantArgs[i + 1] !== undefined ? ['--config', variantArgs[i + 1]] : [];
|
|
32
44
|
}
|
|
33
45
|
/** Executes only the closed probe enum. Raw output is intentionally discarded. */
|
|
34
46
|
async function runCompatibilityProbe(probe, evidence, executor = exec_1.runStructuredCommand) {
|
|
@@ -67,10 +67,11 @@ function registerSensorsCommand(program) {
|
|
|
67
67
|
.option('--no-configure', 'skip copying sensor pack config files into the project')
|
|
68
68
|
.option('--registry-root <path>', 'path to AWM registry root')
|
|
69
69
|
.option('--pack <name>', 'skip auto-detection, use this pack explicitly')
|
|
70
|
+
.option('--package-root <dir>', 'run detection/execution from this subdirectory (monorepo support) — the manifest still writes at the current directory')
|
|
70
71
|
.action(async (opts) => {
|
|
71
72
|
const registryRoot = opts.registryRoot ?? (0, registries_1.capabilityRoot)('sensor-packs') ?? undefined;
|
|
72
73
|
try {
|
|
73
|
-
const result = await (0, init_1.initSensors)({ configure: opts.configure, registryRoot, pack: opts.pack });
|
|
74
|
+
const result = await (0, init_1.initSensors)({ configure: opts.configure, registryRoot, pack: opts.pack, packageRoot: opts.packageRoot });
|
|
74
75
|
prompts_1.log.success(`Detected: ${result.detection.pack} (${result.detection.indicators.join(', ') || 'fallback'})`);
|
|
75
76
|
// Said BEFORE "Wrote .awm/sensors.json": the manifest about to be
|
|
76
77
|
// reported as written is not the one the detection implied.
|
|
@@ -213,6 +213,10 @@ async function initSensors(opts = {}) {
|
|
|
213
213
|
const cwd = opts.cwd ?? process.cwd();
|
|
214
214
|
const configure = opts.configure ?? true; // configure (copy pack config files) by default
|
|
215
215
|
const manifestPath = path_1.default.join(cwd, '.awm', 'sensors.json');
|
|
216
|
+
// Monorepo support: detection and evidence both need to see the real project,
|
|
217
|
+
// not the (possibly unrelated) directory the manifest lives in. The manifest
|
|
218
|
+
// write itself stays pinned to cwd regardless — see manifestPath above.
|
|
219
|
+
const detectionCwd = opts.packageRoot ? path_1.default.resolve(cwd, opts.packageRoot) : cwd;
|
|
216
220
|
// --pack skips the heuristic entirely. Only validate against the registry when a
|
|
217
221
|
// registryRoot was actually given — same tolerance pattern as readPackDefaults /
|
|
218
222
|
// buildManifest elsewhere in this file for a missing registry: nothing to validate
|
|
@@ -224,7 +228,7 @@ async function initSensors(opts = {}) {
|
|
|
224
228
|
detection = { pack: opts.pack, indicators: ['--pack override'] };
|
|
225
229
|
}
|
|
226
230
|
else {
|
|
227
|
-
detection = detectStack(
|
|
231
|
+
detection = detectStack(detectionCwd);
|
|
228
232
|
}
|
|
229
233
|
let existing;
|
|
230
234
|
let existingV2;
|
|
@@ -254,7 +258,7 @@ async function initSensors(opts = {}) {
|
|
|
254
258
|
const resolvedV2 = readV2Pack(resolvedPack, opts.registryRoot);
|
|
255
259
|
if (resolvedV2) {
|
|
256
260
|
const packSelection = opts.pack ? 'explicit' : undefined;
|
|
257
|
-
const live = await (0, live_1.resolveParsedPackCompatibility)(
|
|
261
|
+
const live = await (0, live_1.resolveParsedPackCompatibility)(detectionCwd, resolvedV2.pack, { packSelection });
|
|
258
262
|
const compatibility = live.sensors;
|
|
259
263
|
const sensors = {};
|
|
260
264
|
for (const [name, sensor] of Object.entries(live.pack.sensors)) {
|
|
@@ -285,7 +289,8 @@ async function initSensors(opts = {}) {
|
|
|
285
289
|
}
|
|
286
290
|
const materialized = (0, materialize_1.materializeResolvedSensors)({
|
|
287
291
|
projectRoot: cwd, packRoot: resolvedV2.packRoot,
|
|
288
|
-
pack: resolvedPack, ...(packSelection ? { packSelection } : {}), registryRoot: opts.registryRoot,
|
|
292
|
+
pack: resolvedPack, ...(packSelection ? { packSelection } : {}), registryRoot: opts.registryRoot,
|
|
293
|
+
...(opts.packageRoot ? { packageRoot: opts.packageRoot } : {}), sensors, configure,
|
|
289
294
|
});
|
|
290
295
|
return { detection, ...materialized,
|
|
291
296
|
compatibility, ...(unavailablePack ? { unavailablePack } : {}) };
|
|
@@ -154,7 +154,15 @@ async function runSensors(opts = {}) {
|
|
|
154
154
|
if (scopeError)
|
|
155
155
|
changed.error = scopeError;
|
|
156
156
|
}
|
|
157
|
-
|
|
157
|
+
// Monorepo support: a v2 manifest may declare packageRoot so detection and
|
|
158
|
+
// execution both happen against the real package (e.g. "cli"), while the
|
|
159
|
+
// manifest itself stays discoverable at the repo root via findManifestDir.
|
|
160
|
+
// Legacy manifests never carry this field — manifestDir is always correct
|
|
161
|
+
// for them, unchanged.
|
|
162
|
+
const projectCwd = parsed.kind === 'v2' && parsed.pack.packageRoot
|
|
163
|
+
? path_1.default.resolve(manifestDir, parsed.pack.packageRoot)
|
|
164
|
+
: manifestDir;
|
|
165
|
+
const live = parsed.kind === 'v2' ? await resolveLiveV2(projectCwd, parsed) : null;
|
|
158
166
|
const drift = parsed.kind === 'legacy' ? detectPackDrift(manifestDir, parsed.pack) : undefined;
|
|
159
167
|
const requestedScope = opts.changed ? 'changed' : 'full';
|
|
160
168
|
const prepared = [];
|
|
@@ -173,7 +181,7 @@ async function runSensors(opts = {}) {
|
|
|
173
181
|
: execution);
|
|
174
182
|
}
|
|
175
183
|
const results = await pooled(prepared.map(entry => async () => {
|
|
176
|
-
const result = await (0, result_1.executePrepared)(entry,
|
|
184
|
+
const result = await (0, result_1.executePrepared)(entry, projectCwd);
|
|
177
185
|
return baseline ? (0, result_1.applyBaseline)(result, baseline[entry.name]) : result;
|
|
178
186
|
}), resolveConcurrency(parsed.pack, prepared.length));
|
|
179
187
|
let overall = (0, verdict_1.reduceVerdict)(results);
|
|
@@ -145,10 +145,14 @@ async function computeSensorStatus(cwd = process.cwd()) {
|
|
|
145
145
|
const raw = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
|
|
146
146
|
const parsed = (0, manifest_1.parseSensorManifest)(raw, manifestPath);
|
|
147
147
|
if (parsed.kind === 'v2') {
|
|
148
|
+
// Monorepo support (mirrors run.ts/init.ts): detection and structured-command
|
|
149
|
+
// asset resolution both need to see the real package, not the (possibly
|
|
150
|
+
// unrelated) directory the manifest lives in.
|
|
151
|
+
const projectCwd = parsed.pack.packageRoot ? path_1.default.resolve(cwd, parsed.pack.packageRoot) : cwd;
|
|
148
152
|
const checks = {};
|
|
149
153
|
let compatibility;
|
|
150
154
|
try {
|
|
151
|
-
compatibility = resolveStaticV2Compatibility(
|
|
155
|
+
compatibility = resolveStaticV2Compatibility(projectCwd, parsed.pack);
|
|
152
156
|
}
|
|
153
157
|
catch (error) {
|
|
154
158
|
const detail = error instanceof Error ? error.message : 'live compatibility unavailable';
|
|
@@ -163,7 +167,7 @@ async function computeSensorStatus(cwd = process.cwd()) {
|
|
|
163
167
|
continue;
|
|
164
168
|
}
|
|
165
169
|
checks[name] = staticCompatibilityCheck(sensor, compatibility[name])
|
|
166
|
-
?? checkStructuredCommand(sensor.command,
|
|
170
|
+
?? checkStructuredCommand(sensor.command, projectCwd, sensor.assets);
|
|
167
171
|
}
|
|
168
172
|
return { overall: Object.keys(checks).length > 0 && Object.values(checks).every(check => check.ok) ? 'READY' : 'DEGRADED', pack: parsed.pack.pack, checks };
|
|
169
173
|
}
|
|
@@ -82,6 +82,18 @@ describe('sensor manifest contract', () => {
|
|
|
82
82
|
const sensor = { enabled: true, variantId: 'eslint-9', command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] }, initializedCompatibility: { state: 'certified', reason: 'ok', variantId: 'eslint-9', toolVersion: '10.0.0', runtimeVersion: '24.0.0', certifiedRange: '>=9 <10', evidence: [] } };
|
|
83
83
|
expect(() => (0, manifest_1.parseSensorManifest)({ schemaVersion: 2, pack: 'js-ts', sensors: { lint: sensor } }, 'source')).toThrow('certifiedRange');
|
|
84
84
|
});
|
|
85
|
+
it('accepts a v2 packageRoot for monorepo-scoped sensor detection/execution', () => {
|
|
86
|
+
const manifest = { ...validV2Manifest(), packageRoot: 'cli' };
|
|
87
|
+
expect((0, manifest_1.parseSensorManifest)(manifest, 'source')).toMatchObject({ kind: 'v2', pack: { packageRoot: 'cli' } });
|
|
88
|
+
});
|
|
89
|
+
it('rejects a packageRoot that escapes the manifest directory', () => {
|
|
90
|
+
const manifest = { ...validV2Manifest(), packageRoot: '../outside' };
|
|
91
|
+
expect(() => (0, manifest_1.parseSensorManifest)(manifest, 'source')).toThrow('packageRoot');
|
|
92
|
+
});
|
|
93
|
+
it('rejects an absolute packageRoot', () => {
|
|
94
|
+
const manifest = { ...validV2Manifest(), packageRoot: '/etc' };
|
|
95
|
+
expect(() => (0, manifest_1.parseSensorManifest)(manifest, 'source')).toThrow('packageRoot');
|
|
96
|
+
});
|
|
85
97
|
test.each([
|
|
86
98
|
[null, 'object'],
|
|
87
99
|
[{}, 'pack'],
|
|
@@ -53,4 +53,31 @@ describe('runCompatibilityProbe', () => {
|
|
|
53
53
|
await (0, probe_1.runCompatibilityProbe)({ kind: 'version' }, { ...evidence, toolExecutable, toolResolution: 'python-environment' }, fakeExecutor);
|
|
54
54
|
expect(fakeExecutor).toHaveBeenCalledWith(expect.objectContaining({ executable: toolExecutable, resolution: 'python-environment', args: ['--version'] }), expect.any(Object));
|
|
55
55
|
});
|
|
56
|
+
// A pack asset is never named the tool's own default (e.g. `eslint.config.awm.mjs`,
|
|
57
|
+
// not `eslint.config.js`) so the real command always pins it via `--config`. A probe
|
|
58
|
+
// that omits the same flag never finds a config to load and always reports
|
|
59
|
+
// not-matched — a false negative on every AWM-configured project. The probe must
|
|
60
|
+
// mirror the variant's own `--config` argument, not guess a bare invocation.
|
|
61
|
+
it('carries the variant\'s --config argument into an eslint-print-config probe', async () => {
|
|
62
|
+
await (0, probe_1.runCompatibilityProbe)({ kind: 'eslint-print-config' }, {
|
|
63
|
+
...evidence, variantArgs: ['.', '--config', 'eslint.config.awm.mjs', '--cache', '--format', 'json'],
|
|
64
|
+
}, fakeExecutor);
|
|
65
|
+
expect(fakeExecutor).toHaveBeenCalledWith(expect.objectContaining({ args: ['--config', 'eslint.config.awm.mjs', '--print-config', 'eslint.config.js'] }), expect.any(Object));
|
|
66
|
+
});
|
|
67
|
+
it('carries the variant\'s --config argument into a typescript-show-config probe', async () => {
|
|
68
|
+
await (0, probe_1.runCompatibilityProbe)({ kind: 'typescript-show-config' }, {
|
|
69
|
+
...evidence, variantArgs: ['--config', 'tsconfig.awm.json', '--noEmit'],
|
|
70
|
+
}, fakeExecutor);
|
|
71
|
+
expect(fakeExecutor).toHaveBeenCalledWith(expect.objectContaining({ args: ['--config', 'tsconfig.awm.json', '--showConfig'] }), expect.any(Object));
|
|
72
|
+
});
|
|
73
|
+
it('carries the variant\'s --config argument into a semgrep-validate probe', async () => {
|
|
74
|
+
await (0, probe_1.runCompatibilityProbe)({ kind: 'semgrep-validate' }, {
|
|
75
|
+
...evidence, variantArgs: ['--config', '.semgrep.awm.yml', '--json', '.'],
|
|
76
|
+
}, fakeExecutor);
|
|
77
|
+
expect(fakeExecutor).toHaveBeenCalledWith(expect.objectContaining({ args: ['--config', '.semgrep.awm.yml', '--validate'] }), expect.any(Object));
|
|
78
|
+
});
|
|
79
|
+
it('omits --config from the probe when the variant command declares none', async () => {
|
|
80
|
+
await (0, probe_1.runCompatibilityProbe)({ kind: 'eslint-print-config' }, { ...evidence, variantArgs: ['.', '--cache'] }, fakeExecutor);
|
|
81
|
+
expect(fakeExecutor).toHaveBeenCalledWith(expect.objectContaining({ args: ['--print-config', 'eslint.config.js'] }), expect.any(Object));
|
|
82
|
+
});
|
|
56
83
|
});
|
|
@@ -434,6 +434,59 @@ describe('initSensors', () => {
|
|
|
434
434
|
}
|
|
435
435
|
});
|
|
436
436
|
});
|
|
437
|
+
describe('initSensors — packageRoot (monorepo)', () => {
|
|
438
|
+
let tmpDir;
|
|
439
|
+
beforeEach(() => {
|
|
440
|
+
tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-init-monorepo-'));
|
|
441
|
+
});
|
|
442
|
+
afterEach(() => {
|
|
443
|
+
fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
|
|
444
|
+
});
|
|
445
|
+
it('detects the real package under packageRoot, not the manifest directory', async () => {
|
|
446
|
+
const v2Registry = makeV2Registry();
|
|
447
|
+
try {
|
|
448
|
+
// The manifest will live at tmpDir/.awm/sensors.json (repo root), but the
|
|
449
|
+
// real package — package.json, node_modules — lives under tmpDir/cli.
|
|
450
|
+
const packageDir = path_1.default.join(tmpDir, 'cli');
|
|
451
|
+
fs_1.default.mkdirSync(packageDir, { recursive: true });
|
|
452
|
+
fs_1.default.writeFileSync(path_1.default.join(packageDir, 'package.json'), JSON.stringify({ devDependencies: { eslint: '^10.0.0' } }));
|
|
453
|
+
fs_1.default.mkdirSync(path_1.default.join(packageDir, 'node_modules', 'eslint'), { recursive: true });
|
|
454
|
+
fs_1.default.writeFileSync(path_1.default.join(packageDir, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version: '10.0.0' }));
|
|
455
|
+
const result = await (0, init_1.initSensors)({ cwd: tmpDir, registryRoot: v2Registry, packageRoot: 'cli' });
|
|
456
|
+
// Real detection succeeded — a manifest built against tmpDir root (with no
|
|
457
|
+
// package.json there) would leave lint unresolved/absent.
|
|
458
|
+
expect(result.manifest).toMatchObject({ packageRoot: 'cli', sensors: { lint: { variantId: 'eslint-10' } } });
|
|
459
|
+
// The manifest itself stays discoverable at the repo root (findManifestDir
|
|
460
|
+
// walks up from cwd, never down into subdirectories).
|
|
461
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'))).toBe(true);
|
|
462
|
+
expect(fs_1.default.existsSync(path_1.default.join(packageDir, '.awm', 'sensors.json'))).toBe(false);
|
|
463
|
+
const written = JSON.parse(fs_1.default.readFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), 'utf8'));
|
|
464
|
+
expect(written.packageRoot).toBe('cli');
|
|
465
|
+
}
|
|
466
|
+
finally {
|
|
467
|
+
fs_1.default.rmSync(v2Registry, { recursive: true, force: true });
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
it('copies configured assets into packageRoot, not the manifest directory', async () => {
|
|
471
|
+
const v2Registry = makeV2Registry();
|
|
472
|
+
try {
|
|
473
|
+
const packageDir = path_1.default.join(tmpDir, 'cli');
|
|
474
|
+
fs_1.default.mkdirSync(packageDir, { recursive: true });
|
|
475
|
+
fs_1.default.writeFileSync(path_1.default.join(packageDir, 'package.json'), JSON.stringify({ devDependencies: { eslint: '^10.0.0' } }));
|
|
476
|
+
fs_1.default.mkdirSync(path_1.default.join(packageDir, 'node_modules', 'eslint'), { recursive: true });
|
|
477
|
+
fs_1.default.writeFileSync(path_1.default.join(packageDir, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version: '10.0.0' }));
|
|
478
|
+
await (0, init_1.initSensors)({ cwd: tmpDir, registryRoot: v2Registry, packageRoot: 'cli' });
|
|
479
|
+
// The asset must land where the lint sensor will actually execute (cwd=cli),
|
|
480
|
+
// not at the manifest's own directory (repo root) — otherwise a config-relative
|
|
481
|
+
// tool invocation can never find it.
|
|
482
|
+
expect(fs_1.default.existsSync(path_1.default.join(packageDir, 'eslint.config.awm.mjs'))).toBe(true);
|
|
483
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpDir, 'eslint.config.awm.mjs'))).toBe(false);
|
|
484
|
+
}
|
|
485
|
+
finally {
|
|
486
|
+
fs_1.default.rmSync(v2Registry, { recursive: true, force: true });
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
});
|
|
437
490
|
describe('initSensors — --pack override', () => {
|
|
438
491
|
let tmpDir;
|
|
439
492
|
let registryRoot;
|
|
@@ -282,6 +282,33 @@ describe('runSensors v2 lifecycle contract', () => {
|
|
|
282
282
|
expect(result).toEqual(expect.objectContaining({ overall: 'pass' }));
|
|
283
283
|
expect(mockRunStructuredCommand).toHaveBeenCalledWith(expect.objectContaining({ executable: 'live-eslint' }), expect.any(Object));
|
|
284
284
|
});
|
|
285
|
+
it('scopes applicability detection and execution to packageRoot for a monorepo manifest', async () => {
|
|
286
|
+
// Move the fixture project into a subdirectory ("cli") and point the
|
|
287
|
+
// manifest at it via packageRoot — mirrors a monorepo where the
|
|
288
|
+
// manifest lives at the repo root but the real package lives deeper.
|
|
289
|
+
const packageDir = path_1.default.join(project, 'cli');
|
|
290
|
+
fs_1.default.mkdirSync(packageDir, { recursive: true });
|
|
291
|
+
fs_1.default.renameSync(path_1.default.join(project, 'package.json'), path_1.default.join(packageDir, 'package.json'));
|
|
292
|
+
fs_1.default.renameSync(path_1.default.join(project, 'node_modules'), path_1.default.join(packageDir, 'node_modules'));
|
|
293
|
+
const manifestPath = path_1.default.join(project, '.awm', 'sensors.json');
|
|
294
|
+
const manifest = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf8'));
|
|
295
|
+
manifest.packageRoot = 'cli';
|
|
296
|
+
fs_1.default.writeFileSync(manifestPath, JSON.stringify(manifest));
|
|
297
|
+
const { runSensors } = require('../../../src/commands/sensors/run');
|
|
298
|
+
const result = await runSensors({ cwd: project, fast: true });
|
|
299
|
+
expect(result).toEqual(expect.objectContaining({ overall: 'pass' }));
|
|
300
|
+
expect(mockRunStructuredCommand).toHaveBeenCalledWith(expect.objectContaining({ executable: 'live-eslint' }), expect.objectContaining({ cwd: packageDir }));
|
|
301
|
+
});
|
|
302
|
+
it('degrades to not_certified (never a false pass, never a crash) when packageRoot points nowhere', async () => {
|
|
303
|
+
const manifestPath = path_1.default.join(project, '.awm', 'sensors.json');
|
|
304
|
+
const manifest = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf8'));
|
|
305
|
+
manifest.packageRoot = 'does-not-exist';
|
|
306
|
+
fs_1.default.writeFileSync(manifestPath, JSON.stringify(manifest));
|
|
307
|
+
const { runSensors } = require('../../../src/commands/sensors/run');
|
|
308
|
+
const result = await runSensors({ cwd: project, fast: true });
|
|
309
|
+
expect(result.overall).toBe('not_certified');
|
|
310
|
+
expect(mockRunStructuredCommand).not.toHaveBeenCalled();
|
|
311
|
+
});
|
|
285
312
|
it('honors disabled v2 sensors before dispatching them', async () => {
|
|
286
313
|
const manifest = JSON.parse(fs_1.default.readFileSync(path_1.default.join(project, '.awm', 'sensors.json'), 'utf8'));
|
|
287
314
|
manifest.sensors.lint.enabled = false;
|
|
@@ -182,6 +182,64 @@ describe('computeSensorStatus', () => {
|
|
|
182
182
|
expect(result.pack).toBe('python');
|
|
183
183
|
expect(result.checks).toEqual({});
|
|
184
184
|
});
|
|
185
|
+
it('resolves v2 compatibility and structured-command assets against packageRoot in a monorepo', async () => {
|
|
186
|
+
// Monorepo support (mirrors run.ts/init.ts): the manifest lives at the repo
|
|
187
|
+
// root, but package.json/node_modules/the config asset all live under the
|
|
188
|
+
// declared packageRoot subdirectory. Without threading packageRoot through,
|
|
189
|
+
// detection finds no package.json at the manifest's own directory and every
|
|
190
|
+
// sensor reads back "not-applicable" — a false compatibility drift on every
|
|
191
|
+
// check, even immediately after a correct `awm sensors init --package-root`.
|
|
192
|
+
const previousHome = process.env.AWM_HOME;
|
|
193
|
+
const home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-home-'));
|
|
194
|
+
try {
|
|
195
|
+
process.env.AWM_HOME = home;
|
|
196
|
+
const registry = path_1.default.join(home, 'registries', 'baseline');
|
|
197
|
+
fs_1.default.mkdirSync(path_1.default.join(registry, 'sensor-packs', 'js-ts'), { recursive: true });
|
|
198
|
+
fs_1.default.writeFileSync(path_1.default.join(home, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.test/baseline.git' }]));
|
|
199
|
+
fs_1.default.writeFileSync(path_1.default.join(registry, 'sensor-packs', 'js-ts', 'pack.json'), JSON.stringify({
|
|
200
|
+
schemaVersion: 2, name: 'js-ts', description: 'test', detects: ['package.json'],
|
|
201
|
+
coverage: { schemaVersion: 1, classes: { lint: { description: 'lint', detectors: [{ sensor: 'lint' }], remedy: { summary: 'fix lint', command: 'awm sensors init --pack js-ts' } } } },
|
|
202
|
+
sensors: { lint: {
|
|
203
|
+
applicability: { allFiles: ['package.json'] },
|
|
204
|
+
variants: [{
|
|
205
|
+
id: 'eslint-10', priority: 10, certifiedRange: '>=10.0.0 <11.0.0',
|
|
206
|
+
requirements: { tool: 'eslint', toolRange: '>=10.0.0 <11.0.0', runtime: 'node', runtimeRange: '>=0.0.0' },
|
|
207
|
+
assets: ['eslint.config.awm.mjs'], formatter: 'eslint-llm', probe: { kind: 'package-script-present' },
|
|
208
|
+
command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.', '--config', 'eslint.config.awm.mjs'] },
|
|
209
|
+
}],
|
|
210
|
+
} },
|
|
211
|
+
}));
|
|
212
|
+
const packageDir = path_1.default.join(tmpDir, 'cli');
|
|
213
|
+
fs_1.default.mkdirSync(packageDir, { recursive: true });
|
|
214
|
+
fs_1.default.writeFileSync(path_1.default.join(packageDir, 'package.json'), JSON.stringify({ devDependencies: { eslint: '^10.0.0' }, scripts: { lint: 'eslint .' } }));
|
|
215
|
+
fs_1.default.mkdirSync(path_1.default.join(packageDir, 'node_modules', 'eslint'), { recursive: true });
|
|
216
|
+
fs_1.default.writeFileSync(path_1.default.join(packageDir, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version: '10.4.1' }));
|
|
217
|
+
fs_1.default.mkdirSync(path_1.default.join(packageDir, 'node_modules', '.bin'), { recursive: true });
|
|
218
|
+
fs_1.default.writeFileSync(path_1.default.join(packageDir, 'node_modules', '.bin', 'eslint'), '');
|
|
219
|
+
fs_1.default.writeFileSync(path_1.default.join(packageDir, 'eslint.config.awm.mjs'), 'export default []');
|
|
220
|
+
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
221
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
222
|
+
schemaVersion: 2, pack: 'js-ts', packageRoot: 'cli', registryRoot: registry,
|
|
223
|
+
sensors: { lint: {
|
|
224
|
+
enabled: true, variantId: 'eslint-10', command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.', '--config', 'eslint.config.awm.mjs'] },
|
|
225
|
+
assets: ['eslint.config.awm.mjs'],
|
|
226
|
+
initializedCompatibility: { state: 'certified', reason: 'range-and-probe', variantId: 'eslint-10', toolVersion: '10.4.1', runtimeVersion: process.versions.node, certifiedRange: '>=10.0.0 <11.0.0', evidence: [] },
|
|
227
|
+
} },
|
|
228
|
+
}));
|
|
229
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
230
|
+
expect(result.checks.lint).toMatchObject({ ok: true });
|
|
231
|
+
expect(result.overall).toBe('READY');
|
|
232
|
+
expect(runCommand).not.toHaveBeenCalled();
|
|
233
|
+
expect(runStructuredCommand).not.toHaveBeenCalled();
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
if (previousHome === undefined)
|
|
237
|
+
delete process.env.AWM_HOME;
|
|
238
|
+
else
|
|
239
|
+
process.env.AWM_HOME = previousHome;
|
|
240
|
+
fs_1.default.rmSync(home, { recursive: true, force: true });
|
|
241
|
+
}
|
|
242
|
+
});
|
|
185
243
|
it('marks disabled sensors as ok', async () => {
|
|
186
244
|
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
187
245
|
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
@@ -0,0 +1,237 @@
|
|
|
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 crypto_1 = __importDefault(require("crypto"));
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
/**
|
|
12
|
+
* This acceptance gate is deliberately opt-in. It downloads two release
|
|
13
|
+
* artifacts and installs fixture dependencies, so normal unit/PR runs must not
|
|
14
|
+
* accidentally treat a mutable checkout as publication evidence.
|
|
15
|
+
*
|
|
16
|
+
* Run it with:
|
|
17
|
+
* AWM_RUN_PUBLISHED_SENSOR_GATE_MATRIX=1 npm test -- published-sensor-gate-matrix
|
|
18
|
+
*/
|
|
19
|
+
const enabled = process.env.AWM_RUN_PUBLISHED_SENSOR_GATE_MATRIX === '1';
|
|
20
|
+
const acceptance = enabled ? describe : describe.skip;
|
|
21
|
+
const cliVersion = process.env.AWM_PUBLISHED_CLI_VERSION ?? '8.1.6';
|
|
22
|
+
const registryTag = process.env.AWM_PUBLISHED_REGISTRY_TAG ?? 'v3.0.0';
|
|
23
|
+
const registryRemote = process.env.AWM_PUBLISHED_REGISTRY_REMOTE ?? 'https://github.com/Kodria/awm-baseline-registry.git';
|
|
24
|
+
// Keep the shorter name used by the release orchestrator, while accepting the
|
|
25
|
+
// original explicit name for local invocation.
|
|
26
|
+
const reportFile = process.env.AWM_PUBLISHED_MATRIX_REPORT ?? process.env.AWM_PUBLISHED_SENSOR_GATE_MATRIX_REPORT;
|
|
27
|
+
function hash(value) {
|
|
28
|
+
return crypto_1.default.createHash('sha256').update(value).digest('hex');
|
|
29
|
+
}
|
|
30
|
+
function writeJson(file, value) {
|
|
31
|
+
fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
|
|
32
|
+
fs_1.default.writeFileSync(file, JSON.stringify(value, null, 2));
|
|
33
|
+
}
|
|
34
|
+
function assertProcess(result, description) {
|
|
35
|
+
if (result.error || result.status !== 0 || result.signal !== null) {
|
|
36
|
+
throw new Error(`${description} failed: status=${String(result.status)} signal=${String(result.signal)} error=${result.error?.message ?? 'none'}\n${result.stderr}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function command(cwd, executable, args, env = process.env) {
|
|
40
|
+
return (0, child_process_1.spawnSync)(executable, args, { cwd, encoding: 'utf8', env, maxBuffer: 64 * 1024 * 1024 });
|
|
41
|
+
}
|
|
42
|
+
function installArtifacts(root) {
|
|
43
|
+
const artifacts = path_1.default.join(root, 'artifacts');
|
|
44
|
+
fs_1.default.mkdirSync(artifacts, { recursive: true });
|
|
45
|
+
writeJson(path_1.default.join(artifacts, 'package.json'), { private: true, name: 'published-sensor-gate-artifacts' });
|
|
46
|
+
assertProcess(command(artifacts, 'npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund', `agentic-workflow-manager@${cliVersion}`]), `npm install agentic-workflow-manager@${cliVersion}`);
|
|
47
|
+
const registryRoot = path_1.default.join(root, 'registry');
|
|
48
|
+
assertProcess(command(root, 'git', ['clone', '--depth', '1', '--branch', registryTag, registryRemote, registryRoot]), `git clone ${registryTag}`);
|
|
49
|
+
const actualVersion = JSON.parse(fs_1.default.readFileSync(path_1.default.join(artifacts, 'node_modules', 'agentic-workflow-manager', 'package.json'), 'utf8')).version;
|
|
50
|
+
if (actualVersion !== cliVersion)
|
|
51
|
+
throw new Error(`npm resolved CLI ${String(actualVersion)} instead of immutable ${cliVersion}`);
|
|
52
|
+
const actualTag = command(registryRoot, 'git', ['describe', '--exact-match', '--tags', 'HEAD']);
|
|
53
|
+
assertProcess(actualTag, `verify registry tag ${registryTag}`);
|
|
54
|
+
if (actualTag.stdout.trim() !== registryTag)
|
|
55
|
+
throw new Error(`registry checkout resolved ${actualTag.stdout.trim()} instead of ${registryTag}`);
|
|
56
|
+
return { cliRoot: path_1.default.join(artifacts, 'node_modules', 'agentic-workflow-manager'), registryRoot };
|
|
57
|
+
}
|
|
58
|
+
function createFixture(root, registryRoot, name) {
|
|
59
|
+
const fixtureRoot = path_1.default.join(root, name);
|
|
60
|
+
const project = path_1.default.join(fixtureRoot, 'project');
|
|
61
|
+
const awmHome = path_1.default.join(fixtureRoot, 'awm-home');
|
|
62
|
+
fs_1.default.mkdirSync(project, { recursive: true });
|
|
63
|
+
fs_1.default.mkdirSync(path_1.default.join(awmHome, 'registries'), { recursive: true });
|
|
64
|
+
fs_1.default.cpSync(registryRoot, path_1.default.join(awmHome, 'registries', 'baseline'), { recursive: true });
|
|
65
|
+
writeJson(path_1.default.join(awmHome, 'registries.json'), [{ name: 'baseline', remote: registryRemote }]);
|
|
66
|
+
writeJson(path_1.default.join(project, 'package.json'), { name: `published-${name}`, private: true });
|
|
67
|
+
// The real v3 pack requires an ESLint 8 eslintrc project. These dependencies
|
|
68
|
+
// belong to the isolated fixture, never to the CLI under test.
|
|
69
|
+
assertProcess(command(project, 'npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund', '--save-dev', 'eslint@8.57.1']), `install ESLint fixture for ${name}`);
|
|
70
|
+
fs_1.default.writeFileSync(path_1.default.join(project, '.eslintrc.js'), 'module.exports = { env: { node: true, es2022: true } };\n');
|
|
71
|
+
fs_1.default.writeFileSync(path_1.default.join(project, 'clean.js'), 'const answer = 42;\nconsole.log(answer);\n');
|
|
72
|
+
return { root: fixtureRoot, project, awmHome };
|
|
73
|
+
}
|
|
74
|
+
function parseJson(result, description) {
|
|
75
|
+
if (!result.stdout.trim())
|
|
76
|
+
throw new Error(`${description} emitted no JSON; stderr=${result.stderr}`);
|
|
77
|
+
return JSON.parse(result.stdout);
|
|
78
|
+
}
|
|
79
|
+
function runCli(cliRoot, fixture, ...args) {
|
|
80
|
+
return command(fixture.project, process.execPath, [path_1.default.join(cliRoot, 'dist', 'src', 'index.js'), ...args], {
|
|
81
|
+
...process.env,
|
|
82
|
+
AWM_HOME: fixture.awmHome,
|
|
83
|
+
AWM_NO_UPDATE_CHECK: '1',
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function record(records, name, argv, result, output) {
|
|
87
|
+
records.push({ name, argv, exit: result.status, signal: result.signal, stdoutHash: hash(result.stdout), stderrHash: hash(result.stderr), ...(output ? { output } : {}) });
|
|
88
|
+
}
|
|
89
|
+
function runJson(records, name, cliRoot, fixture, ...args) {
|
|
90
|
+
const result = runCli(cliRoot, fixture, ...args);
|
|
91
|
+
let output;
|
|
92
|
+
try {
|
|
93
|
+
output = parseJson(result, name);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
// A missing JSON envelope is itself release evidence. Persist argv,
|
|
97
|
+
// process status and hashes before surfacing the failed acceptance gate.
|
|
98
|
+
record(records, name, args, result);
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
record(records, name, args, result, output);
|
|
102
|
+
return { result, output };
|
|
103
|
+
}
|
|
104
|
+
function assertExitMatchesVerdict(result, output) {
|
|
105
|
+
expect(result.status).toBe(output.overall === 'pass' ? 0 : 1);
|
|
106
|
+
}
|
|
107
|
+
function writeReport(root, cliRoot, registryRoot, records) {
|
|
108
|
+
const target = reportFile ? path_1.default.resolve(reportFile) : path_1.default.join(root, 'published-sensor-gate-matrix.json');
|
|
109
|
+
writeJson(target, {
|
|
110
|
+
schemaVersion: 1,
|
|
111
|
+
generatedAt: new Date().toISOString(),
|
|
112
|
+
cli: { version: cliVersion, packageHash: hash(fs_1.default.readFileSync(path_1.default.join(cliRoot, 'package.json'), 'utf8')) },
|
|
113
|
+
registry: {
|
|
114
|
+
tag: registryTag,
|
|
115
|
+
commit: command(registryRoot, 'git', ['rev-parse', 'HEAD']).stdout.trim(),
|
|
116
|
+
packHash: hash(fs_1.default.readFileSync(path_1.default.join(registryRoot, 'sensor-packs', 'js-ts', 'pack.json'), 'utf8')),
|
|
117
|
+
},
|
|
118
|
+
commands: records,
|
|
119
|
+
});
|
|
120
|
+
process.stdout.write(`published sensor gate matrix: ${target}\n`);
|
|
121
|
+
}
|
|
122
|
+
acceptance('published sensor gate matrix', () => {
|
|
123
|
+
jest.setTimeout(10 * 60_000);
|
|
124
|
+
test('records real legacy, v2, changed, baseline, status and preflight evidence from immutable releases', () => {
|
|
125
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-published-sensor-gates-'));
|
|
126
|
+
const records = [];
|
|
127
|
+
let artifacts;
|
|
128
|
+
try {
|
|
129
|
+
artifacts = installArtifacts(root);
|
|
130
|
+
const { cliRoot, registryRoot } = artifacts;
|
|
131
|
+
expect(fs_1.default.existsSync(path_1.default.join(cliRoot, 'dist', 'src', 'index.js'))).toBe(true);
|
|
132
|
+
expect(fs_1.default.existsSync(path_1.default.join(registryRoot, 'sensor-packs', 'js-ts', 'pack.json'))).toBe(true);
|
|
133
|
+
const v2 = createFixture(root, registryRoot, 'v2');
|
|
134
|
+
const initialized = runCli(cliRoot, v2, 'sensors', 'init', '--registry-root', registryRoot, '--pack', 'js-ts');
|
|
135
|
+
record(records, 'v2-init', ['sensors', 'init', '--registry-root', '<registry-root>', '--pack', 'js-ts'], initialized);
|
|
136
|
+
assertProcess(initialized, 'v2 init');
|
|
137
|
+
const status = runCli(cliRoot, v2, 'sensors', 'status');
|
|
138
|
+
record(records, 'v2-status', ['sensors', 'status'], status);
|
|
139
|
+
expect(status.status).toBe(0);
|
|
140
|
+
const preflight = runJson(records, 'v2-preflight', cliRoot, v2, 'preflight', '--verify-sensors', '--json');
|
|
141
|
+
expect(['ready', 'degraded', 'not_configured']).toContain(preflight.output.status);
|
|
142
|
+
expect(preflight.output.status).toBe('degraded');
|
|
143
|
+
expect(preflight.result.status).toBe(1);
|
|
144
|
+
const clean = runJson(records, 'v2-clean-fast', cliRoot, v2, 'sensors', 'run', '--fast');
|
|
145
|
+
assertExitMatchesVerdict(clean.result, clean.output);
|
|
146
|
+
expect(clean.output.sensors).toEqual(expect.arrayContaining([
|
|
147
|
+
expect.objectContaining({ execution: expect.objectContaining({ timeoutSource: 'pack' }) }),
|
|
148
|
+
]));
|
|
149
|
+
const v2ManifestPath = path_1.default.join(v2.project, '.awm', 'sensors.json');
|
|
150
|
+
const projectTimeoutManifest = JSON.parse(fs_1.default.readFileSync(v2ManifestPath, 'utf8'));
|
|
151
|
+
projectTimeoutManifest.sensors.lint.timeout = 30_000;
|
|
152
|
+
writeJson(v2ManifestPath, projectTimeoutManifest);
|
|
153
|
+
const projectTimeout = runJson(records, 'v2-project-timeout-fast', cliRoot, v2, 'sensors', 'run', '--fast');
|
|
154
|
+
assertExitMatchesVerdict(projectTimeout.result, projectTimeout.output);
|
|
155
|
+
expect(projectTimeout.output.sensors).toEqual(expect.arrayContaining([
|
|
156
|
+
expect.objectContaining({ execution: expect.objectContaining({ timeoutMs: 30_000, timeoutSource: 'project' }) }),
|
|
157
|
+
]));
|
|
158
|
+
delete projectTimeoutManifest.sensors.lint.timeout;
|
|
159
|
+
writeJson(v2ManifestPath, projectTimeoutManifest);
|
|
160
|
+
// A v2 manifest can deliberately disable an otherwise applicable
|
|
161
|
+
// sensor. Exercise the published skip verdict before any failing
|
|
162
|
+
// fixture source is introduced, so this is a real all-skipped run
|
|
163
|
+
// rather than a filtered pass or a baseline side effect.
|
|
164
|
+
const disabledManifest = JSON.parse(fs_1.default.readFileSync(v2ManifestPath, 'utf8'));
|
|
165
|
+
const fastSensor = Object.entries(disabledManifest.sensors).find(([, sensor]) => sensor.fast === true);
|
|
166
|
+
expect(fastSensor).toBeDefined();
|
|
167
|
+
if (!fastSensor)
|
|
168
|
+
throw new Error('published v2 js-ts pack has no fast sensor to disable');
|
|
169
|
+
disabledManifest.sensors[fastSensor[0]].enabled = false;
|
|
170
|
+
writeJson(v2ManifestPath, disabledManifest);
|
|
171
|
+
const skipped = runJson(records, 'v2-disabled-fast', cliRoot, v2, 'sensors', 'run', '--fast');
|
|
172
|
+
assertExitMatchesVerdict(skipped.result, skipped.output);
|
|
173
|
+
expect(skipped.output.overall).toBe('skipped');
|
|
174
|
+
disabledManifest.sensors[fastSensor[0]].enabled = true;
|
|
175
|
+
writeJson(v2ManifestPath, disabledManifest);
|
|
176
|
+
const invalidTimeoutManifest = JSON.parse(fs_1.default.readFileSync(v2ManifestPath, 'utf8'));
|
|
177
|
+
invalidTimeoutManifest.sensors.lint.timeout = 0;
|
|
178
|
+
writeJson(v2ManifestPath, invalidTimeoutManifest);
|
|
179
|
+
const invalidTimeout = runCli(cliRoot, v2, 'sensors', 'run', '--fast');
|
|
180
|
+
expect(invalidTimeout.status).toBe(1);
|
|
181
|
+
// Release 8.1.6 does not surface the parser rejection required by
|
|
182
|
+
// the target contract. Record its actual fallback rather than
|
|
183
|
+
// inventing a pre-spawn guarantee the artifact does not provide.
|
|
184
|
+
const invalidTimeoutOutput = parseJson(invalidTimeout, 'v2 invalid timeout');
|
|
185
|
+
record(records, 'v2-invalid-timeout-fast', ['sensors', 'run', '--fast'], invalidTimeout, invalidTimeoutOutput);
|
|
186
|
+
expect(invalidTimeoutOutput.overall).toBe('not_certified');
|
|
187
|
+
delete invalidTimeoutManifest.sensors.lint.timeout;
|
|
188
|
+
writeJson(v2ManifestPath, invalidTimeoutManifest);
|
|
189
|
+
fs_1.default.writeFileSync(path_1.default.join(v2.project, 'failure.js'), 'missingPublishedSensorGateValue;\n');
|
|
190
|
+
const failing = runJson(records, 'v2-failing-fast', cliRoot, v2, 'sensors', 'run', '--fast');
|
|
191
|
+
assertExitMatchesVerdict(failing.result, failing.output);
|
|
192
|
+
expect(failing.output.overall).toBe('fail');
|
|
193
|
+
const baseline = runCli(cliRoot, v2, 'sensors', 'baseline');
|
|
194
|
+
record(records, 'v2-baseline', ['sensors', 'baseline'], baseline);
|
|
195
|
+
assertProcess(baseline, 'v2 baseline');
|
|
196
|
+
const baselined = runJson(records, 'v2-baselined-fast', cliRoot, v2, 'sensors', 'run', '--fast');
|
|
197
|
+
assertExitMatchesVerdict(baselined.result, baselined.output);
|
|
198
|
+
expect(baselined.output.overall).toBe('pass');
|
|
199
|
+
assertProcess(command(v2.project, 'git', ['init']), 'git init changed fixture');
|
|
200
|
+
assertProcess(command(v2.project, 'git', ['config', 'user.email', 'acceptance@example.invalid']), 'git user email');
|
|
201
|
+
assertProcess(command(v2.project, 'git', ['config', 'user.name', 'Published acceptance']), 'git user name');
|
|
202
|
+
assertProcess(command(v2.project, 'git', ['add', '.']), 'git add changed fixture');
|
|
203
|
+
assertProcess(command(v2.project, 'git', ['commit', '-m', 'baseline']), 'git commit changed fixture');
|
|
204
|
+
fs_1.default.writeFileSync(path_1.default.join(v2.project, 'clean.js'), 'const changed = 1;\nconsole.log(changed);\n');
|
|
205
|
+
const changed = runJson(records, 'v2-changed-fast', cliRoot, v2, 'sensors', 'run', '--fast', '--changed');
|
|
206
|
+
assertExitMatchesVerdict(changed.result, changed.output);
|
|
207
|
+
expect(changed.output).toMatchObject({ overall: 'pass', changedScope: { files: 1 } });
|
|
208
|
+
const legacy = createFixture(root, registryRoot, 'legacy');
|
|
209
|
+
fs_1.default.mkdirSync(path_1.default.join(legacy.project, '.awm'), { recursive: true });
|
|
210
|
+
writeJson(path_1.default.join(legacy.project, '.awm', 'sensors.json'), { pack: 'js-ts', sensors: { lint: { fast: true, cmd: 'npx eslint . --format json' } } });
|
|
211
|
+
const legacyRun = runJson(records, 'legacy-fast', cliRoot, legacy, 'sensors', 'run', '--fast');
|
|
212
|
+
assertExitMatchesVerdict(legacyRun.result, legacyRun.output);
|
|
213
|
+
expect(legacyRun.output.sensors).toEqual(expect.arrayContaining([
|
|
214
|
+
expect.objectContaining({ status: 'pass', execution: expect.objectContaining({ timeoutSource: 'fallback' }) }),
|
|
215
|
+
]));
|
|
216
|
+
expect(legacyRun.output.overall).toBe('not_certified');
|
|
217
|
+
const outcomes = new Map(records.flatMap(record => record.output?.overall ? [[String(record.output.overall), record.exit]] : []));
|
|
218
|
+
// This is an explicit observation table for the immutable release:
|
|
219
|
+
// only pass is process-success; every non-pass is process-failure.
|
|
220
|
+
expect([...outcomes.entries()].sort()).toEqual([
|
|
221
|
+
['fail', 1], ['not_certified', 1], ['pass', 0], ['skipped', 1],
|
|
222
|
+
]);
|
|
223
|
+
writeReport(root, cliRoot, registryRoot, records);
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
// Do not turn a release mismatch into a synthetic success. When an
|
|
227
|
+
// artifact is available, leave the exact partial trace for review.
|
|
228
|
+
if (artifacts)
|
|
229
|
+
writeReport(root, artifacts.cliRoot, artifacts.registryRoot, records);
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
if (!reportFile)
|
|
234
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
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 path_1 = __importDefault(require("path"));
|
|
8
|
+
const acceptancePath = path_1.default.resolve(__dirname, '../../../docs/research/sensor-gate-honesty/published-acceptance.json');
|
|
9
|
+
const reportPath = path_1.default.resolve(__dirname, '../../../docs/research/sensor-gate-honesty/published-sensor-gate-matrix.json');
|
|
10
|
+
describe('published sensor gate acceptance artifact', () => {
|
|
11
|
+
it('contains only complete, portable publication identities and evidence references', () => {
|
|
12
|
+
const artifact = JSON.parse(fs_1.default.readFileSync(acceptancePath, 'utf8'));
|
|
13
|
+
const report = JSON.parse(fs_1.default.readFileSync(reportPath, 'utf8'));
|
|
14
|
+
expect(artifact.schemaVersion).toBe(1);
|
|
15
|
+
expect(artifact.issues).toEqual([95, 96, 97, 98]);
|
|
16
|
+
expect(artifact.verdict).toBe('partial');
|
|
17
|
+
expect(artifact.cli.version).toMatch(/^\d+\.\d+\.\d+$/);
|
|
18
|
+
expect(artifact.cli.packageHash).toMatch(/^[0-9a-f]{64}$/);
|
|
19
|
+
expect(artifact.registry.tag).toMatch(/^v\d+\.\d+\.\d+$/);
|
|
20
|
+
expect(artifact.registry.commit).toMatch(/^[0-9a-f]{40}$/);
|
|
21
|
+
expect(artifact.registry.packHash).toMatch(/^[0-9a-f]{64}$/);
|
|
22
|
+
expect(artifact.matrix.report).toBe('published-sensor-gate-matrix.json');
|
|
23
|
+
expect(artifact.matrix.commandCount).toBe(12);
|
|
24
|
+
expect(report.commands).toHaveLength(artifact.matrix.commandCount);
|
|
25
|
+
expect(report.cli.version).toBe(artifact.cli.version);
|
|
26
|
+
expect(report.registry).toMatchObject(artifact.registry);
|
|
27
|
+
expect(JSON.stringify(report)).not.toMatch(/\/(tmp|srv)\//);
|
|
28
|
+
expect(Object.keys(artifact.platforms).sort()).toEqual(['linux', 'macos', 'windows']);
|
|
29
|
+
for (const platform of Object.values(artifact.platforms)) {
|
|
30
|
+
expect(platform.status).toBe('pass');
|
|
31
|
+
expect(platform.evidence).toMatch(/^https:\/\//);
|
|
32
|
+
}
|
|
33
|
+
expect(fs_1.default.readFileSync(acceptancePath, 'utf8').endsWith('\n')).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-workflow-manager",
|
|
3
|
-
"version": "8.1
|
|
3
|
+
"version": "8.2.1",
|
|
4
4
|
"main": "dist/src/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"awm": "./dist/src/index.js"
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"@types/node": "^25.3.0",
|
|
57
57
|
"@types/semver": "^7.7.1",
|
|
58
58
|
"dependency-cruiser": "^17.4.3",
|
|
59
|
-
"eslint": "
|
|
59
|
+
"eslint": "10.8.1",
|
|
60
60
|
"jest": "^30.2.0",
|
|
61
61
|
"js-yaml": "4.1.0",
|
|
62
62
|
"ts-jest": "^29.4.6",
|