feat: v0.8.2 安全纵深补全 · 协议保真 · 断链修复 — 图片SSRF/根MEMORY.md保护根治 · Anthropic thinking回传+pause_turn续传 · 2523 用例全量回归 + E2E 扩充
CI / 类型检查 + Lint + 单元测试 (push) Failing after 9m45s
CI / 全量测试 (Electron ABI) (push) Failing after 6m28s
CI / 产物编译验证 (push) Successful in 11m18s

This commit is contained in:
2026-09-08 14:30:27 +08:00
parent 69776e447f
commit 4cd6e997b5
86 changed files with 4303 additions and 956 deletions
@@ -0,0 +1,125 @@
// @vitest-environment jsdom
/**
* Sidebar 组件测试(v0.8.2 P3-3 补齐设置面板/侧栏测试空白)
*
* 覆盖契约:
* - 挂载时拉取会话列表并渲染标题
* - v0.8.2 P2-4: session:updated 实时刷新 messageCount/updatedAt/title
* - 搜索框(标题 + 内容全文搜索入口)存在
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Sidebar } from '../Sidebar';
import { useSessionStore } from '@renderer/stores/session-store';
const noop = (): void => {};
const sessionUpdatedHandlers: Array<(data: unknown) => void> = [];
function makeSession(overrides: Record<string, unknown> = {}): {
id: string;
title: string;
createdAt: number;
updatedAt: number;
messageCount: number;
totalTokens: number;
pinned: boolean;
archived: boolean;
} {
return {
id: 's1',
title: '第一个会话',
createdAt: Date.now(),
updatedAt: Date.now(),
messageCount: 2,
totalTokens: 0,
pinned: false,
archived: false,
...overrides,
};
}
beforeEach(() => {
sessionUpdatedHandlers.length = 0;
useSessionStore.getState().setSessions([]);
useSessionStore.getState().setCurrentSession(null);
const bridge = (window as unknown as { metona: Record<string, unknown> }).metona;
bridge.sessions = {
...(bridge.sessions as Record<string, unknown>),
list: vi.fn().mockResolvedValue([makeSession(), makeSession({ id: 's2', title: 'Second' })]),
searchContent: vi.fn().mockResolvedValue({ success: true, data: [] }),
onSessionUpdated: vi.fn((cb: (data: unknown) => void) => {
sessionUpdatedHandlers.push(cb);
return noop;
}),
};
});
describe('Sidebar — 会话列表', () => {
it('挂载时拉取并渲染会话标题', async () => {
render(<Sidebar />);
await waitFor(() => {
expect(screen.getByText('第一个会话')).toBeInTheDocument();
expect(screen.getByText('Second')).toBeInTheDocument();
});
});
it('渲染搜索框与新建会话按钮', () => {
render(<Sidebar />);
expect(screen.getByPlaceholderText(/搜索|Search/)).toBeInTheDocument();
expect(screen.getAllByRole('button').length).toBeGreaterThan(0);
});
it('接收 session:updated 后实时刷新条数(P2-4', async () => {
render(<Sidebar />);
await waitFor(() => {
expect(screen.getByText('第一个会话')).toBeInTheDocument();
});
expect(sessionUpdatedHandlers.length).toBeGreaterThan(0);
act(() => {
sessionUpdatedHandlers.forEach((h) =>
h({ sessionId: 's1', messageCount: 42, updatedAt: Date.now() }),
);
});
await waitFor(() => {
// t('sidebar.messageCount') zh 文案为 "{{count}} 条"
expect(screen.getByText(/42\s*条|42 messages/)).toBeInTheDocument();
});
});
it('接收 session:updated 的 title 补丁 → 标题刷新(LLM 标题生成链路)', async () => {
render(<Sidebar />);
await waitFor(() => {
expect(screen.getByText('第一个会话')).toBeInTheDocument();
});
act(() => {
sessionUpdatedHandlers.forEach((h) => h({ sessionId: 's1', title: 'LLM 生成的新标题' }));
});
await waitFor(() => {
expect(screen.getByText('LLM 生成的新标题')).toBeInTheDocument();
});
});
});
describe('Sidebar — 搜索', () => {
it('输入搜索词触发内容全文搜索(FTS5 通道,300ms 防抖)', async () => {
const user = userEvent.setup();
render(<Sidebar />);
const input = screen.getByPlaceholderText(/搜索|Search/);
await user.type(input, '关键词');
await waitFor(
() => {
const bridge = (window as unknown as { metona: Record<string, unknown> }).metona;
expect(bridge.sessions).toBeDefined();
expect(
(bridge.sessions as { searchContent: { mock: { calls: unknown[][] } } }).searchContent
.mock.calls.length,
).toBeGreaterThan(0);
},
{ timeout: 2_000 },
);
});
});