agentic-workflow-manager 9.4.1 → 9.5.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/dist/src/commands/preflight/checks.js +56 -25
- package/dist/src/commands/sensors/compatibility/manifest.js +46 -1
- package/dist/src/commands/sensors/compatibility/pack-source.js +61 -17
- package/dist/src/commands/sensors/compatibility/safe-file.js +51 -0
- package/dist/src/commands/sensors/compatibility/source.js +75 -0
- package/dist/src/commands/sensors/coverage/resolve.js +17 -21
- package/dist/src/commands/sensors/init.js +3 -1
- package/dist/src/commands/sensors/project.js +107 -0
- package/dist/src/commands/sensors/run.js +64 -45
- package/dist/src/commands/sensors/status.js +97 -56
- package/dist/tests/commands/preflight/preflight.test.js +31 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +23 -0
- package/dist/tests/commands/sensors/compatibility/pack-source.test.js +216 -0
- package/dist/tests/commands/sensors/compatibility/source.test.js +121 -0
- package/dist/tests/commands/sensors/coverage/resolve.test.js +26 -34
- package/dist/tests/commands/sensors/project.test.js +102 -0
- package/dist/tests/commands/sensors/run.test.js +107 -4
- package/dist/tests/commands/sensors/status.test.js +106 -2
- package/dist/tests/integration/preflight-json-pipe.e2e.test.js +5 -0
- package/dist/tests/integration/sensor-compatibility.e2e.test.js +4 -21
- package/dist/tests/integration/sensor-coverage.e2e.test.js +15 -24
- package/package.json +1 -1
|
@@ -7,6 +7,36 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
7
7
|
const os_1 = __importDefault(require("os"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const pack_source_1 = require("../../../../src/commands/sensors/compatibility/pack-source");
|
|
10
|
+
function resolveWithoutNoFollow(...args) {
|
|
11
|
+
let resolve;
|
|
12
|
+
jest.isolateModules(() => {
|
|
13
|
+
jest.doMock('fs', () => {
|
|
14
|
+
const actual = jest.requireActual('fs');
|
|
15
|
+
const constants = { ...actual.constants, O_NOFOLLOW: undefined };
|
|
16
|
+
return { __esModule: true, default: { ...actual, constants }, ...actual, constants };
|
|
17
|
+
});
|
|
18
|
+
resolve = require('../../../../src/commands/sensors/compatibility/pack-source').resolvePackSource;
|
|
19
|
+
});
|
|
20
|
+
jest.dontMock('fs');
|
|
21
|
+
if (!resolve)
|
|
22
|
+
throw new Error('portable pack source resolver could not be loaded');
|
|
23
|
+
return resolve(...args);
|
|
24
|
+
}
|
|
25
|
+
function resolveWithoutNoFollowWithExactStats(...args) {
|
|
26
|
+
let resolve;
|
|
27
|
+
jest.isolateModules(() => {
|
|
28
|
+
jest.doMock('fs', () => {
|
|
29
|
+
const actual = jest.requireActual('fs');
|
|
30
|
+
const constants = { ...actual.constants, O_NOFOLLOW: undefined };
|
|
31
|
+
return { __esModule: true, default: { ...actual, constants }, ...actual, constants };
|
|
32
|
+
});
|
|
33
|
+
resolve = require('../../../../src/commands/sensors/compatibility/pack-source').resolvePackSource;
|
|
34
|
+
});
|
|
35
|
+
jest.dontMock('fs');
|
|
36
|
+
if (!resolve)
|
|
37
|
+
throw new Error('portable exact-stat pack source resolver could not be loaded');
|
|
38
|
+
return resolve(...args);
|
|
39
|
+
}
|
|
10
40
|
describe('resolvePackSource', () => {
|
|
11
41
|
it('uses the first registry containing the exact contained pack file', () => {
|
|
12
42
|
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-pack-source-'));
|
|
@@ -52,4 +82,190 @@ describe('resolvePackSource', () => {
|
|
|
52
82
|
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
53
83
|
}
|
|
54
84
|
});
|
|
85
|
+
it('rejects a symlinked registry content root', () => {
|
|
86
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-pack-source-root-link-'));
|
|
87
|
+
const target = path_1.default.join(root, 'target');
|
|
88
|
+
const linked = path_1.default.join(root, 'linked');
|
|
89
|
+
try {
|
|
90
|
+
fs_1.default.mkdirSync(path_1.default.join(target, 'sensor-packs', 'js-ts'), { recursive: true });
|
|
91
|
+
fs_1.default.writeFileSync(path_1.default.join(target, 'sensor-packs', 'js-ts', 'pack.json'), '{}');
|
|
92
|
+
fs_1.default.symlinkSync(target, linked, 'dir');
|
|
93
|
+
expect(() => (0, pack_source_1.resolvePackSource)('js-ts', { registries: [{ name: 'linked', remote: '', contentRoot: linked }] }))
|
|
94
|
+
.toThrow(/symbolic|symlink/i);
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
it('opens the resolved pack with no-follow semantics before reading it', () => {
|
|
101
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-pack-source-nofollow-'));
|
|
102
|
+
const open = jest.spyOn(fs_1.default, 'openSync');
|
|
103
|
+
try {
|
|
104
|
+
const packFile = path_1.default.join(root, 'sensor-packs', 'js-ts', 'pack.json');
|
|
105
|
+
fs_1.default.mkdirSync(path_1.default.dirname(packFile), { recursive: true });
|
|
106
|
+
fs_1.default.writeFileSync(packFile, '{}');
|
|
107
|
+
(0, pack_source_1.resolvePackSource)('js-ts', { registries: [{ name: 'safe', remote: '', contentRoot: root }] });
|
|
108
|
+
const flags = open.mock.calls[0]?.[1];
|
|
109
|
+
expect(typeof flags).toBe('number');
|
|
110
|
+
if (typeof fs_1.default.constants.O_NOFOLLOW === 'number') {
|
|
111
|
+
expect(flags & fs_1.default.constants.O_NOFOLLOW).toBe(fs_1.default.constants.O_NOFOLLOW);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
expect(flags).toBe(fs_1.default.constants.O_RDONLY);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
open.mockRestore();
|
|
119
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
it('resolves an inspected regular pack when no-follow is unavailable', () => {
|
|
123
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-pack-source-portable-open-'));
|
|
124
|
+
try {
|
|
125
|
+
const packFile = path_1.default.join(root, 'sensor-packs', 'js-ts', 'pack.json');
|
|
126
|
+
fs_1.default.mkdirSync(path_1.default.dirname(packFile), { recursive: true });
|
|
127
|
+
fs_1.default.writeFileSync(packFile, '{"from":"portable"}');
|
|
128
|
+
expect(resolveWithoutNoFollow('js-ts', { registries: [{ name: 'safe', remote: '', contentRoot: root }] }).content)
|
|
129
|
+
.toBe('{"from":"portable"}');
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
it('uses exact bigint file identities for the portable safe-open fallback', () => {
|
|
136
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-pack-source-bigint-'));
|
|
137
|
+
const lstat = jest.spyOn(fs_1.default, 'lstatSync');
|
|
138
|
+
const fstat = jest.spyOn(fs_1.default, 'fstatSync');
|
|
139
|
+
try {
|
|
140
|
+
const packFile = path_1.default.join(root, 'sensor-packs', 'js-ts', 'pack.json');
|
|
141
|
+
fs_1.default.mkdirSync(path_1.default.dirname(packFile), { recursive: true });
|
|
142
|
+
fs_1.default.writeFileSync(packFile, '{"from":"bigint"}');
|
|
143
|
+
expect(resolveWithoutNoFollowWithExactStats('js-ts', { registries: [{ name: 'safe', remote: '', contentRoot: root }] }).content)
|
|
144
|
+
.toBe('{"from":"bigint"}');
|
|
145
|
+
expect(lstat).toHaveBeenCalledWith(expect.any(String), { bigint: true });
|
|
146
|
+
expect(fstat).toHaveBeenCalledWith(expect.any(Number), { bigint: true });
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
lstat.mockRestore();
|
|
150
|
+
fstat.mockRestore();
|
|
151
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
it('rejects a parent-directory swap between inspection and no-follow open', () => {
|
|
155
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-pack-source-parent-swap-'));
|
|
156
|
+
const packDir = path_1.default.join(root, 'sensor-packs', 'js-ts');
|
|
157
|
+
const movedPackDir = path_1.default.join(root, 'sensor-packs', 'js-ts-original');
|
|
158
|
+
const packFile = path_1.default.join(packDir, 'pack.json');
|
|
159
|
+
const originalOpen = fs_1.default.openSync.bind(fs_1.default);
|
|
160
|
+
const open = jest.spyOn(fs_1.default, 'openSync');
|
|
161
|
+
let swapped = false;
|
|
162
|
+
try {
|
|
163
|
+
fs_1.default.mkdirSync(packDir, { recursive: true });
|
|
164
|
+
fs_1.default.writeFileSync(packFile, '{"from":"original"}');
|
|
165
|
+
open.mockImplementation(((file, flags, mode) => {
|
|
166
|
+
if (!swapped && file === fs_1.default.realpathSync(packFile)) {
|
|
167
|
+
swapped = true;
|
|
168
|
+
fs_1.default.renameSync(packDir, movedPackDir);
|
|
169
|
+
fs_1.default.mkdirSync(packDir, { recursive: true });
|
|
170
|
+
fs_1.default.writeFileSync(packFile, '{"from":"replacement"}');
|
|
171
|
+
}
|
|
172
|
+
return originalOpen(file, flags, mode);
|
|
173
|
+
}));
|
|
174
|
+
expect(() => (0, pack_source_1.resolvePackSource)('js-ts', { registries: [{ name: 'safe', remote: '', contentRoot: root }] }))
|
|
175
|
+
.toThrow(/changed|identity|regular|safe/i);
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
open.mockRestore();
|
|
179
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
it('accepts a parent symlink replacement when it resolves to the inspected pack inode', () => {
|
|
183
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-pack-source-same-inode-'));
|
|
184
|
+
const packDir = path_1.default.join(root, 'sensor-packs', 'js-ts');
|
|
185
|
+
const movedPackDir = path_1.default.join(root, 'sensor-packs', 'js-ts-original');
|
|
186
|
+
const packFile = path_1.default.join(packDir, 'pack.json');
|
|
187
|
+
const originalOpen = fs_1.default.openSync.bind(fs_1.default);
|
|
188
|
+
const open = jest.spyOn(fs_1.default, 'openSync');
|
|
189
|
+
let swapped = false;
|
|
190
|
+
try {
|
|
191
|
+
fs_1.default.mkdirSync(packDir, { recursive: true });
|
|
192
|
+
fs_1.default.writeFileSync(packFile, '{"from":"original"}');
|
|
193
|
+
open.mockImplementation(((file, flags, mode) => {
|
|
194
|
+
if (!swapped && file === fs_1.default.realpathSync(packFile)) {
|
|
195
|
+
swapped = true;
|
|
196
|
+
fs_1.default.renameSync(packDir, movedPackDir);
|
|
197
|
+
fs_1.default.symlinkSync(movedPackDir, packDir, 'dir');
|
|
198
|
+
}
|
|
199
|
+
return originalOpen(file, flags, mode);
|
|
200
|
+
}));
|
|
201
|
+
expect((0, pack_source_1.resolvePackSource)('js-ts', { registries: [{ name: 'safe', remote: '', contentRoot: root }] }).content)
|
|
202
|
+
.toBe('{"from":"original"}');
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
open.mockRestore();
|
|
206
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
it('rejects same-inode content growth between inspection and open', () => {
|
|
210
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-pack-source-size-race-'));
|
|
211
|
+
const packFile = path_1.default.join(root, 'sensor-packs', 'js-ts', 'pack.json');
|
|
212
|
+
const originalOpen = fs_1.default.openSync.bind(fs_1.default);
|
|
213
|
+
const open = jest.spyOn(fs_1.default, 'openSync');
|
|
214
|
+
let changed = false;
|
|
215
|
+
try {
|
|
216
|
+
fs_1.default.mkdirSync(path_1.default.dirname(packFile), { recursive: true });
|
|
217
|
+
fs_1.default.writeFileSync(packFile, '{"from":"original"}');
|
|
218
|
+
open.mockImplementation(((file, flags, mode) => {
|
|
219
|
+
if (!changed && file === fs_1.default.realpathSync(packFile)) {
|
|
220
|
+
changed = true;
|
|
221
|
+
fs_1.default.appendFileSync(packFile, 'x');
|
|
222
|
+
}
|
|
223
|
+
return originalOpen(file, flags, mode);
|
|
224
|
+
}));
|
|
225
|
+
expect(() => resolveWithoutNoFollow('js-ts', { registries: [{ name: 'safe', remote: '', contentRoot: root }] }))
|
|
226
|
+
.toThrow(/changed|size|identity/i);
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
open.mockRestore();
|
|
230
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
it('rejects same-inode truncation after safe open before reading', () => {
|
|
234
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-pack-source-truncate-race-'));
|
|
235
|
+
const packFile = path_1.default.join(root, 'sensor-packs', 'js-ts', 'pack.json');
|
|
236
|
+
const originalRead = fs_1.default.readSync.bind(fs_1.default);
|
|
237
|
+
const read = jest.spyOn(fs_1.default, 'readSync');
|
|
238
|
+
let truncated = false;
|
|
239
|
+
try {
|
|
240
|
+
fs_1.default.mkdirSync(path_1.default.dirname(packFile), { recursive: true });
|
|
241
|
+
fs_1.default.writeFileSync(packFile, '{"from":"original"}');
|
|
242
|
+
read.mockImplementation(((descriptor, buffer, offset, length, position) => {
|
|
243
|
+
if (!truncated) {
|
|
244
|
+
truncated = true;
|
|
245
|
+
fs_1.default.truncateSync(packFile, 0);
|
|
246
|
+
}
|
|
247
|
+
return originalRead(descriptor, buffer, offset, length, position);
|
|
248
|
+
}));
|
|
249
|
+
expect(() => resolveWithoutNoFollow('js-ts', { registries: [{ name: 'safe', remote: '', contentRoot: root }] }))
|
|
250
|
+
.toThrow(/changed|size|identity/i);
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
read.mockRestore();
|
|
254
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
it('bounds diagnostics when a long content root cannot be inspected', () => {
|
|
258
|
+
const contentRoot = path_1.default.join(os_1.default.tmpdir(), `awm-${'x'.repeat(200_000)}`);
|
|
259
|
+
let message = '';
|
|
260
|
+
try {
|
|
261
|
+
(0, pack_source_1.resolvePackSource)('js-ts', { registries: [{ name: 'safe', remote: 'https://user:secret@example.invalid/registry', contentRoot }] });
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
message = error instanceof Error ? error.message : String(error);
|
|
265
|
+
}
|
|
266
|
+
expect(message.length).toBeLessThan(4096);
|
|
267
|
+
expect(message).not.toContain(contentRoot);
|
|
268
|
+
expect(message).not.toContain('secret');
|
|
269
|
+
expect(message).toMatch(/inspect|source|registry/i);
|
|
270
|
+
});
|
|
55
271
|
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const manifest_1 = require("../../../../src/commands/sensors/compatibility/manifest");
|
|
10
|
+
const source_1 = require("../../../../src/commands/sensors/compatibility/source");
|
|
11
|
+
function manifest(registryRoot) {
|
|
12
|
+
return (0, manifest_1.parseSensorManifest)({
|
|
13
|
+
schemaVersion: 2, pack: 'js-ts', ...(registryRoot ? { registryRoot } : {}), sensors: {
|
|
14
|
+
lint: {
|
|
15
|
+
enabled: false, variantId: 'eslint-9',
|
|
16
|
+
command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
|
|
17
|
+
initializedCompatibility: { state: 'certified', reason: 'fixture', variantId: 'eslint-9', toolVersion: '9.0.0', runtimeVersion: '24.0.0', certifiedRange: '>=9 <10', evidence: [] },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
}, 'manifest');
|
|
21
|
+
}
|
|
22
|
+
function pack() {
|
|
23
|
+
return {
|
|
24
|
+
schemaVersion: 2, name: 'js-ts', description: 'fixture', detects: ['package.json'], sensors: {
|
|
25
|
+
lint: { applicability: { allFiles: ['package.json'] }, variants: [{
|
|
26
|
+
id: 'eslint-9', priority: 1,
|
|
27
|
+
requirements: { tool: 'eslint', toolRange: '>=9 <10', runtime: 'node', runtimeRange: '>=20' },
|
|
28
|
+
certifiedRange: '>=9 <10', assets: [], formatter: 'generic', probe: { kind: 'eslint-print-config' },
|
|
29
|
+
command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
|
|
30
|
+
}] },
|
|
31
|
+
},
|
|
32
|
+
coverage: { schemaVersion: 1, classes: { linting: { description: 'fixture', detectors: [{ sensor: 'lint' }], remedy: { summary: 'fixture', command: 'npm test' } } } },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function writePack(root) {
|
|
36
|
+
const directory = path_1.default.join(root, 'sensor-packs', 'js-ts');
|
|
37
|
+
fs_1.default.mkdirSync(directory, { recursive: true });
|
|
38
|
+
fs_1.default.writeFileSync(path_1.default.join(directory, 'pack.json'), JSON.stringify(pack()));
|
|
39
|
+
}
|
|
40
|
+
describe('resolveSensorSource', () => {
|
|
41
|
+
const roots = [];
|
|
42
|
+
afterEach(() => { for (const root of roots.splice(0))
|
|
43
|
+
fs_1.default.rmSync(root, { recursive: true, force: true }); });
|
|
44
|
+
it('preserves an existing v2 provenance path without registry-order rebinding', () => {
|
|
45
|
+
const bound = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-source-bound-'));
|
|
46
|
+
roots.push(bound);
|
|
47
|
+
const other = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-source-other-'));
|
|
48
|
+
roots.push(other);
|
|
49
|
+
writePack(bound);
|
|
50
|
+
writePack(other);
|
|
51
|
+
expect((0, source_1.resolveSensorSource)(manifest(bound), { registries: [{ name: 'other', remote: 'local', contentRoot: other }] }))
|
|
52
|
+
.toMatchObject({ kind: 'legacy-bound', source: { registry: { name: 'manifest-provenance' } } });
|
|
53
|
+
});
|
|
54
|
+
it('rebinds an absent v2 provenance only when exactly one registry matches every sensor identity', () => {
|
|
55
|
+
const absent = path_1.default.join(os_1.default.tmpdir(), 'does-not-exist-awma');
|
|
56
|
+
const registry = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-source-rebound-'));
|
|
57
|
+
roots.push(registry);
|
|
58
|
+
writePack(registry);
|
|
59
|
+
expect((0, source_1.resolveSensorSource)(manifest(absent), { registries: [{ name: 'baseline', remote: 'local', contentRoot: registry }] }))
|
|
60
|
+
.toMatchObject({ kind: 'legacy-rebound', source: { registry: { name: 'baseline' } } });
|
|
61
|
+
});
|
|
62
|
+
it('fails closed for zero or multiple structural candidates without exposing paths or remotes', () => {
|
|
63
|
+
const absent = path_1.default.join(os_1.default.tmpdir(), 'does-not-exist-awmb');
|
|
64
|
+
expect((0, source_1.resolveSensorSource)(manifest(absent), { registries: [] })).toMatchObject({ kind: 'source-unavailable', reason: 'no-compatible-registry' });
|
|
65
|
+
const first = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-source-first-'));
|
|
66
|
+
roots.push(first);
|
|
67
|
+
const second = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-source-second-'));
|
|
68
|
+
roots.push(second);
|
|
69
|
+
writePack(first);
|
|
70
|
+
writePack(second);
|
|
71
|
+
const result = (0, source_1.resolveSensorSource)(manifest(absent), { registries: [
|
|
72
|
+
{ name: 'first', remote: 'https://user:secret@example.invalid/first', contentRoot: first },
|
|
73
|
+
{ name: 'second', remote: 'https://user:secret@example.invalid/second', contentRoot: second },
|
|
74
|
+
] });
|
|
75
|
+
expect(result).toMatchObject({ kind: 'source-ambiguous', candidates: ['first', 'second'] });
|
|
76
|
+
expect(JSON.stringify(result)).not.toContain('secret');
|
|
77
|
+
});
|
|
78
|
+
it('rejects a malformed v3 logical registry identity instead of classifying it as unavailable', () => {
|
|
79
|
+
expect(() => (0, source_1.resolveSensorSource)({
|
|
80
|
+
kind: 'v3', pack: { mode: 'project-sensors', pack: 'js-ts', source: { registry: '../escape' } },
|
|
81
|
+
}, { registries: [] })).toThrow(/registry.*stable|invalid.*registry/i);
|
|
82
|
+
});
|
|
83
|
+
it('bounds ambiguity diagnostics for extremely long valid registry names', () => {
|
|
84
|
+
const first = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-source-long-first-'));
|
|
85
|
+
roots.push(first);
|
|
86
|
+
const second = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-source-long-second-'));
|
|
87
|
+
roots.push(second);
|
|
88
|
+
writePack(first);
|
|
89
|
+
writePack(second);
|
|
90
|
+
const result = (0, source_1.resolveSensorSource)(manifest(path_1.default.join(os_1.default.tmpdir(), 'awm-source-long-missing')), { registries: [
|
|
91
|
+
{ name: `a${'x'.repeat(200_000)}`, remote: 'https://user:secret@example.invalid/first', contentRoot: first },
|
|
92
|
+
{ name: `b${'x'.repeat(200_000)}`, remote: 'https://user:secret@example.invalid/second', contentRoot: second },
|
|
93
|
+
] });
|
|
94
|
+
expect(result.kind).toBe('source-ambiguous');
|
|
95
|
+
if (result.kind === 'source-ambiguous') {
|
|
96
|
+
expect(result.candidates.every(candidate => candidate.length <= 128)).toBe(true);
|
|
97
|
+
}
|
|
98
|
+
expect(JSON.stringify(result).length).toBeLessThan(4096);
|
|
99
|
+
expect(JSON.stringify(result)).not.toContain('secret');
|
|
100
|
+
});
|
|
101
|
+
it('bounds malformed-pack diagnostics for an extremely long valid registry name', () => {
|
|
102
|
+
const registryRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-source-malformed-long-'));
|
|
103
|
+
roots.push(registryRoot);
|
|
104
|
+
writePack(registryRoot);
|
|
105
|
+
fs_1.default.writeFileSync(path_1.default.join(registryRoot, 'sensor-packs', 'js-ts', 'pack.json'), '{"schemaVersion":');
|
|
106
|
+
const name = `a${'x'.repeat(200_000)}`;
|
|
107
|
+
let message = '';
|
|
108
|
+
try {
|
|
109
|
+
(0, source_1.resolveSensorSource)(manifest(path_1.default.join(os_1.default.tmpdir(), 'awm-source-malformed-long-missing')), {
|
|
110
|
+
registries: [{ name, remote: 'https://user:secret@example.invalid/registry', contentRoot: registryRoot }],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
message = error instanceof Error ? error.message : String(error);
|
|
115
|
+
}
|
|
116
|
+
expect(message).toMatch(/invalid sensor pack/);
|
|
117
|
+
expect(message.length).toBeLessThan(4096);
|
|
118
|
+
expect(message).not.toContain(name);
|
|
119
|
+
expect(message).not.toContain('secret');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -7,6 +7,21 @@ 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
|
+
function resolveCoverageWithoutNoFollow(cwd) {
|
|
11
|
+
let resolve;
|
|
12
|
+
jest.isolateModules(() => {
|
|
13
|
+
jest.doMock('fs', () => {
|
|
14
|
+
const actual = jest.requireActual('fs');
|
|
15
|
+
const constants = { ...actual.constants, O_NOFOLLOW: undefined };
|
|
16
|
+
return { __esModule: true, default: { ...actual, constants }, ...actual, constants };
|
|
17
|
+
});
|
|
18
|
+
resolve = require('../../../../src/commands/sensors/coverage/resolve').resolveCoverageInputs;
|
|
19
|
+
});
|
|
20
|
+
jest.dontMock('fs');
|
|
21
|
+
if (!resolve)
|
|
22
|
+
throw new Error('portable coverage resolver could not be loaded');
|
|
23
|
+
return resolve(cwd);
|
|
24
|
+
}
|
|
10
25
|
let root;
|
|
11
26
|
let awmHome;
|
|
12
27
|
let project;
|
|
@@ -29,9 +44,6 @@ const writeManifest = (body) => fs_1.default.writeFileSync(path_1.default.join(p
|
|
|
29
44
|
const coverage = { schemaVersion: 1, classes: {
|
|
30
45
|
formatting: { description: 'Formatting', detectors: [{ sensor: 'format' }], remedy: { summary: 'Add formatter', command: 'npm i -D prettier' } },
|
|
31
46
|
} };
|
|
32
|
-
const noFollowUnavailableOnNativeWindows = process.platform === 'win32'
|
|
33
|
-
&& typeof fs_1.default.constants.O_NOFOLLOW !== 'number';
|
|
34
|
-
const safetyError = /platform cannot guarantee no symlink dereference/;
|
|
35
47
|
const writePack = (registry, pack, body) => {
|
|
36
48
|
const dir = path_1.default.join(awmHome, 'registries', registry, 'sensor-packs', pack);
|
|
37
49
|
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
@@ -60,62 +72,42 @@ test('selects the first configured registry containing the exact pack', () => {
|
|
|
60
72
|
configure(['first', 'second']);
|
|
61
73
|
writePack('first', 'generic', { name: 'generic', sensors: {} });
|
|
62
74
|
writePack('second', 'js-ts', { name: 'js-ts', sensors: {}, coverage });
|
|
63
|
-
|
|
64
|
-
expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(safetyError);
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
expect((0, resolve_1.resolveCoverageInputs)(project)).toMatchObject({ kind: 'ready', pack: 'js-ts', registry: 'second' });
|
|
68
|
-
}
|
|
75
|
+
expect((0, resolve_1.resolveCoverageInputs)(project)).toMatchObject({ kind: 'ready', pack: 'js-ts', registry: 'second' });
|
|
69
76
|
});
|
|
70
77
|
test('registry ordering chooses the earlier exact pack when both registries contain it', () => {
|
|
71
78
|
writeManifest({ pack: 'js-ts', sensors: {} });
|
|
72
79
|
configure(['first', 'second']);
|
|
73
80
|
writePack('first', 'js-ts', { name: 'js-ts', sensors: {}, coverage });
|
|
74
81
|
writePack('second', 'js-ts', { name: 'js-ts', sensors: {}, coverage });
|
|
75
|
-
|
|
76
|
-
expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(safetyError);
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
expect((0, resolve_1.resolveCoverageInputs)(project)).toMatchObject({ kind: 'ready', registry: 'first' });
|
|
80
|
-
}
|
|
82
|
+
expect((0, resolve_1.resolveCoverageInputs)(project)).toMatchObject({ kind: 'ready', registry: 'first' });
|
|
81
83
|
});
|
|
82
84
|
test('old pack without coverage is no_reference, not covered', () => {
|
|
83
85
|
writeManifest({ pack: 'js-ts', sensors: {} });
|
|
84
86
|
configure(['baseline']);
|
|
85
87
|
writePack('baseline', 'js-ts', { name: 'js-ts', sensors: {} });
|
|
86
|
-
|
|
87
|
-
expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(safetyError);
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
expect((0, resolve_1.resolveCoverageInputs)(project)).toMatchObject({ kind: 'no_reference', pack: 'js-ts', registry: 'baseline' });
|
|
91
|
-
}
|
|
88
|
+
expect((0, resolve_1.resolveCoverageInputs)(project)).toMatchObject({ kind: 'no_reference', pack: 'js-ts', registry: 'baseline' });
|
|
92
89
|
});
|
|
93
|
-
test('
|
|
90
|
+
test('normal regular coverage inputs resolve regardless of no-follow availability', () => {
|
|
94
91
|
writeManifest({ pack: 'js-ts', sensors: {} });
|
|
95
92
|
configure(['baseline']);
|
|
96
93
|
writePack('baseline', 'js-ts', { name: 'js-ts', sensors: {}, coverage });
|
|
97
|
-
|
|
98
|
-
expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(safetyError);
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
expect((0, resolve_1.resolveCoverageInputs)(project)).toMatchObject({ kind: 'ready' });
|
|
102
|
-
}
|
|
94
|
+
expect(resolveCoverageWithoutNoFollow(project)).toMatchObject({ kind: 'ready' });
|
|
103
95
|
});
|
|
104
96
|
test.each([
|
|
105
97
|
['manifest-malformed', '{broken', /Invalid JSON.*sensors\.json/],
|
|
106
98
|
['manifest-oversize', Buffer.alloc(1024 * 1024 + 1), /sensors\.json.*exceeds 1 MiB/],
|
|
107
99
|
])('rejects %s', (_name, body, expected) => {
|
|
108
100
|
writeManifest(body);
|
|
109
|
-
expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(
|
|
101
|
+
expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(expected);
|
|
110
102
|
});
|
|
111
103
|
test.each([
|
|
112
104
|
['pack-malformed', '{broken', /Invalid JSON.*pack\.json/],
|
|
113
|
-
['pack-oversize', Buffer.alloc(1024 * 1024 + 1), /pack
|
|
105
|
+
['pack-oversize', Buffer.alloc(1024 * 1024 + 1), /pack source exceeds the 1 MiB limit/],
|
|
114
106
|
])('rejects %s', (_name, body, expected) => {
|
|
115
107
|
writeManifest({ pack: 'js-ts', sensors: {} });
|
|
116
108
|
configure(['baseline']);
|
|
117
109
|
writePack('baseline', 'js-ts', body);
|
|
118
|
-
expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(
|
|
110
|
+
expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(expected);
|
|
119
111
|
});
|
|
120
112
|
test('rejects a symlinked manifest without dereferencing it', () => {
|
|
121
113
|
const manifest = path_1.default.join(project, '.awm', 'sensors.json');
|
|
@@ -133,12 +125,12 @@ test('rejects a JSON object that is not a valid pack', () => {
|
|
|
133
125
|
writeManifest({ pack: 'js-ts', sensors: {} });
|
|
134
126
|
configure(['baseline']);
|
|
135
127
|
writePack('baseline', 'js-ts', { coverage });
|
|
136
|
-
expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(
|
|
128
|
+
expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(/Invalid sensor pack.*name/);
|
|
137
129
|
});
|
|
138
130
|
test('rejects registry names that are not safe path components', () => {
|
|
139
131
|
writeManifest({ pack: 'js-ts', sensors: {} });
|
|
140
132
|
configure(['']);
|
|
141
|
-
expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(
|
|
133
|
+
expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(/malformed entry name|Invalid registry name/);
|
|
142
134
|
});
|
|
143
135
|
test.each([
|
|
144
136
|
[{ schemaVersion: 3, name: 'js-ts', sensors: {}, coverage: {} }, /supported: legacy, 2/],
|
|
@@ -147,5 +139,5 @@ test.each([
|
|
|
147
139
|
writeManifest({ pack: 'js-ts', sensors: {} });
|
|
148
140
|
configure(['baseline']);
|
|
149
141
|
writePack('baseline', 'js-ts', pack);
|
|
150
|
-
expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(
|
|
142
|
+
expect(() => (0, resolve_1.resolveCoverageInputs)(project)).toThrow(expected);
|
|
151
143
|
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const projectModule = '../../../src/commands/sensors/project';
|
|
10
|
+
function tempDir() {
|
|
11
|
+
return fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-sensor-project-'));
|
|
12
|
+
}
|
|
13
|
+
function writeJson(file, value) {
|
|
14
|
+
fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
|
|
15
|
+
fs_1.default.writeFileSync(file, JSON.stringify(value));
|
|
16
|
+
}
|
|
17
|
+
function v3Manifest(packageRoot) {
|
|
18
|
+
return {
|
|
19
|
+
schemaVersion: 3,
|
|
20
|
+
mode: 'project-sensors',
|
|
21
|
+
pack: 'js-ts',
|
|
22
|
+
source: { registry: 'baseline' },
|
|
23
|
+
...(packageRoot ? { packageRoot } : {}),
|
|
24
|
+
sensors: {},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
describe('sensor project authority', () => {
|
|
28
|
+
const roots = [];
|
|
29
|
+
afterEach(() => { for (const root of roots.splice(0))
|
|
30
|
+
fs_1.default.rmSync(root, { recursive: true, force: true }); });
|
|
31
|
+
function resolve(startCwd) {
|
|
32
|
+
return require(projectModule).resolveSensorProject(startCwd);
|
|
33
|
+
}
|
|
34
|
+
it('selects the nearest regular manifest without crossing the Git boundary', () => {
|
|
35
|
+
const root = tempDir();
|
|
36
|
+
roots.push(root);
|
|
37
|
+
fs_1.default.mkdirSync(path_1.default.join(root, '.git'));
|
|
38
|
+
writeJson(path_1.default.join(root, '.awm/sensors.json'), v3Manifest());
|
|
39
|
+
writeJson(path_1.default.join(root, 'packages/app/.awm/sensors.json'), v3Manifest());
|
|
40
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'packages/app/src'), { recursive: true });
|
|
41
|
+
const result = resolve(path_1.default.join(root, 'packages/app/src'));
|
|
42
|
+
expect(result).toMatchObject({ state: 'configured', projectRoot: path_1.default.join(root, 'packages/app'), manifestPath: path_1.default.join(root, 'packages/app/.awm/sensors.json') });
|
|
43
|
+
});
|
|
44
|
+
it('treats a .git file as a worktree boundary and rejects an escaping packageRoot', () => {
|
|
45
|
+
const root = tempDir();
|
|
46
|
+
roots.push(root);
|
|
47
|
+
fs_1.default.writeFileSync(path_1.default.join(root, '.git'), 'gitdir: /elsewhere');
|
|
48
|
+
writeJson(path_1.default.join(root, '.awm/sensors.json'), v3Manifest('../outside'));
|
|
49
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'cli/src'), { recursive: true });
|
|
50
|
+
expect(resolve(path_1.default.join(root, 'cli/src'))).toMatchObject({ state: 'invalid', projectRoot: root });
|
|
51
|
+
});
|
|
52
|
+
it('inspects only the exact CWD outside Git and returns missing for absent or malformed manifests', () => {
|
|
53
|
+
const parent = tempDir();
|
|
54
|
+
roots.push(parent);
|
|
55
|
+
writeJson(path_1.default.join(parent, '.awm/sensors.json'), v3Manifest());
|
|
56
|
+
const cwd = path_1.default.join(parent, 'child');
|
|
57
|
+
fs_1.default.mkdirSync(cwd);
|
|
58
|
+
expect(resolve(cwd)).toMatchObject({ state: 'missing', projectRoot: cwd });
|
|
59
|
+
fs_1.default.mkdirSync(path_1.default.join(cwd, '.awm'));
|
|
60
|
+
fs_1.default.writeFileSync(path_1.default.join(cwd, '.awm/sensors.json'), '{bad json');
|
|
61
|
+
expect(resolve(cwd)).toMatchObject({ state: 'invalid', projectRoot: cwd });
|
|
62
|
+
});
|
|
63
|
+
it('rejects a symlinked manifest and a packageRoot that is not a directory', () => {
|
|
64
|
+
const root = tempDir();
|
|
65
|
+
roots.push(root);
|
|
66
|
+
fs_1.default.mkdirSync(path_1.default.join(root, '.git'));
|
|
67
|
+
fs_1.default.mkdirSync(path_1.default.join(root, '.awm'), { recursive: true });
|
|
68
|
+
writeJson(path_1.default.join(root, 'elsewhere.json'), v3Manifest());
|
|
69
|
+
fs_1.default.symlinkSync(path_1.default.join(root, 'elsewhere.json'), path_1.default.join(root, '.awm/sensors.json'));
|
|
70
|
+
expect(resolve(root)).toMatchObject({ state: 'invalid', reason: expect.stringMatching(/regular file/) });
|
|
71
|
+
fs_1.default.unlinkSync(path_1.default.join(root, '.awm/sensors.json'));
|
|
72
|
+
fs_1.default.symlinkSync(path_1.default.join(root, 'missing.json'), path_1.default.join(root, '.awm/sensors.json'));
|
|
73
|
+
expect(resolve(root)).toMatchObject({ state: 'invalid', reason: expect.stringMatching(/regular file/) });
|
|
74
|
+
fs_1.default.unlinkSync(path_1.default.join(root, '.awm/sensors.json'));
|
|
75
|
+
writeJson(path_1.default.join(root, '.awm/sensors.json'), v3Manifest('not-a-directory'));
|
|
76
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'not-a-directory'), 'file');
|
|
77
|
+
expect(resolve(root)).toMatchObject({ state: 'invalid', reason: expect.stringMatching(/existing directory/) });
|
|
78
|
+
writeJson(path_1.default.join(root, '.awm/sensors.json'), v3Manifest('missing-directory'));
|
|
79
|
+
expect(resolve(root)).toMatchObject({ state: 'invalid', reason: expect.stringMatching(/existing directory/) });
|
|
80
|
+
});
|
|
81
|
+
it('rejects invalid UTF-8 and packageRoot symlinks escaping the manifest directory', () => {
|
|
82
|
+
const root = tempDir();
|
|
83
|
+
roots.push(root);
|
|
84
|
+
fs_1.default.mkdirSync(path_1.default.join(root, '.git'));
|
|
85
|
+
fs_1.default.mkdirSync(path_1.default.join(root, '.awm'));
|
|
86
|
+
fs_1.default.writeFileSync(path_1.default.join(root, '.awm/sensors.json'), Buffer.from([0xff, 0xfe]));
|
|
87
|
+
expect(resolve(root)).toMatchObject({ state: 'invalid', reason: expect.stringMatching(/UTF-8/) });
|
|
88
|
+
writeJson(path_1.default.join(root, '.awm/sensors.json'), v3Manifest('linked-package'));
|
|
89
|
+
const outside = tempDir();
|
|
90
|
+
roots.push(outside);
|
|
91
|
+
fs_1.default.symlinkSync(outside, path_1.default.join(root, 'linked-package'));
|
|
92
|
+
expect(resolve(root)).toMatchObject({ state: 'invalid', reason: expect.stringMatching(/escapes/) });
|
|
93
|
+
});
|
|
94
|
+
it('never selects a parent manifest above the Git boundary', () => {
|
|
95
|
+
const parent = tempDir();
|
|
96
|
+
roots.push(parent);
|
|
97
|
+
writeJson(path_1.default.join(parent, '.awm/sensors.json'), v3Manifest());
|
|
98
|
+
const root = path_1.default.join(parent, 'repository');
|
|
99
|
+
fs_1.default.mkdirSync(path_1.default.join(root, '.git'), { recursive: true });
|
|
100
|
+
expect(resolve(root)).toMatchObject({ state: 'missing', projectRoot: root });
|
|
101
|
+
});
|
|
102
|
+
});
|