Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
8770df8
docs: 按 dev 基线重写跨仓清单
claude Sep 1, 2026
788eb1e
docs: 定稿 dev 基线接入的六条契约增补 (C6~C11)
claude Sep 1, 2026
707c08e
docs: 回写 dev 基线接入决策到架构设计
claude Sep 1, 2026
f5ac9c9
docs: 登记 M13 dev 基线接入配套任务
claude Sep 1, 2026
bf18964
docs: 修订 C10 显式包索引首选与改写前缀声明 (T13.4)
qiyinxi Sep 1, 2026
8de0da9
feat(backend): managed 后端工作目录改为 app-root (T13.1)
qiyinxi Sep 1, 2026
806cba7
docs: 记录 T13.1 完成情况
qiyinxi Sep 1, 2026
b34053d
docs: 记录 T13.4 依赖同步镜像改写设计与计划 (T13.4)
qiyinxi Sep 1, 2026
81f47a3
feat: 为包索引源显式声明锁改写前缀 (T13.4)
qiyinxi Sep 1, 2026
f20cd1b
feat: 增加 uv.lock 两处前缀的纯字符串改写 (T13.4)
qiyinxi Sep 1, 2026
30fe0ba
feat(process): Job 允许显式请求脱离 (T13.2)
qiyinxi Sep 1, 2026
4cfdd95
docs: 记录 T13.2 完成情况
qiyinxi Sep 1, 2026
a94228f
feat: 增加依赖同步临时项目目录与受控删除类别 (T13.4)
qiyinxi Sep 1, 2026
39cf370
feat(cli): backend supervise 新增 --shutdown-timeout (T13.3)
qiyinxi Sep 1, 2026
9d21bc1
feat: dependencies sync 按包索引镜像轮换改写锁副本 (T13.4)
qiyinxi Sep 1, 2026
81029ea
feat: 显式包索引首选改为排在轮换最前 (T13.4)
qiyinxi Sep 1, 2026
f12f070
feat: dependencies sync 报告镜像源与尝试次数 (T13.4)
qiyinxi Sep 1, 2026
f204746
test: 让假 uv 识别改写后的临时项目并按锁内容注入失败 (T13.4)
qiyinxi Sep 1, 2026
e5ef0ab
fix(backend): 优雅关闭不再误报 BACKEND_FORCE_TERMINATED (T13.3)
qiyinxi Sep 1, 2026
cf7598a
docs: 记录 T13.3 完成情况
qiyinxi Sep 1, 2026
a53bab7
test: 覆盖依赖同步镜像轮换的组件矩阵 (T13.4)
qiyinxi Sep 1, 2026
850c61b
test: 契约锁定 dependencies sync 的镜像源 details (T13.4)
qiyinxi Sep 1, 2026
bba97c9
refactor: 移除包索引的 --default-index 死代码路径 (T13.4)
qiyinxi Sep 1, 2026
eef95db
docs: 记录 T13.4 依赖同步镜像改写完成情况
qiyinxi Sep 1, 2026
cd7f69f
Merge branch 'feat/t13-mirror-rewrite-20260901' into integ/t13-20260901
qiyinxi Sep 1, 2026
1402129
docs: 记录 T13.5 受管基础设施与镜像源注入设计与计划 (T13.5)
qiyinxi Sep 1, 2026
25a04fb
feat(uv): 受管进程注入基础设施与有序镜像源 (T13.5)
qiyinxi Sep 1, 2026
8918e4b
feat(backend): 解析有序镜像源并随受管环境下发 (T13.5)
qiyinxi Sep 1, 2026
020b3b9
test: 端到端证明后端收到受管基础设施与镜像源 (T13.5)
qiyinxi Sep 1, 2026
1228110
fix(cli): bootstrap 与 repair 同样上报依赖同步的镜像源 (T13.4)
qiyinxi Sep 1, 2026
bde5d73
docs: 回写协议实现偏差与能力标识矛盾
qiyinxi Sep 1, 2026
1f6b13c
feat(protocol): backend supervise 公告 stdin.shutdown 与 stdin.status
qiyinxi Sep 1, 2026
ba27db3
docs: 记录 T13.5 注入面完成情况
qiyinxi Sep 1, 2026
bcadcb3
docs: AGENTS.md 状态表回写 T13.4 完成与 T13.5 注入面
qiyinxi Sep 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Vue/Electron/Python 的改写、CI/CD 发布流程本身。

---

## 2. 当前状态(截至 2026-08-13
## 2. 当前状态(截至 2026-09-02

| 里程碑 | 状态 |
| --- | --- |
Expand All @@ -47,6 +47,7 @@ Vue/Electron/Python 的改写、CI/CD 发布流程本身。
| M10 工程可维护性收敛 | T10.1 文档信息架构已完成;后续阶段见维护设计 |
| M11 跨平台适配(Linux/macOS) | 规划中(决策 D7,2026-08-04 立项);仅 T11.1 设计任务可执行 |
| M12 遥测与错误观测(Sentry-only) | T12.1、T12.2、T12.4、T12.5、T12.7 已完成;T12.6 本地发布配置已提交,待 Repository secret 与授权后的远端验收;T12.3/Umami 已取消 |
| M13 dev 基线接入配套 | 2026-08-31 按决策 D12 与 `doc/契约补充-v1-增补1.md`(C6~C11)立项;T13.1(后端工作目录)、T13.2(Job 允许显式脱离)、T13.3(关闭超时参数化,含优雅关闭误报修复)、T13.4(锁副本改写参与包索引镜像轮换,显式 package-index 排最前)**已完成**;T13.5 注入面已完成(`AUTO_MAS_UV_CACHE_DIR` / `AUTO_MAS_UV_PYTHON_INSTALL_DIR` / `AUTO_MAS_MIRROR_PACKAGE_INDEX` / `AUTO_MAS_MIRROR_PYTHON`),池目录重新分类未做,任务整体 🚧;T13.6 未开始 |

代码现状:

Expand Down Expand Up @@ -77,12 +78,13 @@ Git:远端 `origin` = `git@github.com:AUTO-MAS-Project/AUTO-MAS-Runtime.git`
| [doc/README.md](doc/README.md) | 文档导航、分层与生命周期规则 | 查找任何项目文档时 |
| [doc/架构设计.md](doc/架构设计.md) | 冻结的系统架构:边界、CLI 命令树、NDJSON 协议、错误码/退出码/stage/state 全集、Git 更新流程、uv 策略、目录安全、测试矩阵、验收标准 | 任何涉及对外契约的改动 |
| [doc/契约补充-v1.md](doc/契约补充-v1.md) | 协议 v1 的 5 项定稿细节(C1~C5:固定端口 36163、身份注入环境变量、`failed` 字面量、`AUTO_MAS_SUPERVISED=1`、development 检查边界) | 涉及后端启动/健康检查/环境变量 |
| [doc/任务拆分.md](doc/任务拆分.md) | 逐任务清单、依赖、验收项、决策记录 D1~D6、待决项 D-open-*、AUTO-MAS 侧 TODO、变更记录 | **每次开工前**确认自己在做哪个任务 |
| [doc/契约补充-v1-增补1.md](doc/契约补充-v1-增补1.md) | 对 v1 的增量修订(C6~C11:后端工作目录、受监督优先级扩展到端口、Job 逃逸、关闭预算参数化、依赖镜像改写轮换、运行池基础设施共享),并修订 C2 第 1 条与 C4 第 1 条 | 同上;**与 `契约补充-v1.md` 冲突时以本文件为准** |
| [doc/任务拆分.md](doc/任务拆分.md) | 逐任务清单、依赖、验收项、决策记录 D1~D12、待决项 D-open-*、AUTO-MAS 侧 TODO、变更记录 | **每次开工前**确认自己在做哪个任务 |
| [doc/代码审查清单.md](doc/代码审查清单.md) | 自动化门禁覆盖不到的架构边界检查 | 提交前自查、审查他人代码 |
| `doc/current/M*/` | 尚未完成任务的设计与实施计划 | 执行某个具体任务时 |
| `doc/archive/M*/` | 已完成阶段仍有解释价值的设计与审查记录 | 追溯设计背景时 |

**优先级:** 契约补充-v1(更具体) > 架构设计(概括) > 任务拆分(派生清单)。
**优先级:** 契约补充-v1-增补1(最新增量修订) > 契约补充-v1(更具体) > 架构设计(概括) > 任务拆分(派生清单)。

---

Expand Down Expand Up @@ -472,5 +474,7 @@ Go 测试惯例([Go Code Review Comments](https://go.dev/wiki/CodeReviewCommen
“退出码为 0”“输出不含 `[no tests to run]`”“出现 `--- PASS:`”,照抄这个模式。
- **注释中文、标识符英文**:注释(含 doc comment)写中文,doc comment 仍以英文标识符开头;
Go error 字符串保持英文小写;不要把设计文档写成英文,也不要在同一声明里中英混排。
- **改协议前先看 `doc/契约补充-v1.md`**:架构设计里的概括描述常被它进一步收紧。
- **决策已冻结的事项不要重开**:D1~D6 与 C1~C5 是用户已确认的结论;D-open-4~7 才是待决项。
- **改协议前先看 `doc/契约补充-v1.md` 和 `doc/契约补充-v1-增补1.md`**:架构设计里的概括描述常被它们进一步收紧;
增补 1 还修订了 C2 第 1 条(`protocol` 由后端自报而非回显)与 C4 第 1 条(受监督但非管理员时记 warning 并继续运行)。
- **决策已冻结的事项不要重开**:D1~D12 与 C1~C11 是用户已确认的结论(D2/D4/D8/D9/D10 已被后续决策取代,原文只作追溯);
当前仍待决的是 D-open-4、D-open-5、D-open-7、D-open-10。
5 changes: 3 additions & 2 deletions doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
| 文档 | 用途 |
| --- | --- |
| [架构设计](./架构设计.md) | 系统边界、对外契约、目录安全、命令树和验收标准 |
| [协议 v1 契约补充](./契约补充-v1.md) | 后端端口、身份注入、健康检查等具体契约 |
| [协议 v1 契约补充](./契约补充-v1.md) | 后端端口、身份注入、健康检查等具体契约(C1~C5) |
| [协议 v1 契约补充 增补 1](./契约补充-v1-增补1.md) | 对 v1 的增量修订:工作目录、受监督优先级、Job 逃逸、关闭预算、依赖镜像、运行池基础设施(C6~C11) |
| [任务拆分](./任务拆分.md) | 任务依赖、验收、决策和当前进度 |
| [代码审查清单](./代码审查清单.md) | 自动化门禁之外的架构边界检查 |
| [T1.3 生命周期设计](./设计-T1.3-生命周期状态机.md) | 协议测试直接读取的生命周期契约夹具 |

权威优先级仍为:`契约补充-v1.md` > `架构设计.md` > `任务拆分.md`。
权威优先级仍为:`契约补充-v1-增补1.md` > `契约补充-v1.md` > `架构设计.md` > `任务拆分.md`。

## 按用途浏览

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# 设计与计划 T13.1 后端工作目录与绝对入口路径

- 契约:[增补 1 C6](../../契约补充-v1-增补1.md#c6后端进程工作目录与入口路径)
- 任务:[任务拆分 T13.1](../../任务拆分.md)
- 状态:设计 + 计划(本文件同时承担四段式的前两段)

## 目标

managed 模式下把 Runtime 创建的后端子进程工作目录从 uv 的 project dir(`<app-root>/repo`)
改为 **app-root**,入口参数从裸 `main.py` 改为绝对路径 `<app-root>/repo/main.py`。
`--project` 仍指向 `<app-root>/repo`。development 模式的 cwd 与入口**不变**。

修的是实现偏离文档:架构设计「目录模型」与「插件环境职责边界」本就要求 cwd = 项目根目录,
而 `internal/uv/managed.go:83` 无条件用 `resolved.ProjectDir`。按现实现,后端会把
`config/ data/ history/ script/ debug/` 建进 `repo/`,被 `workspace sync` 的整体替换清掉。

## 边界(不负责)

- 不改 development 模式的任何行为(cwd 仍是 `--repo`,入口仍是裸 `main.py`);
- 不改一次性 uv 命令(`uv python install` / `uv sync` / `uv pip`)的既有行为——新字段为空时
回退 `ProjectDir`,这些调用方一个都不传新字段;
- 不新增、不改名任何协议字段、`stage`、`state`、错误码或 error `details` 键(M13 不得扩大契约面);
- 不改 Python 侧(`AUTO-MAS` 的 `TODO-PY-4` / `TODO-PY-10` 成对落地,本仓库管不到)。

## API 形态

`internal/uv`:

```go
// RunOptions
WorkingDir string // 子进程工作目录;为空回退 ProjectDir
```

`resolvedRunOptions` 同步增加 `WorkingDir`,`resolveOptions` 在解析出 `ProjectDir` 之后
用它做默认值,显式非空值覆盖。`UVRunner.Run` 与 `UVRunner.StartManaged` 都改用
`resolved.WorkingDir` 作为子进程 `Dir`。

`internal/backend`:

- `supervisor.go` 的 managed 无控制通道路径:argv 末项改 `s.layout.BackendEntryFile()`,
`RunOptions.WorkingDir = s.layout.AppRoot()`;
- `control.go` 的受控启动路径(**managed 与 development 共用同一处 `StartManaged`**,
与任务描述里「control.go 是 development 路径」的说法不符,以仓库现状为准):按 mode 分支,
managed 用绝对入口 + app-root,development 保持 `"main.py"` + 空 `WorkingDir`;
- `production.go` 的 `productionUV.StartManaged` 整体透传 `uv.ManagedOptions`,无需改动。

## 失败语义

`WorkingDir` 与其余受管路径一样进 `validateRunnerPaths`:空字符串(回退后仍为空)或含 NUL
时返回 `UV_EXEC_FAILED`,与既有路径校验完全一致,不新增错误码。启动失败的
`details` 键集合保持不变。

## Task 拆分

### Task 1:`internal/uv` 的显式工作目录

- 文件:`internal/uv/runner.go`、`internal/uv/managed.go`、`internal/uv/managed_test.go`、
`internal/uv/runner_test.go`
- 红灯:`TestManaged_WorkingDirOverridesProjectDir`(StartManaged 传 `WorkingDir` 后子进程
`os.Getwd()` 等于该目录)与 `TestRunner_WorkingDirDefaultsToProjectDir`(一次性 `Run`
不传时 cwd 仍为 ProjectDir、传了则用新值)——字段不存在,编译失败
- 绿灯:加 `WorkingDir` 字段与回退逻辑
- 验证:`go test ./internal/uv -run 'WorkingDir' -count=1 -v`

### Task 2:managed 的 app-root 与绝对入口

- 文件:`internal/backend/supervisor.go`、`internal/backend/control.go`、
`internal/backend/supervisor_test.go`、`internal/backend/control_test.go`
- 红灯:`TestSupervise_ManagedUsesAppRootAndAbsoluteEntry`、
`TestControl_ManagedUsesAppRootAndAbsoluteEntry`、
`TestControl_DevelopmentKeepsRepoWorkingDirAndRelativeEntry`
- 绿灯:按 mode 分支设置 argv 末项与 `WorkingDir`
- 验证:`go test ./internal/backend -run 'AppRoot|RelativeEntry' -count=1 -v`

### Task 3:E2E 的真实 cwd 证明

- 文件:`testdata/fakebackend/main.go`(health 响应增加 `workingDir`,仅测试夹具,不是协议)、
`internal/backend/e2e_windows_test.go`
- 红灯:development E2E 断言子进程 cwd == `--repo`;新增 uv 记录断言 argv 末项形态
- 绿灯:夹具落地 cwd 文件 + 断言通过
- 验证:`go test ./internal/backend -run 'BackendE2E' -count=1 -v`

## 验收对照

| 任务拆分验收项 | 覆盖方式 |
| --- | --- |
| 后端 cwd 为 app-root、入口为绝对路径 | Task 2 单测(`StartSpec.Dir`/argv)+ Task 1 真实子进程 `os.Getwd()` |
| development E2E 与既有一次性命令无回退 | Task 1 回退语义 + Task 3 development E2E cwd 断言 |
| 单次自动重启后 cwd 与入口不漂移 | `control.go` 的重启复用同一处 `StartManaged`,Task 2 断言覆盖两次调用 |
| 用户数据在 `workspace sync` 后仍存在 | 由 cwd 移出 `repo/` 直接保证;跨仓联合验收(TODO-PY-4/10)才能端到端证明 |
59 changes: 59 additions & 0 deletions doc/current/M13/设计-T13.2-Job-Object-允许显式脱离.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# 设计与计划 T13.2 Job Object 允许游戏与模拟器脱离

- 契约:[增补 1 C8](../../契约补充-v1-增补1.md#c8job-object-逃逸与游戏模拟器的进程归属)
- 任务:[任务拆分 T13.2](../../任务拆分.md)
- 状态:设计 + 计划

## 目标

Runtime 创建的 Job Object 在 `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` 之外追加
`JOB_OBJECT_LIMIT_BREAKAWAY_OK`,使 AUTO-MAS 在拉起模拟器与 PC 游戏时可以用
`CREATE_BREAKAWAY_FROM_JOB` 让它们脱离受管进程树,从而**不随后端退出**——这是为保持
今天的用户可见行为而定的产品决策。

## 边界(不负责)

- **不用 `JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK`。** 那一位让所有子进程默认脱离,
后端自己的 worker 与 Agent 会一起逃出回收边界;只放开「显式请求」这一条路径;
- `KILL_ON_JOB_CLOSE` 保留,未请求脱离的进程归属、`Snapshot`/`WaitEmpty` 的证明方式、
`details.pid` 语义、`BACKEND_FORCE_TERMINATED` 警告一律不变;
- 不引入任何按进程名的清理(红线第 5 条);
- Runtime 单独开这一位不产生任何行为变化——真正的效果要 AUTO-MAS 侧 `TODO-PY-11`
带上 `CREATE_BREAKAWAY_FROM_JOB` 才出现,验收以跨仓联合「跑着游戏关 AUTO-MAS」为准。

## API 形态

无导出 API 变化。`internal/process/job_windows.go` 的 `NewJob` 只改一行 `LimitFlags`,
并更新 `NewJob` 的 doc comment 与 `internal/process/doc.go` 的包注释措辞——注释是文档的
一部分,「进程树唯一受管」的绝对表述必须收敛为「未显式请求脱离的进程唯一受管」。

## 失败语义

不变。`SetInformationJobObject` 失败仍关闭句柄并返回原错误。
需要注意的是**加这一位之前**,带 `CREATE_BREAKAWAY_FROM_JOB` 的 `CreateProcess`
会直接失败(`ERROR_ACCESS_DENIED`),这正是红灯测试的失败原因。

## Task 拆分

### Task 1:Job 允许显式脱离

- 文件:`internal/process/job_windows.go`、`internal/process/doc.go`、
`internal/process/managed_windows_test.go`
- 红灯:
- `TestJob_BreakawayGrandchildSurvivesJobClose`——孙进程带
`CREATE_BREAKAWAY_FROM_JOB` 启动,`IsProcessInJob(孙, 本 Job)` 为 false,
`Close()` 之后仍存活(测试自行终止它);无 `BREAKAWAY_OK` 时孙进程根本起不来
- `TestJob_NonBreakawayGrandchildStaysInJobAndIsReaped`——不带该标志的孙进程
`IsProcessInJob` 为 true,`Close()` 之后被杀
- 绿灯:`LimitFlags |= windows.JOB_OBJECT_LIMIT_BREAKAWAY_OK`
- 验证:`go test ./internal/process -run 'Breakaway|StaysInJob' -count=1 -v`,
以及 `go test ./internal/process -count=100`(并发相关)与全仓 race

## 验收对照

| 任务拆分验收项 | 覆盖方式 |
| --- | --- |
| 带 `CREATE_BREAKAWAY_FROM_JOB` 的子进程能脱离、Job 关闭后仍存活 | `TestJob_BreakawayGrandchildSurvivesJobClose` |
| 不带该标志的子进程仍随 Job 回收 | `TestJob_NonBreakawayGrandchildStaysInJobAndIsReaped` + 既有 `TestJobE2E_RuntimeTerminationReapsGrandchildren` |
| 既有进程树清理、`BACKEND_FORCE_TERMINATED`、`details.pid` 无回退 | 既有 `internal/process` 与 `internal/backend` 全套测试不改 |
| 「跑着游戏关 AUTO-MAS」 | 跨仓联合验收,本仓库单侧做不到 |
70 changes: 70 additions & 0 deletions doc/current/M13/设计-T13.3-关闭超时参数化.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# 设计与计划 T13.3 关闭超时参数化

- 契约:[增补 1 C9](../../契约补充-v1-增补1.md#c9关闭预算参数化)
- 任务:[任务拆分 T13.3](../../任务拆分.md)
- 状态:设计 + 计划

## 目标

`backend supervise` 新增 `--shutdown-timeout <秒>`:正整数,合法范围 `1`~`120`,**默认 `5`**。
语义是「从向后端发出 `POST /api/core/close` 到进程退出的等待上限,超时才收 Job」。
默认值不变,所以既有行为与既有 E2E 一个字节都不动——本任务**只加开关,不改默认值**。

## 边界(不负责)

- 就绪侧预算(总启动 60 秒、轮询 500 毫秒、单次请求 2 秒、连续成功 2 次)**不参数化**,
保持编译期常量;`defaultRestartDelay` 同样不动;
- 不改默认值:调多少要等 AUTO-MAS `TODO-PY-12` 的实测数据;
- 不改 `hello.capabilities`——那表达的是 stdin 控制命令,不是 CLI 选项;
- 超时之后的语义不变:仍关 Job 兜底,确认树空即发 `BACKEND_FORCE_TERMINATED` 警告并正常完成。

## API 形态

CLI:`command.Flags().StringVar(&shutdownTimeout, "shutdown-timeout", "5", ...)`。
**刻意用 string 而不是 int**:pflag 的 int 解析失败发生在 Cobra 解析阶段,只会走
`diagnosticExit`(stderr + 退出码 2),产生不了 `INVALID_ARGUMENT` 的 NDJSON `result`。
用 string 自行 `strconv.Atoi` 才能让「非整数」和「越界」走同一条 `commandError` 路径,
与 C9「越界按参数错误处理并映射 `INVALID_ARGUMENT`」一致,也与既有 `--protocol` 的处理一致。

`internal/backend`:`Request` 新增 `ShutdownTimeout time.Duration`。

为什么放 `Request` 而不是改 `backendFactory` 签名:超时是**单次监督请求**的参数,
和 `Mode` / `DevelopmentRepo` 同级;而 `backendFactory` 的四参签名被十余处测试引用,
改它会产生与本任务无关的大面积 diff。`Dependencies.ShutdownTimeout` 作为组件测试的
注入点保留,优先级为 `Request.ShutdownTimeout` > `Dependencies.ShutdownTimeout` >
`defaultShutdownTimeout`,只在 `shutdownBackend` 一处解析。

## 失败语义

`--shutdown-timeout` 非整数、`<1`、`>120`(含 `0`、`121`、负数、空串、小数)一律返回
`INVALID_ARGUMENT`(退出码 2、不可重试),`details.field = "shutdown-timeout"`,
在 `backendFactory` 之前拒绝,不建立任何后端资源。不新增错误码、`stage` 或 `state`。

## Task 拆分

### Task 1:CLI 选项与参数校验

- 文件:`internal/cli/backend.go`、`internal/cli/backend_test.go`
- 红灯:`TestBackendSupervise_ShutdownTimeoutArgument`(表驱动:默认 5、边界 1/120、
合法中间值,以及 `0`/`121`/`-1`/`abc`/空串六种拒绝,并断言拒绝时 factory 零调用)
- 绿灯:注册 flag + `strconv.Atoi` + 范围校验 + 写入 `Request.ShutdownTimeout`
- 验证:`go test ./internal/cli -run ShutdownTimeout -count=1 -v`

### Task 2:监督器消费该预算

- 文件:`internal/backend/types.go`、`internal/backend/control.go`、
`internal/backend/control_test.go`
- 红灯:`TestBackend_ShutdownTimeoutBudgetComesFromRequest`——用记录关闭上下文 deadline 的
假 HTTP closer 证明预算随配置变化;配 30 秒时「close 后才退出」优雅收场、
配 10 毫秒且后端不退出时走 Job 兜底并发 `BACKEND_FORCE_TERMINATED`
- 绿灯:`Request.ShutdownTimeout` 字段 + `shutdownBackend` 的三级回退
- 验证:`go test ./internal/backend -run ShutdownTimeout -count=1 -v`

## 验收对照

| 任务拆分验收项 | 覆盖方式 |
| --- | --- |
| 表驱动覆盖合法值、边界 1/120、越界与非数字 | Task 1 |
| 不传该选项时行为与改动前完全一致 | Task 1 的 default 用例(Request 得到 5 秒)+ 既有全套 E2E 不改 |
| 假后端证明配置值真实生效(Job 兜底时刻随配置变化) | Task 2 的 deadline 断言 + 两条分支行为断言 |
| 既有关闭与单次重启契约测试无回退 | `go test ./... -count=1` 全绿 |
Loading