agentic-workflow-manager 9.21.0 → 9.22.1
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/change/index.js +16 -5
- package/dist/src/commands/init.js +8 -4
- package/dist/src/commands/preflight/checks.js +22 -0
- package/dist/src/commands/registry/add.js +9 -3
- package/dist/src/commands/sensors/compatibility/contract.js +56 -3
- package/dist/src/commands/sensors/compatibility/manifest.js +16 -2
- package/dist/src/commands/sensors/index.js +10 -1
- package/dist/src/commands/sensors/inputs-digest.js +315 -0
- package/dist/src/commands/sensors/prepare.js +10 -0
- package/dist/src/commands/sensors/result-cache.js +278 -0
- package/dist/src/commands/sensors/result.js +31 -12
- package/dist/src/commands/sensors/run.js +265 -8
- package/dist/src/commands/sensors/status.js +66 -7
- package/dist/src/commands/sync.js +11 -5
- package/dist/src/commands/update.js +23 -12
- package/dist/src/core/changes/derive.js +6 -3
- package/dist/src/core/changes/git.js +1 -2
- package/dist/src/core/changes/receipt.js +219 -9
- package/dist/src/core/changes/worktree.js +15 -3
- package/dist/src/core/init/steps.js +3 -8
- package/dist/src/core/registries.js +20 -15
- package/dist/src/core/registry-git-errors.js +107 -0
- package/dist/src/core/registry-git.js +93 -0
- package/dist/src/core/registry-sync-report.js +74 -0
- package/dist/src/core/versioning.js +3 -6
- package/dist/src/index.js +5 -8
- package/dist/tests/commands/change/new.test.js +104 -58
- package/dist/tests/commands/change/validate-unmeasured.test.js +246 -0
- package/dist/tests/commands/change/validate.test.js +164 -108
- package/dist/tests/commands/changes/list.test.js +41 -22
- package/dist/tests/commands/job/gate-reconcile.test.js +13 -2
- package/dist/tests/commands/preflight/sensors-inputs.test.js +110 -0
- package/dist/tests/commands/registry-sync-honesty.test.js +175 -0
- package/dist/tests/commands/sensors/compatibility/contract.test.js +60 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +94 -0
- package/dist/tests/commands/sensors/exec.test.js +3 -1
- package/dist/tests/commands/sensors/inputs-digest.test.js +373 -0
- package/dist/tests/commands/sensors/result-cache.test.js +352 -0
- package/dist/tests/commands/sensors/run-cache.test.js +434 -0
- package/dist/tests/commands/sensors/run-slice.test.js +171 -0
- package/dist/tests/commands/sensors/status-inputs.test.js +271 -0
- package/dist/tests/commands/update.test.js +8 -8
- package/dist/tests/commands/watch/supervisor-loop.test.js +21 -5
- package/dist/tests/core/changes/helpers.js +15 -3
- package/dist/tests/core/changes/list-paging.test.js +131 -110
- package/dist/tests/core/changes/list.test.js +133 -83
- package/dist/tests/core/changes/receipt.test.js +279 -8
- package/dist/tests/core/diagnostics/provider-checks.test.js +3 -0
- package/dist/tests/core/init/steps-registry-sync.test.js +6 -8
- package/dist/tests/core/registry-git.test.js +153 -0
- package/dist/tests/helpers/commit-seeded-registry.js +18 -0
- package/dist/tests/integration/codex-provider-isolated.test.js +7 -2
- package/dist/tests/integration/copilot-init-isolated.test.js +7 -4
- package/dist/tests/structural/active-documentation.test.js +0 -41
- package/dist/tests/structural/bundle-skill-reference-contract.test.js +0 -12
- package/dist/tests/structural/cli-reference-matches-commander.test.js +94 -0
- package/dist/tests/structural/native-release-artifacts.test.js +116 -107
- package/dist/tests/structural/sensor-documentation-contract.test.js +0 -47
- package/dist/tests/structural/sensor-portability-contract.test.js +0 -7
- package/dist/tests/structural/simple-git-v4-pin.test.js +49 -0
- package/package.json +3 -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
- package/dist/tests/structural/cloud-admission-docs-contract.test.js +0 -54
- package/dist/tests/structural/documentation-phase-is-mechanized.test.js +0 -29
- package/dist/tests/structural/operator-environments-docs-contract.test.js +0 -70
- package/dist/tests/structural/process-model-description-contract.test.js +0 -34
- package/dist/tests/structural/project-declared-sensors-docs-contract.test.js +0 -104
- package/dist/tests/structural/r4-plan-currentness-docs.test.js +0 -37
|
@@ -57,20 +57,46 @@ afterEach(() => expect(listReal()).toEqual(realBefore));
|
|
|
57
57
|
const UNSAFE = /[\u0000-\u001f\u007f-\u009f\u2028\u2029\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/;
|
|
58
58
|
let repo;
|
|
59
59
|
let template;
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
/** The template plus `buildHistory`, built once: `history()` starts a test from a copy of it (no git process). */
|
|
61
|
+
let historic;
|
|
62
|
+
beforeAll(() => {
|
|
63
|
+
template = (0, helpers_1.makeRepo)();
|
|
64
|
+
historic = (0, helpers_1.cloneRepo)(template);
|
|
65
|
+
buildHistory(historic);
|
|
66
|
+
});
|
|
67
|
+
afterAll(() => { historic.cleanup(); template.cleanup(); });
|
|
62
68
|
beforeEach(() => { repo = (0, helpers_1.cloneRepo)(template); });
|
|
63
69
|
afterEach(() => { jest.restoreAllMocks(); repo.cleanup(); });
|
|
64
70
|
const FIRST = '2026-10-16-first';
|
|
71
|
+
function buildHistory(into) {
|
|
72
|
+
into.commit({ 'old.ts': '1' }, 'feat: before adoption', '2026-10-16T09:00:00Z');
|
|
73
|
+
into.commit({ [(0, list_fixtures_1.recordPath)(FIRST)]: (0, list_fixtures_1.record)(FIRST), 'a.ts': '1' }, 'feat: first (#10)', '2026-10-16T20:00:00Z');
|
|
74
|
+
into.commit({ 'b.ts': '1' }, 'fix: no record', '2026-10-16T21:00:00Z');
|
|
75
|
+
into.commit({ 'c.ts': '1' }, 'chore(release): v1.0.0 [skip ci]\n\nChange-Record: none', '2026-10-16T22:00:00Z');
|
|
76
|
+
into.commit({ [(0, list_fixtures_1.recordPath)('2026-10-17-bad')]: '---\nschema: nope\n---\n' }, 'feat: bad record', '2026-10-17T01:00:00Z');
|
|
77
|
+
into.gitAt('2026-10-17T02:00:00Z', 'tag', '-a', 'v1.0.0', '-m', 'v1.0.0');
|
|
78
|
+
}
|
|
79
|
+
/** The repository after `buildHistory` (a copy of the prebuilt one). */
|
|
65
80
|
function history() {
|
|
66
|
-
repo.
|
|
67
|
-
repo
|
|
68
|
-
repo.commit({ 'b.ts': '1' }, 'fix: no record', '2026-10-16T21:00:00Z');
|
|
69
|
-
repo.commit({ 'c.ts': '1' }, 'chore(release): v1.0.0 [skip ci]\n\nChange-Record: none', '2026-10-16T22:00:00Z');
|
|
70
|
-
repo.commit({ [(0, list_fixtures_1.recordPath)('2026-10-17-bad')]: '---\nschema: nope\n---\n' }, 'feat: bad record', '2026-10-17T01:00:00Z');
|
|
71
|
-
repo.gitAt('2026-10-17T02:00:00Z', 'tag', '-a', 'v1.0.0', '-m', 'v1.0.0');
|
|
81
|
+
repo.cleanup();
|
|
82
|
+
repo = (0, helpers_1.cloneRepo)(historic);
|
|
72
83
|
}
|
|
73
84
|
const run = (opts = {}, deps = {}) => (0, changes_1.runChangesList)(repo.root, opts, { now: list_fixtures_1.now, ...deps });
|
|
85
|
+
const sharedRuns = new Map();
|
|
86
|
+
/**
|
|
87
|
+
* `run(opts)` on the untouched prebuilt history. The command is read-only (R5.7, asserted below), so each distinct run is made once
|
|
88
|
+
* and its result shared by the tests that only read it.
|
|
89
|
+
*/
|
|
90
|
+
function onHistory(opts) {
|
|
91
|
+
const key = JSON.stringify(Object.entries(opts).sort(([a], [b]) => a.localeCompare(b)));
|
|
92
|
+
let r = sharedRuns.get(key);
|
|
93
|
+
if (r === undefined) {
|
|
94
|
+
r = (0, changes_1.runChangesList)(historic.root, opts, { now: list_fixtures_1.now });
|
|
95
|
+
sharedRuns.set(key, r);
|
|
96
|
+
}
|
|
97
|
+
return r;
|
|
98
|
+
}
|
|
99
|
+
const historyJson = () => JSON.parse(onHistory({ since: '48h', format: 'json' }).stdout);
|
|
74
100
|
const json = (opts = {}, deps = {}) => JSON.parse(run({ since: '48h', format: 'json', ...opts }, deps).stdout);
|
|
75
101
|
const failure = (r, code) => {
|
|
76
102
|
expect(r.code).toBe(2);
|
|
@@ -209,8 +235,7 @@ describe('awm changes list: adoption (R5.4)', () => {
|
|
|
209
235
|
});
|
|
210
236
|
describe('awm changes list: classification and derived fields (R5.1, R5.3, R5.4, R5.5)', () => {
|
|
211
237
|
test('classifies and derives', () => {
|
|
212
|
-
|
|
213
|
-
const out = json();
|
|
238
|
+
const out = historyJson();
|
|
214
239
|
const byClass = Object.fromEntries(out.changes.map((c) => [c.subject, c.classification]));
|
|
215
240
|
expect(byClass).toEqual({ 'feat: first (#10)': 'recorded', 'fix: no record': 'missing', 'chore(release): v1.0.0 [skip ci]': 'exempt', 'feat: bad record': 'invalid-record' });
|
|
216
241
|
const first = out.changes.find((c) => c.subject === 'feat: first (#10)');
|
|
@@ -219,8 +244,7 @@ describe('awm changes list: classification and derived fields (R5.1, R5.3, R5.4,
|
|
|
219
244
|
expect(out.repo).toBeNull();
|
|
220
245
|
});
|
|
221
246
|
test('pre-adoption commits are omitted', () => {
|
|
222
|
-
|
|
223
|
-
expect(JSON.stringify(json())).not.toContain('before adoption');
|
|
247
|
+
expect(JSON.stringify(historyJson())).not.toContain('before adoption');
|
|
224
248
|
});
|
|
225
249
|
test('repo identity strips credentials', () => {
|
|
226
250
|
history();
|
|
@@ -291,28 +315,24 @@ describe('awm changes list: mixed invalid commits, odd dates and shallow clones
|
|
|
291
315
|
});
|
|
292
316
|
describe('awm changes list: formats (R5.6)', () => {
|
|
293
317
|
test('json is one document on one line', () => {
|
|
294
|
-
|
|
295
|
-
const r = run({ since: '48h', format: 'json' });
|
|
318
|
+
const r = onHistory({ since: '48h', format: 'json' });
|
|
296
319
|
expect(r.stdout.split('\n')).toHaveLength(2);
|
|
297
320
|
expect(Object.keys(JSON.parse(r.stdout))).toEqual(['schema', 'repo', 'base', 'since', 'adopted', 'truncated', 'omitted', 'changes']);
|
|
298
321
|
expect(JSON.parse(r.stdout).adopted).toBe(true);
|
|
299
322
|
});
|
|
300
323
|
test('voice speaks recorded changes and counts the rest', () => {
|
|
301
|
-
|
|
302
|
-
const r = run({ since: '48h', format: 'voice' });
|
|
324
|
+
const r = onHistory({ since: '48h', format: 'voice' });
|
|
303
325
|
expect(r.stdout).toContain('Primer cambio. feat, verificado, 2 validaciones manuales pendientes.');
|
|
304
326
|
expect(r.stdout).toMatch(/1 cambio\(s\) sin record y 1 con record inválido/);
|
|
305
327
|
});
|
|
306
328
|
test('text is a table with the classification of each change', () => {
|
|
307
|
-
|
|
308
|
-
const out = run({ since: '48h' }).stdout;
|
|
329
|
+
const out = onHistory({ since: '48h' }).stdout;
|
|
309
330
|
for (const word of ['recorded', 'missing', 'exempt', 'invalid-record', 'v1.0.0', '#10'])
|
|
310
331
|
expect(out).toContain(word);
|
|
311
332
|
expect(out.split('\n')[0]).toMatch(/^WHEN\s+CLASSIFICATION/);
|
|
312
333
|
});
|
|
313
334
|
test('text is the default format', () => {
|
|
314
|
-
|
|
315
|
-
expect(run({ since: '48h' }).stdout).toBe(run({ since: '48h', format: 'text' }).stdout);
|
|
335
|
+
expect(onHistory({ since: '48h' }).stdout).toBe(onHistory({ since: '48h', format: 'text' }).stdout);
|
|
316
336
|
});
|
|
317
337
|
test('an empty window says so in text', () => {
|
|
318
338
|
history();
|
|
@@ -328,9 +348,8 @@ describe('awm changes list: formats (R5.6)', () => {
|
|
|
328
348
|
expect(run({ since: '48h', format: 'voice' }, deps).stdout).toMatch(/Hay 2 cambio\(s\) más antiguo\(s\) sin mostrar/);
|
|
329
349
|
});
|
|
330
350
|
test('no format prints anything on stderr when it succeeds', () => {
|
|
331
|
-
history();
|
|
332
351
|
for (const format of ['text', 'json', 'voice'])
|
|
333
|
-
expect(
|
|
352
|
+
expect(onHistory({ since: '48h', format }).stderr).toBe('');
|
|
334
353
|
});
|
|
335
354
|
});
|
|
336
355
|
describe('awm changes list: read-only (R5.7)', () => {
|
|
@@ -425,9 +425,20 @@ describe('reap — limpieza explicita con identidad validada (R2.2)', () => {
|
|
|
425
425
|
expect(m.aliveWithIdentity).toBe(false);
|
|
426
426
|
});
|
|
427
427
|
test('planReap reporta aliveWithIdentity: true para un proceso genuinamente vivo', () => {
|
|
428
|
-
|
|
428
|
+
// Same full-spawn retry as process.test.ts: on windows-11-arm (and
|
|
429
|
+
// historically windows-latest) a freshly spawned `node -e` can die
|
|
430
|
+
// before OpenProcess/pidExistsNative sees it. Re-polling the same pid
|
|
431
|
+
// never helps; a fresh instance can. Three failures still fail loudly.
|
|
432
|
+
const { spawnStructured, refIsAlive } = require('../../../src/core/journal/process');
|
|
429
433
|
const s = (0, types_1.emptyState)('r');
|
|
430
|
-
|
|
434
|
+
let { child, ref } = spawnStructured(['node', '-e', 'setTimeout(()=>{}, 5000)'], process.cwd(), 'nLive');
|
|
435
|
+
for (let attempt = 1; attempt < 3 && !refIsAlive(ref); attempt++) {
|
|
436
|
+
try {
|
|
437
|
+
child.kill('SIGKILL');
|
|
438
|
+
}
|
|
439
|
+
catch { /* already gone */ }
|
|
440
|
+
({ child, ref } = spawnStructured(['node', '-e', 'setTimeout(()=>{}, 5000)'], process.cwd(), 'nLive'));
|
|
441
|
+
}
|
|
431
442
|
s.jobs['vivo'] = job({ id: 'vivo', executionState: 'running', processRef: ref });
|
|
432
443
|
try {
|
|
433
444
|
const plan = (0, reap_1.planReap)(s);
|
|
@@ -0,0 +1,110 @@
|
|
|
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 checks_1 = require("../../../src/commands/preflight/checks");
|
|
10
|
+
const preflight_1 = require("../../../src/commands/preflight");
|
|
11
|
+
/**
|
|
12
|
+
* RF-3.4 — preflight surfaces a non-blocking (advisory) warning when configured
|
|
13
|
+
* enabled sensors lack effective `inputs`. Opt-out / native-gate must stay quiet.
|
|
14
|
+
*/
|
|
15
|
+
describe('preflight sensors-inputs warning (RF-3.4)', () => {
|
|
16
|
+
const dirs = [];
|
|
17
|
+
const homes = [];
|
|
18
|
+
let previousHome;
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
previousHome = process.env.AWM_HOME;
|
|
21
|
+
const home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-pf-inputs-home-'));
|
|
22
|
+
homes.push(home);
|
|
23
|
+
process.env.AWM_HOME = home;
|
|
24
|
+
fs_1.default.writeFileSync(path_1.default.join(home, 'registries.json'), JSON.stringify([]));
|
|
25
|
+
});
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
if (previousHome === undefined)
|
|
28
|
+
delete process.env.AWM_HOME;
|
|
29
|
+
else
|
|
30
|
+
process.env.AWM_HOME = previousHome;
|
|
31
|
+
});
|
|
32
|
+
afterAll(() => {
|
|
33
|
+
for (const dir of dirs)
|
|
34
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
35
|
+
for (const home of homes)
|
|
36
|
+
fs_1.default.rmSync(home, { recursive: true, force: true });
|
|
37
|
+
});
|
|
38
|
+
function project(opts = {}) {
|
|
39
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-pf-inputs-'));
|
|
40
|
+
dirs.push(dir);
|
|
41
|
+
for (const f of opts.context ?? ['AGENTS.md']) {
|
|
42
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, f), '# ctx\n');
|
|
43
|
+
}
|
|
44
|
+
for (const b of opts.bins ?? ['eslint']) {
|
|
45
|
+
fs_1.default.mkdirSync(path_1.default.join(dir, 'node_modules', '.bin'), { recursive: true });
|
|
46
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'node_modules', '.bin', b), '');
|
|
47
|
+
}
|
|
48
|
+
if (opts.manifest !== undefined) {
|
|
49
|
+
fs_1.default.mkdirSync(path_1.default.join(dir, '.awm'), { recursive: true });
|
|
50
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, '.awm', 'sensors.json'), typeof opts.manifest === 'string' ? opts.manifest : JSON.stringify(opts.manifest));
|
|
51
|
+
}
|
|
52
|
+
return dir;
|
|
53
|
+
}
|
|
54
|
+
function check(report, id) {
|
|
55
|
+
return report.checks.find(c => c.id === id);
|
|
56
|
+
}
|
|
57
|
+
function onlyProjectSensor(extra = {}) {
|
|
58
|
+
return {
|
|
59
|
+
schemaVersion: 3,
|
|
60
|
+
mode: 'project-sensors',
|
|
61
|
+
sensors: {
|
|
62
|
+
custom: {
|
|
63
|
+
source: 'project',
|
|
64
|
+
enabled: true,
|
|
65
|
+
command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
|
|
66
|
+
formatter: 'exit-code',
|
|
67
|
+
...extra,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
it('emits an advisory sensors-inputs warning when an enabled sensor lacks inputs', async () => {
|
|
73
|
+
const dir = project({ manifest: onlyProjectSensor() });
|
|
74
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
75
|
+
expect(report.status).toBe('ready');
|
|
76
|
+
expect((0, preflight_1.exitCodeFor)(report)).toBe(0);
|
|
77
|
+
expect(check(report, 'sensors-inputs')).toMatchObject({
|
|
78
|
+
ok: false,
|
|
79
|
+
advisory: true,
|
|
80
|
+
detail: expect.stringMatching(/custom/i),
|
|
81
|
+
remedy: expect.stringMatching(/inputs|cache/i),
|
|
82
|
+
});
|
|
83
|
+
expect(check(report, 'sensors-inputs').detail).toMatch(/not cacheable|no effective inputs|lacks inputs/i);
|
|
84
|
+
expect((0, preflight_1.formatReport)(report)).toMatch(/⚠.*sensors-inputs/s);
|
|
85
|
+
});
|
|
86
|
+
it('omits sensors-inputs when every enabled sensor has effective inputs', async () => {
|
|
87
|
+
const dir = project({ manifest: onlyProjectSensor({ inputs: ['**/*.ts'] }) });
|
|
88
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
89
|
+
expect(report.status).toBe('ready');
|
|
90
|
+
expect(check(report, 'sensors-inputs')).toBeUndefined();
|
|
91
|
+
});
|
|
92
|
+
it('does not warn for disabled sensors that lack inputs', async () => {
|
|
93
|
+
const dir = project({
|
|
94
|
+
manifest: onlyProjectSensor({ enabled: false }),
|
|
95
|
+
});
|
|
96
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
97
|
+
expect(check(report, 'sensors-inputs')).toBeUndefined();
|
|
98
|
+
});
|
|
99
|
+
it.each([
|
|
100
|
+
['native-gate', 'CI is authoritative.', 'native_gate'],
|
|
101
|
+
['opt-out', 'No local gate.', 'ungated'],
|
|
102
|
+
])('emits no sensors-inputs warning for v3 %s', async (mode, reason, status) => {
|
|
103
|
+
const dir = project({
|
|
104
|
+
manifest: { schemaVersion: 3, mode, reason },
|
|
105
|
+
});
|
|
106
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
107
|
+
expect(report.status).toBe(status);
|
|
108
|
+
expect(check(report, 'sensors-inputs')).toBeUndefined();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
// #238 S2 / SG-7 — soft (add/list) vs hard (sync/update/init/registry add) sync failure UX.
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const registry_sync_report_1 = require("../../src/core/registry-sync-report");
|
|
11
|
+
describe('registry-sync-report pure helpers (#238 S2)', () => {
|
|
12
|
+
it('formatClassifiedRegistryFailure includes name, remote, class, fatal, remedy — never URL userinfo', () => {
|
|
13
|
+
const token = 'ghp_TESTSECRET_DO_NOT_ECHO';
|
|
14
|
+
const text = (0, registry_sync_report_1.formatClassifiedRegistryFailure)({
|
|
15
|
+
name: 'baseline',
|
|
16
|
+
remote: `https://${token}@example.test/baseline.git`,
|
|
17
|
+
err: new Error(`fatal: could not read Username for 'https://${token}@example.test': terminal prompts disabled`),
|
|
18
|
+
});
|
|
19
|
+
expect(text).toContain('baseline');
|
|
20
|
+
expect(text).toContain('example.test/baseline.git');
|
|
21
|
+
expect(text).toMatch(/authentication/i);
|
|
22
|
+
expect(text).toMatch(/terminal prompts disabled/i);
|
|
23
|
+
expect(text).toMatch(/credential\.helper/);
|
|
24
|
+
expect(text).not.toContain(token);
|
|
25
|
+
expect(text).not.toMatch(/ghp_/);
|
|
26
|
+
});
|
|
27
|
+
it('formatRegistrySyncSoftFailure states cached version plus SG-6 cause/remedy', () => {
|
|
28
|
+
const classified = (0, registry_sync_report_1.formatClassifiedRegistryFailure)({
|
|
29
|
+
name: 'docs',
|
|
30
|
+
remote: 'https://example.test/docs.git',
|
|
31
|
+
err: new Error('fatal: unable to access: Could not resolve host: example.test'),
|
|
32
|
+
});
|
|
33
|
+
const soft = (0, registry_sync_report_1.formatRegistrySyncSoftFailure)({
|
|
34
|
+
name: 'docs',
|
|
35
|
+
cachedVersion: '1.4.0',
|
|
36
|
+
classifiedMessage: classified,
|
|
37
|
+
});
|
|
38
|
+
expect(soft).toMatch(/cached version v1\.4\.0/i);
|
|
39
|
+
expect(soft).toContain('docs');
|
|
40
|
+
expect(soft).toMatch(/network/i);
|
|
41
|
+
expect(soft).toMatch(/Could not resolve host/i);
|
|
42
|
+
expect(soft).toMatch(/remedy:/);
|
|
43
|
+
// B3: soft line must not say "sync failed" twice before the classified clause.
|
|
44
|
+
expect(soft.match(/sync failed/gi)?.length ?? 0).toBe(1);
|
|
45
|
+
});
|
|
46
|
+
it('formatRegistrySyncSoftFailure uses unknown when cachedVersion is null', () => {
|
|
47
|
+
const soft = (0, registry_sync_report_1.formatRegistrySyncSoftFailure)({
|
|
48
|
+
name: 'docs',
|
|
49
|
+
cachedVersion: null,
|
|
50
|
+
classifiedMessage: 'registry docs sync failed (class=network); remedy: check connectivity',
|
|
51
|
+
});
|
|
52
|
+
expect(soft).toMatch(/cached version unknown/i);
|
|
53
|
+
});
|
|
54
|
+
it('hardFailOnAnyRegistrySyncError is true when any result is action:error', () => {
|
|
55
|
+
const ok = [{ name: 'baseline', action: 'pulled', version: 'v1.0.0' }];
|
|
56
|
+
const mixed = [
|
|
57
|
+
{ name: 'baseline', action: 'pulled', version: 'v1.0.0' },
|
|
58
|
+
{ name: 'docs', action: 'error', error: 'network unreachable' },
|
|
59
|
+
];
|
|
60
|
+
expect((0, registry_sync_report_1.hardFailOnAnyRegistrySyncError)(ok)).toBe(false);
|
|
61
|
+
expect((0, registry_sync_report_1.hardFailOnAnyRegistrySyncError)(mixed)).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
it('formatSoftRegistrySyncFailures emits only usable errored registries with cached version', async () => {
|
|
64
|
+
const results = [
|
|
65
|
+
{ name: 'ok', action: 'pulled', version: 'v1.0.0' },
|
|
66
|
+
{ name: 'stale', action: 'error', error: 'registry stale sync failed (class=network); remedy: check connectivity' },
|
|
67
|
+
{ name: 'gone', action: 'error', error: 'registry gone sync failed (class=not-found); remedy: verify the remote URL' },
|
|
68
|
+
];
|
|
69
|
+
const lines = await (0, registry_sync_report_1.formatSoftRegistrySyncFailures)(results, {
|
|
70
|
+
isUsable: (name) => name === 'stale',
|
|
71
|
+
cachedVersion: (name) => (name === 'stale' ? '2.0.0' : null),
|
|
72
|
+
});
|
|
73
|
+
expect(lines).toHaveLength(1);
|
|
74
|
+
expect(lines[0]).toMatch(/stale/);
|
|
75
|
+
expect(lines[0]).toMatch(/cached version v2\.0\.0/i);
|
|
76
|
+
expect(lines[0]).toMatch(/class=network/);
|
|
77
|
+
expect(lines.join('\n')).not.toMatch(/\bgone\b/);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe('awm sync / update hard-exit on sync error (#238 S2 / SG-7)', () => {
|
|
81
|
+
const realHome = process.env.HOME;
|
|
82
|
+
const realAwmHome = process.env.AWM_HOME;
|
|
83
|
+
let tmpHome;
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-sync-honesty-'));
|
|
86
|
+
process.env.HOME = tmpHome;
|
|
87
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
88
|
+
fs_1.default.mkdirSync(process.env.AWM_HOME, { recursive: true });
|
|
89
|
+
jest.resetModules();
|
|
90
|
+
jest.doMock('@clack/prompts', () => ({
|
|
91
|
+
confirm: jest.fn(),
|
|
92
|
+
isCancel: jest.fn(),
|
|
93
|
+
intro: jest.fn(),
|
|
94
|
+
outro: jest.fn(),
|
|
95
|
+
spinner: jest.fn(() => ({ start: jest.fn(), stop: jest.fn() })),
|
|
96
|
+
}));
|
|
97
|
+
});
|
|
98
|
+
afterEach(() => {
|
|
99
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
100
|
+
if (realHome === undefined)
|
|
101
|
+
delete process.env.HOME;
|
|
102
|
+
else
|
|
103
|
+
process.env.HOME = realHome;
|
|
104
|
+
if (realAwmHome === undefined)
|
|
105
|
+
delete process.env.AWM_HOME;
|
|
106
|
+
else
|
|
107
|
+
process.env.AWM_HOME = realAwmHome;
|
|
108
|
+
jest.resetModules();
|
|
109
|
+
jest.dontMock('@clack/prompts');
|
|
110
|
+
});
|
|
111
|
+
function writePrefs() {
|
|
112
|
+
const { savePreferences } = require('../../src/utils/config');
|
|
113
|
+
savePreferences({
|
|
114
|
+
defaultAgent: 'claude-code',
|
|
115
|
+
enabledAgents: ['claude-code'],
|
|
116
|
+
installMethod: 'symlink',
|
|
117
|
+
defaultScope: 'local',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function seedUsableDocsRegistry() {
|
|
121
|
+
const registriesDir = path_1.default.join(process.env.AWM_HOME, 'registries');
|
|
122
|
+
fs_1.default.mkdirSync(path_1.default.join(registriesDir, 'docs', 'skills'), { recursive: true });
|
|
123
|
+
fs_1.default.writeFileSync(path_1.default.join(registriesDir, 'docs', 'skills', 'x.md'), '# x\n');
|
|
124
|
+
fs_1.default.writeFileSync(path_1.default.join(process.env.AWM_HOME, 'registries.json'), JSON.stringify([{ name: 'docs', remote: 'https://example.test/docs.git' }]));
|
|
125
|
+
}
|
|
126
|
+
it('runSyncCore returns code !== 0 when any registry result is action:error (even if usable on disk)', async () => {
|
|
127
|
+
writePrefs();
|
|
128
|
+
seedUsableDocsRegistry();
|
|
129
|
+
const project = path_1.default.join(tmpHome, 'proj');
|
|
130
|
+
fs_1.default.mkdirSync(path_1.default.join(project, '.awm'), { recursive: true });
|
|
131
|
+
fs_1.default.writeFileSync(path_1.default.join(project, '.awm', 'profile.json'), JSON.stringify({ version: 1, extensions: [], agents: ['claude-code'] }));
|
|
132
|
+
const reconcile = jest.fn(() => []);
|
|
133
|
+
const { runSyncCore } = require('../../src/commands/sync');
|
|
134
|
+
const result = await runSyncCore({ cwd: project }, {
|
|
135
|
+
syncRegistries: async () => [
|
|
136
|
+
{ name: 'docs', action: 'error', error: 'network unreachable' },
|
|
137
|
+
],
|
|
138
|
+
verifyMinCliVersions: () => [],
|
|
139
|
+
verifyProjectPins: async () => [],
|
|
140
|
+
syncProfile: () => ({ extensions: [], installed: [], skipped: [], transactionIds: [] }),
|
|
141
|
+
reconcileProjectSkillLinks: reconcile,
|
|
142
|
+
});
|
|
143
|
+
expect(result.code).not.toBe(0);
|
|
144
|
+
// A1: later stages still run when usable content remains.
|
|
145
|
+
expect(reconcile).toHaveBeenCalled();
|
|
146
|
+
});
|
|
147
|
+
it('runUpdateCore returns code !== 0 when a registry fails even with usable on-disk content', async () => {
|
|
148
|
+
writePrefs();
|
|
149
|
+
seedUsableDocsRegistry();
|
|
150
|
+
const regenerate = jest.fn(() => []);
|
|
151
|
+
const planReconciliation = jest.fn(() => ({ operations: [], records: [], reports: [] }));
|
|
152
|
+
const applyInstallPlan = jest.fn(() => ({ installed: [], skipped: [], transactionId: 'tx', modifiedFiles: [] }));
|
|
153
|
+
const offerSelfUpdate = jest.fn(async () => { });
|
|
154
|
+
const { runUpdateCore, updateOutro } = require('../../src/commands/update');
|
|
155
|
+
const result = await runUpdateCore({}, {
|
|
156
|
+
syncRegistries: async () => [
|
|
157
|
+
{ name: 'docs', action: 'error', error: 'network unreachable' },
|
|
158
|
+
],
|
|
159
|
+
verifyMinCliVersions: () => [],
|
|
160
|
+
regenerateGlobalContext: regenerate,
|
|
161
|
+
planReconciliation,
|
|
162
|
+
applyInstallPlan,
|
|
163
|
+
resyncInstalledHooks: () => [],
|
|
164
|
+
offerSelfUpdate,
|
|
165
|
+
});
|
|
166
|
+
expect(result.code).not.toBe(0);
|
|
167
|
+
expect(result.registries.failed).toEqual(['docs']);
|
|
168
|
+
// A1: pipeline continues after sync error when content is usable.
|
|
169
|
+
expect(regenerate).toHaveBeenCalled();
|
|
170
|
+
expect(planReconciliation).toHaveBeenCalled();
|
|
171
|
+
expect(applyInstallPlan).toHaveBeenCalled();
|
|
172
|
+
expect(offerSelfUpdate).toHaveBeenCalled();
|
|
173
|
+
expect(updateOutro(result)).toMatch(/Finished with registry sync errors|Update failed/i);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -302,4 +302,64 @@ describe('sensor pack v2 contract', () => {
|
|
|
302
302
|
])('rejects malformed pack %j', (input, message) => {
|
|
303
303
|
expect(() => (0, contract_1.parseSensorPack)(input, 'pack.json')).toThrow(message);
|
|
304
304
|
});
|
|
305
|
+
describe('inputs and sensor-level closureOnly (RF-1.1, RF-1.2, RF-1.5, RF-1.7)', () => {
|
|
306
|
+
it('accepts inputs on variants and closureOnly on the sensor (RF-1.2 pack defaults)', () => {
|
|
307
|
+
const pack = validPack();
|
|
308
|
+
pack.sensors.lint.closureOnly = true;
|
|
309
|
+
pack.sensors.lint.variants[0].inputs = ['**/*.ts', '**/*.[cm]js'];
|
|
310
|
+
expect((0, contract_1.parseSensorPack)(pack, 'pack.json')).toMatchObject({
|
|
311
|
+
kind: 'v2',
|
|
312
|
+
pack: {
|
|
313
|
+
sensors: {
|
|
314
|
+
lint: {
|
|
315
|
+
closureOnly: true,
|
|
316
|
+
variants: [{ inputs: ['**/*.ts', '**/*.[cm]js'] }],
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
it('treats omitted sensor closureOnly as false and does not invent variant inputs', () => {
|
|
323
|
+
const parsed = (0, contract_1.parseSensorPack)(validPack(), 'pack.json');
|
|
324
|
+
expect(parsed).toMatchObject({ kind: 'v2' });
|
|
325
|
+
if (parsed.kind !== 'v2')
|
|
326
|
+
throw new Error('expected v2 pack');
|
|
327
|
+
const lint = parsed.pack.sensors.lint;
|
|
328
|
+
expect(lint.closureOnly ?? false).toBe(false);
|
|
329
|
+
expect('inputs' in lint.variants[0]).toBe(false);
|
|
330
|
+
});
|
|
331
|
+
it('rejects closureOnly on a variant (RF-1.7 sensor-level only)', () => {
|
|
332
|
+
const pack = validPack();
|
|
333
|
+
pack.sensors.lint.variants[0].closureOnly = true;
|
|
334
|
+
expect(() => (0, contract_1.parseSensorPack)(pack, 'pack.json')).toThrow(/unknown field "closureOnly"/);
|
|
335
|
+
});
|
|
336
|
+
it('rejects inputs on the sensor object (inputs belong on variants)', () => {
|
|
337
|
+
const pack = validPack();
|
|
338
|
+
pack.sensors.lint.inputs = ['**/*.ts'];
|
|
339
|
+
expect(() => (0, contract_1.parseSensorPack)(pack, 'pack.json')).toThrow(/unknown field "inputs"/);
|
|
340
|
+
});
|
|
341
|
+
it.each([
|
|
342
|
+
[[''], /nonempty single-line/],
|
|
343
|
+
[['/abs'], /absolute paths are invalid/],
|
|
344
|
+
[['has\u0000nul'], /NUL or control/],
|
|
345
|
+
[['../escape'], /contained relative glob/],
|
|
346
|
+
[['src/{a,b}.ts'], /brace expansion/],
|
|
347
|
+
[['!negated.ts'], /negation/],
|
|
348
|
+
[[], /must be a nonempty array/],
|
|
349
|
+
[['@(a|b).ts'], /extglob|parentheses/],
|
|
350
|
+
[['+(a).ts'], /extglob|parentheses/],
|
|
351
|
+
[['*(a).ts'], /extglob|parentheses/],
|
|
352
|
+
[['?(a).ts'], /extglob|parentheses/],
|
|
353
|
+
[['(a|b).ts'], /extglob|parentheses/],
|
|
354
|
+
])('rejects invalid variant inputs %j', (inputs, reason) => {
|
|
355
|
+
const pack = validPack();
|
|
356
|
+
pack.sensors.lint.variants[0].inputs = inputs;
|
|
357
|
+
expect(() => (0, contract_1.parseSensorPack)(pack, 'pack.json')).toThrow(reason);
|
|
358
|
+
});
|
|
359
|
+
it('rejects non-boolean sensor closureOnly', () => {
|
|
360
|
+
const pack = validPack();
|
|
361
|
+
pack.sensors.lint.closureOnly = 1;
|
|
362
|
+
expect(() => (0, contract_1.parseSensorPack)(pack, 'pack.json')).toThrow(/closureOnly must be a boolean/);
|
|
363
|
+
});
|
|
364
|
+
});
|
|
305
365
|
});
|
|
@@ -396,4 +396,98 @@ describe('sensor manifest contract', () => {
|
|
|
396
396
|
expect(soft.invalidEntries.some(entry => /not valid JSON|valid JSON/i.test(entry.reason))).toBe(false);
|
|
397
397
|
});
|
|
398
398
|
});
|
|
399
|
+
describe('inputs and sensor-level closureOnly (RF-1.1, RF-1.3, RF-1.5, RF-1.7, RF-1.8)', () => {
|
|
400
|
+
it('accepts inputs and closureOnly on pack-bound and project-declared entries', () => {
|
|
401
|
+
const packBound = {
|
|
402
|
+
...validV2Manifest().sensors.lint,
|
|
403
|
+
inputs: ['**/*.ts', 'src/**/?est.ts'],
|
|
404
|
+
closureOnly: true,
|
|
405
|
+
};
|
|
406
|
+
const project = projectSensor({
|
|
407
|
+
inputs: ['lib/**/*.js'],
|
|
408
|
+
closureOnly: false,
|
|
409
|
+
});
|
|
410
|
+
const parsed = (0, manifest_1.parseSensorManifest)({
|
|
411
|
+
schemaVersion: 3,
|
|
412
|
+
mode: 'project-sensors',
|
|
413
|
+
pack: 'js-ts',
|
|
414
|
+
source: { registry: 'baseline' },
|
|
415
|
+
sensors: { lint: packBound, custom: project },
|
|
416
|
+
}, 'sensors.json');
|
|
417
|
+
expect(parsed).toMatchObject({
|
|
418
|
+
kind: 'v3',
|
|
419
|
+
pack: {
|
|
420
|
+
sensors: {
|
|
421
|
+
lint: { inputs: ['**/*.ts', 'src/**/?est.ts'], closureOnly: true },
|
|
422
|
+
custom: { inputs: ['lib/**/*.js'], closureOnly: false },
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
it('accepts inputs and closureOnly on v2 pack-bound sensors', () => {
|
|
428
|
+
const manifest = validV2Manifest();
|
|
429
|
+
manifest.sensors.lint.inputs = ['**/*.[jt]s'];
|
|
430
|
+
manifest.sensors.lint.closureOnly = true;
|
|
431
|
+
expect((0, manifest_1.parseSensorManifest)(manifest, 'sensors.json')).toMatchObject({
|
|
432
|
+
kind: 'v2',
|
|
433
|
+
pack: { sensors: { lint: { inputs: ['**/*.[jt]s'], closureOnly: true } } },
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
it('treats omitted closureOnly as false and does not invent inputs (RF-1.8)', () => {
|
|
437
|
+
const parsed = (0, manifest_1.parseSensorManifest)(v3WithPackAndProject(), 'sensors.json');
|
|
438
|
+
expect(parsed).toMatchObject({ kind: 'v3' });
|
|
439
|
+
if (parsed.kind !== 'v3' || parsed.pack.mode !== 'project-sensors')
|
|
440
|
+
throw new Error('expected v3 project-sensors');
|
|
441
|
+
const lint = parsed.pack.sensors.lint;
|
|
442
|
+
const custom = parsed.pack.sensors.custom;
|
|
443
|
+
expect(lint.closureOnly ?? false).toBe(false);
|
|
444
|
+
expect(custom.closureOnly ?? false).toBe(false);
|
|
445
|
+
expect('inputs' in lint).toBe(false);
|
|
446
|
+
expect('inputs' in custom).toBe(false);
|
|
447
|
+
});
|
|
448
|
+
it.each([
|
|
449
|
+
[[''], /sensors\.custom\.inputs\[0\] must be a nonempty single-line/],
|
|
450
|
+
[['/abs/path'], /sensors\.custom\.inputs\[0\].*absolute paths are invalid/],
|
|
451
|
+
[['C:/windows'], /sensors\.custom\.inputs\[0\].*absolute paths are invalid/],
|
|
452
|
+
[['has\u0000nul'], /sensors\.custom\.inputs\[0\].*NUL or control/],
|
|
453
|
+
[['../escape'], /sensors\.custom\.inputs\[0\].*contained relative glob/],
|
|
454
|
+
[['foo/../bar'], /sensors\.custom\.inputs\[0\].*contained relative glob/],
|
|
455
|
+
[['a{b,c}'], /sensors\.custom\.inputs\[0\].*brace expansion/],
|
|
456
|
+
[['!negated'], /sensors\.custom\.inputs\[0\].*negation/],
|
|
457
|
+
[['path/with!bang'], /sensors\.custom\.inputs\[0\].*negation/],
|
|
458
|
+
[[], /sensors\.custom\.inputs must be a nonempty array/],
|
|
459
|
+
[['@(a|b).ts'], /sensors\.custom\.inputs\[0\].*(?:extglob|parentheses)/],
|
|
460
|
+
[['+(a).ts'], /sensors\.custom\.inputs\[0\].*(?:extglob|parentheses)/],
|
|
461
|
+
[['*(a).ts'], /sensors\.custom\.inputs\[0\].*(?:extglob|parentheses)/],
|
|
462
|
+
[['?(a).ts'], /sensors\.custom\.inputs\[0\].*(?:extglob|parentheses)/],
|
|
463
|
+
[['(a|b).ts'], /sensors\.custom\.inputs\[0\].*(?:extglob|parentheses)/],
|
|
464
|
+
])('rejects invalid project inputs %j', (inputs, reason) => {
|
|
465
|
+
expect(() => (0, manifest_1.parseSensorManifest)(v3WithPackAndProject({ inputs }), 'sensors.json'))
|
|
466
|
+
.toThrow(reason);
|
|
467
|
+
});
|
|
468
|
+
it('soft-isolates a project entry with bad inputs while keeping siblings', () => {
|
|
469
|
+
const soft = (0, manifest_1.parseSensorManifestWithIssues)(v3WithPackAndProject({ inputs: ['**/{a,b}.ts'] }), 'sensors.json');
|
|
470
|
+
expect(soft.invalidEntries).toEqual([
|
|
471
|
+
expect.objectContaining({
|
|
472
|
+
name: 'custom',
|
|
473
|
+
reason: expect.stringMatching(/sensors\.custom\.inputs\[0\].*brace expansion/),
|
|
474
|
+
}),
|
|
475
|
+
]);
|
|
476
|
+
expect(soft.invalidEntries[0]?.reason).not.toMatch(/Invalid sensor pack/);
|
|
477
|
+
expect(soft).toMatchObject({
|
|
478
|
+
kind: 'v3',
|
|
479
|
+
pack: { sensors: { lint: { variantId: 'eslint-9' } } },
|
|
480
|
+
});
|
|
481
|
+
expect('custom' in soft.pack.sensors).toBe(false);
|
|
482
|
+
});
|
|
483
|
+
it('rejects closureOnly on a non-boolean sensor field', () => {
|
|
484
|
+
const soft = (0, manifest_1.parseSensorManifestWithIssues)(v3WithPackAndProject({ closureOnly: 'yes' }), 'sensors.json');
|
|
485
|
+
expect(soft.invalidEntries).toEqual([
|
|
486
|
+
expect.objectContaining({
|
|
487
|
+
name: 'custom',
|
|
488
|
+
reason: expect.stringMatching(/sensors\.custom\.closureOnly must be a boolean/),
|
|
489
|
+
}),
|
|
490
|
+
]);
|
|
491
|
+
});
|
|
492
|
+
});
|
|
399
493
|
});
|
|
@@ -44,6 +44,7 @@ const exec_1 = require("../../../src/commands/sensors/exec");
|
|
|
44
44
|
const result_1 = require("../../../src/commands/sensors/result");
|
|
45
45
|
const baseline_1 = require("../../../src/commands/sensors/baseline");
|
|
46
46
|
const prepare_1 = require("../../../src/commands/sensors/prepare");
|
|
47
|
+
const fixture_cleanup_1 = require("../../helpers/fixture-cleanup");
|
|
47
48
|
const sensor = (overrides = {}) => ({
|
|
48
49
|
name: 'lint',
|
|
49
50
|
command: { kind: 'legacy', value: 'node -e "setTimeout(() => {}, 1000)"' },
|
|
@@ -401,7 +402,8 @@ describe('runStructuredCommand — Windows local native binaries', () => {
|
|
|
401
402
|
delete process.env.PATHEXT;
|
|
402
403
|
else
|
|
403
404
|
process.env.PATHEXT = savedPathExt;
|
|
404
|
-
|
|
405
|
+
// Windows keeps a just-executed copy of node locked for a moment (EBUSY on unlink).
|
|
406
|
+
(0, fixture_cleanup_1.removeFixtureTree)(dir);
|
|
405
407
|
});
|
|
406
408
|
it('executes a contained native executable with literal shell-sensitive argv', async () => {
|
|
407
409
|
const executable = path_1.default.join(dir, 'node_modules', '.bin', 'fixture.exe');
|