diff --git a/.changeset/short-rstest-socket-roots.md b/.changeset/short-rstest-socket-roots.md new file mode 100644 index 000000000..b508c476b --- /dev/null +++ b/.changeset/short-rstest-socket-roots.md @@ -0,0 +1,5 @@ +--- +"agent-bundle": patch +--- + +Keep Doctor socket fixtures reliable under long local-CI temporary directory names by deriving short, isolated Rstest worker roots. diff --git a/docs/local-ci.md b/docs/local-ci.md index b0975cf18..56e270710 100644 --- a/docs/local-ci.md +++ b/docs/local-ci.md @@ -42,9 +42,13 @@ each other's temp traffic: suites that assert temp-root hygiene (for example `cli.test.ts` scans `os.tmpdir()` for leaked `agent-bundle-artifact-*` directories) only ever see their own leg's directories, so a sibling leg's in-flight work cannot fail them — while a directory the leg itself leaks -still fails its own scan. Legs live under `.worktrees/local-ci/` -(gitignored), are reused across runs for warm caches, and can be recreated -with `--fresh`. +still fails its own scan. Rstest re-hashes that leg directory, worker ID, and +invocation identity to `/tmp/ab-rstest-` before exposing its worker +`TMPDIR`; this leaves headroom below Linux's 108-byte `sun_path` cap for nested +socket fixtures without sacrificing per-leg, per-worker, or concurrent-run +isolation. Legs live under +`.worktrees/local-ci/` (gitignored), are reused across runs for warm caches, +and can be recreated with `--fresh`. The three Verify legs below mirror the hosted `main`-push matrix. On PRs, only `verify (24)` runs hosted. diff --git a/packages/agent-bundle/tests/rstest-worker-isolation.test.ts b/packages/agent-bundle/tests/rstest-worker-isolation.test.ts new file mode 100644 index 000000000..c4cc985a4 --- /dev/null +++ b/packages/agent-bundle/tests/rstest-worker-isolation.test.ts @@ -0,0 +1,29 @@ +import { join } from 'node:path'; + +import { expect, it } from '@rstest/core'; + +import { rstestWorkerRootPath } from '../../../rstest.worker-isolation.ts'; + +it('keeps Doctor socket fixtures below the Linux AF_UNIX pathname cap', () => { + const longLocalCiRoot = join( + '/tmp', + `abci-repository-hash-${'verify-current-pathological-node-version-'.repeat(3)}`, + ); + const workerRoot = rstestWorkerRootPath(longLocalCiRoot, '123', 'linux'); + const longestDoctorEndpoint = join( + workerRoot, + 'agent-bundle-doctor-XXXXXX', + 'endpoints', + 'event-identity.sock', + ); + + // Linux sun_path is 108 bytes including NUL; 96 leaves 12 bytes of headroom. + expect(Buffer.byteLength(longestDoctorEndpoint, 'utf8') + 1).toBeLessThanOrEqual(96); +}); + +it('isolates concurrent Rstest invocations that share a host temporary root', () => { + const firstRoot = rstestWorkerRootPath('/tmp', '1', 'linux', '/workspace/first\0' + '101'); + const secondRoot = rstestWorkerRootPath('/tmp', '1', 'linux', '/workspace/second\0' + '202'); + + expect(firstRoot).not.toBe(secondRoot); +}); diff --git a/rstest.worker-isolation.ts b/rstest.worker-isolation.ts index 7ada03076..4329eb0e9 100644 --- a/rstest.worker-isolation.ts +++ b/rstest.worker-isolation.ts @@ -1,3 +1,4 @@ +import { createHash } from 'node:crypto'; import { mkdirSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -6,8 +7,26 @@ export const rstestWorkerId = (): string => process.env['RSTEST_WORKER_ID'] ?? ' const hostTemporaryRoot = tmpdir(); +export const rstestWorkerRootPath = ( + temporaryRoot: string, + workerId: string, + platform: NodeJS.Platform = process.platform, + invocationId: string = process.cwd() + '\0' + String(process.pid), +): string => { + if (platform === 'win32') return join(temporaryRoot, 'agent-bundle-rstest-w' + workerId); + const hash = createHash('sha256') + .update(temporaryRoot, 'utf8') + .update('\0', 'utf8') + .update(workerId, 'utf8') + .update('\0', 'utf8') + .update(invocationId, 'utf8') + .digest('hex') + .slice(0, 16); + return join('/tmp', `ab-rstest-${hash}`); +}; + export const rstestWorkerRoot = (): string => { - const root = join(hostTemporaryRoot, 'agent-bundle-rstest-w' + rstestWorkerId()); + const root = rstestWorkerRootPath(hostTemporaryRoot, rstestWorkerId()); mkdirSync(root, { recursive: true }); return root; };