// v1.13.17-cross-repo-reads: pathGuard now accepts an optional extraRoots // list. Validates the primary-root path stays the source of truth and that // extra roots are consulted when (and only when) the primary rejects. import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { mkdtemp, rm, mkdir, writeFile, symlink } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { realpath } from 'node:fs/promises'; import { pathGuard, PathScopeError } from '../path_guard.js'; let tmp: string; let projectRoot: string; let altRoot: string; let outsideDir: string; beforeAll(async () => { tmp = await realpath(await mkdtemp(join(tmpdir(), 'boocode-pg-'))); projectRoot = join(tmp, 'project'); altRoot = join(tmp, 'alt'); outsideDir = join(tmp, 'outside'); await mkdir(projectRoot, { recursive: true }); await mkdir(altRoot, { recursive: true }); await mkdir(outsideDir, { recursive: true }); await writeFile(join(projectRoot, 'inside.txt'), 'p'); await writeFile(join(altRoot, 'cross.txt'), 'a'); await writeFile(join(outsideDir, 'forbidden.txt'), 'x'); }); afterAll(async () => { await rm(tmp, { recursive: true, force: true }); }); describe('pathGuard (v1.13.17 extraRoots)', () => { it('accepts paths inside the primary projectRoot', async () => { const real = await pathGuard(projectRoot, 'inside.txt'); expect(real).toBe(join(projectRoot, 'inside.txt')); }); it('rejects paths outside the primary root when no extra roots given', async () => { await expect(pathGuard(projectRoot, join(outsideDir, 'forbidden.txt'))).rejects.toBeInstanceOf( PathScopeError, ); }); it('accepts cross-root paths when the matching extra root is provided', async () => { const real = await pathGuard(projectRoot, join(altRoot, 'cross.txt'), [altRoot]); expect(real).toBe(join(altRoot, 'cross.txt')); }); it('rejects cross-root paths even with extra roots when no root matches', async () => { await expect( pathGuard(projectRoot, join(outsideDir, 'forbidden.txt'), [altRoot]), ).rejects.toBeInstanceOf(PathScopeError); }); it('ignores empty-string extra roots silently', async () => { const real = await pathGuard(projectRoot, join(altRoot, 'cross.txt'), ['', altRoot]); expect(real).toBe(join(altRoot, 'cross.txt')); }); it('error message contains the request_read_access hint when scope rejects', async () => { try { await pathGuard(projectRoot, join(outsideDir, 'forbidden.txt')); throw new Error('should have thrown'); } catch (err) { expect(err).toBeInstanceOf(PathScopeError); expect((err as Error).message).toContain('request_read_access'); } }); it('still resolves symlinks before the scope check', async () => { const linkPath = join(projectRoot, 'link-to-outside'); await symlink(join(outsideDir, 'forbidden.txt'), linkPath); // Symlink target escapes both primary and the single extra root, so // even though the surface path "looks" inside projectRoot, the real // path resolves outside and the guard rejects. await expect(pathGuard(projectRoot, linkPath, [altRoot])).rejects.toBeInstanceOf( PathScopeError, ); // But adding outsideDir as an extra root accepts (realpath inside it). const real = await pathGuard(projectRoot, linkPath, [altRoot, outsideDir]); expect(real).toBe(join(outsideDir, 'forbidden.txt')); }); it('tries extra roots in order until one accepts', async () => { const real = await pathGuard(projectRoot, join(altRoot, 'cross.txt'), [ outsideDir, // rejects altRoot, // accepts ]); expect(real).toBe(join(altRoot, 'cross.txt')); }); });