import { beforeEach, describe, expect, it } from 'vitest'; import { resetAccountData } from '../../src/lib/resetAccountData'; import { useContactStore } from '../../src/stores/contactStore'; import { useMailStore } from '../../src/stores/mailStore'; import { useSessionStore } from '../../src/stores/sessionStore'; /** * ★ 切身份必须清空全部账号数据(2026-09-26 加的行为锁)。 * * ── 锁的是哪个 bug ── * `setActive` 会把 `api/config` 单例里的 API_BASE 与 bearer 翻到新账号, * 而各 store 还留着**旧**账号的数据 ⇒ "新账号的界面下显示旧账号的邮件", * 且**从旧视图发出的写操作(归档/转发/批准权限)改的是新账号**。 * 不报错、不空屏、没有任何可见征兆 —— 只能靠判据钉住。 * * ── 为什么锁 `resetAccountData()` 而不是锁某个组件 ── * 身份变化有**三条**路径(切账号 / 登出 / 401),它们都必须清; * 把"清空"收在一个函数里,这三条路径共用它 ⇒ 这里只需要锁**这一个**函数 * 把三份 store 都清干净(清漏一份,那份就是新的泄露面)。 */ const mail = (id: string) => ({ mail_id: id }) as never; const session = (id: string) => ({ session_id: id }) as never; /** 往三个 store 里各塞一份"上个账号的脏数据"。 */ function seedStaleData() { useMailStore.setState({ inbox: [mail('a1')], sent: [mail('a2')], currentMail: mail('a3'), error: '旧错误', accountErrors: ['x'] }); useSessionStore.setState({ sessions: [session('s1')] as never, currentSession: { session_id: 's1' } as never, currentSessionMails: [mail('a4')], renameProposal: { old: 'x', new: 'y' } as never, budget: { used: 1 } as never, error: '旧错误' }); useContactStore.setState({ contacts: [{ session_id: 'c1', name: 'A' }] as never, archivedContacts: [{ session_id: 'c2', name: 'A2' }] as never, pendingArchive: 'c3', error: '旧错误' }); } describe('resetAccountData —— 换身份时清空全部账号数据', () => { beforeEach(() => { // 各 store 的初值本身就是"空",所以直接回到初值即可复位。 useMailStore.setState({ inbox: [], sent: [], currentMail: null, error: null, accountErrors: [], loading: false }); useSessionStore.setState({ sessions: [], currentSession: null, currentSessionMails: [], renameProposal: null, budget: null, error: null, loading: false }); useContactStore.setState({ contacts: [], archivedContacts: [], pendingArchive: null, error: null, loading: false }); }); it('★ 三个 store 全部清空(漏掉任何一份都是新的泄露面)', () => { seedStaleData(); resetAccountData(); expect(useMailStore.getState().inbox).toEqual([]); expect(useMailStore.getState().sent).toEqual([]); expect(useMailStore.getState().currentMail).toBeNull(); expect(useSessionStore.getState().sessions).toEqual([]); expect(useSessionStore.getState().currentSession).toBeNull(); expect(useSessionStore.getState().currentSessionMails).toEqual([]); expect(useSessionStore.getState().renameProposal).toBeNull(); expect(useSessionStore.getState().budget).toBeNull(); expect(useContactStore.getState().contacts).toEqual([]); expect(useContactStore.getState().archivedContacts).toEqual([]); expect(useContactStore.getState().pendingArchive).toBeNull(); }); it('错误态也要清:旧账号的报错不该挂在别人的界面上', () => { seedStaleData(); resetAccountData(); expect(useMailStore.getState().error).toBeNull(); expect(useSessionStore.getState().error).toBeNull(); expect(useContactStore.getState().error).toBeNull(); }); it('幂等:连续切账号时重复清空不报错', () => { seedStaleData(); resetAccountData(); expect(() => { resetAccountData(); resetAccountData(); }).not.toThrow(); }); it('清空**不碰**纯界面状态(那是 uiStore 的职责,不在这里)', () => { /* * 这条是**边界**:复位界面状态(窄屏分栏、写信态)由 `App.tsx` 的 `resetUI()` * 负责。`resetAccountData` 若顺手去清那些,会让"切账号"变成"退出登录", * 把两件不同的事混在一起 —— 所以这里显式锁住"只清数据"。 */ const ui = useSessionStore.getState(); expect(typeof ui.resetAll).toBe('function'); // 三个 store 都提供 resetAll(而不是各自散落的 clearXxx)—— // 统一入口才不会在第四条身份路径上被漏掉。 expect(typeof useMailStore.getState().resetAll).toBe('function'); expect(typeof useContactStore.getState().resetAll).toBe('function'); }); });