adventure-table

P2-E Closeout Checklist

P2-E — Session Lifecycle & Late Join closeout scope。編號對應 實作規格「P2-E — Session Lifecycle & Late Join」的各節要求。

Start Session

Active Character freeze

Concurrent Session invariant

Late Join

End / Abandon

Resume

Verification evidence

分支 p2-e-session-lifecycle-late-join,review 修正後 code SHA d0cecb7

Branch / final code SHA
  p2-e-session-lifecycle-late-join @ d0cecb7

Alembic heads
  0009_p2a_character_head (character) (head)
  0014_p2e_sessions (web) (head)
  0014 接在 0013_p2d_campaign_seats 之後,只落在 web track;
  standalone SQLite 仍只升 character@head,不會長出 sessions /
  session_participants / active_character_session_leases。

Backend pytest
  1046 collected / 180 files
  1036 passed, 10 skipped, 0 failed, exit 0
  cwd apps/server,直譯器 ..\..\.venv\Scripts\python.exe
  10 個 skip 全部是缺 P2_POSTGRES_URL 的 PostgreSQL-gated 測試
  (含 test_p2e_postgres_sessions.py 三條),改由下方 CI 覆蓋。

CI P2 Non-E2E(正式 workflow evidence)
  run id 34140748598 @ d0cecb7(exact final code SHA)
  https://github.com/shellpig/adventure-table/actions/runs/34140748598
  conclusion success;backend / frontend / postgres-migrations 三個 job 全綠。
  postgres-migrations 在真 PostgreSQL 上跑
  test_p2a_postgres_migration.py + test_p2b_postgres_workspace.py
  + test_p2e_postgres_sessions.py。
  review 前的 run 34136642169 @ 1666044 亦為 success。

Frontend unit
  219 passed / 47 files
  (d0cecb7 當下為 218;本 closeout commit 把 RoomSessionPage.tsx 加進
  hardcodedUiCopy.test.ts 的掃描清單,該檔多一條斷言。)

TypeScript / build
  npm run build(tsc --noEmit && vite build)exit 0

Full Web Playwright
  npm run test:e2e:docker exit 0 @ d0cecb7 + 本 closeout commit 的文件/掃描清單改動
  第一輪 107 tests:104 passed / 0 failed / 3 skipped(7.4m)
  第二輪 xge-less M03-C 子集:7 passed(6.5s)
  3 個 skip:m01j-subclass-expansion.spec.ts 的 direct-vs-sequential 等價
  (KI-M01J-001 intentional fixme,不計入 P2-E 功能證據),以及
  m03c-character-import.spec.ts 兩條需要缺 pack 後端的案例——後者正是
  第二輪跑的那批,已在該輪通過。
  執行時 compose server 已在 0014_p2e_sessions。

docker compose config
  exit 0

驗收條目 → 測試對應

條目 證據
1, 6, 8 (schema) test_p2e_persistence_contract.py::test_p2e_session_schema_matches_history_contract::test_p2e_participant_schema_keeps_non_null_seat_history::test_p2e_active_character_lease_uses_character_primary_key
2, 9 test_p2e_session_lifecycle.py::test_start_authority_matrix_offline_players_and_reopen_after_finalization(Owner 未被 assign、另一 DM Key holder、一般 member 三種 deny;player controller 心跳逾時仍可 Start;空 Player Seat 與未被選到的 Roster 角色都不進 Session)
3, 10 同上(finalize 後可重開下一場);test_p2e_session_lifecycle.py::test_start_freezes_dm_and_selected_players_then_end_releases_lease
4, 14 test_p2e_session_persistence.py::test_lease_collision_rolls_back_the_entire_second_session
5, 7, 23 test_p2e_session_lifecycle.py::test_start_freezes_dm_and_selected_players_then_end_releases_lease::test_start_authority_matrix_offline_players_and_reopen_after_finalization(DM 心跳逾時後 Session 仍 active,且仍由同一 identity End)
8 (guard) test_p2e_session_persistence.py::test_session_referenced_seat_cannot_hard_delete_but_can_archive
11, 12 test_p2e_session_lifecycle.py::test_active_character_snapshot_ignores_lobby_and_roster_changestest_p2e_session_api.py::test_session_http_error_contract_covers_read_join_lock_and_finalize(409 session_active_character_locked
13, 16 test_p2e_live_character_scope.py::test_live_character_state_http_scope_tracks_current_player_controller_and_fixed_dm::test_versioned_builder_mutations_use_same_live_character_scope::test_active_character_cannot_be_archived_until_session_releases_lease
15 test_p2e_postgres_sessions.py::test_concurrent_session_lease_collision_has_one_atomic_winner::test_cross_campaign_start_maps_existing_global_lease_to_stable_domain_error::test_end_and_late_join_serialize_without_partial_lease(CI postgres-migrations job)
17, 18, 19, 20 test_p2e_late_join.py 五條:::test_late_join_is_current_dm_only_and_snapshots_selected_character::test_late_join_revalidates_terminal_roster_status_inside_transaction::test_late_join_revalidates_archived_character_inside_transaction::test_late_join_revalidates_same_room_even_if_persistence_is_tampered::test_late_join_maps_existing_global_lease_without_partial_participant
21, 22 test_p2e_session_lifecycle.py::test_owner_can_abandon_without_becoming_replacement_dm::test_start_authority_matrix_offline_players_and_reopen_after_finalization(另一 DM Key holder End 被拒)
24 test_p2e_end_state_preservation.py::test_end_session_preserves_exact_rich_character_state_and_history(HP / temp HP / conditions / prepared spells / spell slots / resources / hit dice / inventory 全部改過後整份 model_dump 比對,並先斷言每個欄位確實非空以防退化成 vacuous 相等)
25, 28 test_p2e_session_api.py::test_session_routes_expose_start_resume_late_join_lock_end_and_abandon(router 只有 Start / Resume / Get / Late Join / lock / End / Abandon,沒有 token、chat、roll、combat、snapshot 面)
26, 27 test_p2e_session_resume.py::test_resume_composes_current_p2_truth_without_a_second_snapshot::test_resume_without_active_session_keeps_room_and_campaign_truth
Session 歷史 vs Campaign / Character 刪除 test_p2e_campaign_history_guard.py::test_campaign_with_session_history_cannot_be_hard_deleted_even_if_returned_to_drafttest_p2e_character_history_guard.py 兩條(Session history 擋個別 Character permanent delete,未被引用者仍走中性 delete primitive)
Room Hard Delete 的 RESTRICT 圖 test_p2e_room_hard_delete.py::test_room_hard_delete_removes_session_restrict_graph_before_owned_history
Restart persistence test_p2e_restart_persistence.py::test_restart_preserves_active_session_controller_participants_leases_and_access
穩定 API error code test_p2e_session_api.py::test_start_session_http_error_contract::test_session_http_error_contract_covers_read_join_lock_and_finalizetest_p2e_session_start_error_typing.py 兩條(分類依例外型別,不依訊息文字)
standalone 不長 Session schema test_m03d_schema_parity.py(三張新表已加入 FORBIDDEN_MULTIPLAYER_TABLES)、test_m03_import_boundary.pytest_p2a_room_import_boundary.py
migration track 分離 test_p2a_migration_tracks.pyWEB_REVISIONS 已納入 0014_p2e_sessions
前端 route / 權限 / 文案 RoomSessionPage.test.ts 六條、RoomLobbySession.test.ts 兩條、sessions.test.ts 四條、sessionMessages.test.tsseatMessages.test.tsp2eRequestMessages.test.tsroutes.test.ts/rooms/{id}/campaigns/{cid}/sessions/{sid}session capability)、hardcodedUiCopy.test.tsRoomSessionPage.tsx 已納入掃描清單)

關門過程中修正的問題

Review 提出 4 項,全數已修並補上 regression。

  1. character_in_active_session 沒有 localized message,會顯示成誤導的重新載入提示。 require_live_character_write() / require_character_unleased() 回 409 character_in_active_session,但 localizedRequestErrorMessage() 對未知 code 且 status 409 一律 fallback 到 revision_conflict——「資料已在其他操作中更新,請重新載入後再試一次。」實際原因是角色正被本場另一位參與者控制,重新載入不會有任何幫助。已把該 code 加進 REQUEST_CODE_MESSAGES,訊息同時涵蓋 live edit 與 archive 兩種觸發情境,並由 p2eRequestMessages.test.ts 斷言 zh 不含「重新載入」、en 不含 Reload

  2. seat_history_referenced 在 Lobby 只會吐英文 server message。 RoomLobbyPage 的錯誤處理對 SeatApiError 直接顯示 error.message,等於在 zh-TW UI 新增一段未翻譯字串。已新增 seatMessages.tsSeatApiError.message 改為走 currentSystemLocale() 的 lazy getter,與 createLocalizedRequestError 同一 pattern;順手把整組 seat error code(共 8 個,逐一對回 app/api/rooms/seats.py 的實際 APIError)一併補齊,不只補新增的那一個。

  3. 新的 Session route 落在 campaign capability gate。 protectedCapabilityForPath() 只特判 Lobby,/rooms/{id}/campaigns/{cid}/sessions/{sid} 會被 ROOM_CAMPAIGN_ROUTE 吃掉而回 campaign。因為 standalone 六個 multiplayer capability 全 false,boundary 沒有實際破口,但與 Lobby 明確 gate 成 seat 的做法不一致。已加 ROOM_SESSION_ROUTEsession,排在 campaign 之前,並補 routes.test.ts 斷言。

  4. Start 的錯誤分類靠字串比對。 start_session() 原本以 if "Caller" in message or "Start requires" in message 決定要丟 DMControllerMismatchError 還是 SessionLobbyUnavailableError——改動 persistence 的錯誤字句就會靜默改掉 HTTP error code。已新增 SessionStartControllerMismatchPersistenceErrorSessionStartPersistenceError 的子類),persistence 的兩個 authority 拒絕點改丟子類,domain 端字串比對整段移除。test_p2e_session_start_error_typing.py 兩條分別驗「訊息完全改寫仍正確分類」與「訊息含 Caller 也不會誤判成 dm mismatch」。

順帶修掉的既有問題

承接 P2-D 的未結清項目

Boundary

已知限制

Handoff

P2-E 已完成並關門。下一步是 P2-F — Full P2 Integration & Closeout

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