狀態:✅ 已關門(2026-09-14;同機 after wall-clock 已取得、無倒退,見 §14.7)
2026-09-14 實作註記:§2~§13 保留開工時的問題描述與契約語氣,實際完成證據集中記錄於 §14。
U01 是 Test / Development Efficiency Optimization 長期優化軌,用來持續收斂測試速度、開發迴圈成本、CI 效率與相關可靠性問題。
U 類不是正常產品 Phase,也不是 Maintenance / Content Phase:
P0 → P1 → ... 產品 Roadmap。U01 本身長期保持 open,不設 Full Closeout。U01-A、U01-B、U01-C… 持續新增。AGENTS.md,見 §12。本文件只定義 U01-A。
目前本機 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 等資料。
另外兩個現況會直接影響隔離設計:
apps/web/scripts/e2e-docker.mjs 以 docker compose up -d --build server web 重建的是日常使用的 server / web 容器,E2E backend 的 DATABASE_URL 就是日常容器的 env;xge-less 第二輪也是重啟同一個日常 server。server 的啟動命令已是 alembic upgrade heads && uvicorn ...,migration 是容器啟動的一部分。同時,現有完整 E2E 已偏慢;這次隔離不能以「再啟一整套 PostgreSQL、再多一套昂貴初始化流程」換安全。後續 U01 還要持續把 E2E 執行時間往下壓。
U01-A 完成後必須同時滿足:
adventure_table。adventure_table_e2e。ADVENTURE_TABLE_E2E_ALLOW_DESTRUCTIVE_RESET=1 保留。adventure_table_e2e。current_database() 不是 adventure_table_e2e,立即 fail,絕不執行 truncate。server / web 容器的設定。adventure_table。採用 單一 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)
日常 server 的 DATABASE_URL 繼續指向:
postgresql+psycopg://...@db:5432/adventure_table
E2E 不得 truncate、drop、seed 或 migrate 這個 database 作為測試初始化手段。日常 server / web service 定義不因 U01-A 改變。
E2E backend 為 server-e2e,DATABASE_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 即可。
U01-A 的核心目標是資料安全,因此 DB 必須隔離;而 DB 隔離在現行 compose 結構下蘊含 server 隔離。
理由:E2E backend 的 DATABASE_URL 是容器 env。若沿用日常 server 容器,唯一能讓它指向 adventure_table_e2e 的方法是換 env 重啟,這會讓日常網站在整個 E2E 期間連到 E2E DB,跑完還要再重啟一次還原——直接違反 §3.5。因此 U01-A 明確要求:
server-e2e 與 web-e2e 兩個 service,掛在 profiles: [e2e] 下,日常 docker compose up 不會啟動它們。server-e2e 使用與 server 相同的 build: context 與 Dockerfile;第二次 build 全部命中 layer cache,不構成額外 image 成本。web-e2e 同理。server-e2e 8001、web-e2e 5174。web-e2e 的 VITE_API_PROXY_TARGET 指向 http://server-e2e:8000。e2e-docker.mjs 只操作 db、server-e2e、web-e2e;不得再出現 up server web。xge-less 第二輪的 ADVENTURE_TABLE_ENABLED_CONTENT_PACKS 子集只套在 server-e2e。附帶效果:現行 xge-less 第二輪會重啟日常 server 兩次的問題一併消失。
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,不免除第二項。
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 查詢照舊
adventure_table_e2e 在 reset script 內是常數。不得用環境變數讓 guard 比對另一個名字——一旦可以換名字,guard 就退化回「靠操作者記得設對」。驗收 C 需要的「刻意打錯 DB」由 script 的 --database 參數提供(它只改 psql 連線目標,不改 guard 比對值)。
reset 抽成 apps/web/scripts/e2e-reset-db.mjs,global setup 呼叫它。這樣:
e2eDockerScript.test.ts 寫 vitest 釘住 guard 行為(SQL 內含 current_database() 檢查、預設目標為 adventure_table_e2e、guard 常數不可被 env 覆蓋)。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 啟動前存在。
在 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
U01-A 不是單純「安全優先,速度以後再說」。隔離方案本身就必須避免製造新的慢點。
server-e2e 與 server 共用 build cache)。使用者入口仍應保持單一命令:
npm run test:e2e:docker
該命令自行確保 db 已起、E2E DB 存在、server-e2e / web-e2e 已重建且 schema 已更新、target 正確、資料已 reset。
使用者不應在每次 run 前手動切 DB、手動 create database、手動 migrate 或手動 restore 日常資料。
server-e2e、web-e2e、adventure_table_e2e、8001、5174 這組常數由 e2e-docker.mjs、e2e-reset-db.mjs、e2e-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。
.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 範圍包含:
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 只維護一條路徑。workflow_dispatch workflow(m01i-e2e.yml、m03b-e2e.yml、p0a-foundation.yml、p2-e2e.yml)擇一:同步改法,或在檔頭標註 deprecated 並說明已被 p3-e2e.yml 涵蓋。不得留下「dispatch 就會撞 guard」的 workflow 而不說明。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),這個缺口會一併補上,關門時要明寫是否已補。
U01-A 至少需要以下證據:
adventure_table 建立可辨識 sentinel data(至少一個 Room、一隻 Character、一張 AI OAuth client)。npm run test:e2e:docker(含 xge-less 第二輪)。adventure_table。server 容器的 StartedAt 未改變(docker inspect)。adventure_table_e2e 建立垃圾測試資料。以 e2e-reset-db.mjs --database adventure_table(帶 ADVENTURE_TABLE_E2E_ALLOW_DESTRUCTIVE_RESET=1)刻意打日常 DB:
adventure_table 任何資料不得被修改(A 的 sentinel 前後比對)。另加 vitest 釘住:SQL 含 current_database() 檢查且位於 TRUNCATE 之前、guard 比對值為常數 adventure_table_e2e、環境變數無法改變該比對值。
adventure_table_e2e 經 server-e2e 啟動的 Alembic upgrade heads 後可執行完整 E2E。開工前先量一次現行 npm run test:e2e:docker 的完整 wall-clock(含 rebuild 與第二輪),記錄在本檔 §13。U01-A 完成後同機器同條件再量一次。隔離機制本身不得造成可觀測的固定時間倒退;若同時做了其他速度改善,應分開記錄,避免把隔離成本藏在總體改善內。
p3-e2e.yml 依 §9 修改後 dispatch 一次全綠,run id 記錄於 §13。
以下是 U01 長期可處理,但不要求塞進 U01-A:
test:e2e:docker 每次無條件 --build 的成本。這些項目可依量測結果新增為 U01-B、U01-C…,不需要等 U01 整體關門。
U 類是新的 Phase 類型,現行 AGENTS.md 與 PROJECT_BRIEF.md 都還不認識它。U01-A 開工的第一個 commit 必須同時:
AGENTS.md「Phase / Subphase 設計原則」新增 U 類定義:Test / Development Efficiency 優化軌;單檔格式(不套三份文件制);長期 open;不進 P Roadmap 順序;每個 U Subphase 仍各自關門並附證據。PROJECT_BRIEF.md:Roadmap 表加 U01 一列;「Subphase 進度」加 ### U01 並一列一個 Subphase;U01-A 完成後把「E2E global setup 無條件清空 Character」那條未結清事項改寫為已解。已知問題.md / README.md 中描述 E2E 需先備份資料的段落,在 U01-A 關門時同步更新。U01-A 實作時以「最小變更完成硬隔離」為目標,依下列順序,每步有可驗證的 check:
npm run test:e2e:docker wall-clock → 記錄於此。server-e2e / web-e2e(profile e2e,共用 build,8001 / 5174)→ verify:docker compose --profile e2e config 通過;docker compose config(無 profile)不含兩個新 service。e2e-reset-db.mjs 含 §6 guard + vitest → verify:打 adventure_table 非零退出且資料不動;打 adventure_table_e2e 正常 truncate。e2e-docker.mjs:up -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 容器未重啟。server-e2e / 8001。p3-e2e 全綠。完成 U01-A 不代表 U01 關門;下一個優化項目依量測結果續編。
| 日期 | 機器 | 條件 | Playwright 主套件 | wrapper 總 wall-clock | 備註 |
|---|---|---|---|---|---|
| 2026-09-13 | 使用者 Windows 開發機 | 舊版 test:e2e:docker(daily server / web、adventure_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 |
docker-compose.yml 新增 profile e2e 的 server-e2e / web-e2e;共用既有 Dockerfile / build context,host ports 固定 8001 / 5174,server-e2e 固定連 adventure_table_e2e。apps/web/scripts/e2e-env.mjs 成為 E2E database / service / port / URL 常數單一來源。e2e-docker.mjs 只確保共用 db healthy、缺少時建立 adventure_table_e2e,再 rebuild / restart E2E pair;xge-less 第二輪只 recreate E2E services。e2e-reset-db.mjs 把 opt-in/CI fuse 與 PostgreSQL current_database() hard guard 分層,identity check + truncate 同一份 psql --single-transaction -v ON_ERROR_STOP=1 執行;guard 常數不可由 env 改寫。server-e2e,baseline Room 改打 8001。p3-e2e.yml 改走與本機相同的 npm run test:e2e:docker,因此 CI 已補上原本缺失的 xge-less 第二輪。p0a-foundation.yml 不再跑舊 browser stack,但保留獨有的 P0→P1 migration / legacy persistence non-E2E gate。adventure_table。34800987506驗收 commit:1b0be4d51aefbe55414edf49e32b147fb88438d0。GitHub Actions P3 Full-Stack E2E 最終 success。
--profile e2e config 均 passed;前者只有 daily services,後者顯示 server-e2e → adventure_table_e2e、8001,web-e2e → 5174。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 四項均已有實機證據。
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
以 --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-transaction、ON_ERROR_STOP=1、固定 guard database name 與 CI/local fuse。
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 並全綠。
使用者 Windows 開發機、branch u01-a-e2e-isolation @ 09ef4d8,daily db / server / web 持續運行,本機 .env 帶著 M04-B 遺留的 ADVENTURE_TABLE_MCP_PUBLIC_ORIGIN(未清除)。
第一次 run(首次建立 adventure_table_e2e):wrapper 自動 createdb;server-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 postgres → ERROR: 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.mjs 以 spawnSync(..., { shell: true }) 帶參數會觸發 Node DEP0190 deprecation warning,且 --database 值直接拼進 shell;目前只供本機/CI 操作者使用,留給後續 U01 項目一併處理。
§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 列出的慢點續編。