adventure-table

P2 — 測試指南

Phase:P2 — Room / Campaign / Session / Seat
本文件定義 P2-A~P2-F 的自動/人工驗收與 closeout evidence。產品完成條件以 實作規格.md 為準;具體架構與 migration / API contract以 開發設計方針.md 為準。

最後更新:2026-09-06


1. P2 Subphase 順序

三份 P2 文件固定使用:

P2-A — Room Foundation & Web Entry
P2-B — Room Character Workspace
P2-C — Campaign & Party Roster
P2-D — Seat, Controller & Lobby
P2-E — Session Lifecycle & Late Join
P2-F — Full P2 Integration & Closeout

每個 Subphase 的測試新增要跟該段 code一起完成;不要把「Room scope / permission / migration」全部留到 P2-F才第一次驗。


2. P2 測試核心原則

P2 從單機 Character 系統進入多人 namespace,最危險的 regression不只是功能壞掉,而是:

因此 P2 的 minimum gate不是「頁面打得開」,而是:

Scope isolation
+ persistence atomicity
+ migration correctness
+ concurrency invariant
+ controller authority
+ standalone independence
+ existing Web regression continuity
+ real PostgreSQL evidence
+ real-backend UX

3. 執行環境與指令原則

Backend

Windows / PowerShell 下不要使用 && 串 command。Backend Python 指令若依賴 alembic.ini / relative path,cwd在 apps/server,interpreter使用 repo root .venv

例如從 repo root:

Set-Location .\apps\server
..\..\.venv\Scripts\python.exe -m pytest

Focused test同理:

Set-Location .\apps\server
..\..\.venv\Scripts\python.exe -m pytest tests\test_p2a_rooms.py -q

PostgreSQL focused tests

P2-A 起 PostgreSQL-specific gate使用統一 env var:

P2_POSTGRES_URL

CI的 P2 Non-E2E workflow提供乾淨 PostgreSQL service並同時設定:

P2_POSTGRES_URL=<test db url>
DATABASE_URL=$P2_POSTGRES_URL

本機可從 repo root建立 dedicated test DB;PowerShell 5.1 不使用 &&

docker compose up -d db
docker compose exec -T db psql -U adventure -d postgres -c "DROP DATABASE IF EXISTS adventure_table_p2_test;"
docker compose exec -T db psql -U adventure -d postgres -c "CREATE DATABASE adventure_table_p2_test;"
Set-Location .\apps\server
$env:P2_POSTGRES_URL = "postgresql+psycopg://adventure:adventure@localhost:5432/adventure_table_p2_test"
$env:DATABASE_URL = $env:P2_POSTGRES_URL
..\..\.venv\Scripts\python.exe -m pytest <P2 PostgreSQL focused tests> -q

需要 clean DB語意的 focused test不得假設開發用 adventure_table可隨便 drop;一律使用 dedicated adventure_table_p2_test或 CI fresh service。

Frontend unit / build

Set-Location .\apps\web
npm test -- --run
npm run build

E2E

KI-ENV-001,完整 Playwright一律走 Docker/Linux web service:

Set-Location .\apps\web
npm run test:e2e:docker

不要在 Windows 上改用 Playwright-managed Vite來「暫時過測試」。

Actions

P2-A 必須建立專屬:

.github/workflows/p2-non-e2e.yml
Display name: P2 Non-E2E

它是 P2 PostgreSQL migration / persistence / concurrency non-E2E evidence的正式 workflow。P2-B~E持續在同 workflow擴充各自 focused tests。

GitHub Actions只代表其 workflow實際覆蓋的 gate;不得因 M03 standalone workflow或舊 M01 workflow綠燈就宣稱 P2 backend / PostgreSQL / permission / E2E全綠。

每個 closeout要記錄:


4. Test layers

Layer A — Domain / pure policy

驗:

Layer B — Persistence / migration

驗:

Layer C — API integration

驗:

Layer D — Concurrency

P2 首次有真正多人 race condition,需要明確測:

不能只用 sequential API test推論 concurrency安全。

Layer E — Frontend unit / routing

驗:

Layer F — Real-backend Playwright

驗真正 browser + FastAPI + PostgreSQL:

Room
→ Character
→ Campaign
→ Lobby
→ Session

以及多 Room隔離、reload、restart後仍成立。

Layer F 包含既有 Web Playwright regression suite,不只新增 P2 journey。 P2-A/P2-B改 route時,既有 P1/M01/M02 spec必須透過 Room-aware helper繼續跑 Web channel。

Layer G — Standalone frozen regression

至少:

Standalone gate是獨立保護,不是既有 Web regression的替代執行環境。


5. Alembic branch gate(P2-A 起永久)

5.1 Graph shape

Automated test直接檢 Alembic ScriptDirectory:

不要只 grep檔名。

5.2 Cross-branch dependency

建立 migration graph fixture / structural assertion:

5.3 Fresh Web PostgreSQL

Empty PostgreSQL:

alembic upgrade heads

驗:

此 gate至少在 P2 Non-E2E的 PostgreSQL service執行。

5.4 Legacy Web upgrade

建立 fixture DB停在:

0008_m03c_import_records

seed:

再:

upgrade heads

驗既有 payload byte/semantic等價;P2-B再驗 Room claim workflow。

5.5 Standalone SQLite

Fresh SQLite與 legacy M03 SQLite都跑:

upgrade character@head

驗:

5.6 Existing migration / launcher / smoke rewiring gate

P2-A 不只掃 operational command,也必須改既有 pytest、frozen helper與 standalone smoke的 single-head contract。

test_m03c_migration.py

test_m03d_migration_sqlite.py

test_m03d_schema_parity.py

scripts/smoke_standalone.py

test_m03e_smoke_script.py

test_m03e_alembic_bundle_ready.py

test_m03e_launcher_headless.py

Static scan範圍至少包含:

apps/server/tests/**
scripts/**
.github/workflows/**
active dev / release helpers

判準是任何 repo-global single-head 假設,不只是字面 "head" target。至少掃:

upgrade(..., "head") / alembic upgrade head
get_current_head()
len(heads) == 1 / expected one Alembic head
自製 AST / filename 唯一-head inference
Web assertion把 alembic_version 永遠讀成單列 scalar

逐一分類成 Web heads / revision set、Standalone character@head / Character revision,或明確的歷史文件文字。歷史 closeout文件文字不算 active contract。

5.7 Character schema parity allowlist gate

Parity test至少做兩次等價情境:

  1. 只 import neutral Character persistence後執行。
  2. 先 import P2 Room tables,再執行同一 parity helper。

兩者都必須:

這直接鎖死 import-order flake。

5.8 Actual command / assumption wiring gate

P2-A code review / automated static test檢查 active operational paths:

目前已知 P2-A branch split必須檢查的舊 workflow至少:

p0a-foundation.yml
m01f-non-e2e.yml
m01g-non-e2e.yml
m01h-non-e2e.yml
m01i-non-e2e.yml
m01j-non-e2e.yml
m03a-non-e2e.yml
m03b-non-e2e.yml

若這些 workflow在目前 main仍執行 migration,就不能因名稱屬舊 Phase而保留 ambiguous upgrade head

歷史 closeout / archive文件可以保留當時的 head證據,不列為 stale operational command。

5.9 Wrong target negative test

加入測試證明如果 standalone誤跑 heads,test會偵測 multiplayer tables;launcher test則鎖死實際 target為 character@head

目的不是故意讓 production做錯,而是確保 gate能抓到未來 regression。


6. P2-A — Room Foundation & Web Entry

6.0 第一個 implementation checkpoint:封存 M03 unstable fixture

任何 v1 exporter / parser code修改之前,先建立並 commit:

apps/server/tests/fixtures/character_export_m03_unstable.json

產生規則:

6.1 Backend focused tests

Room create / code

Password contract / throttle

Enter Room

Secret rotation

如果 P2-A實作 rotate:

6.2 Heartbeat contract

P2-A直接交付 heartbeat endpoint,因此本 Subphase就測:

POST /api/rooms/{room_id}/access/heartbeat

P2-D再測 last_seen_at到 Connected/Offline的 projection;不重做第二套 heartbeat。

6.3 Capability tests

apps/server/tests/test_m03e_capabilities.py 是 P2-A 必改 regression,不是新增 P2 test取代它。

Web:

channel=web
room=true

Standalone:

channel=standalone
room=false
campaign=false
seat=false
session=false

要求:

6.4 Web landing / incremental compatibility

P2-A Web capability:

Standalone capability:

P2-A 為避免單一 Subphase讓既有 Web Character功能失效,direct legacy /characters / Character APIs 可以暫時存在;但要有 test證明首頁不導流,也要列成 P2-B 必須清掉的 transitional surface。

P2-B closeout後再把 direct Web global route改成 room-required / redirect negative gate。

不要靠 build-time separate SPA測;同一 frontend build切 capability。

6.5 Character JSON v1 tests

legacy fixture先存在

在修改 exporter前,先 assert §6.0 committed fixture:

v1 export

assert:

schema_version == "1"
schema_status == "locked"
export_type == "character"

以及不含:

room_id / room_code / campaign_id / seat_id / session_id

legacy M03 import

使用§6.0真正封存的 fixture:

legacy unstable
→ normalize
→ preview
→ import
→ export

最後輸出 v1,Character semantics不變。

v1 roundtrip

v1 export → import as new identity → export;除了 source identity / timestamps等預期變化,Build / State / Version chain semantics等價。

6.6 Import boundary

test_m03_import_boundary.py

6.7 Existing Web Playwright Room bootstrap gate

P2-A 必須先為既有 Web E2E建立共用 Room bootstrap / navigation seam;不要等 P2-B關掉 /characters才一次撞完整 suite。

建議 contract(檔名可按現有 style):

enterRoom(page, options?)
openCharacterWorkshop(page, roomContext)

驗收原則:

  1. 測 Character / Builder / M01/M02 rules content 的既有 specs從 P2-A 起不再各自 hardcode page.goto('/characters');先透過 enterRoom()取得 isolated Room context,再由 openCharacterWorkshop()進 Workshop。
  2. P2-A transitional window中,openCharacterWorkshop()可以先導向既有 /characters;因為 P2-B Room-scoped Character API尚未完成。這不是 bypass,而是讓 route transition集中在 helper。
  3. P2-B只改 shared helper / route context一次到 /rooms/{roomId}/characters,不逐一再改二十多個 spec。
  4. 真正測首頁本身的 specs(例如 app shell / localization landing)直接更新對 / 的期待,改驗 Create / Enter Room與 Room-first雙語 copy;不能用 helper跳過首頁後宣稱 landing有測。
  5. 每個 spec / worker使用獨立 Room或 deterministic reset,避免平行 suite共享 mutable Room。
  6. 既有 suite仍是 Web channel regression;不得整批改用 standalone capability、不得因 Room-first migration大量 skip/fixme

P2-A closeout至少要證明:完整既有 Playwright suite在新的 Room-first首頁存在後仍可執行;P2-B closeout再證明同一 suite在 global Web /characters收口後仍全程走 Room-scoped Web path。


7. P2-B — Room Character Workspace

7.1 Association constraints

Persistence test:

7.2 Create Draft atomicity

Fault-injection test:

create neutral draft
→ insert room_builder_drafts故意 fail

整個 transaction rollback;不能留下 unscoped Draft。

反向 fault也要測:association不能存在但 Draft不存在。

7.3 Confirm atomicity

Room Draft Confirm:

全部在一 transaction。

在 link insert前注入 failure → Character / Version / State / confirmed marker全部 rollback。

Repeated Confirm:

7.4 Versioned Draft inheritance

Room A Character:

API沒有 target Room欄位可把它搬到 B。

7.5 Cross-Room API matrix

用 Room A token + Room B resource id逐項驗:

每條都必須 fail,不得只測 GET。

7.6 Global Web route negative gate

P2-B後 Web app:

Standalone對應 endpoints仍 green。

7.7 Legacy claim

至少兩種 upgrade fixture;Primary Case 是 P2-A 已被使用、Room 已存在,zero-Room auto-claim只保留 bootstrap edge coverage。

Primary Case — P2-A 已建立 Room

Bootstrap Edge Case — P2-B 時沒有 Room

7.8 Import target Room

Web Room A import:

7.9 Archive / permanent delete authority

7.10 Room Hard Delete

seed:

Hard Delete Room後 query所有 P2 association / Character core tables,沒有屬於該 Room的 orphan。

其他 Room資料完全不動。

7.11 Existing Web backend Character regression migration

P2-B 收掉 app.main 的 global Character routers時,既有 Web HTTP regression必須改成 Room-scoped,不改綁 standalone app來逃避 authorization / route migration

目前至少覆蓋:

apps/server/tests/test_character_api.py
apps/server/tests/test_character_archive.py
apps/server/tests/test_character_builder_api.py
apps/server/tests/test_character_workshop_api.py
apps/server/tests/test_m02d_workshop_presentation.py
apps/server/tests/test_p1f_character_creation.py
apps/server/tests/test_p1g_legacy_adapter.py
apps/server/tests/m01k_support.py

驗收:


8. P2-C — Campaign & Party Roster

8.1 Campaign lifecycle authority

Table-driven:

8.2 Roster

DM / Owner:

Member:所有 Roster mutation deny。

8.3 Same Character / Multi-Campaign State

建立:

Room A
├─ Campaign A roster → Mira
└─ Campaign B roster → Mira

在 Campaign A context修改 Mira Current HP後,在 Campaign B context讀同一 Character Sheet:看到相同 Current HP。

再改 Inventory / prepared spell / resource至少各一個代表,證明不是只碰 HP巧合。

DB assert:沒有第二份 campaign state payload。

8.4 Character archive interaction


9. P2-D — Seat, Controller & Lobby

9.1 Seat validation

9.2 Seat history lifecycle

先建立未使用 Seat:

再建立 ended Session reference該 Seat:

9.3 Human一人多角

同一 RoomAccessSession:

Seat 1 → Mira
Seat 2 → Luna

合法。

反向:

Seat 1 → Mira + Luna

資料模型做不到/API拒絕。

9.4 Controller shape

9.5 Owner-controlled DM assignment

建立 Owner、DM A、DM B access sessions:

這裡鎖的是Start前 assignment;Session開始後由P2-E current DM Controller snapshot鎖死。

9.6 Presence

Fake clock測 P2-A heartbeat projection:

不要用 sleep造成 flaky test。

9.7 Permission matrix

至少 table-driven測:

member / dm / owner
×
room settings / campaign lifecycle / roster / player-seat / dm-seat-assignment / character workspace

鎖定:

每個 deny都測 server response,不只檢 frontend button hidden。


10. P2-E — Session Lifecycle & Late Join

10.1 Start Session happy path

Seed:

Owner-assigned DM Seat Human controller Start:

10.2 Start authority negative matrix

同一 Lobby:

Start成功後固定為 current DM Controller。

10.3 不強制 Ready / Online

10.4 DM fixed

10.5 Active Character freeze

Session active後:

10.6 Lease concurrency

這是 P2 必須有的真正 concurrent PostgreSQL DB test。

兩個 transaction同時:

Session A start → Mira
Session B start → Mira

只能一個 commit成功。

失敗側:

至少在 P2 Non-E2E PostgreSQL service執行;不要只在 SQLite mock concurrency,也不要用 M03 workflow綠燈代替。

10.7 Same Campaign second active Session

同一 Campaign已有 active Session時第二次 Start拒絕,即使選不同 Characters。

Ended / abandoned後才可開下一 Session。

10.8 Late Join authority / validation

Active Session:

10.9 End Session state preservation

Start前 snapshot一個 rich Character State:

Session內先做合法 state改動;記住 end前 exact state。

current DM Controller End Session後:

state_after_end == state_immediately_before_end

不能只 assert HP。

Leases已釋放;Session participant/history與non-null historical Seat references仍存在。

10.10 Abandon

Owner或 current DM Controller abandon:

10.11 Restart persistence

Server restart後:


11. Web live Character write-scope tests

P2-B/E 完成後,特別驗現有 Character State API不被 Room wrapper漏掉。

No active Session

Room member可依 Room協作規則使用 Room Character Workshop / Sheet。

Active Session

Mira已 lease給 Seat A / Human A:

Borin若未在 active Session:依 Room一般協作規則處理。

Versioned Builder對 active Character也做同樣 Seat/current-DM scope guard,避免一般 member、另一 DM或 Owner用 Build Edit繞過 active table控制權。


12. Frontend unit / component test matrix

Routing

P2-B後 Web:

/ → Room landing
/characters → room-required / redirect,不是 global list
/rooms/A/characters → A Workshop
/rooms/A/characters/Mira → A Sheet

Standalone:

/ → Character landing
/characters → Workshop
/rooms/A → capability-disabled

Shared Web E2E Room helper

P2-A起為 shared helper本身加 focused coverage:

Room context retention

從:

Room A Workshop
→ Builder
→ Review
→ Confirm
→ Sheet
→ Version History
→ Back

每一步 return link仍在 Room A。

Cross-room stale frontend state

先進 Room A,再直接 navigation Room B:cache / query key必須包含 Room scope,不可把 A Character list短暫顯示在 B。

Legacy data migration card

legacy_unscoped_character_count / draft_count > 0

Heartbeat lifecycle

Locale

P2新頁面兩語各跑:

assert沒有 raw translation key。


13. P2 Real-Backend E2E journeys

P2-F前至少有下列 browser journeys。這些是新增 P2 journeys,不取代既有 P1/M01/M02 Web specs。

Journey 1 — Room-first Character creation

Homepage
→ Create Room A
→ Room A
→ Characters
→ New Character
→ save partial Draft
→ reload browser
→ Draft still exists in A
→ complete / Confirm
→ Sheet
→ Version History
→ back to Room A Workshop

Journey 2 — Multi-Room isolation

Create Room A + Mira
Create Room B + Borin

A token / A UI
→ never lists Borin
→ manually navigate B character UUID under A route
→ not found / denied

再反向 B→A。

Journey 3 — Standalone-style Character JSON into Web Room

用 locked v1 fixture或真正封存的 M03 legacy fixture:

Room B
→ Import
→ Preview
→ Confirm
→ new Character appears only in Room B
→ Export
→ v1

Journey 4 — Campaign / Roster

Room A
→ Owner creates Campaign Alpha
→ DM/Owner adds Mira + Luna
→ Mira active / Luna inactive
→ Owner selects active Campaign

Journey 5 — Lobby / Session

Campaign Alpha
→ Owner assigns DM A to DM Seat
→ Player Seat Mira
→ Player Seat Luna
→ assigned DM A Start Session
→ Session page
→ same DM A End Session
→ Character states preserved

Journey 6 — Late Join

Start with Mira only
→ Session active
→ current DM Late Join Luna
→ Luna appears
→ reload
→ assignments remain

Journey 7 — Cross-Campaign same Character

Campaign Alpha roster Mira
Campaign Beta roster Mira
→ change Mira state in Alpha context
→ open Beta context
→ same state

Journey 8 — Active collision

可主要由 backend PostgreSQL concurrency證明;browser至少顯示可理解的 conflict文案,不顯 raw 409 / raw code。

Journey 9 — Room Hard Delete

Room with Character / Draft / Campaign / ended Session
→ Owner Room Settings
→ strong confirmation
→ delete
→ Room code no longer enterable

E2E不需要直接查 DB;backend integration另外做 orphan assert。


14. Standalone regression matrix(每個會碰 shared boundary 的 P2 Subphase)

Static

Runtime app

Standalone capability:

character_builder=true
character_import_export=true
room=false
campaign=false
seat=false
session=false

SQLite schema

Character parity使用explicit allowlist;不得存在:

rooms
room_access_sessions
room_characters
room_builder_drafts
campaigns
campaign_roster_entries
campaign_seats
sessions
session_participants
active_character_session_leases

Character workflow

至少:

Create → Confirm → Sheet
Level Up → Confirm → Version History
Export → Import → Sheet

Frozen build

P2-A(migration split)、P2-B(shared Character API / persistence refactor)、P2-F一定跑 Windows standalone build / smoke。

P2-A frozen smoke必須真的通過 branch-aware scripts/smoke_standalone.py;不能只因 build產出 exe就算 standalone gate green。

P2-C/D/E若只新增 app.*.rooms而未碰 shared/standalone graph,可依 diff使用 import-boundary + existing standalone unit gate;P2-F仍完整重跑。


15. Security / privacy negative matrix

P2 沒有 DM secrets world data yet,但 namespace與access credential已是 security boundary。

至少:

  1. Room A token + Room B id。
  2. revoked token。
  3. random token。
  4. Room password failed-attempt throttle / 429。
  5. member呼叫 Owner action。
  6. DM呼叫 Owner-only Campaign lifecycle action。
  7. DM Key holder嘗試 self-assign DM Seat。
  8. member改別人 active Seat Character state。
  9. non-current-DM End Session。
  10. Owner但非 current DM Controller Start / Late Join / End Session。
  11. 另一 DM key holder但非 current DM Controller寫本場 Character。
  12. DM試圖 Hard Delete Room。
  13. URL UUID guessing。

Response不得洩漏:

Room code本身是public locator;不把「能判斷code是否存在」當秘密洩漏,但沒有正確Password仍不可取得Room data。


16. Static review checklist by Subphase

P2-A review

P2-B review

P2-C review

P2-D review

P2-E review

P2-F review

重跑 開發設計方針.md §18 全 checklist並逐項附 evidence。


17. Human smoke

P2 人工 smoke只驗 automation難以判斷的 UX,不人工重做全部 permission matrix。

建議代表流程:

  1. Web首頁確認沒有 Character Workshop,Create Room / Enter Room清楚。
  2. 建 Room A,確認10字元Room code可讀;Owner / DM credential presentation容易理解、不誤把 key當一般 password。
  3. 若存在 legacy unscoped Character / Draft,Owner在 Room A Characters 頁可看到 migration card、確認 target Room與數量;一般 member不看到此入口。
  4. Room A → Characters,創一隻角色,刻意中途離開再回來,Draft位置清楚。
  5. Confirm後 Sheet / Version History 的 Back不會掉出 Room。
  6. 建 Room B,視覺上不會看到 Room A Characters / Drafts。
  7. Room B import一隻 JSON角色,確認「這是 B 的 new copy」容易理解。
  8. Owner建 Campaign;DM管理 Roster / Lobby,但DM assignment由Owner操作,authority差異不讓人困惑。
  9. Lobby建立 DM + 2 Player Seats;一個 browser identity控制兩 Seat的 UX仍可理解。
  10. 由Owner-assigned DM Seat Controller Start Session、late join、End Session;不強迫 Ready。
  11. Owner但不是 current DM時,UI不應讓人誤以為可以接管/End;Abandon入口需清楚是重手段。
  12. End後回 Character Sheet確認 HP / resources沒有莫名 reset。
  13. Standalone啟動確認仍直接進 Character Workshop,看不到 Room / Campaign字樣。
  14. zh-TW / en 各巡一次 P2主要頁面,檢查 overflow / raw key。

若 human smoke發現 UX confusion但不影響資料 correctness,可在 P2-F closeout列為 polish;若容易讓使用者誤刪 Room、進錯 Room、把 legacy data claim 到錯 Room、以為 Character跨 Room共用、誤以為 DM Key可自行取得本場DM assignment、或誤以為 Owner可接管 active Session,視為 blocker。


18. P2-F final regression

P2-F 不只跑 test_p2*.py

Backend

Set-Location .\apps\server
..\..\.venv\Scripts\python.exe -m pytest

要求:所有 backend tests green;已知 intentional skip需有 已知問題.md對應。

特別確認既有 Web Character / Builder HTTP tests不是藉由改綁 standalone才變綠;P2-B後它們應透過 Room-aware fixture繼續驗 app.main

P2 PostgreSQL non-E2E

在 exact final SHA執行 P2 Non-E2E,至少包含:

記錄 workflow run id;別的workflow不能替代。

Frontend unit / build

Set-Location .\apps\web
npm test -- --run
npm run build

E2E

Set-Location .\apps\web
npm run test:e2e:docker

這裡要求的是完整既有 Web suite + 新 P2 journeys。Room-first route migration不得靠把舊 specs搬去 standalone、刪檔、或新增沒有已知問題依據的 skip來取得綠燈。

若 KI-M01J-001仍是 intentional fixme,不得把它算成 P2功能通過證據;只照 已知問題.md記錄。

Standalone

依當時 M03 release contract跑:

Migration

Restart

用含:

的 dataset重啟 server,再跑核心讀寫。


19. P2 closeout evidence

P2-F closeout至少留下可追溯文件,記錄:

Branch / final SHA
Alembic character head / web head(s)
Backend pytest result
P2 Non-E2E workflow + run id + PostgreSQL result
Frontend unit result
TypeScript / build result
Full Web Playwright result (existing suite + P2 journeys)
Standalone import-boundary result
Standalone frozen build / branch-aware smoke result
Fresh + legacy migration result
Character schema parity allowlist result
Single-head assumption scan result
Existing Web regression migration result
Concurrency lease test result
Authorization matrix result
Human smoke result
Known skips / known issues

Closeout不能只寫:

CI green

要能回答「哪個 CI / 哪個 commit / 哪些 test / PostgreSQL到底有沒有跑 / standalone到底有沒有跑到 Room schema / 既有 Web regression是否還真的在 Web 跑」。


20. P2 後永久保留的 regression

P2 關門後以下不再是「只有 P2才跑」:

  1. Multi-Room isolation。
  2. Room Character / Draft scope。
  3. Roster same-Room invariant。
  4. same Character multi-Campaign state sharing。
  5. active Session Character lease collision。
  6. Owner-controlled DM assignment + fixed DM Controller / Owner不能 takeover。
  7. Session End state preservation。
  8. Seat history archive / non-null historical FK。
  9. Character JSON v1 backward import + immutable M03 unstable fixture。
  10. standalone no-multiplayer import graph。
  11. standalone character-only migration branch / SQLite schema allowlist。
  12. Alembic dependency direction Web → Character only。
  13. existing migration / launcher / smoke tests保持multi-head-aware,不回退到任何 repo-global single-head假設。
  14. Room access throttle / heartbeat authorization boundary。
  15. Legacy unscoped data primary claim UX保持 Owner-only、target-Room scoped,zero-Room auto-claim只作 bootstrap fallback。
  16. 既有 P1/M01/M02 Character browser/API regression持續在 Web Room-scoped channel執行;Standalone regression不得取代 Web coverage。
  17. Campaign/Seat/Session FK delete semantics保持:Seat屬Campaign可cascade、Session history對Campaign/Seat採restrict,Room Hard Delete走explicit order。

後續 M01如果修改 Character shared core、P3/P4如果修改 Seat/Session consumer、P7如果加入 export/snapshot,都必須把直接受影響的上述 regression帶著走。