agentic-workflow-manager 9.16.0 → 9.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/preflight/checks.js +63 -13
- package/dist/src/commands/sensors/bootstrap.js +2 -1
- package/dist/src/commands/sensors/compatibility/contract.js +30 -26
- package/dist/src/commands/sensors/compatibility/discovery.js +21 -0
- package/dist/src/commands/sensors/compatibility/manifest.js +169 -17
- package/dist/src/commands/sensors/compatibility/materialize.js +30 -5
- package/dist/src/commands/sensors/compatibility/source.js +3 -2
- package/dist/src/commands/sensors/coverage/render.js +8 -2
- package/dist/src/commands/sensors/coverage/resolve.js +7 -0
- package/dist/src/commands/sensors/init.js +15 -2
- package/dist/src/commands/sensors/migrate.js +13 -10
- package/dist/src/commands/sensors/prepare.js +53 -6
- package/dist/src/commands/sensors/project.js +60 -6
- package/dist/src/commands/sensors/result.js +18 -3
- package/dist/src/commands/sensors/run.js +124 -16
- package/dist/src/commands/sensors/status.js +163 -18
- package/dist/src/core/init/steps.js +5 -1
- package/dist/src/core/journal/process.js +22 -8
- package/dist/tests/commands/preflight/preflight.test.js +121 -0
- package/dist/tests/commands/sensors/bootstrap.test.js +10 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +204 -0
- package/dist/tests/commands/sensors/init.test.js +107 -0
- package/dist/tests/commands/sensors/migrate.test.js +4 -3
- package/dist/tests/commands/sensors/partial-pack-bootstrap.test.js +1 -1
- package/dist/tests/commands/sensors/prepare.test.js +62 -0
- package/dist/tests/commands/sensors/result.test.js +66 -0
- package/dist/tests/commands/sensors/run.test.js +331 -0
- package/dist/tests/commands/sensors/status.test.js +204 -0
- package/dist/tests/core/journal/process.test.js +33 -10
- package/dist/tests/structural/native-release-artifacts.test.js +4 -2
- package/dist/tests/structural/project-declared-sensors-docs-contract.test.js +104 -0
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -14,6 +14,7 @@ const resolve_1 = require("./compatibility/resolve");
|
|
|
14
14
|
const project_1 = require("./project");
|
|
15
15
|
const source_1 = require("./compatibility/source");
|
|
16
16
|
const registries_1 = require("../../core/registries");
|
|
17
|
+
const manifest_1 = require("./compatibility/manifest");
|
|
17
18
|
/** First non-flag token after `npx` — the tool the command actually runs. */
|
|
18
19
|
function npxTool(parts) {
|
|
19
20
|
for (let i = 1; i < parts.length; i++) {
|
|
@@ -197,6 +198,90 @@ function invalidStatus(project, pack, reason = 'manifest-malformed', remedy = 'r
|
|
|
197
198
|
...projectMetadata(project, 'invalid', reason, { remedy }),
|
|
198
199
|
};
|
|
199
200
|
}
|
|
201
|
+
function hasSelectedPack(manifest) {
|
|
202
|
+
return typeof manifest.pack === 'string' && manifest.pack.length > 0;
|
|
203
|
+
}
|
|
204
|
+
function projectSensorCheck(sensor, cwd) {
|
|
205
|
+
if (sensor.enabled === false) {
|
|
206
|
+
return { ok: true, detail: 'disabled (project-declared)', certification: 'project-declared' };
|
|
207
|
+
}
|
|
208
|
+
const check = checkStructuredCommand(sensor.command, cwd, sensor.assets);
|
|
209
|
+
return {
|
|
210
|
+
ok: check.ok,
|
|
211
|
+
detail: check.ok ? `project-declared: ${check.detail}` : `${check.detail} (project-declared)`,
|
|
212
|
+
certification: 'project-declared',
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function packBoundOnly(manifest) {
|
|
216
|
+
const sensors = Object.fromEntries(Object.entries(manifest.sensors).filter(([, sensor]) => !(0, manifest_1.isProjectDeclaredSensor)(sensor)));
|
|
217
|
+
const pack = 'pack' in manifest && typeof manifest.pack === 'string' ? manifest.pack : '';
|
|
218
|
+
return { schemaVersion: 2, pack, sensors };
|
|
219
|
+
}
|
|
220
|
+
/** Live pack sensor ids, or null when the pack cannot be resolved (fail closed). */
|
|
221
|
+
function livePackSensorIds(source) {
|
|
222
|
+
try {
|
|
223
|
+
const parsed = (0, contract_1.parseSensorPack)(JSON.parse(source.content), source.path);
|
|
224
|
+
if (parsed.kind !== 'v2')
|
|
225
|
+
return null;
|
|
226
|
+
return new Set(Object.keys(parsed.pack.sensors));
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* RF-1.7 status: never label a colliding project id as project-declared READY.
|
|
234
|
+
* When pack is selected and live is unavailable, fail closed rather than claiming
|
|
235
|
+
* the project override is safe.
|
|
236
|
+
*/
|
|
237
|
+
function projectDeclaredOrCollisionCheck(name, sensor, packageRoot, packSelected, liveIds) {
|
|
238
|
+
if (packSelected && (liveIds === null || liveIds.has(name))) {
|
|
239
|
+
return {
|
|
240
|
+
ok: false,
|
|
241
|
+
detail: liveIds === null
|
|
242
|
+
? `project-sensor-name-collision: ${name} (live pack unavailable)`
|
|
243
|
+
: `project-sensor-name-collision: ${name}; pack sensor kept`,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return projectSensorCheck(sensor, packageRoot);
|
|
247
|
+
}
|
|
248
|
+
/** Soft-isolate valid siblings into checks while authority remains invalid (RF soft-parse). */
|
|
249
|
+
function softIsolatedChecks(project) {
|
|
250
|
+
const recovered = (0, project_1.softRecoverInvalidProject)(project);
|
|
251
|
+
if (!recovered)
|
|
252
|
+
return {};
|
|
253
|
+
const checks = {};
|
|
254
|
+
const v3Project = recovered.manifest.kind === 'v3' && recovered.manifest.pack.mode === 'project-sensors'
|
|
255
|
+
? recovered.manifest.pack
|
|
256
|
+
: null;
|
|
257
|
+
const sensors = v3Project?.sensors ?? {};
|
|
258
|
+
// RF-1.7: same collision path as configured status — live pack probe when pack is selected.
|
|
259
|
+
const packSelected = v3Project !== null && hasSelectedPack(v3Project);
|
|
260
|
+
let liveIds = null;
|
|
261
|
+
if (packSelected) {
|
|
262
|
+
try {
|
|
263
|
+
const resolution = (0, source_1.resolveSensorSource)(recovered.manifest, { registries: (0, registries_1.listRegistries)() });
|
|
264
|
+
liveIds = 'source' in resolution ? livePackSensorIds(resolution.source) : null;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
liveIds = null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
for (const [name, sensor] of Object.entries(sensors)) {
|
|
271
|
+
if ((0, manifest_1.isProjectDeclaredSensor)(sensor)) {
|
|
272
|
+
checks[name] = projectDeclaredOrCollisionCheck(name, sensor, recovered.packageRoot, packSelected, liveIds);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const packSensor = sensor;
|
|
276
|
+
checks[name] = packSensor.enabled === false
|
|
277
|
+
? { ok: true, detail: 'disabled' }
|
|
278
|
+
: checkStructuredCommand(packSensor.command, recovered.packageRoot, packSensor.assets);
|
|
279
|
+
}
|
|
280
|
+
for (const entry of recovered.invalidEntries) {
|
|
281
|
+
checks[entry.name] = { ok: false, detail: entry.reason };
|
|
282
|
+
}
|
|
283
|
+
return checks;
|
|
284
|
+
}
|
|
200
285
|
async function computeSensorStatus(cwd = process.cwd()) {
|
|
201
286
|
let project;
|
|
202
287
|
try {
|
|
@@ -211,15 +296,40 @@ async function computeSensorStatus(cwd = process.cwd()) {
|
|
|
211
296
|
}
|
|
212
297
|
if (project.state === 'invalid') {
|
|
213
298
|
let pack = null;
|
|
299
|
+
let reason = 'manifest-malformed';
|
|
300
|
+
let remedy = 'repair-sensor-manifest';
|
|
214
301
|
try {
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
302
|
+
const text = fs_1.default.readFileSync(project.manifestPath, 'utf-8');
|
|
303
|
+
try {
|
|
304
|
+
const raw = JSON.parse(text);
|
|
305
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
306
|
+
const record = raw;
|
|
307
|
+
if (!('schemaVersion' in record) && typeof record.pack === 'string')
|
|
308
|
+
pack = record.pack;
|
|
309
|
+
else if (typeof record.pack === 'string')
|
|
310
|
+
pack = record.pack;
|
|
311
|
+
}
|
|
312
|
+
// JSON.parse succeeded — this is schema invalidity, not "not valid JSON" (RF-4.1).
|
|
313
|
+
if (project.reason.includes('unsupported manifest schemaVersion')) {
|
|
314
|
+
reason = 'schema-unsupported';
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
reason = `schema-invalid: ${project.reason}`;
|
|
318
|
+
// Project-entry schema failures must not lead with `awm sensors init` (RF-3.3).
|
|
319
|
+
remedy = 'repair-the-invalid-sensor-entry-in-sensors-json';
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
reason = 'manifest-malformed';
|
|
218
324
|
}
|
|
219
325
|
}
|
|
220
|
-
catch { /*
|
|
221
|
-
|
|
222
|
-
|
|
326
|
+
catch { /* unreadable manifest stays malformed */ }
|
|
327
|
+
// Soft-isolate valid siblings into checks (same WithIssues path as run) while
|
|
328
|
+
// keeping mode=invalid so preflight RF-4.1 still sees schema-invalid authority.
|
|
329
|
+
return {
|
|
330
|
+
...invalidStatus(project, pack, reason, remedy),
|
|
331
|
+
checks: softIsolatedChecks(project),
|
|
332
|
+
};
|
|
223
333
|
}
|
|
224
334
|
const packageRoot = project.packageRoot;
|
|
225
335
|
const parsed = project.manifest;
|
|
@@ -229,38 +339,73 @@ async function computeSensorStatus(cwd = process.cwd()) {
|
|
|
229
339
|
...projectMetadata(project, parsed.pack.mode, `${parsed.pack.mode}-declared`, { declarationReason: parsed.pack.reason }),
|
|
230
340
|
};
|
|
231
341
|
}
|
|
342
|
+
if (parsed.kind === 'v3' && parsed.pack.mode === 'project-sensors' && !hasSelectedPack(parsed.pack)) {
|
|
343
|
+
// Only-project: no pack source to resolve. Report each project sensor with
|
|
344
|
+
// project-declared provenance rather than an empty Pack:none DEGRADED (RF-2.8).
|
|
345
|
+
const checks = {};
|
|
346
|
+
for (const [name, sensor] of Object.entries(parsed.pack.sensors)) {
|
|
347
|
+
if ((0, manifest_1.isProjectDeclaredSensor)(sensor)) {
|
|
348
|
+
checks[name] = projectSensorCheck(sensor, packageRoot);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
checks[name] = { ok: false, detail: 'pack-bound sensor requires a nonempty pack' };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
overall: Object.keys(checks).length > 0 && Object.values(checks).every(check => check.ok) ? 'READY' : 'DEGRADED',
|
|
356
|
+
pack: null,
|
|
357
|
+
checks,
|
|
358
|
+
...projectMetadata(project, 'project-sensors', 'project-declared-only'),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
232
361
|
if (parsed.kind === 'v2' || parsed.kind === 'v3') {
|
|
233
362
|
const parsedPack = parsed.pack;
|
|
363
|
+
const packId = typeof parsedPack.pack === 'string' ? parsedPack.pack : null;
|
|
364
|
+
const packSelected = typeof parsedPack.pack === 'string' && parsedPack.pack.length > 0;
|
|
234
365
|
let resolution;
|
|
235
366
|
try {
|
|
236
367
|
resolution = (0, source_1.resolveSensorSource)(parsed, { registries: (0, registries_1.listRegistries)() });
|
|
237
368
|
}
|
|
238
369
|
catch {
|
|
239
|
-
return invalidStatus(project,
|
|
370
|
+
return invalidStatus(project, packId, 'sensor-source-invalid', 'repair-or-run-awm-update');
|
|
240
371
|
}
|
|
241
372
|
if (!('source' in resolution))
|
|
242
|
-
return sourceFailure(project,
|
|
373
|
+
return sourceFailure(project, packId ?? 'unknown', resolution);
|
|
243
374
|
const sourceMeta = resolvedSourceMetadata(resolution);
|
|
244
|
-
const
|
|
375
|
+
const liveIds = livePackSensorIds(resolution.source);
|
|
376
|
+
const packManifest = packBoundOnly(parsedPack);
|
|
245
377
|
const checks = {};
|
|
246
378
|
let compatibility;
|
|
247
379
|
try {
|
|
248
|
-
compatibility = resolveStaticV2Compatibility(packageRoot,
|
|
380
|
+
compatibility = resolveStaticV2Compatibility(packageRoot, packManifest, resolution.source);
|
|
249
381
|
}
|
|
250
382
|
catch (error) {
|
|
251
383
|
const detail = error instanceof Error ? error.message : 'live compatibility unavailable';
|
|
252
|
-
for (const [name, sensor] of Object.entries(
|
|
253
|
-
|
|
254
|
-
|
|
384
|
+
for (const [name, sensor] of Object.entries(parsedPack.sensors)) {
|
|
385
|
+
if ((0, manifest_1.isProjectDeclaredSensor)(sensor)) {
|
|
386
|
+
checks[name] = projectDeclaredOrCollisionCheck(name, sensor, packageRoot, packSelected, liveIds);
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
checks[name] = sensor.enabled === false
|
|
390
|
+
? { ok: true, detail: 'disabled' }
|
|
391
|
+
: { ok: false, detail };
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return { overall: 'DEGRADED', pack: packId, checks, ...projectMetadata(project, 'project-sensors', sourceMeta.reason, { ...sourceMeta }) };
|
|
255
395
|
}
|
|
256
|
-
for (const [name, sensor] of Object.entries(
|
|
257
|
-
|
|
258
|
-
|
|
396
|
+
for (const [name, sensor] of Object.entries(parsedPack.sensors)) {
|
|
397
|
+
if ((0, manifest_1.isProjectDeclaredSensor)(sensor)) {
|
|
398
|
+
checks[name] = projectDeclaredOrCollisionCheck(name, sensor, packageRoot, packSelected, liveIds);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const packSensor = sensor;
|
|
402
|
+
checks[name] = packSensor.enabled === false ? { ok: true, detail: 'disabled' }
|
|
403
|
+
: staticCompatibilityCheck(packSensor, compatibility[name]) ?? checkStructuredCommand(packSensor.command, packageRoot, packSensor.assets);
|
|
259
404
|
}
|
|
260
|
-
const uninitialized = uninitializedSensors(
|
|
405
|
+
const uninitialized = uninitializedSensors(packManifest, compatibility);
|
|
261
406
|
return {
|
|
262
407
|
overall: Object.keys(checks).length > 0 && Object.values(checks).every(check => check.ok) ? 'READY' : 'DEGRADED',
|
|
263
|
-
pack:
|
|
408
|
+
pack: packId, checks,
|
|
264
409
|
...(uninitialized ? { uninitialized } : {}),
|
|
265
410
|
...projectMetadata(project, 'project-sensors', sourceMeta.reason, { ...sourceMeta }),
|
|
266
411
|
};
|
|
@@ -75,7 +75,11 @@ exports.defaultActions = {
|
|
|
75
75
|
throw new Error('sensor init does not migrate an existing v2 manifest; run awm sensors bootstrap');
|
|
76
76
|
if (plan.kind === 'create')
|
|
77
77
|
(0, bootstrap_1.applySensorBootstrap)(plan);
|
|
78
|
-
const
|
|
78
|
+
const detected = (0, detection_1.detectStack)(o.cwd).pack;
|
|
79
|
+
const declared = plan.kind === 'create' && plan.manifest.mode === 'project-sensors' ? plan.manifest.pack : undefined;
|
|
80
|
+
const pack = typeof declared === 'string' && declared.length > 0 ? declared : detected;
|
|
81
|
+
if (typeof pack !== 'string' || pack.length === 0)
|
|
82
|
+
throw new Error('sensor init could not resolve a pack id');
|
|
79
83
|
return { detection: { pack }, manifest: { pack } };
|
|
80
84
|
},
|
|
81
85
|
addExtension: (root, name) => { (0, profile_1.addExtension)(root, name); },
|
|
@@ -470,7 +470,10 @@ function descendantActivity(psTable, rootPid) {
|
|
|
470
470
|
const children = new Map();
|
|
471
471
|
let rows = 0;
|
|
472
472
|
for (const line of psTable.split('\n')) {
|
|
473
|
-
|
|
473
|
+
// time may be empty on some Darwin samples for a just-spawned process;
|
|
474
|
+
// treat blank as "0" so the pid still joins the descendant tree (otherwise
|
|
475
|
+
// a healthy detached grandchild is invisible and activity reads as frozen).
|
|
476
|
+
const match = /^\s*(\d+)\s+(\d+)(?:\s+(\S+))?\s*$/.exec(line);
|
|
474
477
|
if (match === null)
|
|
475
478
|
continue;
|
|
476
479
|
if (++rows > MAX_PS_ROWS)
|
|
@@ -478,7 +481,7 @@ function descendantActivity(psTable, rootPid) {
|
|
|
478
481
|
const pid = Number(match[1]);
|
|
479
482
|
const ppid = Number(match[2]);
|
|
480
483
|
const siblings = children.get(ppid) ?? [];
|
|
481
|
-
siblings.push({ pid, time: match[3] });
|
|
484
|
+
siblings.push({ pid, time: match[3] && match[3].length > 0 ? match[3] : '0' });
|
|
482
485
|
children.set(ppid, siblings);
|
|
483
486
|
}
|
|
484
487
|
const seen = new Set([rootPid]);
|
|
@@ -499,13 +502,24 @@ function descendantActivity(psTable, rootPid) {
|
|
|
499
502
|
return `${found.length}:${crypto_1.default.createHash('sha256').update(found.join('\n')).digest('hex')}`;
|
|
500
503
|
}
|
|
501
504
|
function observeDescendants(rootPid) {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
505
|
+
// One transient `ps` failure under CI load used to freeze the snapshot at
|
|
506
|
+
// `unknown`, which made the Darwin integration test (and a real supervisor
|
|
507
|
+
// tick) treat a later concrete `0:none` as "activity changed" without ever
|
|
508
|
+
// seeing the grandchild. Retry once before degrading.
|
|
509
|
+
let lastError;
|
|
510
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
511
|
+
try {
|
|
512
|
+
const table = (0, child_process_1.execFileSync)('ps', ['-A', '-o', 'pid=,ppid=,time='], {
|
|
513
|
+
encoding: 'utf8', stdio: exports.EXEC_STDIO, timeout: PS_TIMEOUT_MS, maxBuffer: 16 * 1024 * 1024,
|
|
514
|
+
});
|
|
515
|
+
return descendantActivity(table, rootPid);
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
lastError = error;
|
|
519
|
+
}
|
|
508
520
|
}
|
|
521
|
+
void lastError;
|
|
522
|
+
return 'unknown';
|
|
509
523
|
}
|
|
510
524
|
/** Llamada UNA VEZ POR TICK por el supervisor (ver superviseController en
|
|
511
525
|
* commands/watch/supervisor.ts) mientras un controlador este activo — el
|
|
@@ -397,6 +397,19 @@ describe('preflight', () => {
|
|
|
397
397
|
expect(execution.remedy).toContain('awm sensors init');
|
|
398
398
|
expect(execution.remedy).not.toContain('named sensor');
|
|
399
399
|
});
|
|
400
|
+
it('does not treat overall=pass as unconditional green when invalidEntries are present', async () => {
|
|
401
|
+
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
402
|
+
mockRunSensors.mockResolvedValue({
|
|
403
|
+
overall: 'pass',
|
|
404
|
+
sensors: [{ name: 'good', status: 'pass', errors: [] }],
|
|
405
|
+
invalidEntries: [{ name: 'broken', reason: 'sensors.broken.formatter is not a registered formatter id' }],
|
|
406
|
+
});
|
|
407
|
+
const report = await (0, checks_1.preflight)(dir, { verifySensors: true });
|
|
408
|
+
const execution = check(report, 'sensors-execution');
|
|
409
|
+
expect(execution.ok).toBe(false);
|
|
410
|
+
expect(execution.detail).toMatch(/invalidEntries|broken/i);
|
|
411
|
+
expect(execution.detail).not.toBe('all selected sensors completed with pass');
|
|
412
|
+
});
|
|
400
413
|
it('reports not_configured when no sensor manifest exists', async () => {
|
|
401
414
|
// The team-rollout case: a developer clones the repo and never runs
|
|
402
415
|
// `awm sensors init`. Today nothing notices until an unattended run is already
|
|
@@ -813,4 +826,112 @@ describe('preflight', () => {
|
|
|
813
826
|
expect(check(report, 'manifest').detail).not.toContain('no .awm/sensors.json');
|
|
814
827
|
expect(check(report, 'tools')).toBeUndefined();
|
|
815
828
|
});
|
|
829
|
+
describe('project-declared diagnostics (S3)', () => {
|
|
830
|
+
it('does not claim not-valid-JSON when JSON parses but a project field fails schema (RF-4.1/4.2)', async () => {
|
|
831
|
+
const dir = make({
|
|
832
|
+
manifest: {
|
|
833
|
+
schemaVersion: 3,
|
|
834
|
+
mode: 'project-sensors',
|
|
835
|
+
sensors: {
|
|
836
|
+
broken: {
|
|
837
|
+
source: 'project',
|
|
838
|
+
enabled: true,
|
|
839
|
+
command: { executable: 'terraform', resolution: 'path', args: ['fmt', '-check'] },
|
|
840
|
+
formatter: 'not-a-formatter',
|
|
841
|
+
},
|
|
842
|
+
},
|
|
843
|
+
},
|
|
844
|
+
});
|
|
845
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
846
|
+
const manifestCheck = check(report, 'manifest');
|
|
847
|
+
expect(manifestCheck.ok).toBe(false);
|
|
848
|
+
expect(manifestCheck.detail).not.toBe('.awm/sensors.json is not valid JSON');
|
|
849
|
+
expect(manifestCheck.detail).not.toMatch(/not valid JSON/i);
|
|
850
|
+
expect(manifestCheck.detail).toMatch(/broken/);
|
|
851
|
+
expect(manifestCheck.detail).toMatch(/formatter/);
|
|
852
|
+
expect(report.reason).toMatch(/schema-invalid/i);
|
|
853
|
+
});
|
|
854
|
+
it('still reports not-valid-JSON for truly broken JSON (RF-4.1)', async () => {
|
|
855
|
+
const dir = make({ manifest: '{ not json' });
|
|
856
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
857
|
+
expect(check(report, 'manifest').detail).toContain('not valid JSON');
|
|
858
|
+
});
|
|
859
|
+
it('does not recommend awm sensors init as the first remedy for project schema invalidity (RF-3.3)', async () => {
|
|
860
|
+
const dir = make({
|
|
861
|
+
manifest: {
|
|
862
|
+
schemaVersion: 3,
|
|
863
|
+
mode: 'project-sensors',
|
|
864
|
+
sensors: {
|
|
865
|
+
broken: {
|
|
866
|
+
source: 'project',
|
|
867
|
+
enabled: true,
|
|
868
|
+
command: { executable: 'terraform', resolution: 'path', args: ['fmt'] },
|
|
869
|
+
formatter: 'not-a-formatter',
|
|
870
|
+
},
|
|
871
|
+
},
|
|
872
|
+
},
|
|
873
|
+
});
|
|
874
|
+
const remedy = check(await (0, checks_1.preflight)(dir), 'manifest').remedy ?? '';
|
|
875
|
+
const firstLine = remedy.split('\n')[0] ?? remedy;
|
|
876
|
+
expect(firstLine).not.toMatch(/awm sensors init/i);
|
|
877
|
+
// RF-3.3 positive: first remedy names repair of the entry/field, not regen.
|
|
878
|
+
expect(firstLine).toMatch(/repair|entry|field/i);
|
|
879
|
+
});
|
|
880
|
+
it('uses an only-project-honest tools remedy when sensors are empty and pack is null', async () => {
|
|
881
|
+
const dir = make({
|
|
882
|
+
manifest: {
|
|
883
|
+
schemaVersion: 3,
|
|
884
|
+
mode: 'project-sensors',
|
|
885
|
+
sensors: {},
|
|
886
|
+
},
|
|
887
|
+
});
|
|
888
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
889
|
+
const tools = check(report, 'tools');
|
|
890
|
+
expect(tools.ok).toBe(false);
|
|
891
|
+
expect(tools.remedy ?? '').not.toMatch(/['"]null['"]/);
|
|
892
|
+
expect(tools.remedy ?? '').not.toMatch(/awm update/i);
|
|
893
|
+
expect(tools.remedy ?? '').not.toMatch(/registry has no pack/i);
|
|
894
|
+
expect(tools.remedy ?? '').toMatch(/source:"project"|declare a pack/i);
|
|
895
|
+
});
|
|
896
|
+
it('accepts only-project READY status without inventing a pack id (RF-2.8)', async () => {
|
|
897
|
+
const pathDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-preflight-only-project-path-'));
|
|
898
|
+
const previousPath = process.env.PATH;
|
|
899
|
+
try {
|
|
900
|
+
fs_1.default.writeFileSync(path_1.default.join(pathDir, 'terraform'), '#!/bin/sh\nexit 0\n');
|
|
901
|
+
fs_1.default.chmodSync(path_1.default.join(pathDir, 'terraform'), 0o755);
|
|
902
|
+
process.env.PATH = pathDir;
|
|
903
|
+
const dir = make({
|
|
904
|
+
manifest: {
|
|
905
|
+
schemaVersion: 3,
|
|
906
|
+
mode: 'project-sensors',
|
|
907
|
+
sensors: {
|
|
908
|
+
'iac-format': {
|
|
909
|
+
source: 'project',
|
|
910
|
+
enabled: true,
|
|
911
|
+
command: { executable: 'terraform', resolution: 'path', args: ['fmt', '-check'] },
|
|
912
|
+
formatter: 'exit-code',
|
|
913
|
+
},
|
|
914
|
+
},
|
|
915
|
+
},
|
|
916
|
+
});
|
|
917
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
918
|
+
const manifestCheck = check(report, 'manifest');
|
|
919
|
+
expect(manifestCheck.ok).toBe(true);
|
|
920
|
+
expect(manifestCheck.detail).not.toMatch(/sensor authority is unavailable/i);
|
|
921
|
+
expect(manifestCheck.detail).not.toMatch(/pack ['"]?generic['"]?/i);
|
|
922
|
+
// only-project READY must say so — loose "sensors enabled" alone is pack wording.
|
|
923
|
+
expect(manifestCheck.detail).toMatch(/only-project/i);
|
|
924
|
+
expect(manifestCheck.detail).toMatch(/project-declared/i);
|
|
925
|
+
expect(check(report, 'tools').ok).toBe(true);
|
|
926
|
+
expect(report.mode).toBe('project-sensors');
|
|
927
|
+
}
|
|
928
|
+
finally {
|
|
929
|
+
if (previousPath === undefined)
|
|
930
|
+
delete process.env.PATH;
|
|
931
|
+
else
|
|
932
|
+
process.env.PATH = previousPath;
|
|
933
|
+
fs_1.default.rmSync(pathDir, { recursive: true, force: true });
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
});
|
|
816
937
|
});
|
|
@@ -230,3 +230,13 @@ describe('planSensorBootstrap', () => {
|
|
|
230
230
|
}
|
|
231
231
|
});
|
|
232
232
|
});
|
|
233
|
+
describe('bootstrap only-project honesty (S3)', () => {
|
|
234
|
+
it('does not invent pack:"generic" when planning an only-project write path is requested via pack null (RF-3.2)', async () => {
|
|
235
|
+
// Bootstrap options today reject null pack (stable id only). Until an explicit
|
|
236
|
+
// pack-less planner mode exists, requesting pack:null must fail closed — never
|
|
237
|
+
// coerce to generic and write a lying pack pin.
|
|
238
|
+
project_1.resolveSensorProject.mockReturnValue(missing);
|
|
239
|
+
await expect((0, bootstrap_1.planSensorBootstrap)(root, { mode: 'project-sensors', pack: null })).rejects.toThrow(/pack/i);
|
|
240
|
+
expect(detection_1.detectStack).not.toHaveBeenCalled();
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -19,6 +19,26 @@ function validV2Manifest() {
|
|
|
19
19
|
},
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
|
+
function projectSensor(overrides = {}) {
|
|
23
|
+
return {
|
|
24
|
+
source: 'project',
|
|
25
|
+
enabled: true,
|
|
26
|
+
command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function v3WithPackAndProject(projectOverrides = {}) {
|
|
31
|
+
return {
|
|
32
|
+
schemaVersion: 3,
|
|
33
|
+
mode: 'project-sensors',
|
|
34
|
+
pack: 'js-ts',
|
|
35
|
+
source: { registry: 'baseline' },
|
|
36
|
+
sensors: {
|
|
37
|
+
lint: validV2Manifest().sensors.lint,
|
|
38
|
+
custom: projectSensor(projectOverrides),
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
22
42
|
describe('sensor manifest contract', () => {
|
|
23
43
|
it('keeps compatibility contracts on an acyclic import boundary', () => {
|
|
24
44
|
const source = (relative) => fs_1.default.readFileSync(path_1.default.join(__dirname, '../../../../src/commands/sensors', relative), 'utf8');
|
|
@@ -192,4 +212,188 @@ describe('sensor manifest contract', () => {
|
|
|
192
212
|
])('rejects malformed manifest %j', (input, message) => {
|
|
193
213
|
expect(() => (0, manifest_1.parseSensorManifest)(input, 'sensors.json')).toThrow(message);
|
|
194
214
|
});
|
|
215
|
+
describe('project-declared sensors (S1)', () => {
|
|
216
|
+
it('accepts a source:project entry alongside a pack-bound lint sensor', () => {
|
|
217
|
+
const manifest = v3WithPackAndProject();
|
|
218
|
+
const parsed = (0, manifest_1.parseSensorManifest)(manifest, 'sensors.json');
|
|
219
|
+
expect(parsed).toMatchObject({
|
|
220
|
+
kind: 'v3',
|
|
221
|
+
pack: {
|
|
222
|
+
mode: 'project-sensors',
|
|
223
|
+
pack: 'js-ts',
|
|
224
|
+
sensors: {
|
|
225
|
+
lint: { variantId: 'eslint-9', enabled: true },
|
|
226
|
+
custom: {
|
|
227
|
+
source: 'project',
|
|
228
|
+
enabled: true,
|
|
229
|
+
command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
|
|
230
|
+
formatter: 'exit-code',
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
expect(parsed.kind === 'v3' && parsed.pack.mode === 'project-sensors' && 'variantId' in parsed.pack.sensors.custom).toBe(false);
|
|
236
|
+
expect(parsed.kind === 'v3' && parsed.pack.mode === 'project-sensors' && 'initializedCompatibility' in parsed.pack.sensors.custom).toBe(false);
|
|
237
|
+
});
|
|
238
|
+
it('accepts an only-project manifest with pack omitted or null', () => {
|
|
239
|
+
const base = {
|
|
240
|
+
schemaVersion: 3,
|
|
241
|
+
mode: 'project-sensors',
|
|
242
|
+
sensors: { custom: projectSensor() },
|
|
243
|
+
};
|
|
244
|
+
for (const manifest of [base, { ...base, pack: null }]) {
|
|
245
|
+
const parsed = (0, manifest_1.parseSensorManifest)(manifest, 'sensors.json');
|
|
246
|
+
expect(parsed).toMatchObject({
|
|
247
|
+
kind: 'v3',
|
|
248
|
+
pack: {
|
|
249
|
+
mode: 'project-sensors',
|
|
250
|
+
sensors: { custom: { source: 'project', formatter: 'exit-code' } },
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
if (parsed.kind === 'v3' && parsed.pack.mode === 'project-sensors') {
|
|
254
|
+
expect(parsed.pack.pack === undefined || parsed.pack.pack === null).toBe(true);
|
|
255
|
+
expect(parsed.pack.source).toBeUndefined();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
it('accepts an empty only-project sensors map', () => {
|
|
260
|
+
expect((0, manifest_1.parseSensorManifest)({
|
|
261
|
+
schemaVersion: 3,
|
|
262
|
+
mode: 'project-sensors',
|
|
263
|
+
sensors: {},
|
|
264
|
+
}, 'sensors.json')).toMatchObject({
|
|
265
|
+
kind: 'v3',
|
|
266
|
+
pack: { mode: 'project-sensors', sensors: {} },
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
it.each(['variantId', 'initializedCompatibility'])('rejects a project entry that declares forbidden pack field %s', field => {
|
|
270
|
+
const value = field === 'variantId'
|
|
271
|
+
? 'eslint-9'
|
|
272
|
+
: { state: 'certified', reason: 'ok', variantId: 'eslint-9', toolVersion: '9.0.0', runtimeVersion: '24.0.0', certifiedRange: '>=9 <10', evidence: [] };
|
|
273
|
+
const soft = (0, manifest_1.parseSensorManifestWithIssues)(v3WithPackAndProject({ [field]: value }), 'sensors.json');
|
|
274
|
+
expect(soft.invalidEntries).toEqual([
|
|
275
|
+
expect.objectContaining({
|
|
276
|
+
name: 'custom',
|
|
277
|
+
reason: expect.stringMatching(new RegExp(`sensors\\.custom\\.${field}`)),
|
|
278
|
+
}),
|
|
279
|
+
]);
|
|
280
|
+
expect(soft).toMatchObject({
|
|
281
|
+
kind: 'v3',
|
|
282
|
+
pack: { sensors: { lint: { variantId: 'eslint-9' } } },
|
|
283
|
+
});
|
|
284
|
+
expect('custom' in soft.pack.sensors).toBe(false);
|
|
285
|
+
});
|
|
286
|
+
it('soft-rejects an invalid project command with sensors.<name>.command in the reason (RF-1.12)', () => {
|
|
287
|
+
const soft = (0, manifest_1.parseSensorManifestWithIssues)(v3WithPackAndProject({
|
|
288
|
+
command: { executable: 'bash', resolution: 'path', args: ['-c', 'true'] },
|
|
289
|
+
}), 'sensors.json');
|
|
290
|
+
expect(soft.invalidEntries).toHaveLength(1);
|
|
291
|
+
expect(soft.invalidEntries[0]?.reason).toMatch(/sensors\.custom\.command/);
|
|
292
|
+
expect(soft).toMatchObject({
|
|
293
|
+
kind: 'v3',
|
|
294
|
+
pack: { sensors: { lint: { variantId: 'eslint-9' } } },
|
|
295
|
+
});
|
|
296
|
+
expect('custom' in soft.pack.sensors).toBe(false);
|
|
297
|
+
});
|
|
298
|
+
it('soft-rejects an unmarked custom command as missing source (RF-1.2)', () => {
|
|
299
|
+
const soft = (0, manifest_1.parseSensorManifestWithIssues)({
|
|
300
|
+
schemaVersion: 3,
|
|
301
|
+
mode: 'project-sensors',
|
|
302
|
+
pack: 'js-ts',
|
|
303
|
+
source: { registry: 'baseline' },
|
|
304
|
+
sensors: {
|
|
305
|
+
lint: validV2Manifest().sensors.lint,
|
|
306
|
+
custom: {
|
|
307
|
+
enabled: true,
|
|
308
|
+
command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
}, 'sensors.json');
|
|
312
|
+
expect(soft.invalidEntries).toEqual([
|
|
313
|
+
expect.objectContaining({
|
|
314
|
+
name: 'custom',
|
|
315
|
+
reason: expect.stringMatching(/sensors\.custom\.source/),
|
|
316
|
+
}),
|
|
317
|
+
]);
|
|
318
|
+
expect(soft.invalidEntries[0]?.reason).toMatch(/source/);
|
|
319
|
+
expect(soft).toMatchObject({
|
|
320
|
+
kind: 'v3',
|
|
321
|
+
pack: { sensors: { lint: { variantId: 'eslint-9' } } },
|
|
322
|
+
});
|
|
323
|
+
// Soft-isolation: unmarked custom must be dropped, pack sibling kept.
|
|
324
|
+
expect('custom' in soft.pack.sensors).toBe(false);
|
|
325
|
+
});
|
|
326
|
+
it('rejects unknown formatter and defaults omitted formatter to exit-code', () => {
|
|
327
|
+
const soft = (0, manifest_1.parseSensorManifestWithIssues)(v3WithPackAndProject({ formatter: 'not-a-formatter' }), 'sensors.json');
|
|
328
|
+
expect(soft.invalidEntries).toEqual([
|
|
329
|
+
expect.objectContaining({
|
|
330
|
+
name: 'custom',
|
|
331
|
+
reason: expect.stringMatching(/sensors\.custom\.formatter/),
|
|
332
|
+
}),
|
|
333
|
+
]);
|
|
334
|
+
const ok = (0, manifest_1.parseSensorManifest)(v3WithPackAndProject(), 'sensors.json');
|
|
335
|
+
expect(ok).toMatchObject({
|
|
336
|
+
kind: 'v3',
|
|
337
|
+
pack: { sensors: { custom: { formatter: 'exit-code' } } },
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
it('treats omitted applicability as always applicable and rejects invalid shapes', () => {
|
|
341
|
+
const ok = (0, manifest_1.parseSensorManifest)(v3WithPackAndProject(), 'sensors.json');
|
|
342
|
+
expect(ok.kind === 'v3' && ok.pack.mode === 'project-sensors' && !('applicability' in ok.pack.sensors.custom)).toBe(true);
|
|
343
|
+
const withApplicability = (0, manifest_1.parseSensorManifest)(v3WithPackAndProject({ applicability: { allFiles: ['package.json'] } }), 'sensors.json');
|
|
344
|
+
expect(withApplicability).toMatchObject({
|
|
345
|
+
kind: 'v3',
|
|
346
|
+
pack: { sensors: { custom: { applicability: { allFiles: ['package.json'] } } } },
|
|
347
|
+
});
|
|
348
|
+
const soft = (0, manifest_1.parseSensorManifestWithIssues)(v3WithPackAndProject({ applicability: {} }), 'sensors.json');
|
|
349
|
+
expect(soft.invalidEntries).toEqual([
|
|
350
|
+
expect.objectContaining({
|
|
351
|
+
name: 'custom',
|
|
352
|
+
reason: expect.stringMatching(/sensors\.custom\.applicability/),
|
|
353
|
+
}),
|
|
354
|
+
]);
|
|
355
|
+
});
|
|
356
|
+
it('rejects schemaVersion 2 and legacy manifests that use source:project with migrate-to-v3', () => {
|
|
357
|
+
const v2 = {
|
|
358
|
+
schemaVersion: 2,
|
|
359
|
+
pack: 'js-ts',
|
|
360
|
+
sensors: {
|
|
361
|
+
custom: {
|
|
362
|
+
source: 'project',
|
|
363
|
+
enabled: true,
|
|
364
|
+
command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
expect(() => (0, manifest_1.parseSensorManifest)(v2, 'sensors.json')).toThrow(/migrate-to-v3|migrate to v3/i);
|
|
369
|
+
const legacy = {
|
|
370
|
+
pack: 'js-ts',
|
|
371
|
+
sensors: {
|
|
372
|
+
custom: {
|
|
373
|
+
source: 'project',
|
|
374
|
+
enabled: true,
|
|
375
|
+
cmd: 'npm run lint',
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
expect(() => (0, manifest_1.parseSensorManifest)(legacy, 'sensors.json')).toThrow(/migrate-to-v3|migrate to v3/i);
|
|
380
|
+
});
|
|
381
|
+
it('soft-isolates one invalid project entry while keeping a valid pack sensor', () => {
|
|
382
|
+
const soft = (0, manifest_1.parseSensorManifestWithIssues)(v3WithPackAndProject({ formatter: 'not-a-formatter' }), 'sensors.json');
|
|
383
|
+
expect(soft.invalidEntries).toHaveLength(1);
|
|
384
|
+
expect(soft.invalidEntries[0]).toMatchObject({
|
|
385
|
+
name: 'custom',
|
|
386
|
+
reason: expect.stringMatching(/sensors\.custom\.formatter/),
|
|
387
|
+
});
|
|
388
|
+
expect(soft).toMatchObject({
|
|
389
|
+
kind: 'v3',
|
|
390
|
+
pack: {
|
|
391
|
+
mode: 'project-sensors',
|
|
392
|
+
sensors: { lint: { enabled: true, variantId: 'eslint-9' } },
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
expect('custom' in soft.pack.sensors).toBe(false);
|
|
396
|
+
expect(soft.invalidEntries.some(entry => /not valid JSON|valid JSON/i.test(entry.reason))).toBe(false);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
195
399
|
});
|