agentic-workflow-manager 9.16.1 → 9.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/src/commands/preflight/checks.js +63 -13
  2. package/dist/src/commands/sensors/bootstrap.js +2 -1
  3. package/dist/src/commands/sensors/compatibility/contract.js +30 -26
  4. package/dist/src/commands/sensors/compatibility/discovery.js +21 -0
  5. package/dist/src/commands/sensors/compatibility/manifest.js +169 -17
  6. package/dist/src/commands/sensors/compatibility/materialize.js +30 -5
  7. package/dist/src/commands/sensors/compatibility/source.js +3 -2
  8. package/dist/src/commands/sensors/coverage/render.js +8 -2
  9. package/dist/src/commands/sensors/coverage/resolve.js +7 -0
  10. package/dist/src/commands/sensors/init.js +15 -2
  11. package/dist/src/commands/sensors/migrate.js +13 -10
  12. package/dist/src/commands/sensors/prepare.js +53 -6
  13. package/dist/src/commands/sensors/project.js +60 -6
  14. package/dist/src/commands/sensors/result.js +18 -3
  15. package/dist/src/commands/sensors/run.js +124 -16
  16. package/dist/src/commands/sensors/status.js +163 -18
  17. package/dist/src/core/init/steps.js +5 -1
  18. package/dist/tests/commands/preflight/preflight.test.js +121 -0
  19. package/dist/tests/commands/sensors/bootstrap.test.js +10 -0
  20. package/dist/tests/commands/sensors/compatibility/manifest.test.js +204 -0
  21. package/dist/tests/commands/sensors/init.test.js +107 -0
  22. package/dist/tests/commands/sensors/migrate.test.js +4 -3
  23. package/dist/tests/commands/sensors/partial-pack-bootstrap.test.js +1 -1
  24. package/dist/tests/commands/sensors/prepare.test.js +62 -0
  25. package/dist/tests/commands/sensors/result.test.js +66 -0
  26. package/dist/tests/commands/sensors/run.test.js +331 -0
  27. package/dist/tests/commands/sensors/status.test.js +204 -0
  28. package/dist/tests/structural/native-release-artifacts.test.js +4 -2
  29. package/dist/tests/structural/project-declared-sensors-docs-contract.test.js +104 -0
  30. package/package.json +1 -1
  31. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  32. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  33. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  34. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -95,3 +95,110 @@ describe('initSensors compatibility API', () => {
95
95
  expect(bootstrap_1.applySensorBootstrap).not.toHaveBeenCalled();
96
96
  });
97
97
  });
98
+ describe('buildManifest project-declared preserve (S3)', () => {
99
+ let tmpDir;
100
+ let registryRoot;
101
+ beforeEach(() => {
102
+ tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-init-s3-'));
103
+ registryRoot = makeRegistry();
104
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'package.json'), '{}');
105
+ });
106
+ afterEach(() => {
107
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
108
+ fs_1.default.rmSync(registryRoot, { recursive: true, force: true });
109
+ });
110
+ it('preserves an existing source:project entry byte-stable when regenerating pack sensors (RF-3.1)', () => {
111
+ const projectEntry = {
112
+ source: 'project',
113
+ enabled: true,
114
+ fast: true,
115
+ command: { executable: 'terraform', resolution: 'path', args: ['fmt', '-check'] },
116
+ formatter: 'exit-code',
117
+ description: 'keep-me',
118
+ };
119
+ // Collides with a pack default name: per-field merge would otherwise inject pack `cmd`.
120
+ const existing = {
121
+ pack: 'js-ts',
122
+ sensors: {
123
+ typecheck: projectEntry,
124
+ },
125
+ };
126
+ const merged = (0, init_1.buildManifest)('js-ts', existing, registryRoot, tmpDir);
127
+ expect(merged.sensors.typecheck).toEqual(projectEntry);
128
+ expect(merged.sensors.typecheck).not.toHaveProperty('cmd');
129
+ });
130
+ it('writes an only-project manifest without forcing pack:"generic" (RF-3.2)', () => {
131
+ const projectEntry = {
132
+ source: 'project',
133
+ enabled: true,
134
+ command: { executable: 'terraform', resolution: 'path', args: ['fmt', '-check'] },
135
+ formatter: 'exit-code',
136
+ };
137
+ const onlyProject = (0, init_1.buildManifest)(null, {
138
+ sensors: { 'iac-format': projectEntry },
139
+ }, undefined, tmpDir);
140
+ expect(onlyProject).not.toMatchObject({ pack: 'generic' });
141
+ expect(Object.prototype.hasOwnProperty.call(onlyProject, 'pack')).toBe(false);
142
+ expect(onlyProject.sensors['iac-format']).toEqual(projectEntry);
143
+ expect(JSON.stringify(onlyProject)).not.toMatch(/"pack"\s*:/);
144
+ });
145
+ it('preserves source:project entries when materialize regenerates pack sensors (RF-3.1)', () => {
146
+ const { materializePortableSensors } = require('../../../src/commands/sensors/compatibility/materialize');
147
+ const home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-init-mat-home-'));
148
+ const previousHome = process.env.AWM_HOME;
149
+ try {
150
+ process.env.AWM_HOME = home;
151
+ const packDir = path_1.default.join(home, 'registries', 'baseline', 'sensor-packs', 'js-ts');
152
+ fs_1.default.mkdirSync(packDir, { recursive: true });
153
+ fs_1.default.writeFileSync(path_1.default.join(packDir, 'eslint.config.awm.mjs'), 'export default []\n');
154
+ fs_1.default.writeFileSync(path_1.default.join(packDir, 'pack.json'), JSON.stringify({
155
+ schemaVersion: 2, name: 'js-ts', description: 'fixture', detects: ['package.json'],
156
+ coverage: { schemaVersion: 1, classes: {} },
157
+ sensors: { lint: { variants: [] } },
158
+ }));
159
+ const projectEntry = {
160
+ source: 'project',
161
+ enabled: true,
162
+ command: { executable: 'terraform', resolution: 'path', args: ['fmt', '-check'] },
163
+ formatter: 'exit-code',
164
+ description: 'preserve-me',
165
+ };
166
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
167
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
168
+ schemaVersion: 3, mode: 'project-sensors', pack: 'js-ts', source: { registry: 'baseline' },
169
+ sensors: { 'iac-format': projectEntry },
170
+ }));
171
+ const evidence = {
172
+ state: 'certified', reason: 'range-and-probe', variantId: 'eslint-10',
173
+ toolVersion: '10.0.0', runtimeVersion: process.versions.node, certifiedRange: '>=10 <11', evidence: [],
174
+ };
175
+ const result = materializePortableSensors({
176
+ projectRoot: tmpDir,
177
+ pack: 'js-ts',
178
+ source: {
179
+ path: path_1.default.join(packDir, 'pack.json'),
180
+ content: fs_1.default.readFileSync(path_1.default.join(packDir, 'pack.json'), 'utf8'),
181
+ registry: { name: 'baseline', remote: 'https://example.test/baseline.git', contentRoot: path_1.default.join(home, 'registries', 'baseline') },
182
+ },
183
+ configure: false,
184
+ sensors: {
185
+ lint: {
186
+ enabled: true, variantId: 'eslint-10',
187
+ command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
188
+ assets: ['eslint.config.awm.mjs'],
189
+ initializedCompatibility: evidence,
190
+ },
191
+ },
192
+ });
193
+ expect(result.manifest.sensors['iac-format']).toEqual(projectEntry);
194
+ expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), 'utf8')).sensors['iac-format']).toEqual(projectEntry);
195
+ }
196
+ finally {
197
+ if (previousHome === undefined)
198
+ delete process.env.AWM_HOME;
199
+ else
200
+ process.env.AWM_HOME = previousHome;
201
+ fs_1.default.rmSync(home, { recursive: true, force: true });
202
+ }
203
+ });
204
+ });
@@ -154,11 +154,12 @@ describe('planV2Migration', () => {
154
154
  try {
155
155
  fs_1.default.writeFileSync(manifestPath, original);
156
156
  const candidate = (0, migrate_1.planV2Migration)({ manifest: v2, source: resolvedSource() }).candidate;
157
- const withoutPolicy = { ...candidate.sensors.lint };
157
+ const lint = candidate.sensors.lint;
158
+ const withoutPolicy = { ...lint };
158
159
  delete withoutPolicy.policyRef;
159
160
  for (const changed of [
160
- { ...candidate.sensors.lint, fast: false },
161
- { ...candidate.sensors.lint, variantId: 'eslint-8', initializedCompatibility: { ...candidate.sensors.lint.initializedCompatibility, variantId: 'eslint-8', toolVersion: '8.0.0', certifiedRange: '>=8 <9' } },
161
+ { ...lint, fast: false },
162
+ { ...lint, variantId: 'eslint-8', initializedCompatibility: { ...lint.initializedCompatibility, variantId: 'eslint-8', toolVersion: '8.0.0', certifiedRange: '>=8 <9' } },
162
163
  withoutPolicy,
163
164
  ]) {
164
165
  expect(() => (0, migrate_1.replaceV2ManifestWithV3)(manifestPath, { ...candidate, sensors: { lint: changed } }, resolvedSource())).toThrow('semantic mismatch');
@@ -52,7 +52,7 @@ describe('project-sensors bootstrap with a partially resolvable pack', () => {
52
52
  const manifest = created.manifest;
53
53
  // The manifest contents are the assertion, not the exit code.
54
54
  expect(Object.keys(manifest.sensors)).toEqual(['lint']);
55
- expect(manifest.sensors.lint.variantId).toBe('eslint-10');
55
+ expect(manifest.sensors.lint).toMatchObject({ variantId: 'eslint-10' });
56
56
  expect(created.unresolved).toEqual([{ sensor: 'security', state: 'missing-tool', reason: 'tool-not-found' }]);
57
57
  // Nothing is invented for the skipped sensor: no entry, and none of its assets.
58
58
  expect(created.changes.map(change => change.path)).toEqual(['.awm/sensors.json']);
@@ -86,6 +86,68 @@ describe('validateRunOptions', () => {
86
86
  .toThrow(/refusing to combine --changed with a baseline capture/);
87
87
  });
88
88
  });
89
+ describe('prepareV2Sensor: project-declared entries (S2)', () => {
90
+ const projectSensor = (overrides = {}) => ({
91
+ source: 'project',
92
+ enabled: true,
93
+ command: { executable: 'node', resolution: 'path', args: ['scripts/iac-format.js'] },
94
+ formatter: 'exit-code',
95
+ timeout: 45_000,
96
+ ...overrides,
97
+ });
98
+ test('uses the manifest command, default exit-code formatter, project timeout, and project-declared provenance', () => {
99
+ const prepared = (0, prepare_1.prepareV2Sensor)({
100
+ name: 'iac-format',
101
+ sensor: projectSensor(),
102
+ requestedScope: 'full',
103
+ });
104
+ expect(prepared.command).toEqual({
105
+ kind: 'structured',
106
+ value: { executable: 'node', resolution: 'path', args: ['scripts/iac-format.js'] },
107
+ });
108
+ expect(prepared.formatter).toBe('exit-code');
109
+ expect(prepared.timeoutMs).toBe(45_000);
110
+ expect(prepared.timeoutSource).toBe('project');
111
+ expect(prepared.certification).toBe('project-declared');
112
+ expect(prepared.certification).not.toBe('certified');
113
+ expect(prepared.syntheticStatus).toBeUndefined();
114
+ });
115
+ test('skips live variant re-resolution for project entries', () => {
116
+ const prepared = (0, prepare_1.prepareV2Sensor)({
117
+ name: 'iac-format',
118
+ sensor: projectSensor(),
119
+ liveState: {
120
+ state: 'missing-tool',
121
+ reason: 'tool-not-found',
122
+ variantId: null,
123
+ toolVersion: null,
124
+ runtimeVersion: null,
125
+ certifiedRange: null,
126
+ evidence: [],
127
+ },
128
+ requestedScope: 'full',
129
+ });
130
+ expect(prepared.command).toEqual({
131
+ kind: 'structured',
132
+ value: { executable: 'node', resolution: 'path', args: ['scripts/iac-format.js'] },
133
+ });
134
+ expect(prepared.certification).toBe('project-declared');
135
+ });
136
+ test('returns synthetic not-applicable when project applicabilityPaths are not met', () => {
137
+ const prepared = (0, prepare_1.prepareV2Sensor)({
138
+ name: 'iac-format',
139
+ sensor: projectSensor({
140
+ applicability: { allFiles: ['terraform.tf'] },
141
+ }),
142
+ applicabilityPaths: ['README.md'],
143
+ requestedScope: 'full',
144
+ });
145
+ expect(prepared.command).toBeUndefined();
146
+ expect(prepared.syntheticStatus).toBe('inconclusive');
147
+ expect(prepared.syntheticReason).toBe('not-applicable: applicability-not-met');
148
+ expect(prepared.certification).toBe('project-declared');
149
+ });
150
+ });
89
151
  // El bloqueo que esto arregla: `compatible-unverified` significa que la herramienta esta,
90
152
  // cae en el rango operativo declarado por el pack y su probe coincidio — solo la version
91
153
  // exacta no es la que el registry congelo. Saltearla dejaba el veredicto en
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const result_1 = require("../../../src/commands/sensors/result");
4
+ const exec_fixtures_1 = require("./exec-fixtures");
5
+ function prepared(overrides = {}) {
6
+ return {
7
+ name: 'iac-format',
8
+ formatter: 'exit-code',
9
+ timeoutMs: 30_000,
10
+ timeoutSource: 'project',
11
+ requestedScope: 'full',
12
+ effectiveScope: 'full',
13
+ certification: 'project-declared',
14
+ command: { kind: 'structured', value: { executable: 'node', resolution: 'path', args: ['check.js'] } },
15
+ ...overrides,
16
+ };
17
+ }
18
+ describe('interpretResult exit-code formatter (RF-2.4)', () => {
19
+ it('maps exit 0 to pass with no per-file findings', () => {
20
+ const result = (0, result_1.interpretResult)(prepared(), (0, exec_fixtures_1.ok)('all good\n'));
21
+ expect(result).toMatchObject({
22
+ name: 'iac-format',
23
+ status: 'pass',
24
+ errors: [],
25
+ certification: 'project-declared',
26
+ });
27
+ });
28
+ it('maps non-zero exit to fail with truncated combined stdout/stderr evidence and no invented per-file findings', () => {
29
+ const long = `${'x'.repeat(300)}\n`;
30
+ const result = (0, result_1.interpretResult)(prepared(), (0, exec_fixtures_1.exited)(1, long, 'stderr-tail\n'));
31
+ expect(result.status).toBe('fail');
32
+ expect(result.errors).toHaveLength(1);
33
+ expect(result.errors[0].file).toBeUndefined();
34
+ expect(result.errors[0].line).toBeUndefined();
35
+ expect(result.errors[0].rule).toBeUndefined();
36
+ // Combined = stdout+stderr; truncateEvidence caps at 200 — must be truncated
37
+ // stdout-first content, not a loose "exit 1" / stderr-only fallback.
38
+ expect(result.errors[0].message.length).toBeLessThanOrEqual(200);
39
+ expect(result.errors[0].message).toMatch(/^x{10,}/);
40
+ expect(result.errors[0].message).not.toMatch(/exit 1/);
41
+ expect(result.certification).toBe('project-declared');
42
+ });
43
+ it('maps non-zero exit with empty stdout/stderr to fail evidence that includes exit N', () => {
44
+ const result = (0, result_1.interpretResult)(prepared(), (0, exec_fixtures_1.exited)(7, '', ''));
45
+ expect(result.status).toBe('fail');
46
+ expect(result.errors).toHaveLength(1);
47
+ expect(result.errors[0].file).toBeUndefined();
48
+ expect(result.errors[0].message).toMatch(/exit 7/);
49
+ });
50
+ });
51
+ describe('executePrepared synthetic provenance (RF-2.2)', () => {
52
+ it('includes certification on synthetic/disabled results the same way interpretResult does', async () => {
53
+ const result = await (0, result_1.executePrepared)(prepared({
54
+ command: undefined,
55
+ syntheticStatus: 'skipped',
56
+ syntheticReason: 'disabled',
57
+ certification: 'project-declared',
58
+ }));
59
+ expect(result).toMatchObject({
60
+ name: 'iac-format',
61
+ status: 'skipped',
62
+ skipReason: 'disabled',
63
+ certification: 'project-declared',
64
+ });
65
+ });
66
+ });
@@ -641,3 +641,334 @@ describe('runSensors — honest floor (not_certified over real stack)', () => {
641
641
  }
642
642
  });
643
643
  });
644
+ describe('runSensors project-declared parity (S2)', () => {
645
+ let project;
646
+ let home;
647
+ const version = '10.0.0';
648
+ const packBoundLint = () => ({
649
+ enabled: true,
650
+ fast: true,
651
+ variantId: 'eslint-10',
652
+ command: { executable: 'stale-eslint', resolution: 'node-modules-bin', args: ['.'] },
653
+ assets: ['eslint.config.awm.mjs'],
654
+ initializedCompatibility: {
655
+ state: 'certified',
656
+ reason: 'range-and-probe',
657
+ variantId: 'eslint-10',
658
+ toolVersion: version,
659
+ runtimeVersion: process.versions.node,
660
+ certifiedRange: '>=10 <11',
661
+ evidence: [],
662
+ },
663
+ });
664
+ const projectIac = (exitScript = 'process.exit(0)') => ({
665
+ source: 'project',
666
+ enabled: true,
667
+ fast: true,
668
+ command: { executable: 'node', resolution: 'path', args: ['-e', exitScript] },
669
+ formatter: 'exit-code',
670
+ });
671
+ beforeEach(() => {
672
+ jest.resetModules();
673
+ project = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-v3-project-run-'));
674
+ home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-v3-project-home-'));
675
+ process.env.AWM_HOME = home;
676
+ const registry = path_1.default.join(home, 'registries', 'baseline', 'sensor-packs', 'js-ts');
677
+ fs_1.default.mkdirSync(registry, { recursive: true });
678
+ fs_1.default.writeFileSync(path_1.default.join(registry, 'eslint.config.awm.mjs'), 'export default []');
679
+ fs_1.default.writeFileSync(path_1.default.join(home, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: 'https://example.test/baseline.git' }]));
680
+ fs_1.default.writeFileSync(path_1.default.join(project, 'package.json'), JSON.stringify({ scripts: { lint: 'eslint .' }, devDependencies: { eslint: '^10.0.0' } }));
681
+ fs_1.default.mkdirSync(path_1.default.join(project, 'node_modules', 'eslint'), { recursive: true });
682
+ fs_1.default.writeFileSync(path_1.default.join(project, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version }));
683
+ fs_1.default.writeFileSync(path_1.default.join(registry, 'pack.json'), JSON.stringify({
684
+ schemaVersion: 2, name: 'js-ts', description: 'fixture', detects: ['package.json'],
685
+ coverage: { schemaVersion: 1, classes: { lint: { description: 'lint', detectors: [{ sensor: 'lint' }], remedy: { summary: 'fix', command: 'awm sensors init' } } } },
686
+ sensors: {
687
+ lint: {
688
+ fast: true,
689
+ applicability: { allFiles: ['package.json'] },
690
+ variants: [{
691
+ id: 'eslint-10', priority: 1, certifiedRange: '>=10 <11',
692
+ requirements: { tool: 'eslint', toolRange: '>=10 <11', runtime: 'node', runtimeRange: '>=0' },
693
+ assets: ['eslint.config.awm.mjs'], formatter: 'generic',
694
+ probe: { kind: 'package-script-present', script: 'lint' },
695
+ command: { executable: 'live-eslint', resolution: 'node-modules-bin', args: ['.'] },
696
+ }],
697
+ },
698
+ typecheck: {
699
+ fast: true,
700
+ applicability: { allFiles: ['package.json'] },
701
+ variants: [{
702
+ id: 'tsc-5', priority: 1, certifiedRange: '>=5 <6',
703
+ requirements: { tool: 'tsc', toolRange: '>=5 <6', runtime: 'node', runtimeRange: '>=0' },
704
+ assets: [], formatter: 'tsc',
705
+ probe: { kind: 'version' },
706
+ command: { executable: 'tsc', resolution: 'node-modules-bin', args: ['--noEmit'] },
707
+ }],
708
+ },
709
+ },
710
+ }));
711
+ fs_1.default.mkdirSync(path_1.default.join(project, '.awm'));
712
+ mockRunStructuredCommand.mockReset();
713
+ mockRunCommand.mockReset();
714
+ });
715
+ afterEach(() => {
716
+ delete process.env.AWM_HOME;
717
+ fs_1.default.rmSync(project, { recursive: true, force: true });
718
+ fs_1.default.rmSync(home, { recursive: true, force: true });
719
+ });
720
+ function writeManifest(sensors, pack = 'js-ts') {
721
+ const body = {
722
+ schemaVersion: 3,
723
+ mode: 'project-sensors',
724
+ sensors,
725
+ };
726
+ if (pack !== undefined) {
727
+ body.pack = pack;
728
+ if (typeof pack === 'string' && pack.length > 0)
729
+ body.source = { registry: 'baseline' };
730
+ }
731
+ fs_1.default.writeFileSync(path_1.default.join(project, '.awm', 'sensors.json'), JSON.stringify(body));
732
+ }
733
+ it('runs pack lint and project iac-format together with project-declared provenance and overall pass (RF-2.1/2.2/2.3)', async () => {
734
+ writeManifest({ lint: packBoundLint(), 'iac-format': projectIac('process.exit(0)') });
735
+ mockRunStructuredCommand.mockImplementation(async (command) => {
736
+ if (command.executable === 'live-eslint')
737
+ return ok();
738
+ if (command.executable === 'node')
739
+ return ok();
740
+ return ok();
741
+ });
742
+ const { runSensors } = require('../../../src/commands/sensors/run');
743
+ const result = await runSensors({ cwd: project, fast: true });
744
+ expect(result.sensors.map((s) => s.name).sort()).toEqual(['iac-format', 'lint']);
745
+ const projectRow = result.sensors.find((s) => s.name === 'iac-format');
746
+ expect(projectRow).toMatchObject({ status: 'pass', certification: 'project-declared' });
747
+ expect(projectRow.certification).not.toBe('certified');
748
+ expect(result.overall).toBe('pass');
749
+ expect(result.invalidEntries ?? []).toEqual([]);
750
+ });
751
+ it('fails overall when the project sensor exits non-zero (RF-2.5)', async () => {
752
+ writeManifest({ lint: packBoundLint(), 'iac-format': projectIac('process.exit(1)') });
753
+ mockRunStructuredCommand.mockImplementation(async (command) => {
754
+ if (command.executable === 'live-eslint')
755
+ return ok();
756
+ if (command.executable === 'node')
757
+ return exited(1, 'fmt drift\n');
758
+ return ok();
759
+ });
760
+ const { runSensors } = require('../../../src/commands/sensors/run');
761
+ const result = await runSensors({ cwd: project, fast: true });
762
+ expect(result.sensors.find((s) => s.name === 'iac-format')).toMatchObject({ status: 'fail' });
763
+ expect(result.overall).toBe('fail');
764
+ });
765
+ it('skips a schema-invalid project sibling, still runs pack, and lists the invalid entry (RF-2.6)', async () => {
766
+ writeManifest({
767
+ lint: packBoundLint(),
768
+ broken: {
769
+ source: 'project',
770
+ enabled: true,
771
+ fast: true,
772
+ command: { executable: 'node', resolution: 'path', args: ['.'] },
773
+ formatter: 'not-a-formatter',
774
+ },
775
+ });
776
+ mockRunStructuredCommand.mockResolvedValue(ok());
777
+ const { runSensors } = require('../../../src/commands/sensors/run');
778
+ const result = await runSensors({ cwd: project, fast: true });
779
+ expect(result.sensors.some((s) => s.name === 'lint')).toBe(true);
780
+ expect(result.sensors.some((s) => s.name === 'broken')).toBe(false);
781
+ expect(result.invalidEntries).toEqual([
782
+ expect.objectContaining({
783
+ name: 'broken',
784
+ reason: expect.stringMatching(/sensors\.broken\.formatter|not a registered formatter/i),
785
+ }),
786
+ ]);
787
+ expect(result.reason).not.toBe('manifest-malformed');
788
+ expect(result.sensors.length).toBeGreaterThan(0);
789
+ });
790
+ it('suppresses a baselined project exit-code failure as sensor-level debt (RF-2.7)', async () => {
791
+ writeManifest({ 'iac-format': projectIac('process.exit(1)') }, null);
792
+ mockRunStructuredCommand.mockResolvedValue(exited(1, 'fmt drift\n'));
793
+ const { runSensors } = require('../../../src/commands/sensors/run');
794
+ const { buildBaseline, writeBaseline } = require('../../../src/commands/sensors/baseline');
795
+ const first = await runSensors({ cwd: project, all: true });
796
+ expect(first.overall).toBe('fail');
797
+ const row = first.sensors.find((s) => s.name === 'iac-format');
798
+ expect(row.status).toBe('fail');
799
+ writeBaseline(project, buildBaseline(first.sensors.map((s) => ({ name: s.name, errors: s.errors }))));
800
+ mockRunStructuredCommand.mockResolvedValue(exited(1, 'fmt drift\n'));
801
+ const second = await runSensors({ cwd: project, all: true });
802
+ const suppressed = second.sensors.find((s) => s.name === 'iac-format');
803
+ expect(suppressed).toMatchObject({ status: 'pass', baselineCount: 1 });
804
+ expect(second.overall).toBe('pass');
805
+ });
806
+ it('yields overall pass for a passing project-only run (RF-2.3 provenance never gates)', async () => {
807
+ writeManifest({ 'iac-format': projectIac('process.exit(0)') }, null);
808
+ mockRunStructuredCommand.mockResolvedValue(ok());
809
+ const { runSensors } = require('../../../src/commands/sensors/run');
810
+ const result = await runSensors({ cwd: project, all: true });
811
+ expect(result.sensors).toEqual([
812
+ expect.objectContaining({ name: 'iac-format', status: 'pass', certification: 'project-declared' }),
813
+ ]);
814
+ expect(result.overall).toBe('pass');
815
+ });
816
+ it('marks a colliding project entry invalid and still prepares/runs the pack sensor for that id (RF-1.7)', async () => {
817
+ // Project entry reuses pack id `lint` — invalid, but pack lint must remain executable.
818
+ writeManifest({ lint: projectIac('process.exit(0)') });
819
+ mockRunStructuredCommand.mockResolvedValue(ok());
820
+ const { runSensors } = require('../../../src/commands/sensors/run');
821
+ const result = await runSensors({ cwd: project, all: true });
822
+ expect(result.invalidEntries).toEqual([
823
+ expect.objectContaining({
824
+ name: 'lint',
825
+ reason: expect.stringMatching(/project-sensor-name-collision:\s*lint/),
826
+ }),
827
+ ]);
828
+ const lint = result.sensors.find((s) => s.name === 'lint');
829
+ expect(lint).toBeDefined();
830
+ expect(lint.certification).not.toBe('project-declared');
831
+ expect(lint.status).toBe('pass');
832
+ expect(mockRunStructuredCommand).toHaveBeenCalledWith(expect.objectContaining({ executable: 'live-eslint' }), expect.any(Object));
833
+ });
834
+ it('on collision with null liveState.variantId still prepares pack (not project) for that id (RF-1.7)', async () => {
835
+ writeManifest({ lint: projectIac('process.exit(0)') });
836
+ // Remove the tool so live resolution yields missing-tool with variantId null,
837
+ // while the pack still defines the lint sensor.
838
+ fs_1.default.rmSync(path_1.default.join(project, 'node_modules', 'eslint'), { recursive: true, force: true });
839
+ fs_1.default.writeFileSync(path_1.default.join(project, 'package.json'), JSON.stringify({ scripts: { lint: 'eslint .' } }));
840
+ mockRunStructuredCommand.mockResolvedValue(ok());
841
+ const { runSensors } = require('../../../src/commands/sensors/run');
842
+ const result = await runSensors({ cwd: project, all: true });
843
+ expect(result.invalidEntries).toEqual([
844
+ expect.objectContaining({
845
+ name: 'lint',
846
+ reason: expect.stringMatching(/project-sensor-name-collision:\s*lint/),
847
+ }),
848
+ ]);
849
+ const lint = result.sensors.find((s) => s.name === 'lint');
850
+ expect(lint).toBeDefined();
851
+ expect(lint.certification).not.toBe('project-declared');
852
+ // Pack prepare/synthetic outcome — never the project node command.
853
+ expect(mockRunStructuredCommand).not.toHaveBeenCalledWith(expect.objectContaining({ executable: 'node' }), expect.any(Object));
854
+ });
855
+ it('carries project-declared provenance on a disabled/synthetic project sensor (RF-2.2)', async () => {
856
+ writeManifest({
857
+ 'iac-format': { ...projectIac('process.exit(0)'), enabled: false },
858
+ }, null);
859
+ const { runSensors } = require('../../../src/commands/sensors/run');
860
+ const result = await runSensors({ cwd: project, all: true });
861
+ expect(result.sensors).toEqual([
862
+ expect.objectContaining({
863
+ name: 'iac-format',
864
+ status: 'skipped',
865
+ skipReason: 'disabled',
866
+ certification: 'project-declared',
867
+ }),
868
+ ]);
869
+ expect(mockRunStructuredCommand).not.toHaveBeenCalled();
870
+ });
871
+ it('runs a project sensor when its allFiles marker exists', async () => {
872
+ writeManifest({
873
+ 'iac-format': {
874
+ ...projectIac('process.exit(0)'),
875
+ applicability: { allFiles: ['package.json'] },
876
+ },
877
+ }, null);
878
+ mockRunStructuredCommand.mockResolvedValue(ok());
879
+ const { runSensors } = require('../../../src/commands/sensors/run');
880
+ const result = await runSensors({ cwd: project, all: true });
881
+ expect(result.sensors).toEqual([
882
+ expect.objectContaining({ name: 'iac-format', status: 'pass', certification: 'project-declared' }),
883
+ ]);
884
+ expect(mockRunStructuredCommand).toHaveBeenCalledWith(expect.objectContaining({ executable: 'node' }), expect.any(Object));
885
+ });
886
+ it('keeps a project sensor synthetic not-applicable when its allFiles marker is missing', async () => {
887
+ writeManifest({
888
+ 'iac-format': {
889
+ ...projectIac('process.exit(0)'),
890
+ applicability: { allFiles: ['missing-stack.marker'] },
891
+ },
892
+ }, null);
893
+ mockRunStructuredCommand.mockResolvedValue(ok());
894
+ const { runSensors } = require('../../../src/commands/sensors/run');
895
+ const result = await runSensors({ cwd: project, all: true });
896
+ expect(result.sensors).toEqual([
897
+ expect.objectContaining({
898
+ name: 'iac-format',
899
+ status: 'inconclusive',
900
+ skipReason: 'not-applicable: applicability-not-met',
901
+ certification: 'project-declared',
902
+ }),
903
+ ]);
904
+ expect(mockRunStructuredCommand).not.toHaveBeenCalled();
905
+ });
906
+ it('does not soft-recover when packageRoot is a symlink escaping the project root', async () => {
907
+ const outside = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-soft-escape-out-'));
908
+ try {
909
+ fs_1.default.symlinkSync(outside, path_1.default.join(project, 'pkg'));
910
+ const body = {
911
+ schemaVersion: 3,
912
+ mode: 'project-sensors',
913
+ packageRoot: 'pkg',
914
+ sensors: {
915
+ broken: {
916
+ source: 'project',
917
+ enabled: true,
918
+ fast: true,
919
+ command: { executable: 'node', resolution: 'path', args: ['-e', 'process.exit(0)'] },
920
+ formatter: 'not-a-formatter',
921
+ },
922
+ good: projectIac('process.exit(0)'),
923
+ },
924
+ };
925
+ fs_1.default.writeFileSync(path_1.default.join(project, '.awm', 'sensors.json'), JSON.stringify(body));
926
+ mockRunStructuredCommand.mockResolvedValue(ok());
927
+ const { runSensors } = require('../../../src/commands/sensors/run');
928
+ const result = await runSensors({ cwd: project, all: true });
929
+ expect(result.overall).toBe('not_certified');
930
+ expect(result.mode).toBe('invalid');
931
+ expect(result.sensors).toEqual([]);
932
+ expect(mockRunStructuredCommand).not.toHaveBeenCalled();
933
+ }
934
+ finally {
935
+ fs_1.default.rmSync(outside, { recursive: true, force: true });
936
+ }
937
+ });
938
+ it('does not soft-recover a symlinked sensors.json (non-regular-file authority)', async () => {
939
+ const realManifest = path_1.default.join(project, 'elsewhere-sensors.json');
940
+ fs_1.default.writeFileSync(realManifest, JSON.stringify({
941
+ schemaVersion: 3,
942
+ mode: 'project-sensors',
943
+ sensors: { good: projectIac('process.exit(0)') },
944
+ }));
945
+ const manifestPath = path_1.default.join(project, '.awm', 'sensors.json');
946
+ if (fs_1.default.existsSync(manifestPath))
947
+ fs_1.default.unlinkSync(manifestPath);
948
+ fs_1.default.symlinkSync(realManifest, manifestPath);
949
+ mockRunStructuredCommand.mockResolvedValue(ok());
950
+ const { runSensors } = require('../../../src/commands/sensors/run');
951
+ const result = await runSensors({ cwd: project, all: true });
952
+ expect(result.overall).toBe('not_certified');
953
+ expect(result.mode).toBe('invalid');
954
+ expect(result.sensors).toEqual([]);
955
+ expect(mockRunStructuredCommand).not.toHaveBeenCalled();
956
+ });
957
+ it('fail-closes a project id collision when the live pack cannot be resolved (live null)', async () => {
958
+ writeManifest({ lint: projectIac('process.exit(0)') });
959
+ fs_1.default.writeFileSync(path_1.default.join(home, 'registries', 'baseline', 'sensor-packs', 'js-ts', 'pack.json'), 'not-valid-json{');
960
+ mockRunStructuredCommand.mockResolvedValue(ok());
961
+ const { runSensors } = require('../../../src/commands/sensors/run');
962
+ const result = await runSensors({ cwd: project, all: true });
963
+ expect(result.invalidEntries).toEqual([
964
+ expect.objectContaining({
965
+ name: 'lint',
966
+ reason: expect.stringMatching(/project-sensor-name-collision:\s*lint/),
967
+ }),
968
+ ]);
969
+ const lint = result.sensors.find((s) => s.name === 'lint');
970
+ if (lint)
971
+ expect(lint.certification).not.toBe('project-declared');
972
+ expect(mockRunStructuredCommand).not.toHaveBeenCalledWith(expect.objectContaining({ executable: 'node' }), expect.any(Object));
973
+ });
974
+ });