diff --git a/AGENTS.md b/AGENTS.md index e86ce4d..63d4f85 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,8 +17,8 @@ | `@app/models` | Drizzle 表定义(唯一 schema 源) | drizzle-orm | | `@app/db` | libSQL/Turso 客户端单例 + drizzle-kit | models | | `@app/dao` | 数据访问层(`userDao` / `mountDao` / `mediaItemDao` 等) | db, models | -| `@app/trpc` | tRPC 路由 + service 业务逻辑(调用 DAO) | types, dao | -| `@app/web` | Astro 前端 + React islands + 本地 tRPC HTTP 适配 | trpc, types, design-tokens, seo-geo, i18n, ui | +| `@app/trpc` | tRPC 路由 + service 业务逻辑(调用 DAO) | types, dao, danmaku | +| `@app/web` | Astro 前端 + React islands + 本地 tRPC HTTP 适配 | trpc, types, danmaku(`./browser`), design-tokens, seo-geo, i18n, ui | | `@app/ui` | shadcn 风格基础 UI 组件(源码直出 + Storybook) | design-tokens, react (peer) | | `@app/design-tokens` | 共享 CSS 设计令牌(`tokens.css`) | — | | `@app/seo-geo` | SEO/GEO 纯函数(meta、sitemap、JSON-D、llms.txt) | zod, schema-dts, escape-html, fast-xml-builder, serialize-javascript, mdast-util-to-markdown | diff --git a/packages/danmaku/src/client.test.ts b/packages/danmaku/src/client.test.ts index ab0df62..10ccc68 100644 --- a/packages/danmaku/src/client.test.ts +++ b/packages/danmaku/src/client.test.ts @@ -1,5 +1,12 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { hasOpenCredentials, openApiBase, openHeaders, readOpenCredentials } from "./client.js"; +import { + hasOpenCredentials, + openApiBase, + openCommentXml, + openHeaders, + openMatch, + readOpenCredentials, +} from "./client.js"; afterEach(() => { vi.unstubAllEnvs(); @@ -50,3 +57,59 @@ describe("openHeaders", () => { expect(openHeaders("/api/v2/match", null)).toEqual({ "Content-Type": "application/json" }); }); }); + +function stubFetchJson(json: unknown) { + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => json, + }); + vi.stubGlobal("fetch", fetchMock); + return fetchMock; +} + +describe("openCommentXml 请求契约", () => { + it("请求路径含 ?withRelated=true(关联弹幕计入)", async () => { + const fetchMock = stubFetchJson({ success: true, comments: [] }); + await openCommentXml(555); + expect(fetchMock).toHaveBeenCalledTimes(1); + const url = String(fetchMock.mock.calls[0]?.[0]); + expect(url).toBe("https://api.dandanplay.net/api/v2/comment/555?withRelated=true"); + }); + + it("comments 序列化为 xml,HTTP 失败返回 null", async () => { + const fetchMock = stubFetchJson({ + success: true, + comments: [{ p: "12.5,1,0,0,0,0,,", m: "hi" }], + }); + await expect(openCommentXml(555)).resolves.toContain(" ({}) }); + await expect(openCommentXml(555)).resolves.toBeNull(); + }); +}); + +describe("openMatch 请求契约", () => { + it("POST /api/v2/match,matchMode 随 hash 切换", async () => { + const fetchMock = stubFetchJson({ success: true, isMatched: true, matches: [] }); + + await openMatch("Show - 02", 100, "abc"); + const [url1, init1] = fetchMock.mock.calls[0] ?? []; + expect(String(url1)).toBe("https://api.dandanplay.net/api/v2/match"); + expect(init1?.method).toBe("POST"); + expect(JSON.parse(String(init1?.body))).toEqual({ + fileName: "Show - 02", + fileHash: "abc", + fileSize: 100, + matchMode: "hashAndFileName", + }); + + await openMatch("Show - 02", 100, null); + const init2 = fetchMock.mock.calls[1]?.[1]; + expect(JSON.parse(String(init2?.body))).toMatchObject({ + fileHash: "", + matchMode: "fileNameOnly", + }); + }); +});