@aviaratech/ai-delivery 0.3.4 → 0.3.6

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.
Files changed (55) hide show
  1. package/README.md +135 -20
  2. package/dist/agent.d.ts +2 -0
  3. package/dist/agent.js +1 -0
  4. package/dist/agent.js.map +1 -1
  5. package/dist/cli.js +77 -2
  6. package/dist/cli.js.map +1 -1
  7. package/dist/dispatch.js +39 -1
  8. package/dist/dispatch.js.map +1 -1
  9. package/dist/github/client.d.ts +1 -0
  10. package/dist/github/client.js +4 -1
  11. package/dist/github/client.js.map +1 -1
  12. package/dist/github/commandIdentityPolicy.js +3 -0
  13. package/dist/github/commandIdentityPolicy.js.map +1 -1
  14. package/dist/issue.d.ts +1 -0
  15. package/dist/issue.js +24 -10
  16. package/dist/issue.js.map +1 -1
  17. package/dist/lifecycle.test.js +496 -30
  18. package/dist/lifecycle.test.js.map +1 -1
  19. package/dist/mcp/index.js +1 -1
  20. package/dist/mcp/tools.d.ts +63 -0
  21. package/dist/mcp/tools.js +66 -0
  22. package/dist/mcp/tools.js.map +1 -1
  23. package/dist/pr.d.ts +4 -4
  24. package/dist/pr.js +10 -6
  25. package/dist/pr.js.map +1 -1
  26. package/dist/review.test.js +163 -0
  27. package/dist/review.test.js.map +1 -1
  28. package/dist/services/deliveryAdmission.d.ts +13 -2
  29. package/dist/services/deliveryAdmission.js +41 -2
  30. package/dist/services/deliveryAdmission.js.map +1 -1
  31. package/dist/services/worktreeRegistry.d.ts +12 -0
  32. package/dist/services/worktreeRegistry.js +146 -4
  33. package/dist/services/worktreeRegistry.js.map +1 -1
  34. package/dist/setup.d.ts +58 -0
  35. package/dist/setup.js +415 -0
  36. package/dist/setup.js.map +1 -0
  37. package/dist/setup.test.d.ts +1 -0
  38. package/dist/setup.test.js +744 -0
  39. package/dist/setup.test.js.map +1 -0
  40. package/dist/verification.d.ts +37 -0
  41. package/dist/verification.js +214 -15
  42. package/dist/verification.js.map +1 -1
  43. package/dist/worktree.d.ts +3 -0
  44. package/dist/worktree.js +38 -8
  45. package/dist/worktree.js.map +1 -1
  46. package/dist/worktreeTransition.d.ts +173 -0
  47. package/dist/worktreeTransition.js +1012 -0
  48. package/dist/worktreeTransition.js.map +1 -0
  49. package/dist/worktreeTransition.test.d.ts +1 -0
  50. package/dist/worktreeTransition.test.js +1179 -0
  51. package/dist/worktreeTransition.test.js.map +1 -0
  52. package/package.json +1 -1
  53. package/plugins/ai-delivery/.claude-plugin/plugin.json +1 -1
  54. package/plugins/ai-delivery/README.md +1 -1
  55. package/plugins/ai-delivery/skills/worktree-lifecycle/SKILL.md +9 -0
@@ -0,0 +1,744 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFileSync, spawn, spawnSync } from 'node:child_process';
3
+ import { chmodSync, existsSync, linkSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, utimesSync, writeFileSync, } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { dirname, join } from 'node:path';
6
+ import { test, vi } from 'vitest';
7
+ import { loadDeliveryConfig } from './config/deliveryConfig.js';
8
+ import * as privateFiles from './delivery/common.js';
9
+ import { digestBytes } from './delivery/index.js';
10
+ import { syntheticDiscoveryClients, syntheticOverrides, syntheticDiscoveryConfig } from './fixtures/discovery.js';
11
+ import { assertDeliveryRuntimeAdmitted } from './services/deliveryAdmission.js';
12
+ import * as atomicJson from './utils/atomicJson.js';
13
+ const syncFailure = vi.hoisted(() => ({ directory: '' }));
14
+ vi.mock('node:fs', async (original) => {
15
+ const actual = await original();
16
+ return {
17
+ ...actual,
18
+ fsyncSync: (fd) => {
19
+ if (syncFailure.directory &&
20
+ actual.existsSync(syncFailure.directory) &&
21
+ actual.fstatSync(fd).ino === actual.statSync(syncFailure.directory).ino)
22
+ throw new Error('Synthetic directory durability unavailable');
23
+ actual.fsyncSync(fd);
24
+ },
25
+ };
26
+ });
27
+ const actors = vi.hoisted(() => ({
28
+ author: 'host-user',
29
+ reviewer: 'reviewer[bot]',
30
+ access: true,
31
+ projectId: 'PROJECT-1',
32
+ }));
33
+ // Only GitHub HTTP/authentication is synthetic. Resolver, process ownership and file operations are real.
34
+ vi.mock('./github/client.js', async (original) => ({
35
+ ...(await original()),
36
+ createDeliveryGitHubClients: async (input) => ({
37
+ ...syntheticDiscoveryClients(),
38
+ graphql: async (query, variables) => {
39
+ const result = await syntheticDiscoveryClients().graphql(query, variables);
40
+ if (query.includes('ProjectDeliveryConfiguration'))
41
+ result.organization.projectV2.id = actors.projectId;
42
+ return result;
43
+ },
44
+ authSource: input.role === 'author' ? 'personal' : 'app',
45
+ credentialSource: input.role === 'author' ? 'env:AUTHOR_TOKEN' : 'app:reviewer',
46
+ role: input.role,
47
+ authenticatedAuthor: async () => ({ actorLogin: actors.author, credentialIdentity: 'user:37' }),
48
+ appActorLogin: async () => actors.reviewer,
49
+ rest: {
50
+ repos: {
51
+ get: async () => {
52
+ if (!actors.access)
53
+ throw new Error('Synthetic reviewer access denied');
54
+ return { data: { full_name: 'example/widget' } };
55
+ },
56
+ },
57
+ request: async () => {
58
+ throw new Error('Synthetic rules visibility unavailable');
59
+ },
60
+ },
61
+ }),
62
+ }));
63
+ async function producer() {
64
+ const modulePath = new URL('./setup.js', import.meta.url).href;
65
+ const result = (await import(modulePath).catch(() => ({})));
66
+ assert.equal(typeof result.stageRuntime, 'function', 'supported producer stage API is missing');
67
+ assert.equal(typeof result.admitRuntime, 'function', 'supported producer admission API is missing');
68
+ return result;
69
+ }
70
+ function git(root, ...args) {
71
+ return execFileSync('git', args, { cwd: root, encoding: 'utf8' }).trim();
72
+ }
73
+ async function fixture() {
74
+ actors.author = 'host-user';
75
+ actors.reviewer = 'reviewer[bot]';
76
+ actors.access = true;
77
+ actors.projectId = 'PROJECT-1';
78
+ const base = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-setup-')));
79
+ const root = join(base, 'consumer');
80
+ mkdirSync(root);
81
+ git(root, 'init', '-q', '-b', 'main');
82
+ git(root, 'config', 'user.name', 'Synthetic Delivery');
83
+ git(root, 'config', 'user.email', 'delivery@example.test');
84
+ git(root, 'remote', 'add', 'origin', 'https://github.com/example/widget.git');
85
+ writeFileSync(join(root, 'ai-delivery.config.json'), JSON.stringify(syntheticOverrides(syntheticDiscoveryConfig)));
86
+ const roles = {
87
+ author: { authSource: 'personal', identity: 'host-author', credentialEnv: { token: 'AUTHOR_TOKEN' } },
88
+ reviewer: {
89
+ identity: 'synthetic-reviewer',
90
+ credentialEnv: {
91
+ appId: 'REVIEWER_APP_ID',
92
+ installationId: 'REVIEWER_INSTALLATION_ID',
93
+ privateKeyPath: 'REVIEWER_KEY_PATH',
94
+ },
95
+ },
96
+ };
97
+ writeFileSync(join(root, 'policy.mjs'), `export const deliverySettings=${JSON.stringify({ roles, commandPolicy: { checks: { format: 'REQUIRED', gitClean: 'REQUIRED', lint: 'REQUIRED', test: 'REQUIRED', typecheck: 'REQUIRED' }, timeoutsMs: { lint: 60000, test: 60000, typecheck: 60000 } } })};\n`);
98
+ git(root, 'add', '.');
99
+ git(root, 'commit', '-qm', 'Synthetic consumer');
100
+ git(root, 'update-ref', 'refs/remotes/origin/main', 'HEAD');
101
+ const packageRoot = join(base, 'archive', 'package');
102
+ const files = {
103
+ 'package.json': JSON.stringify({
104
+ name: '@aviaratech/ai-delivery',
105
+ version: '0.3.5',
106
+ type: 'module',
107
+ bin: { 'ai-delivery': './dist/cli.js' },
108
+ }),
109
+ 'dist/cli.js': "#!/usr/bin/env node\nconsole.log('0.3.5');\n",
110
+ 'dist/agent.js': 'export const proof = 2;\n',
111
+ 'plugins/ai-delivery/dist/mcp-launcher.js': '/* Synthetic launcher bytes */\n',
112
+ 'plugins/ai-delivery/.claude-plugin/plugin.json': JSON.stringify({
113
+ name: 'ai-delivery',
114
+ packageVersion: '0.3.5',
115
+ deliveryCapabilityVersion: 2,
116
+ }),
117
+ };
118
+ for (const [relative, bytes] of Object.entries(files)) {
119
+ const path = join(packageRoot, relative);
120
+ mkdirSync(dirname(path), { recursive: true });
121
+ writeFileSync(path, bytes);
122
+ }
123
+ const archivePath = join(base, 'reviewed archive; inert.tgz');
124
+ execFileSync('tar', ['-czf', archivePath, '-C', dirname(packageRoot), 'package']);
125
+ const bin = join(base, 'bin');
126
+ mkdirSync(bin);
127
+ const counter = join(base, 'installs');
128
+ const marker = join(base, 'installer-running');
129
+ // Synthetic installer boundary: the real owned command copies fixture bytes; no process manager is mocked.
130
+ writeFileSync(join(bin, 'npm'), `#!${process.execPath}\nconst fs=require('fs'),cp=require('child_process'),path=require('path');\nconst args=process.argv.slice(2);\nif(args[0]!=='install'||!['--omit=dev','--ignore-scripts','--no-audit','--no-fund'].every(x=>args.includes(x)))process.exit(9);\nfs.appendFileSync(${JSON.stringify(counter)},'x');\nfs.writeFileSync(${JSON.stringify(marker)},String(process.pid));\nconst prefix=args[args.indexOf('--prefix')+1],archive=args.at(-1);\nfs.writeFileSync(${JSON.stringify(join(base, 'installer-archive'))},archive);\nconst original=${JSON.stringify(archivePath)},replacement=${JSON.stringify(join(base, 'replacement.tgz'))};\nconst saved=fs.existsSync(replacement)?fs.readFileSync(original):undefined;\nif(saved)fs.copyFileSync(replacement,original);\nconst target=path.join(prefix,'node_modules/@aviaratech/ai-delivery');fs.mkdirSync(target,{recursive:true});\ntry{cp.execFileSync('tar',['-xzf',archive,'--strip-components=1','-C',target]);}finally{if(saved)fs.writeFileSync(original,saved);}\nfs.mkdirSync(path.join(prefix,'node_modules/.bin'),{recursive:true});\nfs.symlinkSync('../@aviaratech/ai-delivery/dist/cli.js',path.join(prefix,'node_modules/.bin/ai-delivery'));\nif(fs.existsSync(${JSON.stringify(join(base, 'fail'))}))process.exit(7);\nif(fs.existsSync(${JSON.stringify(join(base, 'wait'))}))setInterval(()=>{},1000);\n`);
131
+ chmodSync(join(bin, 'npm'), 0o755);
132
+ const previousPath = process.env.PATH;
133
+ process.env.PATH = `${bin}:${previousPath ?? ''}`;
134
+ const input = {
135
+ authority: 'runtime:stage',
136
+ identity: 'host-author',
137
+ repoRoot: root,
138
+ archivePath,
139
+ expectedArchiveSha256: digestBytes(readFileSync(archivePath)),
140
+ packageVersion: '0.3.5',
141
+ expectedSourceCommit: git(root, 'rev-parse', 'HEAD'),
142
+ expectedConfigDigest: (await loadDeliveryConfig(root)).configDigest,
143
+ runtimeDirectory: join(base, 'staged'),
144
+ };
145
+ return {
146
+ base,
147
+ root,
148
+ input,
149
+ counter,
150
+ marker,
151
+ cleanup: () => {
152
+ process.env.PATH = previousPath;
153
+ rmSync(base, { recursive: true, force: true });
154
+ },
155
+ };
156
+ }
157
+ function admitInput(input, stageId, prior = null) {
158
+ const { archivePath: _archive, expectedArchiveSha256: _digest, packageVersion: _version, ...rest } = input;
159
+ return { ...rest, authority: 'runtime:admit', stageId, expectedPriorAdmissionSha256: prior };
160
+ }
161
+ test('public producer stages exact bytes, explicitly admits and reuses completed work without installing again', async () => {
162
+ const api = await producer();
163
+ const f = await fixture();
164
+ try {
165
+ const staged = await api.stageRuntime(f.input);
166
+ const admissionPath = join(f.root, '.git/ai-delivery/runtime-admission.json');
167
+ assert.equal(existsSync(admissionPath), false);
168
+ assert.equal(staged.admission.sourceCommit, f.input.expectedSourceCommit);
169
+ assert.equal(staged.admission.sourceArchiveSha256, f.input.expectedArchiveSha256);
170
+ const admitted = await api.admitRuntime(admitInput(f.input, staged.stageId));
171
+ assert.equal((await assertDeliveryRuntimeAdmitted({ repoRoot: f.root, runtimeEntryPath: staged.admission.cliPath }))
172
+ .admissionId, admitted.admission.admissionId);
173
+ assert.equal((await api.stageRuntime(f.input)).reused, true);
174
+ assert.equal((await api.admitRuntime(admitInput(f.input, staged.stageId))).reused, true);
175
+ assert.equal(readFileSync(f.counter, 'utf8'), 'x');
176
+ }
177
+ finally {
178
+ f.cleanup();
179
+ }
180
+ });
181
+ test('archive replacement and restoration during install cannot admit alternate bytes under the reviewed digest', async () => {
182
+ const api = await producer();
183
+ const f = await fixture();
184
+ try {
185
+ const reviewed = readFileSync(f.input.archivePath);
186
+ const cli = join(f.base, 'archive/package/dist/cli.js');
187
+ const expectedCli = readFileSync(cli);
188
+ writeFileSync(cli, "#!/usr/bin/env node\nconsole.log('same version, alternate archive bytes');\n");
189
+ execFileSync('tar', ['-czf', join(f.base, 'replacement.tgz'), '-C', join(f.base, 'archive'), 'package']);
190
+ assert.notEqual(digestBytes(readFileSync(join(f.base, 'replacement.tgz'))), f.input.expectedArchiveSha256);
191
+ // The separate installer process replaces the public pathname before extraction and restores A afterward.
192
+ const staged = await api.stageRuntime(f.input);
193
+ const admitted = await api.admitRuntime(admitInput(f.input, staged.stageId));
194
+ assert.ok(readFileSync(f.input.archivePath).equals(reviewed), 'the original archive was restored before revalidation');
195
+ assert.ok(readFileSync(admitted.admission.cliPath).equals(expectedCli), 'alternate archive B was installed and admitted under reviewed archive A digest');
196
+ assert.equal(admitted.admission.sourceArchiveSha256, digestBytes(reviewed));
197
+ assert.equal(admitted.admission.cliSha256, digestBytes(expectedCli));
198
+ const snapshot = join(f.input.runtimeDirectory, 'reviewed-archive.tgz');
199
+ assert.equal(readFileSync(join(f.base, 'installer-archive'), 'utf8'), snapshot);
200
+ assert.ok(readFileSync(snapshot).equals(reviewed));
201
+ const metadata = lstatSync(snapshot);
202
+ assert.ok(metadata.isFile() && !metadata.isSymbolicLink());
203
+ assert.equal(metadata.mode & 0o777, 0o600);
204
+ assert.equal(metadata.nlink, 1);
205
+ assert.equal((await api.stageRuntime(f.input)).reused, true);
206
+ assert.equal(readFileSync(f.counter, 'utf8'), 'x');
207
+ console.log('ARCHIVE_SUBSTITUTION_RECEIPT', JSON.stringify({
208
+ stageId: staged.stageId,
209
+ archiveSha256: digestBytes(reviewed),
210
+ installedCliSha256: digestBytes(expectedCli),
211
+ originalRestored: true,
212
+ privateSnapshotInstalled: true,
213
+ completeRetrySkipped: true,
214
+ }));
215
+ }
216
+ finally {
217
+ f.cleanup();
218
+ }
219
+ });
220
+ for (const corruption of ['missing', 'corrupt', 'symlink', 'hardlink']) {
221
+ test(`completed snapshot ${corruption} rejects reuse and admission while preserving current admission`, async () => {
222
+ const api = await producer();
223
+ const f = await fixture();
224
+ try {
225
+ const staged = await api.stageRuntime(f.input);
226
+ await api.admitRuntime(admitInput(f.input, staged.stageId));
227
+ const admissionPath = join(f.root, '.git/ai-delivery/runtime-admission.json');
228
+ const prior = readFileSync(admissionPath);
229
+ const snapshot = join(f.input.runtimeDirectory, 'reviewed-archive.tgz');
230
+ assert.ok(existsSync(snapshot), 'completed stage must retain its verified archive snapshot');
231
+ if (corruption === 'corrupt')
232
+ writeFileSync(snapshot, 'alternate bytes');
233
+ else {
234
+ rmSync(snapshot);
235
+ if (corruption === 'symlink')
236
+ symlinkSync(f.input.archivePath, snapshot);
237
+ if (corruption === 'hardlink')
238
+ linkSync(f.input.archivePath, snapshot);
239
+ }
240
+ await assert.rejects(api.stageRuntime(f.input));
241
+ await assert.rejects(api.admitRuntime(admitInput(f.input, staged.stageId)));
242
+ assert.ok(readFileSync(admissionPath).equals(prior));
243
+ assert.equal(existsSync(f.input.runtimeDirectory), true);
244
+ assert.equal(readFileSync(f.counter, 'utf8'), 'x');
245
+ }
246
+ finally {
247
+ f.cleanup();
248
+ }
249
+ });
250
+ }
251
+ test('archive snapshot consumes the bounded stage output allowance before running npm', async () => {
252
+ const api = await producer();
253
+ const f = await fixture();
254
+ try {
255
+ await assert.rejects(api.stageRuntime({
256
+ ...f.input,
257
+ resourceBounds: {
258
+ maxAggregateRssBytes: 1024 ** 3,
259
+ minFreeDiskBytes: 256 * 1024 ** 2,
260
+ maxNewOutputBytes: readFileSync(f.input.archivePath).length - 1,
261
+ },
262
+ }), /output.*limit|allowance/u);
263
+ assert.equal(existsSync(f.counter), false);
264
+ assert.equal(existsSync(f.input.runtimeDirectory), false);
265
+ assert.equal(digestBytes(readFileSync(f.input.archivePath)), f.input.expectedArchiveSha256);
266
+ }
267
+ finally {
268
+ f.cleanup();
269
+ }
270
+ });
271
+ test('snapshot durability failure removes the owned partial copy and preserves the original and unrelated files', async () => {
272
+ const api = await producer();
273
+ const f = await fixture();
274
+ const actual = privateFiles.writeCreateOnly;
275
+ const snapshot = join(f.input.runtimeDirectory, 'reviewed-archive.tgz');
276
+ const unrelated = join(f.base, 'unrelated');
277
+ try {
278
+ writeFileSync(unrelated, 'keep');
279
+ const write = vi.spyOn(privateFiles, 'writeCreateOnly').mockImplementation((path, bytes, digest) => {
280
+ if (path === snapshot)
281
+ syncFailure.directory = dirname(path);
282
+ try {
283
+ return actual(path, bytes, digest);
284
+ }
285
+ finally {
286
+ syncFailure.directory = '';
287
+ }
288
+ });
289
+ await assert.rejects(api.stageRuntime(f.input), /durability unavailable/u);
290
+ write.mockRestore();
291
+ assert.equal(existsSync(f.counter), false);
292
+ assert.equal(existsSync(f.input.runtimeDirectory), false);
293
+ assert.equal(digestBytes(readFileSync(f.input.archivePath)), f.input.expectedArchiveSha256);
294
+ assert.equal(readFileSync(unrelated, 'utf8'), 'keep');
295
+ const stage = await api.stageRuntime(f.input);
296
+ assert.equal(stage.reused, false);
297
+ assert.equal(readFileSync(f.counter, 'utf8'), 'x');
298
+ }
299
+ finally {
300
+ syncFailure.directory = '';
301
+ vi.restoreAllMocks();
302
+ f.cleanup();
303
+ }
304
+ });
305
+ for (const denied of [
306
+ 'authority',
307
+ 'archive',
308
+ 'version',
309
+ 'source',
310
+ 'config',
311
+ 'identity',
312
+ 'same-actor',
313
+ 'reviewer-access',
314
+ 'unowned',
315
+ ]) {
316
+ test(`stage rejects ${denied} before adopting or publishing a runtime`, async () => {
317
+ const api = await producer();
318
+ const f = await fixture();
319
+ try {
320
+ const input = { ...f.input };
321
+ if (denied === 'authority')
322
+ input.authority = '';
323
+ if (denied === 'archive')
324
+ writeFileSync(input.archivePath, 'corrupt archive');
325
+ if (denied === 'version')
326
+ input.packageVersion = '0.3.4';
327
+ if (denied === 'source')
328
+ input.expectedSourceCommit = 'a'.repeat(40);
329
+ if (denied === 'config')
330
+ input.expectedConfigDigest = `sha256:${'a'.repeat(64)}`;
331
+ if (denied === 'identity')
332
+ input.identity = 'ambient-author';
333
+ if (denied === 'same-actor')
334
+ actors.reviewer = actors.author;
335
+ if (denied === 'reviewer-access')
336
+ actors.access = false;
337
+ if (denied === 'unowned') {
338
+ mkdirSync(input.runtimeDirectory);
339
+ writeFileSync(join(input.runtimeDirectory, 'preserve'), 'unrelated');
340
+ }
341
+ await assert.rejects(api.stageRuntime(input));
342
+ assert.equal(existsSync(join(f.root, '.git/ai-delivery/runtime-admission.json')), false);
343
+ if (denied === 'unowned')
344
+ assert.equal(readFileSync(join(input.runtimeDirectory, 'preserve'), 'utf8'), 'unrelated');
345
+ else
346
+ assert.equal(existsSync(input.runtimeDirectory), false);
347
+ }
348
+ finally {
349
+ f.cleanup();
350
+ }
351
+ });
352
+ }
353
+ test('admission rejects CAS and actual byte or actor drift while preserving the prior record', async () => {
354
+ const api = await producer();
355
+ const f = await fixture();
356
+ try {
357
+ const staged = await api.stageRuntime(f.input);
358
+ const path = join(f.root, '.git/ai-delivery/runtime-admission.json');
359
+ mkdirSync(dirname(path), { recursive: true });
360
+ writeFileSync(path, 'prior admission\n', { mode: 0o600 });
361
+ await assert.rejects(api.admitRuntime(admitInput(f.input, staged.stageId)), /prior|conflict|compare/u);
362
+ const prior = digestBytes(readFileSync(path));
363
+ actors.author = 'changed-host-user';
364
+ await assert.rejects(api.admitRuntime(admitInput(f.input, staged.stageId, prior)), /actor|route/u);
365
+ actors.author = 'host-user';
366
+ writeFileSync(staged.admission.cliPath, 'changed bytes');
367
+ await assert.rejects(api.admitRuntime(admitInput(f.input, staged.stageId, prior)), /source|bytes|admission/u);
368
+ assert.equal(readFileSync(path, 'utf8'), 'prior admission\n');
369
+ }
370
+ finally {
371
+ f.cleanup();
372
+ }
373
+ });
374
+ test('post-rename errors reconcile desired bytes and never overwrite third-party replacement', async () => {
375
+ const api = await producer();
376
+ const f = await fixture();
377
+ try {
378
+ const staged = await api.stageRuntime(f.input);
379
+ const path = join(f.root, '.git/ai-delivery/runtime-admission.json');
380
+ const actual = atomicJson.writePrivateJsonFileAtomically;
381
+ const write = vi.spyOn(atomicJson, 'writePrivateJsonFileAtomically').mockImplementation((target, value) => {
382
+ actual(target, value);
383
+ if (target === path)
384
+ throw new Error('Synthetic response lost after rename');
385
+ });
386
+ assert.equal((await api.admitRuntime(admitInput(f.input, staged.stageId))).admission.admissionId, staged.admission.admissionId);
387
+ write.mockRestore();
388
+ assert.equal((await api.admitRuntime(admitInput(f.input, staged.stageId))).reused, true);
389
+ rmSync(path);
390
+ const replacement = vi.spyOn(atomicJson, 'writePrivateJsonFileAtomically').mockImplementation((target, value) => {
391
+ actual(target, value);
392
+ if (target === path) {
393
+ writeFileSync(path, 'third-party replacement\n');
394
+ throw new Error('Synthetic concurrent replacement');
395
+ }
396
+ });
397
+ await assert.rejects(api.admitRuntime(admitInput(f.input, staged.stageId)), /conflict|replacement/u);
398
+ replacement.mockRestore();
399
+ assert.equal(readFileSync(path, 'utf8'), 'third-party replacement\n');
400
+ }
401
+ finally {
402
+ vi.restoreAllMocks();
403
+ f.cleanup();
404
+ }
405
+ });
406
+ test('failure and cancellation remove only the owned incomplete stage and preserve unrelated output', async () => {
407
+ const api = await producer();
408
+ const f = await fixture();
409
+ const controller = new AbortController();
410
+ try {
411
+ const unrelated = join(f.base, 'unrelated');
412
+ mkdirSync(unrelated);
413
+ writeFileSync(join(unrelated, 'keep'), 'keep');
414
+ writeFileSync(join(f.base, 'fail'), 'go');
415
+ await assert.rejects(api.stageRuntime(f.input), /exit 7/u);
416
+ assert.equal(existsSync(f.input.runtimeDirectory), false);
417
+ rmSync(join(f.base, 'fail'));
418
+ writeFileSync(join(f.base, 'wait'), 'go');
419
+ const running = api.stageRuntime({ ...f.input, signal: controller.signal });
420
+ void running.catch(() => undefined);
421
+ for (let i = 0; i < 100 && !existsSync(join(f.input.runtimeDirectory, 'node_modules/.bin/ai-delivery')); i++)
422
+ await new Promise((resolve) => setTimeout(resolve, 20));
423
+ assert.equal(digestBytes(readFileSync(join(f.input.runtimeDirectory, 'reviewed-archive.tgz'))), f.input.expectedArchiveSha256);
424
+ controller.abort();
425
+ await assert.rejects(running, /cancelled/u);
426
+ assert.equal(existsSync(f.input.runtimeDirectory), false);
427
+ assert.equal(readFileSync(join(unrelated, 'keep'), 'utf8'), 'keep');
428
+ assert.equal(digestBytes(readFileSync(f.input.archivePath)), f.input.expectedArchiveSha256);
429
+ }
430
+ finally {
431
+ controller.abort();
432
+ f.cleanup();
433
+ }
434
+ });
435
+ test('admission reports unknown durability with exact same-stage reconciliation and preserves desired bytes', async () => {
436
+ const api = await producer();
437
+ const f = await fixture();
438
+ try {
439
+ const stage = await api.stageRuntime(f.input);
440
+ const path = join(f.root, '.git/ai-delivery/runtime-admission.json');
441
+ syncFailure.directory = dirname(path);
442
+ await assert.rejects(api.admitRuntime(admitInput(f.input, stage.stageId)), /commit-status-unknown.*same stage/u);
443
+ const desired = readFileSync(path, 'utf8');
444
+ assert.equal(JSON.parse(desired).admissionId, stage.admission.admissionId);
445
+ syncFailure.directory = '';
446
+ assert.equal((await api.admitRuntime(admitInput(f.input, stage.stageId))).reused, true);
447
+ assert.equal(readFileSync(path, 'utf8'), desired);
448
+ }
449
+ finally {
450
+ syncFailure.directory = '';
451
+ f.cleanup();
452
+ }
453
+ });
454
+ for (const denied of [
455
+ 'candidate',
456
+ 'symlink',
457
+ 'capability',
458
+ 'complete-corrupt',
459
+ 'archive-drift',
460
+ 'source-drift',
461
+ 'config-drift',
462
+ ]) {
463
+ test(`setup preserves completed output and admission on ${denied}`, async () => {
464
+ const api = await producer();
465
+ const f = await fixture();
466
+ try {
467
+ if (denied === 'candidate') {
468
+ const candidate = join(f.base, 'candidate');
469
+ git(f.root, 'worktree', 'add', '-qb', 'candidate', candidate);
470
+ await assert.rejects(api.stageRuntime({ ...f.input, repoRoot: candidate }), /primary/u);
471
+ assert.equal(existsSync(f.input.runtimeDirectory), false);
472
+ return;
473
+ }
474
+ if (denied === 'symlink') {
475
+ symlinkSync(f.base, join(f.base, 'alias'));
476
+ await assert.rejects(api.stageRuntime({ ...f.input, runtimeDirectory: join(f.base, 'alias', 'stage') }), /symbolic link/u);
477
+ return;
478
+ }
479
+ if (denied === 'capability') {
480
+ const manifest = join(f.base, 'archive/package/plugins/ai-delivery/.claude-plugin/plugin.json');
481
+ writeFileSync(manifest, JSON.stringify({ name: 'ai-delivery', packageVersion: '0.3.5', deliveryCapabilityVersion: 1 }));
482
+ execFileSync('tar', ['-czf', f.input.archivePath, '-C', join(f.base, 'archive'), 'package']);
483
+ f.input.expectedArchiveSha256 = digestBytes(readFileSync(f.input.archivePath));
484
+ await assert.rejects(api.stageRuntime(f.input), /capability/u);
485
+ assert.equal(existsSync(f.input.runtimeDirectory), false);
486
+ return;
487
+ }
488
+ const stage = await api.stageRuntime(f.input);
489
+ await api.admitRuntime(admitInput(f.input, stage.stageId));
490
+ const path = join(f.root, '.git/ai-delivery/runtime-admission.json');
491
+ const before = readFileSync(path, 'utf8');
492
+ if (denied === 'complete-corrupt')
493
+ writeFileSync(join(f.input.runtimeDirectory, 'runtime-stage.json'), 'corrupt completion');
494
+ if (denied === 'archive-drift')
495
+ writeFileSync(f.input.archivePath, 'changed archive');
496
+ if (denied === 'source-drift')
497
+ git(f.root, 'commit', '--allow-empty', '-qm', 'source drift');
498
+ if (denied === 'config-drift') {
499
+ writeFileSync(join(f.root, 'policy.mjs'), readFileSync(join(f.root, 'policy.mjs'), 'utf8') + 'deliverySettings.commandPolicy.checks.test="SKIP";\n');
500
+ git(f.root, 'add', '.');
501
+ git(f.root, 'commit', '-qm', 'config drift');
502
+ f.input.expectedSourceCommit = git(f.root, 'rev-parse', 'HEAD');
503
+ }
504
+ await assert.rejects(api.admitRuntime(admitInput(f.input, stage.stageId)));
505
+ assert.equal(readFileSync(path, 'utf8'), before);
506
+ assert.equal(existsSync(f.input.runtimeDirectory), true);
507
+ assert.equal(readFileSync(f.counter, 'utf8'), 'x');
508
+ }
509
+ finally {
510
+ f.cleanup();
511
+ }
512
+ });
513
+ }
514
+ test('setup interruption child', { skip: !process.env.AI_DELIVERY_SETUP_INTERRUPT_INPUT }, async () => {
515
+ const api = await producer();
516
+ await api.stageRuntime(JSON.parse(process.env.AI_DELIVERY_SETUP_INTERRUPT_INPUT));
517
+ });
518
+ test('stopped setup owners stay busy and orphan recovery confirms cleanup before retrying the incomplete install', { timeout: 30_000 }, async () => {
519
+ const api = await producer();
520
+ const f = await fixture();
521
+ let child;
522
+ let ownedPid;
523
+ let ownedIdentity;
524
+ let ownerPid;
525
+ const peer = spawn(process.execPath, ['-e', 'setInterval(()=>{},1000)'], { detached: true, stdio: 'ignore' });
526
+ try {
527
+ writeFileSync(join(f.base, 'wait'), 'go');
528
+ child = spawn(process.execPath, [
529
+ join(process.cwd(), 'node_modules/vitest/vitest.mjs'),
530
+ 'run',
531
+ 'dist/setup.test.js',
532
+ '-t',
533
+ '^setup interruption child$',
534
+ ], {
535
+ cwd: process.cwd(),
536
+ stdio: 'ignore',
537
+ env: { ...process.env, AI_DELIVERY_SETUP_INTERRUPT_INPUT: JSON.stringify(f.input) },
538
+ });
539
+ const closed = new Promise((resolve) => child.once('close', resolve));
540
+ const writerPath = join(f.root, '.git/ai-delivery/writers@1', `${(await import('./delivery/index.js')).digestValue(f.root).slice(7)}.json`);
541
+ let record;
542
+ for (let i = 0; i < 200; i++) {
543
+ if (existsSync(writerPath))
544
+ record = JSON.parse(readFileSync(writerPath, 'utf8'));
545
+ if (record?.command.phase === 'running' && existsSync(f.marker))
546
+ break;
547
+ await new Promise((resolve) => setTimeout(resolve, 20));
548
+ }
549
+ assert.equal(record?.command.phase, 'running');
550
+ const snapshot = join(f.input.runtimeDirectory, 'reviewed-archive.tgz');
551
+ assert.equal(digestBytes(readFileSync(snapshot)), f.input.expectedArchiveSha256);
552
+ ownerPid = record.owner.pid;
553
+ ownedPid = record.command.root.pid;
554
+ ownedIdentity = record.command.root.identity;
555
+ process.kill(ownerPid, 'SIGSTOP');
556
+ const stale = new Date(Date.now() - 20_000);
557
+ utimesSync(`${writerPath}.lock`, stale, stale);
558
+ await assert.rejects(api.stageRuntime(f.input), /live verification writer/u);
559
+ assert.equal(readFileSync(f.counter, 'utf8'), 'x');
560
+ process.kill(ownerPid, 'SIGKILL');
561
+ ownerPid = undefined;
562
+ await closed;
563
+ process.kill(ownedPid, 0);
564
+ writeFileSync(snapshot, 'interrupted incomplete snapshot');
565
+ rmSync(join(f.base, 'wait'));
566
+ const recovered = await api.stageRuntime(f.input);
567
+ assert.equal(recovered.reused, false);
568
+ assert.equal(readFileSync(f.counter, 'utf8'), 'xx');
569
+ assert.equal(existsSync(writerPath), false);
570
+ assert.equal(digestBytes(readFileSync(snapshot)), f.input.expectedArchiveSha256);
571
+ const completedSnapshot = lstatSync(snapshot);
572
+ process.kill(peer.pid, 0);
573
+ const state = spawnSync('/bin/ps', ['-p', String(ownedPid), '-o', 'stat='], { encoding: 'utf8' });
574
+ assert.ok(state.status === 1 || state.stdout.trim().startsWith('Z'));
575
+ ownedPid = undefined;
576
+ assert.equal((await api.stageRuntime(f.input)).reused, true);
577
+ assert.equal(readFileSync(f.counter, 'utf8'), 'xx');
578
+ assert.equal(lstatSync(snapshot).ino, completedSnapshot.ino);
579
+ console.log('SETUP_INTERRUPTION_RECEIPT', JSON.stringify({
580
+ completedStageId: recovered.stageId,
581
+ installerExecutions: 2,
582
+ peerPreserved: true,
583
+ ownedCleanupConfirmed: true,
584
+ completedRetrySkipped: true,
585
+ incompleteSnapshotRebuilt: true,
586
+ completedSnapshotReused: true,
587
+ archiveSha256: f.input.expectedArchiveSha256,
588
+ }));
589
+ }
590
+ finally {
591
+ if (ownerPid !== undefined) {
592
+ process.kill(ownerPid, 'SIGCONT');
593
+ process.kill(ownerPid, 'SIGKILL');
594
+ }
595
+ if (ownedPid !== undefined &&
596
+ spawnSync('/bin/ps', ['-p', String(ownedPid), '-o', 'lstart='], { encoding: 'utf8' }).stdout.trim() ===
597
+ ownedIdentity)
598
+ process.kill(ownedPid, 'SIGKILL');
599
+ if (peer.exitCode === null && peer.signalCode === null)
600
+ peer.kill('SIGKILL');
601
+ if (child?.exitCode === null && child.signalCode === null)
602
+ child.kill('SIGKILL');
603
+ f.cleanup();
604
+ }
605
+ });
606
+ test('CLI stage and admit execute the same public producer with explicit authority and prior absence', async () => {
607
+ const f = await fixture();
608
+ const argv = process.argv;
609
+ const exitCode = process.exitCode;
610
+ const stdout = vi.spyOn(process.stdout, 'write');
611
+ try {
612
+ const runCli = async (args) => {
613
+ const begin = stdout.mock.calls.length;
614
+ process.argv = [
615
+ process.execPath,
616
+ join(process.cwd(), 'dist/cli.js'),
617
+ '--repo-root',
618
+ f.root,
619
+ '--identity',
620
+ 'host-author',
621
+ ...args,
622
+ ];
623
+ vi.resetModules();
624
+ await import('./cli.js');
625
+ for (let i = 0; i < 300; i++) {
626
+ const output = stdout.mock.calls
627
+ .slice(begin)
628
+ .map(([bytes]) => String(bytes))
629
+ .find((bytes) => bytes.startsWith('{'));
630
+ if (output)
631
+ return JSON.parse(output);
632
+ await new Promise((resolve) => setTimeout(resolve, 20));
633
+ }
634
+ throw new Error('CLI completion output unavailable');
635
+ };
636
+ const common = [
637
+ '--source-commit',
638
+ f.input.expectedSourceCommit,
639
+ '--config-digest',
640
+ f.input.expectedConfigDigest,
641
+ '--runtime-directory',
642
+ f.input.runtimeDirectory,
643
+ ];
644
+ const staged = await runCli([
645
+ 'runtime:stage',
646
+ '--authorize-stage',
647
+ '--archive',
648
+ f.input.archivePath,
649
+ '--archive-sha256',
650
+ f.input.expectedArchiveSha256,
651
+ '--package-version',
652
+ f.input.packageVersion,
653
+ ...common,
654
+ ]);
655
+ assert.equal(typeof staged.stageId, 'string');
656
+ const admitted = await runCli([
657
+ 'runtime:admit',
658
+ '--authorize-admit',
659
+ '--stage-id',
660
+ String(staged.stageId),
661
+ '--expected-absent',
662
+ ...common,
663
+ ]);
664
+ assert.equal(admitted.admission.sourceCommit, f.input.expectedSourceCommit);
665
+ assert.equal(readFileSync(f.counter, 'utf8'), 'x');
666
+ assert.equal(process.exitCode, exitCode);
667
+ }
668
+ finally {
669
+ stdout.mockRestore();
670
+ process.argv = argv;
671
+ process.exitCode = exitCode;
672
+ f.cleanup();
673
+ }
674
+ }, 15_000);
675
+ test('actual reviewed 0.3.4 archive qualifies with real npm production installation in a temporary synthetic consumer', { skip: !process.env.AI_DELIVERY_REAL_PACKAGE_ARCHIVE, timeout: 0 }, async () => {
676
+ const realPath = process.env.PATH;
677
+ const api = await producer();
678
+ const f = await fixture();
679
+ try {
680
+ process.env.PATH = realPath;
681
+ const input = {
682
+ ...f.input,
683
+ archivePath: process.env.AI_DELIVERY_REAL_PACKAGE_ARCHIVE,
684
+ packageVersion: '0.3.4',
685
+ expectedArchiveSha256: 'sha256:4e5171a4d398dcc486852f08e58f2ef4267765e6f33a34a3ecd5e1ad01830e6d',
686
+ };
687
+ const stage = await api.stageRuntime(input);
688
+ assert.equal(execFileSync(process.execPath, [stage.admission.cliPath, '--version'], { encoding: 'utf8' }).trim(), '0.3.4');
689
+ assert.equal(stage.admission.capability.mcp, 2);
690
+ assert.equal(existsSync(f.counter), false);
691
+ await api.admitRuntime(admitInput(input, stage.stageId));
692
+ const installedValidatorUrl = new URL(`file://${join(dirname(stage.admission.cliPath), 'services/deliveryAdmission.js')}`).href;
693
+ const installed = (await import(installedValidatorUrl));
694
+ const admitted = await installed.assertDeliveryRuntimeAdmitted({
695
+ repoRoot: f.root,
696
+ runtimeEntryPath: stage.admission.cliPath,
697
+ configuration: await loadDeliveryConfig(f.root),
698
+ });
699
+ assert.equal(admitted.admissionId, stage.admission.admissionId);
700
+ assert.equal((await api.stageRuntime(input)).reused, true);
701
+ console.log('REAL_INSTALLER_QUALIFICATION_RECEIPT', JSON.stringify({
702
+ archiveSha256: input.expectedArchiveSha256,
703
+ packageVersion: admitted.packageVersion,
704
+ packageDistSha256: admitted.packageDistSha256,
705
+ cliSha256: admitted.cliSha256,
706
+ pluginManifestSha256: admitted.pluginManifestSha256,
707
+ installedValidator: true,
708
+ installer: 'real npm production dependencies; ignore-scripts',
709
+ httpBoundary: 'synthetic GitHub authentication/discovery',
710
+ temporaryConsumer: true,
711
+ hostActivated: false,
712
+ sharedAdmissionWritten: false,
713
+ }));
714
+ }
715
+ finally {
716
+ f.cleanup();
717
+ }
718
+ });
719
+ test('live routing and reviewer access drift cannot replace an already admitted stage', async () => {
720
+ const api = await producer();
721
+ const f = await fixture();
722
+ try {
723
+ const stage = await api.stageRuntime(f.input);
724
+ await api.admitRuntime(admitInput(f.input, stage.stageId));
725
+ const path = join(f.root, '.git/ai-delivery/runtime-admission.json');
726
+ const before = readFileSync(path);
727
+ actors.projectId = 'PROJECT-2';
728
+ await assert.rejects(api.admitRuntime(admitInput(f.input, stage.stageId)), /config digest/u);
729
+ const changed = { ...f.input, expectedConfigDigest: (await loadDeliveryConfig(f.root)).configDigest };
730
+ await assert.rejects(api.admitRuntime(admitInput(changed, stage.stageId)), /configuration/u);
731
+ actors.projectId = 'PROJECT-1';
732
+ actors.access = false;
733
+ await assert.rejects(api.admitRuntime(admitInput(f.input, stage.stageId)), /cannot read/u);
734
+ actors.access = true;
735
+ await assert.rejects(api.admitRuntime({ ...admitInput(f.input, stage.stageId), authority: '' }), /authority/u);
736
+ await assert.rejects(api.stageRuntime({ ...f.input, packageVersion: '0.3.4' }), /incompatible/u);
737
+ assert.deepEqual(readFileSync(path), before);
738
+ assert.equal(readFileSync(f.counter, 'utf8'), 'x');
739
+ }
740
+ finally {
741
+ f.cleanup();
742
+ }
743
+ });
744
+ //# sourceMappingURL=setup.test.js.map