# 用户迁移 E2E 执行计划 ## 背景 本计划用于为“海外历史用户迁移到国内主站”能力补齐完整的 E2E 测试体系。目标不是补几条临时脚本,而是形成一套可以本地稳定复跑、能定位失败原因、后续可接 CI 的测试资产。 本计划覆盖四个层面: - 双节点真实运行环境 - API 黑盒 E2E - UI Smoke E2E - 一键执行脚本、失败产物与文档 ## 当前仓库现状 | 项目 | 现状 | |------|------| | 浏览器 E2E 框架 | 仓库内暂无 Playwright/Cypress | | 现有 E2E 风格 | 只有零散的 `test/e2e_*.go`、`test/e2e_*.py` | | 迁移相关单测/集成测 | 已较完整,`service/user_migration/service_test.go` 已覆盖主链路 | | 自动初始化 | 已支持 `POST /api/setup` 创建 root | | 自动登录 | 已支持 `POST /api/user/login` | | region_sync 配置 | 走 root `option` API 写入数据库 | | CI 工作流 | 当前 worktree 下未见 `.github/workflows/` | | 现成 `test/e2e/` 目录 | 尚未建立 | ## 固定约定 以下约定在第一版中固定,不在实施过程中反复修改: | 项目 | 固定值 | |------|--------| | CN 节点端口 | `3100` | | OV 节点端口 | `3101` | | CN 数据库 | `test-artifacts/user-migration-e2e/cn.db` | | OV 数据库 | `test-artifacts/user-migration-e2e/ov.db` | | 产物目录 | `test-artifacts/user-migration-e2e/` | | CN session 名 | `cn_session` | | OV session 名 | `ov_session` | | Sync API Key | `e2e-sync-key` | | 第一版运行方式 | 双进程 + 双 SQLite,不先上 Docker | | 第一版 UI 覆盖 | Playwright smoke,不做重业务断言 | ## 非目标 本计划明确不做以下事情: - 第一版不引入 Docker Compose - 第一版不把所有迁移正确性断言都放到浏览器层 - 第一版不先接 CI,再回头补本地一键执行 - 第一版不做全站级别的通用 E2E 基建重构 ## 交付物 本计划完成后,仓库中应至少新增以下内容: - `test/e2e/` Go API 黑盒 E2E - `test/e2e/helpers/` 编排、登录、配置、seed、断言 helper - `web/e2e/` Playwright UI smoke 测试 - `web/playwright.config.ts` - `scripts/run-user-migration-e2e.ps1` - `test-artifacts/user-migration-e2e/.gitignore` - `docs/testing/user-migration-e2e.md` ## 总体执行顺序 严格按以下顺序推进,避免返工: 1. 建目录与产物骨架 2. 做双节点进程编排 3. 做 HTTP / root setup / login / option 配置 helper 4. 做 seed 和断言 helper 5. 做 API E2E 主链路 6. 做 API E2E 边界场景 7. 再上 Playwright 基础设施 8. 做 UI smoke 9. 做一键执行脚本 10. 做文档 11. 做全量回归验证 12. 视情况接 CI ## 阶段 0:冻结基础决策 **目标**:锁定实现边界,避免中途改架构。 **任务** - [ ] 确认 API E2E 使用 Go - [ ] 确认 UI E2E 使用 Playwright - [ ] 确认第一版仅做 UI smoke - [ ] 确认双节点端口、数据库文件路径、session 名称 - [ ] 确认第一版不做 Docker 化 **验收** - 后续阶段不再调整端口、数据库路径、session 命名 ## 阶段 1:目录与文档骨架 **目标**:先把结构搭起来。 **文件** - Create: `test/e2e/` - Create: `test/e2e/helpers/` - Create: `web/e2e/` - Create: `test-artifacts/user-migration-e2e/.gitignore` - Create: `docs/testing/user-migration-e2e.md` **任务** - [ ] 建立 `test/e2e/` 和 `test/e2e/helpers/` - [ ] 建立 `web/e2e/` - [ ] 建立 `test-artifacts/user-migration-e2e/` - [ ] 写入产物目录 `.gitignore` - [ ] 创建 E2E 文档骨架 **验收** - 所有目录和骨架文件已存在 **建议提交** - `chore(e2e): scaffold user migration e2e directories` ## 阶段 2:双节点进程编排 **目标**:自动拉起 `cn` 和 `ov` 两个真实后端实例。 **文件** - Create: `test/e2e/helpers/cluster.go` **任务** - [ ] 定义 `NodeProcess` - [ ] 定义 `Cluster` - [ ] 实现 `StartNode` - [ ] 实现 `StartCluster` - [ ] 实现 `WaitReady` - [ ] 实现 `Stop` - [ ] 将 stdout/stderr 重定向到 `cn.log`、`ov.log` **强制环境变量** - `PORT` - `SQLITE_PATH` - `SESSION_SECRET` - `SESSION_NAME` - `SESSION_SECURE=false` - `SESSION_SAMESITE=lax` - `GLOBAL_API_RATE_LIMIT_ENABLE=false` - `GLOBAL_WEB_RATE_LIMIT_ENABLE=false` - `CRITICAL_RATE_LIMIT_ENABLE=false` - `GIN_MODE=release` **节点差异** - CN:`NODE_TYPE=master` - OV:`NODE_TYPE=slave` **验收** - `3100` 和 `3101` 均能返回 `/api/status` - 测试退出时双进程都能被回收 **建议提交** - `test(e2e): add dual-node cluster runner for migration tests` ## 阶段 3:HTTP 客户端与认证 helper **目标**:封装请求、会话、root 初始化和登录。 **文件** - Create: `test/e2e/helpers/http_client.go` - Create: `test/e2e/helpers/auth.go` **任务** - [ ] 定义 `APIClient` - [ ] 实现 `GetJSON` - [ ] 实现 `PostJSON` - [ ] 实现 `PutJSON` - [ ] 实现 `DeleteJSON` - [ ] 支持 cookie jar - [ ] 失败时落请求/响应 dump - [ ] 实现 `SetupRoot` - [ ] 实现 `Login` - [ ] 实现 root 登录快捷函数 **验收** - 可通过 `POST /api/setup` 自动创建 root - 可通过 `POST /api/user/login` 自动拿到会话 **建议提交** - `test(e2e): add setup and session helpers for migration tests` ## 阶段 4:region_sync 配置 helper **目标**:通过 root `option` API 配好双节点联通。 **文件** - Create: `test/e2e/helpers/options.go` **任务** - [ ] 实现 `SetOption` - [ ] 实现 `ConfigureCNRegionSync` - [ ] 实现 `ConfigureOVRegionSync` - [ ] 实现 `WaitOptionApplied` **需要写入的配置** CN 侧: - `region_sync.enabled = true` - `region_sync.region_id = "cn"` - `region_sync.is_master = true` - `region_sync.master_endpoint = "http://127.0.0.1:3100"` - `region_sync.slave_endpoints = ["http://127.0.0.1:3101"]` - `region_sync.sync_api_key = "e2e-sync-key"` OV 侧: - `region_sync.enabled = true` - `region_sync.region_id = "ov"` - `region_sync.is_master = false` - `region_sync.master_endpoint = "http://127.0.0.1:3100"` - `region_sync.slave_endpoints = []` - `region_sync.sync_api_key = "e2e-sync-key"` **注意** - `slave_endpoints` 通过 JSON 字符串写入,不要直接写 Go slice 的字符串表示 **验收** - CN 能通过真实 HTTP 访问 OV 的内部迁移接口 **建议提交** - `test(e2e): add region sync configuration helpers` ## 阶段 5:测试 seed helper **目标**:每次测试从固定数据出发,结果稳定。 **文件** - Create: `test/e2e/helpers/seed.go` **任务** - [ ] 实现 `ResetDB` - [ ] 实现 `OpenSQLite` - [ ] 实现 `SeedCNData` - [ ] 实现 `SeedOVData` - [ ] 实现 `SeedOVSyncedCopy` - [ ] 可选:实现 `SeedCustomOAuthData` **固定数据集** CN: - `cn-merge-target` - `cn-synced-copy-owner` - `taken-username` - `aff-owner` OV: - `ov-create` - `ov-merge` - `ov-hard-synced` - `ov-username-conflict` - `ov-aff-soft` 额外: - 一条 OV `source=synced` 用户,`remote_user_id` 指向 `cn-synced-copy-owner` **验收** - 重复跑测试时,扫描结果和执行结果稳定一致 **建议提交** - `test(e2e): add migration seed data for cn and ov nodes` ## 阶段 6:断言与轮询 helper **目标**:把复杂轮询和 DB 断言收敛到 helper,主测试保持清晰。 **文件** - Create: `test/e2e/helpers/assert.go` **任务** - [ ] 实现 `WaitBatchStatus` - [ ] 实现 `GetBatchDetail` - [ ] 实现 `FindItemBySourceUserID` - [ ] 实现 `AssertCNUserExists` - [ ] 实现 `AssertOVUserSynced` - [ ] 实现 `AssertQuota` - [ ] 实现 `AssertNoDuplicateQuotaGrant` **验收** - 主测试文件中不再出现重复的轮询和 JSON 解包样板 **建议提交** - `test(e2e): add assertion helpers for migration flow` ## 阶段 7:API E2E 主测试骨架 **目标**:建立统一生命周期和测试入口。 **文件** - Create: `test/e2e/user_migration_test.go` **任务** - [ ] 实现 `TestMain` - [ ] 在 `TestMain` 中启动 cluster - [ ] 在 `TestMain` 中 setup root - [ ] 在 `TestMain` 中 login root - [ ] 在 `TestMain` 中配置 region sync - [ ] 为每个测试实现 DB 重建与 seed **建议** - 为保证稳定性,每个测试单独重建数据库并重启双节点 **验收** - 测试环境可完全自动初始化,无人工前置 **建议提交** - `test(e2e): add user migration test main lifecycle` ## 阶段 8:API E2E P0 主链路 **目标**:先证明迁移全链路真实可用。 **文件** - Modify: `test/e2e/user_migration_test.go` **必须实现的用例** - [ ] `TestUserMigrationE2E_FullFlow_GrayScenarios` - [ ] `TestUserMigrationE2E_IdempotentRetry` - [ ] `TestUserMigrationE2E_ExecutionTimeDriftFails` **`FullFlow_GrayScenarios` 覆盖点** - 创建批次 - 调 `scan` - 检查 5 类用户扫描结果 - 冲突项人工 `resolve` - 执行 `execute` - 调 `verify` - 校验 CN 用户状态 - 校验 OV `source=synced` - 校验 `remote_user_id` - 校验 `synced_quota == cn.quota` **验收命令** - `go test -v ./test/e2e -run TestUserMigrationE2E_FullFlow_GrayScenarios -count=1` - `go test -v ./test/e2e -run TestUserMigrationE2E_IdempotentRetry -count=1` - `go test -v ./test/e2e -run TestUserMigrationE2E_ExecutionTimeDriftFails -count=1` **建议提交** - `test(e2e): add core migration flow tests` ## 阶段 9:API E2E P1 边界与稳定性 **目标**:补齐重扫、分页、权限、异常输入等场景。 **文件** - Modify: `test/e2e/user_migration_test.go` **必须实现的用例** - [ ] `TestUserMigrationE2E_RescanPreservesHumanDecision` - [ ] `TestUserMigrationE2E_ScanPagination` - [ ] `TestUserMigrationE2E_RootPermission` - [ ] `TestUserMigrationE2E_ResolveInvalidPayload` - [ ] `TestUserMigrationE2E_ScanConflict_WhenAlreadyScanning` - [ ] `TestUserMigrationE2E_ExecuteConflict_WhenAlreadyRunning` - [ ] `TestUserMigrationE2E_VerifyQuotaMismatchDetected` - [ ] `TestUserMigrationE2E_SkipNeverConvertsRemote` **验收命令** - `go test -v ./test/e2e -count=1` **建议提交** - `test(e2e): add migration edge case coverage` ## 阶段 10:Playwright 基础设施 **目标**:搭 UI smoke 运行底座。 **文件** - Create: `web/playwright.config.ts` - Create: `web/e2e/global-setup.ts` - Create: `web/e2e/global-teardown.ts` - Create: `web/e2e/fixtures/cluster.ts` **任务** - [ ] 安装 `@playwright/test` - [ ] 配置 `testDir` - [ ] 配置 `trace` - [ ] 配置 `screenshot` - [ ] 配置 `video` - [ ] 在 `globalSetup` 中启动双节点 - [ ] 在 `globalSetup` 中 setup root - [ ] 在 `globalSetup` 中创建普通用户 - [ ] 在 `globalSetup` 中 seed UI 测试数据 - [ ] 生成 `root-storage.json` - [ ] 生成 `user-storage.json` - [ ] 在 `globalTeardown` 中回收进程 **验收** - `cd web && bunx playwright test --list` 可正常执行 **建议提交** - `test(ui): add playwright infrastructure for migration smoke tests` ## 阶段 11:UI Smoke 测试 **目标**:验证 root 迁移中心页面真正可用。 **文件** - Create: `web/e2e/user-migration.spec.ts` - Modify: `web/src/components/table/users/migration/*`(如需补 `data-testid`) **必须实现的用例** - [ ] `root sees migration tab` - [ ] `non-root cannot see migration tab` - [ ] `can create batch` - [ ] `scan updates status with polling` - [ ] `can resolve conflict item` - [ ] `execute shows freeze-window confirmation` - [ ] `verify results table is rendered` - [ ] `page refresh preserves data loading` **规则** - UI 层只断言页面行为,不做 DB 最终值断言 - 重业务正确性仍由 API E2E 兜底 **验收** - `cd web && bunx playwright test` **建议提交** - `test(ui): add migration center smoke tests` ## 阶段 12:一键执行脚本 **目标**:本地单命令跑完全部 E2E。 **文件** - Create: `scripts/run-user-migration-e2e.ps1` **任务** - [ ] 支持 `-ApiOnly` - [ ] 支持 `-UiOnly` - [ ] 支持 `-All` - [ ] 先清理旧产物 - [ ] 跑 API E2E - [ ] 跑 UI E2E - [ ] 汇总结果 - [ ] 失败时保留日志、DB、trace **验收** - 一条命令可以跑完整套测试 **建议提交** - `chore(e2e): add user migration e2e runner` ## 阶段 13:文档补全 **目标**:让别人能跑、能排查、能扩展。 **文件** - Modify: `docs/testing/user-migration-e2e.md` **任务** - [ ] 写明测试架构 - [ ] 写明覆盖范围 - [ ] 写明本地运行命令 - [ ] 写明失败产物位置 - [ ] 写明常见失败与排查方式 - [ ] 写明如何新增迁移场景 **验收** - 新同事可按文档独立跑通 **建议提交** - `docs(e2e): document user migration end-to-end tests` ## 阶段 14:总体验证 **目标**:确认新增 E2E 不破坏现有代码。 **必须执行** - [ ] `go test ./...` - [ ] `go test -v ./test/e2e -count=1` - [ ] `cd web && bun run build` - [ ] `cd web && bunx playwright test` **验收** - 后端单测全绿 - API E2E 全绿 - 前端构建通过 - UI smoke 全绿 ## 阶段 15:可选 CI 接入 **目标**:将 E2E 纳入自动化执行。 **说明** 当前仓库中尚未看到 `.github/workflows/`,本阶段默认后置,不作为本轮硬性前置条件。 **任务** - [ ] 新增 API E2E job - [ ] 新增 UI smoke job - [ ] 上传 `test-artifacts/user-migration-e2e/` **验收** - CI 可独立稳定执行,失败时能下载产物 ## 开始前仍需要的最少步骤 如果要立即开始实施,正式开工前只需要做这些最小动作: - [ ] 确认第一批只做 API E2E 基础设施 - [ ] 确认第一版进程启动方式是 `go run .` - [ ] 确认允许在迁移前端组件中补 `data-testid` - [ ] 确认第一版先不接 CI 确认完后,直接从**阶段 1**开始执行,不需要额外准备。 ## 最终完成定义 只有同时满足以下条件,才算该 E2E 计划完成: - `test/e2e` API E2E 可稳定运行 - `web/e2e` UI smoke 可稳定运行 - 一键执行脚本可用 - 失败产物可保留 - 文档完整 - `go test ./...` 通过 - `cd web && bun run build` 通过