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
|
@@ -7,6 +7,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
7
7
|
const child_process_1 = require("child_process");
|
|
8
8
|
const os_1 = __importDefault(require("os"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
10
11
|
const json_1 = require("../../../src/core/plan/json");
|
|
11
12
|
const validate_1 = require("../../../src/core/plan/validate");
|
|
12
13
|
jest.mock('child_process', () => ({ exec: jest.fn(), execFileSync: jest.fn(), execSync: jest.fn(), spawn: jest.fn(), spawnSync: jest.fn() }));
|
|
@@ -21,6 +22,18 @@ function expectApprovedPlanValid(report) {
|
|
|
21
22
|
}
|
|
22
23
|
expect(report.schema).toBe('compact-slices/v1');
|
|
23
24
|
}
|
|
25
|
+
function assertFrozenObjectGraph(value, location = 'report', visited = new WeakSet()) {
|
|
26
|
+
if (value === null || typeof value !== 'object' || visited.has(value))
|
|
27
|
+
return;
|
|
28
|
+
visited.add(value);
|
|
29
|
+
if (!Object.isFrozen(value))
|
|
30
|
+
throw new Error(`${location} is mutable`);
|
|
31
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
32
|
+
const property = Object.getOwnPropertyDescriptor(value, key);
|
|
33
|
+
if (property && 'value' in property)
|
|
34
|
+
assertFrozenObjectGraph(property.value, `${location}.${String(key)}`, visited);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
24
37
|
function fixture(root, mutate) {
|
|
25
38
|
fs_1.default.mkdirSync(path_1.default.join(root, 'docs'), { recursive: true });
|
|
26
39
|
fs_1.default.writeFileSync(path_1.default.join(root, 'docs', 'source.md'), '## Source\nKnown fact\n');
|
|
@@ -38,20 +51,268 @@ function fixture(root, mutate) {
|
|
|
38
51
|
return plan;
|
|
39
52
|
}
|
|
40
53
|
describe('validatePlanFile', () => {
|
|
54
|
+
it('validates an authenticated snapshot without reopening a swapped plan parent', () => {
|
|
55
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-plan-snapshot-'));
|
|
56
|
+
const outside = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-plan-snapshot-outside-'));
|
|
57
|
+
try {
|
|
58
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'docs'));
|
|
59
|
+
const snapshot = '### Task 1: anchored\n';
|
|
60
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'docs', 'plan.md'), snapshot);
|
|
61
|
+
fs_1.default.rmSync(path_1.default.join(root, 'docs'), { recursive: true });
|
|
62
|
+
fs_1.default.writeFileSync(path_1.default.join(outside, 'plan.md'), '<!-- attacker -->');
|
|
63
|
+
fs_1.default.symlinkSync(outside, path_1.default.join(root, 'docs'));
|
|
64
|
+
expect((0, validate_1.validatePlanSnapshot)('docs/plan.md', root, snapshot)).toMatchObject({ state: 'migration-required' });
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
68
|
+
fs_1.default.rmSync(outside, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
41
71
|
let root;
|
|
42
72
|
beforeEach(() => { root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-plan-')); });
|
|
43
73
|
afterEach(() => { fs_1.default.rmSync(root, { recursive: true, force: true }); });
|
|
74
|
+
test.each([
|
|
75
|
+
['valid.md', 'valid'],
|
|
76
|
+
['unmarked.md', 'migration-required'],
|
|
77
|
+
['future-schema.md', 'unsupported'],
|
|
78
|
+
['missing-section.md', 'invalid'],
|
|
79
|
+
])('classifies the versioned v1 corpus member %s as %s without transforms', (file, state) => {
|
|
80
|
+
const corpus = path_1.default.resolve(__dirname, 'fixtures', 'compact-slices-v1');
|
|
81
|
+
expect((0, validate_1.validatePlanFile)(file, corpus).state).toBe(state);
|
|
82
|
+
});
|
|
44
83
|
test('accepts a valid compact plan without modifying it', () => {
|
|
45
84
|
const plan = fixture(root);
|
|
46
85
|
const before = fs_1.default.readFileSync(plan, 'utf8');
|
|
47
86
|
expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'valid', schema: 'compact-slices/v1' });
|
|
48
87
|
expect(fs_1.default.readFileSync(plan, 'utf8')).toBe(before);
|
|
49
88
|
});
|
|
89
|
+
test('makes an approved valid report and its nested manifest immutable after verification', () => {
|
|
90
|
+
const report = (0, validate_1.validatePlanFile)(fixture(root), root);
|
|
91
|
+
expectApprovedPlanValid(report);
|
|
92
|
+
if (report.state !== 'valid')
|
|
93
|
+
throw new Error('expected an approved compact plan');
|
|
94
|
+
const slice = report.manifest.slices[0];
|
|
95
|
+
assertFrozenObjectGraph(report);
|
|
96
|
+
expect(Reflect.set(report.manifest.requirements, '0', 'R4-FORGED')).toBe(false);
|
|
97
|
+
expect(Reflect.set(report.manifest.sources[0], 'fact', 'forged fact')).toBe(false);
|
|
98
|
+
expect(Reflect.set(report.manifest.commands[0].args, '0', 'forged-arg')).toBe(false);
|
|
99
|
+
expect(Reflect.set(slice.fallback, '0', 'forged fallback')).toBe(false);
|
|
100
|
+
expect(Reflect.set(slice.reviewEvidence, '1', 'bogus')).toBe(false);
|
|
101
|
+
expect(Reflect.set(slice, 'risk', 'unbounded')).toBe(false);
|
|
102
|
+
expect(Reflect.set(report.manifest.closureCommands, '0', 'CMD-MISSING')).toBe(false);
|
|
103
|
+
expect(report.manifest.requirements).toEqual(['R4-VAL-2']);
|
|
104
|
+
expect(report.manifest.sources[0].fact).toBe('Known fact');
|
|
105
|
+
expect(report.manifest.commands[0].args).toEqual(['test']);
|
|
106
|
+
expect(slice.fallback).toEqual(['Use a reviewed fallback']);
|
|
107
|
+
expect(slice.reviewEvidence).toEqual(['specification', 'code-quality']);
|
|
108
|
+
expect(slice.risk).toBe('bounded');
|
|
109
|
+
expect(report.manifest.closureCommands).toEqual(['CMD-ONE']);
|
|
110
|
+
});
|
|
111
|
+
test('detects a selectively thawed command-args node even when every ancestor is frozen', () => {
|
|
112
|
+
const report = (0, validate_1.validatePlanFile)(fixture(root), root);
|
|
113
|
+
expectApprovedPlanValid(report);
|
|
114
|
+
const thawed = JSON.parse(JSON.stringify(report));
|
|
115
|
+
const mutableArgs = thawed.manifest.commands[0].args;
|
|
116
|
+
const freezeExceptArgs = (value) => {
|
|
117
|
+
if (value === null || typeof value !== 'object' || value === mutableArgs)
|
|
118
|
+
return;
|
|
119
|
+
for (const child of Object.values(value))
|
|
120
|
+
freezeExceptArgs(child);
|
|
121
|
+
Object.freeze(value);
|
|
122
|
+
};
|
|
123
|
+
freezeExceptArgs(thawed);
|
|
124
|
+
expect(Object.isFrozen(thawed.manifest.commands[0])).toBe(true);
|
|
125
|
+
expect(Object.isFrozen(mutableArgs)).toBe(false);
|
|
126
|
+
expect(Reflect.set(mutableArgs, '0', 'tampered')).toBe(true);
|
|
127
|
+
expect(() => assertFrozenObjectGraph(thawed)).toThrow('report.manifest.commands.0.args is mutable');
|
|
128
|
+
});
|
|
129
|
+
test('scans bounded markerless brace runs without quadratic retries', () => {
|
|
130
|
+
const plan = path_1.default.join(root, 'braces.md');
|
|
131
|
+
fs_1.default.writeFileSync(plan, '{'.repeat(32_000));
|
|
132
|
+
const started = Date.now();
|
|
133
|
+
const report = (0, validate_1.validatePlanFile)(plan, root);
|
|
134
|
+
expect(report).toEqual({ state: 'migration-required', reason: 'unmarked-plan' });
|
|
135
|
+
expect(Date.now() - started).toBeLessThan(500);
|
|
136
|
+
});
|
|
137
|
+
test('retains markerless unsupported classification for nested JSON in an unclosed outer object', () => {
|
|
138
|
+
const plan = path_1.default.join(root, 'nested-future.md');
|
|
139
|
+
fs_1.default.writeFileSync(plan, '# Notes\n{"outer":\n{"\\u0073chema":"compact\\u002dslices\\/v2"}\n');
|
|
140
|
+
expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'unsupported', schema: 'compact-slices/v2' });
|
|
141
|
+
});
|
|
142
|
+
test('uses exact bigint filesystem identities for legitimate Windows-sized inode values', () => {
|
|
143
|
+
const plan = fixture(root);
|
|
144
|
+
const nativeLstat = fs_1.default.lstatSync;
|
|
145
|
+
const nativeFstat = fs_1.default.fstatSync;
|
|
146
|
+
const largeIdentity = (observed) => Object.assign(Object.create(observed), {
|
|
147
|
+
ino: typeof observed.ino === 'bigint' ? 9007199254740993n : 9007199254740992,
|
|
148
|
+
});
|
|
149
|
+
const lstat = jest.spyOn(fs_1.default, 'lstatSync').mockImplementation(((...args) => largeIdentity(Reflect.apply(nativeLstat, fs_1.default, args))));
|
|
150
|
+
const fstat = jest.spyOn(fs_1.default, 'fstatSync').mockImplementation(((...args) => largeIdentity(Reflect.apply(nativeFstat, fs_1.default, args))));
|
|
151
|
+
try {
|
|
152
|
+
expectApprovedPlanValid((0, validate_1.validatePlanFile)(plan, root));
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
lstat.mockRestore();
|
|
156
|
+
fstat.mockRestore();
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
test('retains the manifest byte limit beneath the bounded plan-file limit', () => {
|
|
160
|
+
const plan = fixture(root);
|
|
161
|
+
const text = fs_1.default.readFileSync(plan, 'utf8');
|
|
162
|
+
fs_1.default.writeFileSync(plan, text.replace(END, `${' '.repeat(256 * 1024)}\n${END}`));
|
|
163
|
+
expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_LIMIT' })] });
|
|
164
|
+
});
|
|
165
|
+
test.each([
|
|
166
|
+
['plan', 'PLAN_LIMIT'],
|
|
167
|
+
['source', 'PLAN_SOURCE_LIMIT'],
|
|
168
|
+
])('bounds %s bytes when the inspected file grows during descriptor read', (target, code) => {
|
|
169
|
+
const plan = fixture(root);
|
|
170
|
+
const targetPath = target === 'plan' ? plan : path_1.default.join(root, 'docs', 'source.md');
|
|
171
|
+
const targetInode = fs_1.default.statSync(targetPath, { bigint: true }).ino;
|
|
172
|
+
const nativeFstat = fs_1.default.fstatSync;
|
|
173
|
+
let grew = false;
|
|
174
|
+
const fstat = jest.spyOn(fs_1.default, 'fstatSync').mockImplementation((descriptor) => {
|
|
175
|
+
const observed = nativeFstat(descriptor, { bigint: true });
|
|
176
|
+
if (!grew && observed.ino === targetInode) {
|
|
177
|
+
fs_1.default.appendFileSync(targetPath, Buffer.alloc(1024 * 1024));
|
|
178
|
+
grew = true;
|
|
179
|
+
}
|
|
180
|
+
return observed;
|
|
181
|
+
});
|
|
182
|
+
try {
|
|
183
|
+
expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code })] });
|
|
184
|
+
expect(grew).toBe(true);
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
fstat.mockRestore();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
test.each([
|
|
191
|
+
['plan', 'final', 'PLAN_PATH_UNSAFE'],
|
|
192
|
+
['plan', 'ancestor', 'PLAN_PATH_UNSAFE'],
|
|
193
|
+
['source', 'final', 'PLAN_SOURCE_UNSAFE'],
|
|
194
|
+
['source', 'ancestor', 'PLAN_SOURCE_UNSAFE'],
|
|
195
|
+
])('rejects a %s %s symlink swap after inspection without reading outside', (target, swap, code) => {
|
|
196
|
+
let plan = fixture(root);
|
|
197
|
+
const outside = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-plan-race-outside-'));
|
|
198
|
+
const targetDirectory = target === 'plan' ? path_1.default.join(root, 'plans') : path_1.default.join(root, 'docs');
|
|
199
|
+
if (target === 'plan') {
|
|
200
|
+
fs_1.default.mkdirSync(targetDirectory);
|
|
201
|
+
const nestedPlan = path_1.default.join(targetDirectory, 'plan.md');
|
|
202
|
+
fs_1.default.renameSync(plan, nestedPlan);
|
|
203
|
+
plan = nestedPlan;
|
|
204
|
+
}
|
|
205
|
+
const inspectedPath = target === 'plan' ? plan : path_1.default.join(root, 'docs', 'source.md');
|
|
206
|
+
const outsideFile = path_1.default.join(outside, path_1.default.basename(inspectedPath));
|
|
207
|
+
fs_1.default.copyFileSync(inspectedPath, outsideFile);
|
|
208
|
+
const outsideIdentity = fs_1.default.statSync(outsideFile);
|
|
209
|
+
const nativeRealpath = fs_1.default.realpathSync;
|
|
210
|
+
let swapped = false;
|
|
211
|
+
const realpath = jest.spyOn(fs_1.default, 'realpathSync').mockImplementation((file) => {
|
|
212
|
+
const resolved = nativeRealpath(file);
|
|
213
|
+
if (!swapped && file === inspectedPath) {
|
|
214
|
+
swapped = true;
|
|
215
|
+
if (swap === 'final') {
|
|
216
|
+
fs_1.default.renameSync(inspectedPath, `${inspectedPath}-original`);
|
|
217
|
+
fs_1.default.symlinkSync(outsideFile, inspectedPath, 'file');
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
fs_1.default.renameSync(targetDirectory, `${targetDirectory}-original`);
|
|
221
|
+
fs_1.default.symlinkSync(outside, targetDirectory, 'dir');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return resolved;
|
|
225
|
+
});
|
|
226
|
+
const reads = jest.spyOn(fs_1.default, 'readFileSync');
|
|
227
|
+
const nativeReadSync = fs_1.default.readSync;
|
|
228
|
+
let outsideDescriptorReads = 0;
|
|
229
|
+
const descriptorReads = jest.spyOn(fs_1.default, 'readSync').mockImplementation((...args) => {
|
|
230
|
+
const opened = fs_1.default.fstatSync(args[0]);
|
|
231
|
+
if (opened.dev === outsideIdentity.dev && opened.ino === outsideIdentity.ino)
|
|
232
|
+
outsideDescriptorReads += 1;
|
|
233
|
+
return Reflect.apply(nativeReadSync, fs_1.default, args);
|
|
234
|
+
});
|
|
235
|
+
try {
|
|
236
|
+
expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code })] });
|
|
237
|
+
expect(swapped).toBe(true);
|
|
238
|
+
expect(reads.mock.calls.some(([file]) => file === inspectedPath)).toBe(false);
|
|
239
|
+
expect(outsideDescriptorReads).toBe(0);
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
descriptorReads.mockRestore();
|
|
243
|
+
reads.mockRestore();
|
|
244
|
+
realpath.mockRestore();
|
|
245
|
+
fs_1.default.rmSync(outside, { recursive: true, force: true });
|
|
246
|
+
}
|
|
247
|
+
});
|
|
50
248
|
test('accepts a valid compact plan converted to CRLF', () => {
|
|
51
249
|
const plan = fixture(root);
|
|
52
250
|
fs_1.default.writeFileSync(plan, fs_1.default.readFileSync(plan, 'utf8').replace(/\n/g, '\r\n'));
|
|
53
251
|
expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'valid', schema: 'compact-slices/v1' });
|
|
54
252
|
});
|
|
253
|
+
test('returns the SHA-256 identity of the complete normalized valid plan', () => {
|
|
254
|
+
const plan = fixture(root);
|
|
255
|
+
const lf = fs_1.default.readFileSync(plan, 'utf8');
|
|
256
|
+
const expected = crypto_1.default.createHash('sha256').update(lf).digest('hex');
|
|
257
|
+
const lfReport = (0, validate_1.validatePlanFile)(plan, root);
|
|
258
|
+
fs_1.default.writeFileSync(plan, lf.replace(/\n/g, '\r\n'));
|
|
259
|
+
const crlfReport = (0, validate_1.validatePlanFile)(plan, root);
|
|
260
|
+
expect(lfReport).toMatchObject({ state: 'valid', planDigest: expected });
|
|
261
|
+
expect(crlfReport).toMatchObject({ state: 'valid', planDigest: expected });
|
|
262
|
+
});
|
|
263
|
+
test.each([
|
|
264
|
+
['manifest requirement', (text) => text.replace(/R4-VAL-2/g, 'R4-VAL-3')],
|
|
265
|
+
['source fact', (text) => text.replace('Known fact', 'Changed fact')],
|
|
266
|
+
['command argv', (text) => text.replace('"test"', '"build"')],
|
|
267
|
+
['slice prose', (text) => text.replace('One evidence item.', 'Changed evidence item.')],
|
|
268
|
+
['execution-mode prose', (text) => `${text}\nModo de ejecución: desatendido\n`],
|
|
269
|
+
])('changes the plan identity when %s changes', (_name, edit) => {
|
|
270
|
+
const plan = fixture(root);
|
|
271
|
+
const first = (0, validate_1.validatePlanFile)(plan, root);
|
|
272
|
+
expect(first.state).toBe('valid');
|
|
273
|
+
if (first.state !== 'valid')
|
|
274
|
+
throw new Error('expected valid fixture');
|
|
275
|
+
fs_1.default.writeFileSync(plan, edit(fs_1.default.readFileSync(plan, 'utf8')));
|
|
276
|
+
const amended = (0, validate_1.validatePlanFile)(plan, root);
|
|
277
|
+
expect(amended.state).toBe('valid');
|
|
278
|
+
if (amended.state !== 'valid')
|
|
279
|
+
throw new Error('expected valid amended fixture');
|
|
280
|
+
expect(amended.planDigest).not.toBe(first.planDigest);
|
|
281
|
+
});
|
|
282
|
+
test('separates exact progress updates from the full normalized identity', () => {
|
|
283
|
+
const plan = fixture(root);
|
|
284
|
+
fs_1.default.appendFileSync(plan, '\n- [ ] Reviewed task.\n');
|
|
285
|
+
const before = (0, validate_1.validatePlanFile)(plan, root);
|
|
286
|
+
fs_1.default.writeFileSync(plan, fs_1.default.readFileSync(plan, 'utf8').replace('- [ ] Reviewed task.', '- [x] Reviewed task.')
|
|
287
|
+
.replace('#### Evidence', '<!-- awm-qa-complete: 2026-09-16 -->\n<!-- awm-docs-complete: 2026-09-16 -->\n<!-- awm-retro-complete: Release R1 -->\n#### Evidence'));
|
|
288
|
+
const after = (0, validate_1.validatePlanFile)(plan, root);
|
|
289
|
+
expect(before.executionDigest).toEqual(expect.any(String));
|
|
290
|
+
expect(after.planDigest).not.toBe(before.planDigest);
|
|
291
|
+
expect(after.executionDigest).toBe(before.executionDigest);
|
|
292
|
+
});
|
|
293
|
+
test.each([
|
|
294
|
+
['task prose', '- [x] Changed task.\n'],
|
|
295
|
+
['inline marker', 'prose <!-- awm-qa-complete: 2026-09-16 -->\n'],
|
|
296
|
+
['unknown marker', '<!-- awm-qa-complete: arbitrary requirements -->\n'],
|
|
297
|
+
['fenced task', '```md\n- [x] Reviewed task.\n```\n'],
|
|
298
|
+
['fenced marker', '~~~~md\n<!-- awm-qa-complete: 2026-09-16 -->\n~~~~\n'],
|
|
299
|
+
])('does not canonicalize %s away from execution identity', (_label, addition) => {
|
|
300
|
+
const plan = fixture(root);
|
|
301
|
+
const before = (0, validate_1.validatePlanFile)(plan, root);
|
|
302
|
+
fs_1.default.appendFileSync(plan, addition);
|
|
303
|
+
const after = (0, validate_1.validatePlanFile)(plan, root);
|
|
304
|
+
expect(before.executionDigest).toEqual(expect.any(String));
|
|
305
|
+
expect(after.executionDigest).not.toBe(before.executionDigest);
|
|
306
|
+
});
|
|
307
|
+
test('a false fence closer cannot make code checkboxes and markers into lifecycle progress', () => {
|
|
308
|
+
const plan = fixture(root);
|
|
309
|
+
fs_1.default.appendFileSync(plan, '\n~~~~md\n~~~~not-a-closer\n- [ ] Code task.\n<!-- awm-qa-complete: 2026-09-16 -->\n~~~~\n');
|
|
310
|
+
const before = (0, validate_1.validatePlanFile)(plan, root);
|
|
311
|
+
fs_1.default.writeFileSync(plan, fs_1.default.readFileSync(plan, 'utf8').replace('- [ ] Code task.', '- [x] Code task.')
|
|
312
|
+
.replace('<!-- awm-qa-complete: 2026-09-16 -->', '<!-- awm-docs-complete: 2026-09-16 -->'));
|
|
313
|
+
const after = (0, validate_1.validatePlanFile)(plan, root);
|
|
314
|
+
expect(after.executionDigest).not.toBe(before.executionDigest);
|
|
315
|
+
});
|
|
55
316
|
test('accepts a contained plan when path.relative returns Windows separators', () => {
|
|
56
317
|
const plan = fixture(root);
|
|
57
318
|
const nestedPlan = path_1.default.join(root, 'docs', 'plan.md');
|
|
@@ -73,6 +334,59 @@ describe('validatePlanFile', () => {
|
|
|
73
334
|
const repositoryRoot = path_1.default.resolve(__dirname, '../../../..');
|
|
74
335
|
expectApprovedPlanValid((0, validate_1.validatePlanFile)('docs/plans/2026-08-26-r4a-compact-plan-cli-plan.md', repositoryRoot));
|
|
75
336
|
});
|
|
337
|
+
test('preserves every previously valid tracked compact-v1 plan', () => {
|
|
338
|
+
const repositoryRoot = path_1.default.resolve(__dirname, '../../../..');
|
|
339
|
+
const tracked = jest.requireActual('child_process')
|
|
340
|
+
.execFileSync('git', ['ls-files', '--', 'docs/plans/*.md'], { cwd: repositoryRoot, encoding: 'utf8' })
|
|
341
|
+
.trim().split('\n').filter(Boolean);
|
|
342
|
+
const marked = tracked.filter((file) => fs_1.default.readFileSync(path_1.default.join(repositoryRoot, file), 'utf8').includes(START));
|
|
343
|
+
const previouslyValid = [
|
|
344
|
+
'docs/plans/2026-08-26-r4a-compact-plan-cli-plan.md',
|
|
345
|
+
'docs/plans/2026-08-27-sensor-portability-publication-a-plan.md',
|
|
346
|
+
'docs/plans/2026-09-07-retire-onsignal.md',
|
|
347
|
+
'docs/plans/2026-09-14-compact-only-bootstrap-plan.md',
|
|
348
|
+
];
|
|
349
|
+
expect(marked.length).toBeGreaterThan(0);
|
|
350
|
+
expect(previouslyValid.length).toBeGreaterThan(0);
|
|
351
|
+
const corpusCounts = `marked=${marked.length}; previouslyValid=${previouslyValid.length}; historicalInvalid=${marked.length - previouslyValid.length}`;
|
|
352
|
+
for (const file of previouslyValid) {
|
|
353
|
+
if (!marked.includes(file))
|
|
354
|
+
throw new Error(`previously valid compact-v1 plan is not tracked with a marker: ${file}; ${corpusCounts}`);
|
|
355
|
+
const report = (0, validate_1.validatePlanFile)(file, repositoryRoot);
|
|
356
|
+
if (report.state !== 'valid')
|
|
357
|
+
throw new Error(`previously valid compact-v1 plan failed: ${file}; state=${report.state}; ${corpusCounts}`);
|
|
358
|
+
assertFrozenObjectGraph(report, file);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
test.each(['RF-1.3', 'RNF-T.2', 'R4-VAL-2', 'A'.repeat(62) + '.1'])('accepts canonical bounded requirement ID %s in every requirement reference', (id) => {
|
|
362
|
+
const report = (0, validate_1.validatePlanFile)(fixture(root, (manifest) => {
|
|
363
|
+
manifest.requirements = [id];
|
|
364
|
+
manifest.commands[0].covers = [id];
|
|
365
|
+
manifest.slices[0].requirements = [id];
|
|
366
|
+
}), root);
|
|
367
|
+
expect(report).toMatchObject({ state: 'valid', manifest: { requirements: [id] } });
|
|
368
|
+
});
|
|
369
|
+
test.each([
|
|
370
|
+
'.RF-1', 'RF-1.', 'RF..1', 'RF--1', 'RF-', 'rf-1.3', 'RF-1.a', ' RF-1.3', 'RF-1.3 ',
|
|
371
|
+
'RF-1. 3', 'RF-1/3', 'RF-1\\3', 'RF-1.é', 'RF-1.\u0000', 'RF-1.\u007f',
|
|
372
|
+
'A'.repeat(63) + '.1',
|
|
373
|
+
])('rejects malformed or oversized requirement ID %j', (id) => {
|
|
374
|
+
const report = (0, validate_1.validatePlanFile)(fixture(root, (manifest) => {
|
|
375
|
+
manifest.requirements = [id];
|
|
376
|
+
manifest.commands[0].covers = [id];
|
|
377
|
+
manifest.slices[0].requirements = [id];
|
|
378
|
+
}), root);
|
|
379
|
+
expect(report).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_SHAPE' })] });
|
|
380
|
+
});
|
|
381
|
+
test.each([
|
|
382
|
+
['source', (m) => { m.sources[0].id = 'SRC.ONE'; }, 'PLAN_SOURCE_SHAPE'],
|
|
383
|
+
['command', (m) => { m.commands[0].id = 'CMD.ONE'; }, 'PLAN_COMMAND_SHAPE'],
|
|
384
|
+
['slice', (m) => { m.slices[0].id = 'S.1'; }, 'PLAN_SLICE_SHAPE'],
|
|
385
|
+
])('keeps dotted %s entity IDs invalid', (_name, mutate, code) => {
|
|
386
|
+
expect((0, validate_1.validatePlanFile)(fixture(root, mutate), root)).toMatchObject({
|
|
387
|
+
state: 'invalid', diagnostics: [expect.objectContaining({ code })],
|
|
388
|
+
});
|
|
389
|
+
});
|
|
76
390
|
test('performs no execution, network request, model work, grouping, or rewrite', () => {
|
|
77
391
|
const plan = fixture(root);
|
|
78
392
|
const before = fs_1.default.readFileSync(plan, 'utf8');
|
|
@@ -229,12 +543,12 @@ describe('validatePlanFile', () => {
|
|
|
229
543
|
const report = (0, validate_1.validatePlanFile)(fixture(root, (m) => { m.slices[0].sectionAnchor = 'a'.repeat(4097); }), root);
|
|
230
544
|
expect(report).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_SLICE_SHAPE' })] });
|
|
231
545
|
});
|
|
232
|
-
test('
|
|
546
|
+
test('requires migration for a document without any compact signal', () => {
|
|
233
547
|
const plan = path_1.default.join(root, 'legacy.md');
|
|
234
548
|
fs_1.default.writeFileSync(plan, '# Legacy plan\n');
|
|
235
|
-
expect((0, validate_1.validatePlanFile)(plan, root)).toEqual({ state: '
|
|
549
|
+
expect((0, validate_1.validatePlanFile)(plan, root)).toEqual({ state: 'migration-required', reason: 'unmarked-plan' });
|
|
236
550
|
});
|
|
237
|
-
test('does not classify an escaped future schema signal as
|
|
551
|
+
test('does not classify an escaped future schema signal as migration-required', () => {
|
|
238
552
|
const plan = path_1.default.join(root, 'escaped-future.md');
|
|
239
553
|
fs_1.default.writeFileSync(plan, '{"schema":"compact-slices\\u002fv2"}');
|
|
240
554
|
expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'unsupported', schema: 'compact-slices/v2' });
|
|
@@ -242,17 +556,17 @@ describe('validatePlanFile', () => {
|
|
|
242
556
|
test.each([
|
|
243
557
|
['escaped schema key', '{"\\u0073chema":"compact-slices/v2"}'],
|
|
244
558
|
['escaped schema hyphen', '{"schema":"compact\\u002dslices/v2"}'],
|
|
245
|
-
])('does not classify a future schema with an %s as
|
|
559
|
+
])('does not classify a future schema with an %s as migration-required', (_name, manifest) => {
|
|
246
560
|
const plan = path_1.default.join(root, 'escaped-future-schema.md');
|
|
247
561
|
fs_1.default.writeFileSync(plan, manifest);
|
|
248
562
|
expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'unsupported', schema: 'compact-slices/v2' });
|
|
249
563
|
});
|
|
250
|
-
test('does not classify an escaped future schema embedded in Markdown without markers as
|
|
564
|
+
test('does not classify an escaped future schema embedded in Markdown without markers as migration-required', () => {
|
|
251
565
|
const plan = path_1.default.join(root, 'embedded-escaped-future.md');
|
|
252
566
|
fs_1.default.writeFileSync(plan, '# Plan notes\n\n```json\n{"\\u0073chema":"compact\\u002dslices\\u002fv2"}\n```\n');
|
|
253
567
|
expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'unsupported', schema: 'compact-slices/v2' });
|
|
254
568
|
});
|
|
255
|
-
test('rejects an unparseable escaped compact schema embedded in Markdown rather than
|
|
569
|
+
test('rejects an unparseable escaped compact schema embedded in Markdown rather than requiring migration', () => {
|
|
256
570
|
const plan = path_1.default.join(root, 'embedded-escaped-malformed.md');
|
|
257
571
|
fs_1.default.writeFileSync(plan, '# Plan notes\n\n{"\\u0073chema":"compact\\u002dslices\\u002fv2"\n');
|
|
258
572
|
expect((0, validate_1.validatePlanFile)(plan, root)).toMatchObject({ state: 'invalid', diagnostics: [expect.objectContaining({ code: 'PLAN_MARKERS' })] });
|
|
@@ -38,7 +38,10 @@ it('el tarball empaquetado corre awm update sin el monorepo', () => {
|
|
|
38
38
|
const awmHome = path_1.default.join(home, '.awm');
|
|
39
39
|
fs_1.default.mkdirSync(awmHome, { recursive: true });
|
|
40
40
|
fs_1.default.writeFileSync(path_1.default.join(awmHome, 'registries.json'), JSON.stringify([{ name: 'baseline', remote: source }]));
|
|
41
|
-
|
|
41
|
+
// El artefacto se debe ejecutar con el mismo runtime que corre la prueba:
|
|
42
|
+
// usar `node` desde PATH podría probar accidentalmente otra versión. En
|
|
43
|
+
// particular, esto cubre Node 22, declarado como compatible por package.json.
|
|
44
|
+
(0, child_process_1.execSync)(`"${process.execPath}" "${path_1.default.join(pkgDir, 'dist/src/index.js')}" update`, {
|
|
42
45
|
env: { ...process.env, HOME: home, AWM_HOME: awmHome, AWM_NO_UPDATE_CHECK: '1' },
|
|
43
46
|
stdio: 'pipe',
|
|
44
47
|
});
|
|
@@ -105,13 +105,17 @@ test('verify-sensors degrades parseably for a v2 sensor that exits 2 without fin
|
|
|
105
105
|
writeJson(path_1.default.join(project, 'package.json'), { name: 'preflight-verify-fixture', private: true });
|
|
106
106
|
writeJson(path_1.default.join(project, 'node_modules', 'fixture-sensor', 'package.json'), { name: 'fixture-sensor', version: '1.0.0' });
|
|
107
107
|
fs_1.default.writeFileSync(path_1.default.join(project, 'fixture-sensor.config.mjs'), 'export default {};\n');
|
|
108
|
-
|
|
108
|
+
const fixtureArgs = ['-e', 'process.exit(2);', '--', '--fixture'];
|
|
109
|
+
const fixtureBin = path_1.default.join(project, 'node_modules', '.bin', process.platform === 'win32' ? 'fixture-sensor.exe' : 'fixture-sensor');
|
|
110
|
+
fs_1.default.mkdirSync(path_1.default.dirname(fixtureBin), { recursive: true });
|
|
111
|
+
fs_1.default.copyFileSync(process.execPath, fixtureBin);
|
|
112
|
+
fs_1.default.chmodSync(fixtureBin, 0o755);
|
|
109
113
|
writeJson(path_1.default.join(registry, 'sensor-packs', 'fixture', 'pack.json'), {
|
|
110
114
|
schemaVersion: 2, name: 'fixture', description: 'preflight exit-2 fixture', detects: ['package.json'],
|
|
111
115
|
sensors: { lint: { applicability: { allFiles: ['package.json'] }, variants: [{
|
|
112
116
|
id: 'fixture-v1', priority: 100,
|
|
113
117
|
requirements: { tool: 'fixture-sensor', toolRange: '>=1 <2', runtime: 'node', runtimeRange: '>=20', configFiles: ['fixture-sensor.config.mjs'] },
|
|
114
|
-
certifiedRange: '>=1 <2', command: { executable: '
|
|
118
|
+
certifiedRange: '>=1 <2', command: { executable: 'fixture-sensor', resolution: 'node-modules-bin', args: fixtureArgs },
|
|
115
119
|
assets: ['fixture-sensor.config.mjs'], formatter: 'generic', probe: { kind: 'config-present' },
|
|
116
120
|
}] } },
|
|
117
121
|
coverage: { schemaVersion: 1, classes: {
|
|
@@ -124,7 +128,7 @@ test('verify-sensors degrades parseably for a v2 sensor that exits 2 without fin
|
|
|
124
128
|
writeJson(path_1.default.join(awmHome, 'registries.json'), [{ name: 'baseline', remote: 'fixture' }]);
|
|
125
129
|
writeJson(path_1.default.join(project, '.awm', 'sensors.json'), {
|
|
126
130
|
schemaVersion: 2, pack: 'fixture', packSelection: 'explicit', sensors: { lint: {
|
|
127
|
-
enabled: true, fast: true, variantId: 'fixture-v1', command: { executable: '
|
|
131
|
+
enabled: true, fast: true, variantId: 'fixture-v1', command: { executable: 'fixture-sensor', resolution: 'node-modules-bin', args: fixtureArgs }, assets: ['fixture-sensor.config.mjs'],
|
|
128
132
|
initializedCompatibility: { state: 'certified', reason: 'fixture', variantId: 'fixture-v1', toolVersion: '1.0.0', runtimeVersion: process.versions.node, certifiedRange: '>=1 <2', evidence: [] },
|
|
129
133
|
} },
|
|
130
134
|
});
|
|
@@ -14,6 +14,7 @@ const safe_file_1 = require("../../src/commands/sensors/compatibility/safe-file"
|
|
|
14
14
|
const native_bridge_1 = require("../../src/core/secure-fs/native-bridge");
|
|
15
15
|
const cliRoot = path_1.default.resolve(__dirname, '../..');
|
|
16
16
|
const bin = path_1.default.join(cliRoot, 'dist/src/index.js');
|
|
17
|
+
const nativeArtifact = path_1.default.join(cliRoot, 'prebuilds', `${process.platform}-${process.arch}`, 'secure_fs.node');
|
|
17
18
|
const fixtureRoot = path_1.default.join(cliRoot, 'tests/fixtures/sensor-compatibility');
|
|
18
19
|
function hashTree(root) {
|
|
19
20
|
const hash = crypto_1.default.createHash('sha256');
|
|
@@ -70,6 +71,8 @@ function v2Manifest() {
|
|
|
70
71
|
beforeAll(() => {
|
|
71
72
|
if (!fs_1.default.existsSync(bin))
|
|
72
73
|
throw new Error(`Sensor bootstrap E2E requires the compiled CLI at ${bin}; run npm run build before this test.`);
|
|
74
|
+
if (!fs_1.default.existsSync(nativeArtifact))
|
|
75
|
+
throw new Error(`Sensor bootstrap E2E requires the host secure-fs artifact at ${nativeArtifact}; run npm run native:build before this test.`);
|
|
73
76
|
});
|
|
74
77
|
test('compiled bootstrap creates once and the exact second invocation is a byte-stable no-op', () => {
|
|
75
78
|
const subject = fixture();
|
|
@@ -163,3 +166,37 @@ test('compiled bootstrap migrates v2 once, preserves all non-manifest project by
|
|
|
163
166
|
fs_1.default.rmSync(subject.root, { recursive: true, force: true });
|
|
164
167
|
}
|
|
165
168
|
});
|
|
169
|
+
test('compiled bootstrap requires explicit project-sensors mode before replacing a legacy manifest', () => {
|
|
170
|
+
const subject = fixture();
|
|
171
|
+
try {
|
|
172
|
+
const manifest = path_1.default.join(subject.project, '.awm', 'sensors.json');
|
|
173
|
+
const legacy = {
|
|
174
|
+
pack: 'js-ts',
|
|
175
|
+
sensors: { lint: { cmd: 'npx eslint .' } },
|
|
176
|
+
};
|
|
177
|
+
fs_1.default.writeFileSync(manifest, JSON.stringify(legacy, null, 2) + '\n');
|
|
178
|
+
const before = fs_1.default.readFileSync(manifest);
|
|
179
|
+
const machineBefore = hashTree(subject.awmHome);
|
|
180
|
+
const blocked = run(subject, '--dry-run');
|
|
181
|
+
expect(blocked.status).toBe(1);
|
|
182
|
+
expect(`${blocked.stdout}${blocked.stderr}`).toContain('legacy-replacement-requires-project-sensors-mode');
|
|
183
|
+
expect(fs_1.default.readFileSync(manifest)).toEqual(before);
|
|
184
|
+
const dryRun = run(subject, '--mode', 'project-sensors', '--dry-run');
|
|
185
|
+
expect(dryRun.status).toBe(0);
|
|
186
|
+
expect(fs_1.default.readFileSync(manifest)).toEqual(before);
|
|
187
|
+
const migrated = run(subject, '--mode', 'project-sensors');
|
|
188
|
+
expect(migrated.status).toBe(0);
|
|
189
|
+
expect(`${migrated.stdout}${migrated.stderr}`).toContain('migrated');
|
|
190
|
+
expect(`${migrated.stdout}${migrated.stderr}`).not.toContain('legacy-v1-preserved');
|
|
191
|
+
expect(JSON.parse(fs_1.default.readFileSync(manifest, 'utf8'))).toMatchObject({
|
|
192
|
+
schemaVersion: 3,
|
|
193
|
+
mode: 'project-sensors',
|
|
194
|
+
pack: 'js-ts',
|
|
195
|
+
source: { registry: 'baseline' },
|
|
196
|
+
});
|
|
197
|
+
expect(hashTree(subject.awmHome)).toBe(machineBefore);
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
fs_1.default.rmSync(subject.root, { recursive: true, force: true });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
@@ -26,6 +26,7 @@ function createFixture() {
|
|
|
26
26
|
const awmHome = path_1.default.join(root, 'awm-home');
|
|
27
27
|
const registryRoot = path_1.default.join(awmHome, 'registries', 'baseline');
|
|
28
28
|
const literal = shellSensitiveLiteral;
|
|
29
|
+
const args = ['-e', "require('fs').writeFileSync('structured-argv.json', JSON.stringify(process.argv.slice(1)));", '--', literal];
|
|
29
30
|
writeJson(path_1.default.join(project, 'package.json'), { name: 'structured-run-fixture', private: true });
|
|
30
31
|
// Local metadata is the compatibility evidence. It is not a tool found in a
|
|
31
32
|
// global installation, so init and run must keep resolving this fixture's pack.
|
|
@@ -33,10 +34,12 @@ function createFixture() {
|
|
|
33
34
|
name: 'fixture-sensor', version: '1.0.0',
|
|
34
35
|
});
|
|
35
36
|
fs_1.default.writeFileSync(path_1.default.join(project, 'fixture-sensor.config.mjs'), 'export default {};\n');
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
const fixtureBin = path_1.default.join(project, 'node_modules', '.bin', process.platform === 'win32' ? 'fixture-sensor.exe' : 'fixture-sensor');
|
|
38
|
+
fs_1.default.mkdirSync(path_1.default.dirname(fixtureBin), { recursive: true });
|
|
39
|
+
// A real local native executable works on Windows too; shebang scripts do
|
|
40
|
+
// not. The controlled program and adversarial literal remain separate argv.
|
|
41
|
+
fs_1.default.copyFileSync(process.execPath, fixtureBin);
|
|
42
|
+
fs_1.default.chmodSync(fixtureBin, 0o755);
|
|
40
43
|
writeJson(path_1.default.join(registryRoot, 'sensor-packs', 'fixture', 'pack.json'), {
|
|
41
44
|
schemaVersion: 2,
|
|
42
45
|
name: 'fixture',
|
|
@@ -53,10 +56,9 @@ function createFixture() {
|
|
|
53
56
|
runtime: 'node', runtimeRange: '>=20', configFiles: ['fixture-sensor.config.mjs'],
|
|
54
57
|
},
|
|
55
58
|
certifiedRange: '>=1 <2',
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
command: { executable: 'node', resolution: 'path', args: ['fixture-sensor.mjs', literal] },
|
|
59
|
+
// The fixture executable is project-local. Keeping the literal
|
|
60
|
+
// as one argv element proves no shell parses it.
|
|
61
|
+
command: { executable: 'fixture-sensor', resolution: 'node-modules-bin', args },
|
|
60
62
|
assets: ['fixture-sensor.config.mjs'], formatter: 'generic', probe: { kind: 'config-present' },
|
|
61
63
|
}],
|
|
62
64
|
},
|
|
@@ -72,7 +74,7 @@ function createFixture() {
|
|
|
72
74
|
},
|
|
73
75
|
});
|
|
74
76
|
writeJson(path_1.default.join(awmHome, 'registries.json'), [{ name: 'baseline', remote: 'fixture' }]);
|
|
75
|
-
return { root, project, awmHome, registryRoot, literal };
|
|
77
|
+
return { root, project, awmHome, registryRoot, literal, args };
|
|
76
78
|
}
|
|
77
79
|
function runCli(fixture, ...args) {
|
|
78
80
|
return (0, child_process_1.spawnSync)(process.execPath, [bin, 'sensors', ...args], {
|
|
@@ -111,7 +113,7 @@ test('compiled sensors run materializes a v2 registry command and passes its lit
|
|
|
111
113
|
mode: 'project-sensors',
|
|
112
114
|
pack: 'fixture',
|
|
113
115
|
source: { registry: 'baseline' },
|
|
114
|
-
sensors: { structured: { variantId: 'fixture-v1', command: { executable: '
|
|
116
|
+
sensors: { structured: { variantId: 'fixture-v1', command: { executable: 'fixture-sensor', resolution: 'node-modules-bin', args: fixture.args } } },
|
|
115
117
|
});
|
|
116
118
|
const result = runCli(fixture, 'run', '--fast');
|
|
117
119
|
assertSuccessfulCli(result, 'run --fast', true);
|
|
@@ -6,18 +6,26 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const os_1 = __importDefault(require("os"));
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
const providers_1 = require("../../src/providers");
|
|
9
|
+
const paths_1 = require("../../src/core/paths");
|
|
9
10
|
describe('getInjection', () => {
|
|
10
11
|
it('returns cc-settings-merge for claude-code', () => {
|
|
11
12
|
const inj = (0, providers_1.getInjection)('claude-code');
|
|
12
13
|
expect(inj?.type).toBe('cc-settings-merge');
|
|
13
14
|
});
|
|
14
15
|
it('returns config-instructions for opencode pointing at the global opencode.json', () => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
// Windows os.homedir() uses USERPROFILE while the CLI honors HOME.
|
|
17
|
+
const systemHome = jest.spyOn(os_1.default, 'homedir').mockReturnValue(path_1.default.resolve('different-system-profile'));
|
|
18
|
+
try {
|
|
19
|
+
const inj = (0, providers_1.getInjection)('opencode');
|
|
20
|
+
expect(inj).toEqual({
|
|
21
|
+
type: 'config-instructions',
|
|
22
|
+
configPath: path_1.default.join((0, paths_1.homeDir)(), '.config/opencode/opencode.json'),
|
|
23
|
+
field: 'instructions',
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
systemHome.mockRestore();
|
|
28
|
+
}
|
|
21
29
|
});
|
|
22
30
|
it('returns undefined for antigravity (no injection mechanism wired yet)', () => {
|
|
23
31
|
expect((0, providers_1.getInjection)('antigravity')).toBeUndefined();
|