import { describe, expect, test } from 'bun:test'; import { join } from 'node:path'; import { runAsArgv } from './os-user'; import { findPathAs } from './read-as-user'; // Can the platform LOCATE a member's file? Same shape as `spawn-as-member.live.test.ts`, and for the same // reason: the failure only exists where a second Linux account does, which is the production host and no // developer machine. // // ── The bug this pins ── // // `findTaskOutput` globbed `/tmp/claude-*/…` as the platform's own uid. `/tmp/claude-` is `drwx------`, // so for a member the glob matched nothing — and a glob that cannot traverse does not throw, it returns an // empty iterator. Downstream that read as "no such task", which a member saw as a background job that // streamed no logs and answered `{"stopped":false,"reason":"no such task"}` on Stop. // // The assertion is the two identities DISAGREEING. A test that only checked `findPathAs(member, …)` finds // the file would pass just as happily on a world-readable tree, which is not the situation that broke. // // ── Running it ── // // OFFICER_TEST_MEMBER=green bun test src/servers/read-as-user.live.test.ts // // Skips without it. A skipped run is not a pass; the log says which it was. const osUser = process.env.OFFICER_TEST_MEMBER; const live = Boolean(osUser); const runAs = (user: string, command: string[]) => { const result = Bun.spawnSync(runAsArgv(user, command), { stdout: 'pipe', stderr: 'pipe' }); if (result.exitCode !== 0) { throw new Error(`as ${user}: ${new TextDecoder().decode(result.stderr).trim() || `exit ${result.exitCode}`}`); } return new TextDecoder().decode(result.stdout).trim(); }; describe.skipIf(!live)(`findPathAs (live, as ${osUser ?? 'nobody'})`, () => { const user = osUser!; // The real layout, at the real depth: /claude-///tasks/.output const uid = live ? runAs(user, ['id', '-u']) : ''; const taskId = 'blivetest01'; const dir = join('/tmp', `claude-${uid}`, '-live-test-slug', 'live-test-session', 'tasks'); const file = join(dir, `${taskId}.output`); test('the member can be found where the platform is blind', () => { // Created BY the member, so its modes are the ones `claude` would produce rather than ones we chose. runAs(user, ['mkdir', '-p', dir]); runAs(user, ['sh', '-c', 'printf "live\\n" > "$1"', '_', file]); runAs(user, ['chmod', '700', join('/tmp', `claude-${uid}`)]); try { const predicate = ['-mindepth', '5', '-maxdepth', '5', '-name', `${taskId}.output`]; // As the member: found. expect(findPathAs(user, '/tmp', predicate)).toBe(file); // As the platform: NOT found — this is the half that used to be the only half. expect(findPathAs(null, '/tmp', predicate)).toBeNull(); } finally { runAs(user, ['rm', '-rf', join('/tmp', `claude-${uid}`, '-live-test-slug')]); } }); test('a task id that does not exist is null, not a throw', () => { expect(findPathAs(user, '/tmp', ['-mindepth', '5', '-maxdepth', '5', '-name', 'no-such-task.output'])).toBeNull(); }); test('an unreadable directory in the walk does not discard a real match', () => { // `find /tmp` from any identity hits another account's `drwx------` and exits 1 having already printed // what it found. This is the regression that cost the first version of the fix: the match was located // and then thrown away with the exit code. runAs(user, ['mkdir', '-p', dir]); runAs(user, ['sh', '-c', 'printf "live\\n" > "$1"', '_', file]); try { expect(findPathAs(user, '/tmp', ['-mindepth', '5', '-maxdepth', '5', '-name', `${taskId}.output`])).toBe(file); } finally { runAs(user, ['rm', '-rf', join('/tmp', `claude-${uid}`, '-live-test-slug')]); } }); });