agentic-workflow-manager 9.21.0 → 9.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/src/commands/change/index.js +16 -5
  2. package/dist/src/commands/init.js +8 -4
  3. package/dist/src/commands/preflight/checks.js +22 -0
  4. package/dist/src/commands/registry/add.js +9 -3
  5. package/dist/src/commands/sensors/compatibility/contract.js +56 -3
  6. package/dist/src/commands/sensors/compatibility/manifest.js +16 -2
  7. package/dist/src/commands/sensors/index.js +10 -1
  8. package/dist/src/commands/sensors/inputs-digest.js +315 -0
  9. package/dist/src/commands/sensors/prepare.js +10 -0
  10. package/dist/src/commands/sensors/result-cache.js +278 -0
  11. package/dist/src/commands/sensors/result.js +31 -12
  12. package/dist/src/commands/sensors/run.js +265 -8
  13. package/dist/src/commands/sensors/status.js +66 -7
  14. package/dist/src/commands/sync.js +11 -5
  15. package/dist/src/commands/update.js +23 -12
  16. package/dist/src/core/changes/derive.js +6 -3
  17. package/dist/src/core/changes/git.js +1 -2
  18. package/dist/src/core/changes/receipt.js +219 -9
  19. package/dist/src/core/changes/worktree.js +15 -3
  20. package/dist/src/core/init/steps.js +3 -8
  21. package/dist/src/core/registries.js +20 -15
  22. package/dist/src/core/registry-git-errors.js +107 -0
  23. package/dist/src/core/registry-git.js +93 -0
  24. package/dist/src/core/registry-sync-report.js +74 -0
  25. package/dist/src/core/versioning.js +3 -6
  26. package/dist/src/index.js +5 -8
  27. package/dist/tests/commands/change/new.test.js +104 -58
  28. package/dist/tests/commands/change/validate-unmeasured.test.js +246 -0
  29. package/dist/tests/commands/change/validate.test.js +164 -108
  30. package/dist/tests/commands/changes/list.test.js +41 -22
  31. package/dist/tests/commands/job/gate-reconcile.test.js +13 -2
  32. package/dist/tests/commands/preflight/sensors-inputs.test.js +110 -0
  33. package/dist/tests/commands/registry-sync-honesty.test.js +175 -0
  34. package/dist/tests/commands/sensors/compatibility/contract.test.js +60 -0
  35. package/dist/tests/commands/sensors/compatibility/manifest.test.js +94 -0
  36. package/dist/tests/commands/sensors/exec.test.js +3 -1
  37. package/dist/tests/commands/sensors/inputs-digest.test.js +373 -0
  38. package/dist/tests/commands/sensors/result-cache.test.js +352 -0
  39. package/dist/tests/commands/sensors/run-cache.test.js +434 -0
  40. package/dist/tests/commands/sensors/run-slice.test.js +171 -0
  41. package/dist/tests/commands/sensors/status-inputs.test.js +271 -0
  42. package/dist/tests/commands/update.test.js +8 -8
  43. package/dist/tests/commands/watch/supervisor-loop.test.js +21 -5
  44. package/dist/tests/core/changes/helpers.js +15 -3
  45. package/dist/tests/core/changes/list-paging.test.js +131 -110
  46. package/dist/tests/core/changes/list.test.js +133 -83
  47. package/dist/tests/core/changes/receipt.test.js +279 -8
  48. package/dist/tests/core/diagnostics/provider-checks.test.js +3 -0
  49. package/dist/tests/core/init/steps-registry-sync.test.js +6 -8
  50. package/dist/tests/core/registry-git.test.js +153 -0
  51. package/dist/tests/helpers/commit-seeded-registry.js +18 -0
  52. package/dist/tests/integration/codex-provider-isolated.test.js +7 -2
  53. package/dist/tests/integration/copilot-init-isolated.test.js +7 -4
  54. package/dist/tests/structural/active-documentation.test.js +0 -41
  55. package/dist/tests/structural/bundle-skill-reference-contract.test.js +0 -12
  56. package/dist/tests/structural/cli-reference-matches-commander.test.js +94 -0
  57. package/dist/tests/structural/native-release-artifacts.test.js +116 -107
  58. package/dist/tests/structural/sensor-documentation-contract.test.js +0 -47
  59. package/dist/tests/structural/sensor-portability-contract.test.js +0 -7
  60. package/dist/tests/structural/simple-git-v4-pin.test.js +49 -0
  61. package/package.json +3 -2
  62. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  63. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  64. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  65. package/prebuilds/win32-x64/secure_fs.node +0 -0
  66. package/dist/tests/structural/cloud-admission-docs-contract.test.js +0 -54
  67. package/dist/tests/structural/documentation-phase-is-mechanized.test.js +0 -29
  68. package/dist/tests/structural/operator-environments-docs-contract.test.js +0 -70
  69. package/dist/tests/structural/process-model-description-contract.test.js +0 -34
  70. package/dist/tests/structural/project-declared-sensors-docs-contract.test.js +0 -104
  71. package/dist/tests/structural/r4-plan-currentness-docs.test.js +0 -37
@@ -0,0 +1,434 @@
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 child_process_1 = require("child_process");
10
+ const baseline_1 = require("../../../src/commands/sensors/baseline");
11
+ const mockRunStructuredCommand = jest.fn();
12
+ const mockRunCommand = jest.fn();
13
+ jest.mock('../../../src/commands/sensors/exec', () => ({
14
+ runCommand: (...args) => mockRunCommand(...args),
15
+ runStructuredCommand: (...args) => mockRunStructuredCommand(...args),
16
+ }));
17
+ const { ok, exited } = require('./exec-fixtures');
18
+ function git(cwd, args) {
19
+ return (0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', ...args], {
20
+ cwd,
21
+ encoding: 'utf8',
22
+ stdio: ['ignore', 'pipe', 'pipe'],
23
+ });
24
+ }
25
+ describe('runSensors result cache (RF-2.*, RF-6.*, RF-E.1/E.3)', () => {
26
+ let project;
27
+ let home;
28
+ const version = '10.0.0';
29
+ const eslintFinding = [{
30
+ filePath: 'src/a.ts',
31
+ messages: [{ line: 1, column: 1, message: 'no-x', ruleId: 'no-x', severity: 2 }],
32
+ }];
33
+ beforeEach(() => {
34
+ jest.resetModules();
35
+ mockRunStructuredCommand.mockReset();
36
+ mockRunCommand.mockReset();
37
+ project = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-run-cache-'));
38
+ home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-run-cache-home-'));
39
+ process.env.AWM_HOME = home;
40
+ const registry = path_1.default.join(home, 'registries', 'baseline', 'sensor-packs', 'js-ts');
41
+ fs_1.default.mkdirSync(registry, { recursive: true });
42
+ fs_1.default.writeFileSync(path_1.default.join(registry, 'eslint.config.awm.mjs'), 'export default []');
43
+ fs_1.default.writeFileSync(path_1.default.join(home, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.test/baseline.git' }]));
44
+ fs_1.default.writeFileSync(path_1.default.join(project, 'package.json'), JSON.stringify({ scripts: { lint: 'eslint .' }, devDependencies: { eslint: '^10.0.0' } }));
45
+ fs_1.default.mkdirSync(path_1.default.join(project, 'node_modules', 'eslint'), { recursive: true });
46
+ fs_1.default.writeFileSync(path_1.default.join(project, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version }));
47
+ fs_1.default.mkdirSync(path_1.default.join(project, 'src'), { recursive: true });
48
+ fs_1.default.writeFileSync(path_1.default.join(project, 'src', 'a.ts'), 'export const a = 1;\n');
49
+ fs_1.default.writeFileSync(path_1.default.join(registry, 'pack.json'), JSON.stringify({
50
+ schemaVersion: 2, name: 'js-ts', description: 'fixture', detects: ['package.json'],
51
+ coverage: { schemaVersion: 1, classes: { lint: { description: 'lint', detectors: [{ sensor: 'lint' }], remedy: { summary: 'fix', command: 'awm sensors init' } } } },
52
+ sensors: {
53
+ lint: {
54
+ fast: true,
55
+ applicability: { allFiles: ['package.json'] },
56
+ variants: [{
57
+ id: 'eslint-10', priority: 1, certifiedRange: '>=10 <11',
58
+ requirements: { tool: 'eslint', toolRange: '>=10 <11', runtime: 'node', runtimeRange: '>=0' },
59
+ assets: ['eslint.config.awm.mjs'], formatter: 'eslint-llm',
60
+ probe: { kind: 'package-script-present', script: 'lint' },
61
+ command: { executable: 'live-eslint', resolution: 'node-modules-bin', args: ['.'] },
62
+ inputs: ['src/**/*.ts'],
63
+ }],
64
+ },
65
+ },
66
+ }));
67
+ fs_1.default.mkdirSync(path_1.default.join(project, '.awm'), { recursive: true });
68
+ fs_1.default.writeFileSync(path_1.default.join(project, '.awm', 'sensors.json'), JSON.stringify({
69
+ schemaVersion: 2, pack: 'js-ts', registryRoot: path_1.default.join(home, 'registries', 'baseline'), sensors: {
70
+ lint: {
71
+ enabled: true, fast: true, variantId: 'eslint-10',
72
+ command: { executable: 'stale-eslint', resolution: 'node-modules-bin', args: ['.'] },
73
+ assets: ['eslint.config.awm.mjs'],
74
+ inputs: ['src/**/*.ts'],
75
+ initializedCompatibility: {
76
+ state: 'certified', reason: 'range-and-probe', variantId: 'eslint-10',
77
+ toolVersion: version, runtimeVersion: process.versions.node, certifiedRange: '>=10 <11', evidence: [],
78
+ },
79
+ },
80
+ },
81
+ }));
82
+ git(project, ['init', '-q', '-b', 'main']);
83
+ git(project, ['add', '-A']);
84
+ git(project, ['commit', '-qm', 'init']);
85
+ mockRunStructuredCommand.mockResolvedValue(ok());
86
+ });
87
+ afterEach(() => {
88
+ delete process.env.AWM_HOME;
89
+ fs_1.default.rmSync(project, { recursive: true, force: true });
90
+ fs_1.default.rmSync(home, { recursive: true, force: true });
91
+ });
92
+ const load = () => require('../../../src/commands/sensors/run');
93
+ it('caches a full-scope pass and reuses it with cached:true + provenance; --no-cache forces exec (RF-2.2/2.8/2.9/2.11)', async () => {
94
+ const { runSensors } = load();
95
+ const first = await runSensors({ cwd: project, useCache: true });
96
+ expect(first.sensors[0]).toMatchObject({ status: 'pass', name: 'lint' });
97
+ expect(first.sensors[0].cached).toBeUndefined();
98
+ expect(mockRunStructuredCommand).toHaveBeenCalledTimes(1);
99
+ mockRunStructuredCommand.mockClear();
100
+ const hit = await runSensors({ cwd: project, useCache: true });
101
+ expect(mockRunStructuredCommand).not.toHaveBeenCalled();
102
+ expect(hit.sensors[0]).toMatchObject({
103
+ status: 'pass',
104
+ cached: true,
105
+ cacheProvenance: expect.objectContaining({
106
+ inputDigest: expect.stringMatching(/^sha256:/),
107
+ sensorFingerprint: expect.stringMatching(/^sha256:/),
108
+ }),
109
+ });
110
+ mockRunStructuredCommand.mockResolvedValue(ok());
111
+ const forced = await runSensors({ cwd: project, useCache: false });
112
+ expect(mockRunStructuredCommand).toHaveBeenCalledTimes(1);
113
+ expect(forced.sensors[0].cached).toBeUndefined();
114
+ });
115
+ it('does not read or write cache when useCache is unset, readOnly, or ignoreBaseline (RF-2.11/2.13)', async () => {
116
+ const { runSensors } = load();
117
+ await runSensors({ cwd: project });
118
+ expect(mockRunStructuredCommand).toHaveBeenCalled();
119
+ const runs = path_1.default.join(project, '.awm', 'sensors', 'runs');
120
+ const cacheFiles = fs_1.default.existsSync(runs) ? fs_1.default.readdirSync(runs).filter((n) => n.startsWith('cache-')) : [];
121
+ expect(cacheFiles).toEqual([]);
122
+ mockRunStructuredCommand.mockClear();
123
+ mockRunStructuredCommand.mockResolvedValue(ok());
124
+ await runSensors({ cwd: project, useCache: true });
125
+ expect(fs_1.default.readdirSync(path_1.default.join(project, '.awm', 'sensors', 'runs')).some((n) => n.startsWith('cache-'))).toBe(true);
126
+ mockRunStructuredCommand.mockClear();
127
+ await runSensors({ cwd: project, useCache: true, ignoreBaseline: true });
128
+ expect(mockRunStructuredCommand).toHaveBeenCalled();
129
+ mockRunStructuredCommand.mockClear();
130
+ await runSensors({ cwd: project, useCache: true, readOnly: true });
131
+ expect(mockRunStructuredCommand).toHaveBeenCalled();
132
+ });
133
+ it('re-applies baseline on cache hit: findings-fail→pass, then remove baseline→fail without re-exec (RF-2.13)', async () => {
134
+ mockRunStructuredCommand.mockResolvedValue(exited(1, JSON.stringify(eslintFinding)));
135
+ const { runSensors } = load();
136
+ const { writeBaseline } = require('../../../src/commands/sensors/baseline');
137
+ const err = { file: 'src/a.ts', line: 1, column: 1, message: 'no-x', rule: 'no-x' };
138
+ writeBaseline(project, { lint: [(0, baseline_1.fingerprint)('lint', err)] });
139
+ const first = await runSensors({ cwd: project, useCache: true });
140
+ expect(first.sensors[0]).toMatchObject({ status: 'pass', baselineCount: 1 });
141
+ expect(mockRunStructuredCommand).toHaveBeenCalledTimes(1);
142
+ mockRunStructuredCommand.mockClear();
143
+ const hitPass = await runSensors({ cwd: project, useCache: true });
144
+ expect(mockRunStructuredCommand).not.toHaveBeenCalled();
145
+ expect(hitPass.sensors[0]).toMatchObject({ status: 'pass', cached: true, baselineCount: 1 });
146
+ writeBaseline(project, {});
147
+ const hitFail = await runSensors({ cwd: project, useCache: true });
148
+ expect(mockRunStructuredCommand).not.toHaveBeenCalled();
149
+ expect(hitFail.sensors[0]).toMatchObject({ status: 'fail', cached: true, failureKind: 'findings' });
150
+ });
151
+ it('does not write cache on --changed even when useCache is true (RF-2.7)', async () => {
152
+ const { runSensors } = load();
153
+ await runSensors({ cwd: project, useCache: true, changed: true });
154
+ const runs = path_1.default.join(project, '.awm', 'sensors', 'runs');
155
+ const cacheFiles = fs_1.default.existsSync(runs) ? fs_1.default.readdirSync(runs).filter((n) => n.startsWith('cache-')) : [];
156
+ expect(cacheFiles).toEqual([]);
157
+ });
158
+ it('never caches project-declared sensors without live tool version (RF-2.10)', async () => {
159
+ fs_1.default.writeFileSync(path_1.default.join(project, '.awm', 'sensors.json'), JSON.stringify({
160
+ schemaVersion: 3,
161
+ mode: 'project-sensors',
162
+ pack: null,
163
+ sensors: {
164
+ custom: {
165
+ source: 'project',
166
+ enabled: true,
167
+ fast: true,
168
+ command: { executable: 'node', resolution: 'path', args: ['-e', 'process.exit(0)'] },
169
+ formatter: 'exit-code',
170
+ inputs: ['src/**/*.ts'],
171
+ },
172
+ },
173
+ }));
174
+ mockRunStructuredCommand.mockResolvedValue(ok());
175
+ const { runSensors } = load();
176
+ await runSensors({ cwd: project, useCache: true });
177
+ const runs = path_1.default.join(project, '.awm', 'sensors', 'runs');
178
+ const cacheFiles = fs_1.default.existsSync(runs) ? fs_1.default.readdirSync(runs).filter((n) => n.startsWith('cache-')) : [];
179
+ expect(cacheFiles).toEqual([]);
180
+ expect(mockRunStructuredCommand).toHaveBeenCalled();
181
+ });
182
+ it('attaches receipt measurements for project-declared sensors with inputs even when cache is disabled (RF-4.6 / RF-2.10)', async () => {
183
+ fs_1.default.writeFileSync(path_1.default.join(project, '.awm', 'sensors.json'), JSON.stringify({
184
+ schemaVersion: 3,
185
+ mode: 'project-sensors',
186
+ pack: null,
187
+ sensors: {
188
+ custom: {
189
+ source: 'project',
190
+ enabled: true,
191
+ fast: true,
192
+ command: { executable: 'node', resolution: 'path', args: ['-e', 'process.exit(0)'] },
193
+ formatter: 'exit-code',
194
+ inputs: ['src/**/*.ts'],
195
+ },
196
+ },
197
+ }));
198
+ mockRunStructuredCommand.mockResolvedValue(ok());
199
+ const { runSensors } = load();
200
+ const out = await runSensors({ cwd: project, useCache: true });
201
+ expect(out.sensors[0]).toMatchObject({
202
+ name: 'custom',
203
+ status: 'pass',
204
+ measurement: expect.objectContaining({
205
+ inputs: ['src/**/*.ts'],
206
+ inputDigest: expect.stringMatching(/^sha256:/),
207
+ sensorFingerprint: expect.stringMatching(/^sha256:/),
208
+ measuredPaths: expect.arrayContaining(['src/a.ts']),
209
+ }),
210
+ });
211
+ expect(out.sensors[0].cached).toBeUndefined();
212
+ const runs = path_1.default.join(project, '.awm', 'sensors', 'runs');
213
+ const cacheFiles = fs_1.default.existsSync(runs) ? fs_1.default.readdirSync(runs).filter((n) => n.startsWith('cache-')) : [];
214
+ expect(cacheFiles).toEqual([]);
215
+ });
216
+ it('misses when live tool version changes (RF-2.10 liveState fingerprint)', async () => {
217
+ const { runSensors } = load();
218
+ await runSensors({ cwd: project, useCache: true });
219
+ mockRunStructuredCommand.mockClear();
220
+ fs_1.default.writeFileSync(path_1.default.join(project, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version: '10.9.0' }));
221
+ mockRunStructuredCommand.mockResolvedValue(ok());
222
+ const miss = await runSensors({ cwd: project, useCache: true });
223
+ expect(mockRunStructuredCommand).toHaveBeenCalled();
224
+ expect(miss.sensors[0].cached).toBeUndefined();
225
+ });
226
+ it('records originDirty when writing cache on a dirty tree (RF-2.14)', async () => {
227
+ fs_1.default.writeFileSync(path_1.default.join(project, 'src', 'a.ts'), 'export const a = 2;\n');
228
+ const { runSensors } = load();
229
+ await runSensors({ cwd: project, useCache: true });
230
+ mockRunStructuredCommand.mockClear();
231
+ const hit = await runSensors({ cwd: project, useCache: true });
232
+ expect(hit.sensors[0]).toMatchObject({
233
+ cached: true,
234
+ cacheProvenance: expect.objectContaining({ originDirty: true }),
235
+ });
236
+ });
237
+ it('does not write cache when digest changes during execute (RF-2.16)', async () => {
238
+ mockRunStructuredCommand.mockImplementation(async () => {
239
+ fs_1.default.writeFileSync(path_1.default.join(project, 'src', 'a.ts'), 'export const a = 99;\n');
240
+ return ok();
241
+ });
242
+ const { runSensors } = load();
243
+ await runSensors({ cwd: project, useCache: true });
244
+ const runs = path_1.default.join(project, '.awm', 'sensors', 'runs');
245
+ const cacheFiles = fs_1.default.existsSync(runs) ? fs_1.default.readdirSync(runs).filter((n) => n.startsWith('cache-')) : [];
246
+ expect(cacheFiles).toEqual([]);
247
+ });
248
+ it('does not write exit-code or missing-binary fails; may write findings fails (RF-2.4)', async () => {
249
+ mockRunStructuredCommand.mockResolvedValue(exited(1, JSON.stringify(eslintFinding)));
250
+ const { runSensors } = load();
251
+ await runSensors({ cwd: project, useCache: true });
252
+ const runs = path_1.default.join(project, '.awm', 'sensors', 'runs');
253
+ expect(fs_1.default.readdirSync(runs).some((n) => n.startsWith('cache-'))).toBe(true);
254
+ for (const f of fs_1.default.readdirSync(runs).filter((n) => n.startsWith('cache-'))) {
255
+ fs_1.default.unlinkSync(path_1.default.join(runs, f));
256
+ }
257
+ // Switch to exit-code formatter via project override is heavy; assert helper path via second pack run with generic tool-missing.
258
+ mockRunStructuredCommand.mockResolvedValue(exited(127, '', 'command not found'));
259
+ // formatter eslint-llm with empty findings + tool missing heuristics → missing-binary
260
+ await runSensors({ cwd: project, useCache: true });
261
+ // After findings cache was deleted, missing-binary must not recreate cache for a miss-exec that is not cacheable.
262
+ // Note: digest still matches; if a findings cache remained it would hit — we deleted it.
263
+ const afterMissing = fs_1.default.readdirSync(runs).filter((n) => n.startsWith('cache-'));
264
+ expect(afterMissing).toEqual([]);
265
+ });
266
+ it('keeps cache hits when authored globs match baseline.json; re-applies new baseline without re-exec (RF-2.12/2.13)', async () => {
267
+ // Broad JSON globs would include .awm/sensors.baseline.json unless the digest hard-excludes it.
268
+ const manifest = JSON.parse(fs_1.default.readFileSync(path_1.default.join(project, '.awm', 'sensors.json'), 'utf8'));
269
+ manifest.sensors.lint.inputs = ['**/*.json', 'src/**/*.ts'];
270
+ fs_1.default.writeFileSync(path_1.default.join(project, '.awm', 'sensors.json'), JSON.stringify(manifest));
271
+ const pack = JSON.parse(fs_1.default.readFileSync(path_1.default.join(home, 'registries', 'baseline', 'sensor-packs', 'js-ts', 'pack.json'), 'utf8'));
272
+ pack.sensors.lint.variants[0].inputs = ['**/*.json', 'src/**/*.ts'];
273
+ fs_1.default.writeFileSync(path_1.default.join(home, 'registries', 'baseline', 'sensor-packs', 'js-ts', 'pack.json'), JSON.stringify(pack));
274
+ git(project, ['add', '-A']);
275
+ git(project, ['commit', '-qm', 'broad-inputs']);
276
+ mockRunStructuredCommand.mockResolvedValue(exited(1, JSON.stringify(eslintFinding)));
277
+ const { runSensors } = load();
278
+ const { writeBaseline } = require('../../../src/commands/sensors/baseline');
279
+ const err = { file: 'src/a.ts', line: 1, column: 1, message: 'no-x', rule: 'no-x' };
280
+ writeBaseline(project, { lint: [(0, baseline_1.fingerprint)('lint', err)] });
281
+ await runSensors({ cwd: project, useCache: true });
282
+ expect(mockRunStructuredCommand).toHaveBeenCalledTimes(1);
283
+ // Change baseline content (would bust digest if baseline were measured) and clear acceptance.
284
+ writeBaseline(project, {});
285
+ mockRunStructuredCommand.mockClear();
286
+ const hit = await runSensors({ cwd: project, useCache: true });
287
+ expect(mockRunStructuredCommand).not.toHaveBeenCalled();
288
+ expect(hit.sensors[0]).toMatchObject({ status: 'fail', cached: true, failureKind: 'findings' });
289
+ });
290
+ it('does not apply cache hits over synthetic skip/disabled prepare results (RF-2.2/2.6/6.1)', async () => {
291
+ const { runSensors } = load();
292
+ await runSensors({ cwd: project, useCache: true });
293
+ expect(mockRunStructuredCommand).toHaveBeenCalled();
294
+ expect(fs_1.default.readdirSync(path_1.default.join(project, '.awm', 'sensors', 'runs')).some((n) => n.startsWith('cache-'))).toBe(true);
295
+ const manifest = JSON.parse(fs_1.default.readFileSync(path_1.default.join(project, '.awm', 'sensors.json'), 'utf8'));
296
+ manifest.sensors.lint.enabled = false;
297
+ fs_1.default.writeFileSync(path_1.default.join(project, '.awm', 'sensors.json'), JSON.stringify(manifest));
298
+ mockRunStructuredCommand.mockClear();
299
+ const skipped = await runSensors({ cwd: project, useCache: true });
300
+ expect(mockRunStructuredCommand).not.toHaveBeenCalled();
301
+ expect(skipped.sensors[0]).toMatchObject({ status: 'skipped', skipReason: 'disabled' });
302
+ expect(skipped.sensors[0].cached).toBeUndefined();
303
+ });
304
+ it('treats planted uncacheable raw cache entries as a miss and executes (RF-2.15/2.4)', async () => {
305
+ const { runSensors } = load();
306
+ await runSensors({ cwd: project, useCache: true });
307
+ const runs = path_1.default.join(project, '.awm', 'sensors', 'runs');
308
+ const cacheFiles = fs_1.default.readdirSync(runs).filter((n) => n.startsWith('cache-'));
309
+ expect(cacheFiles).toHaveLength(1);
310
+ const cachePath = path_1.default.join(runs, cacheFiles[0]);
311
+ const record = JSON.parse(fs_1.default.readFileSync(cachePath, 'utf8'));
312
+ record.raw = { name: 'lint', status: 'fail', failureKind: 'exit-code', errors: [{ message: 'exit 1' }] };
313
+ fs_1.default.writeFileSync(cachePath, JSON.stringify(record, null, 2) + '\n');
314
+ mockRunStructuredCommand.mockClear();
315
+ mockRunStructuredCommand.mockResolvedValue(ok());
316
+ const miss = await runSensors({ cwd: project, useCache: true });
317
+ expect(mockRunStructuredCommand).toHaveBeenCalled();
318
+ expect(miss.sensors[0]).toMatchObject({ status: 'pass' });
319
+ expect(miss.sensors[0].cached).toBeUndefined();
320
+ });
321
+ it('RF-1.4: sensor without authored inputs never reads or writes cache', async () => {
322
+ const manifest = JSON.parse(fs_1.default.readFileSync(path_1.default.join(project, '.awm', 'sensors.json'), 'utf8'));
323
+ delete manifest.sensors.lint.inputs;
324
+ fs_1.default.writeFileSync(path_1.default.join(project, '.awm', 'sensors.json'), JSON.stringify(manifest));
325
+ const pack = JSON.parse(fs_1.default.readFileSync(path_1.default.join(home, 'registries', 'baseline', 'sensor-packs', 'js-ts', 'pack.json'), 'utf8'));
326
+ delete pack.sensors.lint.variants[0].inputs;
327
+ fs_1.default.writeFileSync(path_1.default.join(home, 'registries', 'baseline', 'sensor-packs', 'js-ts', 'pack.json'), JSON.stringify(pack));
328
+ const { runSensors } = load();
329
+ await runSensors({ cwd: project, useCache: true });
330
+ expect(mockRunStructuredCommand).toHaveBeenCalledTimes(1);
331
+ const runs = path_1.default.join(project, '.awm', 'sensors', 'runs');
332
+ const cacheFiles = fs_1.default.existsSync(runs) ? fs_1.default.readdirSync(runs).filter((n) => n.startsWith('cache-')) : [];
333
+ expect(cacheFiles).toEqual([]);
334
+ mockRunStructuredCommand.mockClear();
335
+ mockRunStructuredCommand.mockResolvedValue(ok());
336
+ const second = await runSensors({ cwd: project, useCache: true });
337
+ expect(mockRunStructuredCommand).toHaveBeenCalledTimes(1);
338
+ expect(second.sensors[0].cached).toBeUndefined();
339
+ expect(second.sensors[0].measurement).toBeUndefined();
340
+ });
341
+ it('RF-1.6: changing the inputs list produces a fingerprint miss and re-executes', async () => {
342
+ const { runSensors } = load();
343
+ await runSensors({ cwd: project, useCache: true });
344
+ expect(mockRunStructuredCommand).toHaveBeenCalledTimes(1);
345
+ const manifest = JSON.parse(fs_1.default.readFileSync(path_1.default.join(project, '.awm', 'sensors.json'), 'utf8'));
346
+ manifest.sensors.lint.inputs = ['src/**/*.ts', 'lib/**/*.ts'];
347
+ fs_1.default.writeFileSync(path_1.default.join(project, '.awm', 'sensors.json'), JSON.stringify(manifest));
348
+ const pack = JSON.parse(fs_1.default.readFileSync(path_1.default.join(home, 'registries', 'baseline', 'sensor-packs', 'js-ts', 'pack.json'), 'utf8'));
349
+ pack.sensors.lint.variants[0].inputs = ['src/**/*.ts', 'lib/**/*.ts'];
350
+ fs_1.default.writeFileSync(path_1.default.join(home, 'registries', 'baseline', 'sensor-packs', 'js-ts', 'pack.json'), JSON.stringify(pack));
351
+ mockRunStructuredCommand.mockClear();
352
+ mockRunStructuredCommand.mockResolvedValue(ok());
353
+ const miss = await runSensors({ cwd: project, useCache: true });
354
+ expect(mockRunStructuredCommand).toHaveBeenCalledTimes(1);
355
+ expect(miss.sensors[0].cached).toBeUndefined();
356
+ });
357
+ it('RF-E.3: cache hit pass + executed fail → overall is not pass', async () => {
358
+ const pack = JSON.parse(fs_1.default.readFileSync(path_1.default.join(home, 'registries', 'baseline', 'sensor-packs', 'js-ts', 'pack.json'), 'utf8'));
359
+ pack.sensors.typecheck = {
360
+ fast: true,
361
+ applicability: { allFiles: ['package.json'] },
362
+ variants: [{
363
+ id: 'tsc', priority: 1, certifiedRange: '>=0',
364
+ requirements: { tool: 'eslint', toolRange: '>=10 <11', runtime: 'node', runtimeRange: '>=0' },
365
+ assets: ['eslint.config.awm.mjs'], formatter: 'exit-code',
366
+ probe: { kind: 'package-script-present', script: 'lint' },
367
+ command: { executable: 'live-tsc', resolution: 'node-modules-bin', args: ['.'] },
368
+ inputs: ['src/**/*.ts'],
369
+ }],
370
+ };
371
+ fs_1.default.writeFileSync(path_1.default.join(home, 'registries', 'baseline', 'sensor-packs', 'js-ts', 'pack.json'), JSON.stringify(pack));
372
+ const manifest = JSON.parse(fs_1.default.readFileSync(path_1.default.join(project, '.awm', 'sensors.json'), 'utf8'));
373
+ manifest.sensors.typecheck = {
374
+ enabled: true, fast: true, variantId: 'tsc',
375
+ command: { executable: 'stale-tsc', resolution: 'node-modules-bin', args: ['.'] },
376
+ assets: ['eslint.config.awm.mjs'],
377
+ inputs: ['src/**/*.ts'],
378
+ initializedCompatibility: {
379
+ state: 'certified', reason: 'range-and-probe', variantId: 'tsc',
380
+ toolVersion: version, runtimeVersion: process.versions.node, certifiedRange: '>=0', evidence: [],
381
+ },
382
+ };
383
+ fs_1.default.writeFileSync(path_1.default.join(project, '.awm', 'sensors.json'), JSON.stringify(manifest));
384
+ mockRunStructuredCommand.mockResolvedValue(ok());
385
+ const { runSensors } = load();
386
+ await runSensors({ cwd: project, useCache: true });
387
+ expect(mockRunStructuredCommand).toHaveBeenCalledTimes(2);
388
+ // Drop only the typecheck cache entry so lint can hit while typecheck re-executes.
389
+ const runs = path_1.default.join(project, '.awm', 'sensors', 'runs');
390
+ for (const f of fs_1.default.readdirSync(runs).filter((n) => n.startsWith('cache-'))) {
391
+ const p = path_1.default.join(runs, f);
392
+ const rec = JSON.parse(fs_1.default.readFileSync(p, 'utf8'));
393
+ if (rec.sensorId === 'typecheck')
394
+ fs_1.default.unlinkSync(p);
395
+ }
396
+ mockRunStructuredCommand.mockReset();
397
+ mockRunStructuredCommand.mockImplementation(async (cmd) => {
398
+ const exe = cmd?.executable ?? '';
399
+ if (String(exe).includes('tsc'))
400
+ return exited(1, 'tsc fail');
401
+ return ok();
402
+ });
403
+ const mixed = await runSensors({ cwd: project, useCache: true });
404
+ const lint = mixed.sensors.find((s) => s.name === 'lint');
405
+ const typecheck = mixed.sensors.find((s) => s.name === 'typecheck');
406
+ expect(lint).toMatchObject({ status: 'pass', cached: true });
407
+ expect(typecheck).toMatchObject({ status: 'fail' });
408
+ expect(typecheck?.cached).toBeUndefined();
409
+ expect(mixed.overall).not.toBe('pass');
410
+ });
411
+ it('inter-run measured-file edit → cache miss and re-exec', async () => {
412
+ const { runSensors } = load();
413
+ await runSensors({ cwd: project, useCache: true });
414
+ expect(mockRunStructuredCommand).toHaveBeenCalledTimes(1);
415
+ fs_1.default.writeFileSync(path_1.default.join(project, 'src', 'a.ts'), 'export const a = 42;\n');
416
+ mockRunStructuredCommand.mockClear();
417
+ mockRunStructuredCommand.mockResolvedValue(ok());
418
+ const miss = await runSensors({ cwd: project, useCache: true });
419
+ expect(mockRunStructuredCommand).toHaveBeenCalledTimes(1);
420
+ expect(miss.sensors[0].cached).toBeUndefined();
421
+ });
422
+ it('does not write cache for exit-code fail (not only missing-binary)', async () => {
423
+ const pack = JSON.parse(fs_1.default.readFileSync(path_1.default.join(home, 'registries', 'baseline', 'sensor-packs', 'js-ts', 'pack.json'), 'utf8'));
424
+ pack.sensors.lint.variants[0].formatter = 'exit-code';
425
+ fs_1.default.writeFileSync(path_1.default.join(home, 'registries', 'baseline', 'sensor-packs', 'js-ts', 'pack.json'), JSON.stringify(pack));
426
+ mockRunStructuredCommand.mockResolvedValue(exited(1, 'policy drift'));
427
+ const { runSensors } = load();
428
+ const out = await runSensors({ cwd: project, useCache: true });
429
+ expect(out.sensors[0]).toMatchObject({ status: 'fail', failureKind: 'exit-code' });
430
+ const runs = path_1.default.join(project, '.awm', 'sensors', 'runs');
431
+ const cacheFiles = fs_1.default.existsSync(runs) ? fs_1.default.readdirSync(runs).filter((n) => n.startsWith('cache-')) : [];
432
+ expect(cacheFiles).toEqual([]);
433
+ });
434
+ });
@@ -0,0 +1,171 @@
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 receipt_1 = require("../../../src/core/changes/receipt");
10
+ const mockRunStructuredCommand = jest.fn();
11
+ const mockRunCommand = jest.fn();
12
+ jest.mock('../../../src/commands/sensors/exec', () => ({
13
+ runCommand: (...args) => mockRunCommand(...args),
14
+ runStructuredCommand: (...args) => mockRunStructuredCommand(...args),
15
+ }));
16
+ const { ok } = require('./exec-fixtures');
17
+ describe('runSensors --slice and isFullSelection (RF-2.6, RF-5.10)', () => {
18
+ let project;
19
+ let home;
20
+ const version = '10.0.0';
21
+ beforeEach(() => {
22
+ jest.resetModules();
23
+ mockRunStructuredCommand.mockReset();
24
+ mockRunCommand.mockReset();
25
+ mockRunStructuredCommand.mockResolvedValue(ok());
26
+ mockRunCommand.mockResolvedValue(ok());
27
+ project = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-run-slice-'));
28
+ home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-run-slice-home-'));
29
+ process.env.AWM_HOME = home;
30
+ const registry = path_1.default.join(home, 'registries', 'baseline', 'sensor-packs', 'js-ts');
31
+ fs_1.default.mkdirSync(registry, { recursive: true });
32
+ fs_1.default.writeFileSync(path_1.default.join(registry, 'eslint.config.awm.mjs'), 'export default []');
33
+ fs_1.default.writeFileSync(path_1.default.join(home, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.test/baseline.git' }]));
34
+ fs_1.default.writeFileSync(path_1.default.join(project, 'package.json'), JSON.stringify({ scripts: { lint: 'eslint .' }, devDependencies: { eslint: '^10.0.0' } }));
35
+ fs_1.default.mkdirSync(path_1.default.join(project, 'node_modules', 'eslint'), { recursive: true });
36
+ fs_1.default.writeFileSync(path_1.default.join(project, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version }));
37
+ fs_1.default.writeFileSync(path_1.default.join(registry, 'pack.json'), JSON.stringify({
38
+ schemaVersion: 2, name: 'js-ts', description: 'fixture', detects: ['package.json'],
39
+ coverage: {
40
+ schemaVersion: 1,
41
+ classes: {
42
+ lint: { description: 'lint', detectors: [{ sensor: 'lint' }], remedy: { summary: 'fix', command: 'awm sensors init' } },
43
+ mutation: { description: 'mutation', detectors: [{ sensor: 'mutation' }], remedy: { summary: 'fix', command: 'awm sensors init' } },
44
+ test: { description: 'test', detectors: [{ sensor: 'test' }], remedy: { summary: 'fix', command: 'awm sensors init' } },
45
+ },
46
+ },
47
+ sensors: {
48
+ lint: {
49
+ fast: true,
50
+ applicability: { allFiles: ['package.json'] },
51
+ variants: [{
52
+ id: 'eslint-10', priority: 1, certifiedRange: '>=10 <11',
53
+ requirements: { tool: 'eslint', toolRange: '>=10 <11', runtime: 'node', runtimeRange: '>=0' },
54
+ assets: ['eslint.config.awm.mjs'], formatter: 'generic',
55
+ probe: { kind: 'package-script-present', script: 'lint' },
56
+ command: { executable: 'live-eslint', resolution: 'node-modules-bin', args: ['.'] },
57
+ }],
58
+ },
59
+ mutation: {
60
+ fast: false,
61
+ closureOnly: true,
62
+ applicability: { allFiles: ['package.json'] },
63
+ variants: [{
64
+ id: 'stryker', priority: 1, certifiedRange: '>=0',
65
+ requirements: { tool: 'stryker', toolRange: '>=0', runtime: 'node', runtimeRange: '>=0' },
66
+ assets: [], formatter: 'generic',
67
+ probe: { kind: 'version' },
68
+ command: { executable: 'stryker', resolution: 'node-modules-bin', args: ['run'] },
69
+ }],
70
+ },
71
+ test: {
72
+ fast: false,
73
+ applicability: { allFiles: ['package.json'] },
74
+ variants: [{
75
+ id: 'jest', priority: 1, certifiedRange: '>=0',
76
+ requirements: { tool: 'jest', toolRange: '>=0', runtime: 'node', runtimeRange: '>=0' },
77
+ assets: [], formatter: 'test',
78
+ probe: { kind: 'version' },
79
+ command: { executable: 'jest', resolution: 'node-modules-bin', args: ['--runInBand'] },
80
+ }],
81
+ },
82
+ },
83
+ }));
84
+ // Install fake tool versions so mutation/test resolve as compatible-unverified or certified.
85
+ for (const tool of ['stryker', 'jest']) {
86
+ fs_1.default.mkdirSync(path_1.default.join(project, 'node_modules', tool), { recursive: true });
87
+ fs_1.default.writeFileSync(path_1.default.join(project, 'node_modules', tool, 'package.json'), JSON.stringify({ version: '1.0.0' }));
88
+ }
89
+ fs_1.default.mkdirSync(path_1.default.join(project, '.awm'), { recursive: true });
90
+ const compat = (variantId, toolVersion, certifiedRange = '>=0') => ({
91
+ state: 'certified',
92
+ reason: 'range-and-probe',
93
+ variantId,
94
+ toolVersion,
95
+ runtimeVersion: process.versions.node,
96
+ certifiedRange,
97
+ evidence: [],
98
+ });
99
+ fs_1.default.writeFileSync(path_1.default.join(project, '.awm', 'sensors.json'), JSON.stringify({
100
+ schemaVersion: 2, pack: 'js-ts', registryRoot: path_1.default.join(home, 'registries', 'baseline'), sensors: {
101
+ lint: {
102
+ enabled: true, fast: true, variantId: 'eslint-10',
103
+ command: { executable: 'stale', resolution: 'node-modules-bin', args: ['.'] },
104
+ assets: ['eslint.config.awm.mjs'],
105
+ initializedCompatibility: compat('eslint-10', version, '>=10 <11'),
106
+ },
107
+ mutation: {
108
+ enabled: true, fast: false, variantId: 'stryker',
109
+ command: { executable: 'stale', resolution: 'node-modules-bin', args: ['run'] },
110
+ initializedCompatibility: compat('stryker', '1.0.0'),
111
+ },
112
+ test: {
113
+ enabled: true, fast: false, variantId: 'jest',
114
+ command: { executable: 'stale', resolution: 'node-modules-bin', args: ['--runInBand'] },
115
+ initializedCompatibility: compat('jest', '1.0.0'),
116
+ },
117
+ },
118
+ }));
119
+ });
120
+ afterEach(() => {
121
+ delete process.env.AWM_HOME;
122
+ fs_1.default.rmSync(project, { recursive: true, force: true });
123
+ fs_1.default.rmSync(home, { recursive: true, force: true });
124
+ });
125
+ const load = () => require('../../../src/commands/sensors/run');
126
+ it('isFullSelection({slice:true}) === false (RF-5.10)', () => {
127
+ expect((0, receipt_1.isFullSelection)({ slice: true })).toBe(false);
128
+ expect((0, receipt_1.isFullSelection)({ slice: true, all: true })).toBe(false);
129
+ expect((0, receipt_1.isFullSelection)({ slice: true, fast: true })).toBe(false);
130
+ expect((0, receipt_1.isFullSelection)({})).toBe(true);
131
+ });
132
+ it('skips closureOnly sensors with an explicit skipped reason (RF-5.10)', async () => {
133
+ const { runSensors } = load();
134
+ const out = await runSensors({ cwd: project, slice: true });
135
+ const byName = Object.fromEntries(out.sensors.map((s) => [s.name, s]));
136
+ expect(Object.keys(byName).sort()).toEqual(['lint', 'mutation', 'test']);
137
+ expect(byName.mutation).toMatchObject({ status: 'skipped', skipReason: expect.stringMatching(/closureOnly|slice/i) });
138
+ expect(byName.lint.status).toBe('pass');
139
+ expect(byName.test.status).toBe('pass');
140
+ // Live probes may spawn tools; the slice contract is selection, not probe silence.
141
+ expect(byName.mutation.skipReason).toMatch(/closureOnly|slice/i);
142
+ });
143
+ it('rejects --slice with --all and --slice with --slow (RF-2.6)', async () => {
144
+ const { runSensors } = load();
145
+ await expect(runSensors({ cwd: project, slice: true, all: true })).rejects.toThrow(/slice/i);
146
+ await expect(runSensors({ cwd: project, slice: true, slow: true })).rejects.toThrow(/slice/i);
147
+ });
148
+ it('--slice --fast is the intersection: fast and not closureOnly (RF-2.6)', async () => {
149
+ const { runSensors } = load();
150
+ const out = await runSensors({ cwd: project, slice: true, fast: true });
151
+ const names = out.sensors.map((s) => `${s.name}:${s.status}`);
152
+ expect(names).toContain('lint:pass');
153
+ expect(out.sensors.find((s) => s.name === 'test')).toBeUndefined();
154
+ // mutation is not fast, so filtered by --fast before/with slice — either absent or not executed.
155
+ const mutation = out.sensors.find((s) => s.name === 'mutation');
156
+ if (mutation)
157
+ expect(mutation.status).toBe('skipped');
158
+ expect(out.sensors.every((s) => s.name === 'lint' || s.name === 'mutation')).toBe(true);
159
+ });
160
+ it('wires --slice and --no-cache on the sensors run CLI action (RF-2.9/2.11)', () => {
161
+ const { Command } = require('commander');
162
+ const { registerSensorsCommand } = require('../../../src/commands/sensors/index');
163
+ const program = new Command();
164
+ program.exitOverride();
165
+ registerSensorsCommand(program);
166
+ const run = program.commands.find((c) => c.name() === 'sensors')
167
+ .commands.find((c) => c.name() === 'run');
168
+ const opts = run.options.map((o) => o.long).filter(Boolean);
169
+ expect(opts).toEqual(expect.arrayContaining(['--slice', '--no-cache']));
170
+ });
171
+ });