- git mv 保留历史:ui/(Vue3+Vite)、sdk/(BFF 中间层)
- 路径语义修正:db_path/figures_cache 改相对 sdk/(BFF cwd),
历史库 sdk/backend/data 迁移至 sdk/data/(会话/图片缓存无损)
- 引用修复 ~15 处:config.py、deploy/*.sh、vitest/playwright 配置、
单测导入路径、docs、README、gitignore、.env.example
- 骨架:agent/{qa,methodology,legacy-rag,vendor}、knowledge/manifest.yaml、
tooling/{compile,kb,crypto,assemble,audit,tools,scripts} + README
- 门禁验证:sdk pytest 125 通过、ui vitest 158 通过、
生产 BFF 脚本重启四上游全绿 + 静态托管 + 会话库无损
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
163 lines
7.7 KiB
JavaScript
163 lines
7.7 KiB
JavaScript
// @vitest-environment jsdom
|
||
// sessions.store.test.js —— sessions store 单测
|
||
// 覆盖:分组(今天/昨天/更早)、搜索过滤、CRUD、标题自动生成(FR-SESSION-001/005/008)。
|
||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||
import { setActivePinia, createPinia } from 'pinia';
|
||
|
||
const updateCalls = [];
|
||
vi.mock('../../../../ui/src/api/sessions.js', () => ({
|
||
listSessions: vi.fn(async () => []),
|
||
createSession: vi.fn(async () => ({ id: 'new-1', title: '新会话', status: 'idle', created_at: '2026-08-25T00:00:00Z', updated_at: '2026-08-25T00:00:00Z', message_count: 0, meta: {} })),
|
||
getSession: vi.fn(async () => ({})),
|
||
updateSession: vi.fn(async (id, body) => { updateCalls.push({ id, body }); return { id, ...body }; }),
|
||
deleteSession: vi.fn(async () => {}),
|
||
createMessage: vi.fn(),
|
||
updateMessage: vi.fn(),
|
||
getSessionSources: vi.fn(),
|
||
}));
|
||
|
||
import { useSessionsStore, groupKeyOf, deriveTitle } from '../../../../ui/src/stores/sessions.js';
|
||
|
||
const iso = (s) => new Date(s).toISOString();
|
||
|
||
describe('groupKeyOf', () => {
|
||
const now = new Date('2026-08-25T15:00:00');
|
||
|
||
it('今天/昨天/更早分界(本地日界)', () => {
|
||
expect(groupKeyOf(iso('2026-08-25T00:01:00'), now)).toBe('today');
|
||
expect(groupKeyOf(iso('2026-08-24T23:59:00'), now)).toBe('yesterday');
|
||
expect(groupKeyOf(iso('2026-08-24T00:00:00'), now)).toBe('yesterday');
|
||
expect(groupKeyOf(iso('2026-08-23T23:59:00'), now)).toBe('older');
|
||
expect(groupKeyOf(iso('2026-08-20T09:00:00'), now)).toBe('older');
|
||
});
|
||
|
||
it('无效/缺失时间归更早', () => {
|
||
expect(groupKeyOf(null, now)).toBe('older');
|
||
expect(groupKeyOf('不是时间', now)).toBe('older');
|
||
});
|
||
|
||
it('未来时间归今天', () => {
|
||
expect(groupKeyOf(iso('2026-08-26T00:00:00'), now)).toBe('today');
|
||
});
|
||
});
|
||
|
||
describe('deriveTitle(FR-SESSION-008)', () => {
|
||
it('前 30 字符 + 省略号', () => {
|
||
const q = '水稻SPL基因家族的功能研究有哪些关键进展?这是超过三十个字符的问题内容继续延长';
|
||
expect(deriveTitle(q).length).toBeLessThanOrEqual(30);
|
||
expect(deriveTitle(q)).toBe(q.slice(0, 30));
|
||
});
|
||
it('空白回退「新建会话」(labels.T.newTask)', () => {
|
||
expect(deriveTitle(' ')).toBe('新建会话');
|
||
});
|
||
});
|
||
|
||
describe('sessions store', () => {
|
||
beforeEach(() => {
|
||
setActivePinia(createPinia());
|
||
updateCalls.length = 0;
|
||
vi.clearAllMocks();
|
||
});
|
||
|
||
it('分组:按 updated_at 倒序 + 三组', async () => {
|
||
const { listSessions } = await import('../../../../ui/src/api/sessions.js');
|
||
// 2026-08-26 修复:夹具日期相对「真实今天」动态生成——硬编码日期会随时间
|
||
// 腐烂(store 分组用真实 new Date()),此前每天翻车。
|
||
const d = (daysAgo) => {
|
||
const t = new Date();
|
||
t.setDate(t.getDate() - daysAgo);
|
||
t.setHours(10, 0, 0, 0);
|
||
return t.toISOString();
|
||
};
|
||
listSessions.mockResolvedValueOnce([
|
||
{ id: 'a', title: '今天会话', status: 'done', created_at: 'x', updated_at: d(0), message_count: 2, meta: {} },
|
||
{ id: 'b', title: '昨天会话', status: 'done', created_at: 'x', updated_at: d(1), message_count: 4, meta: {} },
|
||
{ id: 'c', title: '更早会话', status: 'error', created_at: 'x', updated_at: d(5), message_count: 1, meta: {} },
|
||
]);
|
||
const store = useSessionsStore();
|
||
await store.load();
|
||
expect(store.groups.map((g) => g.label)).toEqual(['今天', '昨天', '更早']);
|
||
expect(store.groups[0].items.map((s) => s.id)).toEqual(['a']);
|
||
expect(store.groups[1].items.map((s) => s.id)).toEqual(['b']);
|
||
expect(store.groups[2].items.map((s) => s.id)).toEqual(['c']);
|
||
});
|
||
|
||
it('搜索过滤:title 子串不区分大小写 + 无匹配(FR-SESSION-005)', async () => {
|
||
const { listSessions } = await import('../../../../ui/src/api/sessions.js');
|
||
listSessions.mockResolvedValueOnce([
|
||
{ id: 'a', title: '水稻SPL基因家族', status: 'done', created_at: 'x', updated_at: iso('2026-08-25T10:00:00'), message_count: 2, meta: {} },
|
||
{ id: 'b', title: 'NAC 转录因子研究综述', status: 'done', created_at: 'x', updated_at: iso('2026-08-24T10:00:00'), message_count: 2, meta: {} },
|
||
]);
|
||
const store = useSessionsStore();
|
||
await store.load();
|
||
store.searchQuery = 'spl';
|
||
expect(store.matchedCount).toBe(1);
|
||
expect(store.filtered[0].id).toBe('a');
|
||
store.searchQuery = '不存在的词';
|
||
expect(store.noMatch).toBe(true);
|
||
expect(store.matchedCount).toBe(0);
|
||
store.searchQuery = '';
|
||
expect(store.matchedCount).toBe(2);
|
||
expect(store.noMatch).toBe(false);
|
||
});
|
||
|
||
it('新建会话:切为活动会话 + 连点两次产生两个独立会话(FR-SESSION-003)', async () => {
|
||
const { createSession } = await import('../../../../ui/src/api/sessions.js');
|
||
let n = 0;
|
||
createSession.mockImplementation(async () => ({ id: `new-${++n}`, title: '新会话', status: 'idle', created_at: 'x', updated_at: 'x', message_count: 0, meta: {} }));
|
||
const store = useSessionsStore();
|
||
const s1 = await store.create();
|
||
const s2 = await store.create();
|
||
expect(s1.id).not.toBe(s2.id);
|
||
expect(store.activeId).toBe(s2.id);
|
||
expect(store.items).toHaveLength(2);
|
||
});
|
||
|
||
it('删除:删除当前会话 → 切最新一个;列表空 → activeId=null(FR-SESSION-007)', async () => {
|
||
const { listSessions, deleteSession } = await import('../../../../ui/src/api/sessions.js');
|
||
listSessions.mockResolvedValueOnce([
|
||
{ id: 'a', title: 'A', status: 'done', created_at: 'x', updated_at: iso('2026-08-25T10:00:00'), message_count: 2, meta: {} },
|
||
{ id: 'b', title: 'B', status: 'done', created_at: 'x', updated_at: iso('2026-08-24T10:00:00'), message_count: 2, meta: {} },
|
||
]);
|
||
const store = useSessionsStore();
|
||
await store.load();
|
||
store.activeId = 'a';
|
||
await store.remove('a');
|
||
expect(store.activeId).toBe('b');
|
||
expect(store.items.map((s) => s.id)).toEqual(['b']);
|
||
await store.remove('b');
|
||
expect(store.activeId).toBeNull();
|
||
expect(store.empty).toBe(true);
|
||
expect(deleteSession).toHaveBeenCalledTimes(2);
|
||
});
|
||
|
||
it('标题自动生成:首问 PATCH 前 30 字符;title_manual 不再覆盖(FR-SESSION-008)', async () => {
|
||
const { listSessions } = await import('../../../../ui/src/api/sessions.js');
|
||
const q = '水稻SPL基因家族的功能研究有哪些关键进展?';
|
||
listSessions.mockResolvedValueOnce([
|
||
{ id: 'a', title: '新会话', status: 'running', created_at: 'x', updated_at: iso('2026-08-25T10:00:00'), message_count: 1, meta: { title_manual: false } },
|
||
{ id: 'b', title: '手动改名', status: 'running', created_at: 'x', updated_at: iso('2026-08-25T10:00:00'), message_count: 1, meta: { title_manual: true } },
|
||
]);
|
||
const store = useSessionsStore();
|
||
await store.load();
|
||
await store.ensureTitle('a', q);
|
||
expect(store.items.find((s) => s.id === 'a').title).toBe(q.slice(0, 30));
|
||
expect(updateCalls.some((c) => c.id === 'a' && c.body.title === q.slice(0, 30))).toBe(true);
|
||
await store.ensureTitle('b', q);
|
||
expect(store.items.find((s) => s.id === 'b').title).toBe('手动改名');
|
||
expect(updateCalls.some((c) => c.id === 'b')).toBe(false);
|
||
});
|
||
|
||
it('重命名:PATCH 后本地同步(FR-SESSION-006)', async () => {
|
||
const { listSessions } = await import('../../../../ui/src/api/sessions.js');
|
||
listSessions.mockResolvedValueOnce([
|
||
{ id: 'a', title: '旧标题', status: 'done', created_at: 'x', updated_at: iso('2026-08-25T10:00:00'), message_count: 2, meta: { title_manual: false } },
|
||
]);
|
||
const store = useSessionsStore();
|
||
await store.load();
|
||
await store.rename('a', '新标题');
|
||
expect(store.items[0].title).toBe('新标题');
|
||
expect(store.items[0].meta.title_manual).toBe(true);
|
||
});
|
||
});
|