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.
1 line
7.8 KiB
JSON
1 line
7.8 KiB
JSON
{"repo": "akokk0/bilibili-notify", "files": {"apps/server/src/routes/__tests__/globals-redact-fns.test.ts": {"ddd9f7f98ee49347444b40a8e8546fa472a865ce": {"t": 1786068554.548478, "n": 5760, "h": "25380cde94c8dcfa", "c": "/**\n * 单元测试 —— globals 的密钥脱敏。\n *\n * 密钥数量随「各家一套配置」翻了好几倍:**每个服务商桶里两把**(主模型的\n * `apiKey` 与看图副模型的 `vision.apiKey`),而桶最多五个。副模型常在另一家\n * (DeepSeek 没有视觉模型,跨厂商是常态而非例外),所以那把是真正独立的密钥。\n *\n * 每一把都得走同一套:GET 出去要打码,PATCH 回来见到打码占位要**保留原值**。\n * 漏掉任何一把的后果都很具体 —— 漏了脱敏,那把 key 就明文躺在浏览器的响应里;\n * 漏了 strip,主人在页面上随便改一项别的设置、点保存,就会把真 key 覆盖成\n * `__BN_REDACTED__` 这个字符串,而且**没有任何报错**,直到下次调用失败才发现。\n *\n * 所以这里刻意**遍历所有桶**去断言,而不是只挑一两家试 —— 写死家数的实现会在\n * 注册表加一家时静默漏掉那一家。\n */\n\nimport { AI_PROVIDER_IDS, makeDefaultGlobalConfig } from \"@bilibili-notify/internal\";\nimport { describe, expect, it } from \"vite-plus/test\";\nimport { REDACTED_API_KEY, redactGlobals, stripRedactedSecrets } from \"../globals.js\";\n\n/** 造一份「每家都配齐、两把 key 都填了」的 globals,key 里嵌 provider 名便于溯源。 */\nfunction withAllBuckets() {\n\tconst g = makeDefaultGlobalConfig();\n\tg.defaults.ai.providers = Object.fromEntries(\n\t\tAI_PROVIDER_IDS.map((id) => [\n\t\t\tid,\n\t\t\t{\n\t\t\t\tapiKey: `sk-main-${id}`,\n\t\t\t\tbaseUrl: \"https://x/v1\",\n\t\t\t\tmodel: \"m\",\n\t\t\t\ttemperature: 0.7,\n\t\t\t\tenableThinking: false,\n\t\t\t\tthinkingLevel: \"medium\" as const,\n\t\t\t\textraParams: \"\",\n\t\t\t\tenableVision: false,\n\t\t\t\tvision: { baseUrl: \"\", apiKey: `sk-vision-${id}`, model: \"\" },\n\t\t\t},\n\t\t]),\n\t);\n\treturn g;\n}\n\ndescribe(\"redactGlobals — 每个桶的两把 key 都要打码\", () => {\n\tit(\"所有桶、两把 key 全替换成占位\", () => {\n\t\tconst out = redactGlobals(withAllBuckets());\n\t\tfor (const id of AI_PROVIDER_IDS) {\n\t\t\texpect(out.defaults.ai.providers[id]?.apiKey).toBe(REDACTED_API_KEY);\n\t\t\texpect(out.defaults.ai.providers[id]?.vision.apiKey).toBe(REDACTED_API_KEY);\n\t\t}\n\t});\n\n\tit(\"整个响应里搜不到任何一段真 key\", () => {\n\t\t// 这条是兜底:哪怕将来加了新的密钥字段,只要它带 `sk-` 前缀就会被这里抓到。\n\t\tconst json = JSON.stringify(redactGlobals(withAllBuckets()));\n\t\texpect(json).not.toContain(\"sk-main-\");\n\t\texpect(json).not.toContain(\"sk-vision-\");\n\t});\n\n\tit(\"空值保持空 —— 前端要靠它区分「未配置」和「已配置」\", () => {\n\t\tconst g = makeDefaultGlobalConfig();\n\t\tg.defaults.ai.providers = {\n\t\t\tdeepseek: {\n\t\t\t\tapiKey: \"\",\n\t\t\t\tbaseUrl: \"\",\n\t\t\t\tmodel: \"\",\n\t\t\t\ttemperature: 0.7,\n\t\t\t\tenableThinking: false,\n\t\t\t\tthinkingLevel: \"medium\",\n\t\t\t\textraParams: \"\",\n\t\t\t\tenableVision: false,\n\t\t\t\tvision: { baseUrl: \"\", apiKey: \"\", model: \"\" },\n\t\t\t},\n\t\t};\n\t\tconst out = redactGlobals(g);\n\t\texpect(out.defaults.ai.providers.deepseek?.apiKey).toBe(\"\");\n\t\texpect(out.defaults.ai.providers.deepseek?.vision.apiKey).toBe(\"\");\n\t});\n\n\tit(\"只有副模型 key 的桶也照样打码 —— 别看主 key 空就跳过整桶\", () => {\n\t\tconst g = makeDefaultGlobalConfig();\n\t\tg.defaults.ai.providers = {\n\t\t\tdeepseek: {\n\t\t\t\tapiKey: \"\",\n\t\t\t\tbaseUrl: \"\",\n\t\t\t\tmodel: \"\",\n\t\t\t\ttemperature: 0.7,\n\t\t\t\tenableThinking: false,\n\t\t\t\tthinkingLevel: \"medium\",\n\t\t\t\textraParams: \"\",\n\t\t\t\tenableVision: false,\n\t\t\t\tvision: { baseUrl: \"\", apiKey: \"sk-vision-only\", model: \"qwen-vl\" },\n\t\t\t},\n\t\t};\n\t\texpect(redactGlobals(g).defaults.ai.providers.deepseek?.vision.apiKey).toBe(REDACTED_API_KEY);\n\t});\n\n\tit(\"没添加过的家不会被凭空造出来\", () => {\n\t\t// 打码顺手补全五个桶的话,设置页左栏会凭空多出四家。\n\t\tconst g = makeDefaultGlobalConfig();\n\t\tg.defaults.ai.providers = {\n\t\t\tdeepseek: {\n\t\t\t\tapiKey: \"sk-x\",\n\t\t\t\tbaseUrl: \"\",\n\t\t\t\tmodel: \"\",\n\t\t\t\ttemperature: 0.7,\n\t\t\t\tenableThinking: false,\n\t\t\t\tthinkingLevel: \"medium\",\n\t\t\t\textraParams: \"\",\n\t\t\t\tenableVision: false,\n\t\t\t\tvision: { baseUrl: \"\", apiKey: \"\", model: \"\" },\n\t\t\t},\n\t\t};\n\t\texpect(Object.keys(redactGlobals(g).defaults.ai.providers)).toEqual([\"deepseek\"]);\n\t});\n\n\tit(\"一把 key 都没配时原对象直出,不做无谓复制\", () => {\n\t\tconst g = makeDefaultGlobalConfig();\n\t\texpect(redactGlobals(g)).toBe(g);\n\t});\n});\n\ndescribe(\"stripRedactedSecrets — 占位回传即保留原值\", () => {\n\ttype AiPatch = { defaults: { ai: { providers: Record<string, Record<string, unknown>> } } };\n\t/** 取一个桶。tsconfig 开了 noUncheckedIndexedAccess,索引结果要兜一下才能断言。 */\n\tconst bucket = (out: unknown, id: string): Record<string, unknown> =>\n\t\t(out as AiPatch).defaults.ai.providers[id] ?? {};\n\n\tit(\"桶里的主 key 是占位 → 剔除,不覆盖真值\", () => {\n\t\tconst patch = {\n\t\t\tdefaults: { ai: { providers: { deepseek: { apiKey: REDACTED_API_KEY, model: \"ds\" } } } },\n\t\t};\n\t\tconst out = stripRedactedSecrets(patch);\n\t\texpect(bucket(out, \"deepseek\")).not.toHaveProperty(\"apiKey\");\n\t\t// 同一桶的其他字段必须留着 —— 主人这次改的就是模型名。\n\t\texpect(bucket(out, \"deepseek\").model).toBe(\"ds\");\n\t});\n\n\tit(\"桶里的副模型 key 是占位 → 剔除,同层其他字段留着\", () => {\n\t\tconst patch = {\n\t\t\tdefaults: {\n\t\t\t\tai: {\n\t\t\t\t\tproviders: { deepseek: { vision: { apiKey: REDACTED_API_KEY, model: \"qwen-vl\" } } },\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t\tconst out = stripRedactedSecrets(patch);\n\t\tconst vision = bucket(out, \"deepseek\").vision as Record<string, unknown>;\n\t\texpect(vision).not.toHaveProperty(\"apiKey\");\n\t\texpect(vision.model).toBe(\"qwen-vl\");\n\t});\n\n\tit(\"多个桶同时回传占位 → 每桶两把都剔除\", () => {\n\t\tconst patch = {\n\t\t\tdefaults: {\n\t\t\t\tai: {\n\t\t\t\t\tproviders: {\n\t\t\t\t\t\tdeepseek: { apiKey: REDACTED_API_KEY, vision: { apiKey: REDACTED_API_KEY } },\n\t\t\t\t\t\topenrouter: { apiKey: REDACTED_API_KEY, vision: { apiKey: REDACTED_API_KEY } },\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t\tconst out = stripRedactedSecrets(patch);\n\t\tfor (const id of [\"deepseek\", \"openrouter\"]) {\n\t\t\texpect(bucket(out, id)).not.toHaveProperty(\"apiKey\");\n\t\t\texpect(bucket(out, id).vision).not.toHaveProperty(\"apiKey\");\n\t\t}\n\t});\n\n\tit(\"主人真的换了 key → 原样放行,不能当占位吞掉\", () => {\n\t\tconst patch = {\n\t\t\tdefaults: {\n\t\t\t\tai: {\n\t\t\t\t\tproviders: {\n\t\t\t\t\t\tdeepseek: { apiKey: \"sk-brand-new\", vision: { apiKey: \"sk-vision-new\" } },\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t\tconst out = stripRedactedSecrets(patch);\n\t\texpect(bucket(out, \"deepseek\").apiKey).toBe(\"sk-brand-new\");\n\t\texpect((bucket(out, \"deepseek\").vision as Record<string, unknown>).apiKey).toBe(\n\t\t\t\"sk-vision-new\",\n\t\t);\n\t});\n\n\tit(\"只改了别的设置(patch 里压根没有 providers)时不炸也不改动\", () => {\n\t\tconst patch = { defaults: { ai: { provider: \"deepseek\" } } };\n\t\texpect(stripRedactedSecrets(patch)).toBe(patch);\n\t});\n\n\tit(\"patch 里完全没碰 ai 时原样返回\", () => {\n\t\tconst patch = { defaults: { cardStyle: { enabled: false } } };\n\t\texpect(stripRedactedSecrets(patch)).toBe(patch);\n\t});\n});\n"}}}} |