@aviaratech/ai-delivery 0.3.8 → 0.3.10

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 CHANGED
@@ -323,10 +323,12 @@ The classifier owns the digest values and must include every relevant source, co
323
323
 
324
324
  Verification holds one file lease per worktree and durably records its writer and observed command process identities. A second writer in the same worktree is rejected before execution. Separate worktrees can overlap, including at the same head, with separate stage inputs and mutable manifests. Callers must first qualify their commands' CPU, memory, disk, I/O and shared-file/service requirements; a resource class alone grants no shared-resource exclusivity. The runtime adds no global executor cap. On retry after writer interruption, it refuses live or unknown ownership and reconciles only the recorded command group and observed descendants before reusing compatible completed stages. A launch interrupted before command identity was recorded requires explicit reconciliation. Process identity observation is required on supported POSIX hosts; unavailable or ambiguous observation fails closed.
325
325
 
326
- The `--admit` flag explicitly admits additional resource classes. Publication creates a draft PR only after exact-head verification; a high-risk policy also requires a prepublication review artifact. Ready promotion and merge require a submitted formal review. Merge checks live blockers, checks, base/head coordinates, and the policy boundary. Repositories may require an exact base/head lease for merge.
326
+ The `--admit` flag explicitly admits additional resource classes for execution. Compatible complete stages are reused only after their existing stage, artifact, command-output and applicable resource proofs are fully validated, without admitting execution of those classes. Missing or incompatible proof requires the current invocation's admission for the actual stage class before any command runs; corrupt proof remains a hard failure. This permits serial `source_only`, `model`, then `postgres_docker` invocations to reuse prior completed stages while admitting only the next class to execute. Publication creates a draft PR only after exact-head verification; a high-risk policy also requires a prepublication review artifact. Ready promotion and merge require a submitted formal review. Merge checks live blockers, checks, base/head coordinates, and the policy boundary. Repositories may require an exact base/head lease for merge.
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
+ During a running command, an `ENOENT` from symlink resolution can reflect ordinary tree removal. The observer reports the first path and errno and retries complete scans for at most one second; it never accepts a partial scan or resets that observation-stall window. Cancellation interrupts the wait, and RSS and disk checks continue. Baseline and final scans remain strict; persistent broken links, cycles and escaping aliases fail closed. Failed commands retain captured stdout/stderr in the existing private content-addressed command-output store, with its digest and the actual exit status in the failure diagnostic. Output still buffered inside a killed child is unavailable to the runner.
331
+
330
332
  Real filesystem-negative tests can use `withVerificationFilesystemFixture` from `@aviaratech/ai-delivery` (or its `agent` export):
331
333
 
332
334
  ```js
@@ -12,7 +12,7 @@ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
12
12
  import { AI_DELIVERY_MCP_TOOLS } from './mcp/tools.js';
13
13
  import { createAiDeliveryMcpServer } from './mcp/index.js';
14
14
  import { loadDeliveryConfig, parseDeliveryConfig } from './config/deliveryConfig.js';
15
- import { digestValue } from './delivery/index.js';
15
+ import { digestValue, stableJson } from './delivery/index.js';
16
16
  import { contextFor, executeTool, resumedIssueUpdate, startTrackedIssue } from './dispatch.js';
17
17
  import { defaultBaseRef, gitCommonDir } from './git.js';
18
18
  import * as githubClient from './github/client.js';
@@ -210,6 +210,7 @@ async function fixture(options = {}) {
210
210
  const remoteName = options.remote ?? 'origin';
211
211
  const counter = join(root, '.git', 'stage-count.txt');
212
212
  const secondCounter = join(root, '.git', 'second-stage-count.txt');
213
+ const thirdCounter = join(root, '.git', 'third-stage-count.txt');
213
214
  const failSecond = join(root, '.git', 'fail-second-stage');
214
215
  try {
215
216
  git(root, 'init', '-q', '-b', 'main');
@@ -290,11 +291,16 @@ export default {
290
291
  requiredStages: [{ id: 'check', dependsOn: [], semanticInputKeys: ['source'],
291
292
  ${options.componentPolicy ? "semanticInputs: [{ key: 'source', digest: bytes(readFileSync(new URL('./artifact.txt', import.meta.url))) }]," : ''}
292
293
  resourceClass: 'source_only', commands: [{ label: 'count', argv: [process.execPath, '-e',
293
- ${JSON.stringify(options.firstStageScript ?? `const fs=require('fs');const p=${JSON.stringify(counter)};fs.writeFileSync(p,String(Number(fs.existsSync(p)?fs.readFileSync(p,'utf8'):0)+1));`)}] }] }${options.twoStages
294
+ ${JSON.stringify(options.firstStageScript ?? `const fs=require('fs');const p=${JSON.stringify(counter)};fs.writeFileSync(p,String(Number(fs.existsSync(p)?fs.readFileSync(p,'utf8'):0)+1));`)}] }] }${options.twoStages || options.serialResourceStages
294
295
  ? `, { id: 'second', dependsOn: ['check'], semanticInputKeys: ['source'],
295
296
  ${options.componentPolicy ? "semanticInputs: [{ key: 'source', digest: bytes(readFileSync(new URL('./component.txt', import.meta.url))) }]," : ''}
296
- resourceClass: 'source_only', commands: [{ label: 'retry', argv: [process.execPath, '-e',
297
+ resourceClass: '${options.serialResourceStages ? 'model' : 'source_only'}', commands: [{ label: 'retry', argv: [process.execPath, '-e',
297
298
  ${JSON.stringify(options.secondStageScript ?? `const fs=require('fs');const p=${JSON.stringify(secondCounter)};fs.writeFileSync(p,String(Number(fs.existsSync(p)?fs.readFileSync(p,'utf8'):0)+1));if(fs.existsSync(${JSON.stringify(failSecond)}))process.exit(7);`)}] }] }`
299
+ : ''}${options.serialResourceStages
300
+ ? `, { id: 'postgres', dependsOn: ['second'], semanticInputKeys: ['source'],
301
+ ${options.componentPolicy ? "semanticInputs: [{ key: 'source', digest: bytes(readFileSync(new URL('./component.txt', import.meta.url))) }]," : ''}
302
+ resourceClass: 'postgres_docker', commands: [{ label: 'count', argv: [process.execPath, '-e',
303
+ ${JSON.stringify(`const fs=require('fs');const p=${JSON.stringify(thirdCounter)};fs.writeFileSync(p,String(Number(fs.existsSync(p)?fs.readFileSync(p,'utf8'):0)+1));`)}] }] }`
298
304
  : ''}],
299
305
  risk: 'standard' };
300
306
  },
@@ -353,7 +359,7 @@ export default {
353
359
  mkdirSync(dirname(admissionPath), { recursive: true });
354
360
  if (!options.omitRuntimeAdmission)
355
361
  writeFileSync(admissionPath, JSON.stringify({ ...content, admissionId: digestValue(content) }), { mode: 0o600 });
356
- return { root, counter, secondCounter, failSecond, runtimeEntryPath };
362
+ return { root, counter, secondCounter, thirdCounter, failSecond, runtimeEntryPath };
357
363
  }
358
364
  catch (error) {
359
365
  rmSync(root, { recursive: true, force: true });
@@ -1803,6 +1809,224 @@ test('a bounded passing run records measured resources in versioned evidence', a
1803
1809
  rmSync(root, { recursive: true, force: true });
1804
1810
  }
1805
1811
  });
1812
+ test.each([
1813
+ { bounded: false, componentPolicy: false },
1814
+ { bounded: true, componentPolicy: false },
1815
+ { bounded: true, componentPolicy: true },
1816
+ ])('serial admission reuses complete stages with bounds $bounded and component policy $componentPolicy', async ({ bounded, componentPolicy }) => {
1817
+ const { root, counter, secondCounter, thirdCounter } = await fixture({
1818
+ serialResourceStages: true,
1819
+ componentPolicy,
1820
+ });
1821
+ try {
1822
+ const row = await prepareIssueWorktree({
1823
+ baseRef: 'main',
1824
+ identity: 'synthetic-author',
1825
+ issueNumber: 17,
1826
+ repoRoot: root,
1827
+ });
1828
+ const input = {
1829
+ issueNumber: 17,
1830
+ repoRoot: row.path,
1831
+ ...(bounded ? { resourceBounds: { maxAggregateRssBytes: 1_000_000_000, minFreeDiskBytes: 1 } } : {}),
1832
+ };
1833
+ await assert.rejects(verifyIssue({ ...input, admittedResourceClasses: ['source_only'] }), /Stage 'second' requires explicit model admission/u);
1834
+ assert.equal(readFileSync(counter, 'utf8'), '1');
1835
+ assert.equal(existsSync(secondCounter), false);
1836
+ assert.equal(existsSync(thirdCounter), false);
1837
+ await assert.rejects(verifyIssue({ ...input, admittedResourceClasses: ['model'] }), /Stage 'postgres' requires explicit postgres_docker admission/u);
1838
+ assert.equal(readFileSync(counter, 'utf8'), '1');
1839
+ assert.equal(readFileSync(secondCounter, 'utf8'), '1');
1840
+ assert.equal(existsSync(thirdCounter), false);
1841
+ const completed = await verifyIssue({ ...input, admittedResourceClasses: ['postgres_docker'] });
1842
+ const repeated = await verifyIssue({ ...input, admittedResourceClasses: [] });
1843
+ assert.deepEqual(repeated.stageReceipts, completed.stageReceipts);
1844
+ for (const path of [counter, secondCounter, thirdCounter])
1845
+ assert.equal(readFileSync(path, 'utf8'), '1');
1846
+ if (bounded) {
1847
+ assert.deepEqual(repeated.resources, completed.resources);
1848
+ assert.ok((repeated.resources?.sampleCount ?? 0) > 0);
1849
+ const first = completed.stageReceipts[0];
1850
+ const stagePath = join(root, '.git', 'ai-delivery', first.input.schemaVersion === 'ai-delivery.stage-input@2' ? 'verification@2' : 'verification@1', 'stages', 'check', `${first.input.inputId.slice(7)}.json`);
1851
+ rmSync(stagePath);
1852
+ const repaired = await verifyIssue({ ...input, admittedResourceClasses: [] });
1853
+ assert.deepEqual(repaired.stageReceipts, completed.stageReceipts);
1854
+ assert.deepEqual(repaired.resources, completed.resources);
1855
+ assert.equal(existsSync(stagePath), true);
1856
+ assert.equal(readFileSync(counter, 'utf8'), '1');
1857
+ }
1858
+ }
1859
+ finally {
1860
+ rmSync(root, { recursive: true, force: true });
1861
+ }
1862
+ });
1863
+ test.each(['missing', 'unmeasured', 'source', 'command', 'environment', 'runtime', 'producer', 'bounds'])('serial admission refuses execution after %s proof invalidation', async (changed) => {
1864
+ const { root, counter, secondCounter, thirdCounter } = await fixture({ serialResourceStages: true });
1865
+ const previousPath = process.env.PATH;
1866
+ const versionDescriptor = Object.getOwnPropertyDescriptor(process, 'version');
1867
+ const producerPath = join(import.meta.dirname, 'serial-producer-fixture.js');
1868
+ let producerOwned = false;
1869
+ try {
1870
+ const row = await prepareIssueWorktree({
1871
+ baseRef: 'main',
1872
+ identity: 'synthetic-author',
1873
+ issueNumber: 17,
1874
+ repoRoot: root,
1875
+ });
1876
+ const input = {
1877
+ issueNumber: 17,
1878
+ repoRoot: row.path,
1879
+ resourceBounds: { maxAggregateRssBytes: 1_000_000_000, minFreeDiskBytes: 1 },
1880
+ };
1881
+ const completed = await verifyIssue({
1882
+ ...(changed === 'unmeasured' ? { issueNumber: input.issueNumber, repoRoot: input.repoRoot } : input),
1883
+ admittedResourceClasses: ['source_only', 'model', 'postgres_docker'],
1884
+ });
1885
+ if (changed === 'missing') {
1886
+ const id = completed.stageReceipts[0].input.inputId.slice(7);
1887
+ rmSync(join(root, '.git', 'ai-delivery', 'verification@1', 'stages', 'check', `${id}.json`));
1888
+ rmSync(join(root, '.git', 'ai-delivery', 'resource-stages@1', `${id}.json`));
1889
+ }
1890
+ else if (changed === 'source' || changed === 'command') {
1891
+ const path = join(row.path, changed === 'source' ? 'change.txt' : 'policy.mjs');
1892
+ writeFileSync(path, changed === 'source'
1893
+ ? 'changed source\n'
1894
+ : readFileSync(path, 'utf8').replace("label: 'count'", "label: 'changed-count'"));
1895
+ git(row.path, 'add', '.');
1896
+ git(row.path, 'commit', '-qm', 'synthetic checkpoint invalidation');
1897
+ }
1898
+ else if (changed === 'environment')
1899
+ process.env.PATH = `${previousPath ?? ''}:/synthetic-environment`;
1900
+ else if (changed === 'runtime')
1901
+ Object.defineProperty(process, 'version', { value: 'v0.0.0' });
1902
+ else if (changed === 'producer') {
1903
+ assert.equal(existsSync(producerPath), false);
1904
+ writeFileSync(producerPath, '// changed synthetic runner identity\n');
1905
+ producerOwned = true;
1906
+ }
1907
+ else if (changed === 'bounds')
1908
+ input.resourceBounds.maxAggregateRssBytes -= 1;
1909
+ await assert.rejects(verifyIssue({ ...input, admittedResourceClasses: ['postgres_docker'] }), /Stage 'check' requires explicit source_only admission/u);
1910
+ for (const path of [counter, secondCounter, thirdCounter])
1911
+ assert.equal(readFileSync(path, 'utf8'), '1');
1912
+ }
1913
+ finally {
1914
+ if (previousPath === undefined)
1915
+ delete process.env.PATH;
1916
+ else
1917
+ process.env.PATH = previousPath;
1918
+ Object.defineProperty(process, 'version', versionDescriptor);
1919
+ if (producerOwned)
1920
+ rmSync(producerPath);
1921
+ rmSync(root, { recursive: true, force: true });
1922
+ }
1923
+ });
1924
+ test.each([
1925
+ 'stage',
1926
+ 'canonical',
1927
+ 'artifact',
1928
+ 'output',
1929
+ 'resource',
1930
+ 'resource-bounds',
1931
+ 'resource-samples',
1932
+ 'missing-resource',
1933
+ 'baseline',
1934
+ ])('serial admission rejects corrupt %s proof before considering execution', async (corrupt) => {
1935
+ const { root, counter, secondCounter, thirdCounter } = await fixture({ serialResourceStages: true });
1936
+ try {
1937
+ const row = await prepareIssueWorktree({
1938
+ baseRef: 'main',
1939
+ identity: 'synthetic-author',
1940
+ issueNumber: 17,
1941
+ repoRoot: root,
1942
+ });
1943
+ mkdirSync(join(row.path, '.issue-cli'), { recursive: true });
1944
+ const input = {
1945
+ issueNumber: 17,
1946
+ repoRoot: row.path,
1947
+ resourceBounds: {
1948
+ maxAggregateRssBytes: 1_000_000_000,
1949
+ minFreeDiskBytes: 1,
1950
+ maxNewOutputBytes: 1_000_000,
1951
+ outputRoots: ['.issue-cli'],
1952
+ },
1953
+ };
1954
+ const completed = await verifyIssue({
1955
+ ...input,
1956
+ admittedResourceClasses: ['source_only', 'model', 'postgres_docker'],
1957
+ });
1958
+ const first = completed.stageReceipts[0];
1959
+ const common = join(root, '.git', 'ai-delivery');
1960
+ const id = first.input.inputId.slice(7);
1961
+ const stagePath = join(common, 'verification@1', 'stages', 'check', `${id}.json`);
1962
+ const resourcePath = join(common, 'resource-stages@1', `${id}.json`);
1963
+ if (corrupt === 'stage')
1964
+ writeFileSync(stagePath, 'corrupt stage proof\n');
1965
+ else if (corrupt === 'canonical')
1966
+ writeFileSync(stagePath, `${readFileSync(stagePath, 'utf8')}\n`);
1967
+ else if (corrupt === 'artifact') {
1968
+ const { receiptId: _receiptId, ...content } = first;
1969
+ const altered = { ...content, artifacts: [{ path: 'artifact.txt', digest: digestValue('corrupt artifact') }] };
1970
+ writeFileSync(stagePath, stableJson({ ...altered, receiptId: digestValue(altered) }));
1971
+ }
1972
+ else if (corrupt === 'output') {
1973
+ writeFileSync(join(common, 'verification@1', 'command-output', `${first.commands[0].outputDigest.slice(7)}.bin`), 'corrupt output');
1974
+ }
1975
+ else if (corrupt === 'resource')
1976
+ writeFileSync(resourcePath, 'corrupt resource proof\n');
1977
+ else if (corrupt === 'missing-resource')
1978
+ rmSync(resourcePath);
1979
+ else if (corrupt === 'baseline') {
1980
+ const directory = join(common, 'output-baselines@1');
1981
+ const file = readdirSync(directory).find((name) => name.endsWith('.json') && !name.endsWith('.state.json'));
1982
+ writeFileSync(join(directory, file), 'corrupt baseline proof\n');
1983
+ }
1984
+ else {
1985
+ const { checkpointId: _checkpointId, ...content } = JSON.parse(readFileSync(resourcePath, 'utf8'));
1986
+ if (corrupt === 'resource-bounds')
1987
+ content.resources.bounds.maxAggregateRssBytes += 1;
1988
+ else
1989
+ content.resources.sampleCount = 0;
1990
+ writeFileSync(resourcePath, JSON.stringify({ ...content, checkpointId: digestValue(content) }));
1991
+ }
1992
+ await assert.rejects(verifyIssue({ ...input, admittedResourceClasses: ['postgres_docker'] }), (error) => error instanceof Error && !error.message.includes('admission'));
1993
+ for (const path of [counter, secondCounter, thirdCounter])
1994
+ assert.equal(readFileSync(path, 'utf8'), '1');
1995
+ }
1996
+ finally {
1997
+ rmSync(root, { recursive: true, force: true });
1998
+ }
1999
+ });
2000
+ test('serial admission requires dependent execution after a real upstream rerun', async () => {
2001
+ const { root, counter, secondCounter, thirdCounter } = await fixture({ serialResourceStages: true });
2002
+ try {
2003
+ const row = await prepareIssueWorktree({
2004
+ baseRef: 'main',
2005
+ identity: 'synthetic-author',
2006
+ issueNumber: 17,
2007
+ repoRoot: root,
2008
+ });
2009
+ const input = {
2010
+ issueNumber: 17,
2011
+ repoRoot: row.path,
2012
+ resourceBounds: { maxAggregateRssBytes: 1_000_000_000, minFreeDiskBytes: 1 },
2013
+ };
2014
+ const completed = await verifyIssue({
2015
+ ...input,
2016
+ admittedResourceClasses: ['source_only', 'model', 'postgres_docker'],
2017
+ });
2018
+ const id = completed.stageReceipts[0].input.inputId.slice(7);
2019
+ rmSync(join(root, '.git', 'ai-delivery', 'verification@1', 'stages', 'check', `${id}.json`));
2020
+ rmSync(join(root, '.git', 'ai-delivery', 'resource-stages@1', `${id}.json`));
2021
+ await assert.rejects(verifyIssue({ ...input, admittedResourceClasses: ['source_only'] }), /Stage 'second' requires explicit model admission/u);
2022
+ assert.equal(readFileSync(counter, 'utf8'), '2');
2023
+ assert.equal(readFileSync(secondCounter, 'utf8'), '1');
2024
+ assert.equal(readFileSync(thirdCounter, 'utf8'), '1');
2025
+ }
2026
+ finally {
2027
+ rmSync(root, { recursive: true, force: true });
2028
+ }
2029
+ });
1806
2030
  test('bounded verification reruns unmeasured work and reuses only matching measured stages', async () => {
1807
2031
  const { root, counter, secondCounter, failSecond } = await fixture({ twoStages: true });
1808
2032
  try {
@@ -3797,6 +4021,170 @@ test.each([
3797
4021
  test('an unrelated consumer uses public MCP setup through verified configured-author publication, counted review readback, merge and cleanup', { timeout: 0 }, async () => {
3798
4022
  await syntheticLifecycle({ remote: 'origin', divergentOrigin: false, producerOnboarding: true });
3799
4023
  });
4024
+ test.each(['pass', 'command-error', 'cancel', 'persistent', 'limit'])('ordinary owned symlink teardown preserves observation and %s command evidence', async (mode) => {
4025
+ const output = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-ordinary-teardown-')));
4026
+ const { root } = await fixture();
4027
+ const controller = new AbortController();
4028
+ const events = join(output, 'events');
4029
+ const ready = join(output, 'ready');
4030
+ const release = join(output, 'release');
4031
+ writeFileSync(join(output, 'unrelated'), 'preserved');
4032
+ writeFileSync(join(output, 'target'), 'x'.repeat(128));
4033
+ symlinkSync('./target', join(output, 'alias'));
4034
+ let observedGap = false;
4035
+ const observations = [];
4036
+ let releaseTimer;
4037
+ const progress = vi.spyOn(process.stderr, 'write').mockImplementation((line) => {
4038
+ if (String(line).includes('ENOENT'))
4039
+ observations.push(String(line));
4040
+ if (!observedGap && existsSync(ready) && String(line).includes('sampledAggregateRssBytes')) {
4041
+ observedGap = true;
4042
+ // Synchronize real removal with the sample preceding the synchronous filesystem scan.
4043
+ releaseTimer = setTimeout(() => {
4044
+ if (mode === 'cancel')
4045
+ controller.abort();
4046
+ else if (mode !== 'persistent')
4047
+ writeFileSync(release, 'continue');
4048
+ }, 100);
4049
+ }
4050
+ return true;
4051
+ });
4052
+ const script = `
4053
+ (async () => {
4054
+ const fs = require('node:fs');
4055
+ const trace = value => fs.appendFileSync(${JSON.stringify(events)}, value + '\\n');
4056
+ trace('command-started');
4057
+ if (['command-error', 'persistent'].includes(${JSON.stringify(mode)})) console.error('synthetic buffered preparation failure');
4058
+ fs.unlinkSync(${JSON.stringify(join(output, 'target'))});
4059
+ trace('target-removed'); fs.writeFileSync(${JSON.stringify(ready)}, 'ready');
4060
+ if (${JSON.stringify(mode)} === 'limit') fs.writeFileSync(${JSON.stringify(join(output, 'retained'))}, 'x'.repeat(512));
4061
+ while (!fs.existsSync(${JSON.stringify(release)})) await new Promise(resolve => setTimeout(resolve, 10));
4062
+ fs.unlinkSync(${JSON.stringify(join(output, 'alias'))}); trace('alias-removed');
4063
+ process.exitCode = ${mode === 'command-error' ? 7 : 0};
4064
+ })().catch(error => { console.error(error); process.exitCode = 1; });`;
4065
+ try {
4066
+ const { withRuntimeSetupWriter } = await import('./verification.js');
4067
+ const run = withRuntimeSetupWriter(root, (runner) => runner.run([process.execPath, '-e', script], {
4068
+ maxAggregateRssBytes: 1_000_000_000,
4069
+ minFreeDiskBytes: 1,
4070
+ maxNewOutputBytes: mode === 'limit' ? 256 : 4096,
4071
+ outputRoots: [output],
4072
+ }, controller.signal));
4073
+ if (mode === 'pass')
4074
+ await run;
4075
+ else
4076
+ await assert.rejects(run, (error) => {
4077
+ const message = String(error);
4078
+ assert.match(message, mode === 'cancel'
4079
+ ? /cancelled/u
4080
+ : mode === 'persistent'
4081
+ ? /remained unavailable.*First observation:.*ENOENT/u
4082
+ : mode === 'limit'
4083
+ ? /filesystem output.*exceeded/u
4084
+ : /exit 7/u);
4085
+ assert.doesNotMatch(message, /cleanup failed/u);
4086
+ assert.ok(message.includes(join(output, 'alias')));
4087
+ assert.match(message, /ENOENT/u);
4088
+ if (mode === 'command-error' || mode === 'persistent') {
4089
+ const digest = /Command output (sha256:[a-f0-9]{64})/u.exec(message)?.[1];
4090
+ assert.ok(digest);
4091
+ assert.match(readFileSync(join(root, '.git', 'ai-delivery', 'verification@1', 'command-output', `${digest.slice(7)}.bin`), 'utf8'), /synthetic buffered preparation failure/u);
4092
+ }
4093
+ return true;
4094
+ });
4095
+ assert.ok(observedGap);
4096
+ assert.ok(observations.some((line) => {
4097
+ const observation = JSON.parse(line.slice(line.indexOf('{')));
4098
+ return observation.reason.includes(join(output, 'alias'));
4099
+ }));
4100
+ assert.equal(readFileSync(join(output, 'unrelated'), 'utf8'), 'preserved');
4101
+ console.log(JSON.stringify({
4102
+ scenario: 'ordinary-owned-symlink-teardown',
4103
+ mode,
4104
+ firstObservation: JSON.parse(observations[0].slice(observations[0].indexOf('{')))
4105
+ .reason,
4106
+ events: readFileSync(events, 'utf8').trim().split('\n'),
4107
+ unrelatedPreserved: true,
4108
+ aliasRetained: readdirSync(output).includes('alias'),
4109
+ }));
4110
+ assert.deepEqual(readFileSync(events, 'utf8').trim().split('\n'), mode === 'cancel' || mode === 'persistent'
4111
+ ? ['command-started', 'target-removed']
4112
+ : ['command-started', 'target-removed', 'alias-removed']);
4113
+ if (mode === 'cancel' || mode === 'persistent') {
4114
+ await assert.rejects(withRuntimeSetupWriter(root, (runner) => runner.run([process.execPath, '-e', ''], {
4115
+ maxAggregateRssBytes: 1_000_000_000,
4116
+ minFreeDiskBytes: 1,
4117
+ maxNewOutputBytes: 4096,
4118
+ outputRoots: [output],
4119
+ })), (error) => {
4120
+ assert.ok(String(error).includes(join(output, 'alias')));
4121
+ assert.match(String(error), /ENOENT/u);
4122
+ return true;
4123
+ });
4124
+ }
4125
+ }
4126
+ finally {
4127
+ controller.abort();
4128
+ if (releaseTimer !== undefined)
4129
+ clearTimeout(releaseTimer);
4130
+ progress.mockRestore();
4131
+ rmSync(root, { recursive: true, force: true });
4132
+ rmSync(output, { recursive: true, force: true });
4133
+ }
4134
+ });
4135
+ test('injected observation deadline rejects a late complete real filesystem retry', async () => {
4136
+ const output = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-observation-deadline-')));
4137
+ const { root } = await fixture();
4138
+ const target = join(output, 'target'), alias = join(output, 'alias'), release = join(output, 'release');
4139
+ writeFileSync(target, 'target');
4140
+ symlinkSync('./target', alias);
4141
+ const now = Date.now.bind(Date);
4142
+ let offset = 0;
4143
+ let firstObservation;
4144
+ let advance;
4145
+ const clock = vi.spyOn(Date, 'now').mockImplementation(() => now() + offset);
4146
+ const progress = vi.spyOn(process.stderr, 'write').mockImplementation((line) => {
4147
+ if (firstObservation === undefined && String(line).includes('ENOENT')) {
4148
+ firstObservation = String(line);
4149
+ advance = setTimeout(() => {
4150
+ // This test injects only the deadline. Alias removal and the subsequent complete scan are real.
4151
+ unlinkSync(alias);
4152
+ offset = 1100;
4153
+ writeFileSync(release, 'finish');
4154
+ }, 0);
4155
+ }
4156
+ return true;
4157
+ });
4158
+ const script = `(async () => {
4159
+ const fs = require('node:fs'); fs.unlinkSync(${JSON.stringify(target)});
4160
+ while (!fs.existsSync(${JSON.stringify(release)})) await new Promise(resolve => setTimeout(resolve, 10));
4161
+ })().catch(error => { console.error(error); process.exitCode = 1; });`;
4162
+ try {
4163
+ const { withRuntimeSetupWriter } = await import('./verification.js');
4164
+ await assert.rejects(withRuntimeSetupWriter(root, (runner) => runner.run([process.execPath, '-e', script], {
4165
+ maxAggregateRssBytes: 1_000_000_000,
4166
+ minFreeDiskBytes: 1,
4167
+ maxNewOutputBytes: 4096,
4168
+ outputRoots: [output],
4169
+ })), (error) => {
4170
+ assert.match(String(error), /remained unavailable for 1 second/u);
4171
+ assert.ok(String(error).includes(alias));
4172
+ assert.match(String(error), /ENOENT/u);
4173
+ assert.doesNotMatch(String(error), /cleanup failed/u);
4174
+ return true;
4175
+ });
4176
+ assert.ok(firstObservation);
4177
+ assert.equal(readdirSync(output).includes('alias'), false);
4178
+ }
4179
+ finally {
4180
+ if (advance !== undefined)
4181
+ clearTimeout(advance);
4182
+ progress.mockRestore();
4183
+ clock.mockRestore();
4184
+ rmSync(root, { recursive: true, force: true });
4185
+ rmSync(output, { recursive: true, force: true });
4186
+ }
4187
+ });
3800
4188
  test.each(['escape', 'broken', 'cycle'])('output observation rejects %s aliases without running policy commands', async (kind) => {
3801
4189
  const { root, counter } = await fixture();
3802
4190
  const base = realpathSync(mkdtempSync(join(tmpdir(), 'ai-delivery-output-alias-')));