@aviaratech/ai-delivery 0.3.22 → 0.3.23
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 +32 -6
- package/dist/cli.js +47 -0
- package/dist/cli.js.map +1 -1
- package/dist/delivery/common.d.ts +4 -1
- package/dist/delivery/common.js +49 -6
- package/dist/delivery/common.js.map +1 -1
- package/dist/delivery/stage.d.ts +6 -0
- package/dist/delivery/stage.js +11 -2
- package/dist/delivery/stage.js.map +1 -1
- package/dist/mcp/index.js +2 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/tools.d.ts +1 -0
- package/dist/mcp/tools.js +1 -0
- package/dist/mcp/tools.js.map +1 -1
- package/dist/pluginInstaller.d.ts +51 -0
- package/dist/pluginInstaller.js +774 -0
- package/dist/pluginInstaller.js.map +1 -0
- package/dist/pluginInstaller.test.d.ts +1 -0
- package/dist/pluginInstaller.test.js +543 -0
- package/dist/pluginInstaller.test.js.map +1 -0
- package/dist/pluginPackaging.test.d.ts +1 -0
- package/dist/pluginPackaging.test.js +116 -0
- package/dist/pluginPackaging.test.js.map +1 -0
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +71 -10
- package/dist/setup.js.map +1 -1
- package/dist/setup.test.js +584 -199
- package/dist/setup.test.js.map +1 -1
- package/dist/verification.d.ts +61 -0
- package/dist/verification.js +168 -6
- package/dist/verification.js.map +1 -1
- package/package.json +4 -4
- package/plugins/ai-delivery/.claude-plugin/plugin.json +6 -1
- package/plugins/ai-delivery/LICENSE +21 -0
- package/plugins/ai-delivery/README.md +4 -2
- package/plugins/ai-delivery/dist/mcp-launcher.js +19 -8
- package/plugins/ai-delivery/mcp.json +10 -0
- package/plugins/ai-delivery/plugin.json +10 -0
- package/plugins/ai-delivery/runtime/dist/THIRD-PARTY-NOTICES.md +1010 -0
- package/plugins/ai-delivery/runtime/dist/cli.js +285 -0
- package/plugins/ai-delivery/runtime/package.json +13 -0
package/dist/setup.test.js
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { execFileSync, spawn, spawnSync } from 'node:child_process';
|
|
3
|
-
import { chmodSync, existsSync, linkSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, symlinkSync, utimesSync, writeFileSync, } from 'node:fs';
|
|
3
|
+
import { chmodSync, cpSync, existsSync, truncateSync, linkSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, symlinkSync, utimesSync, writeFileSync, } from 'node:fs';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { dirname, join } from 'node:path';
|
|
6
6
|
import { test, vi } from 'vitest';
|
|
7
7
|
import { loadDeliveryConfig } from './config/deliveryConfig.js';
|
|
8
8
|
import * as privateFiles from './delivery/common.js';
|
|
9
|
-
import { digestBytes } from './delivery/index.js';
|
|
9
|
+
import { digestBytes, digestValue } from './delivery/index.js';
|
|
10
10
|
import { syntheticDiscoveryClients, syntheticOverrides, syntheticDiscoveryConfig } from './fixtures/discovery.js';
|
|
11
11
|
import { assertDeliveryRuntimeAdmitted } from './services/deliveryAdmission.js';
|
|
12
12
|
import * as atomicJson from './utils/atomicJson.js';
|
|
13
13
|
const syncFailure = vi.hoisted(() => ({ directory: '' }));
|
|
14
|
+
const sharedOutputRead = vi.hoisted(() => ({ path: '', unboundedReads: 0 }));
|
|
14
15
|
vi.mock('node:fs', async (original) => {
|
|
15
16
|
const actual = await original();
|
|
16
17
|
return {
|
|
17
18
|
...actual,
|
|
19
|
+
readFileSync: (...args) => {
|
|
20
|
+
if (sharedOutputRead.path && String(args[0]) === sharedOutputRead.path) {
|
|
21
|
+
sharedOutputRead.unboundedReads++;
|
|
22
|
+
throw new Error('Unbounded shared command-output read attempted');
|
|
23
|
+
}
|
|
24
|
+
return actual.readFileSync(...args);
|
|
25
|
+
},
|
|
18
26
|
fsyncSync: (fd) => {
|
|
19
27
|
if (syncFailure.directory &&
|
|
20
28
|
actual.existsSync(syncFailure.directory) &&
|
|
@@ -98,7 +106,7 @@ async function producer() {
|
|
|
98
106
|
function git(root, ...args) {
|
|
99
107
|
return execFileSync('git', args, { cwd: root, encoding: 'utf8' }).trim();
|
|
100
108
|
}
|
|
101
|
-
async function fixture() {
|
|
109
|
+
async function fixture(native = false) {
|
|
102
110
|
actors.author = 'host-user';
|
|
103
111
|
actors.reviewer = 'reviewer[bot]';
|
|
104
112
|
actors.access = true;
|
|
@@ -148,6 +156,11 @@ async function fixture() {
|
|
|
148
156
|
deliveryCapabilityVersion: 2,
|
|
149
157
|
}),
|
|
150
158
|
};
|
|
159
|
+
if (native) {
|
|
160
|
+
files['plugins/ai-delivery/runtime/package.json'] = files['package.json'];
|
|
161
|
+
files['plugins/ai-delivery/runtime/dist/cli.js'] = files['dist/cli.js'];
|
|
162
|
+
files['plugins/ai-delivery/README.md'] = 'Synthetic copied native plugin.\n';
|
|
163
|
+
}
|
|
151
164
|
for (const [relative, bytes] of Object.entries(files)) {
|
|
152
165
|
const path = join(packageRoot, relative);
|
|
153
166
|
mkdirSync(dirname(path), { recursive: true });
|
|
@@ -155,6 +168,9 @@ async function fixture() {
|
|
|
155
168
|
}
|
|
156
169
|
const archivePath = join(base, 'reviewed archive; inert.tgz');
|
|
157
170
|
execFileSync('tar', ['-czf', archivePath, '-C', dirname(packageRoot), 'package']);
|
|
171
|
+
const nativePluginRoot = join(base, 'native-plugin');
|
|
172
|
+
if (native)
|
|
173
|
+
cpSync(join(packageRoot, 'plugins', 'ai-delivery'), nativePluginRoot, { recursive: true });
|
|
158
174
|
const bin = join(base, 'bin');
|
|
159
175
|
mkdirSync(bin);
|
|
160
176
|
const counter = join(base, 'installs');
|
|
@@ -174,6 +190,7 @@ async function fixture() {
|
|
|
174
190
|
expectedSourceCommit: git(root, 'rev-parse', 'HEAD'),
|
|
175
191
|
expectedConfigDigest: (await loadDeliveryConfig(root)).configDigest,
|
|
176
192
|
runtimeDirectory: join(base, 'staged'),
|
|
193
|
+
...(native ? { nativePluginRoot } : {}),
|
|
177
194
|
};
|
|
178
195
|
return {
|
|
179
196
|
base,
|
|
@@ -188,9 +205,69 @@ async function fixture() {
|
|
|
188
205
|
};
|
|
189
206
|
}
|
|
190
207
|
function admitInput(input, stageId, prior = null) {
|
|
191
|
-
const { archivePath: _archive, expectedArchiveSha256: _digest, packageVersion: _version, ...rest } = input;
|
|
208
|
+
const { archivePath: _archive, expectedArchiveSha256: _digest, packageVersion: _version, nativePluginRoot: _native, ...rest } = input;
|
|
192
209
|
return { ...rest, authority: 'runtime:admit', stageId, expectedPriorAdmissionSha256: prior };
|
|
193
210
|
}
|
|
211
|
+
test('native stage binds exact copied runtime paths and bytes, reuses and admits only that completed stage', async () => {
|
|
212
|
+
const f = await fixture(true);
|
|
213
|
+
try {
|
|
214
|
+
const api = await producer();
|
|
215
|
+
const staged = await api.stageRuntime(f.input);
|
|
216
|
+
assert.equal(staged.admission.cliPath, join(f.input.nativePluginRoot, 'runtime', 'dist', 'cli.js'));
|
|
217
|
+
assert.equal(staged.admission.mcpLauncherPath, join(f.input.nativePluginRoot, 'dist', 'mcp-launcher.js'));
|
|
218
|
+
const record = readFileSync(join(f.input.runtimeDirectory, 'runtime-stage.json'));
|
|
219
|
+
assert.equal((await api.stageRuntime(f.input)).reused, true);
|
|
220
|
+
assert.deepEqual(readFileSync(join(f.input.runtimeDirectory, 'runtime-stage.json')), record);
|
|
221
|
+
assert.equal(readFileSync(f.counter, 'utf8'), 'x');
|
|
222
|
+
const other = join(f.base, 'other-native-plugin');
|
|
223
|
+
cpSync(f.input.nativePluginRoot, other, { recursive: true });
|
|
224
|
+
await assert.rejects(api.stageRuntime({ ...f.input, nativePluginRoot: other }), /incompatible setup intent/u);
|
|
225
|
+
const admitted = await api.admitRuntime(admitInput(f.input, staged.stageId));
|
|
226
|
+
assert.equal(admitted.admission.admissionId, staged.admission.admissionId);
|
|
227
|
+
assert.equal((await api.admitRuntime(admitInput(f.input, staged.stageId))).reused, true);
|
|
228
|
+
const readback = await assertDeliveryRuntimeAdmitted({
|
|
229
|
+
repoRoot: f.root,
|
|
230
|
+
runtimeEntryPath: staged.admission.cliPath,
|
|
231
|
+
});
|
|
232
|
+
assert.equal(readback.admissionId, staged.admission.admissionId);
|
|
233
|
+
writeFileSync(join(f.input.nativePluginRoot, 'README.md'), 'Changed native plugin bytes.\n');
|
|
234
|
+
await assert.rejects(api.stageRuntime(f.input), /native plugin.*reviewed/iu);
|
|
235
|
+
await assert.rejects(api.admitRuntime(admitInput(f.input, staged.stageId)), /native plugin.*reviewed/iu);
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
f.cleanup();
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
test('native stage refuses unreviewed cache bytes without completing or mutating the cache', async () => {
|
|
242
|
+
const f = await fixture(true);
|
|
243
|
+
try {
|
|
244
|
+
const api = await producer();
|
|
245
|
+
const target = join(f.input.nativePluginRoot, 'runtime', 'dist', 'cli.js');
|
|
246
|
+
const altered = 'console.log("unreviewed");\n';
|
|
247
|
+
writeFileSync(target, altered);
|
|
248
|
+
await assert.rejects(api.stageRuntime(f.input), /native plugin.*reviewed/iu);
|
|
249
|
+
assert.equal(readFileSync(target, 'utf8'), altered);
|
|
250
|
+
assert.equal(existsSync(join(f.root, '.git', 'ai-delivery', 'runtime-admission.json')), false);
|
|
251
|
+
assert.equal(existsSync(f.input.runtimeDirectory), false);
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
f.cleanup();
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
test('native stage rejects aliased or overlapping cache paths before installing', async () => {
|
|
258
|
+
const f = await fixture(true);
|
|
259
|
+
try {
|
|
260
|
+
const api = await producer();
|
|
261
|
+
const alias = join(f.base, 'native-alias');
|
|
262
|
+
symlinkSync(f.input.nativePluginRoot, alias);
|
|
263
|
+
await assert.rejects(api.stageRuntime({ ...f.input, nativePluginRoot: alias }), /symbolic link/u);
|
|
264
|
+
await assert.rejects(api.stageRuntime({ ...f.input, nativePluginRoot: f.base }), /overlap/iu);
|
|
265
|
+
assert.equal(existsSync(f.counter), false);
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
f.cleanup();
|
|
269
|
+
}
|
|
270
|
+
});
|
|
194
271
|
test('runtime-stage controller memory is bounded before authenticated preflight', async () => {
|
|
195
272
|
const api = await producer();
|
|
196
273
|
const f = await fixture();
|
|
@@ -306,6 +383,137 @@ test('runtime-stage failure logs honor the caller retained-output cap outside th
|
|
|
306
383
|
f.cleanup();
|
|
307
384
|
}
|
|
308
385
|
});
|
|
386
|
+
test('runtime-stage retries share the retained-output allowance and preserve earlier failure evidence', async () => {
|
|
387
|
+
const api = await producer();
|
|
388
|
+
const f = await fixture();
|
|
389
|
+
const logLimit = 1024 ** 2;
|
|
390
|
+
const input = {
|
|
391
|
+
...f.input,
|
|
392
|
+
resourceBounds: {
|
|
393
|
+
maxAggregateRssBytes: 512 * 1024 ** 2,
|
|
394
|
+
minFreeDiskBytes: 1,
|
|
395
|
+
maxNewOutputBytes: 128 * 1024 ** 2,
|
|
396
|
+
maxCapturedOutputBytes: logLimit,
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
const outputDirectory = join(f.root, '.git/ai-delivery/verification@1/command-output');
|
|
400
|
+
try {
|
|
401
|
+
writeFileSync(join(f.base, 'fail'), 'go');
|
|
402
|
+
writeFileSync(join(f.base, 'log-bytes'), String(600 * 1024));
|
|
403
|
+
await assert.rejects(api.stageRuntime(input), /failed/u);
|
|
404
|
+
const prior = readdirSync(outputDirectory).map((name) => ({
|
|
405
|
+
name,
|
|
406
|
+
bytes: readFileSync(join(outputDirectory, name)),
|
|
407
|
+
}));
|
|
408
|
+
writeFileSync(join(f.base, 'log-bytes'), String(700 * 1024));
|
|
409
|
+
await assert.rejects(api.stageRuntime(input), /failed|captured output exceeded/u);
|
|
410
|
+
const total = readdirSync(outputDirectory).reduce((bytes, name) => bytes + lstatSync(join(outputDirectory, name)).size, 0);
|
|
411
|
+
assert.ok(total <= logLimit, `Retained retry logs ${total} exceed total allowance ${logLimit}.`);
|
|
412
|
+
for (const evidence of prior)
|
|
413
|
+
assert.deepEqual(readFileSync(join(outputDirectory, evidence.name)), evidence.bytes);
|
|
414
|
+
assert.equal(readFileSync(f.counter, 'utf8'), 'xx');
|
|
415
|
+
await assert.rejects(api.stageRuntime(input), /retained.*exhausted/u);
|
|
416
|
+
assert.equal(readFileSync(f.counter, 'utf8'), 'xx', 'exhausted allowance must refuse another installer invocation');
|
|
417
|
+
assert.equal(existsSync(f.input.runtimeDirectory), false);
|
|
418
|
+
assert.equal(digestBytes(readFileSync(f.input.archivePath)), f.input.expectedArchiveSha256);
|
|
419
|
+
}
|
|
420
|
+
finally {
|
|
421
|
+
f.cleanup();
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
for (const existing of ['oversized', 'corrupt', 'valid', 'empty']) {
|
|
425
|
+
test(`runtime-stage first publication bounds ${existing} shared output absent from its ledger`, async () => {
|
|
426
|
+
const api = await producer();
|
|
427
|
+
const f = await fixture();
|
|
428
|
+
const bytes = Buffer.alloc(existing === 'empty' ? 0 : 100 * 1024, 65);
|
|
429
|
+
const digest = digestBytes(bytes);
|
|
430
|
+
const directory = join(f.root, '.git/ai-delivery/verification@1/command-output');
|
|
431
|
+
const evidence = join(directory, `${digest.slice(7)}.bin`);
|
|
432
|
+
const ledgerDirectory = join(f.root, '.git/ai-delivery/verification@1/runtime-output', digestValue(f.root).slice(7));
|
|
433
|
+
try {
|
|
434
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
435
|
+
writeFileSync(evidence, existing === 'corrupt' ? Buffer.alloc(bytes.length, 66) : bytes, { mode: 0o600 });
|
|
436
|
+
if (existing === 'oversized')
|
|
437
|
+
truncateSync(evidence, 9 * 1024 ** 2);
|
|
438
|
+
const originalDigest = digestBytes(readFileSync(evidence));
|
|
439
|
+
const originalIdentity = lstatSync(evidence);
|
|
440
|
+
assert.equal(existsSync(ledgerDirectory), false, 'shared evidence must precede this stage ledger');
|
|
441
|
+
writeFileSync(join(f.base, 'fail'), 'go');
|
|
442
|
+
writeFileSync(join(f.base, 'log-bytes'), String(bytes.length));
|
|
443
|
+
sharedOutputRead.path = evidence;
|
|
444
|
+
sharedOutputRead.unboundedReads = 0;
|
|
445
|
+
await assert.rejects(api.stageRuntime(f.input), /exit 7/u);
|
|
446
|
+
assert.equal(sharedOutputRead.unboundedReads, 0, 'publication must bound existing evidence before reading it');
|
|
447
|
+
assert.equal(readFileSync(f.counter, 'utf8'), 'x');
|
|
448
|
+
const ledgerPath = join(ledgerDirectory, readdirSync(ledgerDirectory)[0]);
|
|
449
|
+
const ledger = JSON.parse(readFileSync(ledgerPath, 'utf8'));
|
|
450
|
+
const refused = existing === 'oversized' || existing === 'corrupt';
|
|
451
|
+
assert.deepEqual(ledger.outputs, [{ digest, bytes: bytes.length, status: refused ? 'reserved' : 'complete' }]);
|
|
452
|
+
const retainedLedger = readFileSync(ledgerPath);
|
|
453
|
+
await assert.rejects(api.stageRuntime(f.input), refused ? /corrupt|size.*bound/u : /exit 7/u);
|
|
454
|
+
assert.equal(readFileSync(f.counter, 'utf8'), refused ? 'x' : 'xx');
|
|
455
|
+
assert.equal(sharedOutputRead.unboundedReads, 0);
|
|
456
|
+
assert.deepEqual(readFileSync(ledgerPath), retainedLedger, 'retry must preserve the existing reservation or completed charge');
|
|
457
|
+
sharedOutputRead.path = '';
|
|
458
|
+
assert.equal(digestBytes(readFileSync(evidence)), originalDigest, 'existing shared evidence must remain immutable');
|
|
459
|
+
const after = lstatSync(evidence);
|
|
460
|
+
assert.equal(after.ino, originalIdentity.ino);
|
|
461
|
+
assert.equal(after.size, originalIdentity.size);
|
|
462
|
+
assert.equal(existsSync(f.input.runtimeDirectory), false);
|
|
463
|
+
}
|
|
464
|
+
finally {
|
|
465
|
+
sharedOutputRead.path = '';
|
|
466
|
+
sharedOutputRead.unboundedReads = 0;
|
|
467
|
+
f.cleanup();
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
for (const fault of [
|
|
472
|
+
'corrupt ledger',
|
|
473
|
+
'incompatible controller',
|
|
474
|
+
'missing evidence',
|
|
475
|
+
'corrupt evidence',
|
|
476
|
+
'oversized evidence',
|
|
477
|
+
]) {
|
|
478
|
+
test(`runtime-stage retained-output ${fault} refuses another installer without resetting its allowance`, async () => {
|
|
479
|
+
const api = await producer();
|
|
480
|
+
const f = await fixture();
|
|
481
|
+
try {
|
|
482
|
+
writeFileSync(join(f.base, 'fail'), 'go');
|
|
483
|
+
writeFileSync(join(f.base, 'log-bytes'), String(100 * 1024));
|
|
484
|
+
await assert.rejects(api.stageRuntime(f.input), /exit 7/u);
|
|
485
|
+
const directory = join(f.root, '.git/ai-delivery/verification@1/runtime-output', digestValue(f.root).slice(7));
|
|
486
|
+
const path = join(directory, readdirSync(directory)[0]);
|
|
487
|
+
const ledger = JSON.parse(readFileSync(path, 'utf8'));
|
|
488
|
+
const evidence = join(f.root, '.git/ai-delivery/verification@1/command-output', `${ledger.outputs[0].digest.slice(7)}.bin`);
|
|
489
|
+
if (fault === 'corrupt ledger') {
|
|
490
|
+
ledger.contentDigest = `sha256:${'0'.repeat(64)}`;
|
|
491
|
+
writeFileSync(path, JSON.stringify(ledger));
|
|
492
|
+
}
|
|
493
|
+
else if (fault === 'incompatible controller') {
|
|
494
|
+
ledger.producerDigest = `sha256:${'0'.repeat(64)}`;
|
|
495
|
+
const { contentDigest: _digest, ...content } = ledger;
|
|
496
|
+
ledger.contentDigest = digestValue(content);
|
|
497
|
+
writeFileSync(path, JSON.stringify(ledger));
|
|
498
|
+
}
|
|
499
|
+
else if (fault === 'missing evidence')
|
|
500
|
+
rmSync(evidence);
|
|
501
|
+
else if (fault === 'oversized evidence')
|
|
502
|
+
truncateSync(evidence, 9 * 1024 ** 2);
|
|
503
|
+
else
|
|
504
|
+
writeFileSync(evidence, 'changed immutable output');
|
|
505
|
+
const retained = readFileSync(path);
|
|
506
|
+
await assert.rejects(api.stageRuntime(f.input), /corrupt|incompatible|ENOENT|size.*bound/u);
|
|
507
|
+
assert.equal(readFileSync(f.counter, 'utf8'), 'x');
|
|
508
|
+
assert.deepEqual(readFileSync(path), retained, 'failed integrity check must preserve the charged ledger');
|
|
509
|
+
assert.equal(readdirSync(directory).length, 1);
|
|
510
|
+
assert.equal(existsSync(f.input.runtimeDirectory), false);
|
|
511
|
+
}
|
|
512
|
+
finally {
|
|
513
|
+
f.cleanup();
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
309
517
|
test('runtime-stage explicitly isolates npm cache, logs, configuration and temporary destinations', async () => {
|
|
310
518
|
const api = await producer();
|
|
311
519
|
const f = await fixture();
|
|
@@ -331,6 +539,58 @@ test('runtime-stage explicitly isolates npm cache, logs, configuration and tempo
|
|
|
331
539
|
f.cleanup();
|
|
332
540
|
}
|
|
333
541
|
});
|
|
542
|
+
test('runtime-stage cancellation shares prior failure charges and identical evidence is reused', async () => {
|
|
543
|
+
const api = await producer();
|
|
544
|
+
const f = await fixture();
|
|
545
|
+
const cancellation = new AbortController();
|
|
546
|
+
const input = {
|
|
547
|
+
...f.input,
|
|
548
|
+
resourceBounds: {
|
|
549
|
+
maxAggregateRssBytes: 512 * 1024 ** 2,
|
|
550
|
+
minFreeDiskBytes: 1,
|
|
551
|
+
maxNewOutputBytes: 128 * 1024 ** 2,
|
|
552
|
+
maxCapturedOutputBytes: 1024 ** 2,
|
|
553
|
+
},
|
|
554
|
+
};
|
|
555
|
+
const directory = join(f.root, '.git/ai-delivery/verification@1/command-output');
|
|
556
|
+
const write = process.stderr.write.bind(process.stderr);
|
|
557
|
+
let sawCaptured = false;
|
|
558
|
+
const observation = vi.spyOn(process.stderr, 'write').mockImplementation((chunk, ...args) => {
|
|
559
|
+
const text = String(chunk);
|
|
560
|
+
if (text.startsWith('ai-delivery.verify ')) {
|
|
561
|
+
const value = JSON.parse(text.slice('ai-delivery.verify '.length));
|
|
562
|
+
if (value.capturedOutputBytes === 200 * 1024) {
|
|
563
|
+
sawCaptured = true;
|
|
564
|
+
cancellation.abort();
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return write(chunk, ...args);
|
|
568
|
+
});
|
|
569
|
+
try {
|
|
570
|
+
writeFileSync(join(f.base, 'fail'), 'go');
|
|
571
|
+
writeFileSync(join(f.base, 'log-bytes'), String(100 * 1024));
|
|
572
|
+
await assert.rejects(api.stageRuntime(input), /exit 7/u);
|
|
573
|
+
await assert.rejects(api.stageRuntime(input), /exit 7/u);
|
|
574
|
+
assert.equal(readdirSync(directory).length, 1, 'identical complete command evidence must be reused');
|
|
575
|
+
const prior = readFileSync(join(directory, readdirSync(directory)[0]));
|
|
576
|
+
rmSync(join(f.base, 'fail'));
|
|
577
|
+
writeFileSync(join(f.base, 'wait'), 'go');
|
|
578
|
+
writeFileSync(join(f.base, 'log-bytes'), String(200 * 1024));
|
|
579
|
+
await assert.rejects(api.stageRuntime({ ...input, signal: cancellation.signal }), /cancelled/u);
|
|
580
|
+
assert.equal(sawCaptured, true, 'cancel only after the full owned output is captured');
|
|
581
|
+
assert.ok(readdirSync(directory).some((name) => readFileSync(join(directory, name)).equals(prior)));
|
|
582
|
+
const ledgerRoot = join(f.root, '.git/ai-delivery/verification@1/runtime-output', digestValue(f.root).slice(7));
|
|
583
|
+
const ledger = JSON.parse(readFileSync(join(ledgerRoot, readdirSync(ledgerRoot)[0]), 'utf8'));
|
|
584
|
+
assert.equal(ledger.outputs.reduce((total, output) => total + output.bytes, 0), 300 * 1024);
|
|
585
|
+
assert.equal(readFileSync(f.counter, 'utf8'), 'xxx');
|
|
586
|
+
assert.equal(existsSync(f.input.runtimeDirectory), false);
|
|
587
|
+
}
|
|
588
|
+
finally {
|
|
589
|
+
cancellation.abort();
|
|
590
|
+
observation.mockRestore();
|
|
591
|
+
f.cleanup();
|
|
592
|
+
}
|
|
593
|
+
});
|
|
334
594
|
test('runtime-stage post-install preflight growth cannot publish completion', async () => {
|
|
335
595
|
const api = await producer();
|
|
336
596
|
const f = await fixture();
|
|
@@ -415,87 +675,114 @@ test('runtime-stage changed resource intent preserves compatible completed bytes
|
|
|
415
675
|
f.cleanup();
|
|
416
676
|
}
|
|
417
677
|
});
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
678
|
+
for (const mode of ['success', 'failure', 'cancel']) {
|
|
679
|
+
test(`real npm ${mode} disables file logs and preserves unrelated host destinations`, async () => {
|
|
680
|
+
const api = await producer();
|
|
681
|
+
const realPath = process.env.PATH;
|
|
682
|
+
const f = await fixture();
|
|
683
|
+
const cancellation = new AbortController();
|
|
684
|
+
let operation;
|
|
685
|
+
const previous = {
|
|
686
|
+
cache: process.env.npm_config_cache,
|
|
687
|
+
logs: process.env.npm_config_logs_dir,
|
|
688
|
+
userconfig: process.env.npm_config_userconfig,
|
|
689
|
+
};
|
|
690
|
+
try {
|
|
691
|
+
process.env.PATH = realPath;
|
|
692
|
+
const outside = join(f.base, 'unrelated-host');
|
|
693
|
+
mkdirSync(outside);
|
|
694
|
+
const cache = join(outside, 'cache');
|
|
695
|
+
const logs = join(outside, 'logs');
|
|
696
|
+
for (const path of [cache, logs]) {
|
|
697
|
+
mkdirSync(path);
|
|
698
|
+
writeFileSync(join(path, 'keep'), 'unrelated');
|
|
699
|
+
}
|
|
700
|
+
const userconfig = join(outside, 'user.npmrc');
|
|
701
|
+
const config = `cache=${cache}\nlogs-dir=${logs}\n`;
|
|
702
|
+
writeFileSync(userconfig, config);
|
|
703
|
+
const nativeBin = join(f.base, 'native-bin');
|
|
704
|
+
mkdirSync(nativeBin);
|
|
705
|
+
const probe = join(f.base, 'native-npm-observation.json');
|
|
706
|
+
const npmRoot = join(dirname(process.execPath), '../lib/node_modules/npm');
|
|
707
|
+
writeFileSync(join(nativeBin, 'npm'), `#!${process.execPath}\nconst fs=require('node:fs'),os=require('node:os'),path=require('node:path');\nconst observation={args:process.argv.slice(2),cwd:process.cwd(),tmpdir:os.tmpdir(),npmVersion:require(${JSON.stringify(join(npmRoot, 'package.json'))}).version};\nconst LogFile=require(${JSON.stringify(join(npmRoot, 'lib/utils/log-file.js'))}),load=LogFile.prototype.load;\nconst observe=()=>{const directory=path.join(process.cwd(),'.npm/logs');observation.fileLogs=fs.existsSync(directory)?fs.readdirSync(directory):[];observation.fileLogBytes=observation.fileLogs.reduce((sum,name)=>sum+fs.statSync(path.join(directory,name)).size,0);fs.writeFileSync(${JSON.stringify(probe)},JSON.stringify(observation),{mode:0o600});};\nLogFile.prototype.load=function(options){const result=load.call(this,options);observation.logsMax=options.logsMax;observe();${mode === 'cancel' ? 'setInterval(observe,20);' : ''}return result;};\nprocess.on('exit',observe);\n${mode === 'failure' ? 'process.argv[2]="synthetic-unknown-command";' : ''}\nrequire(${JSON.stringify(join(npmRoot, 'bin/npm-cli.js'))});\n`);
|
|
708
|
+
chmodSync(join(nativeBin, 'npm'), 0o755);
|
|
709
|
+
process.env.PATH = `${nativeBin}:${realPath ?? ''}`;
|
|
710
|
+
process.env.npm_config_cache = cache;
|
|
711
|
+
process.env.npm_config_logs_dir = logs;
|
|
712
|
+
process.env.npm_config_userconfig = userconfig;
|
|
713
|
+
operation = api.stageRuntime({
|
|
714
|
+
...f.input,
|
|
715
|
+
signal: cancellation.signal,
|
|
716
|
+
resourceBounds: {
|
|
717
|
+
maxAggregateRssBytes: 512 * 1024 ** 2,
|
|
718
|
+
minFreeDiskBytes: 1,
|
|
719
|
+
maxNewOutputBytes: 128 * 1024 ** 2,
|
|
720
|
+
maxCapturedOutputBytes: 1024 ** 2,
|
|
721
|
+
},
|
|
722
|
+
});
|
|
723
|
+
void operation.catch(() => undefined);
|
|
724
|
+
if (mode === 'cancel') {
|
|
725
|
+
for (let i = 0; i < 1000 && !existsSync(probe); i++)
|
|
726
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
727
|
+
assert.equal(existsSync(probe), true, 'actual npm log configuration was not observed');
|
|
728
|
+
cancellation.abort();
|
|
729
|
+
await assert.rejects(operation, /cancelled/u);
|
|
730
|
+
}
|
|
731
|
+
else if (mode === 'failure')
|
|
732
|
+
await assert.rejects(operation, /exit 1/u);
|
|
733
|
+
else {
|
|
734
|
+
const stage = await operation;
|
|
735
|
+
assert.equal(existsSync(f.counter), false, 'synthetic npm must not execute');
|
|
736
|
+
assert.equal(execFileSync(process.execPath, [stage.admission.cliPath, '--version'], { encoding: 'utf8' }).trim(), '0.3.5');
|
|
737
|
+
assert.ok(readdirSync(join(f.input.runtimeDirectory, '.npm/cache')).length > 0);
|
|
738
|
+
assert.deepEqual(readdirSync(join(f.input.runtimeDirectory, '.npm/logs')), []);
|
|
739
|
+
}
|
|
740
|
+
const native = JSON.parse(readFileSync(probe, 'utf8'));
|
|
741
|
+
assert.equal(native.npmVersion, '11.19.0');
|
|
742
|
+
assert.equal(native.cwd, f.input.runtimeDirectory);
|
|
743
|
+
assert.equal(native.tmpdir, join(f.input.runtimeDirectory, '.npm/tmp'));
|
|
744
|
+
assert.equal(native.logsMax, 0);
|
|
745
|
+
assert.equal(native.fileLogBytes, 0);
|
|
746
|
+
assert.deepEqual(native.fileLogs, []);
|
|
747
|
+
if (mode !== 'success')
|
|
748
|
+
assert.equal(existsSync(f.input.runtimeDirectory), false);
|
|
749
|
+
for (const option of ['--cache', '--logs-dir', '--userconfig', '--globalconfig']) {
|
|
750
|
+
assert.ok(native.args.includes(option));
|
|
751
|
+
assert.ok(native.args[native.args.indexOf(option) + 1].startsWith(`${f.input.runtimeDirectory}/`));
|
|
752
|
+
}
|
|
753
|
+
assert.deepEqual(readdirSync(cache), ['keep']);
|
|
754
|
+
assert.deepEqual(readdirSync(logs), ['keep']);
|
|
755
|
+
assert.equal(readFileSync(userconfig, 'utf8'), config);
|
|
756
|
+
assert.equal(existsSync(join(f.root, '.git/ai-delivery/runtime-admission.json')), false);
|
|
757
|
+
console.log('REAL_NPM_ISOLATION_RECEIPT', JSON.stringify({
|
|
758
|
+
mode,
|
|
759
|
+
npmFileLogBytes: native.fileLogBytes,
|
|
760
|
+
npmFileLogsDisabled: true,
|
|
761
|
+
temporaryAndConfigInsideStage: true,
|
|
762
|
+
unrelatedHostPreserved: true,
|
|
763
|
+
controllerInclusiveMemoryBytes: 512 * 1024 ** 2,
|
|
764
|
+
retainedLogCapBytes: 1024 ** 2,
|
|
765
|
+
hostActivated: false,
|
|
766
|
+
sharedAdmissionWritten: false,
|
|
767
|
+
}));
|
|
470
768
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
finally {
|
|
486
|
-
for (const [name, value] of [
|
|
487
|
-
['npm_config_cache', previous.cache],
|
|
488
|
-
['npm_config_logs_dir', previous.logs],
|
|
489
|
-
['npm_config_userconfig', previous.userconfig],
|
|
490
|
-
]) {
|
|
491
|
-
if (value === undefined)
|
|
492
|
-
delete process.env[name];
|
|
493
|
-
else
|
|
494
|
-
process.env[name] = value;
|
|
769
|
+
finally {
|
|
770
|
+
cancellation.abort();
|
|
771
|
+
await operation?.catch(() => undefined);
|
|
772
|
+
for (const [name, value] of [
|
|
773
|
+
['npm_config_cache', previous.cache],
|
|
774
|
+
['npm_config_logs_dir', previous.logs],
|
|
775
|
+
['npm_config_userconfig', previous.userconfig],
|
|
776
|
+
]) {
|
|
777
|
+
if (value === undefined)
|
|
778
|
+
delete process.env[name];
|
|
779
|
+
else
|
|
780
|
+
process.env[name] = value;
|
|
781
|
+
}
|
|
782
|
+
f.cleanup();
|
|
495
783
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
});
|
|
784
|
+
});
|
|
785
|
+
}
|
|
499
786
|
test('public producer stages exact bytes, explicitly admits and reuses completed work without installing again', async () => {
|
|
500
787
|
const api = await producer();
|
|
501
788
|
const f = await fixture();
|
|
@@ -741,35 +1028,39 @@ test('post-rename errors reconcile desired bytes and never overwrite third-party
|
|
|
741
1028
|
f.cleanup();
|
|
742
1029
|
}
|
|
743
1030
|
});
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
1031
|
+
for (const native of [false, true]) {
|
|
1032
|
+
test(`${native ? 'native ' : ''}failure and cancellation remove only the owned incomplete stage and preserve unrelated output`, async () => {
|
|
1033
|
+
const api = await producer();
|
|
1034
|
+
const f = await fixture(native);
|
|
1035
|
+
const controller = new AbortController();
|
|
1036
|
+
try {
|
|
1037
|
+
const unrelated = join(f.base, 'unrelated');
|
|
1038
|
+
mkdirSync(unrelated);
|
|
1039
|
+
writeFileSync(join(unrelated, 'keep'), 'keep');
|
|
1040
|
+
writeFileSync(join(f.base, 'fail'), 'go');
|
|
1041
|
+
await assert.rejects(api.stageRuntime(f.input), /exit 7/u);
|
|
1042
|
+
assert.equal(existsSync(f.input.runtimeDirectory), false);
|
|
1043
|
+
rmSync(join(f.base, 'fail'));
|
|
1044
|
+
writeFileSync(join(f.base, 'wait'), 'go');
|
|
1045
|
+
const running = api.stageRuntime({ ...f.input, signal: controller.signal });
|
|
1046
|
+
void running.catch(() => undefined);
|
|
1047
|
+
for (let i = 0; i < 100 && !existsSync(join(f.input.runtimeDirectory, 'node_modules/.bin/ai-delivery')); i++)
|
|
1048
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
1049
|
+
assert.equal(digestBytes(readFileSync(join(f.input.runtimeDirectory, 'reviewed-archive.tgz'))), f.input.expectedArchiveSha256);
|
|
1050
|
+
controller.abort();
|
|
1051
|
+
await assert.rejects(running, /cancelled/u);
|
|
1052
|
+
assert.equal(existsSync(f.input.runtimeDirectory), false);
|
|
1053
|
+
assert.equal(readFileSync(join(unrelated, 'keep'), 'utf8'), 'keep');
|
|
1054
|
+
assert.equal(digestBytes(readFileSync(f.input.archivePath)), f.input.expectedArchiveSha256);
|
|
1055
|
+
if (native)
|
|
1056
|
+
assert.equal(readFileSync(join(f.input.nativePluginRoot, 'README.md'), 'utf8'), 'Synthetic copied native plugin.\n');
|
|
1057
|
+
}
|
|
1058
|
+
finally {
|
|
1059
|
+
controller.abort();
|
|
1060
|
+
f.cleanup();
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
773
1064
|
test('admission reports unknown durability with exact same-stage reconciliation and preserves desired bytes', async () => {
|
|
774
1065
|
const api = await producer();
|
|
775
1066
|
const f = await fixture();
|
|
@@ -851,98 +1142,192 @@ for (const denied of [
|
|
|
851
1142
|
}
|
|
852
1143
|
test('setup interruption child', { skip: !process.env.AI_DELIVERY_SETUP_INTERRUPT_INPUT }, async () => {
|
|
853
1144
|
const api = await producer();
|
|
1145
|
+
const phase = process.env.AI_DELIVERY_SETUP_LOG_INTERRUPT_PHASE;
|
|
1146
|
+
if (phase) {
|
|
1147
|
+
const actual = atomicJson.writePrivateJsonFileAtomically;
|
|
1148
|
+
vi.spyOn(atomicJson, 'writePrivateJsonFileAtomically').mockImplementation((path, value) => {
|
|
1149
|
+
const record = value;
|
|
1150
|
+
const interrupt = path.includes('/runtime-output/') &&
|
|
1151
|
+
record.outputs?.[0]?.status === (phase === 'reservation' ? 'reserved' : 'complete');
|
|
1152
|
+
if (!(interrupt && phase === 'publication'))
|
|
1153
|
+
actual(path, value);
|
|
1154
|
+
if (interrupt) {
|
|
1155
|
+
writeFileSync(process.env.AI_DELIVERY_SETUP_LOG_INTERRUPT_MARKER, JSON.stringify({ phase, path, owner: process.pid }));
|
|
1156
|
+
process.kill(process.pid, 'SIGKILL');
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
854
1160
|
await api.stageRuntime(JSON.parse(process.env.AI_DELIVERY_SETUP_INTERRUPT_INPUT));
|
|
855
1161
|
});
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1162
|
+
for (const phase of ['reservation', 'publication']) {
|
|
1163
|
+
test(`runtime-stage retained-output recovery preserves charges after hard interruption at ${phase}`, async () => {
|
|
1164
|
+
const api = await producer();
|
|
1165
|
+
const f = await fixture();
|
|
1166
|
+
const marker = join(f.base, 'log-interruption-marker');
|
|
1167
|
+
const input = {
|
|
1168
|
+
...f.input,
|
|
1169
|
+
resourceBounds: {
|
|
1170
|
+
maxAggregateRssBytes: 512 * 1024 ** 2,
|
|
1171
|
+
minFreeDiskBytes: 1,
|
|
1172
|
+
maxNewOutputBytes: 128 * 1024 ** 2,
|
|
1173
|
+
maxCapturedOutputBytes: 1024 ** 2,
|
|
1174
|
+
},
|
|
1175
|
+
};
|
|
1176
|
+
let child;
|
|
1177
|
+
try {
|
|
1178
|
+
writeFileSync(join(f.base, 'fail'), 'go');
|
|
1179
|
+
writeFileSync(join(f.base, 'log-bytes'), String(100 * 1024));
|
|
1180
|
+
child = spawn(process.execPath, [
|
|
1181
|
+
join(process.cwd(), 'node_modules/vitest/vitest.mjs'),
|
|
1182
|
+
'run',
|
|
1183
|
+
new URL(import.meta.url).pathname,
|
|
1184
|
+
'-t',
|
|
1185
|
+
'^setup interruption child$',
|
|
1186
|
+
'--maxWorkers=1',
|
|
1187
|
+
'--no-file-parallelism',
|
|
1188
|
+
], {
|
|
1189
|
+
env: {
|
|
1190
|
+
...process.env,
|
|
1191
|
+
AI_DELIVERY_SETUP_INTERRUPT_INPUT: JSON.stringify(input),
|
|
1192
|
+
AI_DELIVERY_SETUP_LOG_INTERRUPT_PHASE: phase,
|
|
1193
|
+
AI_DELIVERY_SETUP_LOG_INTERRUPT_MARKER: marker,
|
|
1194
|
+
},
|
|
1195
|
+
stdio: 'ignore',
|
|
1196
|
+
});
|
|
1197
|
+
await new Promise((resolve) => child.once('close', () => resolve()));
|
|
1198
|
+
assert.equal(existsSync(marker), true, 'hard interruption must occur at the selected durable log boundary');
|
|
1199
|
+
const writerPath = join(f.root, '.git/ai-delivery/writers@1', `${digestValue(f.root).slice(7)}.json`);
|
|
1200
|
+
const stale = new Date(Date.now() - 20_000);
|
|
1201
|
+
utimesSync(`${writerPath}.lock`, stale, stale);
|
|
1202
|
+
const { path } = JSON.parse(readFileSync(marker, 'utf8'));
|
|
1203
|
+
const prior = JSON.parse(readFileSync(path, 'utf8'));
|
|
1204
|
+
assert.equal(prior.outputs[0].status, 'reserved');
|
|
1205
|
+
assert.equal(prior.outputs[0].bytes, 100 * 1024);
|
|
1206
|
+
const outputDirectory = join(f.root, '.git/ai-delivery/verification@1/command-output');
|
|
1207
|
+
const priorOutput = join(outputDirectory, `${prior.outputs[0].digest.slice(7)}.bin`);
|
|
1208
|
+
assert.equal(existsSync(priorOutput), phase === 'publication');
|
|
1209
|
+
const preserved = phase === 'publication' ? readFileSync(priorOutput) : undefined;
|
|
1210
|
+
writeFileSync(join(f.base, 'log-bytes'), String(128 * 1024));
|
|
1211
|
+
await assert.rejects(api.stageRuntime(input), /exit 7/u);
|
|
1212
|
+
const recovered = JSON.parse(readFileSync(path, 'utf8'));
|
|
1213
|
+
assert.equal(recovered.outputs.reduce((total, output) => total + output.bytes, 0), 228 * 1024);
|
|
1214
|
+
assert.equal(recovered.outputs[0].status, phase === 'publication' ? 'complete' : 'reserved');
|
|
1215
|
+
if (preserved)
|
|
1216
|
+
assert.deepEqual(readFileSync(priorOutput), preserved);
|
|
1217
|
+
if (phase === 'reservation') {
|
|
1218
|
+
writeFileSync(join(f.base, 'log-bytes'), String(100 * 1024));
|
|
1219
|
+
await assert.rejects(api.stageRuntime(input), /identical output cannot be rewritten/u);
|
|
1220
|
+
assert.equal(existsSync(priorOutput), false, 'unresolved reservation must not authorize a second raw-log write');
|
|
1221
|
+
}
|
|
1222
|
+
rmSync(join(f.base, 'fail'));
|
|
1223
|
+
rmSync(join(f.base, 'log-bytes'));
|
|
1224
|
+
const completed = await api.stageRuntime(input);
|
|
1225
|
+
const count = readFileSync(f.counter, 'utf8');
|
|
1226
|
+
assert.equal(completed.reused, false);
|
|
1227
|
+
assert.equal((await api.stageRuntime(input)).reused, true);
|
|
1228
|
+
assert.equal(readFileSync(f.counter, 'utf8'), count, 'completed recovered stage must skip the installer');
|
|
1229
|
+
assert.equal(readFileSync(completed.admission.cliPath, 'utf8'), "#!/usr/bin/env node\nconsole.log('0.3.5');\n");
|
|
887
1230
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
process.
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1231
|
+
finally {
|
|
1232
|
+
if (child?.exitCode === null && child.signalCode === null)
|
|
1233
|
+
child.kill('SIGKILL');
|
|
1234
|
+
f.cleanup();
|
|
1235
|
+
}
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
for (const native of [false, true]) {
|
|
1239
|
+
test(`${native ? 'native ' : ''}stopped setup owners stay busy and orphan recovery confirms cleanup before retrying the incomplete install`, async () => {
|
|
1240
|
+
const api = await producer();
|
|
1241
|
+
const f = await fixture(native);
|
|
1242
|
+
let child;
|
|
1243
|
+
let ownedPid;
|
|
1244
|
+
let ownedIdentity;
|
|
1245
|
+
let ownerPid;
|
|
1246
|
+
const peer = spawn(process.execPath, ['-e', 'setInterval(()=>{},1000)'], { detached: true, stdio: 'ignore' });
|
|
1247
|
+
try {
|
|
1248
|
+
writeFileSync(join(f.base, 'wait'), 'go');
|
|
1249
|
+
child = spawn(process.execPath, [
|
|
1250
|
+
join(process.cwd(), 'node_modules/vitest/vitest.mjs'),
|
|
1251
|
+
'run',
|
|
1252
|
+
'dist/setup.test.js',
|
|
1253
|
+
'-t',
|
|
1254
|
+
'^setup interruption child$',
|
|
1255
|
+
], {
|
|
1256
|
+
cwd: process.cwd(),
|
|
1257
|
+
stdio: 'ignore',
|
|
1258
|
+
env: { ...process.env, AI_DELIVERY_SETUP_INTERRUPT_INPUT: JSON.stringify(f.input) },
|
|
1259
|
+
});
|
|
1260
|
+
const closed = new Promise((resolve) => child.once('close', resolve));
|
|
1261
|
+
const writerPath = join(f.root, '.git/ai-delivery/writers@1', `${(await import('./delivery/index.js')).digestValue(f.root).slice(7)}.json`);
|
|
1262
|
+
let record;
|
|
1263
|
+
// Bound this cold installer-start observation, not the recovery run.
|
|
1264
|
+
for (let i = 0; i < 1000; i++) {
|
|
1265
|
+
if (existsSync(writerPath))
|
|
1266
|
+
record = JSON.parse(readFileSync(writerPath, 'utf8'));
|
|
1267
|
+
if (record?.command.phase === 'running' && existsSync(f.marker))
|
|
1268
|
+
break;
|
|
1269
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
1270
|
+
}
|
|
1271
|
+
assert.equal(record?.command.phase, 'running');
|
|
1272
|
+
assert.equal(existsSync(f.marker), true, 'interrupt only after the owned installer actually starts');
|
|
1273
|
+
const snapshot = join(f.input.runtimeDirectory, 'reviewed-archive.tgz');
|
|
1274
|
+
assert.equal(digestBytes(readFileSync(snapshot)), f.input.expectedArchiveSha256);
|
|
1275
|
+
ownerPid = record.owner.pid;
|
|
1276
|
+
ownedPid = record.command.root.pid;
|
|
1277
|
+
ownedIdentity = record.command.root.identity;
|
|
1278
|
+
process.kill(ownerPid, 'SIGSTOP');
|
|
1279
|
+
const stale = new Date(Date.now() - 20_000);
|
|
1280
|
+
utimesSync(`${writerPath}.lock`, stale, stale);
|
|
1281
|
+
await assert.rejects(api.stageRuntime(f.input), /live verification writer/u);
|
|
1282
|
+
assert.equal(readFileSync(f.counter, 'utf8'), 'x');
|
|
933
1283
|
process.kill(ownerPid, 'SIGKILL');
|
|
1284
|
+
ownerPid = undefined;
|
|
1285
|
+
await closed;
|
|
1286
|
+
process.kill(ownedPid, 0);
|
|
1287
|
+
writeFileSync(snapshot, 'interrupted incomplete snapshot');
|
|
1288
|
+
rmSync(join(f.base, 'wait'));
|
|
1289
|
+
const recovered = await api.stageRuntime(f.input);
|
|
1290
|
+
assert.equal(recovered.reused, false);
|
|
1291
|
+
assert.equal(readFileSync(f.counter, 'utf8'), 'xx');
|
|
1292
|
+
assert.equal(existsSync(writerPath), false);
|
|
1293
|
+
assert.equal(digestBytes(readFileSync(snapshot)), f.input.expectedArchiveSha256);
|
|
1294
|
+
const completedSnapshot = lstatSync(snapshot);
|
|
1295
|
+
process.kill(peer.pid, 0);
|
|
1296
|
+
const state = spawnSync('/bin/ps', ['-p', String(ownedPid), '-o', 'stat='], { encoding: 'utf8' });
|
|
1297
|
+
assert.ok(state.status === 1 || state.stdout.trim().startsWith('Z'));
|
|
1298
|
+
ownedPid = undefined;
|
|
1299
|
+
assert.equal((await api.stageRuntime(f.input)).reused, true);
|
|
1300
|
+
assert.equal(readFileSync(f.counter, 'utf8'), 'xx');
|
|
1301
|
+
assert.equal(lstatSync(snapshot).ino, completedSnapshot.ino);
|
|
1302
|
+
console.log('SETUP_INTERRUPTION_RECEIPT', JSON.stringify({
|
|
1303
|
+
completedStageId: recovered.stageId,
|
|
1304
|
+
installerExecutions: 2,
|
|
1305
|
+
peerPreserved: true,
|
|
1306
|
+
ownedCleanupConfirmed: true,
|
|
1307
|
+
completedRetrySkipped: true,
|
|
1308
|
+
incompleteSnapshotRebuilt: true,
|
|
1309
|
+
completedSnapshotReused: true,
|
|
1310
|
+
archiveSha256: f.input.expectedArchiveSha256,
|
|
1311
|
+
nativePluginRoot: f.input.nativePluginRoot ?? null,
|
|
1312
|
+
}));
|
|
934
1313
|
}
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1314
|
+
finally {
|
|
1315
|
+
if (ownerPid !== undefined) {
|
|
1316
|
+
process.kill(ownerPid, 'SIGCONT');
|
|
1317
|
+
process.kill(ownerPid, 'SIGKILL');
|
|
1318
|
+
}
|
|
1319
|
+
if (ownedPid !== undefined &&
|
|
1320
|
+
spawnSync('/bin/ps', ['-p', String(ownedPid), '-o', 'lstart='], { encoding: 'utf8' }).stdout.trim() ===
|
|
1321
|
+
ownedIdentity)
|
|
1322
|
+
process.kill(ownedPid, 'SIGKILL');
|
|
1323
|
+
if (peer.exitCode === null && peer.signalCode === null)
|
|
1324
|
+
peer.kill('SIGKILL');
|
|
1325
|
+
if (child?.exitCode === null && child.signalCode === null)
|
|
1326
|
+
child.kill('SIGKILL');
|
|
1327
|
+
f.cleanup();
|
|
1328
|
+
}
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
946
1331
|
test('CLI stage and admit execute the same public producer with explicit authority and prior absence', async () => {
|
|
947
1332
|
const f = await fixture();
|
|
948
1333
|
const argv = process.argv;
|