/** * v0.8.2 P0-2: 根 MEMORY.md 保护闸门(工具无关的路径形参数匹配) * * 锁定契约: * - delete_file / file_move(source_path / destination_path)等旧名单遗漏的工具 * 对根 MEMORY.md 的操作被拦截(读/写/删/移动/改名任一方向) * - 子目录 MEMORY.md 不受保护(H-5 语义保持) * - 相对路径以 workspacePath 为基解析 * - MCP 工具(任意带路径形参数的工具)同样纳入 * - 非根 MEMORY.md 的路径不误伤 */ import { describe, it, expect, vi } from 'vitest'; import { tmpdir } from 'os'; import { join } from 'path'; vi.mock('electron-log', () => ({ default: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }, })); import { AgentLoopEngine } from '../engine'; const WORKSPACE = join(tmpdir(), 'metona-memory-gate-test'); const ROOT_MEMORY = join(WORKSPACE, 'MEMORY.md'); function makeEngine(): AgentLoopEngine { const engine = new AgentLoopEngine({}, { providerId: 'fake', supportedModels: [], supportsToolCalling: true, supportsThinking: false, send: vi.fn(), sendStream: vi.fn(), } as never); engine.setWorkspacePath(WORKSPACE); return engine; } /** 白盒调用(私有方法契约测试) */ function gate(engine: AgentLoopEngine, args: Record): boolean { return ( engine as unknown as { isTargetingRootMemoryMd: (tc: { args: Record }) => boolean; } ).isTargetingRootMemoryMd({ args }); } describe('根 MEMORY.md 保护闸门(P0-2)', () => { it('delete_file / file_move(旧名单遗漏工具)→ 拦截', () => { const engine = makeEngine(); expect(gate(engine, { file_path: ROOT_MEMORY })).toBe(true); expect(gate(engine, { source_path: ROOT_MEMORY, destination_path: join(WORKSPACE, 'x') })).toBe( true, ); expect( gate(engine, { source_path: join(WORKSPACE, 'note.md'), destination_path: ROOT_MEMORY }), ).toBe(true); }); it('路径参数别名矩阵:path / filePath / destination / dir_path 均命中', () => { const engine = makeEngine(); expect(gate(engine, { path: ROOT_MEMORY })).toBe(true); expect(gate(engine, { filePath: ROOT_MEMORY })).toBe(true); expect(gate(engine, { destination: ROOT_MEMORY })).toBe(true); expect(gate(engine, { dir_path: ROOT_MEMORY })).toBe(true); }); it('相对路径以工作空间为基解析 → 拦截', () => { const engine = makeEngine(); expect(gate(engine, { file_path: 'MEMORY.md' })).toBe(true); expect(gate(engine, { file_path: './MEMORY.md' })).toBe(true); }); it('子目录 MEMORY.md 不受保护(H-5 语义)', () => { const engine = makeEngine(); expect(gate(engine, { file_path: join(WORKSPACE, 'notes', 'MEMORY.md') })).toBe(false); }); it('MCP 工具的路径形参数同样纳入(任意工具生效)', () => { const engine = makeEngine(); expect(gate(engine, { target_path: ROOT_MEMORY, options: { recursive: true } })).toBe(true); }); it('其他文件路径不误伤', () => { const engine = makeEngine(); expect(gate(engine, { file_path: join(WORKSPACE, 'src', 'main.ts') })).toBe(false); expect(gate(engine, { file_path: ROOT_MEMORY + '.bak' })).toBe(false); expect(gate(engine, { command: 'echo hello' })).toBe(false); }); it('未设置工作空间时闸门放行(无根可保护)', () => { const engine = new AgentLoopEngine({}, { providerId: 'fake', supportedModels: [], supportsToolCalling: true, supportsThinking: false, send: vi.fn(), sendStream: vi.fn(), } as never); expect(gate(engine, { file_path: ROOT_MEMORY })).toBe(false); }); });