agentic-workflow-manager 6.5.2 → 8.0.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/scripts/sensor-support-matrix.js +103 -0
- package/dist/scripts/support-matrix.js +40 -17
- package/dist/src/commands/init.js +1 -3
- package/dist/src/commands/ledger/index.js +6 -0
- package/dist/src/commands/preflight/checks.js +4 -4
- package/dist/src/commands/preflight/index.js +2 -2
- package/dist/src/commands/registry/add.js +2 -2
- package/dist/src/commands/registry/index.js +5 -0
- package/dist/src/commands/registry/status.js +7 -0
- package/dist/src/commands/sensors/compatibility/contract.js +261 -0
- package/dist/src/commands/sensors/compatibility/discovery.js +207 -0
- package/dist/src/commands/sensors/compatibility/live.js +59 -0
- package/dist/src/commands/sensors/compatibility/manifest.js +197 -0
- package/dist/src/commands/sensors/compatibility/materialize.js +126 -0
- package/dist/src/commands/sensors/compatibility/pack-source.js +78 -0
- package/dist/src/commands/sensors/compatibility/probe.js +47 -0
- package/dist/src/commands/sensors/compatibility/resolve.js +96 -0
- package/dist/src/commands/sensors/compatibility/types.js +2 -0
- package/dist/src/commands/sensors/coverage/contract.js +0 -50
- package/dist/src/commands/sensors/coverage/empirical.js +131 -0
- package/dist/src/commands/sensors/coverage/evaluate.js +85 -7
- package/dist/src/commands/sensors/coverage/evidence.js +5 -1
- package/dist/src/commands/sensors/coverage/index.js +44 -7
- package/dist/src/commands/sensors/coverage/render.js +214 -14
- package/dist/src/commands/sensors/coverage/resolve.js +24 -38
- package/dist/src/commands/sensors/exec.js +150 -9
- package/dist/src/commands/sensors/index.js +18 -6
- package/dist/src/commands/sensors/init.js +92 -6
- package/dist/src/commands/sensors/run.js +71 -0
- package/dist/src/commands/sensors/status.js +48 -4
- package/dist/src/commands/sync.js +7 -0
- package/dist/src/core/bundles.js +38 -3
- package/dist/src/core/init/steps.js +7 -4
- package/dist/src/core/ledger/scan.js +228 -0
- package/dist/src/core/ledger/store.js +89 -25
- package/dist/src/core/ledger/types.js +54 -0
- package/dist/src/core/paths.js +10 -9
- package/dist/src/core/registries.js +134 -14
- package/dist/src/index.js +24 -1
- package/dist/tests/commands/ledger/index.test.js +11 -0
- package/dist/tests/commands/preflight/preflight.test.js +64 -64
- package/dist/tests/commands/registry/add.test.js +69 -2
- package/dist/tests/commands/registry/status.test.js +14 -0
- package/dist/tests/commands/sensors/compatibility/contract.test.js +98 -0
- package/dist/tests/commands/sensors/compatibility/discovery.test.js +119 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +68 -0
- package/dist/tests/commands/sensors/compatibility/materialize.test.js +53 -0
- package/dist/tests/commands/sensors/compatibility/pack-source.test.js +55 -0
- package/dist/tests/commands/sensors/compatibility/probe.test.js +27 -0
- package/dist/tests/commands/sensors/compatibility/resolve.test.js +75 -0
- package/dist/tests/commands/sensors/coverage/contract.test.js +8 -22
- package/dist/tests/commands/sensors/coverage/empirical.test.js +111 -0
- package/dist/tests/commands/sensors/coverage/evaluate.test.js +29 -0
- package/dist/tests/commands/sensors/coverage/evidence.test.js +9 -0
- package/dist/tests/commands/sensors/coverage/index.test.js +40 -13
- package/dist/tests/commands/sensors/coverage/render.test.js +187 -12
- package/dist/tests/commands/sensors/coverage/resolve.test.js +25 -4
- package/dist/tests/commands/sensors/exec-windows.test.js +92 -2
- package/dist/tests/commands/sensors/exec.test.js +67 -5
- package/dist/tests/commands/sensors/index.test.js +8 -0
- package/dist/tests/commands/sensors/init-pack-unavailable.test.js +12 -12
- package/dist/tests/commands/sensors/init.test.js +132 -48
- package/dist/tests/commands/sensors/run-inconclusive.test.js +9 -2
- package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
- package/dist/tests/commands/sensors/run.test.js +66 -3
- package/dist/tests/commands/sensors/status-windows.test.js +7 -7
- package/dist/tests/commands/sensors/status.test.js +74 -23
- package/dist/tests/commands/update.test.js +1 -1
- package/dist/tests/core/bundles.test.js +37 -0
- package/dist/tests/core/discovery.test.js +4 -0
- package/dist/tests/core/init/orchestrator.test.js +5 -1
- package/dist/tests/core/init/steps.test.js +20 -4
- package/dist/tests/core/ledger/scan.test.js +217 -0
- package/dist/tests/core/ledger/store.test.js +108 -0
- package/dist/tests/core/profile-pins.test.js +2 -1
- package/dist/tests/core/project-skill-links.test.js +22 -0
- package/dist/tests/core/registries-capability.test.js +10 -0
- package/dist/tests/core/registries-sync.test.js +16 -0
- package/dist/tests/core/registries.test.js +129 -1
- package/dist/tests/core/registry-manifest.test.js +7 -0
- package/dist/tests/core/same-existing-path.test.js +35 -0
- package/dist/tests/core/sync-gates.test.js +2 -1
- package/dist/tests/integration/copilot-init-isolated.test.js +18 -0
- package/dist/tests/integration/sensor-compatibility.e2e.test.js +141 -0
- package/dist/tests/integration/sensor-coverage.e2e.test.js +35 -6
- package/dist/tests/integration/sensor-structured-run.e2e.test.js +127 -0
- package/dist/tests/structural/active-documentation.test.js +128 -0
- package/dist/tests/structural/async-entrypoint.test.js +15 -0
- package/dist/tests/structural/jest-environment-is-isolated.test.js +44 -0
- package/dist/tests/structural/r3-cli-major-version.test.js +37 -0
- package/dist/tests/structural/sensor-documentation-contract.test.js +77 -0
- package/dist/tests/structural/support-matrix-is-current.test.js +69 -2
- package/package.json +8 -2
|
@@ -7,6 +7,8 @@ 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 status_1 = require("../../../src/commands/sensors/status");
|
|
10
|
+
const checks_1 = require("../../../src/commands/preflight/checks");
|
|
11
|
+
const run_1 = require("../../../src/commands/sensors/run");
|
|
10
12
|
// `resolveOnPath` resuelve PATH en proceso (ya no invoca un shell — ver
|
|
11
13
|
// core/paths.ts). Por eso estos tests controlan un PATH aislado en vez de
|
|
12
14
|
// mockear `execSync`: ademas de reflejar el mecanismo real, los vuelve
|
|
@@ -36,8 +38,8 @@ describe('computeSensorStatus', () => {
|
|
|
36
38
|
fs_1.default.writeFileSync(p, '#!/bin/sh\nexit 0\n');
|
|
37
39
|
fs_1.default.chmodSync(p, 0o755);
|
|
38
40
|
}
|
|
39
|
-
it('returns NOT_CONFIGURED when .awm/sensors.json missing', () => {
|
|
40
|
-
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
41
|
+
it('returns NOT_CONFIGURED when .awm/sensors.json missing', async () => {
|
|
42
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
41
43
|
expect(result.overall).toBe('NOT_CONFIGURED');
|
|
42
44
|
expect(result.pack).toBeNull();
|
|
43
45
|
});
|
|
@@ -47,42 +49,42 @@ describe('computeSensorStatus', () => {
|
|
|
47
49
|
fs_1.default.mkdirSync(binDir, { recursive: true });
|
|
48
50
|
fs_1.default.writeFileSync(path_1.default.join(binDir, tool), '');
|
|
49
51
|
}
|
|
50
|
-
it('
|
|
52
|
+
it('keeps an operational legacy manifest DEGRADED even when its npx tool is installed locally', async () => {
|
|
51
53
|
installLocalBin('tsc');
|
|
52
54
|
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
53
55
|
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
54
56
|
pack: 'js-ts',
|
|
55
57
|
sensors: { typecheck: { cmd: 'npx tsc --noEmit', fast: true } }
|
|
56
58
|
}));
|
|
57
|
-
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
58
|
-
expect(result.overall).toBe('
|
|
59
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
60
|
+
expect(result.overall).toBe('DEGRADED');
|
|
59
61
|
expect(result.pack).toBe('js-ts');
|
|
60
62
|
expect(result.checks.typecheck.ok).toBe(true);
|
|
61
63
|
});
|
|
62
|
-
it('marks an npx sensor DEGRADED when the tool is NOT installed locally (npx would fetch a remote package)', () => {
|
|
64
|
+
it('marks an npx sensor DEGRADED when the tool is NOT installed locally (npx would fetch a remote package)', async () => {
|
|
63
65
|
// No node_modules/.bin/depcruise → status must NOT report ✔ just because npx exists.
|
|
64
66
|
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
65
67
|
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
66
68
|
pack: 'js-ts',
|
|
67
69
|
sensors: { depcheck: { cmd: 'npx depcruise --config .dep-cruiser.awm.js app', fast: false } }
|
|
68
70
|
}));
|
|
69
|
-
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
71
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
70
72
|
expect(result.overall).toBe('DEGRADED');
|
|
71
73
|
expect(result.checks.depcheck.ok).toBe(false);
|
|
72
74
|
expect(result.checks.depcheck.detail).toMatch(/not installed locally/i);
|
|
73
75
|
});
|
|
74
|
-
it('marks a sensor DEGRADED when its --config file is missing', () => {
|
|
76
|
+
it('marks a sensor DEGRADED when its --config file is missing', async () => {
|
|
75
77
|
installLocalBin('eslint');
|
|
76
78
|
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
77
79
|
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
78
80
|
pack: 'js-ts',
|
|
79
81
|
sensors: { lint: { cmd: 'npx eslint . --config eslint.config.awm.mjs --format json', fast: true } }
|
|
80
82
|
}));
|
|
81
|
-
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
83
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
82
84
|
expect(result.checks.lint.ok).toBe(false);
|
|
83
85
|
expect(result.checks.lint.detail).toMatch(/missing config/i);
|
|
84
86
|
});
|
|
85
|
-
it('is HEALTHY when the npx tool is installed and the --config file exists', () => {
|
|
87
|
+
it('is HEALTHY when the npx tool is installed and the --config file exists', async () => {
|
|
86
88
|
installLocalBin('eslint');
|
|
87
89
|
fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'eslint.config.awm.mjs'), 'export default []');
|
|
88
90
|
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
@@ -90,17 +92,17 @@ describe('computeSensorStatus', () => {
|
|
|
90
92
|
pack: 'js-ts',
|
|
91
93
|
sensors: { lint: { cmd: 'npx eslint . --config eslint.config.awm.mjs --format json', fast: true } }
|
|
92
94
|
}));
|
|
93
|
-
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
95
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
94
96
|
expect(result.checks.lint.ok).toBe(true);
|
|
95
97
|
});
|
|
96
|
-
it('returns DEGRADED when a binary is missing', () => {
|
|
98
|
+
it('returns DEGRADED when a binary is missing', async () => {
|
|
97
99
|
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
98
100
|
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
99
101
|
pack: 'js-ts',
|
|
100
102
|
sensors: { security: { cmd: 'semgrep --json .', fast: false } }
|
|
101
103
|
}));
|
|
102
104
|
// PATH aislado y vacio => semgrep no resuelve.
|
|
103
|
-
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
105
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
104
106
|
expect(result.overall).toBe('DEGRADED');
|
|
105
107
|
expect(result.checks.security.ok).toBe(false);
|
|
106
108
|
});
|
|
@@ -112,19 +114,19 @@ describe('computeSensorStatus', () => {
|
|
|
112
114
|
afterEach(() => {
|
|
113
115
|
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
|
114
116
|
});
|
|
115
|
-
it('resuelve un binario instalado recorriendo PATH, sin invocar un shell', () => {
|
|
117
|
+
it('resuelve un binario instalado recorriendo PATH, sin invocar un shell', async () => {
|
|
116
118
|
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
117
119
|
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
118
120
|
pack: 'js-ts',
|
|
119
121
|
sensors: { security: { cmd: 'semgrep --json .', fast: false } }
|
|
120
122
|
}));
|
|
121
123
|
installOnPath('semgrep');
|
|
122
|
-
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
123
|
-
expect(result.overall).toBe('
|
|
124
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
125
|
+
expect(result.overall).toBe('DEGRADED');
|
|
124
126
|
expect(result.checks.security.ok).toBe(true);
|
|
125
127
|
});
|
|
126
128
|
});
|
|
127
|
-
it('is DEGRADED (never HEALTHY) when the manifest has zero sensor entries', () => {
|
|
129
|
+
it('is DEGRADED (never HEALTHY) when the manifest has zero sensor entries', async () => {
|
|
128
130
|
// `Object.values({}).every(...)` is vacuously true — guard against reading an
|
|
129
131
|
// empty manifest (the honest floor when the registry had no pack.json for the
|
|
130
132
|
// detected stack) as a clean run that found nothing wrong.
|
|
@@ -133,11 +135,11 @@ describe('computeSensorStatus', () => {
|
|
|
133
135
|
pack: 'python',
|
|
134
136
|
sensors: {},
|
|
135
137
|
}));
|
|
136
|
-
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
138
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
137
139
|
expect(result.overall).toBe('DEGRADED');
|
|
138
140
|
expect(result.pack).toBe('python');
|
|
139
141
|
});
|
|
140
|
-
it('degrades gracefully (never throws) when sensors is null in a hand-edited manifest', () => {
|
|
142
|
+
it('degrades gracefully (never throws) when sensors is null in a hand-edited manifest', async () => {
|
|
141
143
|
// Regression for Finding 3: `checkManifest` (preflight) already guards
|
|
142
144
|
// `manifest.sensors ?? {}` — computeSensorStatus needs the same guard, or a
|
|
143
145
|
// corrupted/hand-edited manifest with `"sensors": null` crashes
|
|
@@ -147,20 +149,69 @@ describe('computeSensorStatus', () => {
|
|
|
147
149
|
pack: 'python',
|
|
148
150
|
sensors: null,
|
|
149
151
|
}));
|
|
150
|
-
|
|
151
|
-
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
152
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
152
153
|
expect(result.overall).toBe('DEGRADED');
|
|
153
154
|
expect(result.pack).toBe('python');
|
|
154
155
|
expect(result.checks).toEqual({});
|
|
155
156
|
});
|
|
156
|
-
it('marks disabled sensors as ok', () => {
|
|
157
|
+
it('marks disabled sensors as ok', async () => {
|
|
157
158
|
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
158
159
|
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
159
160
|
pack: 'js-ts',
|
|
160
161
|
sensors: { mutation: { enabled: false } }
|
|
161
162
|
}));
|
|
162
|
-
const result = (0, status_1.computeSensorStatus)(tmpDir);
|
|
163
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
163
164
|
expect(result.checks.mutation.ok).toBe(true);
|
|
164
165
|
expect(result.checks.mutation.detail).toBe('disabled');
|
|
165
166
|
});
|
|
167
|
+
it('does not trust the initialized v2 variant after local tool drift', async () => {
|
|
168
|
+
// The manifest records ESLint 9 at init time, but local evidence is now ESLint
|
|
169
|
+
// 10. Status must resolve the installed pack again, not certify the stale
|
|
170
|
+
// initializedCompatibility record.
|
|
171
|
+
const previousHome = process.env.AWM_HOME;
|
|
172
|
+
const home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-home-'));
|
|
173
|
+
try {
|
|
174
|
+
process.env.AWM_HOME = home;
|
|
175
|
+
const registry = path_1.default.join(home, 'registries', 'baseline');
|
|
176
|
+
fs_1.default.mkdirSync(path_1.default.join(registry, 'sensor-packs', 'js-ts'), { recursive: true });
|
|
177
|
+
fs_1.default.writeFileSync(path_1.default.join(home, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.test/baseline.git' }]));
|
|
178
|
+
const variant = (id, range) => ({
|
|
179
|
+
id, priority: 10, certifiedRange: range,
|
|
180
|
+
requirements: { tool: 'eslint', toolRange: range, runtime: 'node', runtimeRange: '>=0.0.0' },
|
|
181
|
+
assets: ['eslint.config.awm.mjs'], formatter: 'eslint-llm', probe: { kind: 'config-present' },
|
|
182
|
+
command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
|
|
183
|
+
});
|
|
184
|
+
fs_1.default.writeFileSync(path_1.default.join(registry, 'sensor-packs', 'js-ts', 'pack.json'), JSON.stringify({
|
|
185
|
+
schemaVersion: 2, name: 'js-ts', description: 'test', detects: ['package.json'], coverage: { schemaVersion: 1, classes: { lint: { description: 'lint', detectors: [{ sensor: 'lint' }], remedy: { summary: 'fix lint', command: 'awm sensors init --pack js-ts' } } } },
|
|
186
|
+
sensors: { lint: { applicability: { allFiles: ['package.json'] }, variants: [variant('eslint-9', '>=9.0.0 <10.0.0'), variant('eslint-10', '>=10.0.0 <11.0.0')] } },
|
|
187
|
+
}));
|
|
188
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'package.json'), JSON.stringify({ devDependencies: { eslint: '^10.0.0' } }));
|
|
189
|
+
fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'node_modules', 'eslint'), { recursive: true });
|
|
190
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version: '10.0.0' }));
|
|
191
|
+
fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
|
|
192
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
|
|
193
|
+
schemaVersion: 2, pack: 'js-ts', sensors: { lint: {
|
|
194
|
+
enabled: true, variantId: 'eslint-9', command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
|
|
195
|
+
initializedCompatibility: { state: 'certified', reason: 'range-and-probe', variantId: 'eslint-9', toolVersion: '9.0.0', runtimeVersion: process.versions.node, certifiedRange: '>=9.0.0 <10.0.0', evidence: [] },
|
|
196
|
+
} },
|
|
197
|
+
}));
|
|
198
|
+
const result = await (0, status_1.computeSensorStatus)(tmpDir);
|
|
199
|
+
expect(result.overall).toBe('DEGRADED');
|
|
200
|
+
expect(result.checks.lint).toMatchObject({ ok: false, detail: expect.stringContaining('variant-drift') });
|
|
201
|
+
fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'AGENTS.md'), '# test\n');
|
|
202
|
+
const gate = await (0, checks_1.preflight)(tmpDir);
|
|
203
|
+
expect(gate.status).toBe('degraded');
|
|
204
|
+
expect(gate.checks.find(check => check.id === 'tools')).toMatchObject({ ok: false, detail: expect.stringContaining('variant-drift') });
|
|
205
|
+
const run = await (0, run_1.runSensors)({ cwd: tmpDir, all: true });
|
|
206
|
+
expect(run.overall).toBe('not_certified');
|
|
207
|
+
expect(run.sensors).toEqual([expect.objectContaining({ name: 'lint', status: 'inconclusive', skipReason: expect.stringContaining('variant-drift') })]);
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
if (previousHome === undefined)
|
|
211
|
+
delete process.env.AWM_HOME;
|
|
212
|
+
else
|
|
213
|
+
process.env.AWM_HOME = previousHome;
|
|
214
|
+
fs_1.default.rmSync(home, { recursive: true, force: true });
|
|
215
|
+
}
|
|
216
|
+
});
|
|
166
217
|
});
|
|
@@ -137,7 +137,7 @@ describe('awm update — honest outcome', () => {
|
|
|
137
137
|
// `unusableSyncedRegistries` mira contenido EN DISCO: se siembra el content root
|
|
138
138
|
// y el registries.json para que `docs` cuente como stale, no como roto.
|
|
139
139
|
const registriesDir = path_1.default.join(process.env.AWM_HOME, 'registries');
|
|
140
|
-
fs_1.default.mkdirSync(path_1.default.join(registriesDir, 'docs'), { recursive: true });
|
|
140
|
+
fs_1.default.mkdirSync(path_1.default.join(registriesDir, 'docs', 'skills'), { recursive: true });
|
|
141
141
|
fs_1.default.writeFileSync(path_1.default.join(process.env.AWM_HOME, 'registries.json'), JSON.stringify([{ name: 'docs', remote: 'https://example.test/docs.git' }]));
|
|
142
142
|
const { runUpdateCore, updateOutro } = require('../../src/commands/update');
|
|
143
143
|
const result = await runUpdateCore({}, deps({
|
|
@@ -37,6 +37,14 @@ describe('readCatalog', () => {
|
|
|
37
37
|
expect(entries.map((e) => e.name).sort()).toEqual(['dev', 'frontend']);
|
|
38
38
|
expect(entries.find((e) => e.name === 'dev').scope).toBe('baseline');
|
|
39
39
|
});
|
|
40
|
+
it('rejects a catalog symlink instead of reading outside the registry', () => {
|
|
41
|
+
const content = makeFixture();
|
|
42
|
+
const outside = path_1.default.join(path_1.default.dirname(content), 'outside-catalog.json');
|
|
43
|
+
fs_1.default.writeFileSync(outside, JSON.stringify({ bundles: [] }));
|
|
44
|
+
fs_1.default.rmSync(path_1.default.join(content, 'catalog.json'));
|
|
45
|
+
fs_1.default.symlinkSync(outside, path_1.default.join(content, 'catalog.json'));
|
|
46
|
+
expect(() => (0, bundles_1.readCatalog)(content)).toThrow(/symbolic link/);
|
|
47
|
+
});
|
|
40
48
|
});
|
|
41
49
|
describe('discoverBundles', () => {
|
|
42
50
|
it('loads each bundle and normalizes skill refs (string | object)', () => {
|
|
@@ -54,6 +62,35 @@ describe('discoverBundles', () => {
|
|
|
54
62
|
const empty = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-empty-'));
|
|
55
63
|
expect((0, bundles_1.discoverBundles)(empty)).toEqual([]);
|
|
56
64
|
});
|
|
65
|
+
it('rejects catalog sources that escape the registry content root', () => {
|
|
66
|
+
const content = makeFixture();
|
|
67
|
+
const outside = path_1.default.join(path_1.default.dirname(content), 'outside');
|
|
68
|
+
fs_1.default.mkdirSync(outside, { recursive: true });
|
|
69
|
+
fs_1.default.writeFileSync(path_1.default.join(outside, 'bundle.json'), JSON.stringify({ name: 'outside' }));
|
|
70
|
+
fs_1.default.writeFileSync(path_1.default.join(content, 'catalog.json'), JSON.stringify({
|
|
71
|
+
bundles: [{ name: 'outside', source: '../outside', version: '1.0.0', scope: 'project' }],
|
|
72
|
+
}));
|
|
73
|
+
expect(() => (0, bundles_1.discoverBundles)(content)).toThrow(/invalid bundle source/i);
|
|
74
|
+
});
|
|
75
|
+
it('rejects a bundle manifest symlink instead of reading outside the registry', () => {
|
|
76
|
+
const content = makeFixture();
|
|
77
|
+
const outside = path_1.default.join(path_1.default.dirname(content), 'outside-bundle.json');
|
|
78
|
+
fs_1.default.writeFileSync(outside, JSON.stringify({ name: 'outside' }));
|
|
79
|
+
fs_1.default.rmSync(path_1.default.join(content, 'bundles', 'dev', 'bundle.json'));
|
|
80
|
+
fs_1.default.symlinkSync(outside, path_1.default.join(content, 'bundles', 'dev', 'bundle.json'));
|
|
81
|
+
expect(() => (0, bundles_1.discoverBundles)(content)).toThrow(/symbolic link/);
|
|
82
|
+
});
|
|
83
|
+
it('rejects a catalog source that reaches a bundle through an intermediate symlink', () => {
|
|
84
|
+
const content = makeFixture();
|
|
85
|
+
const outside = path_1.default.join(path_1.default.dirname(content), 'outside-bundle');
|
|
86
|
+
fs_1.default.mkdirSync(outside, { recursive: true });
|
|
87
|
+
fs_1.default.writeFileSync(path_1.default.join(outside, 'bundle.json'), JSON.stringify({ name: 'outside' }));
|
|
88
|
+
fs_1.default.symlinkSync(outside, path_1.default.join(content, 'outside'));
|
|
89
|
+
fs_1.default.writeFileSync(path_1.default.join(content, 'catalog.json'), JSON.stringify({
|
|
90
|
+
bundles: [{ name: 'outside', source: 'outside', version: '1.0.0', scope: 'project' }],
|
|
91
|
+
}));
|
|
92
|
+
expect(() => (0, bundles_1.discoverBundles)(content)).toThrow(/symbolic link/);
|
|
93
|
+
});
|
|
57
94
|
});
|
|
58
95
|
describe('resolveBundleSkills', () => {
|
|
59
96
|
it('follows dependsOn transitively and dedupes', () => {
|
|
@@ -18,6 +18,10 @@ const WORKFLOWS_DIR = path_1.default.join(CONTENT_ROOT, 'workflows');
|
|
|
18
18
|
describe('Artifact Discovery', () => {
|
|
19
19
|
beforeEach(() => {
|
|
20
20
|
jest.clearAllMocks();
|
|
21
|
+
fs_1.default.lstatSync.mockImplementation(() => {
|
|
22
|
+
const error = Object.assign(new Error('missing'), { code: 'ENOENT' });
|
|
23
|
+
throw error;
|
|
24
|
+
});
|
|
21
25
|
});
|
|
22
26
|
describe('discoverSkills', () => {
|
|
23
27
|
it('should return a list of skill directories that contain a SKILL.md', () => {
|
|
@@ -19,7 +19,11 @@ function seedRegistry(contentRoot) {
|
|
|
19
19
|
fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'hooks', 'run-hook.cmd'), '#!/bin/sh\n');
|
|
20
20
|
fs_1.default.chmodSync(path_1.default.join(contentRoot, 'hooks', 'run-hook.cmd'), 0o755);
|
|
21
21
|
fs_1.default.mkdirSync(path_1.default.join(contentRoot, 'sensor-packs', 'js-ts'), { recursive: true });
|
|
22
|
-
fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'sensor-packs', 'js-ts', 'pack.json'),
|
|
22
|
+
fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'sensor-packs', 'js-ts', 'pack.json'),
|
|
23
|
+
// R3 validates even a legacy pack at the registry boundary. This fixture
|
|
24
|
+
// intentionally stays legacy (no schemaVersion) but must still carry its
|
|
25
|
+
// required stable identity, just like a real legacy registry pack.
|
|
26
|
+
JSON.stringify({ name: 'js-ts', sensors: { lint: { defaultCmd: 'eslint {{SOURCE_DIRS}}', fast: true } } }));
|
|
23
27
|
// catalog + bundle dev (baseline)
|
|
24
28
|
fs_1.default.writeFileSync(path_1.default.join(contentRoot, 'catalog.json'), JSON.stringify({
|
|
25
29
|
version: 1, bundles: [{ name: 'dev', source: './bundles/dev', version: '1.0.0', scope: 'baseline' }],
|
|
@@ -143,6 +143,22 @@ describe('stepHook / stepDevCore / stepAmbient', () => {
|
|
|
143
143
|
expect((0, steps_1.stepDevCore)(deps({ machine: m, project: null }, a)).action).toBe('applied');
|
|
144
144
|
expect(a.installBundle).toHaveBeenCalled();
|
|
145
145
|
});
|
|
146
|
+
it('defers local-only baseline bundles during --machine-only (R7)', () => {
|
|
147
|
+
const a = spies();
|
|
148
|
+
const m = machine();
|
|
149
|
+
m.devCore = { present: false, brokenLinks: [] };
|
|
150
|
+
const r = (0, steps_1.stepDevCore)(deps({ machine: m, project: null }, a, {
|
|
151
|
+
agent: 'copilot',
|
|
152
|
+
enabledAgents: ['copilot'],
|
|
153
|
+
machineOnly: true,
|
|
154
|
+
}));
|
|
155
|
+
expect(r).toMatchObject({
|
|
156
|
+
action: 'skipped',
|
|
157
|
+
level: 'machine',
|
|
158
|
+
});
|
|
159
|
+
expect(r.detail).toMatch(/project scope.*deferred/i);
|
|
160
|
+
expect(a.installBundle).not.toHaveBeenCalled();
|
|
161
|
+
});
|
|
146
162
|
// Regression for the confirmed production bug: `awm init -a copilot` crashed
|
|
147
163
|
// 100% of the time with "machine.devCore: skill global scope is not
|
|
148
164
|
// supported by Copilot...", rolling back the ENTIRE init transaction (even
|
|
@@ -386,13 +402,13 @@ describe('stepActivation', () => {
|
|
|
386
402
|
});
|
|
387
403
|
});
|
|
388
404
|
describe('stepSensors', () => {
|
|
389
|
-
it('skips when sensors present', () => {
|
|
405
|
+
it('skips when sensors present', async () => {
|
|
390
406
|
const a = spies();
|
|
391
|
-
expect((0, steps_1.stepSensors)(deps({ machine: machine(), project: project() }, a)).action).toBe('skipped');
|
|
407
|
+
expect((await (0, steps_1.stepSensors)(deps({ machine: machine(), project: project() }, a))).action).toBe('skipped');
|
|
392
408
|
});
|
|
393
|
-
it('inits sensors when absent', () => {
|
|
409
|
+
it('inits sensors when absent', async () => {
|
|
394
410
|
const a = spies();
|
|
395
|
-
const r = (0, steps_1.stepSensors)(deps({ machine: machine(), project: project({ sensors: { present: false } }) }, a));
|
|
411
|
+
const r = await (0, steps_1.stepSensors)(deps({ machine: machine(), project: project({ sensors: { present: false } }) }, a));
|
|
396
412
|
expect(r.action).toBe('applied');
|
|
397
413
|
expect(a.initSensors).toHaveBeenCalledWith({ cwd: '/repo', registryRoot: '/cache/registry', configure: true }); // sensorPacksRoot='/cache/registry' from deps()
|
|
398
414
|
});
|
|
@@ -0,0 +1,217 @@
|
|
|
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 scan_1 = require("../../../src/core/ledger/scan");
|
|
10
|
+
function mkTmp() {
|
|
11
|
+
return fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-ledger-scan-'));
|
|
12
|
+
}
|
|
13
|
+
function entry(over = {}) {
|
|
14
|
+
return {
|
|
15
|
+
ts: '2026-08-14T00:00:00.000Z', branch: 'feat-x', phase: 'qa', source_skill: 'post-implementation-qa',
|
|
16
|
+
polarity: 'finding', class: 'logica', signature: 'lint-error', severity: 'important', desc: 'private detail',
|
|
17
|
+
defectClass: 'lint-errors', ...over,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function write(root, relative, lines) {
|
|
21
|
+
const target = path_1.default.join(root, relative);
|
|
22
|
+
fs_1.default.mkdirSync(path_1.default.dirname(target), { recursive: true });
|
|
23
|
+
fs_1.default.writeFileSync(target, lines.map(line => typeof line === 'string' ? line : JSON.stringify(line)).join('\n') + '\n');
|
|
24
|
+
}
|
|
25
|
+
describe('scanProjectLedgers', () => {
|
|
26
|
+
let root;
|
|
27
|
+
beforeEach(() => { root = mkTmp(); });
|
|
28
|
+
afterEach(() => { fs_1.default.rmSync(root, { recursive: true, force: true }); });
|
|
29
|
+
test('reads only direct active and archive files in deterministic path and line order', () => {
|
|
30
|
+
write(root, '.awm/ledger/z.jsonl', [entry({ signature: 'z' })]);
|
|
31
|
+
write(root, '.awm/ledger/a.jsonl', [entry({ signature: 'a' })]);
|
|
32
|
+
write(root, '.awm/ledger/archive/b.jsonl', [entry({ signature: 'b' })]);
|
|
33
|
+
write(root, '.awm/ledger/archive/nested/ignored.jsonl', [entry({ signature: 'ignored' })]);
|
|
34
|
+
const scan = (0, scan_1.scanProjectLedgers)(root);
|
|
35
|
+
expect(scan.entries.map(item => item.source)).toEqual([
|
|
36
|
+
'.awm/ledger/a.jsonl:1', '.awm/ledger/archive/b.jsonl:1', '.awm/ledger/z.jsonl:1',
|
|
37
|
+
]);
|
|
38
|
+
expect(scan.sources).toMatchObject({ activeFiles: 2, archivedFiles: 1, validFindings: 3, skippedFindings: 0 });
|
|
39
|
+
});
|
|
40
|
+
test('orders direct active and archive sources together by their relative path', () => {
|
|
41
|
+
write(root, '.awm/ledger/b.jsonl', [entry({ signature: 'active-b' })]);
|
|
42
|
+
write(root, '.awm/ledger/archive/a.jsonl', [entry({ signature: 'archive-a' })]);
|
|
43
|
+
const scan = (0, scan_1.scanProjectLedgers)(root);
|
|
44
|
+
expect(scan.entries.map(item => item.source)).toEqual([
|
|
45
|
+
'.awm/ledger/archive/a.jsonl:1', '.awm/ledger/b.jsonl:1',
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
test('counts wins as valid but excludes them from findings', () => {
|
|
49
|
+
write(root, '.awm/ledger/a.jsonl', [entry({ polarity: 'win' }), entry({ signature: 'finding' })]);
|
|
50
|
+
const scan = (0, scan_1.scanProjectLedgers)(root);
|
|
51
|
+
expect(scan.entries.map(item => item.entry.signature)).toEqual(['finding']);
|
|
52
|
+
expect(scan.sources).toMatchObject({ validEntries: 2, validFindings: 1 });
|
|
53
|
+
});
|
|
54
|
+
test.each([
|
|
55
|
+
['invalid JSON', 'invalid-json', ['{not-json'], {}],
|
|
56
|
+
['invalid persisted class', 'invalid-defect-class', [{ ...entry(), defectClass: 'Bad_ID' }], {}],
|
|
57
|
+
['oversized line', 'line-too-large', [JSON.stringify(entry()) + 'x'.repeat(100)], { maxLineBytes: 64 }],
|
|
58
|
+
])('reports %s as a typed skipped finding', (_name, reason, lines, limits) => {
|
|
59
|
+
write(root, '.awm/ledger/a.jsonl', lines);
|
|
60
|
+
const scan = (0, scan_1.scanProjectLedgers)(root, limits);
|
|
61
|
+
expect(scan.sources.skippedFindings).toBe(1);
|
|
62
|
+
expect(scan.sources.skippedByReason).toMatchObject({ [reason]: 1 });
|
|
63
|
+
});
|
|
64
|
+
test('rejects a symlink without reading its target', () => {
|
|
65
|
+
const outside = path_1.default.join(root, 'outside.jsonl');
|
|
66
|
+
fs_1.default.writeFileSync(outside, JSON.stringify(entry()));
|
|
67
|
+
const ledger = path_1.default.join(root, '.awm', 'ledger');
|
|
68
|
+
fs_1.default.mkdirSync(ledger, { recursive: true });
|
|
69
|
+
fs_1.default.symlinkSync(outside, path_1.default.join(ledger, 'linked.jsonl'));
|
|
70
|
+
const scan = (0, scan_1.scanProjectLedgers)(root);
|
|
71
|
+
expect(scan.entries).toEqual([]);
|
|
72
|
+
expect(scan.sources.skippedByReason).toMatchObject({ 'symlink-entry': 1 });
|
|
73
|
+
});
|
|
74
|
+
test('records direct non-regular entries and never descends into them', () => {
|
|
75
|
+
fs_1.default.mkdirSync(path_1.default.join(root, '.awm', 'ledger', 'directory.jsonl'), { recursive: true });
|
|
76
|
+
write(root, '.awm/ledger/directory.jsonl/hidden.jsonl', [entry({ signature: 'hidden' })]);
|
|
77
|
+
const scan = (0, scan_1.scanProjectLedgers)(root);
|
|
78
|
+
expect(scan.entries).toEqual([]);
|
|
79
|
+
expect(scan.sources.skippedByReason).toMatchObject({ 'nonregular-entry': 1 });
|
|
80
|
+
});
|
|
81
|
+
test('reports file and entry limits once without reading candidates beyond the bound', () => {
|
|
82
|
+
write(root, '.awm/ledger/a.jsonl', [entry({ signature: 'a' })]);
|
|
83
|
+
write(root, '.awm/ledger/b.jsonl', [entry({ signature: 'b' })]);
|
|
84
|
+
const files = (0, scan_1.scanProjectLedgers)(root, { maxFiles: 1 });
|
|
85
|
+
expect(files.entries.map(item => item.entry.signature)).toEqual(['a']);
|
|
86
|
+
expect(files.sources.skippedByReason).toMatchObject({ 'file-limit': 1 });
|
|
87
|
+
write(root, '.awm/ledger/entries.jsonl', [entry({ signature: 'first' }), entry({ signature: 'second' })]);
|
|
88
|
+
const entries = (0, scan_1.scanProjectLedgers)(root, { maxEntries: 1, maxFiles: 10 });
|
|
89
|
+
expect(entries.entries).toHaveLength(1);
|
|
90
|
+
expect(entries.sources.skippedByReason).toMatchObject({ 'entry-limit': 1 });
|
|
91
|
+
});
|
|
92
|
+
test('enumerates file candidates with bounded memory and never opens a source after maxFiles', () => {
|
|
93
|
+
write(root, '.awm/ledger/a.jsonl', [entry({ signature: 'first' })]);
|
|
94
|
+
write(root, '.awm/ledger/b.jsonl', [entry({ signature: 'must-not-open' })]);
|
|
95
|
+
write(root, '.awm/ledger/archive/c.jsonl', [entry({ signature: 'also-must-not-open' })]);
|
|
96
|
+
const readSpy = jest.spyOn(fs_1.default, 'readFileSync');
|
|
97
|
+
const readdirSpy = jest.spyOn(fs_1.default, 'readdirSync').mockImplementation(() => {
|
|
98
|
+
throw new Error('unbounded readdir must not be used for ledger candidates');
|
|
99
|
+
});
|
|
100
|
+
try {
|
|
101
|
+
const scan = (0, scan_1.scanProjectLedgers)(root, { maxFiles: 1 });
|
|
102
|
+
expect(scan.entries.map(item => item.entry.signature)).toEqual(['first']);
|
|
103
|
+
expect(scan.sources.skippedByReason).toMatchObject({ 'file-limit': 1 });
|
|
104
|
+
expect(readSpy.mock.calls.map(([candidate]) => String(candidate))).not.toContain(path_1.default.join(root, '.awm', 'ledger', 'b.jsonl'));
|
|
105
|
+
expect(readSpy.mock.calls.map(([candidate]) => String(candidate))).not.toContain(path_1.default.join(root, '.awm', 'ledger', 'archive', 'c.jsonl'));
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
readdirSpy.mockRestore();
|
|
109
|
+
readSpy.mockRestore();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
test('sorts the retained candidate window instead of assuming host directory order', () => {
|
|
113
|
+
write(root, '.awm/ledger/a.jsonl', [entry({ signature: 'first' })]);
|
|
114
|
+
write(root, '.awm/ledger/b.jsonl', [entry({ signature: 'witness' })]);
|
|
115
|
+
write(root, '.awm/ledger/c.jsonl', [entry({ signature: 'must-not-enumerate' })]);
|
|
116
|
+
const ledgerDirectory = path_1.default.join(root, '.awm', 'ledger');
|
|
117
|
+
const originalOpen = fs_1.default.opendirSync;
|
|
118
|
+
let readCalls = 0;
|
|
119
|
+
const openSpy = jest.spyOn(fs_1.default, 'opendirSync').mockImplementation((directory, options) => {
|
|
120
|
+
if (String(directory) === ledgerDirectory) {
|
|
121
|
+
const names = ['b.jsonl', 'a.jsonl', 'c.jsonl'];
|
|
122
|
+
let index = 0;
|
|
123
|
+
return {
|
|
124
|
+
closeSync: jest.fn(),
|
|
125
|
+
readSync: jest.fn(() => {
|
|
126
|
+
readCalls += 1;
|
|
127
|
+
if (readCalls > 2)
|
|
128
|
+
throw new Error('candidate enumeration exceeded bounded window');
|
|
129
|
+
const name = names[index++];
|
|
130
|
+
return name === undefined ? null : { name };
|
|
131
|
+
}),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const handle = originalOpen(directory, options);
|
|
135
|
+
const originalRead = handle.readSync.bind(handle);
|
|
136
|
+
jest.spyOn(handle, 'readSync').mockImplementation(() => {
|
|
137
|
+
readCalls += 1;
|
|
138
|
+
if (readCalls > 2)
|
|
139
|
+
throw new Error('candidate enumeration exceeded bounded window');
|
|
140
|
+
return originalRead();
|
|
141
|
+
});
|
|
142
|
+
return handle;
|
|
143
|
+
});
|
|
144
|
+
try {
|
|
145
|
+
const scan = (0, scan_1.scanProjectLedgers)(root, { maxFiles: 1 });
|
|
146
|
+
expect(scan.entries.map(item => item.entry.signature)).toEqual(['first']);
|
|
147
|
+
expect(scan.sources.skippedByReason).toMatchObject({ 'file-limit': 1 });
|
|
148
|
+
expect(readCalls).toBe(2);
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
openSpy.mockRestore();
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
test('shares the candidate window across active and archive directories', () => {
|
|
155
|
+
write(root, '.awm/ledger/a.jsonl', [entry({ signature: 'active-first' })]);
|
|
156
|
+
write(root, '.awm/ledger/b.jsonl', [entry({ signature: 'active-witness' })]);
|
|
157
|
+
write(root, '.awm/ledger/archive/c.jsonl', [entry({ signature: 'archive-must-not-enumerate' })]);
|
|
158
|
+
const originalOpen = fs_1.default.opendirSync;
|
|
159
|
+
let candidateReads = 0;
|
|
160
|
+
const openSpy = jest.spyOn(fs_1.default, 'opendirSync').mockImplementation((directory, options) => {
|
|
161
|
+
const handle = originalOpen(directory, options);
|
|
162
|
+
const originalRead = handle.readSync.bind(handle);
|
|
163
|
+
jest.spyOn(handle, 'readSync').mockImplementation(() => {
|
|
164
|
+
const item = originalRead();
|
|
165
|
+
if (item?.name.endsWith('.jsonl')) {
|
|
166
|
+
candidateReads += 1;
|
|
167
|
+
if (candidateReads > 2)
|
|
168
|
+
throw new Error('candidate enumeration exceeded the global bounded window');
|
|
169
|
+
}
|
|
170
|
+
return item;
|
|
171
|
+
});
|
|
172
|
+
return handle;
|
|
173
|
+
});
|
|
174
|
+
try {
|
|
175
|
+
const scan = (0, scan_1.scanProjectLedgers)(root, { maxFiles: 1 });
|
|
176
|
+
expect(scan.entries.map(item => item.entry.signature)).toEqual(['active-first']);
|
|
177
|
+
expect(scan.sources.skippedByReason).toMatchObject({ 'file-limit': 1 });
|
|
178
|
+
expect(candidateReads).toBe(2);
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
openSpy.mockRestore();
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
test('reports a too-large file without parsing it', () => {
|
|
185
|
+
write(root, '.awm/ledger/a.jsonl', [entry()]);
|
|
186
|
+
const scan = (0, scan_1.scanProjectLedgers)(root, { maxFileBytes: 1 });
|
|
187
|
+
expect(scan.entries).toEqual([]);
|
|
188
|
+
expect(scan.sources.skippedByReason).toMatchObject({ 'file-too-large': 1 });
|
|
189
|
+
});
|
|
190
|
+
test('reports over-deep JSON before parsing it', () => {
|
|
191
|
+
write(root, '.awm/ledger/a.jsonl', ['{"x":{"x":{"x":{"x":{"x":1}}}}}']);
|
|
192
|
+
const scan = (0, scan_1.scanProjectLedgers)(root, { maxJsonDepth: 3 });
|
|
193
|
+
expect(scan.entries).toEqual([]);
|
|
194
|
+
expect(scan.sources.skippedByReason).toMatchObject({ 'json-too-deep': 1 });
|
|
195
|
+
});
|
|
196
|
+
test('keeps every finding for analysis while bounding renderable references per defect class', () => {
|
|
197
|
+
write(root, '.awm/ledger/a.jsonl', [
|
|
198
|
+
entry({ signature: 'first', defectClass: 'lint-errors' }),
|
|
199
|
+
entry({ signature: 'second', defectClass: 'lint-errors' }),
|
|
200
|
+
entry({ signature: 'other', defectClass: 'static-type-errors' }),
|
|
201
|
+
]);
|
|
202
|
+
const scan = (0, scan_1.scanProjectLedgers)(root, { maxRefsPerClass: 1 });
|
|
203
|
+
expect(scan.entries.map(item => item.entry.signature)).toEqual(['first', 'second', 'other']);
|
|
204
|
+
expect(scan.entries.map(item => item.evidenceRef)).toEqual([
|
|
205
|
+
'.awm/ledger/a.jsonl:1', null, '.awm/ledger/a.jsonl:3',
|
|
206
|
+
]);
|
|
207
|
+
expect(scan.omittedEvidenceRefs).toBe(1);
|
|
208
|
+
expect(scan.sources).toMatchObject({
|
|
209
|
+
validFindings: 3,
|
|
210
|
+
skippedFindings: 1,
|
|
211
|
+
skippedByReason: { 'evidence-ref-limit': 1 },
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
test('rejects non-object limit overrides instead of accepting an impossible public input', () => {
|
|
215
|
+
expect(() => (0, scan_1.scanProjectLedgers)(root, null)).toThrow(/limits.*object/i);
|
|
216
|
+
});
|
|
217
|
+
});
|