06 测试报告

受众:工程师与 AI Agent。本册汇总 Podcast 项目两条交付线的测试结果:后端 Catalog API(podcast-api/,Vitest)与 桌面端(desktop/,CEF + Qt,自研压测/计时夹具)。 真实性原则:所有数字与结论均给出 file_path:line 锚点,数据源为已核验的测试代码、夹具与压测/覆盖率产物。读不到的内容标注"未验证/待确认",不臆造。 关联分册:API 行为见《03 API 设计》、数据库与数据集见《02 数据模型》、桌面端架构见《05 桌面端》。本册可独立读懂测试核心。


1. 测试策略与分层

测试目标分两层:功能正确性(端点契约、错误码、鉴权、配额)与 数据一致性(API 返回必须与源数据集 JSONL 逐字段一致)。桌面端额外覆盖 稳定性(崩溃/挂起/进程泄漏)与 性能(启动/关闭耗时)。

层位置连接对象验证内容锚点
单元podcast-api/tests/unit/纯函数(无 IO)cursor 编解码、envelope、auth 解析、Zod schema、duration/日期格式化podcast-api/docs/TEST-PLAN.md:9
集成podcast-api/tests/integration/Route Handler 直调 + 真实 PG端点契约/过滤/排序/翻页/错误码/配额podcast-api/docs/TEST-PLAN.md:10
一致性podcast-api/tests/consistency/Route Handler + 真实 PG + 源 JSONL fixtureAPI/DB 响应 vs 源数据逐字段比对 + 聚合对账podcast-api/docs/TEST-PLAN.md:11
压力desktop/tests/stress-test.mjs签名版 .app(CDP 驱动)500 轮"启动→页面切换→关闭"生命周期稳定性desktop/tests/stress-test.mjs:2
性能desktop/tests/perf-timing.mjs签名/开发版 .app(perf_trace 打点 + CDP)启动/关闭毫秒级 A/B 计时desktop/tests/perf-timing.mjs:6

框架与门禁(podcast-api/vitest.config.ts):

项配置锚点
测试框架vitest@4(vitest run)podcast-api/package.json scripts.test
环境happy-dom;集成/一致性套件用 // @vitest-environment node 覆盖vitest.config.ts:16
includetests/**/*.{test,spec}.{ts,tsx}vitest.config.ts:19
testTimeout / hookTimeout30,000ms(聚合 count(*) 跨 22.8M 行需放宽)vitest.config.ts:22-23
覆盖率 providerv8;include src/lib/** + src/app/api/**;exclude src/lib/db/schema.tsvitest.config.ts:25-30
覆盖率阈值lines/functions/branches/statements 均 70%(不放宽)vitest.config.ts:31

集成/一致性套件要求 DATABASE_URL 指向已装载完毕全量数据的库(293,781 播客 / 22,867,469 剧集),通过 tests/helpers/{api,db,fixtures}.ts 提供 callRoute / expectOk / expectFail / TOKENS / pg 等工具(podcast-api/tests/helpers/api.ts:7,66,79,88)。


2. API 测试结果

2.1 总览:381 个测试全绿

指标结果说明
测试总数381 全绿16 个测试文件
测试文件16 个unit 6 + integration 9 + consistency 1
lint / typecheck通过(npm run lint && npm run typecheck)podcast-api/docs/TEST-PLAN.md:54 通过标准 4

16 文件构成(实际目录核验):tests/unit/(auth/cursor/envelope/format/health.service/validations,6 个)+ tests/integration/(auth/categories/downloads/episode-detail/episodes/health/languages/progress/shows,9 个)+ tests/consistency/consistency.test.ts(1 个)。

2.2 覆盖率(实测,coverage/index.html "All files")

维度比率计数阈值余量
Statements94.7%429 / 45370%+24.7pt
Branches92.83%246 / 26570%+22.83pt
Functions100%63 / 6370%+30pt
Lines93.92%371 / 39570%+23.92pt

锚点:podcast-api/coverage/index.html("All files 94.7% Statements 429/453 / 92.83% Branches 246/265 / 100% Functions 63/63 / 93.92% Lines 371/395")。覆盖范围 src/lib/** + src/app/api/**,排除纯声明文件 schema.ts(vitest.config.ts:28)。

2.3 按层用例分布

层用例数主要文件与覆盖面
单元62format(15)validations(17)auth(12)cursor(11)envelope(4)health.service(2)
集成114shows / episodes / downloads / progress / auth / categories / episode-detail / languages / health
一致性205见 §3
合计381—

单元/集成的"逻辑用例"经 it.each 参数化后展开为运行时用例(例:integration/auth.test.ts:53,61 的 it.each(ROUTES) 对每条受保护路由各跑一遍 401 检查),故运行时用例数高于源码中 it() 字面行数。一致性 205 的精确推导见 §3.1。

2.4 集成层关键用例摘录(按端点)

端点 / 主题代表用例期望锚点
鉴权(全路由)缺 / 非法 Bearer401 / code 1001;合法 token 回显 request_idtests/integration/auth.test.ts:53,61,68,78
languages首项 {id:all}count=总数;各语言 count 之和=总数;降序tests/integration/languages.test.ts:9
categories不带 langall.count=总播客数;15 主类齐全且 subs 非空;lang=en 各 count≤无 langtests/integration/categories.test.ts:16,43,56
shows 列表默认20 条;total=总数;episode_count 降序tests/integration/shows.test.ts:20
shows limit5/50/1005/50/50(钳制);0/-1/abc→code 1000tests/integration/shows.test.ts:54,58,62,66
shows cursor翻 3 页60 个 id 无重复;第 2 页起无 total;与单次大查询切片等价tests/integration/shows.test.ts:79,93
shows 过滤category=news / category+sub / lang=en每项命中;sub 缺 category→code 1000tests/integration/shows.test.ts:114,124,131,135
shows 搜索q=确定性关键词命中目标节目,含 score,按相关度降序tests/integration/shows.test.ts:155
shows 排序/区间sort=name/recent;updated_after/beforeCOLLATE 稳定序;NULLS LAST;闭区间含边界tests/integration/shows.test.ts:186,206
episodes 翻页100-300 集节目全量 keyset 走查累计=total,无重复/遗漏,与 DB 集合一致;NULL publish_date 尾部按 episode_uid 翻页tests/integration/episodes.test.ts:81,111,124
episodes is_downloadable无 audio_urlfalsetests/integration/episodes.test.ts:230
进度POST→GET 回读 / UPSERT / 隔离字段一致,progress_percent 一位小数;负数/超长/非整数→code 1000;跨 token 不可见tests/integration/progress.test.ts:41,65,111,117,139
下载试用配额第 1-3 次 / 第 4 次code 0 且 used 1→3;第 4 次 403 / code 1003tests/integration/downloads.test.ts:53,67
下载日配额注入 300 当日任务后 paid POST403 / code 1004,带 daily_quota 载荷tests/integration/downloads.test.ts:200
下载归属他人 task_id404 / code 2001(owner-scoped)tests/integration/downloads.test.ts:163
配额快照/user/quota/podcastsused+remaining=total,reset_at 为未来时刻tests/integration/downloads.test.ts:174

试用 vs 日配额顺序:API-DESIGN §2.9 规定试用检查先于日配额,故试用 token 即便注入 300 条仍得 1003;日配额 1004 路径用 paid token 验证(tests/integration/downloads.test.ts:11-14 注释 + 第 195、200 行)。


3. 数据一致性套件(核心验收)

唯一真值原则:以源 JSONL 为唯一真值,禁止以 DB 校 DB。任一映射字段不一致即 toEqual 失败并打印 diff(tests/consistency/consistency.test.ts:2-3 头注)。

3.1 用例数推导(205)

套件描述用例数锚点
§3.2 节目详情60 档抽样播客,每档全映射字段比对60consistency.test.ts:49 it.each(fixtures.sampled…)
§3.2 单集有序比对60 档 × newest 前 30 单集,逐项有序比对60consistency.test.ts:84
§3.2 单集详情40 个 episode pick 直查 /episodes/{id}40consistency.test.ts:115
§3.2 stream 逐字节40 个 pick 的 stream_url 与源 audioUrl 逐字节一致40consistency.test.ts:135
§3.3 聚合对账languages / categories / 抽样 total / DB episodes / DB podcasts5consistency.test.ts:160,174,189,205,212
合计205

3.2 抽样规则(确定性随机,可重放)

固定种子伪随机抽样,tests/consistency/build-fixtures.mjs 生成 fixtures.json:

参数值锚点
随机种子 SEED20260611build-fixtures.mjs:23(fixture 实测 seed=20260611)
每 shard 抽样数10(6 shard × 10 = 60 档,seeded reservoir sampling)build-fixtures.mjs:24,72-73,105-110
每档保留单集newest 前 30(publishDate DESC NULLS LAST, episodeUID DESC)build-fixtures.mjs:25,47-57
单集直查 picks40(跨有单集的抽样档分布,独立 PRNG SEED*7)build-fixtures.mjs:26,145-152

fixture 实测核验:sampled=60、episodePicks=40、aggregates.totalPodcasts=293781、aggregates.totalEpisodeRows=22867469、generatedAt=2026-06-10T17:55:23Z(tests/consistency/fixtures.json)。

3.3 字段映射比对(§3.1 映射表)

API 字段源字段变换比对方式
show.id/title/author/cover/color/description/web/language_idpodcastUID/podcastName/author/logo/color/desc/web/lang无(null 透传)全等 toEqual
show.categoriescategories[]去重后无序集合normalizeCategories(tests/helpers/fixtures.ts:88)
show.episode_countepisodes[].length(实存集数,≠episodeCountClaimed)计数全等
episode.durationduration秒→HH:MM:SS(独立实现 secondsToHms,不复用生产代码)全等 tests/helpers/fixtures.ts:78
episode.publish_datepublishDateunix 秒→YYYY-MM-DD(UTC);≤0/越界→null全等 tests/helpers/fixtures.ts:66
stream.stream_urlaudioUrl无逐字节 toBe;无 audio→2002(consistency.test.ts:142-148)

3.4 聚合对账结果

检查期望值锚点
languages all.count= 293,781(=fixtures.aggregates.totalPodcasts),各语言 count 之和=all.count,且逐语言等于 perLangconsistency.test.ts:160-172
categories 各主类 count= 源按 cat 去重的 distinct 播客数(fixture perCatDistinctPodcasts)consistency.test.ts:174-186
抽样 60 档单集 total= 源 episodes 数组长度(全 60 档校验)consistency.test.ts:189-203
DB episodes 行数= 22,867,469consistency.test.ts:205-208
DB podcasts 行数= 293,781consistency.test.ts:210-214

结论:0 差异。 抽样 60 档全字段、60×30 单集有序、40 个 stream 逐字节、聚合对账(293,781 / 22,867,469 + 分类去重计数)全部一致。

分类去重计数实测(fixtures.json:aggregates.perCatDistinctPodcasts,节选主类):news 38,112 / sports 34,238 / health--fitness 29,662 / leisure 29,309 / arts 23,641 / comedy 20,787 / business 20,715 / education 20,480 / religion--spirituality 19,288 / kids--family 16,993 / science 15,951 / tv--film 15,649 / fiction 15,141 / music 14,929 / society--culture 11,489。另有 history/government/true-crime/technology/unknown 各 4(数据集稀疏标签噪声)。 说明:当前数据集 perLang 仅 {en: 293781}(全量为英语;fixtures.json 实测),故"各语言 count 之和=总数"在单语言下退化为单项相等,断言逻辑仍成立。


4. 桌面端 500 轮稳定性压测

被测:dist/PodcastDesktop.app(Developer ID 签名 + 硬化运行时)。夹具 desktop/tests/stress-test.mjs,可重放。报告 desktop/tests/STRESS-REPORT.md。

4.1 测试协议(每轮)

  1. 启动签名版 app(CEF 框架加载 + 5 个 Helper 进程拉起)。
  2. CDP 探测页面就绪后执行 3 次页面切换:主页 → #/show/podcast_106702 → 返回主页 → 点击翻页(next)按钮(stress-test.mjs 第 78-93 行)。
  3. 经与关闭按钮完全相同的代码路径关闭:--auto-close-ms → QWidget::close → closeEvent → CEF 关闭握手(stress-test.mjs:1-3;desktop/src/main.cpp:234-245)。
  4. 逐轮校验 5 项:退出码 / 挂起(30s 硬超时)/ 终止信号 / macOS 系统崩溃报告增量(DiagnosticReports .ips)/ Helper 进程残留(stress-test.mjs 第 12-22、108-121 行)。

4.2 结果:500/500 通过,零崩溃

指标结果
完成轮次500 / 500
失败轮次0
退出码全部 0
挂起(>30s)0
异常信号(SIGSEGV 等)0
系统崩溃报告(.ips)新增0
页面切换成功1500 / 1500(每轮 3/3)
单轮耗时p50=6,964ms / p95=7,036ms / max=7,282ms(含 6s 固定驻留窗口)
Helper 残留仅 3 轮在退出后 300ms 采样点观测到正在终止的 Helper(夹具已清理,无跨轮累积——采样时机噪声,非泄漏)

结论:打开→页面切换→关闭的完整生命周期在 500 轮高频循环下无任何 crash/挂起/进程泄漏。此前用户报告的两类崩溃(关闭死锁、DevTools/关闭时 isHandlingSendEvent 崩溃)经修复后零复现(STRESS-REPORT.md §1.3)。


5. 启动/关闭性能 A/B

夹具 desktop/tests/perf-timing.mjs:解析 perf_trace.h 的 stderr [perf] <name> +<ms> 打点(desktop/src/perf_trace.h:3-5,30),缺打点时回退到外部可观测计时。基线/优化各 15 轮,全部 0 失败。

5.1 A/B 结果(同机静默环境,p50)

指标基线(旧泵)优化后提升
关闭耗时(点击→进程退出)599ms47ms↓92%(12.7×)
关闭 p90618ms58ms↓91%
启动→服务就绪(CDP)526ms364ms↓31%
启动→窗口可见无打点(基线晚于 CefInitialize)151ms感知启动大幅提前
启动→页面加载完成—374ms—
浏览器销毁(DoClose→destroyed)—18ms—

签名硬化版抽验(5 轮):窗口可见 p50=144ms、启动 p50=361ms、关闭 p50=41ms,与开发版一致(STRESS-REPORT.md §2.2)。

5.2 优化项(A/B 对照的改动来源)

#层改动锚点
1消息泵10ms 固定轮询 → CEF 外部消息泵(external_message_pump=1 + OnScheduleMessagePumpWork 精确调度,~30Hz 自持底线防停转)desktop/src/main.cpp:99-103,116,126-129,214;desktop/src/cef_app.cc:38-44
2感知启动窗口先显示、CefInitialize 后置;深色底(#0f1115)防白闪;浏览器创建解耦 showEventdesktop/src/main.cpp:184-194,228;desktop/src/qt_window.cpp:74-86
3启动裁剪关闭 GCM/组件更新/同步等后台服务(disable-background-networking 等开关)desktop/src/cef_app.cc:24-30
4关闭路径关闭安全网泵步进 10ms→1ms(关闭关键路径)desktop/src/main.cpp:258-265
5可观测性perf_trace.h 全链路毫秒级打点(stderr,夹具可解析)desktop/src/perf_trace.h:3-5,30

6. 测试中发现并修复的缺陷

#缺陷表现修复锚点
1int4 溢出未校验进度 position_seconds / duration_seconds 传超大值会击穿 PG int4 存储Zod 加 .max(INT4_MAX=2_147_483_647),越界返回 code 1000podcast-api/src/lib/validations/podcasts.ts:50-57;测试 tests/integration/progress.test.ts:117、tests/unit/validations.test.ts:82
2外部泵调度停转CEF 不保证每次 DoWork 后回调重新调度 → 导航/IPC 冻结按 cefclient 范式加 ~30Hz(33ms)自持底线,并以页面加载打点 + CDP 响应回归验证desktop/src/main.cpp:109-130;STRESS-REPORT.md §2.3
3关闭死锁关闭时 CEF 重发 OS close 被忽略 → close 取消、OnBeforeClose 不触发(关闭按钮像死键)closeEvent 在 browser_closed_ / IsClosing() 时接受重发的 close,CloseBrowser(false) 走 DoClose→OnBeforeClosedesktop/src/qt_window.cpp:88-108
4isHandlingSendEvent 崩溃Qt 的 QNSApplication 未实现 CEF 期望的 CefAppProtocol,DevTools/关闭时 "unrecognized selector" 崩溃给 NSApplication 加 category 补 isHandlingSendEvent/setHandlingSendEvent:desktop/src/browser_embed.mm:10-30
5假 loading(无谓闪烁)缓存/快速加载也弹 spinner,造成可见闪烁延迟显示 spinner(SPINNER_DELAY_MS=150),缓存命中直接翻无 spinnerpodcast-api/public/app/app.js:16,142,270-271,522-523
6公证票据缺口(离线 Gatekeeper)未 staple 的 app 只在能联网访问 Apple 时通过;离线/被墙内网机器报警公证后将票据 staple 进 app bundle,再封进 DMG(离线可过 Gatekeeper)desktop/package.sh:116-119,149-155;desktop/notarize-on-server.sh:69-71

缺陷 1 同时被单元(schema 层)与集成(端到端 400/1000)两层覆盖,体现"同一缺陷多层防回归"。缺陷 3、4 的零复现由 §4 的 500 轮压测背书(STRESS-REPORT.md §1.3)。


7. 复现方式

7.1 API 测试

# 前置:DATABASE_URL 指向已装载完毕的全量库(293,781 播客 / 22,867,469 剧集)
cd podcast-api
npm run test                 # vitest run,全部 381 用例
npm run test:coverage        # 带覆盖率(产物输出到 coverage/)
npm run lint && npm run typecheck   # 门禁(通过标准 4)

一致性 fixture 重建(确定性,可验证可重放,种子 20260611):

node podcast-api/tests/consistency/build-fixtures.mjs <dataset-dir> [out-file]

锚点:podcast-api/tests/consistency/build-fixtures.mjs:10。

7.2 桌面端压测与计时

# 500 轮稳定性压测(启动→3 次页面切换→关闭路径关闭)
node desktop/tests/stress-test.mjs <app-binary> 500 out.json

# 启动/关闭性能计时(N 轮,label 标识,可选输出 JSON)
node desktop/tests/perf-timing.mjs <app-binary> 15 baseline   /tmp/perf-baseline.json
node desktop/tests/perf-timing.mjs <app-binary> 15 optimized  /tmp/perf-optimized.json

锚点:desktop/tests/stress-test.mjs:5、desktop/tests/perf-timing.mjs:6。原始数据:/tmp/stress-500.json、/tmp/perf-{baseline,optimized,signed}.json(STRESS-REPORT.md 附录三)。


附:通过标准与现状

标准(podcast-api/docs/TEST-PLAN.md:50-54)现状
1. 全部用例绿✅ 381/381
2. 覆盖率 ≥70%✅ 实测 statements 94.7% / branches 92.83% / functions 100% / lines 93.92%
3. 一致性套件 0 差异✅ 60 档全字段 + 60×30 单集 + 40 stream 逐字节 + 聚合对账 0 差异
4. lint && typecheck 通过✅
桌面端 500 轮稳定性✅ 500/500、0 崩溃、0 挂起、0 系统崩溃报告
桌面端性能✅ 关闭 599→47ms(↓92%)、窗口可见 151ms、服务就绪 526→364ms