adventure-table

M03 — 測試指南

Phase:M03 — Standalone Character Builder Distribution 本文件定義 M03-A~M03-G 每個 Subphase 的自動 / 人工驗收流程與測試證據要求。驗收意圖見 實作規格.md;實作契約見 開發設計方針.md

最後更新:2026-09-04


1. 測試總則

1.1 M03 的 E2E 範圍對照

分層 gate 的通則見 AGENTS.md「工程實作守則」。本節只記 M03 專屬的對照;已關門的 M03-A / B / C 不回填。

Subphase diff 性質 Subphase 關門要跑的 E2E
M03-D backend / DB / 打包相依,不碰 apps/web 無。以 D.1~D.3 的 SQLite migration 與 FK pytest、D.4 的 Postgres round-trip、docker compose config 與容器起得來為證據
M03-E 新增 standalone entry 與 launcher,且 E.8 動到前端 capability 呈現 E.8 對應的 spec,加上 m03b-character-export.spec.tsm03c-character-import.spec.ts;E.0 frozen smoke 與 E.9 手動冷啟動照原契約跑,不因分層而省
M03-F Windows CI / release / import boundary,前端不動 無新 E2E。F.6 的網頁版 CI 無回歸沿用既有 workflow
M03-G Phase 關門 全套 Playwright,含 e2e-docker.mjs 拿掉 xge 的第二輪;G.2 的無回歸要求不變

合併回 main 前一律比照 M03-G 跑全套,不因單一 Subphase 是 backend-only 而略過。


2. 共通測試資料與觀察值

2.1 Locales

同 M02:zh-TW / en。所有雙語 test 皆為兩 locale 各跑一遍同一條 flow,斷言可見文字純該 locale。

2.2 測試角色 fixture

M03 新增三份匯出範例 JSON,放於 apps/server/tests/data/m03/

前四份於 M03-B 完成後透過 export endpoint 產生並提交進 repo;第五份手動編纂但通過 domain validation。

2.3 CI matrix

2.4 對「不變」的觀察

於每個 Subphase 完成後執行對照觀察,基線為 M03-A start baseline

實作者於 M03-A 開工當下產生一份 M03-A start baseline(docs/M03/baseline/m03a-start.json),作為後續每個 subphase 對照的凍結基準。M01 依使用者決定保持 open,因此這份基線是「M03-A 開工當下」的快照,不是 M01 full closeout 快照。 檔案內容為 enabled pack 清單、逐 pack entry 數與總 entry 數;測試一律經 apps/server/tests/m03_baseline.py 讀取,不得在測試碼內重複抄寫清單或數字。若 M01 後續補內容而使 entry 數變動,需同批更新本檔並在該 subphase 說明。

2.5 Enabled pack fixture


3. M03 Subphase 順序

A. Content Root Path Abstraction & Enabled-Pack SSOT
B. Character JSON Schema, Export & Builder Provenance
C. Character JSON Import via Builder Draft
D. SQLite Migration Chain Gate & FK PRAGMA
E. Standalone Packaging & Launcher
F. Windows CI Build, Release & Import Boundary Test
G. Full M03 Integration & Closeout

以下逐 Subphase 列出測試證據要求。


M03-A — Content Root Path Abstraction & Enabled-Pack SSOT

A.1 Path resolver unit test

apps/server/tests/test_m03a_paths.py

A.2 Registry loads via new resolver

apps/server/tests/test_m03a_registry_uses_resolver.py

A.3 Rules module 使用 resolver

apps/server/tests/test_m03a_rules_uses_resolver.py

A.4 api.dependencies 使用 resolver

apps/server/tests/test_m03a_dependencies_uses_resolver.py

A.5 Repo-wide constant sweep static test

apps/server/tests/test_m03a_no_legacy_path_constants.py

A.6 Enabled pack SSOT

apps/server/tests/test_m03a_enabled_packs.py

A.6.1 Subset unresolved 語意(實作規格 A.5.1)

apps/server/tests/test_m03a_enabled_packs.py

apps/server/tests/test_m01a_content_packs.py

A.7 Settings 環境變數相容性

apps/server/tests/test_m03a_settings_env_compat.py

A.8 網頁版無回歸

Evidence 檔:M03-A closeout 文件列出 A.1~A.8 每條 test 位置與最近一次執行時間戳。

A.9 本 Subphase 不需要


M03-B — Character JSON Schema, Export & Builder Provenance

B.1 Migration & provenance write

apps/server/tests/test_m03b_migration.py

apps/server/tests/test_m03b_confirm_writes_provenance.py

B.2 Pydantic model round trip

apps/server/tests/test_m03b_json_schema.py

B.2.1 Versioned draft seeding SSOT

apps/server/tests/test_m03b_versioned_draft_seeding.py

B.3 Content ref walker unit test(build)

apps/server/tests/test_m03b_build_ref_walker.py

B.4 Export payload builder

apps/server/tests/test_m03b_export_payload.py

B.5 Export endpoint

apps/server/tests/test_m03b_export_api.py

B.6 Frontend export button E2E

apps/web/e2e/m03b-character-export.spec.ts

B.7 Localization

B.8 網頁版功能無回歸

B.9 本 Subphase 不需要


M03-C — Character JSON Import via Builder Draft

C.1 State ref walker unit test

apps/server/tests/test_m03c_state_ref_walker.py

C.2 Import pipeline unit test

apps/server/tests/test_m03c_import_pipeline.py

C.3 Commit tests

apps/server/tests/test_m03c_commit.py

C.4 Rejection tests

apps/server/tests/test_m03c_import_rejections.py

每個 rejection code 一條 test:

每個 rejection 都斷言:

C.4.1 ValidationError 映射

apps/server/tests/test_m03c_validation_error_mapping.py

直接對 map_validation_error() 斷言(不經 HTTP),涵蓋實作規格 C.7.1 的整張表:

C.5 Duplicate hint

apps/server/tests/test_m03c_duplicate_hint.py

C.6 Endpoint

apps/server/tests/test_m03c_import_api.py

C.7 Alembic migration for import records

C.8 Frontend import dialog E2E

apps/web/e2e/m03c-character-import.spec.ts

C.9 Localization

C.10 網頁版功能無回歸

C.11 本 Subphase 不需要


M03-D — SQLite Migration Chain Gate & FK PRAGMA

D.1 Migration up/down/idempotent

apps/server/tests/test_m03d_migration_sqlite.py

D.2 Schema 一致性

apps/server/tests/test_m03d_schema_parity.py

D.3 SQLite FK PRAGMA

apps/server/tests/test_m03d_sqlite_fk.py

D.4 Postgres 未回歸

D.5 psycopg extras

apps/server/tests/test_m03d_dependency_extras.py

D.6 本 Subphase 不需要


M03-E — Standalone Packaging & Launcher

E.0 Minimal frozen smoke gate

順序要求:本 gate 綠燈之前,不進行 E.6 spec 靜態檢查、E.7 build script 與 E.8 前端整合。

理由見實作規格 E.0:_MEIPASS 下的 alembic ScriptDirectoryuvicorn.Config("app.standalone:app", ...) 的字串式 import、hiddenimports 缺漏,這三類問題 unit test 一個都測不到,只有真正 freeze 才會現形。越晚 freeze,爆出來時越難歸因。

scripts/smoke_standalone.py(與 F.2 同一支腳本,此處先以最小 bundle 使用):

  1. 用最小 PyInstaller 設定 freeze 出 launcher + alembic resources + app.standalone(不含 web/、不含 .zip、不進 CI)。
  2. 乾淨 tmp 資料夾執行該 exe,且不設 ADVENTURE_TABLE_DATABASE_PATH
  3. 斷言:
    • exe 同層出現 adventure-table.sqlite3
    • 該檔的 alembic_version 等於當時 head revision;
    • GET /api/meta/capabilities 回 200 且 channel="standalone"
    • stdout / stderr 全程不含 PostgreSQL 連線嘗試或 psycopg 相關訊息;
    • process 可被正常收掉。
  4. 不得python -m app.launcher(非 frozen)代替執行——那正是本 gate 要抓的差異。

通過後最小 spec 併入正式 spec,不保留兩份設定。

E.1 Neutral shared modules

apps/server/tests/test_m03e_error_handlers_shared.py

E.2 Capability endpoint

apps/server/tests/test_m03e_capabilities.py

E.3 Standalone app composition unit

apps/server/tests/test_m03e_standalone_composition.py

E.3.1 Meta router factory 隔離

apps/server/tests/test_m03e_meta_router_factory.py

E.4 SPA fallback

apps/server/tests/test_m03e_spa_fallback.py

E.4.1 Alembic bundle 可執行

apps/server/tests/test_m03e_alembic_bundle_ready.py

E.5 Launcher startup(headless)

apps/server/tests/test_m03e_launcher_headless.py

E.5.1 資料檔路徑解析與 SQLite 守衛

apps/server/tests/test_m03e_database_path.py

E.6 PyInstaller spec 靜態檢查

apps/server/tests/test_m03e_pyinstaller_spec.py

E.7 Build script dry-run

scripts/build-standalone.cmd --dry-run

E.8 Frontend capability integration

apps/web/src/features/capabilities/*.test.tsx

E.9 手動冷啟動(Windows)

於一台乾淨 Windows 11(未裝 Python / Node / Docker):

  1. 手動執行 scripts/build-standalone.cmd 於開發機上產出 .zip
  2. 複製 .zip 到乾淨機器,解壓。
  3. 雙擊 adventure-table.exe
  4. 斷言:
    • Console window 顯示資料檔絕對路徑、content root、spa root、監聽 URL、結束方式提示;資料檔路徑為實際絕對路徑而非佔位字串。
    • 解壓資料夾內確實出現 adventure-table.sqlite3,且與 adventure-table.exe 同層。
    • 瀏覽器自動打開 Landing。
    • Landing SPA 顯示資料檔絕對路徑。
    • 可從空白建立一個 Lv1 Fighter,Sheet 呈現正確。
    • 可匯出 JSON。
    • 手動打 /characters/<uuid> reload → 正確載入 Sheet(SPA fallback 生效)。
    • Ctrl+C on console 停止 process,瀏覽器頁面接連線失敗。
    • 關瀏覽器分頁不停 server(測試:關分頁後 curl 仍成功)。

M03-E closeout 文件列出人工冷啟動的截圖 / 描述。

E.10 本 Subphase 不需要


M03-F — Windows CI Build, Release & Import Boundary Test

F.1 Windows CI 綠燈

F.2 Smoke test 檔

scripts/smoke_standalone.py

F.3 本機發版(不做 GitHub Release)

發版一律在本機進行;CI 不建立 GitHub Release,因此本節沒有 tag 觸發或 gh release 的驗收項。

F.4 Import boundary test

apps/server/tests/test_m03_import_boundary.py

F.5 Standalone composition CI

F.6 網頁版 CI 無回歸

F.7 本 Subphase 不需要


M03-G — Full M03 Integration & Closeout

G.1 End-to-end round-trip 手動 flow

於 M03 期間的最新網頁版 + 最新單機版 .zip 上執行以下六條流程,每條保留 evidence(截圖 / 錄影 / test log):

G.1.a W → S 完整

  1. 網頁版建立一個 Lv5 Multiclass 角色(Fighter 3 / Cleric 2,Life Domain,含 starting equipment、prepared spells、live HP 有變動、Inventory 有加物)。
  2. Character Sheet 匯出 → 檔案 A.json
  3. 於單機版 Workshop 匯入 A.json
  4. Dry-run 顯示全部 resolved(build + state)、landing_mode="character"
  5. 繼續匯入 → Sheet 開啟後與原網頁版 Sheet 對照:
    • HP / 資源 / conditions / prepared / inventory 一致。
    • Version History 顯示 chain 完整(含 correction lineage 若有)。

G.1.b S → W 完整

  1. 單機版建立 Lv3 Ranger with Hunter subclass。
  2. 匯出 → B.json
  3. 網頁版匯入。
  4. 對照兩邊 Sheet 一致。

G.1.c W → S 缺 pack

  1. 網頁版建立引用 xge 的角色(Gloom Stalker Ranger)。
  2. 匯出 → C.json
  3. 於一個 enabled_content_packs 未含 xge 的單機版設定匯入 C.json(透過設 ADVENTURE_TABLE_ENABLED_CONTENT_PACKS 環境變數為 subset,或啟動 launcher 時另傳)。
  4. Dry-run 顯示 landing_mode="draft" 且 unresolved 清單含 subclass ref(origin=build)。
  5. 繼續 → 導向 Draft。
  6. 使用者於 Draft 中改選一個可用的 Ranger subclass。
  7. Confirm → 產生 Version 1 → Sheet 開啟正常。

G.1.d State-only 缺 ref(乾淨情境)

  1. 建立一個 Lv3 Fighter,Sheet 上手動加入一件 inventory item(例:srd5.1:item:potion-of-healing),但這件物品來自 Starting Equipment 或任何 Build 選擇——是 DM 後來給的。匯出為 D.json
  2. 於一個 subset 中把 srd5.1:item:potion-of-healing 刻意標為缺失(可透過測試 fixture 覆寫 registry,讓該 stable key 無法解析)。
  3. Dry-run 顯示 build 全解、state 未解 → landing_mode="draft_with_history_loss";unresolved 內出現 origin="state"
  4. Dialog 顯示 warning banner(history / state 將放棄);使用者二次確認。
  5. 走 Draft 補洞 → Confirm → 產生新 Version 1;舊 Current State 未進,舊 chain 未進。

選擇 inventory item 而非 active infusion 的原因:active infusion 的 infusion_ref 通常同時存在 Build 的 infusion_refs 中;若 pack 缺失,build ref 會先未解析,state ref 只是重複命中,無法乾淨證明「build 全解、僅 state 缺」情境。

G.1.e 重複匯入

  1. 同一份 A.json 於單機版匯入第二次。
  2. Dry-run 顯示 duplicate_hint
  3. 繼續 → 兩個角色並存於 Workshop。

G.1.f Legacy 拒絕

  1. 準備 fixture_legacy_no_provenance.jsonbuilder_provenance 全為 null)。
  2. 於缺對應 pack 的環境匯入。
  3. Dialog 顯示 draft_reconstruction_unavailable 訊息;character_import_records 無新 row;characters 無新 row。

G.2 網頁版無回歸

G.3 CI 綠燈

G.4 Release artifact

G.5 SSOT 更新

G.6 Localization 收尾

G.7 已知問題入 已知問題.md

G.8 本 Subphase 不需要


附錄 A:手動流程 evidence 存放

M03 各手動流程的截圖 / 錄影建議放於 docs/M03/evidence/,並於對應 closeout 文件引用檔名。若使用者選擇口頭 walkthrough 而非留檔,closeout 文件明示。


附錄 B:測試檔命名慣例

命名一致有助於 codex reviewer 定位證據。