resolving a member's task symlink needs their identity too

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>
This commit is contained in:
2026-08-16 03:30:57 +00:00
co-authored by Claude Opus 5
parent 6bee6b856e
commit bf44a4cfab
3 changed files with 50 additions and 8 deletions
+15 -1
View File
@@ -1,7 +1,7 @@
import { describe, expect, test } from 'bun:test';
import { join } from 'node:path';
import { runAsArgv } from './os-user';
import { findPathAs } from './read-as-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
@@ -61,6 +61,20 @@ describe.skipIf(!live)(`findPathAs (live, as ${osUser ?? 'nobody'})`, () => {
}
});
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();
});