/** * ConfirmationHook forgetSession 测试(v0.7.3 P2-3) * * 锁定会话终态清理契约: * F1 forgetSession 清空该会话的决策记忆(拒绝记忆不再残留); * F2 forgetSession 同时拒绝该会话等待中的确认(clearPending 语义); * F3 会话隔离:清理 A 不影响 B; * F4 空/未知 sessionId 幂等无副作用。 */ import { describe, it, expect, vi, beforeEach } from 'vitest'; // 模拟一个存活窗口 —— beforeExecute 的 hasAvailableWindow 守卫需要它, // 否则确认请求在创建 pending 之前即被短路(测不到记忆/pending 路径) const fakeWindow = { isDestroyed: () => false, webContents: { send: vi.fn() }, }; vi.mock('electron', () => ({ BrowserWindow: { getAllWindows: vi.fn(() => [fakeWindow]) }, })); import { ConfirmationHook } from '../confirmation-hook'; import type { MetonaToolCall, MetonaToolDef } from '../../../harness/types'; import { MetonaRiskLevel, MetonaToolCategory } from '../../../harness/types'; function makeToolCall(name: string, id = `tc_${name}`): MetonaToolCall { return { id, name, args: {}, iteration: 1, timestamp: Date.now() }; } const NEEDS_CONFIRM_DEF: MetonaToolDef = { name: 'run_command', description: 'test', parameters: { type: 'object', properties: {}, required: [] }, category: MetonaToolCategory.CODE_EXECUTION, riskLevel: MetonaRiskLevel.HIGH, requiresPermission: true, timeoutMs: 1000, }; describe('ConfirmationHook — forgetSession(P2-3)', () => { let hook: ConfirmationHook; beforeEach(() => { vi.useFakeTimers(); hook = new ConfirmationHook(null, null); hook.setToolDefs([NEEDS_CONFIRM_DEF]); }); it('F1: 会话删除后决策记忆被清空(拒绝记忆不再跨生命周期残留)', async () => { const pending = hook.beforeExecute(makeToolCall('run_command'), 'sess-A'); hook.resolveConfirmation(`tc_run_command`, false, true); // 记住拒绝 await pending; expect(hook.getRememberedDenials('sess-A')).toHaveLength(1); hook.forgetSession('sess-A'); expect(hook.getRememberedDenials('sess-A')).toHaveLength(0); }); it('F2: forgetSession 拒绝该会话等待中的确认(clearPending 语义)', async () => { const p1 = hook.beforeExecute(makeToolCall('run_command'), 'sess-A'); // resolve pending(拒绝)后走记忆清理路径 hook.forgetSession('sess-A'); await expect(p1).resolves.toMatchObject({ blocked: true }); // pending 已清空 —— getPendingConfirmations 无残留 expect(hook.getPendingConfirmations()).toHaveLength(0); }); it('F3: 会话隔离 —— 清理 A 不影响 B 的决策记忆', async () => { const p1 = hook.beforeExecute(makeToolCall('run_command', 'tc_A'), 'sess-A'); hook.resolveConfirmation('tc_A', false, true); await p1; const p2 = hook.beforeExecute(makeToolCall('run_command', 'tc_B'), 'sess-B'); hook.resolveConfirmation('tc_B', false, true); await p2; hook.forgetSession('sess-A'); expect(hook.getRememberedDenials('sess-A')).toHaveLength(0); expect(hook.getRememberedDenials('sess-B')).toHaveLength(1); }); it('F4: 空 sessionId 幂等无副作用;未知会话不抛错', () => { expect(() => hook.forgetSession('')).not.toThrow(); expect(() => hook.forgetSession('nonexistent')).not.toThrow(); expect(hook.getRememberedDenials()).toHaveLength(0); }); });