agentic-workflow-manager 9.11.1 → 9.11.3
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/scripts/support-matrix.js +0 -1
- package/dist/src/commands/preflight/checks.js +9 -2
- package/dist/src/commands/sensors/bootstrap.js +28 -8
- package/dist/src/commands/sensors/index.js +19 -5
- package/dist/src/commands/sensors/init.js +6 -3
- package/dist/src/commands/sensors/status.js +33 -0
- package/dist/src/commands/sensors/unresolved.js +19 -0
- package/dist/src/commands/update.js +0 -1
- package/dist/tests/commands/sensors/bootstrap.test.js +7 -3
- package/dist/tests/commands/sensors/index.test.js +1 -1
- package/dist/tests/commands/sensors/partial-pack-bootstrap.test.js +105 -0
- package/dist/tests/commands/sensors/partial-pack-status.test.js +174 -0
- package/dist/tests/core/export/engine.test.js +0 -1
- package/dist/tests/core/export/pack.test.js +0 -1
- package/dist/tests/structural/lint-covers-typescript.test.js +56 -0
- package/package.json +2 -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
|
@@ -156,7 +156,6 @@ function spliceGenerated(markdown, generated) {
|
|
|
156
156
|
exports.DOC_PATH = path_1.default.join(__dirname, '..', '..', 'docs', 'support-matrix.md');
|
|
157
157
|
/* istanbul ignore next — entrypoint de CLI, ejercitado por el test via las funciones puras */
|
|
158
158
|
if (require.main === module) {
|
|
159
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
160
159
|
const fs = require('fs');
|
|
161
160
|
const current = fs.readFileSync(exports.DOC_PATH, 'utf-8');
|
|
162
161
|
fs.writeFileSync(exports.DOC_PATH, spliceGenerated(current, renderProviderTables()), 'utf-8');
|
|
@@ -155,11 +155,18 @@ function checkManifest(status) {
|
|
|
155
155
|
remedy: `registry has no pack for '${manifest.pack}': run \`awm update\` or add a registry that has it`,
|
|
156
156
|
};
|
|
157
157
|
}
|
|
158
|
+
// An applicable sensor the project cannot initialize is a real gap, so it is
|
|
159
|
+
// named here rather than left as an absence. It does not flip `ok`: whether an
|
|
160
|
+
// uninitialized sensor should block a handoff is a gate-policy decision, and
|
|
161
|
+
// flipping it would restore the dead end #172 is about — a project whose only
|
|
162
|
+
// gap is one missing tool could then configure sensors and still never pass.
|
|
163
|
+
const uninitialized = Object.keys(status.uninitialized ?? {});
|
|
164
|
+
const gap = uninitialized.length === 0 ? '' : `; ${uninitialized.length} applicable sensor(s) not initialized: ${uninitialized.join(', ')}`;
|
|
158
165
|
return {
|
|
159
166
|
id: 'manifest',
|
|
160
167
|
ok: true,
|
|
161
|
-
detail: enabled === 0 ? `pack ${manifest.pack}, all ${total} sensors disabled (deliberate opt-out)`
|
|
162
|
-
: `pack ${manifest.pack}, ${enabled}/${total} sensors enabled
|
|
168
|
+
detail: (enabled === 0 ? `pack ${manifest.pack}, all ${total} sensors disabled (deliberate opt-out)`
|
|
169
|
+
: `pack ${manifest.pack}, ${enabled}/${total} sensors enabled`) + gap,
|
|
163
170
|
};
|
|
164
171
|
}
|
|
165
172
|
/** Every enabled sensor's command must resolve. This is the check nothing was calling. */
|
|
@@ -20,8 +20,8 @@ const safe_file_1 = require("./compatibility/safe-file");
|
|
|
20
20
|
const fs_1 = __importDefault(require("fs"));
|
|
21
21
|
const crypto_1 = require("crypto");
|
|
22
22
|
const path_1 = __importDefault(require("path"));
|
|
23
|
-
function blocked(projectRoot, manifestPath, dryRun, reason, remedy, candidates) {
|
|
24
|
-
return { kind: 'blocked', projectRoot, manifestPath, changes: [], dryRun, reason, remedy, ...(candidates ? { candidates } : {}) };
|
|
23
|
+
function blocked(projectRoot, manifestPath, dryRun, reason, remedy, candidates, unresolved) {
|
|
24
|
+
return { kind: 'blocked', projectRoot, manifestPath, changes: [], dryRun, reason, remedy, ...(candidates ? { candidates } : {}), ...(unresolved && unresolved.length > 0 ? { unresolved } : {}) };
|
|
25
25
|
}
|
|
26
26
|
function options(input) {
|
|
27
27
|
if (input === undefined)
|
|
@@ -84,21 +84,40 @@ async function projectSensors(projectRoot, registryRoot, requestedPack, packageR
|
|
|
84
84
|
return { reason: 'compatibility-unresolvable', remedy: 'repair-project-tools-or-select-another-mode' };
|
|
85
85
|
}
|
|
86
86
|
const sensors = {};
|
|
87
|
+
const unresolved = [];
|
|
88
|
+
let applicable = 0;
|
|
87
89
|
for (const [name, packSensor] of Object.entries(live.pack.sensors)) {
|
|
88
90
|
const evidence = live.sensors[name];
|
|
89
91
|
if (evidence.state === 'not-applicable')
|
|
90
92
|
continue;
|
|
93
|
+
applicable += 1;
|
|
91
94
|
const variant = evidence.variantId === null ? undefined : packSensor.variants.find(item => item.id === evidence.variantId);
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
// One applicable sensor whose tool is absent used to abort the whole
|
|
96
|
+
// manifest, including the sensors that DID resolve, so a project whose
|
|
97
|
+
// only gap was (say) semgrep could not configure sensors at all — and so
|
|
98
|
+
// could not pass preflight, and so could not be handed off. A missing tool
|
|
99
|
+
// is a gap in one sensor, not grounds to refuse the pack. It is skipped
|
|
100
|
+
// and REPORTED by name; nothing is invented for it, because the v3 entry
|
|
101
|
+
// shape requires a real variant, command and probe evidence. See #172.
|
|
102
|
+
if (!variant) {
|
|
103
|
+
unresolved.push({ sensor: name, state: evidence.state, reason: evidence.reason });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
94
106
|
sensors[name] = { enabled: true, ...(packSensor.fast === undefined ? {} : { fast: packSensor.fast }), ...(packSensor.timeout === undefined ? {} : { timeout: packSensor.timeout }), variantId: variant.id, command: variant.command, ...(variant.assets.length === 0 ? {} : { assets: variant.assets }), ...(variant.policyRef ? { policyRef: variant.policyRef } : {}), initializedCompatibility: evidence };
|
|
95
107
|
}
|
|
108
|
+
// A pack with NO applicable sensor still writes an honest empty manifest: that
|
|
109
|
+
// is the distinct "the registry has no pack for this stack" signal preflight
|
|
110
|
+
// already reports. But a pack where sensors apply and none of them resolve is
|
|
111
|
+
// a different condition, and writing an empty manifest would collide with it.
|
|
112
|
+
if (applicable > 0 && Object.keys(sensors).length === 0) {
|
|
113
|
+
return { reason: 'no-sensor-resolvable', remedy: 'repair-project-tools-or-select-another-mode', unresolved };
|
|
114
|
+
}
|
|
96
115
|
const manifest = (0, manifest_1.parseSensorManifest)({ schemaVersion: 3, mode: 'project-sensors', pack, source: { registry: source.registry.name }, ...(packageRoot ? { packageRoot } : {}), sensors }, 'bootstrap candidate');
|
|
97
116
|
if (manifest.kind !== 'v3' || manifest.pack.mode !== 'project-sensors')
|
|
98
117
|
throw new Error('bootstrap project-sensors candidate is invalid');
|
|
99
118
|
(0, manifest_1.serializeManifestV3)(manifest.pack);
|
|
100
119
|
const assets = [...new Set(Object.values(sensors).flatMap(sensor => sensor.assets ?? []))].sort();
|
|
101
|
-
return { manifest: manifest.pack, source, changes: [{ path: '.awm/sensors.json', action: 'create' }, ...assets.map(asset => ({ path: packageRoot ? `${packageRoot}/${asset}` : asset, action: 'create' }))] };
|
|
120
|
+
return { manifest: manifest.pack, source, changes: [{ path: '.awm/sensors.json', action: 'create' }, ...assets.map(asset => ({ path: packageRoot ? `${packageRoot}/${asset}` : asset, action: 'create' }))], unresolved };
|
|
102
121
|
}
|
|
103
122
|
/** Plan a one-time portable bootstrap. This function never writes project or machine state. */
|
|
104
123
|
async function planSensorBootstrap(cwd = process.cwd(), input = {}) {
|
|
@@ -125,7 +144,7 @@ async function planSensorBootstrap(cwd = process.cwd(), input = {}) {
|
|
|
125
144
|
// activate v2 sensors which require an explicit project opt-in.
|
|
126
145
|
const planned = await projectSensors(project.projectRoot, opts.registryRoot, project.manifest.pack.pack, undefined, false);
|
|
127
146
|
if ('reason' in planned)
|
|
128
|
-
return blocked(project.projectRoot, project.manifestPath, opts.dryRun, planned.reason, planned.remedy, 'candidates' in planned ? planned.candidates : undefined);
|
|
147
|
+
return blocked(project.projectRoot, project.manifestPath, opts.dryRun, planned.reason, planned.remedy, 'candidates' in planned ? planned.candidates : undefined, planned.unresolved);
|
|
129
148
|
try {
|
|
130
149
|
const original = fs_1.default.readFileSync(project.manifestPath);
|
|
131
150
|
return {
|
|
@@ -133,6 +152,7 @@ async function planSensorBootstrap(cwd = process.cwd(), input = {}) {
|
|
|
133
152
|
changes: planned.changes.map((change, index) => index === 0 ? { ...change, action: 'replace' } : change),
|
|
134
153
|
dryRun: opts.dryRun, migration: { kind: 'legacy-v1', manifest: planned.manifest }, source: planned.source,
|
|
135
154
|
originalDigest: digest(original),
|
|
155
|
+
...(planned.unresolved.length > 0 ? { unresolved: planned.unresolved } : {}),
|
|
136
156
|
};
|
|
137
157
|
}
|
|
138
158
|
catch {
|
|
@@ -165,8 +185,8 @@ async function planSensorBootstrap(cwd = process.cwd(), input = {}) {
|
|
|
165
185
|
}
|
|
166
186
|
const planned = await projectSensors(project.projectRoot, opts.registryRoot, opts.pack, opts.packageRoot);
|
|
167
187
|
if ('reason' in planned)
|
|
168
|
-
return blocked(project.projectRoot, project.manifestPath, opts.dryRun, planned.reason, planned.remedy, 'candidates' in planned ? planned.candidates : undefined);
|
|
169
|
-
return { kind: 'create', projectRoot: project.projectRoot, manifestPath: project.manifestPath, changes: planned.changes, dryRun: opts.dryRun, manifest: planned.manifest, source: planned.source, configure: opts.configure };
|
|
188
|
+
return blocked(project.projectRoot, project.manifestPath, opts.dryRun, planned.reason, planned.remedy, 'candidates' in planned ? planned.candidates : undefined, planned.unresolved);
|
|
189
|
+
return { kind: 'create', projectRoot: project.projectRoot, manifestPath: project.manifestPath, changes: planned.changes, dryRun: opts.dryRun, manifest: planned.manifest, source: planned.source, configure: opts.configure, ...(planned.unresolved.length > 0 ? { unresolved: planned.unresolved } : {}) };
|
|
170
190
|
}
|
|
171
191
|
/** Apply only a validated, non-dry-run create or migration plan. */
|
|
172
192
|
function applySensorBootstrap(plan) {
|
|
@@ -16,6 +16,7 @@ const render_1 = require("./coverage/render");
|
|
|
16
16
|
const registries_1 = require("../../core/registries");
|
|
17
17
|
const verdict_1 = require("./verdict");
|
|
18
18
|
const bootstrap_1 = require("./bootstrap");
|
|
19
|
+
const unresolved_1 = require("./unresolved");
|
|
19
20
|
/** Commander coercion for coverage recurrence emphasis. It deliberately runs
|
|
20
21
|
* before the action, so an invalid value cannot trigger ledger I/O. */
|
|
21
22
|
function parsePositiveSafeInteger(value) {
|
|
@@ -72,8 +73,10 @@ function registerSensorsCommand(program) {
|
|
|
72
73
|
const registryRoot = opts.registryRoot ?? (0, registries_1.capabilityRoot)('sensor-packs') ?? undefined;
|
|
73
74
|
try {
|
|
74
75
|
const plan = await (0, bootstrap_1.planSensorBootstrap)(process.cwd(), { mode: 'project-sensors', registryRoot, configure: opts.configure, pack: opts.pack, packageRoot: opts.packageRoot });
|
|
75
|
-
if (plan.kind === 'blocked')
|
|
76
|
-
|
|
76
|
+
if (plan.kind === 'blocked') {
|
|
77
|
+
const detail = (0, unresolved_1.describeUnresolved)(plan.unresolved);
|
|
78
|
+
throw new Error(`${plan.reason}: ${plan.remedy}${detail === '' ? '' : ` (${detail})`}`);
|
|
79
|
+
}
|
|
77
80
|
if (plan.kind === 'noop') {
|
|
78
81
|
prompts_1.log.info('already-configured');
|
|
79
82
|
return;
|
|
@@ -83,8 +86,12 @@ function registerSensorsCommand(program) {
|
|
|
83
86
|
const result = (0, bootstrap_1.applySensorBootstrap)(plan);
|
|
84
87
|
const detected = plan.manifest.mode === 'project-sensors' ? plan.manifest.pack : 'none';
|
|
85
88
|
prompts_1.log.success(`Detected: ${detected}`);
|
|
86
|
-
// Said BEFORE "Wrote .awm/sensors.json":
|
|
87
|
-
// reported as
|
|
89
|
+
// Said BEFORE "Wrote .awm/sensors.json": a partially resolved pack
|
|
90
|
+
// must not be reported as a clean write, and the manifest about to
|
|
91
|
+
// be reported as written is not the one the detection implied.
|
|
92
|
+
const skipped = (0, unresolved_1.describeUnresolved)(plan.unresolved);
|
|
93
|
+
if (skipped !== '')
|
|
94
|
+
prompts_1.log.warn(`not initialized (tool unavailable here): ${skipped}`);
|
|
88
95
|
prompts_1.log.success(`${result} .awm/sensors.json`);
|
|
89
96
|
}
|
|
90
97
|
catch (e) {
|
|
@@ -102,7 +109,8 @@ function registerSensorsCommand(program) {
|
|
|
102
109
|
try {
|
|
103
110
|
const plan = await (0, bootstrap_1.planSensorBootstrap)(process.cwd(), { mode: opts.mode, reason: opts.reason, dryRun: opts.dryRun });
|
|
104
111
|
if (plan.kind === 'blocked') {
|
|
105
|
-
|
|
112
|
+
const detail = (0, unresolved_1.describeUnresolved)(plan.unresolved);
|
|
113
|
+
prompts_1.log.error(`${plan.reason}: ${plan.remedy}${detail === '' ? '' : ` (${detail})`}`);
|
|
106
114
|
process.exitCode = 1;
|
|
107
115
|
return;
|
|
108
116
|
}
|
|
@@ -154,6 +162,12 @@ function registerSensorsCommand(program) {
|
|
|
154
162
|
const mark = check.ok ? picocolors_1.default.green('✔') : picocolors_1.default.red('✘');
|
|
155
163
|
console.log(` ${mark} ${name.padEnd(12)} ${check.detail}`);
|
|
156
164
|
}
|
|
165
|
+
// A pack sensor that applies here but has no manifest entry is otherwise
|
|
166
|
+
// indistinguishable from one the pack never applied. It does not change
|
|
167
|
+
// the verdict; it is named so the gap is attributable. See #172.
|
|
168
|
+
for (const [name, gap] of Object.entries(status.uninitialized ?? {})) {
|
|
169
|
+
console.log(` ${picocolors_1.default.yellow('–')} ${name.padEnd(12)} not initialized: ${gap.state}/${gap.reason}`);
|
|
170
|
+
}
|
|
157
171
|
console.log('');
|
|
158
172
|
if (status.overall !== 'READY')
|
|
159
173
|
process.exit(1);
|
|
@@ -11,6 +11,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
11
11
|
const path_1 = __importDefault(require("path"));
|
|
12
12
|
const pack_source_1 = require("./compatibility/pack-source");
|
|
13
13
|
const bootstrap_1 = require("./bootstrap");
|
|
14
|
+
const unresolved_1 = require("./unresolved");
|
|
14
15
|
/** Read a pack only through the shared source resolver. Init is a registry
|
|
15
16
|
* boundary: a pack path that is absent is harmless, but a symlink or escaping
|
|
16
17
|
* claimed source is never silently downgraded to an empty manifest. */
|
|
@@ -155,8 +156,10 @@ async function initSensors(opts = {}) {
|
|
|
155
156
|
mode: 'project-sensors', registryRoot: opts.registryRoot, configure: opts.configure,
|
|
156
157
|
pack: opts.pack, packageRoot: opts.packageRoot,
|
|
157
158
|
});
|
|
158
|
-
if (plan.kind === 'blocked')
|
|
159
|
-
|
|
159
|
+
if (plan.kind === 'blocked') {
|
|
160
|
+
const detail = (0, unresolved_1.describeUnresolved)(plan.unresolved);
|
|
161
|
+
throw new Error(`${plan.reason}: ${plan.remedy}${detail === '' ? '' : ` (${detail})`}`);
|
|
162
|
+
}
|
|
160
163
|
if (plan.kind === 'migrate')
|
|
161
164
|
throw new Error('initSensors does not migrate existing v2 manifests; run awm sensors bootstrap');
|
|
162
165
|
if (plan.kind === 'noop')
|
|
@@ -166,5 +169,5 @@ async function initSensors(opts = {}) {
|
|
|
166
169
|
manifest: {}, detection: detectStack(detectionCwd), configured: [], status: 'already-configured',
|
|
167
170
|
};
|
|
168
171
|
(0, bootstrap_1.applySensorBootstrap)(plan);
|
|
169
|
-
return { manifest: plan.manifest, detection: detectStack(detectionCwd), configured: [], status: 'created' };
|
|
172
|
+
return { manifest: plan.manifest, detection: detectStack(detectionCwd), configured: [], status: 'created', ...(plan.unresolved && plan.unresolved.length > 0 ? { unresolved: plan.unresolved } : {}) };
|
|
170
173
|
}
|
|
@@ -124,9 +124,40 @@ function resolveStaticV2Compatibility(cwd, manifest, source) {
|
|
|
124
124
|
: (0, resolve_1.resolveSensorCompatibility)(sensor, { ...resolutionEvidence, probe: { status: probeStatus } }, { pack: executionPack.name, sensor: name })];
|
|
125
125
|
}));
|
|
126
126
|
}
|
|
127
|
+
/** Applicable pack sensors with no manifest entry, so the gap is attributable in
|
|
128
|
+
* every `awm sensors status`, not only in the output of the `init` that skipped it. */
|
|
129
|
+
function uninitializedSensors(manifest, compatibility) {
|
|
130
|
+
const entries = Object.entries(compatibility)
|
|
131
|
+
.filter(([name, evidence]) => evidence !== undefined && evidence.state !== 'not-applicable' && !Object.prototype.hasOwnProperty.call(manifest.sensors, name))
|
|
132
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
133
|
+
.map(([name, evidence]) => [name, { state: evidence.state, reason: evidence.reason }]);
|
|
134
|
+
return entries.length === 0 ? undefined : Object.fromEntries(entries);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Status never executes, so `discoverProjectEvidence` is called here without
|
|
138
|
+
* `pathToolVersion` and a `resolution: 'path'` tool is structurally invisible to
|
|
139
|
+
* it: `npm` on PATH resolves as `missing-tool`/`tool-not-found` whether it is
|
|
140
|
+
* installed or not. Reporting that as "compatibility drift" asserts something
|
|
141
|
+
* this command did not measure. `checkStructuredCommand` DOES consult PATH for
|
|
142
|
+
* exactly these commands, so defer to it rather than claim drift. Any other live
|
|
143
|
+
* state, or a genuinely different resolved variant, is still real drift.
|
|
144
|
+
*
|
|
145
|
+
* Today `missing-tool`/`tool-not-found` is the ONLY verdict a `path` tool can
|
|
146
|
+
* receive here, because discovery cannot see its version at all — so the state
|
|
147
|
+
* and reason conditions are unreachable as written. They are kept because every
|
|
148
|
+
* one of them can only make this guard NARROWER: a combination they reject falls
|
|
149
|
+
* through to the drift report, never past it. A future discovery that does read
|
|
150
|
+
* PATH would make them load-bearing rather than silently permissive.
|
|
151
|
+
*/
|
|
152
|
+
function unobservableOnPath(sensor, live) {
|
|
153
|
+
return sensor.command.resolution === 'path' && live.variantId === null
|
|
154
|
+
&& live.state === 'missing-tool' && live.reason === 'tool-not-found';
|
|
155
|
+
}
|
|
127
156
|
function staticCompatibilityCheck(sensor, live) {
|
|
128
157
|
if (!live)
|
|
129
158
|
return { ok: false, detail: 'live compatibility unavailable' };
|
|
159
|
+
if (unobservableOnPath(sensor, live))
|
|
160
|
+
return null;
|
|
130
161
|
if (live.variantId !== sensor.variantId) {
|
|
131
162
|
return { ok: false, detail: `compatibility drift: initialized ${sensor.variantId}, resolved ${live.variantId ?? live.state}` };
|
|
132
163
|
}
|
|
@@ -226,9 +257,11 @@ async function computeSensorStatus(cwd = process.cwd()) {
|
|
|
226
257
|
checks[name] = sensor.enabled === false ? { ok: true, detail: 'disabled' }
|
|
227
258
|
: staticCompatibilityCheck(sensor, compatibility[name]) ?? checkStructuredCommand(sensor.command, packageRoot, sensor.assets);
|
|
228
259
|
}
|
|
260
|
+
const uninitialized = uninitializedSensors(manifest, compatibility);
|
|
229
261
|
return {
|
|
230
262
|
overall: Object.keys(checks).length > 0 && Object.values(checks).every(check => check.ok) ? 'READY' : 'DEGRADED',
|
|
231
263
|
pack: manifest.pack, checks,
|
|
264
|
+
...(uninitialized ? { uninitialized } : {}),
|
|
232
265
|
...projectMetadata(project, 'project-sensors', sourceMeta.reason, { ...sourceMeta }),
|
|
233
266
|
};
|
|
234
267
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.describeUnresolved = describeUnresolved;
|
|
4
|
+
/**
|
|
5
|
+
* `sensor-a: missing-tool/tool-not-found; sensor-b: incompatible/...` — which
|
|
6
|
+
* sensor and why. Before #172 an unresolvable sensor produced one code for the
|
|
7
|
+
* whole pack, so an operator could not tell that the other sensors were fine.
|
|
8
|
+
*
|
|
9
|
+
* Its own module so the command wiring and `initSensors` share one formatting,
|
|
10
|
+
* and so neither has to import the other to get it.
|
|
11
|
+
*/
|
|
12
|
+
function describeUnresolved(unresolved) {
|
|
13
|
+
if (!unresolved || unresolved.length === 0)
|
|
14
|
+
return '';
|
|
15
|
+
return [...unresolved]
|
|
16
|
+
.sort((left, right) => left.sensor.localeCompare(right.sensor))
|
|
17
|
+
.map(item => `${item.sensor}: ${item.state}/${item.reason}`)
|
|
18
|
+
.join('; ');
|
|
19
|
+
}
|
|
@@ -40,7 +40,6 @@ const defaultDeps = {
|
|
|
40
40
|
applyInstallPlan: install_transaction_1.applyInstallPlan,
|
|
41
41
|
resyncInstalledHooks: resync_1.resyncInstalledHooks,
|
|
42
42
|
offerSelfUpdate: async (mode) => {
|
|
43
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
44
43
|
const { offerSelfUpdate: real } = require('../core/update-check');
|
|
45
44
|
await real({ mode });
|
|
46
45
|
},
|
|
@@ -128,12 +128,16 @@ describe('planSensorBootstrap', () => {
|
|
|
128
128
|
await (0, bootstrap_1.planSensorBootstrap)(root, { mode: 'project-sensors', packageRoot: 'packages/web' });
|
|
129
129
|
expect(live_1.resolveParsedPackCompatibility).toHaveBeenCalledWith(path_1.default.resolve(root, 'packages/web'), pack);
|
|
130
130
|
});
|
|
131
|
-
|
|
131
|
+
// #172 narrowed this: one unresolvable sensor no longer aborts a pack whose
|
|
132
|
+
// other sensors resolved (see partial-pack-bootstrap.test.ts). What still
|
|
133
|
+
// blocks is a pack where sensors apply and NONE of them resolve, which is a
|
|
134
|
+
// different condition from a pack with no applicable sensor at all.
|
|
135
|
+
it('blocks project-sensors when sensors apply and compatibility can select no variant at all', async () => {
|
|
132
136
|
const variant = { id: 'eslint-9', command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] }, assets: [] };
|
|
133
137
|
pack_source_1.listPackSources.mockReturnValue([source]);
|
|
134
138
|
contract_1.parseSensorPack.mockReturnValue({ kind: 'v2', pack: { schemaVersion: 2, name: 'js-ts', sensors: { lint: { variants: [variant] } } } });
|
|
135
|
-
live_1.resolveParsedPackCompatibility.mockResolvedValue({ pack: { schemaVersion: 2, name: 'js-ts', sensors: { lint: { variants: [variant] } } }, sensors: { lint: { variantId: null } } });
|
|
136
|
-
await expect((0, bootstrap_1.planSensorBootstrap)(root, { mode: 'project-sensors' })).resolves.toMatchObject({ kind: 'blocked', reason: 'sensor-
|
|
139
|
+
live_1.resolveParsedPackCompatibility.mockResolvedValue({ pack: { schemaVersion: 2, name: 'js-ts', sensors: { lint: { variants: [variant] } } }, sensors: { lint: { state: 'missing-tool', reason: 'tool-not-found', variantId: null } } });
|
|
140
|
+
await expect((0, bootstrap_1.planSensorBootstrap)(root, { mode: 'project-sensors' })).resolves.toMatchObject({ kind: 'blocked', reason: 'no-sensor-resolvable', changes: [] });
|
|
137
141
|
});
|
|
138
142
|
it('omits explicit-opt-in sensors that are not applicable while retaining mandatory gates', async () => {
|
|
139
143
|
const variant = { id: 'main', command: { executable: 'tool', resolution: 'path', args: ['run'] }, assets: [] };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
jest.mock('@clack/prompts', () => ({ log: { success: jest.fn(), info: jest.fn(), error: jest.fn() } }));
|
|
3
|
+
jest.mock('@clack/prompts', () => ({ log: { success: jest.fn(), info: jest.fn(), error: jest.fn(), warn: jest.fn() } }));
|
|
4
4
|
jest.mock('picocolors', () => ({ green: (s) => s, yellow: (s) => s, red: (s) => s }));
|
|
5
5
|
jest.mock('../../../src/commands/sensors/run', () => ({ runSensors: jest.fn() }));
|
|
6
6
|
jest.mock('../../../src/commands/sensors/init', () => ({ initSensors: jest.fn() }));
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
jest.mock('../../../src/commands/sensors/project', () => ({ resolveSensorProject: jest.fn() }));
|
|
4
|
+
jest.mock('../../../src/commands/sensors/detection', () => ({ detectStack: jest.fn() }));
|
|
5
|
+
jest.mock('../../../src/core/registries', () => ({ listRegistries: jest.fn(() => []) }));
|
|
6
|
+
jest.mock('../../../src/commands/sensors/compatibility/pack-source', () => ({ listPackSources: jest.fn() }));
|
|
7
|
+
jest.mock('../../../src/commands/sensors/compatibility/contract', () => ({ ...jest.requireActual('../../../src/commands/sensors/compatibility/contract'), parseSensorPack: jest.fn() }));
|
|
8
|
+
jest.mock('../../../src/commands/sensors/compatibility/live', () => ({ resolveParsedPackCompatibility: jest.fn() }));
|
|
9
|
+
jest.mock('../../../src/commands/sensors/compatibility/materialize', () => ({ materializePortableSensors: jest.fn() }));
|
|
10
|
+
jest.mock('../../../src/commands/sensors/compatibility/safe-file', () => ({ writeProjectFile: jest.fn(), withProjectLease: jest.fn((_root, operation) => operation()) }));
|
|
11
|
+
const bootstrap_1 = require("../../../src/commands/sensors/bootstrap");
|
|
12
|
+
const unresolved_1 = require("../../../src/commands/sensors/unresolved");
|
|
13
|
+
const project_1 = require("../../../src/commands/sensors/project");
|
|
14
|
+
const detection_1 = require("../../../src/commands/sensors/detection");
|
|
15
|
+
const pack_source_1 = require("../../../src/commands/sensors/compatibility/pack-source");
|
|
16
|
+
const contract_1 = require("../../../src/commands/sensors/compatibility/contract");
|
|
17
|
+
const live_1 = require("../../../src/commands/sensors/compatibility/live");
|
|
18
|
+
// #172. One applicable sensor whose tool is absent used to abort the entire
|
|
19
|
+
// manifest, including the sensors that DID resolve — so a project whose only gap
|
|
20
|
+
// was semgrep could not configure sensors, could not pass preflight, and could
|
|
21
|
+
// not be handed off. A missing tool is a gap in one sensor, not grounds to refuse
|
|
22
|
+
// the pack.
|
|
23
|
+
const root = '/project';
|
|
24
|
+
const missing = { state: 'missing', projectRoot: root, manifestPath: `${root}/.awm/sensors.json` };
|
|
25
|
+
const source = { path: '/registry/sensor-packs/js-ts/pack.json', content: '{}', registry: { name: 'baseline', remote: 'local', contentRoot: '/registry' } };
|
|
26
|
+
const lintVariant = { id: 'eslint-10', command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] }, assets: [] };
|
|
27
|
+
const securityVariant = { id: 'semgrep-js-ts', command: { executable: 'semgrep', resolution: 'path', args: ['--config', '.semgrep.awm.yml'] }, assets: ['.semgrep.awm.yml'] };
|
|
28
|
+
function resolvedLint() {
|
|
29
|
+
return { state: 'certified', reason: 'range-and-probe', variantId: 'eslint-10', toolVersion: '10.4.1', runtimeVersion: '24.0.0', certifiedRange: '>=10 <11', evidence: [] };
|
|
30
|
+
}
|
|
31
|
+
function unresolvedSensor(state, reason) {
|
|
32
|
+
return { state, reason, variantId: null, toolVersion: null, runtimeVersion: null, certifiedRange: null, evidence: [] };
|
|
33
|
+
}
|
|
34
|
+
/** Wire the mocked pack + live resolution for one planning run. */
|
|
35
|
+
function pack(sensors, live) {
|
|
36
|
+
const parsed = { schemaVersion: 2, name: 'js-ts', sensors };
|
|
37
|
+
pack_source_1.listPackSources.mockReturnValue([source]);
|
|
38
|
+
contract_1.parseSensorPack.mockReturnValue({ kind: 'v2', pack: parsed });
|
|
39
|
+
live_1.resolveParsedPackCompatibility.mockResolvedValue({ pack: parsed, sensors: live });
|
|
40
|
+
}
|
|
41
|
+
describe('project-sensors bootstrap with a partially resolvable pack', () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
jest.clearAllMocks();
|
|
44
|
+
project_1.resolveSensorProject.mockReturnValue(missing);
|
|
45
|
+
detection_1.detectStack.mockReturnValue({ pack: 'js-ts', indicators: ['package.json'] });
|
|
46
|
+
});
|
|
47
|
+
it('writes the sensors that resolved and reports by name the applicable one that did not', async () => {
|
|
48
|
+
pack({ lint: { variants: [lintVariant] }, security: { variants: [securityVariant] } }, { lint: resolvedLint(), security: unresolvedSensor('missing-tool', 'tool-not-found') });
|
|
49
|
+
const plan = await (0, bootstrap_1.planSensorBootstrap)(root, { mode: 'project-sensors' });
|
|
50
|
+
expect(plan.kind).toBe('create');
|
|
51
|
+
const created = plan;
|
|
52
|
+
const manifest = created.manifest;
|
|
53
|
+
// The manifest contents are the assertion, not the exit code.
|
|
54
|
+
expect(Object.keys(manifest.sensors)).toEqual(['lint']);
|
|
55
|
+
expect(manifest.sensors.lint.variantId).toBe('eslint-10');
|
|
56
|
+
expect(created.unresolved).toEqual([{ sensor: 'security', state: 'missing-tool', reason: 'tool-not-found' }]);
|
|
57
|
+
// Nothing is invented for the skipped sensor: no entry, and none of its assets.
|
|
58
|
+
expect(created.changes.map(change => change.path)).toEqual(['.awm/sensors.json']);
|
|
59
|
+
});
|
|
60
|
+
it('carries a sensor with no manifest entry rather than a disabled entry with fabricated evidence', async () => {
|
|
61
|
+
pack({ lint: { variants: [lintVariant] }, security: { variants: [securityVariant] } }, { lint: resolvedLint(), security: unresolvedSensor('incompatible', 'no-operational-variant') });
|
|
62
|
+
const plan = await (0, bootstrap_1.planSensorBootstrap)(root, { mode: 'project-sensors' });
|
|
63
|
+
const manifest = plan.manifest;
|
|
64
|
+
expect(manifest.sensors).not.toHaveProperty('security');
|
|
65
|
+
expect(JSON.stringify(manifest)).not.toContain('semgrep');
|
|
66
|
+
});
|
|
67
|
+
it('still blocks, naming every sensor, when sensors apply and none of them resolve', async () => {
|
|
68
|
+
pack({ lint: { variants: [lintVariant] }, security: { variants: [securityVariant] } }, { lint: unresolvedSensor('missing-tool', 'tool-not-found'), security: unresolvedSensor('missing-tool', 'tool-not-found') });
|
|
69
|
+
const plan = await (0, bootstrap_1.planSensorBootstrap)(root, { mode: 'project-sensors' });
|
|
70
|
+
expect(plan).toMatchObject({ kind: 'blocked', reason: 'no-sensor-resolvable', changes: [] });
|
|
71
|
+
expect(plan.unresolved).toEqual([
|
|
72
|
+
{ sensor: 'lint', state: 'missing-tool', reason: 'tool-not-found' },
|
|
73
|
+
{ sensor: 'security', state: 'missing-tool', reason: 'tool-not-found' },
|
|
74
|
+
]);
|
|
75
|
+
});
|
|
76
|
+
it('keeps the distinct honest-empty manifest when the pack declares no applicable sensor', async () => {
|
|
77
|
+
// This is NOT the same condition as "nothing resolved": it is the registry
|
|
78
|
+
// having no sensor for this stack, which preflight reports with its own
|
|
79
|
+
// remedy. Writing an empty manifest for an unresolvable pack would collide
|
|
80
|
+
// with that signal.
|
|
81
|
+
pack({ lint: { variants: [lintVariant] } }, { lint: unresolvedSensor('not-applicable', 'applicability-not-met') });
|
|
82
|
+
const plan = await (0, bootstrap_1.planSensorBootstrap)(root, { mode: 'project-sensors' });
|
|
83
|
+
const manifest = plan.manifest;
|
|
84
|
+
expect(plan.kind).toBe('create');
|
|
85
|
+
expect(manifest.sensors).toEqual({});
|
|
86
|
+
expect(plan.unresolved).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
it('omits the unresolved field entirely when every applicable sensor resolved', async () => {
|
|
89
|
+
pack({ lint: { variants: [lintVariant] } }, { lint: resolvedLint() });
|
|
90
|
+
const plan = await (0, bootstrap_1.planSensorBootstrap)(root, { mode: 'project-sensors' });
|
|
91
|
+
expect(plan.unresolved).toBeUndefined();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe('describeUnresolved', () => {
|
|
95
|
+
it('names each sensor with its state and reason, in a stable order', () => {
|
|
96
|
+
expect((0, unresolved_1.describeUnresolved)([
|
|
97
|
+
{ sensor: 'security', state: 'missing-tool', reason: 'tool-not-found' },
|
|
98
|
+
{ sensor: 'depcheck', state: 'incompatible', reason: 'no-operational-variant' },
|
|
99
|
+
])).toBe('depcheck: incompatible/no-operational-variant; security: missing-tool/tool-not-found');
|
|
100
|
+
});
|
|
101
|
+
it('is empty for nothing unresolved, so callers add no punctuation for an empty gap', () => {
|
|
102
|
+
expect((0, unresolved_1.describeUnresolved)(undefined)).toBe('');
|
|
103
|
+
expect((0, unresolved_1.describeUnresolved)([])).toBe('');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const status_1 = require("../../../src/commands/sensors/status");
|
|
10
|
+
const checks_1 = require("../../../src/commands/preflight/checks");
|
|
11
|
+
// #172. `checks` only ever covered what the manifest declares, so a pack sensor
|
|
12
|
+
// that applies to this project and could not be initialized was simply absent —
|
|
13
|
+
// indistinguishable from one the pack never applied. These tests hold the gap
|
|
14
|
+
// visible and attributable in every `awm sensors status`, not only in the output
|
|
15
|
+
// of the `init` that skipped it.
|
|
16
|
+
//
|
|
17
|
+
// Isolated HOME/AWM_HOME per test: nothing here may read or write the real
|
|
18
|
+
// `~/.awm`, which belongs to the installer.
|
|
19
|
+
const ESLINT_VARIANT = {
|
|
20
|
+
id: 'eslint-10',
|
|
21
|
+
priority: 100,
|
|
22
|
+
requirements: { tool: 'eslint', toolRange: '>=10.0.0 <12.0.0', runtime: 'node', runtimeRange: '>=20.0.0' },
|
|
23
|
+
certifiedRange: '>=10.0.0 <11.0.0',
|
|
24
|
+
command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
|
|
25
|
+
assets: [],
|
|
26
|
+
formatter: 'eslint',
|
|
27
|
+
probe: { kind: 'config-present' },
|
|
28
|
+
};
|
|
29
|
+
const SEMGREP_VARIANT = {
|
|
30
|
+
id: 'semgrep-js-ts',
|
|
31
|
+
priority: 10,
|
|
32
|
+
requirements: { tool: 'semgrep', toolRange: '>=1.0.0', runtime: 'node', runtimeRange: '>=20.0.0' },
|
|
33
|
+
certifiedRange: '>=1.0.0',
|
|
34
|
+
command: { executable: 'semgrep', resolution: 'path', args: ['--config', '.semgrep.awm.yml'] },
|
|
35
|
+
assets: [],
|
|
36
|
+
formatter: 'semgrep',
|
|
37
|
+
probe: { kind: 'version' },
|
|
38
|
+
};
|
|
39
|
+
function packJson(sensors) {
|
|
40
|
+
const detector = Object.keys(sensors)[0];
|
|
41
|
+
return JSON.stringify({
|
|
42
|
+
schemaVersion: 2, name: 'js-ts', description: '#172 fixture', detects: ['package.json'], sensors,
|
|
43
|
+
coverage: {
|
|
44
|
+
schemaVersion: 1,
|
|
45
|
+
classes: { 'lint-errors': { description: 'Findings', detectors: [{ sensor: detector }], remedy: { summary: 'Configure', command: 'awm sensors init --pack js-ts' } } },
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function lintEntry() {
|
|
50
|
+
return {
|
|
51
|
+
enabled: true, variantId: 'eslint-10',
|
|
52
|
+
command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
|
|
53
|
+
initializedCompatibility: { state: 'certified', reason: 'range-and-probe', variantId: 'eslint-10', toolVersion: '10.4.1', runtimeVersion: '24.0.0', certifiedRange: '>=10.0.0 <11.0.0', evidence: [] },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function semgrepEntry() {
|
|
57
|
+
return {
|
|
58
|
+
enabled: true, variantId: 'semgrep-js-ts',
|
|
59
|
+
command: { executable: 'semgrep', resolution: 'path', args: ['--config', '.semgrep.awm.yml'] },
|
|
60
|
+
initializedCompatibility: { state: 'compatible-unverified', reason: 'operational-range-and-probe', variantId: 'semgrep-js-ts', toolVersion: '1.173.0', runtimeVersion: '24.0.0', certifiedRange: '>=1.0.0', evidence: [] },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
describe('sensor status for a partially initialized pack', () => {
|
|
64
|
+
let root;
|
|
65
|
+
let project;
|
|
66
|
+
let pathDir;
|
|
67
|
+
let previous;
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-172-'));
|
|
70
|
+
project = path_1.default.join(root, 'project');
|
|
71
|
+
pathDir = path_1.default.join(root, 'bin');
|
|
72
|
+
const registry = path_1.default.join(root, 'awm-home', 'registries', 'baseline', 'sensor-packs', 'js-ts');
|
|
73
|
+
fs_1.default.mkdirSync(registry, { recursive: true });
|
|
74
|
+
fs_1.default.mkdirSync(path_1.default.join(project, '.awm'), { recursive: true });
|
|
75
|
+
fs_1.default.mkdirSync(pathDir, { recursive: true });
|
|
76
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'awm-home', 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'fixture' }]));
|
|
77
|
+
fs_1.default.writeFileSync(path_1.default.join(project, 'package.json'), JSON.stringify({ name: 'fixture-172', private: true }));
|
|
78
|
+
fs_1.default.writeFileSync(path_1.default.join(project, 'eslint.config.mjs'), 'export default [];\n');
|
|
79
|
+
fs_1.default.mkdirSync(path_1.default.join(project, 'node_modules', 'eslint'), { recursive: true });
|
|
80
|
+
fs_1.default.writeFileSync(path_1.default.join(project, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ name: 'eslint', version: '10.4.1' }));
|
|
81
|
+
fs_1.default.mkdirSync(path_1.default.join(project, 'node_modules', '.bin'), { recursive: true });
|
|
82
|
+
fs_1.default.writeFileSync(path_1.default.join(project, 'node_modules', '.bin', 'eslint'), '');
|
|
83
|
+
fs_1.default.writeFileSync(path_1.default.join(registry, 'pack.json'), packJson({
|
|
84
|
+
lint: { applicability: { allFiles: ['package.json'] }, variants: [ESLINT_VARIANT] },
|
|
85
|
+
security: { applicability: { allFiles: ['package.json'] }, variants: [SEMGREP_VARIANT] },
|
|
86
|
+
}));
|
|
87
|
+
previous = { home: process.env.HOME, awmHome: process.env.AWM_HOME, path: process.env.PATH };
|
|
88
|
+
process.env.HOME = path_1.default.join(root, 'operator-home');
|
|
89
|
+
process.env.AWM_HOME = path_1.default.join(root, 'awm-home');
|
|
90
|
+
process.env.PATH = pathDir;
|
|
91
|
+
});
|
|
92
|
+
afterEach(() => {
|
|
93
|
+
for (const [key, value] of [['HOME', previous.home], ['AWM_HOME', previous.awmHome], ['PATH', previous.path]]) {
|
|
94
|
+
if (value === undefined)
|
|
95
|
+
delete process.env[key];
|
|
96
|
+
else
|
|
97
|
+
process.env[key] = value;
|
|
98
|
+
}
|
|
99
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
100
|
+
});
|
|
101
|
+
function writeManifest(sensors) {
|
|
102
|
+
fs_1.default.writeFileSync(path_1.default.join(project, '.awm', 'sensors.json'), JSON.stringify({
|
|
103
|
+
schemaVersion: 3, mode: 'project-sensors', pack: 'js-ts', source: { registry: 'baseline' }, sensors,
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
function installOnPath(tool) {
|
|
107
|
+
const file = path_1.default.join(pathDir, tool);
|
|
108
|
+
fs_1.default.writeFileSync(file, '#!/bin/sh\nexit 0\n');
|
|
109
|
+
fs_1.default.chmodSync(file, 0o755);
|
|
110
|
+
}
|
|
111
|
+
it('names the applicable sensor that has no manifest entry, with its live reason', async () => {
|
|
112
|
+
writeManifest({ lint: lintEntry() });
|
|
113
|
+
const status = await (0, status_1.computeSensorStatus)(project);
|
|
114
|
+
expect(Object.keys(status.checks)).toEqual(['lint']);
|
|
115
|
+
expect(status.uninitialized).toEqual({ security: { state: 'missing-tool', reason: 'tool-not-found' } });
|
|
116
|
+
});
|
|
117
|
+
it('does not let an uninitialized sensor change the verdict, which stays a gate-policy decision', async () => {
|
|
118
|
+
writeManifest({ lint: lintEntry() });
|
|
119
|
+
const status = await (0, status_1.computeSensorStatus)(project);
|
|
120
|
+
expect(status.overall).toBe('READY');
|
|
121
|
+
});
|
|
122
|
+
it('omits the field when every applicable pack sensor has an entry', async () => {
|
|
123
|
+
installOnPath('semgrep');
|
|
124
|
+
writeManifest({ lint: lintEntry(), security: semgrepEntry() });
|
|
125
|
+
const status = await (0, status_1.computeSensorStatus)(project);
|
|
126
|
+
expect(status.uninitialized).toBeUndefined();
|
|
127
|
+
});
|
|
128
|
+
it('reports a not-applicable pack sensor as neither a check nor a gap', async () => {
|
|
129
|
+
const registry = path_1.default.join(root, 'awm-home', 'registries', 'baseline', 'sensor-packs', 'js-ts');
|
|
130
|
+
fs_1.default.writeFileSync(path_1.default.join(registry, 'pack.json'), packJson({
|
|
131
|
+
lint: { applicability: { allFiles: ['package.json'] }, variants: [ESLINT_VARIANT] },
|
|
132
|
+
security: { applicability: { allFiles: ['never-present.toml'] }, variants: [SEMGREP_VARIANT] },
|
|
133
|
+
}));
|
|
134
|
+
writeManifest({ lint: lintEntry() });
|
|
135
|
+
const status = await (0, status_1.computeSensorStatus)(project);
|
|
136
|
+
expect(status.uninitialized).toBeUndefined();
|
|
137
|
+
expect(status.checks).not.toHaveProperty('security');
|
|
138
|
+
});
|
|
139
|
+
it('preflight names the gap and still passes the manifest check, so one missing tool is not a dead end', async () => {
|
|
140
|
+
fs_1.default.mkdirSync(path_1.default.join(project, '.git'));
|
|
141
|
+
writeManifest({ lint: lintEntry() });
|
|
142
|
+
const report = await (0, checks_1.preflight)(project);
|
|
143
|
+
const manifest = report.checks.find(check => check.id === 'manifest');
|
|
144
|
+
expect(manifest).toMatchObject({ ok: true });
|
|
145
|
+
expect(manifest?.detail).toContain('1 applicable sensor(s) not initialized: security');
|
|
146
|
+
});
|
|
147
|
+
// `discoverProjectEvidence` is called here without `pathToolVersion` because
|
|
148
|
+
// status never executes, so a `resolution: 'path'` tool is invisible to it.
|
|
149
|
+
// Calling that "drift" asserts something this command did not measure.
|
|
150
|
+
it('does not claim drift for a PATH tool it cannot observe, and defers to the PATH check', async () => {
|
|
151
|
+
installOnPath('semgrep');
|
|
152
|
+
fs_1.default.writeFileSync(path_1.default.join(project, '.semgrep.awm.yml'), 'rules: []\n');
|
|
153
|
+
writeManifest({ security: semgrepEntry() });
|
|
154
|
+
const status = await (0, status_1.computeSensorStatus)(project);
|
|
155
|
+
expect(status.checks.security).toEqual({ ok: true, detail: 'semgrep' });
|
|
156
|
+
expect(status.overall).toBe('READY');
|
|
157
|
+
});
|
|
158
|
+
it('still fails a PATH sensor whose executable is genuinely absent from PATH', async () => {
|
|
159
|
+
writeManifest({ security: semgrepEntry() });
|
|
160
|
+
const status = await (0, status_1.computeSensorStatus)(project);
|
|
161
|
+
expect(status.checks.security.ok).toBe(false);
|
|
162
|
+
expect(status.checks.security.detail).toMatch(/semgrep not found in PATH/);
|
|
163
|
+
});
|
|
164
|
+
it('still reports real drift when the live resolution selects a different variant', async () => {
|
|
165
|
+
const registry = path_1.default.join(root, 'awm-home', 'registries', 'baseline', 'sensor-packs', 'js-ts');
|
|
166
|
+
fs_1.default.writeFileSync(path_1.default.join(registry, 'pack.json'), packJson({
|
|
167
|
+
lint: { applicability: { allFiles: ['package.json'] }, variants: [{ ...ESLINT_VARIANT, id: 'eslint-10-flat' }] },
|
|
168
|
+
}));
|
|
169
|
+
writeManifest({ lint: lintEntry() });
|
|
170
|
+
const status = await (0, status_1.computeSensorStatus)(project);
|
|
171
|
+
expect(status.checks.lint.ok).toBe(false);
|
|
172
|
+
expect(status.checks.lint.detail).toMatch(/compatibility drift: initialized eslint-10, resolved eslint-10-flat/);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -77,7 +77,6 @@ describe('runExport (engine end-to-end)', () => {
|
|
|
77
77
|
// La lectura sale exclusivamente de roots: un root vacío no resuelve nada.
|
|
78
78
|
// Control negativo real: contentRoots() (el fallback al registry instalado)
|
|
79
79
|
// no debe invocarse en absoluto cuando opts.roots viene dado explícitamente.
|
|
80
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
81
80
|
const registries = require('../../../src/core/registries');
|
|
82
81
|
const spy = jest.spyOn(registries, 'contentRoots');
|
|
83
82
|
const empty = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-empty-root-'));
|
|
@@ -7,7 +7,6 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
const os_1 = __importDefault(require("os"));
|
|
9
9
|
const pack_1 = require("../../../src/core/export/pack");
|
|
10
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
11
10
|
const childProcess = require('child_process');
|
|
12
11
|
const okZip = (cwd, zipName) => {
|
|
13
12
|
fs_1.default.writeFileSync(path_1.default.join(cwd, zipName), 'fake-zip');
|
|
@@ -0,0 +1,56 @@
|
|
|
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 child_process_1 = require("child_process");
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
// #171: the lint gate linted 14 files — all js/mjs — and exited 0 on a
|
|
9
|
+
// TypeScript CLI. `eslint.config.awm.mjs` imports a project config that does
|
|
10
|
+
// not exist, swallows the failure, and its remaining objects declare no
|
|
11
|
+
// `files`, so eslint's default universe (js/mjs/cjs) left every .ts out.
|
|
12
|
+
//
|
|
13
|
+
// The gate was green for months and the `lint` sensor certified on it, which
|
|
14
|
+
// `plan admit --verify-sensors` then consumes as the unattended handoff gate.
|
|
15
|
+
//
|
|
16
|
+
// This asserts on the SET OF FILES LINTED, never on the exit code. An exit-code
|
|
17
|
+
// assertion is exactly what let this survive: a lint that examines nothing
|
|
18
|
+
// exits 0.
|
|
19
|
+
const CLI_ROOT = path_1.default.resolve(__dirname, '../..');
|
|
20
|
+
/** One eslint run for the whole suite. Invoked through node against eslint's own
|
|
21
|
+
* js entrypoint rather than `npx`: on Windows `npx` resolves to `npx.cmd`, and
|
|
22
|
+
* `execFileSync` cannot execute a `.cmd` without a shell — the exact trap this
|
|
23
|
+
* repo already documents in tests/integration/published-doctor-evidence.e2e.test.ts,
|
|
24
|
+
* and the one this test hit on both Windows legs of its first CI run. Going
|
|
25
|
+
* through `process.execPath` needs no shell, so no argument escaping either. */
|
|
26
|
+
function lint() {
|
|
27
|
+
const raw = (0, child_process_1.execFileSync)(process.execPath, [
|
|
28
|
+
path_1.default.join(CLI_ROOT, 'node_modules', 'eslint', 'bin', 'eslint.js'),
|
|
29
|
+
'.', '--config', 'eslint.config.awm.mjs', '--format', 'json',
|
|
30
|
+
], { cwd: CLI_ROOT, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
31
|
+
return JSON.parse(raw);
|
|
32
|
+
}
|
|
33
|
+
describe('the lint gate covers TypeScript (#171)', () => {
|
|
34
|
+
jest.setTimeout(10 * 60_000);
|
|
35
|
+
const results = lint();
|
|
36
|
+
const files = results.map(entry => entry.filePath);
|
|
37
|
+
const typescript = files.filter(file => file.endsWith('.ts'));
|
|
38
|
+
test('lints the TypeScript sources, not only the js/mjs tooling', () => {
|
|
39
|
+
// A floor, not a lock: adding sources must not fail this.
|
|
40
|
+
expect(typescript.length).toBeGreaterThan(400);
|
|
41
|
+
expect(typescript.some(file => file.includes(`${path_1.default.sep}src${path_1.default.sep}`))).toBe(true);
|
|
42
|
+
expect(typescript.some(file => file.includes(`${path_1.default.sep}tests${path_1.default.sep}`))).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
test('lints this very file, so the assertion cannot be vacuous', () => {
|
|
45
|
+
expect(files).toContain(path_1.default.join(CLI_ROOT, 'tests', 'structural', 'lint-covers-typescript.test.ts'));
|
|
46
|
+
});
|
|
47
|
+
test('reports no unresolved rule references', () => {
|
|
48
|
+
// Four stale `eslint-disable @typescript-eslint/no-var-requires` comments
|
|
49
|
+
// survived precisely because .ts was never linted: eslint errors on a rule
|
|
50
|
+
// it cannot resolve, and nobody ever saw it. A new one must fail here
|
|
51
|
+
// rather than wait for the next person to turn the gate on.
|
|
52
|
+
const unresolved = results.flatMap(entry => entry.messages.filter(m => /Definition for rule .* was not found/.test(m.message))
|
|
53
|
+
.map(m => `${entry.filePath}: ${m.message}`));
|
|
54
|
+
expect(unresolved).toEqual([]);
|
|
55
|
+
});
|
|
56
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-workflow-manager",
|
|
3
|
-
"version": "9.11.
|
|
3
|
+
"version": "9.11.3",
|
|
4
4
|
"main": "dist/src/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"awm": "./dist/src/index.js"
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"@types/js-yaml": "4.0.9",
|
|
60
60
|
"@types/node": "^25.3.0",
|
|
61
61
|
"@types/semver": "^7.7.1",
|
|
62
|
+
"@typescript-eslint/parser": "^8.70.0",
|
|
62
63
|
"dependency-cruiser": "16.10.4",
|
|
63
64
|
"eslint": "10.8.1",
|
|
64
65
|
"jest": "^30.2.0",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|