AI 编程工具横评实测报告(2026-08-04)
7 款 AI 编程工具,同一模型(deepseek-v4-flash)、同一测试题、同一网关并行实测。 附带 token 消耗对比 + 代码质量 + 速度 + 易用性 + 已知 bug 全记录。
目录
- 快速结论(30 秒版)
- 测试方法(完整版)
- 测试题原文 / 统一环境 / 执行命令 / 判定标准 / 局限与偏差 / 选型决策
- 结果总表
- 分工具详解(7 款逐一分析)
- 模型对比:glm_for_coding vs deepseek-v4-flash
- 最终推荐
- 部署与配置坑(详版)
- Token 数据证据链
- 测试产物全集(7 份完整代码 + pytest 输出)
快速结论(30 秒版)
| 🏆 最佳工具 | OpenCode — 最快(32s)+ 最省 token(11.7K),质量过关 |
| 质量天花板 | Claude Code — 22/22 测试最全最严谨,但 token 消耗最大 |
| 意外之喜 | Codex — 换 deepseek 后从失败×4 变 13/13(之前是模型问题) |
| 配置最坑 | Goose / Crush — 修好配置后都能用,但过程曲折(详见部署坑) |
| 不推荐 | Kilo(OpenCode 劣化版)、Cline CLI(team_status 崩溃 bug) |
| 通用结论 | deepseek-v4-flash 是当前网关下最佳编码模型(比 glm_for_coding 快 5-15 倍) |
阅读导航:想复现测试看 测试方法;想挑工具看 最终推荐;想看各工具实际写的代码看 测试产物全集;想避坑看 部署与配置坑;想核对数字看 Token 证据链。
测试方法(完整版)
1. 测试题(逐字原文,所有工具收到的 prompt 完全相同)
Create todo.py (Python CLI todo app with add/list/done/delete commands and JSON persistence) and test_todo.py (pytest unit tests). Run the tests and make sure they pass.
中文翻译:创建 todo.py(带 add/list/done/delete 命令和 JSON 持久化的 Python CLI 待办应用)和 test_todo.py(pytest 单元测试)。运行测试并确保全部通过。
这个测试题考察的能力:
- 理解需求并拆解(CLI 命令设计、JSON 存储设计)
- 写出可测试的代码(函数/类结构可被 pytest 引用)
- 自己写测试用例覆盖功能
- 执行测试、发现失败、修复到全过(自我验证闭环)
- 工具链使用(argparse、json、pytest、文件读写)
2. 统一环境
| 测试日期 | 2026-08-04 |
| 模型 | deepseek-v4-flash(统一,通过网关 https://api.myagentlab.homes/v1) |
| API key | 同一把网关 key(用户提供,见 credentials) |
| 系统 | root 用户,Linux |
| Python | 3.13.5 |
| pytest | 9.1.1(venv: /tmp/bench-venv,系统无 pip 故用 uv 建 venv 装) |
| 工作目录 | /home/agent/coding-benchmark/test-<工具名>/(每工具独立目录,互不干扰) |
| 执行方式 | 7 工具并行后台运行(同模型同 key 同时跑,网关竞争环境一致) |
3. 每个工具的具体执行命令(实测用的真实命令)
| Claude Code | claude -p \”<测试题>\” –model deepseek-v4-flash –allowedTools \’Bash,Write,Read,Edit\’ –output-format stream-json –verbose | root 不能用 –dangerously-skip-permissions |
| OpenCode | opencode run \”<测试题>\” | 项目级 opencode.json 配 myagentlab provider |
| Codex | codex exec –dangerously-bypass-approvals-and-sandbox –skip-git-repo-check –json \'<测试题>\’ | config.toml 配 wire_api=chat |
| Cline | cline –json \”<测试题>\” | cline auth openai 配好;team_status bug 需 enableTeams:false 缓解 |
| Kilo | kilo run -m myagentlab/deepseek-v4-flash \”<测试题>\” | kilo.jsonc 配 myagentlab provider |
| Crush | crush run -m myagentlab/deepseek-v4-flash \”<测试题>\” | crush.json 配 myagentlab provider |
| Goose | goose run -t \'<测试题>\’ –provider myagentlab –model deepseek-v4-flash | 需自定义 provider + supports_streaming:false |
4. 判定标准
- 通过 = 该工具自己生成的 test_todo.py 用 pytest 跑通(/tmp/bench-venv/bin/python -m pytest test_todo.py),测试数量由工具自己决定(22/12/13/24/10/13/17 各不相同)
- 耗时 = 从启动命令到进程退出(含模型首 token 延迟 + 工具自我迭代)
- token 数据 = 各工具本地记录(来源见\”Token 数据证据链\”章节)
- 代码质量 = 人工检查产物(测试覆盖、边界处理、可读性)
5. 测试的局限与已知偏差(诚实声明)
6. 为什么选 deepseek-v4-flash(决策过程)
- 首轮用 glm_for_coding(网关 GLM 渠道):首 token 40-70 秒,每工具 7-9 分钟,且 Codex heredoc 转义错乱、Crush 有 DEFAULT_FILE bug
- 用户建议测试 deepseek 模型 → 实测 1.7s 首 token,且 Codex/Crush 的问题自动消失
- 最终统一用 deepseek-v4-flash 重测全部 7 工具(第二轮),保证公平
- 首轮 glm 数据仅作对照参考,排名以 deepseek 轮为准
结果总表
| 🥇 | OpenCode | 1.18.12 | ✅ 12/12 | 32s | 11,749 | 40,576 | 1,992 | 115+94 行 |
| 🥈 | Claude Code | 2.1.220 | ✅ 22/22 | 134s | 227,467 | 190,976 | 12,435 | 141+182 行 |
| 🥉 | Codex | 0.50.0 | ✅ 13/13 | 78s | ~132K (估)* | — | ~12K (估)* | 约 180 行 |
| 4 | Goose | 1.45.0 | ✅ 17/17 | 77s | 80,848 | 43,776 | 6,769 | 126+146 行 |
| 5 | Crush | go 编译版 | ✅ 13/13 | 66s | 14,499 | 无此字段† | 40‡ | 124+102 行 |
| 6 | Cline | 3.0.49 | ✅ 24/24 (修复后) | 104s | 11,234 | 5,632 | 6,033 | 154+200 行 |
| 7 | Kilo Code | 7.4.17 | ✅ 10/10 | 121s | 15,488 | 102,272 | 2,291 | 101+74 行 |
* Codex 本地不记录 usage(OpenAI 兼容接口),网关无分 key 明细;按成功 session 的 6 轮 API 请求 × 累积上下文估算(input 12K→32K 递增),单任务成本约 $0.027 † Crush 的 crush.db sessions 表无 cache_read 字段,故无数据 ‡ Crush 记录值(completion_tokens 疑似只记末次请求,实际生成代码见产物)
分工具详解
🥇 1. OpenCode — 综合最优
最快(32s)+ 最省 token(11.7K input,其他工具的 1/5-1/7)+ 代码质量过关。
- 测试:12/12 通过
- 自动发现项目 .venv 里的 pytest 9.1.1 并直接使用
- 安装难点:GitHub 下载慢需镜像(ghfast.top),但配置极简
- Token 数据:本地 SQLite(~/.local/share/opencode/opencode.db session 表)可精确查询
配置要点:
// 项目级 opencode.json
{
\”provider\”: {
\”myagentlab\”: {
\”npm\”: \”@ai-sdk/openai-compatible\”,
\”options\”: {
\”baseURL\”: \”https://api.myagentlab.homes/v1\”, \”apiKey\”: \”见 credentials\” },
\”models\”: {
\”deepseek-v4-flash\”: {
\”name\”: \”deepseek-v4-flash\” } }
}},
\”model\”: \”myagentlab/deepseek-v4-flash\”
}
opencode run \”任务描述\”
🥈 2. Claude Code — 质量天花板
代码质量最高(22 个测试最多最全),但 token 消耗最大。
- 测试:22/22 通过
- 亮点:自己发现并修复了 mktemp 空文件导致 json.load 崩溃的 bug(加了空文件容错 + 2 个对应测试);用 next_id 计数器保证删除后 id 不复用
- 系统提示词最完善,工具定义最全——这也是它 token 大的原因
- 生态最大(134K stars)、文档全、最成熟
Token 分析: input 227K 中 190K 是 cache read(价格是正常 input 的 1/10),实际新计算 ~36K,并非全价消耗。
配置要点:
export ANTHROPIC_API_KEY=<key> ANTHROPIC_BASE_URL=https://api.myagentlab.homes
claude -p \”任务\” –model deepseek-v4-flash –allowedTools \’Bash,Write,Read,Edit\’ –output-format stream-json –verbose
⚠️ root 用户不能用 –dangerously-skip-permissions(被拒),必须用 –allowedTools
🥉 3. Codex — 模型对了就起飞
换 deepseek 后从\”失败×4\”变\”13/13 通过、78s\”,但 token 数据缺失 + 有推理模型兼容隐患。
- 测试:13/13 通过,还做了完整 smoke test(add→done→list→delete 端到端)
- 重要历史:glm_for_coding 下失败 4 次(heredoc 被 chr(39) 转义搞坏、代码测试不一致、连接卡死)——是模型问题不是 Codex 问题
- 隐患:deepseek 是推理模型,Codex 多轮对话时触发 reasoning_content 必须回传 400 报错——复杂多轮任务会随机挂,只适合单轮/简单任务
- Terminal-Bench 2.1 排名第一(83.4%)的工具,能力本身最强
- Token 说明:本地 session 不记录 usage;曾尝试\”账单前后差值法\”但网关为共享 key(后台有 MRS refactor 等任务并发消耗),差值 $2.09 不可信;最终按 6 轮请求 × 累积上下文估算 input ~132K / output ~12K / 成本 ~$0.027
配置要点:
# ~/.codex/config.toml
model = \”deepseek-v4-flash\”
model_provider = \”myagentlab\”
[model_providers.myagentlab]
name = \”myagentlab\”
base_url = \”https://api.myagentlab.homes/v1\”
env_key = \”OPENAI_API_KEY\”
wire_api = \”chat\”
codex exec –dangerously-bypass-approvals-and-sandbox –skip-git-repo-check –json \’任务\’
4. Goose — 大器晚成
配置坑最多但修好后很稳,17/17 通过,77s。
- 测试:17/17 通过,代码质量好(126 行 todo.py 结构清晰,含损坏文件容错、时间戳、自动递增 ID)
- 自带任务管理(todo_write 工具),agent 会自己记账
- Linux Foundation 项目,48K stars,潜力大
⚠️ 配置坑(都是实测踩出来的):
配置要点:
// ~/.config/goose/custom_providers/myagentlab.json
{
\”name\”: \”myagentlab\”,
\”engine\”: \”openai\”,
\”api_key_env\”: \”MYAGENTLAB_API_KEY\”,
\”base_url\”: \”https://api.myagentlab.homes/v1\”,
\”models\”: [ {
\”name\”: \”deepseek-v4-flash\”, \”context_limit\”: 128000 } ],
\”supports_streaming\”: false,
\”requires_auth\”: true
}
MYAGENTLAB_API_KEY=<key> goose run -t \’任务\’ –provider myagentlab –model deepseek-v4-flash
5. Crush — 焕然一新
从 glm 下 13/14(有 bug)变成 deepseek 下 13/13 全过、66s,但配置极坑 + token 记录不全。
- 测试:13/13 通过(glm_for_coding 下有个 DEFAULT_FILE 绑定 bug,换 deepseek 后自动消失)
- 速度快(66s),token 省(14.5K input)
- Charm 出品,UI 好看,但实用性一般
⚠️ 配置坑:
配置要点:
// ~/.config/crush/crush.json
{
\”providers\”: {
\”myagentlab\”: {
\”type\”: \”openai-compat\”,
\”base_url\”: \”https://api.myagentlab.homes/v1\”,
\”api_key\”: \”$MYAGENTLAB_API_KEY\”,
\”models\”: [ {
\”id\”: \”deepseek-v4-flash\”, \”name\”: \”DeepSeek V4 Flash\”, \”context_window\”: 128000, \”default_max_tokens\”: 8192 } ]
}
},
\”defaultModel\”: \”myagentlab/deepseek-v4-flash\”
}
MYAGENTLAB_API_KEY=<key> crush run -m myagentlab/deepseek-v4-flash \”任务\”
6. Cline — 修好代码后全过,但 CLI 有真 bug
原始 22/24(2 个代码 bug),手动修复后 24/24;但 CLI 二进制有 team_status 崩溃 bug。
- 原始测试:22/24。两个 bug:
- _next_id() 用现存任务 max+1,删除后 id 复用(测试要求永不复用)
- except KeyError: print(f\”Error: {exc}\”) 中 KeyError 的 str 自带引号
- 修复方法(已验证 24/24):① 加 self._max_id 实例属性持续递增 ② exc.args[0] 取干净消息
- ⚠️ CLI 真 bug:team_status 工具与 deepseek tool schema 校验随机崩溃(约 1/3 概率报 Invalid schema for function \’team_status\’: null is not of type \”array\”),是 Cline v3.0.49 二进制缺陷
- 缓解:~/.cline/data/settings/settings.json 设 \”enableTeams\”: false(不完全稳定)
- glm_for_coding 下无此问题(21/21 全过)
- CLI 安装配置最简单:cline auth openai -b <url> -k <key> -m <model> 一行搞定
- 真实场景是 VS Code 扩展,CLI 是轻量版
配置要点:
cline auth openai -b https://api.myagentlab.homes/v1 -k <key> -m deepseek-v4-flash
cline –json \”任务\”
7. Kilo Code — OpenCode 换皮但更费
与 OpenCode 同源(fork),功能几乎一样,但 token 消耗高(cache read 102K)且更慢。
- 测试:10/10 通过,还自己修了个 bug(self.done 属性遮蔽 done() 方法,改名 self.is_done)
- 配置和 OpenCode 几乎相同(kilo.jsonc)
- Token 分析:cache read 102K 是 7 个工具最高——它每次请求重发大量工具定义/上下文,但 cache 价格便宜(1/10)
- 结论:没理由选它,直接用 OpenCode 更好
配置要点:
// ~/.config/kilo/kilo.jsonc
{
\”provider\”: {
\”myagentlab\”: {
\”npm\”: \”@ai-sdk/openai-compatible\”,
\”options\”: {
\”baseURL\”: \”https://api.myagentlab.homes/v1\”, \”apiKey\”: \”见 credentials\” },
\”models\”: {
\”deepseek-v4-flash\”: {
\”name\”: \”deepseek-v4-flash\” } }
}}, \”model\”: \”myagentlab/deepseek-v4-flash\” }
kilo run -m myagentlab/deepseek-v4-flash \”任务\”
模型对比:glm_for_coding vs deepseek-v4-flash
| 首 token | 40-70s | 1.7s |
| 工具完成耗时 | 全部 7-9 分钟 | 32s-134s |
| Codex | ❌ 失败×4(heredoc 转义错乱) | ✅ 13/13 |
| Crush | ⚠️ 13/14(DEFAULT_FILE bug) | ✅ 13/13 |
| Cline | ✅ 21/21 | ✅ 24/24(修复后) |
| shell 生成质量 | 差(chr(39) 转义、heredoc 乱) | 干净 |
结论:deepseek-v4-flash 是当前网关下最佳通用编码模型(快 + 干净 + 便宜)。
最终推荐
- 日常开发首选:OpenCode + deepseek-v4-flash(最快最省,质量过关)
- 要最高质量:Claude Code + deepseek-v4-flash(最稳最严谨,但 token 大)
- 单轮/简单任务:Codex(能力强,但推理模型多轮有兼容隐患)
- 避免:Kilo(OpenCode 的劣化版)、Cline CLI(team_status 崩溃 bug 未修,等新版本)
安装记录(踩坑汇总)
| OpenCode | ghfast.top 镜像下载 v1.18.12 glibc 二进制 | GitHub 直连超时;npm 平台包是空壳 |
| Codex | npm @openai/codex | 无 |
| Goose | ghfast.top 镜像下载 v1.45.0 | 配置变量名坑(见上) |
| Crush | go install charmbracelet/crush | 配置文件坑(见上) |
| Cline | npm cline v3.0.49 | team_status 崩溃 bug |
| Kilo | npm @kilocode/cli v7.4.17 | 无 |
| Claude Code | 预装 v2.1.220 | root 不能用 –dangerously-skip-permissions |
网络环境备注:GitHub 直连慢/超时 → 用 https://ghfast.top/https://github.com/… 镜像;npm 用 –registry=https://registry.npmmirror.com;Go 用 GOPROXY=https://goproxy.cn,direct。
测试产物存放
所有测试代码、日志、token 数据保留在 /home/agent/coding-benchmark/:
- test-<工具名>/todo.py + test_todo.py — 各工具产物(可复验 pytest)
- REPORT-deepseek.md — 精简版报告
- /tmp/<工具>-ds.log — 运行日志
- 各工具 token 数据来源:OpenCode/Kilo 本地 SQLite、Goose sessions.db usage_ledger、Cline session JSON metrics、Crush .crush/crush.db、Claude Code stream-json usage
文档维护:CEO Agent,2026-08-04
部署与配置坑(详版)
以下均为本次实测真实踩坑记录(含报错原文)。配置代码见上文各工具详解;完整生成代码见文末 测试产物全集。 侧重点:这里是\”出了什么错、怎么解决\”,上文详解是\”怎么配\”。
Claude Code
- root 用户限制:–dangerously-skip-permissions 直接拒绝(“cannot be used with root/sudo privileges”),必须用 –allowedTools \’Bash,Write,Read,Edit\’
- 环境变量优先级:.bashrc 里的 ANTHROPIC_API_KEY/ANTHROPIC_BASE_URL export 覆盖 ~/.claude/settings.json——要换测试端点必须临时 export,不能只改 settings.json
- 生产配置隔离:测试时用 export 覆盖,不动 ~/.claude/settings.json 和 .bashrc
OpenCode
- GitHub 直连下载超时 → 用 https://ghfast.top/https://github.com/… 镜像下载 opencode-linux-x64.tar.gz(v1.18.12 glibc)
- npm opencode 包 404;@opencode-ai/cli 平台包是空壳——必须下二进制
- 曾遇 EEXIST bug:/tmp/opencode 被 musl 二进制留成文件,glibc 版 mkdir 失败 → rm /tmp/opencode 解决
- opencode version 不是子命令,TUI 是默认命令
Codex
- 配置 ~/.codex/config.toml:wire_api = \”chat\”(OpenAI 兼容),env_key = \”OPENAI_API_KEY\”
- 运行:codex exec –dangerously-bypass-approvals-and-sandbox –skip-git-repo-check –json
- 推理模型隐患:deepseek 是推理模型,Codex 多轮对话触发 reasoning_content 必须回传 400——复杂任务会挂,单轮任务 OK
- token 不落本地:session jsonl 无 usage 字段
Goose(坑最多)
- 变量名坑:必须用 OPENAI_API_KEY/OPENAI_BASE_URL(不是 GOOSE_API_KEY/GOOSE_BASE_URL!)——用错连默认 api.openai.com 必然超时
- 流式坑:默认流式报 Stream decode error → declarative provider 必须设 \”supports_streaming\”: false
- 自定义 provider 路径:~/.config/goose/custom_providers/<name>.json
- 诊断方法:本地 SQLite ~/.local/share/goose/sessions/sessions.db 的 messages 表看请求是否真的发出去(之前只存 user 消息 = 请求没到模型)
Crush
- 配置文件坑:正确文件名是 ~/.config/crush/crush.json(不是 config.json!改 config.json 完全不生效,排查半天)
- models 必须是数组不是对象(对象结构报 “large model not found”)
- crush update-providers 会覆盖 providers.json 手动修改(远程拉 40 个 provider)
- -m provider/model 格式(如 -m myagentlab/deepseek-v4-flash)
- token 记录不全:crush.db sessions 表只有 prompt/completion/cost,无 cache_read
Cline
- 配置最简单:cline auth openai -b <url> -k <key> -m <model> 一行
- ⚠️ CLI 二进制 bug:team_status 工具与 deepseek tool schema 校验随机崩溃(~1/3 概率 Invalid schema for function \’team_status\’: null is not of type \”array\”)
- 缓解:~/.cline/data/settings/settings.json 设 \”enableTeams\”: false(不完全稳定)
- glm_for_coding 下无此问题
- session 数据在 ~/.cline/data/sessions/<id>/,token 在 messages.json 的 assistant 消息 metrics 字段
Kilo Code
- 配置 ~/.config/kilo/kilo.jsonc(opencode fork,格式几乎相同)
- kilo run -m myagentlab/deepseek-v4-flash \”任务\”
- token 在 ~/.local/share/kilo/kilo.db session 表
Token 数据证据链(每个数字从哪来)
| Claude Code | 227,467 | 190,976 | 12,435 | /tmp/cc-ds.log 最后一行 \”usage\”:{…}(stream-json 输出) |
| OpenCode | 11,749 | 40,576 | 1,992 | ~/.local/share/opencode/opencode.db session 表 tokens_input/output/cache_read |
| Codex | ~132K (估) | — | ~12K (估) | 本地不记录;按 6 轮请求×累积上下文估算,成本 ~$0.027 |
| Goose | 80,848 | 43,776 | 6,769 | ~/.local/share/goose/sessions/sessions.db usage_ledger 表 SUM() |
| Crush | 14,499 | 无此字段 | 40* | ~/.crush/crush.db sessions 表 prompt_tokens/completion_tokens |
| Cline | 11,234 | 5,632 | 6,033 | ~/.cline/data/sessions/<id>/<id>.messages.json assistant metrics 累加 |
| Kilo | 15,488 | 102,272 | 2,291 | ~/.local/share/kilo/kilo.db session 表 |
* Crush completion_tokens=40 疑似只记末次请求(实际生成 124 行代码),已按实际产物修正理解
复核命令示例(OpenCode):
SELECT tokens_input, tokens_output, tokens_cache_read FROM session
WHERE model LIKE \’%deepseek%\’ ORDER BY time_created DESC LIMIT 1;
复核命令示例(Goose):
SELECT session_id, SUM(input_tokens), SUM(output_tokens), SUM(cache_read_tokens)
FROM usage_ledger GROUP BY session_id ORDER BY MIN(id) DESC LIMIT 2;
复核命令示例(Claude Code):
grep -oE \’\”usage\”:\\{[^}]*\\}\’ /tmp/cc-ds.log | tail -1
为什么 Codex 没有精确值: 本地 session jsonl 无 usage 字段;网关 billing 接口只给总额(共享 key,后台 MRS 任务并发消耗,差值法不可信);唯一精确途径是 new-api 后台按渠道查明细。
附:测试产物全集(7 份完整代码 + pytest 输出)
以下为 7 款工具在统一测试题下实际生成的全部代码 + pytest 复验输出(机器实跑,非手写)。 复验命令:/tmp/bench-venv/bin/python -m pytest test_todo.py -v(产物目录 /home/agent/coding-benchmark/test-<工具名>/)
本页存放 7 款工具在统一测试题下实际生成的全部代码 + pytest 复验输出。
复验命令:/tmp/bench-venv/bin/python -m pytest test_todo.py -v(产物目录 /home/agent/coding-benchmark/test-<工具名>/)
Claude Code v2.1.220 — 22/22
Claude Code 生成的 todo.py (4619 bytes)
#!/usr/bin/env python3
\”\”\”A simple command-line todo app with JSON persistence.
Usage:
todo.py add <task> Add a new task
todo.py list List all tasks
todo.py done <id> Mark a task as done
todo.py delete <id> Delete a task
todo.py –file <path> Use a custom JSON file (default: ~/.todo.json)
Tasks are stored in a JSON file of the form:
{\”next_id\”: 2, \”todos\”: [{\”id\”: 1, \”task\”: \”…\”, \”done\”: false}]}
The “next_id“ counter is persisted so task IDs are never reused, even
after a delete. This prevents a stale reference from completing or
deleting the wrong task later.
\”\”\”
import argparse
import json
import os
import sys
DEFAULT_FILE = os.path.join(os.path.expanduser(\”~\”), \”.todo.json\”)
class TodoApp:
\”\”\”Todo list backed by a JSON file.\”\”\”
def __init__(self, filepath=DEFAULT_FILE):
self.filepath = filepath
def load(self):
\”\”\”Return the store (“next_id“ counter + “todos“ list).\”\”\”
if not os.path.exists(self.filepath):
return {
\”next_id\”: 1, \”todos\”: []}
with open(self.filepath, \”r\”) as f:
content = f.read()
if not content.strip():
# Empty file (e.g. created by touch/mktemp) means a fresh store.
return {
\”next_id\”: 1, \”todos\”: []}
data = json.loads(content)
# Backwards compatibility: a bare list of todos.
if isinstance(data, list):
data = {
\”next_id\”: max((t[\”id\”] for t in data), default=0) + 1,
\”todos\”: data,
}
return data
def save(self, data):
\”\”\”Write the store to the JSON file.\”\”\”
with open(self.filepath, \”w\”) as f:
json.dump(data, f, indent=2)
def _allocate_id(self, data):
next_id = data[\”next_id\”]
data[\”next_id\”] = next_id + 1
return next_id
def add(self, task):
\”\”\”Add a new task and return the created todo.\”\”\”
data = self.load()
todo = {
\”id\”: self._allocate_id(data), \”task\”: task, \”done\”: False}
data[\”todos\”].append(todo)
self.save(data)
return todo
def list_todos(self):
\”\”\”Return all todos in insertion order.\”\”\”
return self.load()[\”todos\”]
def done(self, todo_id):
\”\”\”Mark a todo as done and return it, or None if not found.\”\”\”
data = self.load()
for todo in data[\”todos\”]:
if todo[\”id\”] == todo_id:
todo[\”done\”] = True
self.save(data)
return todo
return None
def delete(self, todo_id):
\”\”\”Delete a todo and return it, or None if not found.\”\”\”
data = self.load()
for i, todo in enumerate(data[\”todos\”]):
if todo[\”id\”] == todo_id:
removed = data[\”todos\”].pop(i)
self.save(data)
return removed
return None
def main(argv=None):
parser = argparse.ArgumentParser(description=\”A simple todo app\”)
parser.add_argument(
\”–file\”, default=DEFAULT_FILE, help=\”JSON file to store todos\”
)
subparsers = parser.add_subparsers(dest=\”command\”, required=True)
add_parser = subparsers.add_parser(\”add\”, help=\”Add a task\”)
add_parser.add_argument(\”task\”, help=\”Task description\”)
subparsers.add_parser(\”list\”, help=\”List all tasks\”)
done_parser = subparsers.add_parser(\”done\”, help=\”Mark a task as done\”)
done_parser.add_argument(\”id\”, type=int, help=\”Task ID\”)
delete_parser = subparsers.add_parser(\”delete\”, help=\”Delete a task\”)
delete_parser.add_argument(\”id\”, type=int, help=\”Task ID\”)
args = parser.parse_args(argv)
app = TodoApp(args.file)
if args.command == \”add\”:
todo = app.add(args.task)
print(f\”Added todo #{
todo[\’id\’]}: {
todo[\’task\’]}\”)
elif args.command == \”list\”:
todos = app.list_todos()
if not todos:
print(\”No todos.\”)
return 0
for todo in todos:
status = \”[x]\” if todo[\”done\”] else \”[ ]\”
print(f\”{
todo[\’id\’]}. {
status} {
todo[\’task\’]}\”)
elif args.command == \”done\”:
todo = app.done(args.id)
if todo is None:
print(f\”Todo #{
args.id} not found.\”, file=sys.stderr)
return 1
print(f\”Done: {
todo[\’task\’]}\”)
elif args.command == \”delete\”:
todo = app.delete(args.id)
if todo is None:
print(f\”Todo #{
args.id} not found.\”, file=sys.stderr)
return 1
print(f\”Deleted: {
todo[\’task\’]}\”)
return 0
if __name__ == \”__main__\”:
sys.exit(main())
Claude Code 生成的 test_todo.py (5048 bytes)
\”\”\”Unit tests for the todo app.\”\”\”
import json
import pytest
from todo import TodoApp, main
@pytest.fixture
def app(tmp_path):
return TodoApp(filepath=tmp_path / \”todos.json\”)
# — add ——————————————————————
def test_add_returns_created_todo(app):
todo = app.add(\”Buy milk\”)
assert todo == {
\”id\”: 1, \”task\”: \”Buy milk\”, \”done\”: False}
def test_add_persists_to_json(app, tmp_path):
app.add(\”Write tests\”)
data = json.loads((tmp_path / \”todos.json\”).read_text())
assert len(data[\”todos\”]) == 1
assert data[\”todos\”][0][\”task\”] == \”Write tests\”
assert data[\”todos\”][0][\”done\”] is False
def test_add_persists_next_id_counter(app, tmp_path):
app.add(\”first\”)
app.add(\”second\”)
data = json.loads((tmp_path / \”todos.json\”).read_text())
assert data[\”next_id\”] == 3
def test_add_increments_ids(app):
app.add(\”first\”)
second = app.add(\”second\”)
assert second[\”id\”] == 2
assert [t[\”id\”] for t in app.list_todos()] == [1, 2]
def test_add_after_delete_does_not_reuse_id(app):
first = app.add(\”first\”)
app.delete(first[\”id\”])
third = app.add(\”third\”)
assert third[\”id\”] == 2
assert [t[\”id\”] for t in app.list_todos()] == [2]
def test_ids_are_monotonic_across_restarts(tmp_path):
filepath = tmp_path / \”todos.json\”
app1 = TodoApp(filepath=filepath)
first = app1.add(\”first\”)
app2 = TodoApp(filepath=filepath)
app2.delete(first[\”id\”])
third = app2.add(\”third\”)
assert third[\”id\”] == 2
# — list —————————————————————–
def test_list_empty_when_no_file(tmp_path):
app = TodoApp(filepath=tmp_path / \”nonexistent.json\”)
assert app.list_todos() == []
def test_list_empty_when_file_is_empty(tmp_path):
filepath = tmp_path / \”todos.json\”
filepath.write_text(\”\”)
app = TodoApp(filepath=filepath)
assert app.list_todos() == []
def test_add_over_empty_file(tmp_path):
filepath = tmp_path / \”todos.json\”
filepath.write_text(\”\”

