adventure-table

P3-A Closeout Checklist

P3-A — Session Table Runtime & Event Stream closeout scope。編號對應 實作規格 的「完成後必須為真」。

Verification evidence

分支 p3-a-session-table-runtime-event-stream,最終 SHA 96b54e6

Branch / final SHA
  p3-a-session-table-runtime-event-stream @ 96b54e6

Alembic heads(以 alembic branches / heads 實測)
  0008_m03c_import_records (branchpoint)
    -> 0009_p2a_character_head (character)
    -> 0010_p2a_web_rooms (web)
  0015_character_state_revision (character) (head)
  0016_p3a_table_runtime_events (web) (head)
  0016 接在 0014_p2e_sessions 之後,落在 web track,不觸及 character track。

Backend pytest(全套)
  1149 passed / 15 skipped,exit 0
  cwd apps/server,直譯器 ..\..\.venv\Scripts\python.exe
  執行於 bda8ffa;bda8ffa..96b54e6 只動 apps/web 與刪除一支一次性 workflow,
  backend 產品碼與測試零改動,故未重跑全套。

Focused P3-A backend(執行於 96b54e6)
  tests/test_p3a_table_events.py 4
  tests/test_p3a_event_api.py 2
  tests/test_p3a_event_wait.py 4
  tests/test_p3a_event_write_authorization.py 1
  tests/test_p3a_session_resume.py 2
  tests/test_p3_workflow_contract.py 4
  tests/test_p3a_postgres_events.py 3(本機 skip,見下)
  合計 18 passed / 3 skipped

Workflow contract + standalone boundary(執行於 96b54e6)
  tests/test_p3_workflow_contract.py + tests/test_p2a_workflow_contract.py
  + tests/test_m03_import_boundary.py:12 passed

PostgreSQL concurrency / migration gate
  tests/test_p3a_postgres_events.py 3 tests
  本機無 P3_POSTGRES_URL 而 skip;證據取自 P3 Non-E2E 的 postgres-migrations job。
  覆蓋:並發 append 配置唯一且單調的 seq、既有 P2 Session backfill runtime=0、
  migration 產出的 JSONB payload 型別與 metadata 一致。

Private-recipient query strategy
  採 開發設計方針 §14 第二種策略:bounded (session_id, seq) window + Server projection。
  TableEventRepository.list_after() 以 MAX_EVENT_SCAN_LIMIT 夾住 scan_limit 並 .limit(),
  Index ix_session_events_session_seq (session_id, seq)。
  不使用 JSONB/ARRAY membership predicate,故不需要 GIN。
  window 有硬 limit 的證據:test_event_sequence_idempotency_cursor_and_bounded_scan。

Event wait async / no-DB-hold / starvation
  tests/test_p3a_event_wait.py 4 tests
  含 notify 喚醒後 DB recheck 為準、故意遺失 process-local notify 仍由 DB cursor 補到、
  24 waiter vs pool_size=1 不餓死、cancellation 清理 registration 與 timer。

Frontend unit(執行於 96b54e6)
  50 files / 235 tests passed
  含 sessionEventStream.test.ts(cursor reducer)、sessionEventPoll.test.ts(reconnect)、
  sessionsP3a.test.ts(API client)、RoomSessionPage.test.ts(seat merge、連線狀態雙語)

TypeScript / build
  npm run build(tsc --noEmit && vite build)exit 0,執行於 96b54e6

docker compose config
  exit 0,執行於 bda8ffa;至 96b54e6 docker-compose.yml 未變動

P3 Non-E2E
  workflow .github/workflows/p3-non-e2e.yml,Display name「P3 Non-E2E」
  jobs:backend / frontend / postgres-migrations / windows-standalone
  run 34248269253 @ b9649a9,四個 job 全 success
  run 34291341436 @ 96b54e6,workflow_dispatch 補跑 branch tip,四個 job 全 success
  (96b54e6 帶 [skip ci],內容只有刪除一次性 workflow 檔;補跑是為了讓
    「exact SHA 綠」這條 closeout evidence 直接落在 final SHA 上。)

Session lifecycle E2E
  run 34248269818 @ b9649a9,workflow「P3-A Handoff E2E」success
  透過 npm run test:e2e:docker 執行 e2e/p2f-session-lifecycle.spec.ts
  該 workflow 為本次 handoff 的一次性 gate,證據留存後已於 96b54e6 移除,
  run 紀錄仍保留在 Actions history。

全套 E2E(合併回 main 的 gate,未通過)
  run 34291904852 @ dca0c87:106 passed / 2 failed / 3 skipped(12.0m)
  rerun 同一 run:同樣 2 failed
  兩次失敗都是 e2e/m01k-phb-feats-and-spells.spec.ts 的同兩支。
  P2-F 關門時(22220df)全套為 111 tests / 108 passed / 0 failed / 3 skipped,
  總數相同,差別即這兩支。

  失敗歸屬(closeout 後已查明,非 P3-A 造成):

  兩支的簽章都是「Test timeout of 30000ms exceeded」——整支測試用完
  Playwright 預設的 30 秒單測預算,而不是某次存檔沒完成。輸出裡的
  expect(...).toBeGreaterThan 與沒有前進的 revision,只表示截止當下還沒
  觀察到下一版;它與 KI-P1D-001 的「Timeout 5000ms exceeded while waiting
  on the predicate」意義相反,最初被誤讀成同一件事。

  決定性量測:run 34298326870
    只跑 m01k:392,--repeat-each=3 --timeout=180000 --reporter=list
    3/3 通過,耗時 33.8s / 32.5s / 32.6s——只超出 30 秒上限 2~4 秒。

  為何此時才浮現:`p2-e2e.yml` 從未執行過(gh run list 為空),本次是這套
  E2E 第一次在 GitHub runner 上跑。先前所有全套證據(含 P2-F 關門的
  108 passed)都取自本機 Windows→docker,硬體較快,同一支落在 30 秒內。

  m01k:342 的 --repeat-each 失敗另有一個獨立的測試缺陷:固定角色名
  「M01-K Level Up Tough」在共用資料庫上會產生多張同名 workshop card,
  使 toHaveCount(1) 失敗。與 timeout 無關,只在重複執行時出現。

  兩者的修法都不在 P3-A 範圍內,於後續 fix/m01k-e2e-timeout 處理:
  playwright.config.ts 設 timeout: 60_000,m01k:342 的角色名唯一化。

  P3-A 未觸及 Builder / Character 任何程式碼(diff 對
  app/api/character_builder.py、app/api/characters.py、app/persistence/characters.py、
  app/persistence/state_mutations.py 與 apps/web/src/features/character-builder/ 皆為空)。

關門過程中修正的問題

驗證期間發現 3 項,2 項已修並重新驗證,1 項判定為誤報後收回。

  1. Session 頁的座位標籤會被開場快照永久凍住(已修,351a07f)。 P3-A 把 getActiveSession 從 heartbeat 移除以符合 開發設計方針 §14「Lobby heartbeat 與 Session event feed 分離」,但 mergeSessionSeatTruth(lobbySeats, resumeSeats) 是後者覆蓋前者。改動後 resumeSeats 只在 mount / explicit reload 更新,lobby.seats 仍隨 heartbeat 更新,於是掛載當下的 Resume 快照會永久壓住之後刷新的 Lobby 資料——Session 進行中改座位 label,已在畫面上的人要重新整理才看得到。已反轉合併優先序:以 Resume seats 建 base、Lobby seats 覆蓋。該函式原本的用途是聯集(Lobby 不再列出的已封存座位由 Resume 補 label),此行為與對應斷言原封保留。

  2. event long-poll 遇到任何錯誤就永久終止(已修,b8a11a7)。 原迴圈的 catchAbortError 外一律 setErrorreturn,不再重啟。任何暫時性失敗(Wi-Fi 換手、行動裝置喚醒、server 短暫重啟、單次 5xx)都會讓該分頁永久停止接收桌上 event,而錯誤 banner 會被後續狀態更新蓋掉,使用者無從察覺,只能重新整理。P3-A 尚無 in-session 玩法面,可觀察影響有限,但 P3-B 交付 Chat 後即為「別人發言收不到且畫面沒有異常提示」。已抽出 sessionEventPoll.ts:1 秒起、每次翻倍、上限 30 秒的 backoff,成功後歸零;SessionApiError 403 / 404 判定為致命並終止;AbortError 與 effect cleanup 為正常終止且清掉 timer;重連沿用同一 cursor,不退回完整 Resume。連線狀態改為常駐 banner(data-session-event-connection),eventReconnecting / eventDisconnected 兩種 locale 同步交付。

  3. 「DM 會因 access session 輪替而失去控制項」判定為誤報。 初次 review 認為 callerAccessSessionId 不再隨 heartbeat 更新會讓 isCurrentDm 變 stale。追查 RoomAccessService 後確認:access session 由 localStorage 中的 token hash 解析,heartbeat 只做 touch_access_session(),不 rotate、不重建;被撤銷時走 RoomAccessRevokedError 顯性報錯而非靜默改變。該 id 在頁面存活期間為常數,不需隨 heartbeat 更新。

Boundary

已知限制

Handoff

P3-A 已完成並關門。下一步是 P3-B — Exploration, Chat & Actions

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

其他必須同步處理的事項: