agentic-workflow-manager 9.7.1 → 9.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/src/commands/job/exec-wrapper.js +53 -3
- package/dist/src/commands/plan/index.js +124 -6
- package/dist/src/commands/sensors/bootstrap.js +36 -5
- package/dist/src/commands/sensors/compatibility/contract.js +19 -4
- package/dist/src/commands/sensors/compatibility/discovery.js +23 -3
- package/dist/src/commands/sensors/compatibility/live.js +18 -1
- package/dist/src/commands/sensors/compatibility/probe.js +8 -2
- package/dist/src/commands/sensors/compatibility/resolve.js +5 -2
- package/dist/src/commands/sensors/coverage/evidence.js +19 -10
- package/dist/src/commands/sensors/coverage/index.js +11 -2
- package/dist/src/commands/sensors/coverage/resolve.js +10 -0
- package/dist/src/commands/sensors/exec.js +16 -1
- package/dist/src/commands/sensors/migrate.js +62 -2
- package/dist/src/commands/sensors/prepare.js +2 -0
- package/dist/src/commands/sensors/result.js +2 -0
- package/dist/src/commands/sensors/run.js +64 -1
- package/dist/src/commands/sensors/status.js +6 -4
- package/dist/src/commands/watch/archive-unused.js +138 -0
- package/dist/src/commands/watch/generations.js +32 -9
- package/dist/src/commands/watch/index.js +108 -2
- package/dist/src/commands/watch/init.js +149 -18
- package/dist/src/commands/watch/supervisor.js +167 -13
- package/dist/src/commands/watch/teardown-driver.js +13 -13
- package/dist/src/commands/watch/track-state.js +59 -0
- package/dist/src/commands/watch/tracks.js +38 -102
- package/dist/src/core/admission/index.js +181 -0
- package/dist/src/core/admission/registry-contracts.js +290 -0
- package/dist/src/core/journal/fingerprint.js +35 -1
- package/dist/src/core/journal/paths.js +11 -0
- package/dist/src/core/journal/store.js +146 -4
- package/dist/src/core/journal/types.js +35 -1
- package/dist/src/core/migration/historical-provenance.js +18 -0
- package/dist/src/core/migration/index.js +322 -0
- package/dist/src/core/plan/identity.js +75 -0
- package/dist/src/core/plan/validate.js +178 -64
- package/dist/tests/commands/job/exec-wrapper.test.js +16 -0
- package/dist/tests/commands/job/gate-reconcile.test.js +24 -10
- package/dist/tests/commands/pin.test.js +23 -23
- package/dist/tests/commands/plan/index.test.js +630 -14
- package/dist/tests/commands/sensors/bootstrap.test.js +57 -4
- package/dist/tests/commands/sensors/compatibility/contract.test.js +21 -0
- package/dist/tests/commands/sensors/compatibility/discovery.test.js +19 -0
- package/dist/tests/commands/sensors/compatibility/probe.test.js +14 -1
- package/dist/tests/commands/sensors/compatibility/resolve.test.js +17 -0
- package/dist/tests/commands/sensors/coverage/evidence.test.js +51 -27
- package/dist/tests/commands/sensors/coverage/resolve.test.js +51 -0
- package/dist/tests/commands/sensors/exec.test.js +112 -0
- package/dist/tests/commands/sensors/run-changed.test.js +11 -0
- package/dist/tests/commands/sensors/run.test.js +15 -2
- package/dist/tests/commands/sensors/status.test.js +3 -3
- package/dist/tests/commands/watch/e2e-crash.test.js +126 -11
- package/dist/tests/commands/watch/generations.test.js +113 -4
- package/dist/tests/commands/watch/plan-lifecycle.test.js +263 -0
- package/dist/tests/commands/watch/supervisor-loop.test.js +247 -49
- package/dist/tests/commands/watch/track-finalize.test.js +20 -3
- package/dist/tests/commands/watch/track-freeze.test.js +43 -30
- package/dist/tests/commands/watch/track-state-dependency.test.js +15 -0
- package/dist/tests/commands/watch/watch-init.test.js +244 -0
- package/dist/tests/core/admission/index.test.js +87 -0
- package/dist/tests/core/journal/fingerprint.test.js +22 -0
- package/dist/tests/core/journal/store.test.js +53 -0
- package/dist/tests/core/journal/types.test.js +7 -0
- package/dist/tests/core/migration/facts.test.js +289 -0
- package/dist/tests/core/migration/historical-provenance.test.js +25 -0
- package/dist/tests/core/plan/validate.test.js +320 -6
- package/dist/tests/integration/pack-e2e.test.js +4 -1
- package/dist/tests/integration/preflight-json-pipe.e2e.test.js +7 -3
- package/dist/tests/integration/sensor-bootstrap.e2e.test.js +37 -0
- package/dist/tests/integration/sensor-structured-run.e2e.test.js +12 -10
- package/dist/tests/providers/injection-config.test.js +14 -6
- package/dist/tests/structural/jest-environment-is-isolated.test.js +29 -24
- package/dist/tests/structural/native-release-artifacts.test.js +16 -0
- package/package.json +2 -2
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- 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('
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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(
|
|
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(
|
|
97
|
-
|
|
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
|
|
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
|
-
|
|
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
|
};
|