agentic-workflow-manager 9.4.1 → 9.6.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 (57) hide show
  1. package/dist/src/commands/preflight/checks.js +58 -27
  2. package/dist/src/commands/sensors/bootstrap.js +171 -0
  3. package/dist/src/commands/sensors/compatibility/contract.js +18 -15
  4. package/dist/src/commands/sensors/compatibility/manifest.js +75 -2
  5. package/dist/src/commands/sensors/compatibility/materialize.js +256 -54
  6. package/dist/src/commands/sensors/compatibility/pack-source.js +61 -17
  7. package/dist/src/commands/sensors/compatibility/safe-file.js +286 -0
  8. package/dist/src/commands/sensors/compatibility/source.js +75 -0
  9. package/dist/src/commands/sensors/coverage/index.js +2 -2
  10. package/dist/src/commands/sensors/coverage/resolve.js +35 -29
  11. package/dist/src/commands/sensors/detection.js +31 -0
  12. package/dist/src/commands/sensors/exec.js +116 -11
  13. package/dist/src/commands/sensors/index.js +51 -10
  14. package/dist/src/commands/sensors/init.js +17 -168
  15. package/dist/src/commands/sensors/migrate.js +209 -0
  16. package/dist/src/commands/sensors/project.js +107 -0
  17. package/dist/src/commands/sensors/run.js +66 -47
  18. package/dist/src/commands/sensors/status.js +97 -56
  19. package/dist/src/core/init/steps.js +24 -11
  20. package/dist/src/core/secure-fs/native-bridge.js +209 -0
  21. package/dist/tests/commands/preflight/preflight.test.js +31 -0
  22. package/dist/tests/commands/sensors/bootstrap-observational.test.js +24 -0
  23. package/dist/tests/commands/sensors/bootstrap.test.js +175 -0
  24. package/dist/tests/commands/sensors/compatibility/contract.test.js +38 -0
  25. package/dist/tests/commands/sensors/compatibility/manifest.test.js +90 -0
  26. package/dist/tests/commands/sensors/compatibility/materialize.test.js +410 -0
  27. package/dist/tests/commands/sensors/compatibility/pack-source.test.js +249 -0
  28. package/dist/tests/commands/sensors/compatibility/safe-file.test.js +52 -0
  29. package/dist/tests/commands/sensors/compatibility/source.test.js +121 -0
  30. package/dist/tests/commands/sensors/coverage/index.test.js +20 -0
  31. package/dist/tests/commands/sensors/coverage/resolve.test.js +36 -34
  32. package/dist/tests/commands/sensors/exec.test.js +86 -0
  33. package/dist/tests/commands/sensors/index.test.js +54 -0
  34. package/dist/tests/commands/sensors/init-pack-unavailable.test.js +21 -84
  35. package/dist/tests/commands/sensors/init.test.js +56 -490
  36. package/dist/tests/commands/sensors/migrate.test.js +513 -0
  37. package/dist/tests/commands/sensors/project.test.js +102 -0
  38. package/dist/tests/commands/sensors/run.test.js +107 -4
  39. package/dist/tests/commands/sensors/status.test.js +106 -2
  40. package/dist/tests/core/init/orchestrator.test.js +3 -1
  41. package/dist/tests/core/secure-fs/native-bridge.test.js +733 -0
  42. package/dist/tests/integration/preflight-json-pipe.e2e.test.js +5 -0
  43. package/dist/tests/integration/sensor-bootstrap.e2e.test.js +165 -0
  44. package/dist/tests/integration/sensor-compatibility.e2e.test.js +14 -23
  45. package/dist/tests/integration/sensor-coverage.e2e.test.js +15 -24
  46. package/dist/tests/integration/sensor-structured-run.e2e.test.js +3 -1
  47. package/dist/tests/structural/native-release-artifacts.test.js +22 -0
  48. package/dist/tests/structural/release-flags-are-reachable-from-ci.test.js +10 -0
  49. package/dist/tests/structural/release-registry-provenance.test.js +4 -0
  50. package/dist/tests/structural/sensor-portability-contract.test.js +44 -0
  51. package/package.json +6 -2
  52. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  53. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  54. package/prebuilds/linux-arm64/secure_fs.node +0 -0
  55. package/prebuilds/linux-x64/secure_fs.node +0 -0
  56. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  57. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -0,0 +1,175 @@
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
+ jest.mock('../../../src/commands/sensors/project', () => ({ resolveSensorProject: jest.fn() }));
7
+ jest.mock('../../../src/commands/sensors/detection', () => ({ detectStack: jest.fn() }));
8
+ jest.mock('../../../src/core/registries', () => ({ listRegistries: jest.fn(() => []) }));
9
+ jest.mock('../../../src/commands/sensors/compatibility/pack-source', () => ({ listPackSources: jest.fn() }));
10
+ jest.mock('../../../src/commands/sensors/compatibility/contract', () => ({ ...jest.requireActual('../../../src/commands/sensors/compatibility/contract'), parseSensorPack: jest.fn() }));
11
+ jest.mock('../../../src/commands/sensors/compatibility/live', () => ({ resolveParsedPackCompatibility: jest.fn() }));
12
+ jest.mock('../../../src/commands/sensors/compatibility/source', () => ({ resolveSensorSource: jest.fn() }));
13
+ jest.mock('../../../src/commands/sensors/migrate', () => ({ planV2Migration: jest.fn(), replaceV2ManifestWithV3: jest.fn() }));
14
+ jest.mock('../../../src/commands/sensors/compatibility/materialize', () => ({ materializePortableSensors: jest.fn() }));
15
+ jest.mock('../../../src/commands/sensors/compatibility/safe-file', () => ({ writeProjectFile: jest.fn(), withProjectLease: jest.fn((_root, operation) => operation()) }));
16
+ const bootstrap_1 = require("../../../src/commands/sensors/bootstrap");
17
+ const project_1 = require("../../../src/commands/sensors/project");
18
+ const detection_1 = require("../../../src/commands/sensors/detection");
19
+ const pack_source_1 = require("../../../src/commands/sensors/compatibility/pack-source");
20
+ const contract_1 = require("../../../src/commands/sensors/compatibility/contract");
21
+ const live_1 = require("../../../src/commands/sensors/compatibility/live");
22
+ const source_1 = require("../../../src/commands/sensors/compatibility/source");
23
+ const migrate_1 = require("../../../src/commands/sensors/migrate");
24
+ const migrate_2 = require("../../../src/commands/sensors/migrate");
25
+ const materialize_1 = require("../../../src/commands/sensors/compatibility/materialize");
26
+ const safe_file_1 = require("../../../src/commands/sensors/compatibility/safe-file");
27
+ const fs_1 = __importDefault(require("fs"));
28
+ const os_1 = __importDefault(require("os"));
29
+ const path_1 = __importDefault(require("path"));
30
+ const crypto_1 = require("crypto");
31
+ const root = '/project';
32
+ const missing = { state: 'missing', projectRoot: root, manifestPath: `${root}/.awm/sensors.json` };
33
+ const source = { path: '/registry/sensor-packs/js-ts/pack.json', content: '{}', registry: { name: 'baseline', remote: 'local', contentRoot: '/registry' } };
34
+ describe('planSensorBootstrap', () => {
35
+ beforeEach(() => {
36
+ jest.clearAllMocks();
37
+ project_1.resolveSensorProject.mockReturnValue(missing);
38
+ detection_1.detectStack.mockReturnValue({ pack: 'js-ts', indicators: ['package.json'] });
39
+ });
40
+ it('requires an explicit mode for a missing declaration and never calls a writer', async () => {
41
+ await expect((0, bootstrap_1.planSensorBootstrap)(root)).resolves.toMatchObject({ kind: 'blocked', reason: 'mode-required', changes: [] });
42
+ expect(pack_source_1.listPackSources).not.toHaveBeenCalled();
43
+ });
44
+ it.each(['native-gate', 'opt-out'])('requires a reason for missing %s', async (mode) => {
45
+ await expect((0, bootstrap_1.planSensorBootstrap)(root, { mode })).resolves.toMatchObject({ kind: 'blocked', reason: 'reason-required', changes: [] });
46
+ });
47
+ it('plans a versioned native declaration without writing it', async () => {
48
+ await expect((0, bootstrap_1.planSensorBootstrap)(root, { mode: 'native-gate', reason: 'remote CI', dryRun: true })).resolves.toEqual({
49
+ kind: 'create', projectRoot: root, manifestPath: `${root}/.awm/sensors.json`, dryRun: true, configure: true,
50
+ changes: [{ path: '.awm/sensors.json', action: 'create' }],
51
+ manifest: { schemaVersion: 3, mode: 'native-gate', reason: 'remote CI' },
52
+ });
53
+ });
54
+ it('returns an idempotent no-op for an existing v3 declaration', async () => {
55
+ project_1.resolveSensorProject.mockReturnValue({ state: 'configured', projectRoot: root, manifestPath: `${root}/.awm/sensors.json`, packageRoot: root, manifest: { kind: 'v3', pack: { schemaVersion: 3, mode: 'opt-out', reason: 'declared' } } });
56
+ await expect((0, bootstrap_1.planSensorBootstrap)(root)).resolves.toMatchObject({ kind: 'noop', changes: [] });
57
+ });
58
+ it('rejects a mode that conflicts with an existing v3 declaration', async () => {
59
+ project_1.resolveSensorProject.mockReturnValue({ state: 'configured', projectRoot: root, manifestPath: `${root}/.awm/sensors.json`, packageRoot: root, manifest: { kind: 'v3', pack: { schemaVersion: 3, mode: 'opt-out', reason: 'declared' } } });
60
+ await expect((0, bootstrap_1.planSensorBootstrap)(root, { mode: 'native-gate', reason: 'other' })).resolves.toMatchObject({ kind: 'blocked', reason: 'mode-conflicts-with-existing-declaration', changes: [] });
61
+ });
62
+ it('blocks source-unavailable v2 migration without attempting a candidate', async () => {
63
+ const v2 = { schemaVersion: 2, pack: 'js-ts', sensors: {} };
64
+ project_1.resolveSensorProject.mockReturnValue({ state: 'configured', projectRoot: root, manifestPath: `${root}/.awm/sensors.json`, packageRoot: root, manifest: { kind: 'v2', pack: v2 } });
65
+ source_1.resolveSensorSource.mockReturnValue({ kind: 'source-unavailable', reason: 'no-compatible-registry', remedy: 'install-registry-or-run-awm-update' });
66
+ await expect((0, bootstrap_1.planSensorBootstrap)(root)).resolves.toMatchObject({ kind: 'blocked', reason: 'no-compatible-registry', changes: [] });
67
+ expect(migrate_1.planV2Migration).not.toHaveBeenCalled();
68
+ });
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: [] });
72
+ 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
+ source_1.resolveSensorSource.mockReturnValue({ kind: 'source-ambiguous', reason: 'multiple-compatible-registries', remedy: 'configure-one-logical-registry', candidates: ['a', 'b'] });
74
+ await expect((0, bootstrap_1.planSensorBootstrap)(root)).resolves.toMatchObject({ kind: 'blocked', reason: 'multiple-compatible-registries', changes: [] });
75
+ expect(migrate_1.planV2Migration).not.toHaveBeenCalled();
76
+ });
77
+ it('plans a v2 replacement only after semantic migration proves equivalence', async () => {
78
+ const v2 = { schemaVersion: 2, pack: 'js-ts', sensors: {} };
79
+ const resolution = { kind: 'logical', source };
80
+ const migration = { candidate: { schemaVersion: 3, mode: 'project-sensors', pack: 'js-ts', source: { registry: 'baseline' }, sensors: {} }, equivalent: true, equivalence: {} };
81
+ const project = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-bootstrap-plan-'));
82
+ const manifestPath = path_1.default.join(project, 'sensors.json');
83
+ try {
84
+ fs_1.default.writeFileSync(manifestPath, JSON.stringify(v2));
85
+ project_1.resolveSensorProject.mockReturnValue({ state: 'configured', projectRoot: project, manifestPath, packageRoot: project, manifest: { kind: 'v2', pack: v2 } });
86
+ source_1.resolveSensorSource.mockReturnValue(resolution);
87
+ migrate_1.planV2Migration.mockReturnValue(migration);
88
+ await expect((0, bootstrap_1.planSensorBootstrap)(project)).resolves.toMatchObject({ kind: 'migrate', changes: [{ path: '.awm/sensors.json', action: 'replace' }], migration });
89
+ }
90
+ finally {
91
+ fs_1.default.rmSync(project, { recursive: true, force: true });
92
+ }
93
+ });
94
+ it('plans a concrete portable project-sensors declaration from one logical source', async () => {
95
+ const variant = { id: 'eslint-9', command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] }, assets: ['eslint.config.mjs'] };
96
+ pack_source_1.listPackSources.mockReturnValue([source]);
97
+ contract_1.parseSensorPack.mockReturnValue({ kind: 'v2', pack: { schemaVersion: 2, name: 'js-ts', sensors: { lint: { fast: true, variants: [variant] } } } });
98
+ live_1.resolveParsedPackCompatibility.mockResolvedValue({ pack: { schemaVersion: 2, name: 'js-ts', sensors: { lint: { fast: true, variants: [variant] } } }, sensors: { lint: { state: 'certified', reason: 'ok', variantId: 'eslint-9', toolVersion: '9.0.0', runtimeVersion: '24.0.0', certifiedRange: '>=9 <10', evidence: [] } } });
99
+ const plan = await (0, bootstrap_1.planSensorBootstrap)(root, { mode: 'project-sensors' });
100
+ expect(plan).toMatchObject({ kind: 'create', changes: [{ path: '.awm/sensors.json', action: 'create' }, { path: 'eslint.config.mjs', action: 'create' }], manifest: { schemaVersion: 3, mode: 'project-sensors', pack: 'js-ts', source: { registry: 'baseline' } } });
101
+ expect(JSON.stringify(plan.manifest)).not.toContain('/registry');
102
+ });
103
+ it('resolves compatibility from the selected package root', async () => {
104
+ const variant = { id: 'eslint-9', command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] }, assets: [] };
105
+ const pack = { schemaVersion: 2, name: 'js-ts', sensors: { lint: { variants: [variant] } } };
106
+ pack_source_1.listPackSources.mockReturnValue([source]);
107
+ contract_1.parseSensorPack.mockReturnValue({ kind: 'v2', pack });
108
+ 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: [] } } });
109
+ await (0, bootstrap_1.planSensorBootstrap)(root, { mode: 'project-sensors', packageRoot: 'packages/web' });
110
+ expect(live_1.resolveParsedPackCompatibility).toHaveBeenCalledWith(path_1.default.resolve(root, 'packages/web'), pack);
111
+ });
112
+ it('blocks project-sensors when compatibility cannot select every declared sensor variant', async () => {
113
+ const variant = { id: 'eslint-9', command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] }, assets: [] };
114
+ pack_source_1.listPackSources.mockReturnValue([source]);
115
+ contract_1.parseSensorPack.mockReturnValue({ kind: 'v2', pack: { schemaVersion: 2, name: 'js-ts', sensors: { lint: { variants: [variant] } } } });
116
+ live_1.resolveParsedPackCompatibility.mockResolvedValue({ pack: { schemaVersion: 2, name: 'js-ts', sensors: { lint: { variants: [variant] } } }, sensors: { lint: { variantId: null } } });
117
+ await expect((0, bootstrap_1.planSensorBootstrap)(root, { mode: 'project-sensors' })).resolves.toMatchObject({ kind: 'blocked', reason: 'sensor-variant-unresolvable', changes: [] });
118
+ });
119
+ it('converts a compatibility probe failure into a stable blocked plan', async () => {
120
+ pack_source_1.listPackSources.mockReturnValue([source]);
121
+ contract_1.parseSensorPack.mockReturnValue({ kind: 'v2', pack: { schemaVersion: 2, name: 'js-ts', sensors: {} } });
122
+ live_1.resolveParsedPackCompatibility.mockRejectedValue(new Error('tool probe failed'));
123
+ await expect((0, bootstrap_1.planSensorBootstrap)(root, { mode: 'project-sensors' })).resolves.toMatchObject({ kind: 'blocked', reason: 'compatibility-unresolvable', changes: [] });
124
+ });
125
+ it.each([{ mode: 'invalid' }, { reason: 'line\nbreak' }, { dryRun: 'yes' }, { unknown: true }])('rejects invalid bootstrap options %j before planning', async (input) => {
126
+ await expect((0, bootstrap_1.planSensorBootstrap)(root, input)).rejects.toThrow(/bootstrap (mode|reason|dryRun|options)/);
127
+ expect(pack_source_1.listPackSources).not.toHaveBeenCalled();
128
+ });
129
+ it('accepts a Windows absolute registry path without treating it as project state', async () => {
130
+ await expect((0, bootstrap_1.planSensorBootstrap)(root, { mode: 'project-sensors', registryRoot: 'C:\\awm\\registries\\baseline' })).resolves.toMatchObject({
131
+ kind: 'blocked', reason: 'registry-root-not-configured', changes: [],
132
+ });
133
+ });
134
+ it('rejects applying a dry-run plan before any publisher runs', () => {
135
+ expect(() => (0, bootstrap_1.applySensorBootstrap)({ kind: 'create', projectRoot: root, manifestPath: `${root}/.awm/sensors.json`, dryRun: true, changes: [], manifest: { schemaVersion: 3, mode: 'native-gate', reason: 'CI' } })).toThrow('dry-run');
136
+ expect(safe_file_1.writeProjectFile).not.toHaveBeenCalled();
137
+ });
138
+ it('publishes native declarations and project sensors through distinct native boundaries', () => {
139
+ expect((0, bootstrap_1.applySensorBootstrap)({ kind: 'create', projectRoot: root, manifestPath: `${root}/.awm/sensors.json`, dryRun: false, changes: [{ path: '.awm/sensors.json', action: 'create' }], manifest: { schemaVersion: 3, mode: 'native-gate', reason: 'CI' } })).toBe('created');
140
+ expect(safe_file_1.writeProjectFile).toHaveBeenCalledWith(root, '.awm/sensors.json', expect.any(Buffer), { mode: 'create', createParents: true });
141
+ expect(safe_file_1.withProjectLease).toHaveBeenCalledWith(root, expect.any(Function));
142
+ const manifest = { schemaVersion: 3, mode: 'project-sensors', pack: 'js-ts', source: { registry: 'baseline' }, sensors: {} };
143
+ expect((0, bootstrap_1.applySensorBootstrap)({ kind: 'create', projectRoot: root, manifestPath: `${root}/.awm/sensors.json`, dryRun: false, changes: [{ path: '.awm/sensors.json', action: 'create' }], manifest, source })).toBe('created');
144
+ expect(materialize_1.materializePortableSensors).toHaveBeenCalledWith(expect.objectContaining({ projectRoot: root, pack: 'js-ts', source, sensors: {} }));
145
+ });
146
+ it('delegates a validated migration to the fenced replacement', () => {
147
+ const candidate = { schemaVersion: 3, mode: 'project-sensors', pack: 'js-ts', source: { registry: 'baseline' }, sensors: {} };
148
+ const migration = { candidate, equivalent: true, equivalence: {} };
149
+ const project = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-bootstrap-plan-'));
150
+ const manifestPath = path_1.default.join(project, 'sensors.json');
151
+ try {
152
+ const bytes = Buffer.from('{"schemaVersion":2}\n');
153
+ fs_1.default.writeFileSync(manifestPath, bytes);
154
+ const plan = { kind: 'migrate', projectRoot: project, manifestPath, dryRun: false, changes: [{ path: '.awm/sensors.json', action: 'replace' }], migration, source, originalDigest: (0, crypto_1.createHash)('sha256').update(bytes).digest('hex') };
155
+ expect((0, bootstrap_1.applySensorBootstrap)(plan)).toBe('migrated');
156
+ expect(migrate_2.replaceV2ManifestWithV3).toHaveBeenCalledWith(plan.manifestPath, candidate, source);
157
+ }
158
+ finally {
159
+ fs_1.default.rmSync(project, { recursive: true, force: true });
160
+ }
161
+ });
162
+ it('rejects a migration plan whose manifest changed after planning', () => {
163
+ const project = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-bootstrap-stale-'));
164
+ const manifestPath = path_1.default.join(project, 'sensors.json');
165
+ try {
166
+ fs_1.default.writeFileSync(manifestPath, 'replacement');
167
+ const plan = { kind: 'migrate', projectRoot: project, manifestPath, dryRun: false, changes: [{ path: '.awm/sensors.json', action: 'replace' }], migration: { candidate: {}, equivalent: true, equivalence: {} }, source, originalDigest: (0, crypto_1.createHash)('sha256').update('original').digest('hex') };
168
+ expect(() => (0, bootstrap_1.applySensorBootstrap)(plan)).toThrow('stale');
169
+ expect(migrate_2.replaceV2ManifestWithV3).not.toHaveBeenCalled();
170
+ }
171
+ finally {
172
+ fs_1.default.rmSync(project, { recursive: true, force: true });
173
+ }
174
+ });
175
+ });
@@ -8,6 +8,7 @@ const timeout_1 = require("../../../../src/commands/sensors/compatibility/timeou
8
8
  const fs_1 = __importDefault(require("fs"));
9
9
  const os_1 = __importDefault(require("os"));
10
10
  const path_1 = __importDefault(require("path"));
11
+ const safe_file_1 = require("../../../../src/commands/sensors/compatibility/safe-file");
11
12
  const coverage = {
12
13
  schemaVersion: 1,
13
14
  classes: {
@@ -44,6 +45,8 @@ function validPack() {
44
45
  };
45
46
  }
46
47
  describe('sensor pack v2 contract', () => {
48
+ beforeEach(() => (0, safe_file_1.setPortableReadForTests)(true));
49
+ afterEach(() => (0, safe_file_1.setPortableReadForTests)(false));
47
50
  it('exports bounded timeout validation and resolution (R3.1, R3.4)', () => {
48
51
  expect((0, timeout_1.positiveTimeout)(1, 'sensor timeout')).toBe(1);
49
52
  expect((0, timeout_1.resolveTimeout)({ project: 90_000, pack: 30_000, fast: true })).toEqual({ timeoutMs: 90_000, source: 'project' });
@@ -84,6 +87,41 @@ describe('sensor pack v2 contract', () => {
84
87
  fs_1.default.rmSync(sensorPacks, { recursive: true, force: true });
85
88
  }
86
89
  });
90
+ it('rejects a Semgrep policy that changes between inspection and safe open', () => {
91
+ const sensorPacks = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-semgrep-policy-'));
92
+ const packDir = path_1.default.join(sensorPacks, 'python');
93
+ const policyPath = path_1.default.join(sensorPacks, 'shared', 'semgrep-policy.json');
94
+ const originalOpen = fs_1.default.openSync.bind(fs_1.default);
95
+ const open = jest.spyOn(fs_1.default, 'openSync');
96
+ let replaced = false;
97
+ try {
98
+ fs_1.default.mkdirSync(path_1.default.dirname(policyPath), { recursive: true });
99
+ fs_1.default.mkdirSync(packDir);
100
+ fs_1.default.writeFileSync(policyPath, JSON.stringify({
101
+ tool: 'semgrep', toolRange: '>=1.0.0', runtime: 'python', runtimeRange: '>=3.9.0', probe: 'semgrep-validate',
102
+ }));
103
+ const pack = validPack();
104
+ pack.sensors.lint.variants[0] = {
105
+ id: 'semgrep-python', priority: 10, certifiedRange: '>=1.0.0', policyRef: 'shared/semgrep-policy.json',
106
+ command: { executable: 'semgrep', resolution: 'path', args: ['--config', '.semgrep.awm.yml', '--json', '.'] },
107
+ assets: ['.semgrep.awm.yml'], formatter: 'semgrep',
108
+ };
109
+ open.mockImplementation(((file, flags, mode) => {
110
+ if (file === policyPath && !replaced) {
111
+ replaced = true;
112
+ fs_1.default.writeFileSync(policyPath, JSON.stringify({
113
+ tool: 'semgrep', toolRange: '>=2.0.0', runtime: 'python', runtimeRange: '>=3.10.0', probe: 'semgrep-validate',
114
+ }));
115
+ }
116
+ return originalOpen(file, flags, mode);
117
+ }));
118
+ expect(() => (0, contract_1.parseSensorPack)(pack, path_1.default.join(packDir, 'pack.json'))).toThrow(/changed|identity|policyRef/i);
119
+ }
120
+ finally {
121
+ open.mockRestore();
122
+ fs_1.default.rmSync(sensorPacks, { recursive: true, force: true });
123
+ }
124
+ });
87
125
  it('fails closed when a policy reference is not the AWM-owned shared Semgrep policy', () => {
88
126
  const pack = validPack();
89
127
  pack.sensors.lint.variants[0] = {
@@ -38,6 +38,96 @@ describe('sensor manifest contract', () => {
38
38
  expect((0, manifest_1.parseSensorManifest)(manifest, 'sensors.json')).toMatchObject({ kind: 'v2', pack: manifest });
39
39
  expect(JSON.parse((0, manifest_1.serializeManifestV2)(manifest))).toEqual(manifest);
40
40
  });
41
+ it('parses the three exact v3 modes without physical registry provenance', () => {
42
+ const project = {
43
+ schemaVersion: 3,
44
+ mode: 'project-sensors',
45
+ pack: 'js-ts',
46
+ source: { registry: 'baseline' },
47
+ packageRoot: 'cli',
48
+ sensors: validV2Manifest().sensors,
49
+ };
50
+ expect((0, manifest_1.parseSensorManifest)(project, 'sensors.json')).toMatchObject({
51
+ kind: 'v3',
52
+ pack: { mode: 'project-sensors', source: { registry: 'baseline' }, packageRoot: 'cli' },
53
+ });
54
+ expect((0, manifest_1.parseSensorManifest)({ schemaVersion: 3, mode: 'native-gate', reason: 'managed by platform' }, 'sensors.json')).toMatchObject({
55
+ kind: 'v3', pack: { mode: 'native-gate', reason: 'managed by platform' },
56
+ });
57
+ expect((0, manifest_1.parseSensorManifest)({ schemaVersion: 3, mode: 'opt-out', reason: 'not applicable' }, 'sensors.json')).toMatchObject({
58
+ kind: 'v3', pack: { mode: 'opt-out', reason: 'not applicable' },
59
+ });
60
+ expect(() => (0, manifest_1.parseSensorManifest)({ ...project, source: { registry: 'Baseline' } }, 'sensors.json')).toThrow('registry');
61
+ expect(() => (0, manifest_1.parseSensorManifest)({ schemaVersion: 3, mode: 'native-gate' }, 'sensors.json')).toThrow('reason');
62
+ expect(() => (0, manifest_1.parseSensorManifest)({ ...project, registryRoot: '/machine/root' }, 'sensors.json')).toThrow('unknown field');
63
+ });
64
+ it('serializes v3 deterministically with a newline and round-trips portable semantics', () => {
65
+ const manifest = {
66
+ schemaVersion: 3,
67
+ mode: 'project-sensors',
68
+ pack: 'js-ts',
69
+ packSelection: 'explicit',
70
+ source: { registry: 'baseline' },
71
+ packageRoot: 'cli',
72
+ sensors: { lint: { ...validV2Manifest().sensors.lint, enabled: false, fast: true, timeout: 45_000, assets: ['eslint.config.awm.mjs'], policyRef: 'shared/semgrep-policy.json' } },
73
+ concurrency: 2,
74
+ };
75
+ const serialized = (0, manifest_1.serializeManifestV3)(manifest);
76
+ expect(serialized).toBe((0, manifest_1.serializeManifestV3)(manifest));
77
+ expect(serialized.endsWith('\n')).toBe(true);
78
+ expect(serialized).not.toContain('registryRoot');
79
+ expect(serialized).not.toContain('/home/');
80
+ expect((0, manifest_1.parseSensorManifest)(JSON.parse(serialized), 'sensors.json')).toEqual({ kind: 'v3', pack: manifest });
81
+ });
82
+ it.each(['native-gate', 'opt-out'])('serializes a versioned %s declaration canonically', mode => {
83
+ const manifest = { schemaVersion: 3, mode, reason: 'declared by project policy' };
84
+ expect((0, manifest_1.serializeManifestV3)(manifest)).toBe(JSON.stringify(manifest, null, 2) + '\n');
85
+ });
86
+ it('refuses to serialize a v3 declaration containing a nested physical path', () => {
87
+ const manifest = {
88
+ schemaVersion: 3,
89
+ mode: 'project-sensors',
90
+ pack: 'js-ts',
91
+ source: { registry: 'baseline' },
92
+ sensors: {
93
+ lint: {
94
+ ...validV2Manifest().sensors.lint,
95
+ command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['--config=/home/operator/.config/eslint.config.mjs'] },
96
+ },
97
+ },
98
+ };
99
+ expect(() => (0, manifest_1.serializeManifestV3)(manifest)).toThrow('physical path');
100
+ });
101
+ it('serializes a portable URL in a structured command argument', () => {
102
+ const manifest = {
103
+ schemaVersion: 3,
104
+ mode: 'project-sensors',
105
+ pack: 'js-ts',
106
+ source: { registry: 'baseline' },
107
+ sensors: {
108
+ lint: {
109
+ ...validV2Manifest().sensors.lint,
110
+ command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['--rules=https://example.invalid/rules.json'] },
111
+ },
112
+ },
113
+ };
114
+ expect(JSON.parse((0, manifest_1.serializeManifestV3)(manifest)).sensors.lint.command.args).toEqual(['--rules=https://example.invalid/rules.json']);
115
+ });
116
+ it('retains a portable URL in a structured command argument', () => {
117
+ const manifest = {
118
+ schemaVersion: 3,
119
+ mode: 'project-sensors',
120
+ pack: 'js-ts',
121
+ source: { registry: 'baseline' },
122
+ sensors: {
123
+ lint: {
124
+ ...validV2Manifest().sensors.lint,
125
+ command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['--rules-url=https://example.test/eslint/rules.json'] },
126
+ },
127
+ },
128
+ };
129
+ expect(() => (0, manifest_1.serializeManifestV3)(manifest)).not.toThrow();
130
+ });
41
131
  it('accepts and serializes a positive v2 project timeout (R3)', () => {
42
132
  const manifest = validV2Manifest();
43
133
  manifest.sensors.lint.timeout = 45_000;