Files
openchamber/packages/ui/src/components/chat/fileReferenceStat.test.ts
T
Tom Rochette a79ae57ae5 fix(ui): scope file-reference stat probes to the session directory (#3022)
* test: add regression test for stat directory scoping (#3019)

* fix: scope file-reference stat probes to the session directory (#3019)

The /api/fs/stat probe sent by the markdown renderer carried no
directory hint, so the server resolved the workspace from
settings.lastDirectory. When the browsed directory differs from the
session directory the stat returns 400 and the renderer treats it as
file-does-not-exist, silently disabling file-reference links.

Send effectiveDirectory as x-opencode-directory on the probe and
qualify the stat cache key by directory so a rejection under one
directory cannot leak into another. The probe logic moves from
MarkdownRendererImpl into fileReferenceStat with unit coverage.
2026-08-21 16:48:32 +03:00

68 lines
2.8 KiB
TypeScript

import { afterEach, describe, expect, test } from 'bun:test';
import { fileReferenceExists } from './fileReferenceStat';
const originalFetch = globalThis.fetch;
const calls: Array<{ url: string; headers: Headers }> = [];
const stubFetchWith = (respond: () => Response) => {
calls.length = 0;
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
const url = input instanceof Request ? input.url : input.toString();
const headers = new Headers(init?.headers);
calls.push({ url, headers });
return respond();
// SAFETY: the stub preserves the fetch signature; every caller in this
// file restores globalThis.fetch in afterEach.
}) as typeof fetch;
};
afterEach(() => {
globalThis.fetch = originalFetch;
});
describe('fileReferenceExists directory scoping (issue 3019)', () => {
test('sends the session directory on the stat probe', async () => {
stubFetchWith(() => new Response(JSON.stringify({ path: '/repo-b/src/index.ts', isFile: true, size: 12 }), { status: 200 }));
const exists = await fileReferenceExists('/repo-b/src/index.ts', '/repo-b');
expect(exists).toBe(true);
expect(calls).toHaveLength(1);
expect(calls[0].url).toBe('/api/fs/stat?path=%2Frepo-b%2Fsrc%2Findex.ts&optional=true');
expect(calls[0].headers.get('x-opencode-directory')).toBe('/repo-b');
});
test('treats a workspace rejection under one directory as unknown under another directory', async () => {
// Directory A resolves the workspace on the server (the browsed
// lastDirectory), so the probe for a path under B is rejected with 400
// and resolves false. The same path probed under B itself must issue a
// fresh request rather than reuse A's cached rejection.
stubFetchWith(() => {
const directoryHint = calls[calls.length - 1]?.headers.get('x-opencode-directory') ?? null;
if (directoryHint !== '/repo-b') {
return new Response(JSON.stringify({ error: 'Path is outside of active workspace' }), { status: 400 });
}
return new Response(JSON.stringify({ path: '/repo-b/lib/main.ts', isFile: true, size: 12 }), { status: 200 });
});
const rejectedUnderA = await fileReferenceExists('/repo-b/lib/main.ts', '/repo-a');
const acceptedUnderB = await fileReferenceExists('/repo-b/lib/main.ts', '/repo-b');
expect(rejectedUnderA).toBe(false);
expect(acceptedUnderB).toBe(true);
expect(calls).toHaveLength(2);
});
test('serves a repeated probe under the same directory from the cache', async () => {
stubFetchWith(() => new Response(JSON.stringify({ path: '/repo-c/lib.ts', isFile: true, size: 4 }), { status: 200 }));
await fileReferenceExists('/repo-c/lib.ts', '/repo-c');
const warm = await fileReferenceExists('/repo-c/lib.ts', '/repo-c');
expect(warm).toBe(true);
expect(calls).toHaveLength(1);
});
});