@aviaratech/ai-delivery 0.3.11 → 0.3.13
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 +17 -1
- package/dist/issue.js +36 -7
- package/dist/issue.js.map +1 -1
- package/dist/lifecycle.test.js +539 -55
- package/dist/lifecycle.test.js.map +1 -1
- package/dist/pr.js +22 -4
- package/dist/pr.js.map +1 -1
- package/dist/services/worktreeRegistry.d.ts +2 -2
- package/dist/services/worktreeRegistry.js +2 -4
- package/dist/services/worktreeRegistry.js.map +1 -1
- package/dist/verification.js +123 -12
- package/dist/verification.js.map +1 -1
- package/dist/worktree.d.ts +2 -0
- package/dist/worktree.js +18 -9
- package/dist/worktree.js.map +1 -1
- package/dist/worktreeTransition.test.js +5 -5
- package/dist/worktreeTransition.test.js.map +1 -1
- package/package.json +1 -1
- package/plugins/ai-delivery/.claude-plugin/plugin.json +1 -1
package/dist/lifecycle.test.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { execFileSync, spawn, spawnSync } from 'node:child_process';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
+
import { syncBuiltinESMExports } from 'node:module';
|
|
4
5
|
import { cpSync, chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, readdirSync, realpathSync, rmSync, symlinkSync, unlinkSync, utimesSync, writeFileSync, } from 'node:fs';
|
|
5
6
|
import { tmpdir } from 'node:os';
|
|
6
7
|
import { dirname, join } from 'node:path';
|
|
@@ -20,7 +21,7 @@ import * as githubClient from './github/client.js';
|
|
|
20
21
|
import * as atomicJson from './utils/atomicJson.js';
|
|
21
22
|
import { createIssue, developIssue, listIssueSubissues, readyCheck, resumeCreatedIssue, updateIssue, } from './issue.js';
|
|
22
23
|
import { checkoutPr, finishIssue, listPrs, mergePr, prChecks, prInfo, publishPr, submitFormalReview } from './pr.js';
|
|
23
|
-
import { addWorktreeEntry, removeWorktreeEntry, updateIssueWorktreeDelivery } from './services/worktreeRegistry.js';
|
|
24
|
+
import { addWorktreeEntry, assertNativeIssueTrackingAdmission, getIssueWorktreeStrict, removeWorktreeEntry, updateIssueWorktreeDelivery, } from './services/worktreeRegistry.js';
|
|
24
25
|
import { assertDeliveryRuntimeAdmitted } from './services/deliveryAdmission.js';
|
|
25
26
|
import { getDeliveryRecords } from './services/deliveryRecordService.js';
|
|
26
27
|
import { createIssuePhaseEvidence, loadVerifiedRun, verifyIssue } from './verification.js';
|
|
@@ -539,38 +540,13 @@ test('an old active issue remains pinned while a new row uses the same canonical
|
|
|
539
540
|
assert.equal(readFileSync(registryPath, 'utf8'), futureBytes);
|
|
540
541
|
assert.equal(existsSync(join(root, '.worktrees', 'scratch-new')), false);
|
|
541
542
|
writeFileSync(registryPath, JSON.stringify({ worktrees: [old] }));
|
|
542
|
-
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
issues: {
|
|
550
|
-
get: async () => ({ data: { id: 17, number: 17, node_id: 'PARENT-17', state: 'open' } }),
|
|
551
|
-
create: async () => {
|
|
552
|
-
issueWrites += 1;
|
|
553
|
-
return { data: {} };
|
|
554
|
-
},
|
|
555
|
-
update: async () => {
|
|
556
|
-
issueWrites += 1;
|
|
557
|
-
return { data: {} };
|
|
558
|
-
},
|
|
559
|
-
},
|
|
560
|
-
},
|
|
561
|
-
graphql: async () => ({
|
|
562
|
-
repository: {
|
|
563
|
-
issue: {
|
|
564
|
-
parent: { id: 'PARENT-17', number: 17 },
|
|
565
|
-
blockedBy: { nodes: [], pageInfo: { endCursor: null, hasNextPage: false } },
|
|
566
|
-
},
|
|
567
|
-
},
|
|
568
|
-
}),
|
|
569
|
-
},
|
|
570
|
-
};
|
|
571
|
-
await assert.rejects(createIssue(relationshipContext, { title: 'Synthetic child', parentIssueNumber: 17 }), /ownership witness/u);
|
|
572
|
-
await assert.rejects(updateIssue(relationshipContext, { issueNumber: 17, parentIssueNumber: null }), /ownership witness/u);
|
|
573
|
-
assert.equal(issueWrites, 0);
|
|
543
|
+
assert.doesNotThrow(() => assertNativeIssueTrackingAdmission(17, root));
|
|
544
|
+
const original = readFileSync(registryPath, 'utf8');
|
|
545
|
+
writeFileSync(registryPath, JSON.stringify({ worktrees: [old, old] }));
|
|
546
|
+
assert.throws(() => assertNativeIssueTrackingAdmission(17, root), /duplicate/u);
|
|
547
|
+
writeFileSync(registryPath, futureBytes);
|
|
548
|
+
assert.throws(() => assertNativeIssueTrackingAdmission(17, root), /unsupported fields/u);
|
|
549
|
+
writeFileSync(registryPath, original);
|
|
574
550
|
await assert.rejects(prepareIssueWorktree({ identity: 'synthetic-author', issueNumber: 17, repoRoot: root }), /ownership witness/u);
|
|
575
551
|
const row = await prepareStandaloneWorktree({
|
|
576
552
|
branch: 'scratch/new',
|
|
@@ -732,8 +708,7 @@ test.each(['current-blocker', 'new-blocker', 'current-parent', 'new-parent'])('t
|
|
|
732
708
|
await assert.rejects(updateIssue(context, input), /invalid blocked-by relationship evidence/u);
|
|
733
709
|
assert.equal(writes, 0);
|
|
734
710
|
malformedRelationships = false;
|
|
735
|
-
|
|
736
|
-
assert.equal(writes, 0);
|
|
711
|
+
assert.throws(() => getIssueWorktreeStrict(17, root), /ownership witness/u);
|
|
737
712
|
writeFileSync(registryPath, JSON.stringify({ worktrees: [{ ...legacy, schemaVersion: 'unknown@99' }] }));
|
|
738
713
|
await assert.rejects(updateIssue(context, input), /unsupported fields/u);
|
|
739
714
|
assert.equal(writes, 0);
|
|
@@ -3617,7 +3592,10 @@ async function syntheticLifecycle(routing) {
|
|
|
3617
3592
|
},
|
|
3618
3593
|
}),
|
|
3619
3594
|
},
|
|
3620
|
-
repos: {
|
|
3595
|
+
repos: {
|
|
3596
|
+
get: async () => ({ data: { full_name: 'example/widget' } }),
|
|
3597
|
+
getCombinedStatusForRef: async () => ({ data: { statuses: [], state: 'success' } }),
|
|
3598
|
+
},
|
|
3621
3599
|
};
|
|
3622
3600
|
let authenticatedAuthor = 'synthetic-author[bot]';
|
|
3623
3601
|
const context = {
|
|
@@ -3637,7 +3615,71 @@ async function syntheticLifecycle(routing) {
|
|
|
3637
3615
|
},
|
|
3638
3616
|
};
|
|
3639
3617
|
let restoreDispatchClient;
|
|
3618
|
+
const enableDispatch = () => {
|
|
3619
|
+
const dispatchClient = vi.spyOn(githubClient, 'createDeliveryGitHubClients').mockImplementation(async (input) => ({
|
|
3620
|
+
...context.clients,
|
|
3621
|
+
...(input.role === 'reviewer'
|
|
3622
|
+
? {
|
|
3623
|
+
role: 'reviewer',
|
|
3624
|
+
appActorLogin: async () => 'synthetic-reviewer[bot]',
|
|
3625
|
+
}
|
|
3626
|
+
: {}),
|
|
3627
|
+
graphql: (async (query, variables) => query.includes('DeliveryDiscovery') ||
|
|
3628
|
+
query.includes('DeliveryRepository') ||
|
|
3629
|
+
query.includes('ProjectDeliveryConfiguration')
|
|
3630
|
+
? syntheticDiscoveryClients().graphql(query, variables)
|
|
3631
|
+
: graphql(query, variables)),
|
|
3632
|
+
}));
|
|
3633
|
+
restoreDispatchClient = () => dispatchClient.mockRestore();
|
|
3634
|
+
return dispatchClient;
|
|
3635
|
+
};
|
|
3636
|
+
const execution = { repoRoot: root, runtimeEntryPath, identity: 'synthetic-author' };
|
|
3640
3637
|
try {
|
|
3638
|
+
if (routing.legacyNativeMetadata) {
|
|
3639
|
+
enableDispatch();
|
|
3640
|
+
const legacyPath = join(root, '.worktrees', 'issue-17');
|
|
3641
|
+
git(root, 'worktree', 'add', '-b', 'issue/17', legacyPath, 'main');
|
|
3642
|
+
writeFileSync(join(legacyPath, 'artifact.txt'), 'unadopted legacy source\n');
|
|
3643
|
+
const receiptRoot = join(root, '.git', 'legacy-receipts');
|
|
3644
|
+
mkdirSync(receiptRoot);
|
|
3645
|
+
writeFileSync(join(receiptRoot, 'run.json'), '{"producer":"synthetic-legacy"}\n');
|
|
3646
|
+
const registryPath = join(root, '.issue-cli', 'worktrees.json');
|
|
3647
|
+
mkdirSync(dirname(registryPath), { recursive: true });
|
|
3648
|
+
const legacy = {
|
|
3649
|
+
branch: 'issue/17',
|
|
3650
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
3651
|
+
identity: 'synthetic-legacy',
|
|
3652
|
+
issueNumber,
|
|
3653
|
+
path: legacyPath,
|
|
3654
|
+
status: 'active',
|
|
3655
|
+
type: 'issue',
|
|
3656
|
+
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
3657
|
+
};
|
|
3658
|
+
const registryBytes = JSON.stringify({ worktrees: [legacy] });
|
|
3659
|
+
writeFileSync(registryPath, registryBytes);
|
|
3660
|
+
const source = directoryHash(legacyPath);
|
|
3661
|
+
const receipts = directoryHash(receiptRoot);
|
|
3662
|
+
await assert.rejects(executeTool('issue_update', { issueNumber, points: 2 }, {
|
|
3663
|
+
...execution,
|
|
3664
|
+
identity: 'synthetic-reviewer',
|
|
3665
|
+
}), /identity|author|permitted|allowed/u);
|
|
3666
|
+
const updated = (await executeTool('issue_update', {
|
|
3667
|
+
issueNumber,
|
|
3668
|
+
parentIssueNumber: 11,
|
|
3669
|
+
points: 2,
|
|
3670
|
+
priority: 'High',
|
|
3671
|
+
}, execution));
|
|
3672
|
+
assert.equal(updated.points, 2);
|
|
3673
|
+
assert.equal(updated.priority, 'High');
|
|
3674
|
+
assert.equal(parentIssueNumber, 11);
|
|
3675
|
+
assert.equal(parentPoints, undefined);
|
|
3676
|
+
await assert.rejects(executeTool('issue_develop', { issueNumber }, execution), /ownership witness/u);
|
|
3677
|
+
assert.equal(readFileSync(registryPath, 'utf8'), registryBytes);
|
|
3678
|
+
assert.equal(directoryHash(legacyPath), source);
|
|
3679
|
+
assert.equal(directoryHash(receiptRoot), receipts);
|
|
3680
|
+
assert.equal(existsSync(join(gitCommonDir(root), 'ai-delivery/worktree-owners')), false);
|
|
3681
|
+
return;
|
|
3682
|
+
}
|
|
3641
3683
|
if (routing.producerOnboarding) {
|
|
3642
3684
|
assert.equal(existsSync(join(root, 'AGENTS.md')), false);
|
|
3643
3685
|
const archiveBase = join(root, '.git', 'producer-fixture');
|
|
@@ -3941,21 +3983,126 @@ exec "${realGit}" "$@"
|
|
|
3941
3983
|
await assert.rejects(finishIssue(context, { issueNumber, prNumber, strategy: 'merge' }), /exact prepared merge intent/u);
|
|
3942
3984
|
headSha = reviewedHead;
|
|
3943
3985
|
assert.equal(calls.filter((call) => call === 'git:createCommit').length, 1);
|
|
3986
|
+
if (routing.mergedContinuation) {
|
|
3987
|
+
enableDispatch();
|
|
3988
|
+
const terminal = await executeTool('issue_pr_merge', { issueNumber, prNumber, strategy: 'merge' }, execution);
|
|
3989
|
+
assert.equal(terminal.mergeSha, mergeSha);
|
|
3990
|
+
assert.equal(issue.state, 'open');
|
|
3991
|
+
const previous = getIssueWorktreeStrict(issueNumber, root);
|
|
3992
|
+
assert.equal(previous.status, 'merged');
|
|
3993
|
+
const registryPath = join(root, '.issue-cli', 'worktrees.json');
|
|
3994
|
+
const registryBytes = readFileSync(registryPath);
|
|
3995
|
+
const custody = [
|
|
3996
|
+
'runs@2',
|
|
3997
|
+
'publications',
|
|
3998
|
+
'reviews',
|
|
3999
|
+
'merge-intents',
|
|
4000
|
+
'merge-attempts',
|
|
4001
|
+
'merge-results',
|
|
4002
|
+
'merges',
|
|
4003
|
+
];
|
|
4004
|
+
const oldReceipts = [];
|
|
4005
|
+
const saveReceipts = (path) => {
|
|
4006
|
+
if (!existsSync(path))
|
|
4007
|
+
return;
|
|
4008
|
+
for (const entry of readdirSync(path, { withFileTypes: true })) {
|
|
4009
|
+
const child = join(path, entry.name);
|
|
4010
|
+
if (entry.isDirectory())
|
|
4011
|
+
saveReceipts(child);
|
|
4012
|
+
else
|
|
4013
|
+
oldReceipts.push({ path: child, bytes: readFileSync(child) });
|
|
4014
|
+
}
|
|
4015
|
+
};
|
|
4016
|
+
for (const name of custody)
|
|
4017
|
+
saveReceipts(join(gitCommonDir(root), 'ai-delivery', name));
|
|
4018
|
+
const ownerPath = join(gitCommonDir(root), 'ai-delivery/worktree-owners');
|
|
4019
|
+
const ownerHash = directoryHash(ownerPath);
|
|
4020
|
+
const publicationPath = join(gitCommonDir(root), 'ai-delivery/publications', String(issueNumber), `${headSha}.json`);
|
|
4021
|
+
const publicationBytes = readFileSync(publicationPath);
|
|
4022
|
+
const { publicationId: _publicationId, ...publicationContent } = JSON.parse(publicationBytes.toString());
|
|
4023
|
+
for (const stale of [
|
|
4024
|
+
{ issueNumber: 18 },
|
|
4025
|
+
{ headSha: 'c'.repeat(40) },
|
|
4026
|
+
{ baseSha: 'c'.repeat(40) },
|
|
4027
|
+
{ evidenceId: `sha256:${'c'.repeat(64)}` },
|
|
4028
|
+
]) {
|
|
4029
|
+
const content = { ...publicationContent, ...stale };
|
|
4030
|
+
const bytes = Buffer.from(JSON.stringify({ ...content, publicationId: digestValue(content) }));
|
|
4031
|
+
writeFileSync(publicationPath, bytes, { mode: 0o600 });
|
|
4032
|
+
await assert.rejects(executeTool('issue_develop', { issueNumber }, execution), /publication.*(?:verified coordinates|historical approved review)/u);
|
|
4033
|
+
assert.deepEqual(readFileSync(registryPath), registryBytes);
|
|
4034
|
+
assert.deepEqual(readFileSync(publicationPath), bytes);
|
|
4035
|
+
}
|
|
4036
|
+
writeFileSync(publicationPath, publicationBytes, { mode: 0o600 });
|
|
4037
|
+
const reviewPath = join(gitCommonDir(root), 'ai-delivery/reviews', String(issueNumber), `${headSha}.json`);
|
|
4038
|
+
const reviewBytes = readFileSync(reviewPath);
|
|
4039
|
+
const { receiptId: _receiptId, artifact: savedArtifact, ...reviewContent } = JSON.parse(reviewBytes.toString());
|
|
4040
|
+
const { artifactId: _artifactId, ...artifactContent } = savedArtifact;
|
|
4041
|
+
const staleArtifact = { ...artifactContent, diffScopeHash: `sha256:${'c'.repeat(64)}` };
|
|
4042
|
+
const staleReview = { ...reviewContent, artifact: { ...staleArtifact, artifactId: digestValue(staleArtifact) } };
|
|
4043
|
+
const staleReviewBytes = Buffer.from(JSON.stringify({ ...staleReview, receiptId: digestValue(staleReview) }));
|
|
4044
|
+
writeFileSync(reviewPath, staleReviewBytes, { mode: 0o600 });
|
|
4045
|
+
await assert.rejects(executeTool('issue_develop', { issueNumber }, execution), /exact historical approved review/u);
|
|
4046
|
+
assert.deepEqual(readFileSync(registryPath), registryBytes);
|
|
4047
|
+
assert.deepEqual(readFileSync(reviewPath), staleReviewBytes);
|
|
4048
|
+
writeFileSync(reviewPath, reviewBytes, { mode: 0o600 });
|
|
4049
|
+
await assert.rejects(developIssue({
|
|
4050
|
+
...context,
|
|
4051
|
+
config: {
|
|
4052
|
+
...config,
|
|
4053
|
+
roles: {
|
|
4054
|
+
...config.roles,
|
|
4055
|
+
author: { ...config.roles.author, identity: 'other-owner' },
|
|
4056
|
+
},
|
|
4057
|
+
},
|
|
4058
|
+
}, issueNumber), /same preparing owner/u);
|
|
4059
|
+
issue.state = 'closed';
|
|
4060
|
+
await assert.rejects(executeTool('issue_develop', { issueNumber }, execution), /closed/u);
|
|
4061
|
+
issue.state = 'open';
|
|
4062
|
+
headSha = 'b'.repeat(40);
|
|
4063
|
+
await assert.rejects(executeTool('issue_develop', { issueNumber }, execution), /terminal receipt/u);
|
|
4064
|
+
headSha = reviewedHead;
|
|
4065
|
+
const mergePath = join(gitCommonDir(root), 'ai-delivery/merges', String(issueNumber), `${headSha}.json`);
|
|
4066
|
+
const mergeBytes = readFileSync(mergePath);
|
|
4067
|
+
unlinkSync(mergePath);
|
|
4068
|
+
await assert.rejects(executeTool('issue_develop', { issueNumber }, execution), /exact terminal merge receipt/u);
|
|
4069
|
+
writeFileSync(mergePath, mergeBytes, { mode: 0o600 });
|
|
4070
|
+
assert.deepEqual(readFileSync(registryPath), registryBytes);
|
|
4071
|
+
projectStatus = null;
|
|
4072
|
+
failNextProjectSync = true;
|
|
4073
|
+
await assert.rejects(executeTool('issue_develop', { issueNumber }, execution), /synthetic Project interruption/u);
|
|
4074
|
+
const reactivated = getIssueWorktreeStrict(issueNumber, root);
|
|
4075
|
+
assert.equal(reactivated.status, 'active');
|
|
4076
|
+
assert.equal(reactivated.prNumber, undefined);
|
|
4077
|
+
assert.equal(reactivated.identity, previous.identity);
|
|
4078
|
+
assert.equal(reactivated.createdAt, previous.createdAt);
|
|
4079
|
+
const resumed = await executeTool('issue_develop', { issueNumber }, execution);
|
|
4080
|
+
assert.deepEqual(resumed, { path: previous.path, branch: previous.branch });
|
|
4081
|
+
assert.equal(projectStatus, 'Active');
|
|
4082
|
+
await assert.rejects(verifyIssue({ issueNumber, repoRoot: row.path }), /already-merged issue head/u);
|
|
4083
|
+
for (const saved of oldReceipts) {
|
|
4084
|
+
assert.deepEqual(readFileSync(saved.path), saved.bytes);
|
|
4085
|
+
}
|
|
4086
|
+
assert.equal(directoryHash(ownerPath), ownerHash);
|
|
4087
|
+
// A fresh commit produces new verification slots while the complete prior head remains immutable.
|
|
4088
|
+
writeFileSync(join(row.path, 'change.txt'), 'continued feature\n');
|
|
4089
|
+
git(row.path, 'add', 'change.txt');
|
|
4090
|
+
git(row.path, 'commit', '-qm', 'continue unfinished synthetic issue');
|
|
4091
|
+
const continued = await verifyIssue({ issueNumber, repoRoot: row.path });
|
|
4092
|
+
assert.notEqual(continued.classification.head.sha, reviewedHead);
|
|
4093
|
+
for (const saved of oldReceipts)
|
|
4094
|
+
assert.deepEqual(readFileSync(saved.path), saved.bytes);
|
|
4095
|
+
assert.equal(directoryHash(ownerPath), ownerHash);
|
|
4096
|
+
assert.equal(calls.filter((call) => call === 'git:createCommit').length, 1);
|
|
4097
|
+
return;
|
|
4098
|
+
}
|
|
3944
4099
|
const blockedRecordPath = join(gitCommonDir(root), 'ai-delivery', 'deliveries.json');
|
|
3945
4100
|
mkdirSync(blockedRecordPath);
|
|
3946
4101
|
await assert.rejects(finishIssue(context, { issueNumber, prNumber, strategy: 'merge' }), /Delivery record store is invalid/u);
|
|
3947
4102
|
assert.equal(existsSync(row.path), false);
|
|
3948
4103
|
rmSync(blockedRecordPath, { recursive: true });
|
|
3949
|
-
const dispatchClient =
|
|
3950
|
-
|
|
3951
|
-
graphql: (async (query, variables) => query.includes('DeliveryDiscovery') ||
|
|
3952
|
-
query.includes('DeliveryRepository') ||
|
|
3953
|
-
query.includes('ProjectDeliveryConfiguration')
|
|
3954
|
-
? syntheticDiscoveryClients().graphql(query, variables)
|
|
3955
|
-
: graphql(query, variables)),
|
|
3956
|
-
}));
|
|
3957
|
-
restoreDispatchClient = () => dispatchClient.mockRestore();
|
|
3958
|
-
const execution = { repoRoot: root, runtimeEntryPath, identity: 'synthetic-author' };
|
|
4104
|
+
const dispatchClient = enableDispatch();
|
|
4105
|
+
execution.runtimeEntryPath = runtimeEntryPath;
|
|
3959
4106
|
const terminalMerge = (await executeTool('issue_pr_merge', { issueNumber, prNumber, strategy: 'merge' }, execution));
|
|
3960
4107
|
assert.equal(terminalMerge.mergeSha, git(remote, 'rev-parse', 'refs/heads/main'));
|
|
3961
4108
|
const finished = (await executeTool('issue_finish', { issueNumber, prNumber, strategy: 'merge' }, execution));
|
|
@@ -4019,10 +4166,201 @@ test.each([
|
|
|
4019
4166
|
{ remote: 'upstream', divergentOrigin: false },
|
|
4020
4167
|
{ remote: 'upstream', divergentOrigin: true },
|
|
4021
4168
|
])('synthetic lifecycle finishes with remote $remote and divergent origin $divergentOrigin', syntheticLifecycle, 15_000);
|
|
4169
|
+
test('supported develop resumes an open merged issue under the same owner and preserves prior receipts', { timeout: 0 }, async () => syntheticLifecycle({ remote: 'origin', divergentOrigin: false, mergedContinuation: true }));
|
|
4170
|
+
test('supported native metadata updates a legacy target without adopting its source or receipts', { timeout: 0 }, async () => syntheticLifecycle({ remote: 'origin', divergentOrigin: false, legacyNativeMetadata: true }));
|
|
4022
4171
|
test('an unrelated consumer uses public MCP setup through verified configured-author publication, counted review readback, merge and cleanup', { timeout: 0 }, async () => {
|
|
4023
4172
|
await syntheticLifecycle({ remote: 'origin', divergentOrigin: false, producerOnboarding: true });
|
|
4024
4173
|
});
|
|
4025
|
-
|
|
4174
|
+
function ordinaryCliRebuildScript(output, mode = 'pass', target = join(output, 'cli.js')) {
|
|
4175
|
+
return `(async () => {
|
|
4176
|
+
const fs = require('node:fs');
|
|
4177
|
+
const trace = event => fs.appendFileSync(${JSON.stringify(join(output, 'events'))}, event + '\\n');
|
|
4178
|
+
trace('command-started'); fs.unlinkSync(${JSON.stringify(target)}); trace('target-removed');
|
|
4179
|
+
if (${JSON.stringify(dirname(target))} !== ${JSON.stringify(output)}) fs.rmdirSync(${JSON.stringify(dirname(target))});
|
|
4180
|
+
if (${JSON.stringify(mode)} === 'limit') fs.writeFileSync(${JSON.stringify(join(output, 'oversized'))}, 'x'.repeat(4096));
|
|
4181
|
+
for (let unit = 0; unit < 32; unit++) {
|
|
4182
|
+
fs.appendFileSync(${JSON.stringify(join(output, 'compiled'))}, 'build-unit\\n');
|
|
4183
|
+
trace('build-unit-' + unit);
|
|
4184
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
4185
|
+
}
|
|
4186
|
+
if (${JSON.stringify(mode)} !== 'absent') {
|
|
4187
|
+
fs.mkdirSync(${JSON.stringify(dirname(target))}, { recursive: true });
|
|
4188
|
+
fs.writeFileSync(${JSON.stringify(target)}, 'rebuilt CLI'); trace('target-restored');
|
|
4189
|
+
}
|
|
4190
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });`;
|
|
4191
|
+
}
|
|
4192
|
+
test.each(['direct', 'chain', 'directory'])('ordinary bounded rebuild restores its %s CLI target after observable build work', async (shape) => {
|
|
4193
|
+
const output = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-cli-rebuild-')));
|
|
4194
|
+
const { root } = await fixture();
|
|
4195
|
+
const target = shape === 'directory' ? join(output, 'dist', 'cli.js') : join(output, 'cli.js');
|
|
4196
|
+
const alias = join(output, 'cli');
|
|
4197
|
+
const events = join(output, 'events');
|
|
4198
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
4199
|
+
writeFileSync(target, 'original CLI');
|
|
4200
|
+
writeFileSync(join(output, 'unrelated'), 'preserved');
|
|
4201
|
+
if (shape === 'direct')
|
|
4202
|
+
symlinkSync('./cli.js', alias);
|
|
4203
|
+
else {
|
|
4204
|
+
symlinkSync(shape === 'chain' ? './cli.js' : './dist', join(output, 'intermediate'));
|
|
4205
|
+
symlinkSync(shape === 'chain' ? './intermediate' : './intermediate/cli.js', alias);
|
|
4206
|
+
}
|
|
4207
|
+
const observations = [];
|
|
4208
|
+
const duringAbsence = [];
|
|
4209
|
+
const progress = vi.spyOn(process.stderr, 'write').mockImplementation((line) => {
|
|
4210
|
+
observations.push(String(line));
|
|
4211
|
+
if (!existsSync(target) && String(line).includes('sampledNewOutputBytes'))
|
|
4212
|
+
duringAbsence.push(JSON.parse(String(line).slice(String(line).indexOf('{'))));
|
|
4213
|
+
return true;
|
|
4214
|
+
});
|
|
4215
|
+
const script = ordinaryCliRebuildScript(output, 'pass', target);
|
|
4216
|
+
try {
|
|
4217
|
+
const { withRuntimeSetupWriter } = await import('./verification.js');
|
|
4218
|
+
await withRuntimeSetupWriter(root, (runner) => runner.run([process.execPath, '-e', script], {
|
|
4219
|
+
maxAggregateRssBytes: 1_000_000_000,
|
|
4220
|
+
minFreeDiskBytes: 1,
|
|
4221
|
+
maxNewOutputBytes: 8192,
|
|
4222
|
+
outputRoots: [output],
|
|
4223
|
+
}));
|
|
4224
|
+
assert.equal(readFileSync(target, 'utf8'), 'rebuilt CLI');
|
|
4225
|
+
assert.equal(readFileSync(join(output, 'compiled'), 'utf8').split('\n').length, 33);
|
|
4226
|
+
assert.equal(readFileSync(join(output, 'unrelated'), 'utf8'), 'preserved');
|
|
4227
|
+
assert.ok(duringAbsence.some((sample) => (sample.sampledNewOutputBytes ?? 0) > 0));
|
|
4228
|
+
assert.ok(duringAbsence.every((sample) => (sample.sampledAggregateRssBytes ?? 0) > 0));
|
|
4229
|
+
}
|
|
4230
|
+
finally {
|
|
4231
|
+
console.log(JSON.stringify({
|
|
4232
|
+
scenario: 'ordinary-cli-rebuild',
|
|
4233
|
+
shape,
|
|
4234
|
+
firstObservation: observations.find((line) => line.includes('ENOENT')),
|
|
4235
|
+
events: readFileSync(events, 'utf8').trim().split('\n'),
|
|
4236
|
+
targetRestored: existsSync(target),
|
|
4237
|
+
unrelatedPreserved: readFileSync(join(output, 'unrelated'), 'utf8') === 'preserved',
|
|
4238
|
+
}));
|
|
4239
|
+
progress.mockRestore();
|
|
4240
|
+
rmSync(root, { recursive: true, force: true });
|
|
4241
|
+
rmSync(output, { recursive: true, force: true });
|
|
4242
|
+
}
|
|
4243
|
+
});
|
|
4244
|
+
test.each([
|
|
4245
|
+
'bytes',
|
|
4246
|
+
'rss',
|
|
4247
|
+
'disk',
|
|
4248
|
+
'fifo',
|
|
4249
|
+
'permission',
|
|
4250
|
+
'identity',
|
|
4251
|
+
'ancestor',
|
|
4252
|
+
'cycle',
|
|
4253
|
+
'chain-identity',
|
|
4254
|
+
'directory-identity',
|
|
4255
|
+
'chain-cycle',
|
|
4256
|
+
])('a missing validated target retains complete scan and %s rejection across roots', async (mode) => {
|
|
4257
|
+
const output = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-rebuild-boundary-')));
|
|
4258
|
+
const { root } = await fixture();
|
|
4259
|
+
const first = join(output, 'first'), second = join(output, 'second');
|
|
4260
|
+
mkdirSync(first);
|
|
4261
|
+
mkdirSync(second);
|
|
4262
|
+
const directory = join(first, 'dist'), target = join(directory, 'cli.js'), alias = join(first, 'cli');
|
|
4263
|
+
mkdirSync(directory);
|
|
4264
|
+
writeFileSync(target, 'CLI');
|
|
4265
|
+
const intermediate = join(first, 'intermediate');
|
|
4266
|
+
if (mode === 'chain-identity' || mode === 'chain-cycle') {
|
|
4267
|
+
symlinkSync('./dist/cli.js', intermediate);
|
|
4268
|
+
symlinkSync('./intermediate', alias);
|
|
4269
|
+
}
|
|
4270
|
+
else if (mode === 'directory-identity') {
|
|
4271
|
+
symlinkSync('./dist', intermediate);
|
|
4272
|
+
symlinkSync('./intermediate/cli.js', alias);
|
|
4273
|
+
}
|
|
4274
|
+
else
|
|
4275
|
+
symlinkSync('./dist/cli.js', alias);
|
|
4276
|
+
writeFileSync(join(second, 'unrelated'), 'preserved');
|
|
4277
|
+
const released = join(second, 'released');
|
|
4278
|
+
const observations = [];
|
|
4279
|
+
const progress = vi.spyOn(process.stderr, 'write').mockImplementation((line) => {
|
|
4280
|
+
observations.push(String(line));
|
|
4281
|
+
if (String(line).includes('ENOENT'))
|
|
4282
|
+
writeFileSync(released, 'observed');
|
|
4283
|
+
return true;
|
|
4284
|
+
});
|
|
4285
|
+
const script = `(async () => {
|
|
4286
|
+
const fs = require('node:fs'), cp = require('node:child_process');
|
|
4287
|
+
fs.unlinkSync(${JSON.stringify(target)});
|
|
4288
|
+
while (!fs.existsSync(${JSON.stringify(released)})) await new Promise(resolve => setTimeout(resolve, 10));
|
|
4289
|
+
if (${JSON.stringify(mode)} === 'bytes') fs.writeFileSync(${JSON.stringify(join(second, 'oversized'))}, 'x'.repeat(4096));
|
|
4290
|
+
if (${JSON.stringify(mode)} === 'rss') globalThis.retainedBuildMemory = Buffer.alloc(200 * 1024 * 1024, 1);
|
|
4291
|
+
if (${JSON.stringify(mode)} === 'disk') fs.writeFileSync(${JSON.stringify(join(second, 'disk-output'))}, Buffer.alloc(64 * 1024 * 1024, 1));
|
|
4292
|
+
if (${JSON.stringify(mode)} === 'fifo') cp.execFileSync('mkfifo', [${JSON.stringify(join(second, 'unsupported'))}]);
|
|
4293
|
+
if (${JSON.stringify(mode)} === 'permission') {
|
|
4294
|
+
fs.mkdirSync(${JSON.stringify(join(second, 'denied'))});
|
|
4295
|
+
fs.chmodSync(${JSON.stringify(join(second, 'denied'))}, 0);
|
|
4296
|
+
}
|
|
4297
|
+
if (${JSON.stringify(mode)} === 'identity') {
|
|
4298
|
+
fs.unlinkSync(${JSON.stringify(alias)}); fs.writeFileSync(${JSON.stringify(join(first, 'other'))}, 'other');
|
|
4299
|
+
fs.symlinkSync('./other', ${JSON.stringify(alias)});
|
|
4300
|
+
}
|
|
4301
|
+
if (${JSON.stringify(mode)} === 'ancestor') {
|
|
4302
|
+
fs.rmdirSync(${JSON.stringify(directory)}); fs.symlinkSync(${JSON.stringify(second)}, ${JSON.stringify(directory)});
|
|
4303
|
+
}
|
|
4304
|
+
if (${JSON.stringify(mode)} === 'cycle') fs.symlinkSync('./cli.js', ${JSON.stringify(target)});
|
|
4305
|
+
if (${JSON.stringify(mode)} === 'chain-identity') {
|
|
4306
|
+
fs.unlinkSync(${JSON.stringify(intermediate)}); fs.writeFileSync(${JSON.stringify(join(first, 'other'))}, 'other');
|
|
4307
|
+
fs.symlinkSync('./other', ${JSON.stringify(intermediate)});
|
|
4308
|
+
}
|
|
4309
|
+
if (${JSON.stringify(mode)} === 'directory-identity') {
|
|
4310
|
+
fs.unlinkSync(${JSON.stringify(intermediate)}); fs.mkdirSync(${JSON.stringify(join(first, 'other'))});
|
|
4311
|
+
fs.writeFileSync(${JSON.stringify(join(first, 'other', 'cli.js'))}, 'other');
|
|
4312
|
+
fs.symlinkSync('./other', ${JSON.stringify(intermediate)});
|
|
4313
|
+
}
|
|
4314
|
+
if (${JSON.stringify(mode)} === 'chain-cycle') {
|
|
4315
|
+
fs.unlinkSync(${JSON.stringify(intermediate)}); fs.symlinkSync('./cli', ${JSON.stringify(intermediate)});
|
|
4316
|
+
}
|
|
4317
|
+
for (let unit = 0; unit < 32; unit++) {
|
|
4318
|
+
fs.appendFileSync(${JSON.stringify(join(second, 'work'))}, 'unit\\n');
|
|
4319
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
4320
|
+
}
|
|
4321
|
+
fs.writeFileSync(${JSON.stringify(target)}, 'rebuilt CLI');
|
|
4322
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });`;
|
|
4323
|
+
try {
|
|
4324
|
+
const { withRuntimeSetupWriter } = await import('./verification.js');
|
|
4325
|
+
const disk = (await import('node:fs')).statfsSync(output);
|
|
4326
|
+
await assert.rejects(withRuntimeSetupWriter(root, (runner) => runner.run([process.execPath, '-e', script], {
|
|
4327
|
+
maxAggregateRssBytes: mode === 'rss' ? 128 * 1024 * 1024 : 1_000_000_000,
|
|
4328
|
+
minFreeDiskBytes: mode === 'disk' ? disk.bavail * disk.bsize - 32 * 1024 * 1024 : 1,
|
|
4329
|
+
maxNewOutputBytes: mode === 'disk' ? 128 * 1024 * 1024 : 1024,
|
|
4330
|
+
outputRoots: [first, second],
|
|
4331
|
+
})), (error) => {
|
|
4332
|
+
const message = String(error);
|
|
4333
|
+
assert.match(message, mode === 'bytes'
|
|
4334
|
+
? /filesystem output.*exceeded/u
|
|
4335
|
+
: mode === 'rss'
|
|
4336
|
+
? /aggregate RSS.*exceeded/u
|
|
4337
|
+
: mode === 'disk'
|
|
4338
|
+
? /Free disk fell below/u
|
|
4339
|
+
: mode === 'fifo'
|
|
4340
|
+
? /unsupported file/u
|
|
4341
|
+
: mode === 'permission'
|
|
4342
|
+
? /readdir failed.*EACCES/u
|
|
4343
|
+
: mode === 'cycle' || mode === 'chain-cycle'
|
|
4344
|
+
? /ELOOP/u
|
|
4345
|
+
: /identity changed/u);
|
|
4346
|
+
assert.match(message, /First filesystem observation:.*ENOENT.*Last filesystem observation:/u);
|
|
4347
|
+
assert.doesNotMatch(message, /cleanup failed/u);
|
|
4348
|
+
return true;
|
|
4349
|
+
});
|
|
4350
|
+
assert.ok(observations.some((line) => line.includes('ENOENT')));
|
|
4351
|
+
assert.equal(observations.some((line) => line.includes('"state":"completed"')), false);
|
|
4352
|
+
assert.equal(readFileSync(join(second, 'unrelated'), 'utf8'), 'preserved');
|
|
4353
|
+
}
|
|
4354
|
+
finally {
|
|
4355
|
+
const denied = join(second, 'denied');
|
|
4356
|
+
if (existsSync(denied))
|
|
4357
|
+
chmodSync(denied, 0o700);
|
|
4358
|
+
progress.mockRestore();
|
|
4359
|
+
rmSync(root, { recursive: true, force: true });
|
|
4360
|
+
rmSync(output, { recursive: true, force: true });
|
|
4361
|
+
}
|
|
4362
|
+
});
|
|
4363
|
+
test.each(['pass', 'chain-pass', 'command-error', 'cancel', 'persistent', 'limit'])('ordinary owned symlink teardown preserves observation and %s command evidence', async (mode) => {
|
|
4026
4364
|
const output = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-ordinary-teardown-')));
|
|
4027
4365
|
const { root } = await fixture();
|
|
4028
4366
|
const controller = new AbortController();
|
|
@@ -4031,7 +4369,9 @@ test.each(['pass', 'command-error', 'cancel', 'persistent', 'limit'])('ordinary
|
|
|
4031
4369
|
const release = join(output, 'release');
|
|
4032
4370
|
writeFileSync(join(output, 'unrelated'), 'preserved');
|
|
4033
4371
|
writeFileSync(join(output, 'target'), 'x'.repeat(128));
|
|
4034
|
-
|
|
4372
|
+
if (mode === 'chain-pass')
|
|
4373
|
+
symlinkSync('./target', join(output, 'intermediate'));
|
|
4374
|
+
symlinkSync(mode === 'chain-pass' ? './intermediate' : './target', join(output, 'alias'));
|
|
4035
4375
|
let observedGap = false;
|
|
4036
4376
|
const observations = [];
|
|
4037
4377
|
let releaseTimer;
|
|
@@ -4044,7 +4384,7 @@ test.each(['pass', 'command-error', 'cancel', 'persistent', 'limit'])('ordinary
|
|
|
4044
4384
|
releaseTimer = setTimeout(() => {
|
|
4045
4385
|
if (mode === 'cancel')
|
|
4046
4386
|
controller.abort();
|
|
4047
|
-
else
|
|
4387
|
+
else
|
|
4048
4388
|
writeFileSync(release, 'continue');
|
|
4049
4389
|
}, 100);
|
|
4050
4390
|
}
|
|
@@ -4060,7 +4400,10 @@ test.each(['pass', 'command-error', 'cancel', 'persistent', 'limit'])('ordinary
|
|
|
4060
4400
|
trace('target-removed'); fs.writeFileSync(${JSON.stringify(ready)}, 'ready');
|
|
4061
4401
|
if (${JSON.stringify(mode)} === 'limit') fs.writeFileSync(${JSON.stringify(join(output, 'retained'))}, 'x'.repeat(512));
|
|
4062
4402
|
while (!fs.existsSync(${JSON.stringify(release)})) await new Promise(resolve => setTimeout(resolve, 10));
|
|
4063
|
-
|
|
4403
|
+
if (${JSON.stringify(mode)} !== 'persistent') {
|
|
4404
|
+
fs.unlinkSync(${JSON.stringify(join(output, 'alias'))}); trace('alias-removed');
|
|
4405
|
+
if (${JSON.stringify(mode)} === 'chain-pass') fs.unlinkSync(${JSON.stringify(join(output, 'intermediate'))});
|
|
4406
|
+
}
|
|
4064
4407
|
process.exitCode = ${mode === 'command-error' ? 7 : 0};
|
|
4065
4408
|
})().catch(error => { console.error(error); process.exitCode = 1; });`;
|
|
4066
4409
|
try {
|
|
@@ -4071,7 +4414,7 @@ test.each(['pass', 'command-error', 'cancel', 'persistent', 'limit'])('ordinary
|
|
|
4071
4414
|
maxNewOutputBytes: mode === 'limit' ? 256 : 4096,
|
|
4072
4415
|
outputRoots: [output],
|
|
4073
4416
|
}, controller.signal));
|
|
4074
|
-
if (mode === 'pass')
|
|
4417
|
+
if (mode === 'pass' || mode === 'chain-pass')
|
|
4075
4418
|
await run;
|
|
4076
4419
|
else
|
|
4077
4420
|
await assert.rejects(run, (error) => {
|
|
@@ -4079,7 +4422,7 @@ test.each(['pass', 'command-error', 'cancel', 'persistent', 'limit'])('ordinary
|
|
|
4079
4422
|
assert.match(message, mode === 'cancel'
|
|
4080
4423
|
? /cancelled/u
|
|
4081
4424
|
: mode === 'persistent'
|
|
4082
|
-
? /
|
|
4425
|
+
? /broken or cyclic symbolic link.*ENOENT/u
|
|
4083
4426
|
: mode === 'limit'
|
|
4084
4427
|
? /filesystem output.*exceeded/u
|
|
4085
4428
|
: /exit 7/u);
|
|
@@ -4108,7 +4451,7 @@ test.each(['pass', 'command-error', 'cancel', 'persistent', 'limit'])('ordinary
|
|
|
4108
4451
|
unrelatedPreserved: true,
|
|
4109
4452
|
aliasRetained: readdirSync(output).includes('alias'),
|
|
4110
4453
|
}));
|
|
4111
|
-
assert.deepEqual(readFileSync(events, 'utf8').trim().split('\n'), mode === 'cancel' || mode === 'persistent'
|
|
4454
|
+
assert.deepEqual(readFileSync(events, 'utf8').trim().split('\n'), mode === 'cancel' || mode === 'persistent' || mode === 'limit'
|
|
4112
4455
|
? ['command-started', 'target-removed']
|
|
4113
4456
|
: ['command-started', 'target-removed', 'alias-removed']);
|
|
4114
4457
|
if (mode === 'cancel' || mode === 'persistent') {
|
|
@@ -4133,12 +4476,68 @@ test.each(['pass', 'command-error', 'cancel', 'persistent', 'limit'])('ordinary
|
|
|
4133
4476
|
rmSync(output, { recursive: true, force: true });
|
|
4134
4477
|
}
|
|
4135
4478
|
});
|
|
4136
|
-
test('injected
|
|
4479
|
+
test('injected first failed scan duration does not consume the subsequent observation stall window', async () => {
|
|
4480
|
+
const output = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-scan-duration-')));
|
|
4481
|
+
const { root } = await fixture();
|
|
4482
|
+
const target = join(output, 'target'), alias = join(output, 'alias'), release = join(output, 'release');
|
|
4483
|
+
writeFileSync(target, 'target');
|
|
4484
|
+
const fs = (await import('node:fs')).default;
|
|
4485
|
+
const actualRealpath = fs.realpathSync;
|
|
4486
|
+
const now = Date.now.bind(Date);
|
|
4487
|
+
let offset = 0, delayed = false, firstObservation;
|
|
4488
|
+
const clock = vi.spyOn(Date, 'now').mockImplementation(() => now() + offset);
|
|
4489
|
+
const scan = vi.spyOn(fs, 'realpathSync').mockImplementation((path, options) => {
|
|
4490
|
+
try {
|
|
4491
|
+
return actualRealpath(path, options);
|
|
4492
|
+
}
|
|
4493
|
+
catch (error) {
|
|
4494
|
+
if (path === alias && !delayed) {
|
|
4495
|
+
delayed = true;
|
|
4496
|
+
offset = 1500;
|
|
4497
|
+
}
|
|
4498
|
+
throw error;
|
|
4499
|
+
}
|
|
4500
|
+
});
|
|
4501
|
+
syncBuiltinESMExports();
|
|
4502
|
+
const progress = vi.spyOn(process.stderr, 'write').mockImplementation((line) => {
|
|
4503
|
+
if (firstObservation === undefined && String(line).includes('ENOENT')) {
|
|
4504
|
+
firstObservation = String(line);
|
|
4505
|
+
unlinkSync(alias);
|
|
4506
|
+
writeFileSync(release, 'finish');
|
|
4507
|
+
}
|
|
4508
|
+
return true;
|
|
4509
|
+
});
|
|
4510
|
+
const script = `(async () => {
|
|
4511
|
+
const fs = require('node:fs'); fs.unlinkSync(${JSON.stringify(target)});
|
|
4512
|
+
fs.symlinkSync('./target', ${JSON.stringify(alias)});
|
|
4513
|
+
while (!fs.existsSync(${JSON.stringify(release)})) await new Promise(resolve => setTimeout(resolve, 10));
|
|
4514
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });`;
|
|
4515
|
+
try {
|
|
4516
|
+
const { withRuntimeSetupWriter } = await import('./verification.js');
|
|
4517
|
+
await withRuntimeSetupWriter(root, (runner) => runner.run([process.execPath, '-e', script], {
|
|
4518
|
+
maxAggregateRssBytes: 1_000_000_000,
|
|
4519
|
+
minFreeDiskBytes: 1,
|
|
4520
|
+
maxNewOutputBytes: 4096,
|
|
4521
|
+
outputRoots: [output],
|
|
4522
|
+
}));
|
|
4523
|
+
assert.ok(delayed);
|
|
4524
|
+
assert.ok(firstObservation);
|
|
4525
|
+
assert.equal(existsSync(alias), false);
|
|
4526
|
+
}
|
|
4527
|
+
finally {
|
|
4528
|
+
progress.mockRestore();
|
|
4529
|
+
scan.mockRestore();
|
|
4530
|
+
syncBuiltinESMExports();
|
|
4531
|
+
clock.mockRestore();
|
|
4532
|
+
rmSync(root, { recursive: true, force: true });
|
|
4533
|
+
rmSync(output, { recursive: true, force: true });
|
|
4534
|
+
}
|
|
4535
|
+
});
|
|
4536
|
+
test('injected observation stall rejects a late complete retry for an unvalidated alias', async () => {
|
|
4137
4537
|
const output = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-observation-deadline-')));
|
|
4138
4538
|
const { root } = await fixture();
|
|
4139
4539
|
const target = join(output, 'target'), alias = join(output, 'alias'), release = join(output, 'release');
|
|
4140
4540
|
writeFileSync(target, 'target');
|
|
4141
|
-
symlinkSync('./target', alias);
|
|
4142
4541
|
const now = Date.now.bind(Date);
|
|
4143
4542
|
let offset = 0;
|
|
4144
4543
|
let firstObservation;
|
|
@@ -4158,6 +4557,7 @@ test('injected observation deadline rejects a late complete real filesystem retr
|
|
|
4158
4557
|
});
|
|
4159
4558
|
const script = `(async () => {
|
|
4160
4559
|
const fs = require('node:fs'); fs.unlinkSync(${JSON.stringify(target)});
|
|
4560
|
+
fs.symlinkSync('./target', ${JSON.stringify(alias)});
|
|
4161
4561
|
while (!fs.existsSync(${JSON.stringify(release)})) await new Promise(resolve => setTimeout(resolve, 10));
|
|
4162
4562
|
})().catch(error => { console.error(error); process.exitCode = 1; });`;
|
|
4163
4563
|
try {
|
|
@@ -4301,6 +4701,90 @@ const child = require('node:child_process').spawnSync(${JSON.stringify(consumer.
|
|
|
4301
4701
|
if (child.error) throw child.error;
|
|
4302
4702
|
process.exit(child.status ?? 1);`;
|
|
4303
4703
|
}
|
|
4704
|
+
test.each(['pass', 'absent', 'limit', 'cancel'])('Node 26 packed public verification preserves CLI rebuild %s behavior', async (mode) => {
|
|
4705
|
+
const output = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-packed-rebuild-')));
|
|
4706
|
+
const target = join(output, 'cli.js');
|
|
4707
|
+
writeFileSync(target, 'original CLI');
|
|
4708
|
+
symlinkSync('./cli.js', join(output, 'cli'));
|
|
4709
|
+
writeFileSync(join(output, 'unrelated'), 'preserved');
|
|
4710
|
+
const { root } = await fixture({ personalAuthor: true, firstStageScript: ordinaryCliRebuildScript(output, mode) });
|
|
4711
|
+
try {
|
|
4712
|
+
const row = await prepareIssueWorktree({
|
|
4713
|
+
baseRef: 'main',
|
|
4714
|
+
identity: 'host-author',
|
|
4715
|
+
issueNumber: 17,
|
|
4716
|
+
repoRoot: root,
|
|
4717
|
+
});
|
|
4718
|
+
const clients = syntheticDiscoveryClients();
|
|
4719
|
+
const replies = [];
|
|
4720
|
+
for (const fragment of [
|
|
4721
|
+
'query DeliveryRepository',
|
|
4722
|
+
'query ProjectDeliveryConfiguration',
|
|
4723
|
+
'issueFields(first:',
|
|
4724
|
+
'issueTypes(first:',
|
|
4725
|
+
])
|
|
4726
|
+
replies.push({ fragment, data: await clients.graphql(fragment) });
|
|
4727
|
+
const consumer = packedNode26Consumer();
|
|
4728
|
+
const result = execFileSync(consumer.executable, [
|
|
4729
|
+
'-e',
|
|
4730
|
+
`(async () => {
|
|
4731
|
+
const assert = require('node:assert/strict'), fs = require('node:fs');
|
|
4732
|
+
assert.equal(process.version, 'v26.2.0');
|
|
4733
|
+
const replies = ${JSON.stringify(replies)};
|
|
4734
|
+
// Only GitHub discovery transport is synthetic; package, writer, scanner and child filesystem are real.
|
|
4735
|
+
globalThis.fetch = async (url, options) => {
|
|
4736
|
+
assert.equal(String(url), 'https://api.github.com/graphql');
|
|
4737
|
+
const query = JSON.parse(options.body).query;
|
|
4738
|
+
const reply = replies.find(reply => query.includes(reply.fragment));
|
|
4739
|
+
assert.ok(reply, 'unexpected synthetic discovery query');
|
|
4740
|
+
return new Response(JSON.stringify({ data: reply.data }), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
4741
|
+
};
|
|
4742
|
+
process.env.AUTHOR_TOKEN = 'synthetic-author-token';
|
|
4743
|
+
const { verifyIssue } = await import('@aviaratech/ai-delivery/agent');
|
|
4744
|
+
const controller = new AbortController();
|
|
4745
|
+
const observations = []; const write = process.stderr.write.bind(process.stderr);
|
|
4746
|
+
process.stderr.write = line => {
|
|
4747
|
+
observations.push(String(line));
|
|
4748
|
+
if (${JSON.stringify(mode)} === 'cancel' && String(line).includes('ENOENT')) controller.abort();
|
|
4749
|
+
return true;
|
|
4750
|
+
};
|
|
4751
|
+
try {
|
|
4752
|
+
const running = verifyIssue({ issueNumber: 17, repoRoot: ${JSON.stringify(row.path)}, signal: controller.signal,
|
|
4753
|
+
resourceBounds: { maxAggregateRssBytes: 1000000000, minFreeDiskBytes: 1,
|
|
4754
|
+
maxNewOutputBytes: ${mode === 'limit' ? 1024 : 8192}, outputRoots: [${JSON.stringify(output)}] } });
|
|
4755
|
+
if (${JSON.stringify(mode)} === 'pass') {
|
|
4756
|
+
const run = await running;
|
|
4757
|
+
assert.ok(run.resources.sampleCount > 0); assert.ok(run.resources.maxSampledNewOutputBytes > 0);
|
|
4758
|
+
assert.equal(fs.readFileSync(${JSON.stringify(target)}, 'utf8'), 'rebuilt CLI');
|
|
4759
|
+
// Compatible completed proof is reused, but the current final filesystem is still strictly scanned.
|
|
4760
|
+
fs.unlinkSync(${JSON.stringify(target)});
|
|
4761
|
+
await assert.rejects(verifyIssue({ issueNumber: 17, repoRoot: ${JSON.stringify(row.path)},
|
|
4762
|
+
resourceBounds: { maxAggregateRssBytes: 1000000000, minFreeDiskBytes: 1, maxNewOutputBytes: 8192,
|
|
4763
|
+
outputRoots: [${JSON.stringify(output)}] } }), /broken or cyclic symbolic link/);
|
|
4764
|
+
} else {
|
|
4765
|
+
await assert.rejects(running, error => {
|
|
4766
|
+
assert.match(String(error), ${mode === 'cancel' ? '/cancelled/' : mode === 'limit' ? '/filesystem output.*exceeded/' : '/broken or cyclic symbolic link/'});
|
|
4767
|
+
assert.doesNotMatch(String(error), /cleanup failed/); return true;
|
|
4768
|
+
});
|
|
4769
|
+
}
|
|
4770
|
+
assert.ok(observations.some(line => line.includes('ENOENT')));
|
|
4771
|
+
assert.equal(fs.readFileSync(${JSON.stringify(join(output, 'unrelated'))}, 'utf8'), 'preserved');
|
|
4772
|
+
console.log(JSON.stringify({ scenario: 'packed-node26-cli-rebuild', mode: ${JSON.stringify(mode)},
|
|
4773
|
+
firstObservation: observations.find(line => line.includes('ENOENT')), events: fs.readFileSync(${JSON.stringify(join(output, 'events'))}, 'utf8').trim().split('\\n') }));
|
|
4774
|
+
} finally { controller.abort(); process.stderr.write = write; }
|
|
4775
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });`,
|
|
4776
|
+
], {
|
|
4777
|
+
cwd: consumer.root,
|
|
4778
|
+
encoding: 'utf8',
|
|
4779
|
+
maxBuffer: 1024 * 1024,
|
|
4780
|
+
});
|
|
4781
|
+
console.log(result.trim());
|
|
4782
|
+
}
|
|
4783
|
+
finally {
|
|
4784
|
+
rmSync(root, { recursive: true, force: true });
|
|
4785
|
+
rmSync(output, { recursive: true, force: true });
|
|
4786
|
+
}
|
|
4787
|
+
});
|
|
4304
4788
|
function filesystemFixtureScript(output, mode, runtime = 'node24') {
|
|
4305
4789
|
const script = `
|
|
4306
4790
|
(async () => {
|