# MVP v2.5 开发计划（TDD 驱动）

本文是 v2.5 的**工程化开发计划**，强调“先写测试，再写实现”（TDD），并将每个里程碑拆成**可独立验收**的小闭环。

输入依据：
- 路线图：`specs/mvp/mvp_roadmap_v2.md`
- v2.5 设计：`specs/mvp/v2.5/v2.5_design.md`
- v2.5 API 草案：`specs/mvp/v2.5/v2.5_api.md`
- v2.5 验收：`specs/mvp/v2.5/v2.5_acceptance.md`

v2.5 约束（已确认）：
- **不扩展 TaskSpec**：沿用 v2.0/v2.1 的 YAML 结构化字段与语义。
- **不支持自定义 reward function / 不支持用户自定义 verl 代码**。
- 训练输入（verl 代码、HF cache、datasets）统一使用 `/private/common/...`。
- 多用户隔离 v2.5 **先只隔离 jobs 输出目录**：`/private/users/<uid>/jobs/<ray_submission_id>/...`。

---

## 0. TDD 规范（所有功能都遵循）

### 0.1 测试分层

1) **单元测试（fast）**
- 纯 Python 逻辑：DB、鉴权、ID、路径派生、head.json 解析/TTL、watchdog 决策逻辑。
- 目标：不依赖真实 Ray、不依赖 docker、不依赖网络。

2) **组件测试（中等）**
- FastAPI 路由：使用 `fastapi.testclient.TestClient`（现有 v2.0 已采用）。
- 目标：验证 auth/权限隔离、API 行为、状态机。

3) **端到端（慢/手工或脚本）**
- 在 `argus@h1` 上通过 scripts/compose 跑一次“head publish → worker auto-connect → API submit”闭环。
- 目标：验证无状态 worker + watchdog 的真实行为。

### 0.2 测试约定

- 测试目录：`src/mvp/py/tests/`
- 新增功能必须先补齐测试用例，并让其在未实现时失败（红）。
- 实现最小改动让测试变绿（绿）。
- 重构/去重复（重构）。

> 注：现有测试通过 `src/mvp/py/tests/conftest.py` 注入 ray stub，确保单测不依赖真实 ray 包；v2.5 新增模块也应复用此模式。

---

## 1. 里程碑拆分（v2.5 = 4 个可验证闭环）

### M1：User 表/Token 表 + 基础鉴权（不影响现有内部 token 兼容）

**目标**
- 引入 user/token 的持久化与鉴权映射（token → user_id）。
- 兼容现有 `Authorization: Bearer <MVP_INTERNAL_TOKEN>` 的“单租户模式”，避免一次性破坏 v2.0 用法：
  - v2.5 可以先支持两种 token 模式：
    - legacy：环境变量 `MVP_INTERNAL_TOKEN`（全局单租户）；
    - user token：DB 内签发 token（多用户）。
- admin 能创建用户、签发 token、禁用用户。

**TDD 用例（先写测试）**

单测：
- `test_user_db_create_disable()`
  - 创建用户 ACTIVE；禁用后状态变为 DISABLED；重复创建返回冲突或幂等（按最终约定）。
- `test_token_hashing()`
  - 签发 token 时 DB 中只保存 hash，不保存明文。

API 测试（TestClient）：
- `test_admin_create_user_and_issue_token()`
  - admin token 可创建用户并签发 token（明文 token 只返回一次）。
- `test_disabled_user_token_rejected()`
  - 用户被禁用后，使用旧 token 调用 API 返回 401/403。

**实现落点（建议模块）**
- `argus.service.auth`：token 校验与 user_id 解析（兼容 legacy 模式）
- `argus.service.db`：新增 `users`、`api_tokens` 表与 CRUD
- `argus.service.app`：新增 user 管理 endpoints（admin scope）
- `configs/dev.yaml`：补充 admin token/env 相关配置（保持 YAML 风格）

**验收点**
- `v2.5_acceptance.md`：U1 可通过自动化 API 测试覆盖。

---

### M2：Task 绑定 user_id + API 可见性隔离（仍不改 TaskSpec）

**目标**
- 提交 task 时由 token 推导 `user_id`，写入 `tasks.user_id`。
- task 查询/取消/日志默认只允许 owner；他人访问返回 404（避免泄露存在性）。
- queue 默认只返回当前用户队列；admin 可查询全局队列（可选）。

**TDD 用例（先写测试）**

单测：
- `test_tasks_table_has_user_id()`：创建任务必须落 `user_id`，且 `list_queue(user_id=...)` 只返回该用户任务。

API 测试：
- `test_task_visibility_isolated()`
  - user A 创建 task；user B 查询 `/api/v2/tasks/{id}` 返回 404；
  - user B cancel/logs 也返回 404。
- `test_queue_isolated()`
  - A/B 各自创建 task；`GET /api/v2/queue` 只看到自己的。

**实现落点**
- `argus.service.app`：为 task endpoints 增加 user scope
- `argus.service.db`：tasks 表增加 user_id 字段、索引、按 user 过滤的查询方法
- `argus.service.scheduler`：pick_next_runnable_task 等仍按“全局 FIFO”或“按 user FIFO”
  - v2.5 先保持“全局 FIFO”最简单（但 API queue 视角是按 user 过滤）。

**验收点**
- `v2.5_acceptance.md`：U2 可通过 API 测试覆盖。

---

### M3：Jobs 输出目录按 user 隔离（只改输出，不改输入）

**目标**
- Ray Job 的 job_root 目录由服务端统一计算到：
  - `/private/users/<uid>/jobs/<ray_submission_id>/...`
- TaskSpec 内与输入相关的路径字段必须是 `/private/common/...`（v2.5 输入统一 common）。
- 任何用户无法通过 TaskSpec 指定输出写到非 user jobs 目录（避免越权写）。

**TDD 用例（先写测试）**

单测：
- `test_job_root_derivation_per_user()`
  - 给定 user_id 与 ray_submission_id，派生 job_root 固定且正确。
- `test_reject_non_common_inputs()`
  - TaskSpec 中 train_file / val_file / code_path / hf 路径等若不以 `/private/common/` 开头则拒绝（HTTP 400）。

API 测试：
- `test_job_dir_written_under_user_jobs()`
  - 提交 task 后，在 DB 或 submit payload 中能看到 job_root 在 user jobs 下（可通过 mock RayJobTool.submit 捕获 spec）。

**实现落点（建议最小侵入）**
- 在 service 层派生 `job_root` 并注入到 RayJobTool/builders（而不是让用户从 TaskSpec 指定）。
- RayJobTool `_job_dir()` 改为接收“job_root 生成器”或直接接收 `job_root` 参数（由服务层提供）。
  - 目标：保持 RayJobTool 的职责清晰：提交 Ray job；路径策略由 service 决定。

**验收点**
- `v2.5_acceptance.md`：U3/U4 可通过 API/单测覆盖。

---

### M4：Stateless Ray Node Pool（head.json + worker watchdog）+ 端到端脚本验证

**目标**
- head 启动后持续写入 `/private/ray/discovery/<cluster_name>/head.json`（包含 TTL）。
- worker 容器内运行 watchdog（或启动脚本 + watchdog），无需平台显式传 head 地址：
  - 读取 head.json（存在且未过期）→ `ray start --address=<head_ip>:<gcs_port>`
  - head.json 变化 → `ray stop` + `ray start` 重连
- 在 dev 环境（docker compose）提供一键脚本复现（e2e）。

**TDD 用例（先写测试）**

单测（不跑真实 ray）：
- `test_head_json_read_validate_ttl()`
  - 文件不存在/过期 → 返回“不可用”
  - 未过期 → 返回 head 地址
- `test_watchdog_decision_on_change()`
  - head_ip 变化 → 触发重连动作
  - only updated_at 变化（地址不变）→ 不重连（或按策略重连，需确定）

组件/脚本级测试（可选）：
- 如果 watchdog 用 Python 实现，可对“执行命令”层做 stub（不真正跑 `ray start`），只验证会调用什么命令。

端到端脚本（手工/慢）：
- 提供脚本 `scripts/run_all_v25_stateless.sh`（命名示例）：
  1) 起 head（Ray head + API）
  2) 启动 head publisher（写 head.json）
  3) 起 2 个 worker（每个 4 GPU），worker 只跑 watchdog，不传 head 地址
  4) `ray status` 显示 1 head + 2 worker 且 GPU=8
  5) 通过 API 创建用户/签发 token，提交 PPO/GRPO/SFT
  6) 重启 head（或更新 head.json 指向新地址）验证 worker 自动重连

**实现落点（建议实现策略）**

为了可测试性（TDD），推荐把“读 head.json/判定 TTL/生成 ray start 命令”做成 Python 模块：
- `argus.ray.discovery`：read/write head.json（原子写、TTL）
- `argus.ray.worker_watchdog`：watch loop（polling + change detection），执行命令可注入（便于单测 stub）

脚本层保持薄：
- `scripts/` 负责 docker exec / compose 编排与进程守护；
- watchdog 进程由容器内 python 模块运行（更可测、更易移植到生产平台的 entrypoint/command）。

**验收点**
- `v2.5_acceptance.md`：A1/A2/A3 主要通过 e2e 脚本 + dashboard/日志验证。

---

## 2. 回归策略（确保 v2.0 不被破坏）

在 v2.5 过程中保留并持续回归以下用例（至少单测覆盖）：
- 旧的内部 token 模式仍可访问 `GET /api/v2/queue` 与提交 task（若决定保留兼容）。
- scheduler 的“资源不足 → PENDING_RESOURCES → 延迟重试”行为不变（现有 `test_scheduler.py` 覆盖）。
- `ray entrypoint_resources` 强制 driver 落 worker（继续使用 `worker_node` 自定义资源）。

---

## 3. 交付清单（代码/脚本/文档）

### 3.1 代码
- user/tokens：DB schema + auth + API endpoints
- tasks：绑定 user_id + 权限隔离
- job_root：按 user jobs 输出目录派生（输入仍 common）
- discovery/watchdog：head.json + worker 自愈

### 3.2 scripts（dev e2e）
- head：启动 Ray head + head publisher
- workers：以无状态方式启动（不传 head addr）+ watchdog
- `run_all`：一键跑通（含 API submit + 查询 + cancel + 观察队列）

### 3.3 文档
- 更新 `specs/mvp/v2.5/*`（设计/API/验收/开发计划）
- 补充 `src/mvp/README.md` 的 v2.5 使用方式（如需要）

---

## 4. 关键待确认点（开始实现前必须定稿）

1) **legacy token 是否继续兼容**
- 方案 A：保留 `MVP_INTERNAL_TOKEN`（单租户）+ 新增 user token（多租户）
- 方案 B：v2.5 直接切换到 user token（破坏兼容，但更清晰）

2) **调度公平性**
- v2.5 先全局 FIFO（简单）；后续 v3 再引入 per-user 公平调度/配额。

3) **head.json 的生产写入者**
- 方案 A：与 API 同进程线程（最少组件）
- 方案 B：独立进程（更独立、易运维）

