/** * File Guard 单元测试(P1-14 测试基线) * 覆盖:路径遍历防护、前缀碰撞、MEMORY.md 保护、glob 匹配、编码检测 */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; import { isPathWithinWorkspace, isProtectedWorkspaceFile, safeResolvePath, matchGlob, matchAnyGlob, commandTouchesProtectedFile, decodeBufferWithDetection, } from '../file-guard'; describe('isPathWithinWorkspace', () => { const ws = join(tmpdir(), 'metona-test-ws'); beforeAll(() => { mkdirSync(ws, { recursive: true }); }); it('工作空间内的相对路径通过', () => { expect(isPathWithinWorkspace('src/main.ts', ws)).toBe(true); }); it('工作空间内的绝对路径通过', () => { expect(isPathWithinWorkspace(join(ws, 'src/main.ts'), ws)).toBe(true); }); it('工作空间根目录本身通过', () => { expect(isPathWithinWorkspace('.', ws)).toBe(true); }); it('路径遍历(../)被拒绝', () => { expect(isPathWithinWorkspace('../etc/passwd', ws)).toBe(false); }); it('多层遍历(../../..)被拒绝', () => { expect(isPathWithinWorkspace('../../../etc/passwd', ws)).toBe(false); }); it('前缀碰撞不误判(/app-evil 不在 /app 内)', () => { const parent = join(tmpdir(), 'metona-prefix-app'); mkdirSync(parent, { recursive: true }); expect(isPathWithinWorkspace(join(tmpdir(), 'metona-prefix-app-evil/x'), parent)).toBe(false); }); it('绝对路径指向工作空间外被拒绝', () => { expect(isPathWithinWorkspace('C:\\Windows\\System32\\cmd.exe', ws)).toBe(false); }); }); describe('isProtectedWorkspaceFile / safeResolvePath', () => { const ws = join(tmpdir(), 'metona-test-protect'); beforeAll(() => { mkdirSync(join(ws, 'sub'), { recursive: true }); }); it('工作空间根目录的 MEMORY.md 受保护', () => { expect(isProtectedWorkspaceFile('MEMORY.md', ws)).toBe(true); }); it('子目录的 MEMORY.md 不受保护', () => { expect(isProtectedWorkspaceFile(join('sub', 'MEMORY.md'), ws)).toBe(false); }); it('safeResolvePath 拒绝越界路径并抛错', () => { expect(() => safeResolvePath('../outside.txt', ws)).toThrow(/Path traversal/); }); it('safeResolvePath 拒绝根目录 MEMORY.md 并抛错', () => { expect(() => safeResolvePath('MEMORY.md', ws)).toThrow(/MEMORY.md/); }); it('safeResolvePath 正常解析工作空间内路径', () => { const resolved = safeResolvePath('src/a.ts', ws); expect(resolved).toBe(join(ws, 'src/a.ts')); }); }); describe('commandTouchesProtectedFile', () => { it('裸引用 MEMORY.md 被拦截', () => { expect(commandTouchesProtectedFile('cat MEMORY.md')).toBe(true); }); it('子目录 MEMORY.md 不被拦截', () => { expect(commandTouchesProtectedFile('cat sub/MEMORY.md')).toBe(false); expect(commandTouchesProtectedFile('cat sub\\MEMORY.md')).toBe(false); }); it('管道/分号后的 MEMORY.md 被拦截', () => { expect(commandTouchesProtectedFile('echo x | cat MEMORY.md; rm file')).toBe(true); }); it('无关命令不误判', () => { expect(commandTouchesProtectedFile('npm run test')).toBe(false); expect(commandTouchesProtectedFile('git status')).toBe(false); }); }); describe('matchGlob / matchAnyGlob', () => { it('单 glob 匹配', () => { expect(matchGlob('main.ts', '*.ts')).toBe(true); expect(matchGlob('main.js', '*.ts')).toBe(false); }); it('? 单字符匹配', () => { expect(matchGlob('test1.js', 'test?.js')).toBe(true); expect(matchGlob('test12.js', 'test?.js')).toBe(false); }); it('逗号分隔多 glob 任一匹配', () => { expect(matchAnyGlob('a.ts', '*.ts,*.js,*.tsx')).toBe(true); expect(matchAnyGlob('a.jsx', '*.ts,*.js,*.tsx')).toBe(false); }); it('空 glob 字符串匹配所有', () => { expect(matchAnyGlob('anything.txt', '')).toBe(true); }); }); describe('decodeBufferWithDetection', () => { it('UTF-8 无 BOM 正确解码', () => { const buf = Buffer.from('你好 world', 'utf-8'); const { content, encoding } = decodeBufferWithDetection(buf); expect(content).toBe('你好 world'); expect(encoding).toBe('utf-8'); }); it('UTF-8 BOM 被剥离并识别', () => { const body = Buffer.from('hello', 'utf-8'); const buf = Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), body]); const { content, encoding } = decodeBufferWithDetection(buf); expect(content).toBe('hello'); expect(encoding).toBe('utf-8-bom'); }); it('UTF-16 LE BOM 正确解码', () => { const body = '你好'; const buf = Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(body, 'utf16le')]); const { content, encoding } = decodeBufferWithDetection(buf); expect(content).toBe(body); expect(encoding).toBe('utf-16le'); }); it('UTF-16 BE BOM 正确解码(字节交换)', () => { const body = '你好'; const le = Buffer.from(body, 'utf16le'); const be = Buffer.from(le); be.swap16(); const buf = Buffer.concat([Buffer.from([0xfe, 0xff]), be]); const { content, encoding } = decodeBufferWithDetection(buf); expect(content).toBe(body); expect(encoding).toBe('utf-16be'); }); it('空 Buffer 返回空内容', () => { const { content, encoding } = decodeBufferWithDetection(Buffer.alloc(0)); expect(content).toBe(''); expect(encoding).toBe('utf-8'); }); }); describe('workspace 文件读取场景(临时目录)', () => { let ws: string; beforeAll(() => { ws = mkdtempSync(join(tmpdir(), 'metona-guard-')); writeFileSync(join(ws, 'file.txt'), 'content', 'utf-8'); }); afterAll(() => { rmSync(ws, { recursive: true, force: true }); }); it('工作空间内文件路径通过校验', () => { expect(isPathWithinWorkspace('file.txt', ws)).toBe(true); expect(isPathWithinWorkspace(join(ws, 'file.txt'), ws)).toBe(true); }); });