agentic-workflow-manager 9.7.1 → 9.9.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 (107) hide show
  1. package/README.md +5 -1
  2. package/dist/src/commands/job/exec-wrapper.js +53 -3
  3. package/dist/src/commands/job/gate.js +18 -0
  4. package/dist/src/commands/job/index.js +61 -0
  5. package/dist/src/commands/model-policy/index.js +73 -0
  6. package/dist/src/commands/plan/index.js +182 -7
  7. package/dist/src/commands/sensors/bootstrap.js +36 -5
  8. package/dist/src/commands/sensors/compatibility/contract.js +19 -4
  9. package/dist/src/commands/sensors/compatibility/discovery.js +23 -3
  10. package/dist/src/commands/sensors/compatibility/live.js +18 -1
  11. package/dist/src/commands/sensors/compatibility/probe.js +8 -2
  12. package/dist/src/commands/sensors/compatibility/resolve.js +5 -2
  13. package/dist/src/commands/sensors/coverage/evidence.js +19 -10
  14. package/dist/src/commands/sensors/coverage/index.js +11 -2
  15. package/dist/src/commands/sensors/coverage/resolve.js +10 -0
  16. package/dist/src/commands/sensors/exec.js +16 -1
  17. package/dist/src/commands/sensors/migrate.js +62 -2
  18. package/dist/src/commands/sensors/prepare.js +2 -0
  19. package/dist/src/commands/sensors/result.js +2 -0
  20. package/dist/src/commands/sensors/run.js +64 -1
  21. package/dist/src/commands/sensors/status.js +6 -4
  22. package/dist/src/commands/watch/apply.js +32 -0
  23. package/dist/src/commands/watch/archive-unused.js +138 -0
  24. package/dist/src/commands/watch/generations.js +32 -9
  25. package/dist/src/commands/watch/index.js +108 -2
  26. package/dist/src/commands/watch/init.js +149 -18
  27. package/dist/src/commands/watch/supervisor.js +167 -13
  28. package/dist/src/commands/watch/teardown-driver.js +13 -13
  29. package/dist/src/commands/watch/track-state.js +59 -0
  30. package/dist/src/commands/watch/tracks.js +38 -102
  31. package/dist/src/core/admission/index.js +231 -0
  32. package/dist/src/core/admission/registry-contracts.js +290 -0
  33. package/dist/src/core/journal/fingerprint.js +35 -1
  34. package/dist/src/core/journal/paths.js +11 -0
  35. package/dist/src/core/journal/requests.js +1 -1
  36. package/dist/src/core/journal/store.js +146 -4
  37. package/dist/src/core/journal/types.js +57 -2
  38. package/dist/src/core/migration/historical-provenance.js +18 -0
  39. package/dist/src/core/migration/index.js +322 -0
  40. package/dist/src/core/model-policy/canonical.js +18 -0
  41. package/dist/src/core/model-policy/capabilities.js +197 -0
  42. package/dist/src/core/model-policy/capability-types.js +2 -0
  43. package/dist/src/core/model-policy/journal.js +124 -0
  44. package/dist/src/core/model-policy/paths.js +12 -0
  45. package/dist/src/core/model-policy/resolve.js +116 -0
  46. package/dist/src/core/model-policy/store.js +182 -0
  47. package/dist/src/core/model-policy/types.js +2 -0
  48. package/dist/src/core/model-policy/validate.js +112 -0
  49. package/dist/src/core/plan/identity.js +77 -0
  50. package/dist/src/core/plan/validate.js +191 -78
  51. package/dist/src/index.js +2 -0
  52. package/dist/tests/commands/job/exec-wrapper.test.js +16 -0
  53. package/dist/tests/commands/job/gate-reconcile.test.js +40 -10
  54. package/dist/tests/commands/job/gate.test.js +9 -0
  55. package/dist/tests/commands/job/routing-report-cli.test.js +46 -0
  56. package/dist/tests/commands/job/routing.test.js +11 -0
  57. package/dist/tests/commands/model-policy/index.test.js +57 -0
  58. package/dist/tests/commands/pin.test.js +23 -23
  59. package/dist/tests/commands/plan/index.test.js +682 -14
  60. package/dist/tests/commands/sensors/bootstrap.test.js +57 -4
  61. package/dist/tests/commands/sensors/compatibility/contract.test.js +21 -0
  62. package/dist/tests/commands/sensors/compatibility/discovery.test.js +19 -0
  63. package/dist/tests/commands/sensors/compatibility/probe.test.js +14 -1
  64. package/dist/tests/commands/sensors/compatibility/resolve.test.js +17 -0
  65. package/dist/tests/commands/sensors/coverage/evidence.test.js +51 -27
  66. package/dist/tests/commands/sensors/coverage/resolve.test.js +51 -0
  67. package/dist/tests/commands/sensors/exec.test.js +112 -0
  68. package/dist/tests/commands/sensors/run-changed.test.js +11 -0
  69. package/dist/tests/commands/sensors/run.test.js +15 -2
  70. package/dist/tests/commands/sensors/status.test.js +3 -3
  71. package/dist/tests/commands/watch/apply.test.js +12 -0
  72. package/dist/tests/commands/watch/e2e-crash.test.js +126 -11
  73. package/dist/tests/commands/watch/generations.test.js +113 -4
  74. package/dist/tests/commands/watch/plan-lifecycle.test.js +263 -0
  75. package/dist/tests/commands/watch/supervisor-loop.test.js +247 -49
  76. package/dist/tests/commands/watch/track-finalize.test.js +20 -3
  77. package/dist/tests/commands/watch/track-freeze.test.js +43 -30
  78. package/dist/tests/commands/watch/track-state-dependency.test.js +15 -0
  79. package/dist/tests/commands/watch/watch-init.test.js +244 -0
  80. package/dist/tests/core/admission/index.test.js +140 -0
  81. package/dist/tests/core/journal/fingerprint.test.js +22 -0
  82. package/dist/tests/core/journal/store.test.js +53 -0
  83. package/dist/tests/core/journal/types.test.js +7 -0
  84. package/dist/tests/core/migration/facts.test.js +289 -0
  85. package/dist/tests/core/migration/historical-provenance.test.js +25 -0
  86. package/dist/tests/core/model-policy/capabilities.test.js +365 -0
  87. package/dist/tests/core/model-policy/journal.test.js +46 -0
  88. package/dist/tests/core/model-policy/policy.test.js +47 -0
  89. package/dist/tests/core/model-policy/resolve.test.js +111 -0
  90. package/dist/tests/core/model-policy/store.test.js +177 -0
  91. package/dist/tests/core/plan/validate.test.js +360 -8
  92. package/dist/tests/core/secure-fs/native-bridge.test.js +81 -4
  93. package/dist/tests/integration/pack-e2e.test.js +4 -1
  94. package/dist/tests/integration/preflight-json-pipe.e2e.test.js +7 -3
  95. package/dist/tests/integration/sensor-bootstrap.e2e.test.js +37 -0
  96. package/dist/tests/integration/sensor-structured-run.e2e.test.js +12 -10
  97. package/dist/tests/providers/injection-config.test.js +14 -6
  98. package/dist/tests/structural/directory-fsync-is-single-source.test.js +97 -0
  99. package/dist/tests/structural/jest-environment-is-isolated.test.js +29 -24
  100. package/dist/tests/structural/native-release-artifacts.test.js +30 -0
  101. package/package.json +4 -3
  102. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  103. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  104. package/prebuilds/linux-arm64/secure_fs.node +0 -0
  105. package/prebuilds/linux-x64/secure_fs.node +0 -0
  106. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  107. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -10,7 +10,7 @@ jest.mock('../../../src/commands/sensors/compatibility/pack-source', () => ({ li
10
10
  jest.mock('../../../src/commands/sensors/compatibility/contract', () => ({ ...jest.requireActual('../../../src/commands/sensors/compatibility/contract'), parseSensorPack: jest.fn() }));
11
11
  jest.mock('../../../src/commands/sensors/compatibility/live', () => ({ resolveParsedPackCompatibility: jest.fn() }));
12
12
  jest.mock('../../../src/commands/sensors/compatibility/source', () => ({ resolveSensorSource: jest.fn() }));
13
- jest.mock('../../../src/commands/sensors/migrate', () => ({ planV2Migration: jest.fn(), replaceV2ManifestWithV3: jest.fn() }));
13
+ jest.mock('../../../src/commands/sensors/migrate', () => ({ planV2Migration: jest.fn(), replaceV2ManifestWithV3: jest.fn(), replaceLegacyManifestWithV3: jest.fn() }));
14
14
  jest.mock('../../../src/commands/sensors/compatibility/materialize', () => ({ materializePortableSensors: jest.fn() }));
15
15
  jest.mock('../../../src/commands/sensors/compatibility/safe-file', () => ({ writeProjectFile: jest.fn(), withProjectLease: jest.fn((_root, operation) => operation()) }));
16
16
  const bootstrap_1 = require("../../../src/commands/sensors/bootstrap");
@@ -22,6 +22,7 @@ const live_1 = require("../../../src/commands/sensors/compatibility/live");
22
22
  const source_1 = require("../../../src/commands/sensors/compatibility/source");
23
23
  const migrate_1 = require("../../../src/commands/sensors/migrate");
24
24
  const migrate_2 = require("../../../src/commands/sensors/migrate");
25
+ const migrate_3 = require("../../../src/commands/sensors/migrate");
25
26
  const materialize_1 = require("../../../src/commands/sensors/compatibility/materialize");
26
27
  const safe_file_1 = require("../../../src/commands/sensors/compatibility/safe-file");
27
28
  const fs_1 = __importDefault(require("fs"));
@@ -66,9 +67,27 @@ describe('planSensorBootstrap', () => {
66
67
  await expect((0, bootstrap_1.planSensorBootstrap)(root)).resolves.toMatchObject({ kind: 'blocked', reason: 'no-compatible-registry', changes: [] });
67
68
  expect(migrate_1.planV2Migration).not.toHaveBeenCalled();
68
69
  });
69
- it('preserves a legacy v1 manifest and blocks an ambiguous v2 source without writes', async () => {
70
- project_1.resolveSensorProject.mockReturnValueOnce({ state: 'configured', projectRoot: root, manifestPath: `${root}/.awm/sensors.json`, packageRoot: root, manifest: { kind: 'legacy', pack: { pack: 'js-ts', sensors: {}, compatibility: {} } } });
71
- await expect((0, bootstrap_1.planSensorBootstrap)(root)).resolves.toMatchObject({ kind: 'blocked', reason: 'legacy-v1-preserved', changes: [] });
70
+ it('requires explicit project-sensors mode before replacing a legacy v1 manifest', async () => {
71
+ const project = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-bootstrap-legacy-'));
72
+ const manifestPath = path_1.default.join(project, '.awm', 'sensors.json');
73
+ const variant = { id: 'eslint-9', command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] }, assets: [] };
74
+ const pack = { schemaVersion: 2, name: 'js-ts', sensors: { lint: { variants: [variant] } } };
75
+ try {
76
+ fs_1.default.mkdirSync(path_1.default.dirname(manifestPath), { recursive: true });
77
+ fs_1.default.writeFileSync(manifestPath, JSON.stringify({ pack: 'js-ts', sensors: { lint: { cmd: 'npx eslint .' } } }));
78
+ project_1.resolveSensorProject.mockReturnValue({ state: 'configured', projectRoot: project, manifestPath, packageRoot: project, manifest: { kind: 'legacy', pack: { pack: 'js-ts', sensors: { lint: { cmd: 'npx eslint .' } }, compatibility: {} } } });
79
+ pack_source_1.listPackSources.mockReturnValue([source]);
80
+ contract_1.parseSensorPack.mockReturnValue({ kind: 'v2', pack });
81
+ live_1.resolveParsedPackCompatibility.mockResolvedValue({ pack, sensors: { lint: { state: 'certified', reason: 'ok', variantId: 'eslint-9', toolVersion: '9.0.0', runtimeVersion: '24.0.0', certifiedRange: '>=9 <10', evidence: [] } } });
82
+ await expect((0, bootstrap_1.planSensorBootstrap)(project)).resolves.toMatchObject({ kind: 'blocked', reason: 'legacy-replacement-requires-project-sensors-mode', changes: [] });
83
+ await expect((0, bootstrap_1.planSensorBootstrap)(project, { mode: 'project-sensors' })).resolves.toMatchObject({ kind: 'migrate', changes: [{ path: '.awm/sensors.json', action: 'replace' }], migration: { kind: 'legacy-v1' }, source });
84
+ // A legacy manifest identifies the old pack; it is not an explicit
85
+ // opt-in to every optional sensor in that pack.
86
+ expect(live_1.resolveParsedPackCompatibility).toHaveBeenCalledWith(project, pack);
87
+ }
88
+ finally {
89
+ fs_1.default.rmSync(project, { recursive: true, force: true });
90
+ }
72
91
  project_1.resolveSensorProject.mockReturnValueOnce({ state: 'configured', projectRoot: root, manifestPath: `${root}/.awm/sensors.json`, packageRoot: root, manifest: { kind: 'v2', pack: { schemaVersion: 2, pack: 'js-ts', sensors: {} } } });
73
92
  source_1.resolveSensorSource.mockReturnValue({ kind: 'source-ambiguous', reason: 'multiple-compatible-registries', remedy: 'configure-one-logical-registry', candidates: ['a', 'b'] });
74
93
  await expect((0, bootstrap_1.planSensorBootstrap)(root)).resolves.toMatchObject({ kind: 'blocked', reason: 'multiple-compatible-registries', changes: [] });
@@ -116,6 +135,25 @@ describe('planSensorBootstrap', () => {
116
135
  live_1.resolveParsedPackCompatibility.mockResolvedValue({ pack: { schemaVersion: 2, name: 'js-ts', sensors: { lint: { variants: [variant] } } }, sensors: { lint: { variantId: null } } });
117
136
  await expect((0, bootstrap_1.planSensorBootstrap)(root, { mode: 'project-sensors' })).resolves.toMatchObject({ kind: 'blocked', reason: 'sensor-variant-unresolvable', changes: [] });
118
137
  });
138
+ it('omits explicit-opt-in sensors that are not applicable while retaining mandatory gates', async () => {
139
+ const variant = { id: 'main', command: { executable: 'tool', resolution: 'path', args: ['run'] }, assets: [] };
140
+ const pack = { schemaVersion: 2, name: 'js-ts', sensors: {
141
+ security: { variants: [variant] }, test: { variants: [variant] },
142
+ format: { applicability: { kind: 'explicit-opt-in' }, variants: [variant] },
143
+ mutation: { applicability: { kind: 'explicit-opt-in' }, variants: [variant] },
144
+ } };
145
+ pack_source_1.listPackSources.mockReturnValue([source]);
146
+ contract_1.parseSensorPack.mockReturnValue({ kind: 'v2', pack });
147
+ live_1.resolveParsedPackCompatibility.mockResolvedValue({ pack, sensors: {
148
+ security: { state: 'certified', reason: 'range-and-probe', variantId: 'main', toolVersion: '1.0.0', runtimeVersion: '24.0.0', certifiedRange: '>=1', evidence: [] },
149
+ test: { state: 'certified', reason: 'range-and-probe', variantId: 'main', toolVersion: '1.0.0', runtimeVersion: '24.0.0', certifiedRange: '>=1', evidence: [] },
150
+ format: { state: 'not-applicable', reason: 'applicability-not-met', variantId: null, toolVersion: null, runtimeVersion: null, certifiedRange: null, evidence: [] },
151
+ mutation: { state: 'not-applicable', reason: 'applicability-not-met', variantId: null, toolVersion: null, runtimeVersion: null, certifiedRange: null, evidence: [] },
152
+ } });
153
+ const plan = await (0, bootstrap_1.planSensorBootstrap)(root, { mode: 'project-sensors' });
154
+ expect(plan).toMatchObject({ kind: 'create', manifest: { sensors: { security: expect.any(Object), test: expect.any(Object) } } });
155
+ expect(Object.keys(plan.manifest.sensors)).toEqual(['security', 'test']);
156
+ });
119
157
  it('converts a compatibility probe failure into a stable blocked plan', async () => {
120
158
  pack_source_1.listPackSources.mockReturnValue([source]);
121
159
  contract_1.parseSensorPack.mockReturnValue({ kind: 'v2', pack: { schemaVersion: 2, name: 'js-ts', sensors: {} } });
@@ -159,6 +197,21 @@ describe('planSensorBootstrap', () => {
159
197
  fs_1.default.rmSync(project, { recursive: true, force: true });
160
198
  }
161
199
  });
200
+ it('delegates a legacy migration to its fenced CAS replacement', () => {
201
+ const project = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-bootstrap-legacy-'));
202
+ const manifestPath = path_1.default.join(project, 'sensors.json');
203
+ try {
204
+ const bytes = Buffer.from('{"pack":"js-ts","sensors":{}}\n');
205
+ fs_1.default.writeFileSync(manifestPath, bytes);
206
+ const manifest = { schemaVersion: 3, mode: 'project-sensors', pack: 'js-ts', source: { registry: 'baseline' }, sensors: {} };
207
+ const plan = { kind: 'migrate', projectRoot: project, manifestPath, dryRun: false, changes: [{ path: '.awm/sensors.json', action: 'replace' }], migration: { kind: 'legacy-v1', manifest }, source, originalDigest: (0, crypto_1.createHash)('sha256').update(bytes).digest('hex') };
208
+ expect((0, bootstrap_1.applySensorBootstrap)(plan)).toBe('migrated');
209
+ expect(migrate_3.replaceLegacyManifestWithV3).toHaveBeenCalledWith(plan.manifestPath, manifest, source);
210
+ }
211
+ finally {
212
+ fs_1.default.rmSync(project, { recursive: true, force: true });
213
+ }
214
+ });
162
215
  it('rejects a migration plan whose manifest changed after planning', () => {
163
216
  const project = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-bootstrap-stale-'));
164
217
  const manifestPath = path_1.default.join(project, 'sensors.json');
@@ -134,6 +134,13 @@ describe('sensor pack v2 contract', () => {
134
134
  it('parses a valid versioned pack', () => {
135
135
  expect((0, contract_1.parseSensorPack)(validPack(), 'pack.json')).toMatchObject({ kind: 'v2', pack: validPack() });
136
136
  });
137
+ it('accepts the bounded explicit-opt-in applicability contract', () => {
138
+ const pack = validPack();
139
+ pack.sensors.lint.applicability = { kind: 'explicit-opt-in' };
140
+ expect((0, contract_1.parseSensorPack)(pack, 'pack.json')).toMatchObject({
141
+ kind: 'v2', pack: { sensors: { lint: { applicability: { kind: 'explicit-opt-in' } } } },
142
+ });
143
+ });
137
144
  it('accepts one standalone files placeholder in changedCommand (R4)', () => {
138
145
  const pack = validPack();
139
146
  pack.sensors.lint.variants[0].changedCommand = {
@@ -237,8 +244,22 @@ describe('sensor pack v2 contract', () => {
237
244
  it('rejects malformed applicability and probe extensions', () => {
238
245
  const variant = validPack().sensors.lint.variants[0];
239
246
  expect(() => (0, contract_1.parseSensorPack)({ ...validPack(), sensors: { lint: { applicability: { allFiles: [3] }, variants: [variant] } } }, 'pack')).toThrow('allFiles[0]');
247
+ expect(() => (0, contract_1.parseSensorPack)({ ...validPack(), sensors: { lint: { applicability: { kind: 'invented' }, variants: [variant] } } }, 'pack')).toThrow('applicability.kind');
240
248
  expect(() => (0, contract_1.parseSensorPack)({ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...variant, probe: { kind: 'version', extra: true } }] } } }, 'pack')).toThrow('unknown field');
241
249
  });
250
+ it('preserves historical unnamed probes while validating explicit script names', () => {
251
+ const variant = { ...validPack().sensors.lint.variants[0], probe: { kind: 'package-script-present', script: 'test' } };
252
+ expect((0, contract_1.parseSensorPack)({ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [variant] } } }, 'pack'))
253
+ .toMatchObject({ kind: 'v2', pack: { sensors: { lint: { variants: [{ probe: { kind: 'package-script-present', script: 'test' } }] } } } });
254
+ expect((0, contract_1.parseSensorPack)({ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...variant, probe: { kind: 'package-script-present' } }] } } }, 'pack'))
255
+ .toMatchObject({ kind: 'v2', pack: { sensors: { lint: { variants: [{ probe: { kind: 'package-script-present' } }] } } } });
256
+ for (const script of ['', null, 'test;echo unsafe', 'test\u0000']) {
257
+ expect(() => (0, contract_1.parseSensorPack)({ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...variant, probe: { kind: 'package-script-present', script } }] } } }, 'pack'))
258
+ .toThrow('probe.script');
259
+ }
260
+ expect(() => (0, contract_1.parseSensorPack)({ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...variant, probe: { kind: 'version', script: 'test' } }] } } }, 'pack'))
261
+ .toThrow('probe.script');
262
+ });
242
263
  it('preserves config selectors and validates package.json field names', () => {
243
264
  const variant = { ...validPack().sensors.lint.variants[0], requirements: { ...validPack().sensors.lint.variants[0].requirements, configFiles: ['eslint.config.js'], packageJsonFields: ['eslintConfig'] } };
244
265
  expect((0, contract_1.parseSensorPack)({ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [variant] } } }, 'pack')).toMatchObject({ kind: 'v2', pack: { sensors: { lint: { variants: [{ requirements: { configFiles: ['eslint.config.js'], packageJsonFields: ['eslintConfig'] } }] } } } });
@@ -72,6 +72,25 @@ describe('discoverProjectEvidence', () => {
72
72
  fs_1.default.rmSync(root, { recursive: true, force: true });
73
73
  }
74
74
  });
75
+ it('takes a selected PATH package manager version only from its bounded executable provenance', () => {
76
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-package-manager-discovery-'));
77
+ const pathToolVersion = jest.fn((tool) => tool === 'npm' ? '10.9.2' : null);
78
+ try {
79
+ fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), JSON.stringify({ packageManager: 'npm@10.9.2', scripts: { test: 'jest' } }));
80
+ fs_1.default.mkdirSync(path_1.default.join(root, 'node_modules', 'npm'), { recursive: true });
81
+ fs_1.default.writeFileSync(path_1.default.join(root, 'node_modules', 'npm', 'package.json'), JSON.stringify({ version: '999.0.0' }));
82
+ const pack = { schemaVersion: 2, name: 'js-ts', detects: ['package.json'], sensors: {
83
+ test: { applicability: { allFiles: ['package.json'] }, variants: [{ requirements: { tool: 'npm', runtime: 'node' }, command: { executable: 'npm', resolution: 'path', packageManager: 'npm' } }] },
84
+ } };
85
+ const evidence = (0, discovery_1.discoverProjectEvidence)(root, pack, { platform: () => 'darwin', pathToolVersion });
86
+ expect(pathToolVersion).toHaveBeenCalledWith('npm');
87
+ expect(evidence.toolVersions.npm).toBe('10.9.2');
88
+ expect(evidence.toolProvenance.npm).toBe('path');
89
+ }
90
+ finally {
91
+ fs_1.default.rmSync(root, { recursive: true, force: true });
92
+ }
93
+ });
75
94
  test.each([
76
95
  ['linux', ['.venv', 'lib', 'python3.12', 'site-packages']],
77
96
  ['win32', ['.venv', 'Lib', 'site-packages']],
@@ -16,7 +16,7 @@ const evidence = { cwd: process.cwd(), configFiles: ['eslint.config.js'], script
16
16
  describe('runCompatibilityProbe', () => {
17
17
  beforeEach(() => fakeExecutor.mockClear());
18
18
  test.each(['eslint-print-config', 'typescript-show-config', 'semgrep-validate', 'version', 'package-script-present', 'config-present'])('runs the closed probe %s with sanitized output', async (kind) => {
19
- const result = await (0, probe_1.runCompatibilityProbe)({ kind }, evidence, fakeExecutor);
19
+ const result = await (0, probe_1.runCompatibilityProbe)(kind === 'package-script-present' ? { kind, script: 'lint' } : { kind }, evidence, fakeExecutor);
20
20
  expect(['matched', 'unverifiable']).toContain(result.status);
21
21
  expect(JSON.stringify(result)).not.toContain('SECRET_VALUE');
22
22
  });
@@ -26,6 +26,19 @@ describe('runCompatibilityProbe', () => {
26
26
  fakeExecutor.mockResolvedValueOnce(execResult({ overflowed: true, stdout: 'ok' }));
27
27
  await expect((0, probe_1.runCompatibilityProbe)({ kind: 'version' }, evidence, fakeExecutor)).resolves.toMatchObject({ status: 'unverifiable' });
28
28
  });
29
+ it('matches only the registry-declared package script', async () => {
30
+ await expect((0, probe_1.runCompatibilityProbe)({ kind: 'package-script-present', script: 'test' }, { ...evidence, scripts: ['lint', 'test:unit'] }, fakeExecutor)).resolves.toMatchObject({ status: 'not-matched', reason: 'package-script' });
31
+ await expect((0, probe_1.runCompatibilityProbe)({ kind: 'package-script-present', script: 'test' }, { ...evidence, scripts: ['lint', 'test'] }, fakeExecutor)).resolves.toMatchObject({ status: 'matched', reason: 'package-script' });
32
+ expect(fakeExecutor).not.toHaveBeenCalled();
33
+ });
34
+ it('never certifies a historical unnamed package-script probe', async () => {
35
+ await expect((0, probe_1.runCompatibilityProbe)({ kind: 'package-script-present' }, { ...evidence, scripts: ['lint', 'test', 'test:unit'] }, fakeExecutor)).resolves.toEqual({ status: 'unverifiable', reason: 'package-script-name-required' });
36
+ expect(fakeExecutor).not.toHaveBeenCalled();
37
+ });
38
+ it.each(['', null, 'test;echo unsafe'])('rejects malformed explicit script %j', async (script) => {
39
+ await expect((0, probe_1.runCompatibilityProbe)({ kind: 'package-script-present', script }, evidence, fakeExecutor)).rejects.toThrow('requires a named package script');
40
+ expect(fakeExecutor).not.toHaveBeenCalled();
41
+ });
29
42
  it('binds tool probes to the project node_modules executable instead of PATH', async () => {
30
43
  await (0, probe_1.runCompatibilityProbe)({ kind: 'eslint-print-config' }, evidence, fakeExecutor);
31
44
  expect(fakeExecutor).toHaveBeenCalledWith(expect.objectContaining({ executable: 'eslint', resolution: 'node-modules-bin' }), expect.any(Object));
@@ -20,6 +20,13 @@ describe('resolveSensorCompatibility', () => {
20
20
  const resolved = (0, resolve_1.resolveProjectCompatibility)({ schemaVersion: 2, name: 'generic', sensors: { security: { applicability: { kind: 'explicit-or-supported-language' }, variants: [variant('semgrep')] } } }, evidence({ applicable: undefined, paths: [], packSelection: 'explicit' }));
21
21
  expect(resolved.sensors.security.state).not.toBe('not-applicable');
22
22
  });
23
+ it('keeps an explicit-opt-in sensor not-applicable until the project declares it', () => {
24
+ const optIn = { applicability: { kind: 'explicit-opt-in' }, variants: [variant('prettier')] };
25
+ expect((0, resolve_1.resolveSensorCompatibility)(optIn, evidence({ applicable: undefined, packSelection: undefined }), { pack: 'js-ts', sensor: 'format' }))
26
+ .toMatchObject({ state: 'not-applicable', reason: 'applicability-not-met', variantId: null });
27
+ expect((0, resolve_1.resolveSensorCompatibility)(optIn, evidence({ packSelection: 'explicit' }), { pack: 'js-ts', sensor: 'format' }))
28
+ .toMatchObject({ state: 'certified', variantId: 'prettier' });
29
+ });
23
30
  test.each([
24
31
  ['certified', evidence(), 'eslint-main'],
25
32
  ['compatible-unverified', evidence({ toolVersion: '11.0.0' }), 'eslint-main'],
@@ -33,6 +40,16 @@ describe('resolveSensorCompatibility', () => {
33
40
  it('preserves legacy behavior without executing a probe', () => {
34
41
  expect((0, resolve_1.resolveSensorCompatibility)({ defaultCmd: 'eslint .' }, evidence(), context)).toEqual((0, manifest_1.legacyCompatibility)('legacy pack without schemaVersion'));
35
42
  });
43
+ it('does not certify a PATH package manager from node_modules provenance', () => {
44
+ const packageManager = variant('npm-script');
45
+ packageManager.requirements = { tool: 'npm', toolRange: '>=10 <11', runtime: 'node', runtimeRange: '>=20' };
46
+ packageManager.command = { executable: 'npm', resolution: 'path', args: ['test'], packageManager: 'npm' };
47
+ const npmSensor = { applicability: { allFiles: ['package.json'] }, variants: [packageManager] };
48
+ expect((0, resolve_1.resolveSensorCompatibility)(npmSensor, evidence({ toolVersions: { npm: '10.9.2' }, runtimeVersions: { node: '24.0.0' }, toolProvenance: { npm: 'node-modules-bin' } }), context))
49
+ .toMatchObject({ state: 'missing-tool', reason: 'tool-not-found' });
50
+ expect((0, resolve_1.resolveSensorCompatibility)(npmSensor, evidence({ toolVersions: { npm: '10.9.2' }, runtimeVersions: { node: '24.0.0' }, toolProvenance: { npm: 'path' }, probe: { status: 'matched' } }), context))
51
+ .toMatchObject({ state: 'certified', variantId: 'npm-script' });
52
+ });
36
53
  it('fails an equal-precedence match with its pack, sensor, IDs, and ranges', () => {
37
54
  expect(() => (0, resolve_1.resolveProjectCompatibility)({ schemaVersion: 2, name: 'js-ts', sensors: { lint: { ...sensor, variants: [variant('one'), variant('two')] } } }, evidence())).toThrow(/js-ts.*lint.*one.*>=9 <12.*two.*>=9 <12/i);
38
55
  });
@@ -27,21 +27,57 @@ const detector = {
27
27
  files: [{ path: 'eslint.config.js', containsAll: ['no-unreachable'] }],
28
28
  },
29
29
  };
30
- const noFollowUnavailableOnNativeWindows = process.platform === 'win32'
31
- && typeof fs_1.default.constants.O_NOFOLLOW !== 'number';
32
- const regularFileStatus = (posixStatus) => noFollowUnavailableOnNativeWindows ? 'unverifiable' : posixStatus;
30
+ function observeWithoutNoFollow(io, project = root) {
31
+ let result;
32
+ try {
33
+ jest.isolateModules(() => {
34
+ jest.doMock('fs', () => {
35
+ const actual = jest.requireActual('fs');
36
+ const constants = { ...actual.constants, O_NOFOLLOW: undefined };
37
+ return { __esModule: true, default: { ...actual, constants }, ...actual, constants };
38
+ });
39
+ const observe = require('../../../../src/commands/sensors/coverage/evidence').observeDetector;
40
+ result = observe(project, 'style', 0, detector, { cmd: 'eslint --config eslint.config.js' }, io);
41
+ });
42
+ }
43
+ finally {
44
+ jest.dontMock('fs');
45
+ }
46
+ if (!result)
47
+ throw new Error('portable evidence observer did not return a result');
48
+ return result;
49
+ }
50
+ test('native anchored evidence remains readable without O_NOFOLLOW', () => {
51
+ fs_1.default.writeFileSync(path_1.default.join(root, 'eslint.config.js'), 'no-unreachable');
52
+ expect(observeWithoutNoFollow()).toMatchObject({ status: 'covered' });
53
+ });
54
+ test('missing O_NOFOLLOW never lends native trust to injected IO', () => {
55
+ fs_1.default.writeFileSync(path_1.default.join(root, 'eslint.config.js'), 'no-unreachable');
56
+ expect(observeWithoutNoFollow({ lstatSync: fs_1.default.lstatSync })).toMatchObject({ status: 'unverifiable' });
57
+ });
58
+ test.each(['linked-leaf', 'oversize', 'missing-marker'])('native evidence without O_NOFOLLOW rejects %s', kind => {
59
+ const file = path_1.default.join(root, 'eslint.config.js');
60
+ if (kind === 'linked-leaf') {
61
+ const target = path_1.default.join(root, 'target.js');
62
+ fs_1.default.writeFileSync(target, 'no-unreachable');
63
+ fs_1.default.symlinkSync(target, file);
64
+ }
65
+ else
66
+ fs_1.default.writeFileSync(file, kind === 'oversize' ? Buffer.alloc(contract_1.MAX_COVERAGE_FILE_BYTES + 1) : 'different-rule');
67
+ expect(observeWithoutNoFollow().status).toBe(kind === 'missing-marker' ? 'ineffective' : 'unverifiable');
68
+ });
69
+ test('native evidence without O_NOFOLLOW rejects linked ancestors', () => {
70
+ externalRoot = (0, tmp_1.mkCanonicalTmpDir)('awm-coverage-linked-parent-');
71
+ fs_1.default.writeFileSync(path_1.default.join(externalRoot, 'eslint.config.js'), 'no-unreachable');
72
+ const project = path_1.default.join(root, 'linked-project');
73
+ fs_1.default.symlinkSync(externalRoot, project, process.platform === 'win32' ? 'junction' : 'dir');
74
+ expect(observeWithoutNoFollow(undefined, project)).toMatchObject({ status: 'unverifiable' });
75
+ });
33
76
  test('active matching sensor with all AND evidence is covered (R2.2)', () => {
34
77
  fs_1.default.writeFileSync(path_1.default.join(root, 'eslint.config.js'), "rules: { 'no-unreachable': 'error' }");
35
78
  const observed = (0, evidence_1.observeDetector)(root, 'style', 0, detector, {
36
79
  cmd: 'npx eslint . --config eslint.config.js', enabled: true,
37
80
  });
38
- if (noFollowUnavailableOnNativeWindows) {
39
- expect(observed).toMatchObject({
40
- status: 'unverifiable',
41
- evidence: expect.arrayContaining([{ kind: 'file', path: 'eslint.config.js', status: 'unverifiable' }]),
42
- });
43
- return;
44
- }
45
81
  expect(observed).toEqual({
46
82
  classId: 'style',
47
83
  detectorIndex: 0,
@@ -60,7 +96,7 @@ test('structured v2 commands supply structural coverage evidence without exposin
60
96
  enabled: true,
61
97
  command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.', '--config', 'eslint.config.js'] },
62
98
  });
63
- expect(observed.status).toBe(regularFileStatus('covered'));
99
+ expect(observed.status).toBe('covered');
64
100
  expect(JSON.stringify(observed)).not.toContain('--config');
65
101
  });
66
102
  test.each([
@@ -93,25 +129,13 @@ test('recognized command plus missing file is ineffective (R2.4)', () => {
93
129
  test('recognized command plus missing literal marker is ineffective (R2.4)', () => {
94
130
  fs_1.default.writeFileSync(path_1.default.join(root, 'eslint.config.js'), 'export default []');
95
131
  const out = (0, evidence_1.observeDetector)(root, 'style', 0, detector, { cmd: 'eslint --config eslint.config.js' });
96
- expect(out.status).toBe(regularFileStatus('ineffective'));
97
- if (noFollowUnavailableOnNativeWindows) {
98
- expect(out.evidence).toContainEqual({ kind: 'file', path: 'eslint.config.js', status: 'unverifiable' });
99
- }
100
- else {
101
- expect(out.evidence).toContainEqual({ kind: 'marker', path: 'eslint.config.js', ordinal: 1, status: 'missing' });
102
- }
132
+ expect(out.status).toBe('ineffective');
133
+ expect(out.evidence).toContainEqual({ kind: 'marker', path: 'eslint.config.js', ordinal: 1, status: 'missing' });
103
134
  });
104
- test('native Windows without no-follow support never certifies regular evidence from its contents', () => {
135
+ test('native platform securely observes regular evidence from its contents', () => {
105
136
  fs_1.default.writeFileSync(path_1.default.join(root, 'eslint.config.js'), 'no-unreachable');
106
137
  const observed = (0, evidence_1.observeDetector)(root, 'style', 0, detector, { cmd: 'eslint --config eslint.config.js' });
107
- if (noFollowUnavailableOnNativeWindows) {
108
- expect(observed.status).toBe('unverifiable');
109
- expect(observed.status).not.toBe('covered');
110
- expect(observed.status).not.toBe('ineffective');
111
- }
112
- else {
113
- expect(observed.status).toBe('covered');
114
- }
138
+ expect(observed.status).toBe('covered');
115
139
  });
116
140
  test('unverifiable file evidence dominates missing evidence in the detector result (R2.5a)', () => {
117
141
  const mixed = {
@@ -7,6 +7,7 @@ const fs_1 = __importDefault(require("fs"));
7
7
  const path_1 = __importDefault(require("path"));
8
8
  const tmp_1 = require("../../../support/tmp");
9
9
  const resolve_1 = require("../../../../src/commands/sensors/coverage/resolve");
10
+ const coverage_1 = require("../../../../src/commands/sensors/coverage");
10
11
  function resolveCoverageWithoutNoFollow(cwd) {
11
12
  let resolve;
12
13
  jest.isolateModules(() => {
@@ -67,6 +68,15 @@ test('no manifest returns not_configured without reading registries', () => {
67
68
  lstat.mockRestore();
68
69
  }
69
70
  });
71
+ test('a worktree without its own manifest never inherits the parent checkout manifest', () => {
72
+ writeManifest({ pack: 'js-ts', sensors: {} });
73
+ configure(['baseline']);
74
+ writePack('baseline', 'js-ts', { name: 'js-ts', sensors: {}, coverage });
75
+ const worktree = path_1.default.join(project, '.worktrees', 'feature');
76
+ fs_1.default.mkdirSync(worktree, { recursive: true });
77
+ fs_1.default.writeFileSync(path_1.default.join(worktree, '.git'), 'gitdir: ../.git/worktrees/feature');
78
+ expect((0, resolve_1.resolveCoverageInputs)(worktree)).toEqual({ kind: 'not_configured' });
79
+ });
70
80
  test('selects the first configured registry containing the exact pack', () => {
71
81
  writeManifest({ pack: 'js-ts', sensors: {} });
72
82
  configure(['first', 'second']);
@@ -91,12 +101,53 @@ test('v3 project-sensors resolves coverage from its declared logical registry',
91
101
  registryRoot: path_1.default.join(awmHome, 'registries', 'second'),
92
102
  });
93
103
  });
104
+ test('v3 monorepo coverage inspects evidence in its declared packageRoot', async () => {
105
+ const packageRoot = path_1.default.join(project, 'cli');
106
+ fs_1.default.mkdirSync(packageRoot);
107
+ fs_1.default.writeFileSync(path_1.default.join(packageRoot, '.dep-cruiser.awm.js'), 'no-circular');
108
+ writeManifest({
109
+ schemaVersion: 3, mode: 'project-sensors', pack: 'js-ts', source: { registry: 'baseline' }, packageRoot: 'cli',
110
+ sensors: { depcheck: {
111
+ enabled: true, variantId: 'dependency-cruiser',
112
+ command: { executable: 'depcruise', resolution: 'node-modules-bin', args: ['--config', '.dep-cruiser.awm.js', 'src'] },
113
+ initializedCompatibility: {
114
+ state: 'compatible-unverified', reason: 'fixture', variantId: 'dependency-cruiser',
115
+ toolVersion: null, runtimeVersion: null, certifiedRange: null, evidence: [],
116
+ },
117
+ } },
118
+ });
119
+ configure(['baseline']);
120
+ writePack('baseline', 'js-ts', { name: 'js-ts', sensors: {}, coverage: { schemaVersion: 1, classes: {
121
+ 'dependency-boundaries': {
122
+ description: 'Dependency boundaries',
123
+ detectors: [{ sensor: 'depcheck', evidence: { commandIncludes: ['depcruise'], files: [{ path: '.dep-cruiser.awm.js', containsAll: ['no-circular'] }] } }],
124
+ remedy: { summary: 'Enable depcheck', command: 'npm run depcheck' },
125
+ },
126
+ } } });
127
+ const live = { state: 'certified', reason: 'fixture', variantId: 'dependency-cruiser',
128
+ toolVersion: '16.10.4', runtimeVersion: '24.0.0', certifiedRange: '=16.10.4', evidence: [] };
129
+ const report = await (0, coverage_1.runCoverage)(project, { resolveLive: jest.fn().mockResolvedValue({ sensors: { depcheck: live } }) });
130
+ expect(report.static.classes[0]).toMatchObject({ status: 'covered', detectors: [{
131
+ sensor: 'depcheck', status: 'covered', evidence: [
132
+ { kind: 'command', status: 'matched' },
133
+ { kind: 'file', path: '.dep-cruiser.awm.js', status: 'matched' },
134
+ { kind: 'marker', path: '.dep-cruiser.awm.js', ordinal: 1, status: 'matched' },
135
+ ],
136
+ }] });
137
+ });
94
138
  test('old pack without coverage is no_reference, not covered', () => {
95
139
  writeManifest({ pack: 'js-ts', sensors: {} });
96
140
  configure(['baseline']);
97
141
  writePack('baseline', 'js-ts', { name: 'js-ts', sensors: {} });
98
142
  expect((0, resolve_1.resolveCoverageInputs)(project)).toMatchObject({ kind: 'no_reference', pack: 'js-ts', registry: 'baseline' });
99
143
  });
144
+ test('no_reference cannot conceal an invalid declared packageRoot', async () => {
145
+ writeManifest({ schemaVersion: 3, mode: 'project-sensors', pack: 'js-ts',
146
+ source: { registry: 'baseline' }, packageRoot: 'missing-package', sensors: {} });
147
+ configure(['baseline']);
148
+ writePack('baseline', 'js-ts', { name: 'js-ts', sensors: {} });
149
+ await expect((0, coverage_1.runCoverage)(project)).rejects.toThrow(/packageRoot/);
150
+ });
100
151
  test('normal regular coverage inputs resolve regardless of no-follow availability', () => {
101
152
  writeManifest({ pack: 'js-ts', sensors: {} });
102
153
  configure(['baseline']);
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -6,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
39
  const fs_1 = __importDefault(require("fs"));
7
40
  const os_1 = __importDefault(require("os"));
8
41
  const path_1 = __importDefault(require("path"));
42
+ const platformPaths = __importStar(require("../../../src/core/paths"));
9
43
  const exec_1 = require("../../../src/commands/sensors/exec");
10
44
  const result_1 = require("../../../src/commands/sensors/result");
11
45
  const baseline_1 = require("../../../src/commands/sensors/baseline");
@@ -34,6 +68,28 @@ async function until(fn, budgetMs = 4000) {
34
68
  return fn();
35
69
  }
36
70
  describe('runCommand — exit codes and output', () => {
71
+ itPosix('prefers the CLI runtime Node directory over an inherited conflicting PATH entry', async () => {
72
+ const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-exec-runtime-path-'));
73
+ const shadowBin = path_1.default.join(dir, 'bin');
74
+ const runtimeBin = path_1.default.join(dir, 'runtime-bin');
75
+ const originalPath = process.env.PATH;
76
+ const execPathDescriptor = Object.getOwnPropertyDescriptor(process, 'execPath');
77
+ try {
78
+ fs_1.default.mkdirSync(shadowBin);
79
+ fs_1.default.mkdirSync(runtimeBin);
80
+ fs_1.default.writeFileSync(path_1.default.join(shadowBin, 'node'), "#!/bin/sh\nprintf '%s\\n' shadow-node\n", { mode: 0o755 });
81
+ fs_1.default.writeFileSync(path_1.default.join(runtimeBin, 'node'), "#!/bin/sh\nprintf '%s\\n' runtime-node\n", { mode: 0o755 });
82
+ process.env.PATH = `${shadowBin}${path_1.default.delimiter}${originalPath ?? ''}`;
83
+ Object.defineProperty(process, 'execPath', { value: path_1.default.join(runtimeBin, 'node'), configurable: true });
84
+ const result = await (0, exec_1.runCommand)("node -e \"process.stdout.write(process.execPath)\"", { timeout: 5_000, cwd: dir });
85
+ expect(result).toMatchObject({ code: 0, stdout: 'runtime-node\n' });
86
+ }
87
+ finally {
88
+ process.env.PATH = originalPath;
89
+ Object.defineProperty(process, 'execPath', execPathDescriptor);
90
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
91
+ }
92
+ });
37
93
  it('records bounded execution evidence on timeout (R3.2,R3.4,R7.1)', async () => {
38
94
  const result = await (0, result_1.executePrepared)(sensor({ timeoutMs: 25, timeoutSource: 'project' }));
39
95
  expect(result.status).toBe('inconclusive');
@@ -81,6 +137,18 @@ describe('runCommand — exit codes and output', () => {
81
137
  const interpreted = (0, result_1.interpretResult)(sensor({ command: { kind: 'structured', value: { executable: 'node', resolution: 'path', args: ['-e', 'process.exit(3)'] } } }), raw);
82
138
  expect(interpreted.status).not.toBe('pass');
83
139
  });
140
+ it('treats generic stdout from a successful command as informational', () => {
141
+ const result = (0, result_1.interpretResult)(sensor(), {
142
+ code: 0,
143
+ stdout: 'depcruise: no violations found\n',
144
+ stderr: '',
145
+ elapsedMs: 1,
146
+ signal: null,
147
+ timedOut: false,
148
+ overflowed: false,
149
+ });
150
+ expect(result).toMatchObject({ status: 'pass', errors: [] });
151
+ });
84
152
  itLinux('keeps structured stderr separate from exit-0 stdout findings', async () => {
85
153
  const command = {
86
154
  executable: 'perl',
@@ -307,6 +375,50 @@ onPosix('runStructuredCommand — local node_modules binaries', () => {
307
375
  }
308
376
  });
309
377
  });
378
+ describe('runStructuredCommand — Windows local native binaries', () => {
379
+ let dir;
380
+ let windows;
381
+ let savedPathExt;
382
+ beforeEach(() => {
383
+ dir = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-windows-local-bin-')));
384
+ fs_1.default.mkdirSync(path_1.default.join(dir, 'node_modules', '.bin'), { recursive: true });
385
+ windows = jest.spyOn(platformPaths, 'isWindowsNative').mockReturnValue(true);
386
+ savedPathExt = process.env.PATHEXT;
387
+ process.env.PATHEXT = '.EXE;.CMD';
388
+ });
389
+ afterEach(() => {
390
+ windows.mockRestore();
391
+ if (savedPathExt === undefined)
392
+ delete process.env.PATHEXT;
393
+ else
394
+ process.env.PATHEXT = savedPathExt;
395
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
396
+ });
397
+ it('executes a contained native executable with literal shell-sensitive argv', async () => {
398
+ const executable = path_1.default.join(dir, 'node_modules', '.bin', 'fixture.exe');
399
+ fs_1.default.copyFileSync(process.execPath, executable);
400
+ fs_1.default.chmodSync(executable, 0o755);
401
+ const literal = '&echo injected|more';
402
+ await expect((0, exec_1.runStructuredCommand)({ executable: 'fixture', resolution: 'node-modules-bin',
403
+ args: ['-e', 'process.stdout.write(JSON.stringify(process.argv.slice(1)))', '--', literal] }, { cwd: dir, timeout: 5000 }))
404
+ .resolves.toMatchObject({ code: 0, stdout: JSON.stringify([literal]) });
405
+ });
406
+ it.each(['fixture', 'fixture.cmd'])('rejects non-native wrapper %s without executing it', executable => {
407
+ fs_1.default.writeFileSync(path_1.default.join(dir, 'node_modules', '.bin', executable), 'echo injected');
408
+ expect(() => (0, exec_1.runStructuredCommand)({ executable, resolution: 'node-modules-bin', args: ['--version'] }, { cwd: dir, timeout: 5000 }))
409
+ .toThrow(/contained local file|command wrappers/);
410
+ });
411
+ it('rejects a native executable escaping through a linked .bin ancestor', () => {
412
+ const outside = path_1.default.join(dir, 'outside-bin');
413
+ fs_1.default.mkdirSync(outside);
414
+ fs_1.default.copyFileSync(process.execPath, path_1.default.join(outside, 'fixture.exe'));
415
+ const bin = path_1.default.join(dir, 'node_modules', '.bin');
416
+ fs_1.default.rmdirSync(bin);
417
+ fs_1.default.symlinkSync(outside, bin, process.platform === 'win32' ? 'junction' : 'dir');
418
+ expect(() => (0, exec_1.runStructuredCommand)({ executable: 'fixture', resolution: 'node-modules-bin', args: ['--version'] }, { cwd: dir, timeout: 5000 }))
419
+ .toThrow('node_modules executable is not a contained local file');
420
+ });
421
+ });
310
422
  onPosix('runStructuredCommand — Python environments', () => {
311
423
  it.each(['.venv', 'venv'])('rejects a symlinked %s ancestor instead of executing outside the project', environment => {
312
424
  const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-python-env-ancestor-'));
@@ -136,6 +136,17 @@ describe('runSensors --changed', () => {
136
136
  expect(mockChangedFiles).not.toHaveBeenCalled();
137
137
  expect(out.changedScope).toBeUndefined();
138
138
  });
139
+ it('does not certify read-only admission when a sensor changes an ignored .awm file', async () => {
140
+ dir = project({ lint: LINT });
141
+ fs_1.default.writeFileSync(path_1.default.join(dir, '.gitignore'), '.awm/transient\\n');
142
+ mockRunCommand.mockImplementation(async () => {
143
+ fs_1.default.writeFileSync(path_1.default.join(dir, '.awm', 'transient'), 'sensor side effect');
144
+ return ok('');
145
+ });
146
+ const out = await load().runSensors({ cwd: dir, readOnly: true });
147
+ expect(out.overall).toBe('not_certified');
148
+ expect(out.reason).toBe('read-only-mutation-detected');
149
+ });
139
150
  it('passes the requested base through to the scope resolver', async () => {
140
151
  dir = project({ lint: LINT });
141
152
  mockChangedFiles.mockReturnValue({ files: ['src/a.ts'] });
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const fs_1 = __importDefault(require("fs"));
7
7
  const os_1 = __importDefault(require("os"));
8
8
  const path_1 = __importDefault(require("path"));
9
+ const child_process_1 = require("child_process");
9
10
  const run_1 = require("../../../src/commands/sensors/run");
10
11
  const status_1 = require("../../../src/commands/sensors/status");
11
12
  const verdict_1 = require("../../../src/commands/sensors/verdict");
@@ -83,6 +84,18 @@ describe('runSensors', () => {
83
84
  expect(result.sensors.some((s) => s.name === 'security')).toBe(false);
84
85
  expect(result.overall).toBe('not_certified');
85
86
  });
87
+ it('marks sensor evidence non-certifying when a read-only admission run changes the worktree', async () => {
88
+ (0, child_process_1.execFileSync)('git', ['init', '-q', '-b', 'main'], { cwd: tmpDir });
89
+ (0, child_process_1.execFileSync)('git', ['add', '.'], { cwd: tmpDir });
90
+ (0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', 'commit', '-qm', 'initial'], { cwd: tmpDir });
91
+ mockRunCommand.mockImplementation(async () => {
92
+ fs_1.default.writeFileSync(path.join(tmpDir, 'sensor-side-effect.txt'), 'unexpected');
93
+ return ok();
94
+ });
95
+ const { runSensors } = load();
96
+ const result = await runSensors({ fast: true, cwd: tmpDir, readOnly: true });
97
+ expect(result).toMatchObject({ overall: 'not_certified', reason: 'read-only-mutation-detected' });
98
+ });
86
99
  it('runs both fast and slow sensors when --fast and --slow are combined', async () => {
87
100
  mockRunCommand.mockResolvedValue(ok());
88
101
  const { runSensors } = load();
@@ -264,7 +277,7 @@ describe('runSensors v2 lifecycle contract', () => {
264
277
  sensors: { lint: { fast: true, applicability: { allFiles: ['package.json'] }, variants: [{
265
278
  id: 'eslint-10', priority: 1, certifiedRange: '>=10 <11',
266
279
  requirements: { tool: 'eslint', toolRange: '>=10 <11', runtime: 'node', runtimeRange: '>=0' },
267
- assets: ['eslint.config.awm.mjs'], formatter: 'generic', probe: { kind: 'package-script-present' },
280
+ assets: ['eslint.config.awm.mjs'], formatter: 'generic', probe: { kind: 'package-script-present', script: 'lint' },
268
281
  command: { executable: 'live-eslint', resolution: 'node-modules-bin', args: ['.'] },
269
282
  }] } },
270
283
  }));
@@ -470,7 +483,7 @@ describe('runSensors — not_certified + auto-discovery', () => {
470
483
  sensors: { lint: { applicability: { allFiles: ['package.json'] }, variants: [{
471
484
  id: 'eslint-10', priority: 1, certifiedRange: '>=10 <11',
472
485
  requirements: { tool: 'eslint', toolRange: '>=10 <11', runtime: 'node', runtimeRange: '>=0' },
473
- assets: [], formatter: 'generic', probe: { kind: 'package-script-present' },
486
+ assets: [], formatter: 'generic', probe: { kind: 'package-script-present', script: 'lint' },
474
487
  command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
475
488
  }] } },
476
489
  };