@aviaratech/ai-delivery 0.3.6 → 0.3.7
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 -0
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +1 -1
- package/dist/agent.js.map +1 -1
- package/dist/lifecycle.test.js +395 -0
- package/dist/lifecycle.test.js.map +1 -1
- package/dist/verification.d.ts +2 -0
- package/dist/verification.js +346 -52
- package/dist/verification.js.map +1 -1
- package/package.json +1 -1
- package/plugins/ai-delivery/.claude-plugin/plugin.json +1 -1
package/README.md
CHANGED
|
@@ -327,6 +327,23 @@ The `--admit` flag explicitly admits additional resource classes. Publication cr
|
|
|
327
327
|
|
|
328
328
|
For a bounded run, pass `--max-aggregate-rss-bytes`, `--min-free-disk-bytes`, and optionally `--max-new-output-bytes` with one or more `--output-root` paths to `verify`. `issue_verify` accepts the same values in `resourceBounds`. RSS is sampled across the observed child process tree, including observed detached descendants; free disk is checked before commands and during execution on the worktree and declared output filesystems. Positive file-size growth is sampled under the declared roots, with previously measured completed stages carried across a resume. The caller must declare every output root relevant to its allowance; aliases must resolve within those roots. The roots are relative to the issue worktree unless absolute. Output growth is separate from the 8 MiB captured stdout/stderr limit. Resource limits are opt in and do not impose a total runtime deadline.
|
|
329
329
|
|
|
330
|
+
Real filesystem-negative tests can use `withVerificationFilesystemFixture` from `@aviaratech/ai-delivery` (or its `agent` export):
|
|
331
|
+
|
|
332
|
+
```js
|
|
333
|
+
import { withVerificationFilesystemFixture } from '@aviaratech/ai-delivery';
|
|
334
|
+
|
|
335
|
+
await withVerificationFilesystemFixture(async () => {
|
|
336
|
+
// Create the real negative fixture inside a declared output root.
|
|
337
|
+
try {
|
|
338
|
+
// Assert the actual reader rejects it without blocking.
|
|
339
|
+
} finally {
|
|
340
|
+
// Remove every negative fixture before returning or throwing.
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Bounded verification supplies an inherited, run-owned filesystem coordination capability. The helper serializes the fixture's lifetime against strict scans of every declared root; nested verification and runtime setup join the same boundary. Without an inherited observer the helper runs its callback normally. Malformed, foreign or stale capabilities fail closed. The callback must contain only the negative test and its teardown, and must not start verification while holding the fixture. A queued scan takes priority over new fixtures. RSS, disk, captured-log limits and cancellation continue while the scan waits; waiting observations do not claim a new filesystem measurement. A fixture that fails to release a waiting scan within five seconds is a stalled reader/teardown failure: the command is terminated through the existing identity-checked cleanup, without stealing the live fixture lease. This is an operation-specific observation-stall bound, not a verification runtime limit. Successful commands and fully cached resumes receive fresh strict scans; ordinary or leaked unsupported entries still fail. Abrupt termination can prevent callback teardown; leaked fixtures remain visible and fail subsequent verification. After existing writer recovery confirms command quiescence, only validated abandoned control metadata from that worktree is retired. No output roots, special-file kinds or byte growth are exempted.
|
|
346
|
+
|
|
330
347
|
Bounded stage checkpoints bind the limits and sampled observations. Older unmeasured stages and stages with different limits rerun. Current manifests use `ai-delivery.run@3`, bind the worktree and producer, and live under `runs@2/<worktree-digest>/<head>.json`. Bounded manifests also record limits, sample count, and sampled RSS, output, and disk extrema. Historical run versions 1 and 2 remain historical and are available for merged-issue recovery; current publication requires current-producer verification. `processCoverage: "observed-processes-only"` means a passing aggregate is not proof that every detached descendant was drained. A descendant that detaches and closes inherited pipes between samples can evade observation; a pipe holder that prevents command closure fails with unverified cleanup and no checkpoint. Output roots must be canonical and nonoverlapping. Resolvable aliases such as npm `.bin` links may point only within the declared roots; aliases are not traversed or counted twice, and growth of their physical targets remains measured. Escaping, broken and cyclic links fail closed. Output baselines are retained across retries, including failed stages. They are range scoped: a changed baseline regenerates the measured stage proof. Consumers must confirm their command graph remains observable and declare all relevant output roots; this contract does not assert ownership of undeclared output locations or other concurrent writers.
|
|
331
348
|
|
|
332
349
|
A reviewed policy transition can be verified and published before activating
|
package/dist/agent.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export { buildVelocityReport, getDeliveryRecords, recordMergedDelivery } from '.
|
|
|
14
14
|
export type { DeliveryRecord, VelocityReport, PointBucketStatistics } from './services/deliveryRecordService.js';
|
|
15
15
|
export { getIssueWorktreeStrict, listWorktreesStrict } from './services/worktreeRegistry.js';
|
|
16
16
|
export { prepareIssueWorktree, prepareStandaloneWorktree } from './worktree.js';
|
|
17
|
-
export { verifyIssue, createIssuePhaseEvidence } from './verification.js';
|
|
17
|
+
export { verifyIssue, createIssuePhaseEvidence, withVerificationFilesystemFixture } from './verification.js';
|
|
18
18
|
export { discoverDeliveryRouting } from './github/discovery.js';
|
|
19
19
|
export type { DeliveryRouting, DiscoveryClients } from './github/discovery.js';
|
|
20
20
|
export type { DeliveryOverrides, DeliveryPolicySettings, LoadedDeliverySettings } from './config/deliveryConfig.js';
|
package/dist/agent.js
CHANGED
|
@@ -12,7 +12,7 @@ export { normalizeLegacyIssue, planOfflineLegacyIssueMigration } from './service
|
|
|
12
12
|
export { buildVelocityReport, getDeliveryRecords, recordMergedDelivery } from './services/deliveryRecordService.js';
|
|
13
13
|
export { getIssueWorktreeStrict, listWorktreesStrict } from './services/worktreeRegistry.js';
|
|
14
14
|
export { prepareIssueWorktree, prepareStandaloneWorktree } from './worktree.js';
|
|
15
|
-
export { verifyIssue, createIssuePhaseEvidence } from './verification.js';
|
|
15
|
+
export { verifyIssue, createIssuePhaseEvidence, withVerificationFilesystemFixture } from './verification.js';
|
|
16
16
|
export { discoverDeliveryRouting } from './github/discovery.js';
|
|
17
17
|
export { stageRuntime, admitRuntime, RuntimeAdmissionCommitUnknownError } from './setup.js';
|
|
18
18
|
//# sourceMappingURL=agent.js.map
|
package/dist/agent.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACnB,8BAA8B,GAC/B,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAE,2BAA2B,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,gCAAgC,EAAE,qBAAqB,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AACpH,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAC7E,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC/G,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,kBAAkB,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACrH,OAAO,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAC3E,OAAO,EACL,6BAA6B,EAC7B,8BAA8B,EAC9B,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,oBAAoB,EAAE,+BAA+B,EAAE,MAAM,oCAAoC,CAAC;AAC3G,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAC;AAEpH,OAAO,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAC7F,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAChF,OAAO,EAAE,WAAW,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACnB,8BAA8B,GAC/B,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAE,2BAA2B,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,gCAAgC,EAAE,qBAAqB,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AACpH,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAC7E,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC/G,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,kBAAkB,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACrH,OAAO,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAC3E,OAAO,EACL,6BAA6B,EAC7B,8BAA8B,EAC9B,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,oBAAoB,EAAE,+BAA+B,EAAE,MAAM,oCAAoC,CAAC;AAC3G,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAC;AAEpH,OAAO,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAC7F,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAChF,OAAO,EAAE,WAAW,EAAE,wBAAwB,EAAE,iCAAiC,EAAE,MAAM,mBAAmB,CAAC;AAE7G,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAIhE,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,kCAAkC,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/lifecycle.test.js
CHANGED
|
@@ -3663,4 +3663,399 @@ test.each(['escape', 'broken', 'cycle'])('output observation rejects %s aliases
|
|
|
3663
3663
|
rmSync(base, { recursive: true, force: true });
|
|
3664
3664
|
}
|
|
3665
3665
|
});
|
|
3666
|
+
test('fully cached bounded verification still rejects a newly leaked FIFO', async () => {
|
|
3667
|
+
const { root, counter } = await fixture();
|
|
3668
|
+
const output = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-cached-fifo-')));
|
|
3669
|
+
try {
|
|
3670
|
+
const row = await prepareIssueWorktree({
|
|
3671
|
+
baseRef: 'main',
|
|
3672
|
+
identity: 'synthetic-author',
|
|
3673
|
+
issueNumber: 17,
|
|
3674
|
+
repoRoot: root,
|
|
3675
|
+
});
|
|
3676
|
+
const bounds = {
|
|
3677
|
+
maxAggregateRssBytes: 1_000_000_000,
|
|
3678
|
+
minFreeDiskBytes: 1,
|
|
3679
|
+
maxNewOutputBytes: 1024,
|
|
3680
|
+
outputRoots: [output],
|
|
3681
|
+
};
|
|
3682
|
+
await verifyIssue({ issueNumber: 17, repoRoot: row.path, resourceBounds: bounds });
|
|
3683
|
+
assert.equal(readFileSync(counter, 'utf8'), '1');
|
|
3684
|
+
execFileSync('mkfifo', [join(output, 'leaked')]);
|
|
3685
|
+
await assert.rejects(verifyIssue({ issueNumber: 17, repoRoot: row.path, resourceBounds: bounds }), /unsupported file/u);
|
|
3686
|
+
assert.equal(readFileSync(counter, 'utf8'), '1');
|
|
3687
|
+
}
|
|
3688
|
+
finally {
|
|
3689
|
+
rmSync(output, { recursive: true, force: true });
|
|
3690
|
+
rmSync(root, { recursive: true, force: true });
|
|
3691
|
+
}
|
|
3692
|
+
});
|
|
3693
|
+
function filesystemFixtureScript(output, mode) {
|
|
3694
|
+
return `
|
|
3695
|
+
(async () => {
|
|
3696
|
+
const fs = require('node:fs'), cp = require('node:child_process'), path = require('node:path'), assert = require('node:assert/strict');
|
|
3697
|
+
const { withVerificationFilesystemFixture } = await import(${JSON.stringify(new URL('./agent.js', import.meta.url).href)});
|
|
3698
|
+
const { assertPrivateFile } = await import(${JSON.stringify(new URL('./delivery/common.js', import.meta.url).href)});
|
|
3699
|
+
const output = ${JSON.stringify(output)}, fifo = path.join(output, 'fifo'), events = path.join(output, 'events');
|
|
3700
|
+
const gate = JSON.parse(process.env.AI_DELIVERY_OUTPUT_OBSERVATION_GATE);
|
|
3701
|
+
const trace = value => fs.appendFileSync(events, value + '\\n');
|
|
3702
|
+
await withVerificationFilesystemFixture(async () => {
|
|
3703
|
+
cp.execFileSync('mkfifo', ['-m', '600', fifo]);
|
|
3704
|
+
fs.writeFileSync(path.join(output, 'capability'), JSON.stringify(gate));
|
|
3705
|
+
trace('created');
|
|
3706
|
+
try {
|
|
3707
|
+
if (${JSON.stringify(mode)} === 'abrupt') process.exit(9);
|
|
3708
|
+
if (${JSON.stringify(mode)} === 'cancel') await new Promise(() => { setInterval(() => {}, 1000); });
|
|
3709
|
+
if (${JSON.stringify(mode)} === 'block') fs.readFileSync(fifo);
|
|
3710
|
+
while (fs.readdirSync(path.join(gate.directory, 'requests')).length === 0) await new Promise(resolve => setTimeout(resolve, 10));
|
|
3711
|
+
assert.ok(fs.existsSync(fifo)); trace('scan-queued');
|
|
3712
|
+
assert.throws(() => assertPrivateFile(fifo), /private regular file/); trace('reader-rejected');
|
|
3713
|
+
const regular = path.join(output, 'regular'), hard = path.join(output, 'hard'), alias = path.join(output, 'alias');
|
|
3714
|
+
fs.writeFileSync(regular, 'x'.repeat(128), { mode: 0o600 });
|
|
3715
|
+
fs.linkSync(regular, hard); assert.throws(() => assertPrivateFile(hard), /private regular file/); fs.unlinkSync(hard);
|
|
3716
|
+
fs.symlinkSync('./regular', alias); assert.throws(() => assertPrivateFile(alias), /private regular file/); fs.unlinkSync(alias);
|
|
3717
|
+
if (${JSON.stringify(mode)} === 'assertion') throw new Error('synthetic reader assertion failure');
|
|
3718
|
+
} finally { fs.unlinkSync(fifo); trace('cleaned'); }
|
|
3719
|
+
});
|
|
3720
|
+
trace('released');
|
|
3721
|
+
})().catch(error => { console.error(error.message); process.exitCode = 1; });`;
|
|
3722
|
+
}
|
|
3723
|
+
test.each(['pass', 'assertion'])('filesystem fixture gate deterministically queues a scan through reader rejection and %s teardown', async (mode) => {
|
|
3724
|
+
const output = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-fixture-gate-')));
|
|
3725
|
+
const { root } = await fixture({ firstStageScript: filesystemFixtureScript(output, mode) });
|
|
3726
|
+
try {
|
|
3727
|
+
const row = await prepareIssueWorktree({
|
|
3728
|
+
baseRef: 'main',
|
|
3729
|
+
identity: 'synthetic-author',
|
|
3730
|
+
issueNumber: 17,
|
|
3731
|
+
repoRoot: root,
|
|
3732
|
+
});
|
|
3733
|
+
const run = verifyIssue({
|
|
3734
|
+
issueNumber: 17,
|
|
3735
|
+
repoRoot: row.path,
|
|
3736
|
+
resourceBounds: {
|
|
3737
|
+
maxAggregateRssBytes: 1_000_000_000,
|
|
3738
|
+
minFreeDiskBytes: 1,
|
|
3739
|
+
maxNewOutputBytes: 2048,
|
|
3740
|
+
outputRoots: [output],
|
|
3741
|
+
},
|
|
3742
|
+
});
|
|
3743
|
+
if (mode === 'assertion')
|
|
3744
|
+
await assert.rejects(run, /exit 1/u);
|
|
3745
|
+
else
|
|
3746
|
+
assert.ok((await run).resources.maxSampledNewOutputBytes >= 128);
|
|
3747
|
+
assert.equal(existsSync(join(output, 'fifo')), false);
|
|
3748
|
+
const events = readFileSync(join(output, 'events'), 'utf8').trim().split('\n');
|
|
3749
|
+
assert.deepEqual(events, mode === 'pass'
|
|
3750
|
+
? ['created', 'scan-queued', 'reader-rejected', 'cleaned', 'released']
|
|
3751
|
+
: ['created', 'scan-queued', 'reader-rejected', 'cleaned']);
|
|
3752
|
+
const capability = JSON.parse(readFileSync(join(output, 'capability'), 'utf8'));
|
|
3753
|
+
assert.equal(existsSync(capability.directory), false);
|
|
3754
|
+
assert.equal(readFileSync(join(output, 'regular'), 'utf8').length, 128);
|
|
3755
|
+
}
|
|
3756
|
+
finally {
|
|
3757
|
+
rmSync(root, { recursive: true, force: true });
|
|
3758
|
+
rmSync(output, { recursive: true, force: true });
|
|
3759
|
+
}
|
|
3760
|
+
});
|
|
3761
|
+
test.each(['abrupt', 'block', 'cancel'])('filesystem fixture gate preserves leaked FIFO and unrelated files after %s', async (mode) => {
|
|
3762
|
+
const output = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-fixture-failure-')));
|
|
3763
|
+
writeFileSync(join(output, 'unrelated'), 'preserved');
|
|
3764
|
+
const { root } = await fixture({ firstStageScript: filesystemFixtureScript(output, mode) });
|
|
3765
|
+
const controller = new AbortController();
|
|
3766
|
+
let running;
|
|
3767
|
+
const progress = vi.spyOn(process.stderr, 'write');
|
|
3768
|
+
try {
|
|
3769
|
+
const row = await prepareIssueWorktree({
|
|
3770
|
+
baseRef: 'main',
|
|
3771
|
+
identity: 'synthetic-author',
|
|
3772
|
+
issueNumber: 17,
|
|
3773
|
+
repoRoot: root,
|
|
3774
|
+
});
|
|
3775
|
+
const bounds = {
|
|
3776
|
+
maxAggregateRssBytes: 1_000_000_000,
|
|
3777
|
+
minFreeDiskBytes: 1,
|
|
3778
|
+
maxNewOutputBytes: 2048,
|
|
3779
|
+
outputRoots: [output],
|
|
3780
|
+
};
|
|
3781
|
+
running = verifyIssue({
|
|
3782
|
+
issueNumber: 17,
|
|
3783
|
+
repoRoot: row.path,
|
|
3784
|
+
resourceBounds: bounds,
|
|
3785
|
+
signal: controller.signal,
|
|
3786
|
+
}).catch((error) => error);
|
|
3787
|
+
if (mode === 'cancel') {
|
|
3788
|
+
for (let attempt = 0; attempt < 300 && !existsSync(join(output, 'capability')); attempt++)
|
|
3789
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
3790
|
+
assert.ok(existsSync(join(output, 'capability')));
|
|
3791
|
+
const cap = JSON.parse(readFileSync(join(output, 'capability'), 'utf8'));
|
|
3792
|
+
for (let attempt = 0; attempt < 300 && readdirSync(join(cap.directory, 'requests')).length === 0; attempt++)
|
|
3793
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
3794
|
+
assert.ok(readdirSync(join(cap.directory, 'requests')).length > 0);
|
|
3795
|
+
controller.abort();
|
|
3796
|
+
}
|
|
3797
|
+
const failure = String(await running);
|
|
3798
|
+
assert.match(failure, mode === 'abrupt' ? /exit 9/u : mode === 'block' ? /reader or teardown is stalled/u : /cancelled/u);
|
|
3799
|
+
assert.doesNotMatch(failure, /cleanup failed|live holder/u);
|
|
3800
|
+
const capability = JSON.parse(readFileSync(join(output, 'capability'), 'utf8'));
|
|
3801
|
+
assert.equal(existsSync(capability.directory), false);
|
|
3802
|
+
assert.ok(existsSync(join(output, 'fifo')));
|
|
3803
|
+
assert.equal(readFileSync(join(output, 'unrelated'), 'utf8'), 'preserved');
|
|
3804
|
+
if (mode !== 'abrupt')
|
|
3805
|
+
assert.ok(progress.mock.calls.filter(([line]) => String(line).includes('sampledAggregateRssBytes')).length > 2);
|
|
3806
|
+
await assert.rejects(verifyIssue({ issueNumber: 17, repoRoot: row.path, resourceBounds: bounds }), /unsupported file/u);
|
|
3807
|
+
}
|
|
3808
|
+
finally {
|
|
3809
|
+
controller.abort();
|
|
3810
|
+
await running;
|
|
3811
|
+
progress.mockRestore();
|
|
3812
|
+
rmSync(root, { recursive: true, force: true });
|
|
3813
|
+
rmSync(output, { recursive: true, force: true });
|
|
3814
|
+
}
|
|
3815
|
+
});
|
|
3816
|
+
test('filesystem fixture gate still enforces positive byte limits after deterministic contention', async () => {
|
|
3817
|
+
const output = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-fixture-limit-')));
|
|
3818
|
+
const { root } = await fixture({ firstStageScript: filesystemFixtureScript(output, 'pass') });
|
|
3819
|
+
try {
|
|
3820
|
+
const row = await prepareIssueWorktree({
|
|
3821
|
+
baseRef: 'main',
|
|
3822
|
+
identity: 'synthetic-author',
|
|
3823
|
+
issueNumber: 17,
|
|
3824
|
+
repoRoot: root,
|
|
3825
|
+
});
|
|
3826
|
+
await assert.rejects(verifyIssue({
|
|
3827
|
+
issueNumber: 17,
|
|
3828
|
+
repoRoot: row.path,
|
|
3829
|
+
resourceBounds: {
|
|
3830
|
+
maxAggregateRssBytes: 1_000_000_000,
|
|
3831
|
+
minFreeDiskBytes: 1,
|
|
3832
|
+
maxNewOutputBytes: 64,
|
|
3833
|
+
outputRoots: [output],
|
|
3834
|
+
},
|
|
3835
|
+
}), /filesystem output.*exceeded/u);
|
|
3836
|
+
assert.equal(existsSync(join(output, 'fifo')), false);
|
|
3837
|
+
assert.equal(readFileSync(join(output, 'regular'), 'utf8').length, 128);
|
|
3838
|
+
}
|
|
3839
|
+
finally {
|
|
3840
|
+
rmSync(root, { recursive: true, force: true });
|
|
3841
|
+
rmSync(output, { recursive: true, force: true });
|
|
3842
|
+
}
|
|
3843
|
+
});
|
|
3844
|
+
test.each(['verification', 'runtime'])('filesystem fixture gate preserves observation and cancellation after direct %s command closure', async (mode) => {
|
|
3845
|
+
const output = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-fixture-close-')));
|
|
3846
|
+
writeFileSync(join(output, 'unrelated'), 'preserved');
|
|
3847
|
+
const script = `
|
|
3848
|
+
(async () => {
|
|
3849
|
+
const fs = require('node:fs'), cp = require('node:child_process');
|
|
3850
|
+
const child = cp.spawn(process.execPath, ['-e', ${JSON.stringify(filesystemFixtureScript(output, 'cancel'))}], { stdio: 'ignore' });
|
|
3851
|
+
child.unref();
|
|
3852
|
+
fs.writeFileSync(${JSON.stringify(join(output, 'pids'))}, JSON.stringify({ root: process.pid, descendant: child.pid }));
|
|
3853
|
+
while (!fs.existsSync(${JSON.stringify(join(output, 'exit-parent'))})) await new Promise(resolve => setTimeout(resolve, 10));
|
|
3854
|
+
process.exit(0);
|
|
3855
|
+
})().catch(error => { console.error(error.message); process.exitCode = 1; });`;
|
|
3856
|
+
const { root } = await fixture({ firstStageScript: script });
|
|
3857
|
+
const controller = new AbortController();
|
|
3858
|
+
const progress = vi.spyOn(process.stderr, 'write');
|
|
3859
|
+
let running;
|
|
3860
|
+
try {
|
|
3861
|
+
const row = await prepareIssueWorktree({
|
|
3862
|
+
baseRef: 'main',
|
|
3863
|
+
identity: 'synthetic-author',
|
|
3864
|
+
issueNumber: 17,
|
|
3865
|
+
repoRoot: root,
|
|
3866
|
+
});
|
|
3867
|
+
const bounds = {
|
|
3868
|
+
maxAggregateRssBytes: 1_000_000_000,
|
|
3869
|
+
minFreeDiskBytes: 1,
|
|
3870
|
+
maxNewOutputBytes: 4096,
|
|
3871
|
+
outputRoots: [output],
|
|
3872
|
+
};
|
|
3873
|
+
const { withRuntimeSetupWriter } = await import('./verification.js');
|
|
3874
|
+
running = (mode === 'verification'
|
|
3875
|
+
? verifyIssue({ issueNumber: 17, repoRoot: row.path, resourceBounds: bounds, signal: controller.signal })
|
|
3876
|
+
: withRuntimeSetupWriter(row.path, (runner) => runner.run([process.execPath, '-e', script], bounds, controller.signal))).catch((error) => error);
|
|
3877
|
+
for (let attempt = 0; attempt < 300 && !existsSync(join(output, 'capability')); attempt++)
|
|
3878
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
3879
|
+
assert.ok(existsSync(join(output, 'capability')));
|
|
3880
|
+
const cap = JSON.parse(readFileSync(join(output, 'capability'), 'utf8'));
|
|
3881
|
+
const pids = JSON.parse(readFileSync(join(output, 'pids'), 'utf8'));
|
|
3882
|
+
const writerPath = join(root, '.git', 'ai-delivery', 'writers@1', digestValue(row.path).slice(7) + '.json');
|
|
3883
|
+
const descendantObserved = () => {
|
|
3884
|
+
const state = JSON.parse(readFileSync(writerPath, 'utf8'));
|
|
3885
|
+
return state.command.tracked?.some((member) => member.pid === pids.descendant) === true;
|
|
3886
|
+
};
|
|
3887
|
+
for (let attempt = 0; attempt < 300 && (!descendantObserved() || readdirSync(join(cap.directory, 'requests')).length === 0); attempt++)
|
|
3888
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
3889
|
+
assert.ok(descendantObserved());
|
|
3890
|
+
assert.ok(readdirSync(join(cap.directory, 'requests')).length > 0);
|
|
3891
|
+
writeFileSync(join(output, 'exit-parent'), 'exit');
|
|
3892
|
+
const parentAlive = () => spawnSync('ps', ['-p', String(pids.root), '-o', 'stat='], { encoding: 'utf8' }).stdout.trim().length > 0;
|
|
3893
|
+
for (let attempt = 0; attempt < 100 && parentAlive(); attempt++)
|
|
3894
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
3895
|
+
assert.equal(parentAlive(), false);
|
|
3896
|
+
const samples = () => progress.mock.calls.filter(([line]) => String(line).includes('sampledAggregateRssBytes')).length;
|
|
3897
|
+
const afterExit = samples();
|
|
3898
|
+
for (let attempt = 0; attempt < 100 && samples() === afterExit; attempt++)
|
|
3899
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
3900
|
+
const observedAfterExit = samples() > afterExit;
|
|
3901
|
+
controller.abort();
|
|
3902
|
+
const failure = String(await running);
|
|
3903
|
+
assert.match(failure, /cancelled/u);
|
|
3904
|
+
assert.doesNotMatch(failure, /cleanup failed|live holder/u);
|
|
3905
|
+
assert.ok(observedAfterExit, 'RSS and disk observation continues while the closed command awaits its scan');
|
|
3906
|
+
assert.equal(existsSync(cap.directory), false);
|
|
3907
|
+
assert.ok(existsSync(join(output, 'fifo')));
|
|
3908
|
+
assert.equal(readFileSync(join(output, 'unrelated'), 'utf8'), 'preserved');
|
|
3909
|
+
assert.equal(spawnSync('ps', ['-p', String(pids.descendant), '-o', 'stat='], { encoding: 'utf8' }).stdout.trim().length, 0);
|
|
3910
|
+
assert.equal(progress.mock.calls.some(([line]) => String(line).includes('"state":"completed"')), false);
|
|
3911
|
+
}
|
|
3912
|
+
finally {
|
|
3913
|
+
controller.abort();
|
|
3914
|
+
await running;
|
|
3915
|
+
progress.mockRestore();
|
|
3916
|
+
rmSync(root, { recursive: true, force: true });
|
|
3917
|
+
rmSync(output, { recursive: true, force: true });
|
|
3918
|
+
}
|
|
3919
|
+
});
|
|
3920
|
+
test('filesystem fixture helper runs standalone and fails closed on malformed capability', async () => {
|
|
3921
|
+
const { withVerificationFilesystemFixture } = await import('./agent.js');
|
|
3922
|
+
assert.equal(await withVerificationFilesystemFixture(() => 'standalone'), 'standalone');
|
|
3923
|
+
const prior = process.env['AI_DELIVERY_OUTPUT_OBSERVATION_GATE'];
|
|
3924
|
+
try {
|
|
3925
|
+
process.env['AI_DELIVERY_OUTPUT_OBSERVATION_GATE'] = '{}';
|
|
3926
|
+
let executed = false;
|
|
3927
|
+
await assert.rejects(withVerificationFilesystemFixture(() => {
|
|
3928
|
+
executed = true;
|
|
3929
|
+
}), /invalid or stale/u);
|
|
3930
|
+
assert.equal(executed, false);
|
|
3931
|
+
}
|
|
3932
|
+
finally {
|
|
3933
|
+
if (prior === undefined)
|
|
3934
|
+
delete process.env['AI_DELIVERY_OUTPUT_OBSERVATION_GATE'];
|
|
3935
|
+
else
|
|
3936
|
+
process.env['AI_DELIVERY_OUTPUT_OBSERVATION_GATE'] = prior;
|
|
3937
|
+
}
|
|
3938
|
+
});
|
|
3939
|
+
test('nested runtime setup inherits the outer gate for its baseline and child command', async () => {
|
|
3940
|
+
const output = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-nested-fixture-')));
|
|
3941
|
+
const inner = await fixture();
|
|
3942
|
+
const worker = filesystemFixtureScript(output, 'pass');
|
|
3943
|
+
const { root } = await fixture({
|
|
3944
|
+
firstStageScript: `
|
|
3945
|
+
(async () => {
|
|
3946
|
+
const fs = require('node:fs'), cp = require('node:child_process'), assert = require('node:assert/strict');
|
|
3947
|
+
const { withRuntimeSetupWriter } = await import(${JSON.stringify(new URL('./verification.js', import.meta.url).href)});
|
|
3948
|
+
const capability = process.env.AI_DELIVERY_OUTPUT_OBSERVATION_GATE;
|
|
3949
|
+
const child = cp.spawn(process.execPath, ['-e', ${JSON.stringify(worker)}], { stdio: ['ignore', 'inherit', 'inherit'] });
|
|
3950
|
+
const closed = new Promise((resolve, reject) => child.once('exit', code => code === 0 ? resolve() : reject(new Error('fixture child exit ' + code))));
|
|
3951
|
+
while (!fs.existsSync(${JSON.stringify(join(output, 'capability'))})) await new Promise(resolve => setTimeout(resolve, 10));
|
|
3952
|
+
assert.equal(fs.readFileSync(${JSON.stringify(join(output, 'capability'))}, 'utf8'), capability);
|
|
3953
|
+
await withRuntimeSetupWriter(${JSON.stringify(inner.root)}, async runner => {
|
|
3954
|
+
await runner.run([process.execPath, '-e', 'require("node:assert/strict").equal(process.env.AI_DELIVERY_OUTPUT_OBSERVATION_GATE, ' + JSON.stringify(capability) + ')'], { maxAggregateRssBytes: 1000000000, minFreeDiskBytes: 1, maxNewOutputBytes: 2048, outputRoots: [${JSON.stringify(output)}] });
|
|
3955
|
+
});
|
|
3956
|
+
await closed;
|
|
3957
|
+
})().catch(error => { console.error(error.message); process.exitCode = 1; });`,
|
|
3958
|
+
});
|
|
3959
|
+
try {
|
|
3960
|
+
const row = await prepareIssueWorktree({
|
|
3961
|
+
baseRef: 'main',
|
|
3962
|
+
identity: 'synthetic-author',
|
|
3963
|
+
issueNumber: 17,
|
|
3964
|
+
repoRoot: root,
|
|
3965
|
+
});
|
|
3966
|
+
await verifyIssue({
|
|
3967
|
+
issueNumber: 17,
|
|
3968
|
+
repoRoot: row.path,
|
|
3969
|
+
resourceBounds: {
|
|
3970
|
+
maxAggregateRssBytes: 1_000_000_000,
|
|
3971
|
+
minFreeDiskBytes: 1,
|
|
3972
|
+
maxNewOutputBytes: 2048,
|
|
3973
|
+
outputRoots: [output],
|
|
3974
|
+
},
|
|
3975
|
+
});
|
|
3976
|
+
assert.deepEqual(readFileSync(join(output, 'events'), 'utf8').trim().split('\n'), [
|
|
3977
|
+
'created',
|
|
3978
|
+
'scan-queued',
|
|
3979
|
+
'reader-rejected',
|
|
3980
|
+
'cleaned',
|
|
3981
|
+
'released',
|
|
3982
|
+
]);
|
|
3983
|
+
assert.equal(existsSync(join(inner.root, '.git', 'ai-delivery', 'output-observation')), false);
|
|
3984
|
+
}
|
|
3985
|
+
finally {
|
|
3986
|
+
rmSync(root, { recursive: true, force: true });
|
|
3987
|
+
rmSync(inner.root, { recursive: true, force: true });
|
|
3988
|
+
rmSync(output, { recursive: true, force: true });
|
|
3989
|
+
}
|
|
3990
|
+
});
|
|
3991
|
+
test('interrupted fixture owner recovers its gate, preserves completed work and rejects a leaked FIFO before resume', async () => {
|
|
3992
|
+
const output = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-fixture-recovery-')));
|
|
3993
|
+
const marker = join(output, 'capability');
|
|
3994
|
+
const { root, counter, secondCounter, failSecond } = await fixture({ twoStages: true });
|
|
3995
|
+
let child;
|
|
3996
|
+
let childOutput = '';
|
|
3997
|
+
try {
|
|
3998
|
+
const row = await prepareIssueWorktree({
|
|
3999
|
+
baseRef: 'main',
|
|
4000
|
+
identity: 'synthetic-author',
|
|
4001
|
+
issueNumber: 17,
|
|
4002
|
+
repoRoot: root,
|
|
4003
|
+
});
|
|
4004
|
+
const bounds = {
|
|
4005
|
+
maxAggregateRssBytes: 1_000_000_000,
|
|
4006
|
+
minFreeDiskBytes: 1,
|
|
4007
|
+
maxNewOutputBytes: 2048,
|
|
4008
|
+
outputRoots: [output],
|
|
4009
|
+
};
|
|
4010
|
+
writeFileSync(failSecond, 'fail once');
|
|
4011
|
+
await assert.rejects(verifyIssue({ issueNumber: 17, repoRoot: row.path, resourceBounds: bounds }), /exit 7/u);
|
|
4012
|
+
assert.equal(readFileSync(counter, 'utf8'), '1');
|
|
4013
|
+
child = spawn(process.execPath, [
|
|
4014
|
+
'-e',
|
|
4015
|
+
`(async () => {
|
|
4016
|
+
const { withRuntimeSetupWriter } = await import(${JSON.stringify(new URL('./verification.js', import.meta.url).href)});
|
|
4017
|
+
await withRuntimeSetupWriter(${JSON.stringify(row.path)}, runner => runner.run([process.execPath, '-e', ${JSON.stringify(filesystemFixtureScript(output, 'cancel'))}], ${JSON.stringify(bounds)}));
|
|
4018
|
+
})().catch(error => { console.error(error.message); process.exitCode = 1; });`,
|
|
4019
|
+
], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
4020
|
+
const capture = (chunk) => {
|
|
4021
|
+
childOutput += chunk.toString();
|
|
4022
|
+
if (childOutput.length > 1024 * 1024)
|
|
4023
|
+
child.kill('SIGKILL');
|
|
4024
|
+
};
|
|
4025
|
+
child.stdout.on('data', capture);
|
|
4026
|
+
child.stderr.on('data', capture);
|
|
4027
|
+
const closed = new Promise((resolve) => child.once('exit', () => resolve()));
|
|
4028
|
+
for (let attempt = 0; attempt < 500 && !existsSync(marker); attempt++)
|
|
4029
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
4030
|
+
assert.ok(existsSync(marker), childOutput);
|
|
4031
|
+
const priorGate = JSON.parse(readFileSync(marker, 'utf8'));
|
|
4032
|
+
child.kill('SIGKILL');
|
|
4033
|
+
await closed;
|
|
4034
|
+
assert.equal(child.signalCode, 'SIGKILL');
|
|
4035
|
+
// Exercise the existing writer's stale-file recovery after the exact parent has exited.
|
|
4036
|
+
const writerPath = join(root, '.git', 'ai-delivery', 'writers@1', digestValue(row.path).slice(7) + '.json');
|
|
4037
|
+
const stale = new Date(Date.now() - 20_000);
|
|
4038
|
+
utimesSync(writerPath + '.lock', stale, stale);
|
|
4039
|
+
await assert.rejects(verifyIssue({ issueNumber: 17, repoRoot: row.path, resourceBounds: bounds }), /unsupported file/u);
|
|
4040
|
+
assert.equal(existsSync(priorGate.directory), false);
|
|
4041
|
+
assert.ok(existsSync(join(output, 'fifo')));
|
|
4042
|
+
assert.equal(readFileSync(counter, 'utf8'), '1');
|
|
4043
|
+
unlinkSync(join(output, 'fifo'));
|
|
4044
|
+
unlinkSync(failSecond);
|
|
4045
|
+
const resumed = await verifyIssue({ issueNumber: 17, repoRoot: row.path, resourceBounds: bounds });
|
|
4046
|
+
assert.equal(resumed.stageReceipts.length, 2);
|
|
4047
|
+
assert.equal(readFileSync(counter, 'utf8'), '1');
|
|
4048
|
+
assert.equal(readFileSync(secondCounter, 'utf8'), '2');
|
|
4049
|
+
assert.ok(resumed.stageReceipts.every((receipt) => receipt.commands.every((command) => command.exitCode === 0)));
|
|
4050
|
+
const cached = await verifyIssue({ issueNumber: 17, repoRoot: row.path, resourceBounds: bounds });
|
|
4051
|
+
assert.deepEqual(cached.stageReceipts.map((receipt) => receipt.receiptId), resumed.stageReceipts.map((receipt) => receipt.receiptId));
|
|
4052
|
+
await verifyIssue({ issueNumber: 17, repoRoot: row.path, resourceBounds: { ...bounds, maxNewOutputBytes: 4096 } });
|
|
4053
|
+
assert.equal(readFileSync(counter, 'utf8'), '2');
|
|
4054
|
+
}
|
|
4055
|
+
finally {
|
|
4056
|
+
child?.kill('SIGKILL');
|
|
4057
|
+
rmSync(root, { recursive: true, force: true });
|
|
4058
|
+
rmSync(output, { recursive: true, force: true });
|
|
4059
|
+
}
|
|
4060
|
+
});
|
|
3666
4061
|
//# sourceMappingURL=lifecycle.test.js.map
|