One layer below yesterday's fix, and it looked identical on screen: the job was
found, then declared to have written nothing.
`loadBackgroundTask` resolved the located path with `realpathSync`, as the
platform. Resolving a link needs `x` on every directory above it, and
`/tmp/claude-<uid>` is `drwx------` — so it threw EACCES on an INTACT link, and
the catch below it reported that as a dangling symlink and returned null. The
tray polls, gets `{kind:'pending'}`, and shows "Nothing written yet" forever.
The lookup fix could not have exposed this: it made the file findable, which is
precisely what let execution reach the line that then threw it away.
`realpath -e` rather than `readlink -f`, because `-e` requires every component to
exist — that keeps a genuinely dangling link reporting as one instead of
resolving to a path that is not there. Verified against symlink, plain file,
dangling link and missing path: `realpath -e` and `realpathSync` agree on all
four, so the owner path is unchanged.
Audited the rest of claude-sessions.ts for the same shape. Every remaining
mention of `statSync`/`readdirSync`/`Bun.Glob` in that file is a comment
explaining why it is NOT used.
Live coverage extended: the member resolves their own link, the platform gets
null. Skips without OFFICER_TEST_MEMBER.
tsgo clean, frontend builds, 865 pass / 7 fail unchanged, 7 skips.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
95 lines
4.5 KiB
TypeScript
95 lines
4.5 KiB
TypeScript
import { describe, expect, test } from 'bun:test';
|
|
import { join } from 'node:path';
|
|
import { runAsArgv } from './os-user';
|
|
import { findPathAs, realpathAs } 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-<uid>` 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: <tmp>/claude-<uid>/<slug>/<session>/tasks/<id>.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 symlink under the member tree resolves as them and not as us', () => {
|
|
// An AGENT task's `.output` is a symlink into the member's own `~/.claude/.../subagents/*.jsonl`.
|
|
// Resolving it as the platform threw EACCES on an intact link, which the caller reported as dangling —
|
|
// the layer below the lookup, and identical on screen: "Nothing written yet", forever.
|
|
runAs(user, ['mkdir', '-p', dir]);
|
|
runAs(user, ['sh', '-c', 'printf "live\\n" > "$1"; ln -sf "$1" "$2"', '_', file, `${file}.link`]);
|
|
try {
|
|
expect(realpathAs(user, `${file}.link`)).toBe(file);
|
|
expect(realpathAs(null, `${file}.link`)).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')]);
|
|
}
|
|
});
|
|
});
|