import { mkdir, mkdtemp, rm, symlink, writeFile, } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import { afterEach, describe, expect, test, vi } from 'vitest'; const tempDirs: string[] = []; // Point boxHomePaths('t').homeDir at a real tmp home, then import box fresh so // env.workdir (captured at import) reflects the tmp workdir for this test. const loadWithHome = async () => { const workdir = await mkdtemp(path.join(os.tmpdir(), 'spoon-box-')); tempDirs.push(workdir); vi.resetModules(); process.env.SPOON_WORKER_TOKEN = 'test-worker-token'; process.env.GITHUB_APP_ID = '123'; process.env.GITHUB_APP_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----'; process.env.SPOON_AGENT_WORKDIR = workdir; const box = await import('../../src/box'); const homeDir = box.boxHomePaths('t').homeDir; await mkdir(homeDir, { recursive: true }); return { box, homeDir, workdir }; }; describe('box file helpers', () => { afterEach(async () => { await Promise.all( tempDirs.map((dir) => rm(dir, { force: true, recursive: true })), ); tempDirs.length = 0; vi.resetModules(); }); test('writeBoxFile then readBoxFile round-trips', async () => { const { box } = await loadWithHome(); await expect( box.writeBoxFile('t', 'notes/todo.txt', 'hello box'), ).resolves.toEqual({ success: true }); await expect(box.readBoxFile('t', 'notes/todo.txt')).resolves.toBe( 'hello box', ); }); test('readBoxFile rejects a lexical .. escape', async () => { const { box } = await loadWithHome(); await expect( box.readBoxFile('t', '../../etc/passwd'), ).rejects.toThrow(/outside home/); }); test('rejects a symlink inside home that points outside on read and write', async () => { const { box, homeDir, workdir } = await loadWithHome(); const outside = path.join(workdir, 'outside'); await mkdir(outside, { recursive: true }); await writeFile(path.join(outside, 'secret'), 'S'); await symlink(path.join(outside, 'secret'), path.join(homeDir, 'link')); await expect(box.readBoxFile('t', 'link')).rejects.toThrow(/outside home/); await expect( box.writeBoxFile('t', 'link', 'nope'), ).rejects.toThrow(); }); test('listBoxTree includes a created file and excludes node_modules', async () => { const { box, homeDir } = await loadWithHome(); await writeFile(path.join(homeDir, 'readme.md'), '# hi'); await mkdir(path.join(homeDir, 'node_modules', 'pkg'), { recursive: true }); await writeFile(path.join(homeDir, 'node_modules', 'pkg', 'x.js'), '1'); const tree = await box.listBoxTree('t'); expect(tree.name).toBe('~'); expect(tree.path).toBe(''); expect(tree.type).toBe('directory'); const names = (tree.children ?? []).map((child) => child.name); expect(names).toContain('readme.md'); expect(names).not.toContain('node_modules'); }); });