'use client'; import { create } from 'zustand'; import type { CreateTestCaseInput, TestCase, TestCaseStatus } from '@/lib/test-case'; import { generateCaseNo, normalizeTestCases } from '@/lib/test-case'; import { applyTestCaseTransition, normalizeTestCaseOnCreate } from '@/lib/test-case-workflow'; import { createTestCaseByVersionId, createTestCasesByVersionId, deleteTestCaseByVersionId, listTestCasesByVersionId, updateTestCaseByVersionId, updateTestCaseStatusByVersionId, } from '@/lib/domain-api'; import { createEntityId, dedupeEntityIds } from '@/lib/entity-id'; import { scheduleSaveWithOptimisticRollback } from '@/lib/optimistic-persistence'; import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data'; import type { WorkActivity } from '@/lib/work-activity'; import { makeTestCaseCreatedActivity, makeTestCaseStatusActivity, } from '@/lib/work-activity-factory'; import { useWorkActivityStore } from './useWorkActivityStore'; function saveStored(items: TestCase[]) { saveServerData('test-cases', items).catch(() => {}); } let lastTestCasesFetchAt = 0; async function loadStored(): Promise { try { return await loadServerData('test-cases'); } catch {} return null; } interface TestCaseState { testCases: TestCase[]; loaded: boolean; fetchTestCases: (options?: { force?: boolean; versionId?: string }) => Promise; createTestCase: (data: CreateTestCaseInput) => TestCase; createTestCases: (items: CreateTestCaseInput[]) => TestCase[]; updateTestCase: (id: string, data: Partial) => void; deleteTestCase: (id: string) => void; changeStatus: (id: string, to: TestCaseStatus, extra?: { failReason?: string; blockReason?: string }) => { ok: boolean; message?: string }; getByRequirement: (reqId: string) => TestCase[]; getByVersion: (versionId: string) => TestCase[]; getByAssignee: (assigneeId: string) => TestCase[]; } export const useTestCaseStore = create((set, get) => ({ testCases: [], loaded: false, fetchTestCases: async (options) => { if (!options?.force && get().loaded && Date.now() - lastTestCasesFetchAt < SERVER_DATA_CACHE_MS) return; const cached = options?.versionId ? await listTestCasesByVersionId(options.versionId).catch(loadStored) : await loadStored(); if (!options?.force && get().loaded && Date.now() - lastTestCasesFetchAt < SERVER_DATA_CACHE_MS) return; lastTestCasesFetchAt = Date.now(); if (cached) { const result = dedupeEntityIds(normalizeTestCases(cached), 'tc'); if (result.changed) saveStored(result.items); set({ testCases: result.items, loaded: true }); } else { set({ loaded: true }); } }, createTestCase: (data) => { const list = get().testCases; const now = new Date().toISOString(); const tc: TestCase = normalizeTestCaseOnCreate({ ...data, id: createEntityId('tc'), caseNo: generateCaseNo(list), status: 'pending', createdAt: now, updatedAt: now, } as TestCase); const updated = [...list, tc]; set({ testCases: updated, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { try { const result = await createTestCaseByVersionId(tc.versionId, tc); set({ testCases: get().testCases.map((item) => (item.id === tc.id ? { ...tc, ...result.item } : item)), loaded: true, }); appendDomainActivities(result.activities); } catch { await saveTestCasesFallback(updated); useWorkActivityStore.getState().addActivity(makeTestCaseCreatedActivity(tc, tc.createdBy)); } }, expected: updated, getCurrent: () => get().testCases, rollback: () => set({ testCases: list, loaded: true }), }); return tc; }, createTestCases: (items) => { if (items.length === 0) return []; let list = get().testCases; const previous = list; const now = new Date().toISOString(); const created: TestCase[] = []; for (const data of items) { const tc: TestCase = normalizeTestCaseOnCreate({ ...data, id: createEntityId('tc'), caseNo: generateCaseNo(list), status: 'pending', createdAt: now, updatedAt: now, } as TestCase); created.push(tc); list = [...list, tc]; } set({ testCases: list, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { try { if (!created[0]?.versionId) throw new Error('missing versionId'); const result = await createTestCasesByVersionId(created[0].versionId, created); const byCaseNo = new Map(result.items.map((item) => [item.caseNo, item])); set({ testCases: get().testCases.map((item) => { if (!created.some((tc) => tc.id === item.id)) return item; const replacement = byCaseNo.get(item.caseNo); return replacement ? { ...item, ...replacement } : item; }), loaded: true, }); appendDomainActivities(result.activities); } catch { await saveTestCasesFallback(list); created.forEach((tc) => { useWorkActivityStore.getState().addActivity(makeTestCaseCreatedActivity(tc, tc.createdBy)); }); } }, expected: list, getCurrent: () => get().testCases, rollback: () => set({ testCases: previous, loaded: true }), }); return created; }, updateTestCase: (id, data) => { const previous = get().testCases; const updated = previous.map((c) => c.id === id ? { ...c, ...data, aiDraft: false, updatedAt: new Date().toISOString() } : c, ); set({ testCases: updated, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { try { const versionId = previous.find((testCase) => testCase.id === id)?.versionId; if (!versionId) throw new Error('missing versionId'); const result = await updateTestCaseByVersionId(versionId, id, data); appendDomainActivities(result.activities); } catch { await saveTestCasesFallback(updated); } }, expected: updated, getCurrent: () => get().testCases, rollback: () => set({ testCases: previous, loaded: true }), }); }, deleteTestCase: (id) => { const previous = get().testCases; const updated = previous.filter((c) => c.id !== id); set({ testCases: updated, loaded: true }); scheduleSaveWithOptimisticRollback({ save: async () => { try { const versionId = previous.find((testCase) => testCase.id === id)?.versionId; if (!versionId) throw new Error('missing versionId'); await deleteTestCaseByVersionId(versionId, id); } catch { await saveTestCasesFallback(updated); } }, expected: updated, getCurrent: () => get().testCases, rollback: () => set({ testCases: previous, loaded: true }), }); }, changeStatus: (id, to, extra) => { const tc = get().testCases.find((c) => c.id === id); if (!tc) return { ok: false, message: '用例不存在' }; const result = applyTestCaseTransition(tc, to, { now: new Date(), failReason: extra?.failReason, blockReason: extra?.blockReason, }); if (!result.ok || !result.patch) return { ok: false, message: result.message }; const previous = get().testCases; const updated = previous.map((item) => item.id === id ? { ...item, ...result.patch, aiDraft: false, updatedAt: new Date().toISOString() } : item, ); set({ testCases: updated, loaded: true }); const actorId = tc.assigneeId || tc.executedBy || tc.createdBy; const activity = makeTestCaseStatusActivity(tc, tc.status, to, actorId); scheduleSaveWithOptimisticRollback({ save: async () => { try { const result = await updateTestCaseStatusByVersionId(tc.versionId, id, to); appendDomainActivities(result.activities); if (result.activities.length === 0 && activity) useWorkActivityStore.getState().addActivity(activity); } catch { await saveTestCasesFallback(updated); if (activity) useWorkActivityStore.getState().addActivity(activity); } }, expected: updated, getCurrent: () => get().testCases, rollback: () => set({ testCases: previous, loaded: true }), }); return { ok: true }; }, getByRequirement: (reqId) => { return get().testCases.filter((c) => c.requirementId === reqId); }, getByVersion: (versionId) => { return get().testCases.filter((c) => c.versionId === versionId); }, getByAssignee: (assigneeId) => { return get().testCases.filter((c) => c.assigneeId === assigneeId); }, })); async function saveTestCasesFallback(testCases: TestCase[]) { await saveServerData('test-cases', testCases); } function appendDomainActivities(activities: WorkActivity[]) { if (activities.length === 0) return; useWorkActivityStore.setState((state) => ({ activities: [...state.activities, ...activities], loaded: true, })); }