Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
13 KiB
JSON

{"repo": "capeaga/bai-it", "files": {"src/__tests__/options-logic.test.ts": {"f8831af8f1d05538b42278a736f17e96f2e45dee": {"t": 1786068365.985025, "n": 11913, "h": "5e3f1c44d4d00e8d", "c": "/**\n * Options 页面逻辑测试\n *\n * 覆盖:\n * 1. 多 Provider 配置类型 (resolveLLMConfig, migrateLLMConfig)\n * 2. useDashboardData 聚合逻辑\n * 3. useSentences 筛选逻辑\n * 4. useReviewData 选句逻辑\n * 5. constants 映射完整性\n */\n\nimport { describe, it, expect, beforeEach, afterEach } from \"vitest\";\nimport \"fake-indexeddb/auto\";\nimport {\n resolveLLMConfig,\n migrateLLMConfig,\n DEFAULT_PROVIDERS,\n PROVIDER_META,\n} from \"../shared/types.ts\";\nimport type {\n LLMMultiConfig,\n LLMConfig,\n ProviderKey,\n LearningRecord,\n PatternKey,\n} from \"../shared/types.ts\";\nimport {\n openDB,\n closeDB,\n resetDBInstance,\n learningRecordDAO,\n vocabDAO,\n DB_NAME,\n} from \"../shared/db.ts\";\nimport { PATTERN_LABELS, PROVIDER_INFO } from \"../options/constants.ts\";\n\n// ========== resolveLLMConfig ==========\n\ndescribe(\"resolveLLMConfig\", () => {\n it(\"resolves Gemini provider correctly\", () => {\n const multi: LLMMultiConfig = {\n activeProvider: \"gemini\",\n providers: {\n ...DEFAULT_PROVIDERS,\n gemini: { apiKey: \"test-key\", model: \"gemini-2.0-flash\" },\n },\n };\n const result = resolveLLMConfig(multi);\n expect(result.format).toBe(\"gemini\");\n expect(result.apiKey).toBe(\"test-key\");\n expect(result.baseUrl).toBe(\"\");\n expect(result.model).toBe(\"gemini-2.0-flash\");\n });\n\n it(\"resolves ChatGPT provider correctly\", () => {\n const multi: LLMMultiConfig = {\n activeProvider: \"chatgpt\",\n providers: {\n ...DEFAULT_PROVIDERS,\n chatgpt: { apiKey: \"sk-xxx\", model: \"gpt-4o-mini\" },\n },\n };\n const result = resolveLLMConfig(multi);\n expect(result.format).toBe(\"openai-compatible\");\n expect(result.apiKey).toBe(\"sk-xxx\");\n expect(result.baseUrl).toBe(\"https://api.openai.com\");\n expect(result.model).toBe(\"gpt-4o-mini\");\n });\n\n it(\"resolves DeepSeek provider correctly\", () => {\n const multi: LLMMultiConfig = {\n activeProvider: \"deepseek\",\n providers: {\n ...DEFAULT_PROVIDERS,\n deepseek: { apiKey: \"ds-key\", model: \"deepseek-chat\" },\n },\n };\n const result = resolveLLMConfig(multi);\n expect(result.format).toBe(\"openai-compatible\");\n expect(result.baseUrl).toBe(\"https://api.deepseek.com\");\n });\n\n it(\"resolves Qwen provider correctly\", () => {\n const multi: LLMMultiConfig = {\n activeProvider: \"qwen\",\n providers: {\n ...DEFAULT_PROVIDERS,\n qwen: { apiKey: \"qwen-key\", model: \"qwen-turbo\" },\n },\n };\n const result = resolveLLMConfig(multi);\n expect(result.format).toBe(\"openai-compatible\");\n expect(result.baseUrl).toBe(\"https://dashscope.aliyuncs.com/compatible-mode\");\n });\n\n it(\"resolves Kimi provider correctly\", () => {\n const multi: LLMMultiConfig = {\n activeProvider: \"kimi\",\n providers: {\n ...DEFAULT_PROVIDERS,\n kimi: { apiKey: \"kimi-key\", model: \"moonshot-v1-8k\" },\n },\n };\n const result = resolveLLMConfig(multi);\n expect(result.format).toBe(\"openai-compatible\");\n expect(result.baseUrl).toBe(\"https://api.moonshot.cn\");\n });\n});\n\n// ========== migrateLLMConfig ==========\n\ndescribe(\"migrateLLMConfig\", () => {\n it(\"passes through new format unchanged\", () => {\n const multi: LLMMultiConfig = {\n activeProvider: \"chatgpt\",\n providers: {\n ...DEFAULT_PROVIDERS,\n chatgpt: { apiKey: \"sk-xxx\", model: \"gpt-4o\" },\n },\n };\n const result = migrateLLMConfig(multi);\n expect(result.activeProvider).toBe(\"chatgpt\");\n expect(result.providers.chatgpt.apiKey).toBe(\"sk-xxx\");\n });\n\n it(\"migrates old Gemini format\", () => {\n const old = { format: \"gemini\", apiKey: \"old-key\", baseUrl: \"\", model: \"gemini-2.0-flash\" };\n const result = migrateLLMConfig(old);\n expect(result.activeProvider).toBe(\"gemini\");\n expect(result.providers.gemini.apiKey).toBe(\"old-key\");\n expect(result.providers.gemini.model).toBe(\"gemini-2.0-flash\");\n });\n\n it(\"migrates old OpenAI format\", () => {\n const old = { format: \"openai-compatible\", apiKey: \"sk-old\", baseUrl: \"https://api.openai.com\", model: \"gpt-4o-mini\" };\n const result = migrateLLMConfig(old);\n expect(result.activeProvider).toBe(\"chatgpt\");\n expect(result.providers.chatgpt.apiKey).toBe(\"sk-old\");\n });\n\n it(\"handles undefined/null gracefully\", () => {\n const result = migrateLLMConfig(undefined);\n expect(result.activeProvider).toBe(\"gemini\");\n expect(result.providers.gemini.apiKey).toBe(\"\");\n });\n\n it(\"handles empty object gracefully\", () => {\n const result = migrateLLMConfig({});\n expect(result.activeProvider).toBe(\"gemini\");\n });\n});\n\n// ========== Dashboard data aggregation ==========\n\ndescribe(\"Dashboard data aggregation\", () => {\n let db: IDBDatabase;\n\n beforeEach(async () => {\n resetDBInstance();\n db = await openDB(indexedDB);\n });\n\n afterEach(() => {\n closeDB();\n indexedDB.deleteDatabase(DB_NAME);\n });\n\n it(\"counts learning records correctly\", async () => {\n await learningRecordDAO.add(db, {\n sentence: \"Test sentence 1\",\n chunked: \"Test\\n sentence 1\",\n new_words: [],\n pattern_key: \"insertion\",\n });\n await learningRecordDAO.add(db, {\n sentence: \"Test sentence 2\",\n chunked: \"Test\\n sentence 2\",\n new_words: [{ word: \"test\", definition: \"测试\" }],\n pattern_key: \"nested\",\n });\n\n const all = await learningRecordDAO.getAll(db);\n expect(all.length).toBe(2);\n });\n\n it(\"counts vocab by status correctly\", async () => {\n await vocabDAO.add(db, {\n word: \"emergent\",\n status: \"new\",\n definition: \"涌现的\",\n });\n await vocabDAO.add(db, {\n word: \"validate\",\n status: \"mastered\",\n definition: \"验证\",\n });\n\n const all = await vocabDAO.getAll(db);\n const mastered = all.filter((v) => v.status === \"mastered\");\n expect(all.length).toBe(2);\n expect(mastered.length).toBe(1);\n });\n\n it(\"sorts learning records by created_at descending\", async () => {\n const r1 = await learningRecordDAO.add(db, {\n sentence: \"First\",\n chunked: \"First\",\n new_words: [],\n });\n // Ensure different timestamps (Date.now() can return same ms)\n await new Promise(r => setTimeout(r, 10));\n const r2 = await learningRecordDAO.add(db, {\n sentence: \"Second\",\n chunked: \"Second\",\n new_words: [],\n });\n\n const all = await learningRecordDAO.getAll(db);\n const sorted = [...all].sort((a, b) => b.created_at - a.created_at);\n expect(sorted[0].sentence).toBe(\"Second\");\n });\n});\n\n// ========== Sentences filtering ==========\n\ndescribe(\"Sentences filtering\", () => {\n let db: IDBDatabase;\n\n beforeEach(async () => {\n resetDBInstance();\n db = await openDB(indexedDB);\n });\n\n afterEach(() => {\n closeDB();\n indexedDB.deleteDatabase(DB_NAME);\n });\n\n it(\"filters by pattern_key correctly\", async () => {\n await learningRecordDAO.add(db, {\n sentence: \"Insertion sentence\",\n chunked: \"Insertion\\n sentence\",\n new_words: [],\n pattern_key: \"insertion\",\n });\n await learningRecordDAO.add(db, {\n sentence: \"Nested sentence\",\n chunked: \"Nested\\n sentence\",\n new_words: [],\n pattern_key: \"nested\",\n });\n await learningRecordDAO.add(db, {\n sentence: \"Another insertion\",\n chunked: \"Another\\n insertion\",\n new_words: [],\n pattern_key: \"insertion\",\n });\n\n const all = await learningRecordDAO.getAll(db);\n const insertions = all.filter((r) => r.pattern_key === \"insertion\");\n const nested = all.filter((r) => r.pattern_key === \"nested\");\n\n expect(all.length).toBe(3);\n expect(insertions.length).toBe(2);\n expect(nested.length).toBe(1);\n });\n\n it(\"collects available pattern keys from data\", async () => {\n await learningRecordDAO.add(db, {\n sentence: \"S1\",\n chunked: \"S1\",\n new_words: [],\n pattern_key: \"insertion\",\n });\n await learningRecordDAO.add(db, {\n sentence: \"S2\",\n chunked: \"S2\",\n new_words: [],\n pattern_key: \"contrast\",\n });\n await learningRecordDAO.add(db, {\n sentence: \"S3\",\n chunked: \"S3\",\n new_words: [],\n // no pattern_key\n });\n\n const all = await learningRecordDAO.getAll(db);\n const patterns = Array.from(\n new Set(all.map((r) => r.pattern_key).filter(Boolean))\n );\n expect(patterns).toContain(\"insertion\");\n expect(patterns).toContain(\"contrast\");\n expect(patterns.length).toBe(2);\n });\n});\n\n// ========== Review data selection ==========\n\ndescribe(\"Review data selection\", () => {\n let db: IDBDatabase;\n\n beforeEach(async () => {\n resetDBInstance();\n db = await openDB(indexedDB);\n });\n\n afterEach(() => {\n closeDB();\n indexedDB.deleteDatabase(DB_NAME);\n });\n\n it(\"prefers records with pattern_key + sentence_analysis\", async () => {\n const valuable = await learningRecordDAO.add(db, {\n sentence: \"Valuable sentence\",\n chunked: \"Valuable\\n sentence\",\n new_words: [],\n pattern_key: \"insertion\",\n sentence_analysis: \"This is why it's hard.\",\n });\n await learningRecordDAO.add(db, {\n sentence: \"Plain sentence\",\n chunked: \"Plain sentence\",\n new_words: [],\n });\n\n const all = await learningRecordDAO.getAll(db);\n const valuableOnes = all.filter((r) => r.pattern_key && r.sentence_analysis);\n expect(valuableOnes.length).toBe(1);\n expect(valuableOnes[0].sentence).toBe(\"Valuable sentence\");\n });\n\n it(\"falls back to any record when no valuable ones exist\", async () => {\n await learningRecordDAO.add(db, {\n sentence: \"Plain sentence\",\n chunked: \"Plain sentence\",\n new_words: [],\n });\n\n const all = await learningRecordDAO.getAll(db);\n const valuable = all.filter((r) => r.pattern_key && r.sentence_analysis);\n expect(valuable.length).toBe(0);\n expect(all.length).toBe(1);\n });\n\n it(\"counts weekly sentences correctly\", async () => {\n const now = Date.now();\n const weekStart = new Date();\n weekStart.setHours(0, 0, 0, 0);\n weekStart.setDate(weekStart.getDate() - ((weekStart.getDay() + 6) % 7));\n\n // This week's record\n await learningRecordDAO.add(db, {\n sentence: \"This week\",\n chunked: \"This week\",\n new_words: [],\n });\n\n const all = await learningRecordDAO.getAll(db);\n const weekRecords = all.filter((r) => r.created_at >= weekStart.getTime());\n expect(weekRecords.length).toBe(1);\n });\n});\n\n// ========== Constants completeness ==========\n\ndescribe(\"Constants completeness\", () => {\n it(\"PATTERN_LABELS covers all PatternKey values\", () => {\n const patternKeys: PatternKey[] = [\n \"insertion\", \"background_first\", \"nested\", \"long_list\",\n \"inverted\", \"long_subject\", \"omission\", \"contrast\",\n \"condition\", \"long_modifier\", \"other\",\n ];\n for (const key of patternKeys) {\n expect(PATTERN_LABELS[key]).toBeDefined();\n expect(typeof PATTERN_LABELS[key]).toBe(\"string\");\n }\n });\n\n it(\"PROVIDER_INFO covers all 5 providers\", () => {\n const providers: ProviderKey[] = [\"gemini\", \"chatgpt\", \"deepseek\", \"qwen\", \"kimi\"];\n for (const key of providers) {\n expect(PROVIDER_INFO[key]).toBeDefined();\n expect(PROVIDER_INFO[key].models.length).toBeGreaterThan(0);\n expect(PROVIDER_INFO[key].hint.length).toBeGreaterThan(0);\n }\n });\n\n it(\"PROVIDER_META covers all 5 providers\", () => {\n const providers: ProviderKey[] = [\"gemini\", \"chatgpt\", \"deepseek\", \"qwen\", \"kimi\"];\n for (const key of providers) {\n expect(PROVIDER_META[key]).toBeDefined();\n expect(PROVIDER_META[key].label).toBeDefined();\n expect([\"gemini\", \"openai-compatible\"]).toContain(PROVIDER_META[key].format);\n }\n });\n\n it(\"DEFAULT_PROVIDERS has default model for each provider\", () => {\n const providers: ProviderKey[] = [\"gemini\", \"chatgpt\", \"deepseek\", \"qwen\", \"kimi\"];\n for (const key of providers) {\n expect(DEFAULT_PROVIDERS[key].model.length).toBeGreaterThan(0);\n }\n });\n});\n"}}}}