adventure-table

U01-A — E2E Database Isolation & Fast Test Foundation

狀態:✅ 已關門(2026-09-14;同機 after wall-clock 已取得、無倒退,見 §14.7)

2026-09-14 實作註記:§2~§13 保留開工時的問題描述與契約語氣,實際完成證據集中記錄於 §14。

1. U01 定位

U01Test / Development Efficiency Optimization 長期優化軌,用來持續收斂測試速度、開發迴圈成本、CI 效率與相關可靠性問題。

U 類不是正常產品 Phase,也不是 Maintenance / Content Phase:

本文件只定義 U01-A


2. 背景與問題

目前本機 Docker E2E 與日常實際使用共用同一個 PostgreSQL database:adventure_table

Playwright global setup(apps/web/scripts/e2e-global-setup.mjs)在每次完整 E2E 開始前,會對該 database 執行 destructive reset:

TRUNCATE TABLE rooms, characters, ai_oauth_clients RESTART IDENTITY CASCADE;

目前的放行條件是 requireDisposableDatabase()

ADVENTURE_TABLE_E2E_ALLOW_DESTRUCTIVE_RESET == 1
OR (CI == true AND GITHUB_ACTIONS == true)

這只是在執行前要求顯式 opt-in(CI 則自動放行),並沒有真正隔離 E2E 資料與實際使用資料。只要本機 E2E 指向平常使用中的 DB,測試就會清掉真實 Character / Room / Campaign / Session / AI grant / OAuth 等資料。

另外兩個現況會直接影響隔離設計:

同時,現有完整 E2E 已偏慢;這次隔離不能以「再啟一整套 PostgreSQL、再多一套昂貴初始化流程」換安全。後續 U01 還要持續把 E2E 執行時間往下壓。


3. U01-A 目標

U01-A 完成後必須同時滿足:

  1. 實際使用 DB 的資料不受 E2E 影響。
    • 日常資料庫維持 adventure_table
    • E2E 使用獨立 database adventure_table_e2e
    • E2E 的 truncate、seed、migration、測試資料建立與刪除只發生在 E2E database。
  2. 不額外建立第二個 PostgreSQL container。
    • 正式/日常 DB 與 E2E DB 共用同一個 PostgreSQL container / process。
    • 透過 PostgreSQL database boundary 隔離資料。
    • 不為 U01-A 增加第二套 PostgreSQL healthcheck、volume、image pull 或 container startup 成本。
  3. 隔離不得讓完整 E2E 固定成本變高。
    • 新增 E2E database 的建立/確認應是低成本操作。
    • 不能每次測試都因資料隔離而重新建立 PostgreSQL container 或 volume。
    • 若已有 E2E database,後續 run 應直接 reset + seed + test。
  4. destructive reset 必須有 database-level hard guard。
    • ADVENTURE_TABLE_E2E_ALLOW_DESTRUCTIVE_RESET=1 保留。
    • reset 前必須驗證目前 target database 是 adventure_table_e2e
    • current_database() 不是 adventure_table_e2e,立即 fail,絕不執行 truncate。
    • 不可只靠操作者記得切環境變數;CI 自動放行也必須通過同一個 database identity check
  5. 日常 server / web 在 E2E 期間不受影響。
    • 跑 E2E 不得重建、重啟或改變日常 server / web 容器的設定。
    • 日常網站(5173 / 8000)在整個 E2E 期間持續可用且仍連 adventure_table
  6. 為後續 E2E 加速鋪路。
    • U01-A 不要求一次解完所有 E2E 慢點。
    • 但新的隔離設計不得綁死「每次 run 必須 full rebuild」或其他不必要成本。
    • 後續可由 U01-B/U01-C 等項目處理 Docker rebuild、Playwright helper、重複 seed/fixture、suite partition 等速度問題。

4. 目標架構

採用 單一 PostgreSQL container + 兩個 PostgreSQL database + 獨立的 E2E server / web service pair

docker compose project
├── db(單一 PostgreSQL container,不變)
│   ├── adventure_table        ← 日常 / 實際使用資料
│   └── adventure_table_e2e    ← Playwright / destructive test data
│
├── server(日常,8000,DATABASE_URL → adventure_table,不變)
├── web(日常,5173,proxy → server,不變)
│
├── server-e2e(profile: e2e,8001,DATABASE_URL → adventure_table_e2e)
└── web-e2e(profile: e2e,5174,proxy → server-e2e)

4.1 日常使用

日常 serverDATABASE_URL 繼續指向:

postgresql+psycopg://...@db:5432/adventure_table

E2E 不得 truncate、drop、seed 或 migrate 這個 database 作為測試初始化手段。日常 server / web service 定義不因 U01-A 改變。

4.2 E2E

E2E backend 為 server-e2eDATABASE_URL 固定指向:

postgresql+psycopg://...@db:5432/adventure_table_e2e

完整 E2E 的資料生命週期為:

docker compose up -d db
→ ensure adventure_table_e2e exists(缺才 CREATE DATABASE)
→ docker compose --profile e2e up -d --build server-e2e web-e2e
   (server-e2e 啟動命令內的 alembic upgrade heads 即 E2E DB migration)
→ Playwright global setup:
     verify current_database() == adventure_table_e2e
     → destructive reset E2E DB
     → deterministic seed(server-e2e)/ baseline Room(8001)
→ Playwright(baseURL 5174,API 8001)
→ xge-less 第二輪:只重啟 server-e2e / web-e2e

下一次 run 不需要重建 PostgreSQL;直接重用同一個 E2E database,再 reset 即可。


5. Server / Web 隔離:必須是獨立 service pair

U01-A 的核心目標是資料安全,因此 DB 必須隔離;而 DB 隔離在現行 compose 結構下蘊含 server 隔離

理由:E2E backend 的 DATABASE_URL 是容器 env。若沿用日常 server 容器,唯一能讓它指向 adventure_table_e2e 的方法是換 env 重啟,這會讓日常網站在整個 E2E 期間連到 E2E DB,跑完還要再重啟一次還原——直接違反 §3.5。因此 U01-A 明確要求

  1. compose 新增 server-e2eweb-e2e 兩個 service,掛在 profiles: [e2e] 下,日常 docker compose up 不會啟動它們。
  2. server-e2e 使用與 server 相同的 build: context 與 Dockerfile;第二次 build 全部命中 layer cache,不構成額外 image 成本。web-e2e 同理。
  3. host port 固定:server-e2e 8001、web-e2e 5174。web-e2eVITE_API_PROXY_TARGET 指向 http://server-e2e:8000
  4. e2e-docker.mjs 只操作 dbserver-e2eweb-e2e不得再出現 up server web。xge-less 第二輪的 ADVENTURE_TABLE_ENABLED_CONTENT_PACKS 子集只套在 server-e2e
  5. 不建立第二套 Compose project,不複製 PostgreSQL stack。

附帶效果:現行 xge-less 第二輪會重啟日常 server 兩次的問題一併消失。


6. Destructive reset hard guard

6.1 雙重條件

U01-A 後 destructive reset 至少需要:

(ADVENTURE_TABLE_E2E_ALLOW_DESTRUCTIVE_RESET == 1 OR GitHub Actions CI)
AND
current_database() == 'adventure_table_e2e'

任何一項不成立都必須 fail closed。CI 自動放行只免除第一項的手動 opt-in,不免除第二項

6.2 檢查與 truncate 在同一個 transaction

database identity check 與 truncate 必須落在同一個 PostgreSQL execution path,避免「先檢查 A、實際 truncate 卻送到 B」的接線錯誤。做法是把檢查放進同一份 psql --single-transaction -v ON_ERROR_STOP=1 的 SQL 開頭:

DO $$
BEGIN
  IF current_database() <> 'adventure_table_e2e' THEN
    RAISE EXCEPTION 'refusing destructive E2E reset: current database is %', current_database();
  END IF;
END $$;

TRUNCATE TABLE rooms, characters, ai_oauth_clients RESTART IDENTITY CASCADE;
-- 既有 remaining_* count 查詢照舊

6.3 DB 名稱是常數,不提供 env override

adventure_table_e2e 在 reset script 內是常數。不得用環境變數讓 guard 比對另一個名字——一旦可以換名字,guard 就退化回「靠操作者記得設對」。驗收 C 需要的「刻意打錯 DB」由 script 的 --database 參數提供(它只改 psql 連線目標,不改 guard 比對值)。

6.4 抽成獨立 script

reset 抽成 apps/web/scripts/e2e-reset-db.mjs,global setup 呼叫它。這樣:


7. Migration / initialization

E2E database 必須使用與 web runtime 相同的 Alembic web migration heads,不允許用 metadata.create_all() 取代正式 migration chain 作為主要 E2E schema 初始化方式。

server-e2e 沿用 server 的啟動命令 alembic upgrade heads && uvicorn ...,帶自己的 DATABASE_URL,因此 migration 不需要額外步驟:容器起來即 heads。腳本只需保證 database 在 server-e2e 啟動前存在。

ensure database

e2e-docker.mjs 內、up -d db 之後:

docker compose exec -T db psql -U adventure -d postgres -tAc \
  "SELECT 1 FROM pg_database WHERE datname = 'adventure_table_e2e'"
→ 輸出為空 → docker compose exec -T db createdb -U adventure adventure_table_e2e

CREATE DATABASE 只在缺少 E2E database 時發生,不是每次 E2E 的固定成本。

不用 postgres image 的 /docker-entrypoint-initdb.d:它只在 volume 第一次初始化時執行,本機既有 postgres_data volume 不會補建 E2E DB。

兩種情況

第一次:adventure_table_e2e 不存在 → createdb → server-e2e 啟動即 upgrade heads → tests
後續:  adventure_table_e2e 已存在 → server-e2e 啟動即 upgrade heads → reset → tests

8. 效能約束

U01-A 不是單純「安全優先,速度以後再說」。隔離方案本身就必須避免製造新的慢點。

必須避免

應維持

使用者入口仍應保持單一命令:

npm run test:e2e:docker

該命令自行確保 db 已起、E2E DB 存在、server-e2e / web-e2e 已重建且 schema 已更新、target 正確、資料已 reset。

使用者不應在每次 run 前手動切 DB、手動 create database、手動 migrate 或手動 restore 日常資料。

Service 名稱與連線常數單一來源

server-e2eweb-e2eadventure_table_e2e、8001、5174 這組常數由 e2e-docker.mjse2e-reset-db.mjse2e-global-setup.mjs 共用一份定義(例如 apps/web/scripts/e2e-env.mjs),不得各自硬編。global setup 的 seed(python -m app.scripts.seed_p0_fighter_wizard)與 baseline Room 建立必須改打 server-e2e / 8001。


9. CI 同步

.github/workflows/p3-e2e.yml 目前是 docker compose up -d --build(無 profile)後直接 npm run test:e2e 打 5173 / 8000。加上 §6 的 guard 後,這條路徑的 reset 目標是 adventure_table必定 fail。因此 U01-A 範圍包含:

  1. p3-e2e.yml 改走 npm run test:e2e:docker(同一條單一入口),或至少改成 --profile e2e + PLAYWRIGHT_BASE_URL=http://127.0.0.1:5174 + PLAYWRIGHT_API_BASE_URL=http://127.0.0.1:8001 + PLAYWRIGHT_MCP_URL=http://127.0.0.1:8001/mcp。推薦前者:本機與 CI 只維護一條路徑。
  2. 其他仍會執行 Playwright global setup 的 workflow_dispatch workflow(m01i-e2e.ymlm03b-e2e.ymlp0a-foundation.ymlp2-e2e.yml)擇一:同步改法,或在檔頭標註 deprecated 並說明已被 p3-e2e.yml 涵蓋。不得留下「dispatch 就會撞 guard」的 workflow 而不說明。
  3. failure log 步驟 docker compose logs 要加 --profile e2e,否則看不到 server-e2e / web-e2e 的 log。

順帶記錄(非 U01-A 範圍,應登記到 PROJECT_BRIEF.md 未結清事項):CI 目前只跑 npm run test:e2e沒有跑 xge-less 第二輪;M03-C 缺 pack 的 import 契約目前只有本機 test:e2e:docker 覆蓋。若 §9.1 採用推薦做法(CI 改跑 test:e2e:docker),這個缺口會一併補上,關門時要明寫是否已補。


10. U01-A 驗收

U01-A 至少需要以下證據:

A. 真實資料隔離

  1. adventure_table 建立可辨識 sentinel data(至少一個 Room、一隻 Character、一張 AI OAuth client)。
  2. 記錄其 identity / count。
  3. 執行完整 npm run test:e2e:docker(含 xge-less 第二輪)。
  4. 再查 adventure_table
  5. sentinel 與原資料必須仍存在且內容不變。
  6. 全程日常 5173 / 8000 持續可回應,且 server 容器的 StartedAt 未改變(docker inspect)。

B. E2E destructive reset 正常

  1. adventure_table_e2e 建立垃圾測試資料。
  2. 執行 E2E setup。
  3. 確認垃圾資料被 reset。
  4. baseline fixture / Room 正常重建。

C. Wrong-DB guard

e2e-reset-db.mjs --database adventure_table(帶 ADVENTURE_TABLE_E2E_ALLOW_DESTRUCTIVE_RESET=1)刻意打日常 DB:

另加 vitest 釘住:SQL 含 current_database() 檢查且位於 TRUNCATE 之前、guard 比對值為常數 adventure_table_e2e、環境變數無法改變該比對值。

D. Migration parity

E. 速度不倒退

開工前先量一次現行 npm run test:e2e:docker 的完整 wall-clock(含 rebuild 與第二輪),記錄在本檔 §13。U01-A 完成後同機器同條件再量一次。隔離機制本身不得造成可觀測的固定時間倒退;若同時做了其他速度改善,應分開記錄,避免把隔離成本藏在總體改善內。

F. CI

p3-e2e.yml 依 §9 修改後 dispatch 一次全綠,run id 記錄於 §13。


11. 非 U01-A 範圍

以下是 U01 長期可處理,但不要求塞進 U01-A:

這些項目可依量測結果新增為 U01-BU01-C…,不需要等 U01 整體關門。


12. 文件治理(開工同 commit 處理)

U 類是新的 Phase 類型,現行 AGENTS.mdPROJECT_BRIEF.md 都還不認識它。U01-A 開工的第一個 commit 必須同時:

  1. AGENTS.md「Phase / Subphase 設計原則」新增 U 類定義:Test / Development Efficiency 優化軌;單檔格式(不套三份文件制);長期 open;不進 P Roadmap 順序;每個 U Subphase 仍各自關門並附證據。
  2. PROJECT_BRIEF.md:Roadmap 表加 U01 一列;「Subphase 進度」加 ### U01 並一列一個 Subphase;U01-A 完成後把「E2E global setup 無條件清空 Character」那條未結清事項改寫為已解。
  3. 已知問題.md / README.md 中描述 E2E 需先備份資料的段落,在 U01-A 關門時同步更新。

13. 開工實作原則與順序

U01-A 實作時以「最小變更完成硬隔離」為目標,依下列順序,每步有可驗證的 check:

  1. 量 baseline:現行 npm run test:e2e:docker wall-clock → 記錄於此。
  2. compose:新增 server-e2e / web-e2e(profile e2e,共用 build,8001 / 5174)→ verify:docker compose --profile e2e config 通過;docker compose config(無 profile)不含兩個新 service。
  3. e2e-reset-db.mjs 含 §6 guard + vitest → verify:打 adventure_table 非零退出且資料不動;打 adventure_table_e2e 正常 truncate。
  4. e2e-docker.mjsup -d db → ensure database → --profile e2e up -d --build server-e2e web-e2e → baseURL 5174 / API 8001 → 第二輪只動 server-e2e / web-e2e → verify:日常 5173 / 8000 全程可用、server 容器未重啟。
  5. global setup 改呼叫 reset script,seed 與 baseline Room 打 server-e2e / 8001。
  6. CI 依 §9 同步 → verify:dispatch p3-e2e 全綠。
  7. 量 after:同條件再量一次,與步驟 1 對照。
  8. 文件治理 依 §12。

完成 U01-A 不代表 U01 關門;下一個優化項目依量測結果續編。

Baseline / after 記錄

日期 機器 條件 Playwright 主套件 wrapper 總 wall-clock 備註
2026-09-13 使用者 Windows 開發機 舊版 test:e2e:docker(daily server / webadventure_table 10.1m(127 tests / 1 worker) 未量 M01-O closeout,commit 56310b0該證據只有 Playwright 自報的主套件時間,不含 rebuild 與 xge-less 第二輪;開工時本表誤記為「含 rebuild 與第二輪」,2026-09-14 依原始證據更正口徑
2026-09-14 GitHub Actions ubuntu-24.04 新版 isolated test:e2e:docker,fresh CI,旁路 daily sentinel 16 分 15 秒 run 34800987506;只證明 CI correctness,不可與 Windows 數字比較
2026-09-14 同一台使用者 Windows 開發機 新版 isolated test:e2e:docker首次(createdb + server-e2e image 無 layer cache 全量 pip install) 10.0m(122 passed / 1 failed / 4 skipped) 647s(主套件失敗,未進第二輪) 失敗為 m01i flaky,見 §14.7 與 已知問題.md KI-P1D-001
2026-09-14 同一台使用者 Windows 開發機 新版 isolated test:e2e:docker,cache 已暖,含 rebuild 與 xge-less 第二輪 10.1m(123 passed / 4 skipped) 638s(10.6 分) §10.E after 值。 同口徑比較:主套件 10.1m → 10.1m,隔離機制無可觀測固定倒退;wrapper 總 wall-clock 只有 after 值,作為後續 U01 加速項目的 baseline

14. 2026-09-14 實作與驗收證據

14.1 已完成實作

14.2 CI acceptance — run 34800987506

驗收 commit:1b0be4d51aefbe55414edf49e32b147fb88438d0。GitHub Actions P3 Full-Stack E2E 最終 success

14.3 驗收 A — daily 資料與 service 不受 E2E 影響

CI 在 daily adventure_table 以正式 API / seed 建立 Room、固定 P0 Character、OAuth client sentinel,並記錄完整 row snapshot 與 daily server container StartedAt。完整主套件 + xge-less 第二輪後:

[u01-a] daily 8000/5173 remained reachable,
server StartedAt is unchanged,
and all sentinel rows are byte-for-byte unchanged

因此 A 的資料存在性、內容不變、5173/8000 可回應、daily server 未 restart 四項均已有實機證據。

14.4 驗收 B — E2E destructive reset / baseline recovery

Acceptance probe 在 adventure_table_e2e reset 前確認真實資料:

{"characters":3,"rooms":2,"oauthClients":1}

呼叫 canonical e2e-reset-db.mjs 後 Character / Room / OAuth client 均歸零,接著以正式 seed / API 重建 baseline,結果:

1 fixture Character, 1 baseline Room, 0 OAuth clients

14.5 驗收 C — Wrong-DB hard guard

--database adventure_table + destructive opt-in 刻意指向 daily DB,PostgreSQL 明確 non-zero fail:

ERROR: refusing destructive E2E reset: current database is adventure_table

失敗前後都再次驗證 daily StartedAt 與三筆 sentinel 完整 row snapshot不變。e2eResetDb.test.ts 另釘住 identity check 位於 TRUNCATE 前、--single-transactionON_ERROR_STOP=1、固定 guard database name 與 CI/local fuse。

14.6 驗收 D / F — migration parity 與 canonical CI

Fresh CI volume 第一次沒有 adventure_table_e2e;wrapper 成功建立 database,server-e2e 以既有 alembic upgrade heads && uvicorn ... 啟動後跑完整 127 tests,再進 xge-less 7 tests,全程不使用 metadata.create_all()。因此 fresh-create migration parity 已有證據;canonical CI 也已真正走同一支 wrapper 並全綠。

14.7 驗收 E — 同機 after wall-clock 與本機隔離複驗(2026-09-14 關門)

使用者 Windows 開發機、branch u01-a-e2e-isolation @ 09ef4d8,daily db / server / web 持續運行,本機 .env 帶著 M04-B 遺留的 ADVENTURE_TABLE_MCP_PUBLIC_ORIGIN(未清除)。

第一次 run(首次建立 adventure_table_e2e:wrapper 自動 createdbserver-e2e image 因 layer cache 未命中而全量 pip install;主套件 122 passed / 1 failed / 4 skipped (10.0m),wrapper 總 647s;失敗為 m01i-optional-features.spec.ts:389(Level Up 點擊後 5s 內未進入 /character-builder/),主套件失敗因此未進第二輪。該 spec 單獨以同一 wrapper 重跑 5 / 5 passed(該案 13.3s),第二次完整 run 亦通過,判定為 flaky,已登記於 已知問題.md KI-P1D-001「疑似同族」。

第二次 run(cache 已暖):主套件 123 passed / 4 skipped (10.1m);xge-less 第二輪 m03c-character-import.spec.ts 7 passed (6.2s);wrapper 總 638s(10.6 分),exit 0。

§10.E 結論:同口徑(Playwright 主套件)before 10.1m → after 10.1m,隔離機制無可觀測固定倒退。開工時 §13 把 M01-O 的 10.1m 誤記為「含 rebuild 與第二輪」,已更正;wrapper 總 wall-clock 沒有舊版 before 值,10.6 分自此作為後續 U01 加速項目的 baseline。

驗收 A 本機複驗:兩次完整 run 前後,adventure_table 的 rooms / characters / campaigns / sessions / ai_oauth_clients / session_events counts(1 / 2 / 3 / 1 / 0 / 1)與 characters 的 md5(id||name||updated_at) 完全相同;daily server / web container StartedAt 未變;8000 /ready 與 5173 全程 200。

驗收 C 本機複驗ADVENTURE_TABLE_E2E_ALLOW_DESTRUCTIVE_RESET=1 node scripts/e2e-reset-db.mjs --database postgresERROR: refusing destructive E2E reset: current database is postgres,non-zero exit;未帶 opt-in 時 fuse 先拒絕。本機未對 adventure_table 實打,該案以 CI §14.5 為證據。

附帶確認server-e2e 固定 ADVENTURE_TABLE_MCP_PUBLIC_ORIGIN: "" 後,M01-O closeout 記錄的 3 個 m04c-ai-join-kit.spec.ts 本機失敗不再發生,本機不需再手動清空該變數。

已知小項(不阻塞)e2e-reset-db.mjsspawnSync(..., { shell: true }) 帶參數會觸發 Node DEP0190 deprecation warning,且 --database 值直接拼進 shell;目前只供本機/CI 操作者使用,留給後續 U01 項目一併處理。

14.8 關門結論

§10 A~F 六項驗收均有證據(A / B / C / D / F 於 CI run 34800987506,A / C 本機複驗,E 本機同機比較)。U01-A 於 2026-09-14 關門;U01 整體維持 open,下一個優化項目依 §13 的 10.6 分 wrapper baseline 與 §8 列出的慢點續編。