diff --git a/src/background/diagnostic-log-store.ts b/src/background/diagnostic-log-store.ts new file mode 100644 index 0000000..1d586d1 --- /dev/null +++ b/src/background/diagnostic-log-store.ts @@ -0,0 +1,33 @@ +import { type DiagnosticLogEntry, DIAGNOSTIC_STORAGE_KEYS } from '../types/diagnostic'; +import { type StorageManager, storage } from '../utils/storage'; + +const MAX_RECENT_LOGS = 300; + +type DiagnosticStoreStorage = Pick; + +export class DiagnosticLogStore { + constructor(private readonly storageManager: DiagnosticStoreStorage = storage) {} + + async append(entry: DiagnosticLogEntry): Promise { + const recent = await this.getRecent(); + const next = [...recent, entry]; + + if (next.length > MAX_RECENT_LOGS) { + next.splice(0, next.length - MAX_RECENT_LOGS); + } + + await this.storageManager.set(DIAGNOSTIC_STORAGE_KEYS.RECENT_LOGS, next); + } + + async getRecent(): Promise { + const stored = await this.storageManager.get( + DIAGNOSTIC_STORAGE_KEYS.RECENT_LOGS + ); + + return Array.isArray(stored) ? stored : []; + } + + async clear(): Promise { + await this.storageManager.remove(DIAGNOSTIC_STORAGE_KEYS.RECENT_LOGS); + } +} diff --git a/src/types/diagnostic.ts b/src/types/diagnostic.ts new file mode 100644 index 0000000..2d98cdc --- /dev/null +++ b/src/types/diagnostic.ts @@ -0,0 +1,54 @@ +/** + * 诊断日志级别 + */ +export type DiagnosticLogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR'; + +/** + * 单条诊断日志 + */ +export interface DiagnosticLogEntry { + id: string; + level: DiagnosticLogLevel; + message: string; + timestamp?: number; + context?: Record; +} + +/** + * 单次失败记录 + */ +export interface DiagnosticFailureEntry { + id: string; + occurredAt: number; + reason: string; + message?: string; +} + +/** + * 最近一次失败的诊断快照 + */ +export interface LastFailureDiagnosticSnapshot { + failure: DiagnosticFailureEntry; + recentLogs: DiagnosticLogEntry[]; +} + +/** + * 导出诊断载荷 + */ +export interface DiagnosticExportPayload { + exportedAt: number; + lastFailure: LastFailureDiagnosticSnapshot | null; +} + +/** + * 诊断提示状态 + */ +export interface DiagnosticPromptState { + shouldPrompt: boolean; + lastPromptedAt?: number; +} + +export const DIAGNOSTIC_STORAGE_KEYS = { + RECENT_LOGS: 'diagnostic_recent_logs', + LAST_FAILURE: 'diagnostic_last_failure', +} as const; diff --git a/tests/unit/diagnostic-log-store.test.ts b/tests/unit/diagnostic-log-store.test.ts new file mode 100644 index 0000000..eee73f1 --- /dev/null +++ b/tests/unit/diagnostic-log-store.test.ts @@ -0,0 +1,64 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { StorageManager } from '../../src/utils/storage'; +import { DiagnosticLogStore } from '../../src/background/diagnostic-log-store'; +import { DIAGNOSTIC_STORAGE_KEYS, type DiagnosticLogEntry } from '../../src/types/diagnostic'; + +describe('DiagnosticLogStore', () => { + let mockChromeStorage: { + get: ReturnType; + set: ReturnType; + remove: ReturnType; + }; + + beforeEach(() => { + mockChromeStorage = { + get: vi.fn(() => Promise.resolve({})), + set: vi.fn(() => Promise.resolve()), + remove: vi.fn(() => Promise.resolve()), + }; + + global.chrome = { + storage: { + local: mockChromeStorage, + }, + } as any; + + global.localStorage = { + getItem: vi.fn(), + setItem: vi.fn(), + removeItem: vi.fn(), + clear: vi.fn(), + key: vi.fn(), + length: 0, + } as any; + }); + + it('keeps only the latest 300 diagnostic log entries', async () => { + let currentLogs: DiagnosticLogEntry[] = []; + mockChromeStorage.get.mockImplementation(async () => ({ + [DIAGNOSTIC_STORAGE_KEYS.RECENT_LOGS]: currentLogs, + })); + mockChromeStorage.set.mockImplementation(async (payload: Record) => { + currentLogs = payload[DIAGNOSTIC_STORAGE_KEYS.RECENT_LOGS] ?? []; + }); + + const store = new DiagnosticLogStore(new StorageManager()); + + for (let i = 0; i < 305; i++) { + await store.append({ id: `log-${i}`, level: 'INFO', message: `m-${i}` } as DiagnosticLogEntry); + } + + const logs = await store.getRecent(); + expect(logs).toHaveLength(300); + expect(logs[0].id).toBe('log-5'); + expect(logs.at(-1)?.id).toBe('log-304'); + }); + + it('clears the buffer without touching unrelated keys', async () => { + const store = new DiagnosticLogStore(new StorageManager()); + + await store.clear(); + + expect(mockChromeStorage.remove).toHaveBeenCalledWith(DIAGNOSTIC_STORAGE_KEYS.RECENT_LOGS); + }); +});