# MVP v3.0 开发计划(TDD 驱动) 本文是 v3.0 的**工程化开发计划**,强调“先写测试,再写实现”(TDD),并将每个里程碑拆成**可独立验收**的小闭环。 输入依据: - 路线图:`specs/mvp/mvp_roadmap_v2.md` - v3.0 设计:`specs/mvp/v3.0/v3.0_design.md` - v3.0 API:`specs/mvp/v3.0/v3.0_api.md` - v3.0 验收:`specs/mvp/v3.0/v3.0_acceptance.md` - 现状基线:v2.5(Task queue + User mgmt + Stateless ray pool + 单镜像节点守护) v3.0 已确认约束: - 允许用户数据集路径:`/private/users//datasets/...` - 允许用户本地模型路径:`/private/users//models/...` - **不允许执行用户自定义代码**(不注入 user code 到 PYTHONPATH;`code_path` 仍只允许 `/private/common/...`) - SFTPGo 先用 **password** 方案(方案 A:API 联动创建/管理 SFTPGo 用户) - jobs retention:**3 天移入回收站(trash/jobs),再 7 天永久删除**;不提供 keep/延长保留标记 - janitor:**API server 内置后台线程**;删除/移动采用**文件系统直接操作**(不依赖 SFTPGo API) --- ## 0. TDD 规范(所有功能都遵循) ### 0.1 测试分层 1) **单元测试(fast)** - 纯 Python 逻辑:路径策略、SFTPGo client、retention 计算、文件移动/删除策略(用临时目录)。 - 不依赖真实 Ray、不依赖 docker、不依赖网络。 2) **组件测试(中等)** - FastAPI 路由(含 WebUI 路由):`fastapi.testclient.TestClient` - mock/stub SFTPGo client 与 ray client 3) **端到端(慢)** - 在 `argus@h1` 通过 docker compose + scripts: - Ray 集群自动起来(head+2 worker) - SFTPGo 服务可用 - 上传数据 → 提交训练 → 下载产物 → jobs 回收站/清理 ### 0.2 代码与测试约定 - 测试目录:`src/mvp/py/tests/` - 新功能必须先补齐测试用例,并让其在未实现时失败(红) - 最小实现让测试变绿(绿) - 再做重构(重构) - 覆盖率:继续沿用当前阈值(>= 90%) --- ## 1. 里程碑拆分(v3.0 = 5 个可验证闭环) ### M1:TaskSpec 路径策略升级(允许 user datasets/models;code_path 仍仅 common) **目标** - API submit 时的路径校验从 v2.5 的 “仅 `/private/common/`” 升级为: - `train_file` / `val_file`:允许 `/private/common/...` 与 `/private/users//...` - 本地模型路径:允许 `/private/users//models/...`(不改变 YAML 结构,见实现建议) - `code_path`:仍仅允许 `/private/common/...` - 阻止越权路径(`/private/users/other/...`)与非 `/private/...` 路径。 **实现建议(不扩展 TaskSpec)** - `model_id` 字段保持不变: - 若 `model_id` 以 `/private/` 开头 → 视作本地模型路径 - 否则视作 HuggingFace repo id(如 `Qwen/...`) **TDD 用例(先写测试)** - 单测: - `test_paths_allow_common_and_own_user_prefix()` - `test_paths_reject_other_user_prefix()` - `test_model_id_local_path_allowed_only_under_users_models()` - `test_code_path_still_common_only()` - API 测试: - `test_submit_accepts_user_datasets_paths()` - `test_submit_rejects_cross_user_paths_404_or_400()`(按约定返回 400/403) **验收点** - `v3.0_acceptance.md` 的 D 类安全隔离用例可由 API 测试覆盖。 --- ### M2:SFTPGo 集成(方案 A:用户联动创建 + password) **目标** - 引入 `data management (SFTPGo)`: - admin 创建用户时联动创建 SFTPGo 用户(home=/private/users/,chroot) - password 模式:生成一次性密码(reset/create)并返回给 admin(明文只返回一次) - 提供用户自助信息: - `GET /api/v2/me` 返回 SFTP 连接信息、目录约定、retention 提示。 **实现建议** - 新增 `SFTPGoAdminClient`(同步调用): - 通过 `urllib` 或 `httpx`(建议 `urllib`,减少依赖;禁止 hard-code requests 使用) - 支持:create user / disable user / reset password(最小集合) - API server 启动时校验配置(enabled 时必须具备 admin 密码 env)。 - 同步创建用户目录结构(文件系统): - `/private/users//{datasets,models,code,jobs,trash/jobs}`(最小必需) **TDD 用例(先写测试)** - 单测: - `test_sftpgo_client_builds_correct_requests()`(不发真实网络;mock urlopen) - `test_user_dirs_created_on_user_create()`(tmp dir 断言目录存在) - API 测试: - `test_create_user_calls_sftpgo_client()`(stub client,断言调用参数) - `test_me_returns_sftp_info_and_paths()`(含 trash/jobs 与 TTL 字段) **验收点** - `v3.0_acceptance.md` 的 A 类(用户/凭据)与 B 类(上传闭环前置)覆盖。 --- ### M3:WebUI(最小可用,多页面 + 侧边栏) **目标** - WebUI 由 API server 托管(同源,无额外 CORS): - `/ui/login`:token 粘贴登录(localStorage) - `/ui/tasks`:任务列表 + 过滤(最小) - `/ui/tasks/new`:YAML 提交(优先)+(可选)表单生成 YAML - `/ui/tasks/{task_id}`:详情页 - `/ui/tasks/{task_id}/logs`:日志 tail + 可选自动刷新 - `/ui/data`:SFTP 信息 + 目录/retention 提示 - (可选)`/ui/admin/users`:管理员用户管理(若时间允许,强烈建议) **实现建议** - 先不引入 Node 构建: - HTML 模板可用最简单的字符串拼接或 Jinja2(若引入 jinja2,则补齐依赖与测试) - 页面通过 fetch 调用 `/api/v2/...`,并复用 token header **TDD 用例(先写测试)** - 组件测试(TestClient): - `test_ui_routes_render_200()` - `test_ui_contains_sidebar_links()`(简单断言文本包含导航链接) - `test_ui_tasks_detail_shows_ids()`(包含 task_id、state、ray_submission_id) **验收点** - WebUI 能完成:登录→创建任务→查看任务→查看日志→看到 data 页提示。 --- ### M4:Jobs Retention janitor(3 天移入 trash,7 天后 purge) **目标** - API server 内置 janitor 后台线程: - 周期性扫描 DB 中 terminal tasks - 到期后执行: - move:`/private/users//jobs/` → `/private/users//trash/jobs/` - purge:递归删除 `/private/users//trash/jobs/` - 全程严格 path 校验,禁止越界删除 - 清理操作记录到 DB events(审计) **实现建议(数据与状态)** - 需要稳定的时间锚点与幂等: - 使用 attempts.end_time 作为 job 结束时间(latest attempt) - 在 tasks 表新增字段(或新表)记录: - `trashed_at`(首次成功 move 时间) - `purged_at`(成功删除时间) - `trash_path`(可选) - 幂等:重复运行不会报错(目录不存在视为已处理) **TDD 用例(先写测试)** - 单测(用 tmpdir 构造 jobs/trash 目录): - `test_janitor_moves_job_to_trash_after_threshold()` - `test_janitor_purges_trash_after_threshold()` - `test_janitor_never_touches_models_or_datasets()` - `test_janitor_path_escape_rejected()`(恶意 path 不可删) - API/组件测试: - `test_me_includes_retention_fields()`(jobs_trash_after_days/jobs_purge_after_days) **验收点** - `v3.0_acceptance.md` 的 C2 用例可按“把阈值调小到分钟级”完成验证。 --- ### M5:端到端(h1)— SFTP 上传→训练→产物下载→回收站/清理 **目标** - 在 `argus@h1` 落一个一键脚本(或手册)跑通: 1) `docker compose up -d` 拉起 Ray(head+2 worker)+ SFTPGo 2) admin 创建用户 alice(联动创建 SFTPGo 用户 + password) 3) alice 通过 SFTP 上传: - 数据集到 `/private/users/alice/datasets/...` - (可选)本地模型到 `/private/users/alice/models/...` 4) alice 通过 API/WebUI 提交任务引用上述路径 5) 任务成功后: - 从 `jobs/` 下载 logs/checkpoints - 把权重移动到 `models/`,验证不会被清理 6) 把 retention 配置调小,验证 jobs→trash→purge **交付建议** - 新增脚本(命名示例): - `scripts/run_all_v30_api.sh` - `scripts/run_e2e_v30_cases.sh` - 新增 `docker-compose.yaml` 中的 `sftpgo` service(或 `docker-compose.v30.yaml` 叠加文件) **验收点** - `v3.0_acceptance.md` 全部 MUST 用例通过。 --- ## 2. 风险与测试关注点 1) **权限与路径逃逸** - path policy 必须覆盖:train/val/model_id(local)/output dirs(jobs/trash) - 所有删除/移动必须做 prefix 校验 2) **并发与竞态** - janitor 只处理 terminal tasks,避免清理正在写入的目录 - move 使用同文件系统 `os.replace`(原子) 3) **SFTPGo 可用性** - SFTPGo 不在线不应影响训练与 API 核心功能(除了用户创建联动) - janitor 不依赖 SFTPGo(文件系统直连) --- ## 3. 交付清单(代码/配置/脚本/文档) ### 3.1 代码 - Path policy(v3.0) - SFTPGoAdminClient + user create/disable/reset password 联动 - `/api/v2/me` 扩展(SFTP/目录/retention) - WebUI 路由与静态资源 - janitor(trash+purge)后台线程 + DB 记录 ### 3.2 配置 - `configs/dev.yaml` 增加 `data.sftpgo`、`data.retention` 段(详见设计文档) ### 3.3 scripts / compose - compose 增加 `sftpgo`(或新增 overlay compose 文件) - v3.0 e2e 脚本(上传/下载/清理验证) ### 3.4 文档 - 更新 `specs/mvp/v3.0/*` 与 `src/mvp/README.md`(运行方式、路径约定、SFTP 操作、retention 解释)