68 lines
2.8 KiB
TypeScript
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);
|
||
|
|
});
|
||
|
|
});
|