agentic-workflow-manager 8.1.5 → 8.2.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/manifest.js +3 -1
- package/dist/src/commands/sensors/compatibility/materialize.js +6 -2
- package/dist/src/commands/sensors/index.js +2 -1
- package/dist/src/commands/sensors/init.js +8 -3
- package/dist/src/commands/sensors/prepare.js +5 -1
- package/dist/src/commands/sensors/run.js +10 -2
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +12 -0
- package/dist/tests/commands/sensors/exec.test.js +19 -0
- package/dist/tests/commands/sensors/init.test.js +53 -0
- package/dist/tests/commands/sensors/prepare.test.js +2 -2
- package/dist/tests/commands/sensors/run.test.js +27 -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 +1 -1
|
@@ -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);
|
|
@@ -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 } : {}) };
|
|
@@ -42,7 +42,11 @@ function expandFileInput(command, files) {
|
|
|
42
42
|
if (index < 0 || command.args.lastIndexOf(command.fileInput.placeholder) !== index) {
|
|
43
43
|
throw new Error('changed command requires exactly one standalone {files} argument');
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
// `fileInput` describes the unexpanded registry template. Leaving it on the
|
|
46
|
+
// materialized command makes the execution boundary (correctly) demand a
|
|
47
|
+
// placeholder that has already been replaced with literal argv entries.
|
|
48
|
+
const { fileInput: _templateInput, ...materialized } = command;
|
|
49
|
+
return { ...materialized, args: [...command.args.slice(0, index), ...files, ...command.args.slice(index + 1)] };
|
|
46
50
|
}
|
|
47
51
|
function timeout(project, pack, fast) {
|
|
48
52
|
const resolved = (0, timeout_1.resolveTimeout)({ project, pack, fast });
|
|
@@ -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);
|
|
@@ -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'],
|
|
@@ -8,6 +8,7 @@ const os_1 = __importDefault(require("os"));
|
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const exec_1 = require("../../../src/commands/sensors/exec");
|
|
10
10
|
const result_1 = require("../../../src/commands/sensors/result");
|
|
11
|
+
const prepare_1 = require("../../../src/commands/sensors/prepare");
|
|
11
12
|
const sensor = (overrides = {}) => ({
|
|
12
13
|
name: 'lint',
|
|
13
14
|
command: { kind: 'legacy', value: 'node -e "setTimeout(() => {}, 1000)"' },
|
|
@@ -148,6 +149,24 @@ describe('runCommand — spawn failure', () => {
|
|
|
148
149
|
});
|
|
149
150
|
});
|
|
150
151
|
describe('runStructuredCommand — public boundary validation', () => {
|
|
152
|
+
it('dispatches argv materialized from a validated changed-command template without requiring its placeholder again', async () => {
|
|
153
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-exec-changed-argv-'));
|
|
154
|
+
const received = path_1.default.join(dir, 'received.json');
|
|
155
|
+
try {
|
|
156
|
+
const command = (0, prepare_1.expandFileInput)({
|
|
157
|
+
executable: 'node',
|
|
158
|
+
resolution: 'path',
|
|
159
|
+
args: ['-e', "require('fs').writeFileSync(process.argv[1], JSON.stringify(process.argv.slice(2)))", received, '{files}'],
|
|
160
|
+
fileInput: { placeholder: '{files}', extensions: ['.ts'] },
|
|
161
|
+
}, ['src/a.ts', 'src/with space.ts']);
|
|
162
|
+
const result = await (0, exec_1.runStructuredCommand)(command, { timeout: 5_000, cwd: dir });
|
|
163
|
+
expect(result.code).toBe(0);
|
|
164
|
+
expect(JSON.parse(fs_1.default.readFileSync(received, 'utf8'))).toEqual(['src/a.ts', 'src/with space.ts']);
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
151
170
|
it.each(['sh', 'cmd.exe', '/usr/bin/node', 'nested/tool', 'nested\\tool'])('rejects unsafe executable %j before resolution', executable => {
|
|
152
171
|
expect(() => (0, exec_1.runStructuredCommand)({ executable, resolution: 'path', args: ['--version'] }, { timeout: 5000, cwd: process.cwd() }))
|
|
153
172
|
.toThrow(/safe executable name|shell/i);
|
|
@@ -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;
|
|
@@ -30,7 +30,7 @@ function v2Input(overrides = {}) {
|
|
|
30
30
|
describe('prepareV2Sensor', () => {
|
|
31
31
|
test('v2 uses the live command and project > pack > fallback timeout (R1.1, R3.1)', () => {
|
|
32
32
|
const prepared = (0, prepare_1.prepareV2Sensor)(v2Input());
|
|
33
|
-
expect(prepared.command).toEqual({ kind: 'structured', value: {
|
|
33
|
+
expect(prepared.command).toEqual({ kind: 'structured', value: { executable: 'live-eslint', resolution: 'path', args: ['--format', 'json', 'src/a.ts'] } });
|
|
34
34
|
expect(prepared.timeoutMs).toBe(90_000);
|
|
35
35
|
expect(prepared.timeoutSource).toBe('project');
|
|
36
36
|
expect((0, prepare_1.prepareV2Sensor)(v2Input({ projectTimeout: undefined })).timeoutSource).toBe('pack');
|
|
@@ -42,7 +42,7 @@ describe('prepareV2Sensor', () => {
|
|
|
42
42
|
});
|
|
43
43
|
test('expands changed paths as literal argv entries (R4.1, R10.2)', () => {
|
|
44
44
|
const prepared = (0, prepare_1.prepareV2Sensor)(v2Input({ changed: { files: ['src/a b.ts', 'src/$x.ts'] } }));
|
|
45
|
-
expect(prepared.command).toEqual({ kind: 'structured', value: {
|
|
45
|
+
expect(prepared.command).toEqual({ kind: 'structured', value: { executable: 'live-eslint', resolution: 'path', args: ['--format', 'json', 'src/a b.ts', 'src/$x.ts'] } });
|
|
46
46
|
expect(prepared.effectiveScope).toBe('changed');
|
|
47
47
|
});
|
|
48
48
|
test('falls back full with an explicit reason without changedCommand (R4.2)', () => {
|
|
@@ -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;
|
|
@@ -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
|
+
});
|