adventure-table

P2-B Closeout Checklist

P2-B — Room Character Workspace closeout scope。編號對應 實作規格 的「完成後必須為真」。

Verification evidence

分支 p2-b-room-character-workspace,最終 code SHA 8dbbbe0

Branch / final code SHA
  p2-b-room-character-workspace @ 8dbbbe0

Alembic heads
  0009_p2a_character_head (character) (head)
  0011_p2b_room_workspace (web) (head)
  以 `alembic heads` / `alembic branches` 實測。
  0011 接在 0010_p2a_web_rooms 之後,只落在 web track;
  standalone SQLite 仍只升 character@head,不會長出 Room schema。

Backend pytest
  1022 collected, exit 0
  cwd apps/server,直譯器 ..\..\.venv\Scripts\python.exe
  執行於 335ba95;335ba95..8dbbbe0 只觸及 apps/web,未重跑。
  5 個 PostgreSQL-only 測試在本機 skip,由下方 CI job 覆蓋。

P2 Non-E2E CI
  Actions run 34068353554 @ c40ea70,backend / frontend / postgres-migrations 三個 job 全 success
  postgres-migrations job 實際執行本機會 skip 的 5 個測試:
    test_p2a_postgres_migration.py::test_fresh_web_postgres_upgrade_heads_and_readiness
    test_p2a_postgres_migration.py::test_legacy_m03_postgres_upgrade_heads_preserves_character_payloads
    test_p2b_postgres_workspace.py::test_concurrent_cross_room_association_has_exactly_one_winner[character]
    test_p2b_postgres_workspace.py::test_concurrent_cross_room_association_has_exactly_one_winner[draft]
    test_p2b_postgres_workspace.py::test_concurrent_legacy_claim_never_splits_unscoped_workspace_data
  5 passed, 2.70s

Frontend unit
  180 passed / 37 files

TypeScript / build
  npm run build(tsc --noEmit && vite build)exit 0
  tsconfig.json 的 include 涵蓋 e2e/,E2E helper 的型別一併受檢

Full Web Playwright
  npm run test:e2e:docker exit 0 @ 8dbbbe0
  第一輪 105 tests:102 passed / 0 failed / 3 skipped(6.8m)
  第二輪 xge-less M03-C 子集:7 passed
  本次全套無 KI-P1D-001 失敗

Standalone build / frozen smoke
  scripts\build-standalone.cmd --version p2b-check
    STANDALONE_PYTHON 指向 CPython 3.13.13(發版契約要求 3.13,本機預設為 3.14)
    check_standalone_env.py 比對 constraints-standalone-win.txt 通過
    產出 dist\adventure-table-standalone-p2b-check.zip
  .standalone-venv\Scripts\python.exe scripts\smoke_standalone.py dist\adventure-table-standalone --timeout 30
    Standalone smoke passed.

docker compose config
  exit 0

驗收條目 → 測試對應

條目 證據
1, 9 test_p2b_room_character_api.py::test_web_global_character_and_builder_mutation_routes_are_closed
2 test_p2b_workspace_transactions.py::test_create_draft_and_room_association_commit_together
2(反向) test_p2b_workspace_transactions.py::test_create_draft_rolls_back_when_room_association_fails
3 test_p2b_room_character_api.py::test_room_a_cannot_use_room_b_character_or_draft_ids 末段 Room A list 為空
4 同上,character 10 條 + draft 6 條全驗 404
5 test_p2b_workspace_transactions.py::test_bound_core_confirm_rolls_back_character_and_confirm_marker_with_outer_failure
6 test_p2b_acceptance_evidence.py::test_versioned_draft_inherits_character_roomlevel_up / build_edit / correction parametrized)
7 test_p2b_acceptance_evidence.py 三條 import 測試:full character、missing refs repair draft、history-loss 與 dry-run
8 roomCharacterRouting.test.tsApp.test.tsx::roomCharacterRouteFromPathcharacterWorkspace.test.ts
10 test_m03_import_boundary.pytest_p2a_room_import_boundary.py、frozen smoke
11 test_p2b_review_guards.py::test_room_hard_delete_leaves_no_character_workspace_or_access_orphanstest_p2b_legacy_and_room_delete.py::test_room_hard_delete_removes_only_that_workspace_core_and_import_rows
12 test_p2b_legacy_and_room_delete.py::test_first_room_bootstrap_claims_existing_unscoped_character_data_atomically::test_owner_claim_card_exposes_counts_only_and_claim_is_idempotenttest_p2b_review_guards.py::test_first_room_bootstrap_conflict_retries_the_whole_room_transaction::test_legacy_claim_race_is_a_stable_conflict_not_a_500
13 test_p2b_acceptance_evidence.py::test_member_and_dm_can_archive_but_cannot_permanently_delete_character(member / DM 皆得 403 room_owner_required;Owner 需先 archive)
14 web_room_support.pyWebRoomTestClient + 遷移後的 8 支既有 backend regression;e2e/support/roomTest.ts 的 Room proxy + 全套 Playwright
7.1 association 唯一性 test_p2b_workspace_associations.py 三條
PostgreSQL concurrency test_p2b_postgres_workspace.py 三條(CI)

Human smoke

測試指南 §17,P2-B 範圍為第 1–7、13、14 步,由使用者於 2026-09-07 在本機完整執行並回報通過。§17 列的四項 blocker 判準(誤刪 Room、進錯 Room、legacy data claim 到錯 Room、以為 Character 跨 Room 共用)均未構成阻擋。

過程中觀察到兩項 UX 議題,皆非 blocker,列入下方「已知限制」。

關門過程中修正的問題

驗證期間發現 4 項,全部已修並重新驗證。

  1. page.request 未被 Room fixture 代理,16 條 E2E 失敗。 roomTest.ts 只把 scoping proxy 套在 request fixture 上,m01e / m01f / m01l / m01m 共 14 處改用 page.request.* 直打 global /api/character-builder/...。global router 收口後這些呼叫回 404,四支 spec 共 16 條失敗。已新增 roomPageProxy() 覆寫 page fixture,使 page.request 套用同一組 namespace 改寫與 Authorization header。

  2. m02h-bilingual-site-smoke 的 ROUTES 仍是 global 前端路徑,3 條失敗。 只有 /characters 被特判成 openCharacterWorkshop()/characters/{id}/characters/{id}/versions 與 Builder crawl 的 /character-builder/{draftId} 仍是 global。這些路徑會觸發新的 LegacyWebCharacterRedirect,其 window.location.replace()forceLocale()page.evaluate 執行中摧毀 execution context。已改為 routes(roomId) 並讓 Builder crawl 走 Room-scoped 路徑。

  3. 移除 window.fetch monkeypatch 後,m02a 的頁內原生 fetch 漏接。 第 1、2 項修正的同時,production code 把 installRoomCharacterRouting() 的全域 window.fetch 覆寫與 document click 攔截改成 characterWorkspace.ts 的顯式 Room context。m02a-localization.spec.tsreadDraft()page.evaluate 內的原生 fetch,那是第三種既不經 request 也不經 page.request 的呼叫形態,於是回 404。已改用 page.request.get()。全 e2e/ grep 確認頁內原生 fetch 僅此一處。

  4. Room heading strip 撐滿一個 viewport。 .room-character-workspace-heading 借用 .workshop-page 以對齊 Workshop shell,連帶繼承 min-height: 100vh。沒有 legacy migration card 可填時,它就是一整片空白並把 Workshop 推到摺線下。已在 rooms.cssmin-height: 0 / padding-bottom: 0,實測 heading 高度由 900px 降為 69.4px、Workshop hero top 由約 930px 降為 101px;同時在 RoomCharacterWorkspacePage.tsx 補上 import './rooms.css',不再依賴其他 Room 頁面順帶把樣式載進 bundle。

Boundary

已知限制

Handoff

P2-B 已完成並關門。下一步是 P2-C — Campaign & Party Roster

P2-C 直接繼承以下 substrate,不得再造第二套:

P2-C 若讓 Campaign / Roster 產生對 Character 的 history reference,必須同時補上本檔「已知限制」第一條所指的 permanent delete guard 與測試。