Files
SimpleFarm/.workbuddy/skills/simplefarm-verify-and-ship/SKILL.md
T
snowy-yang 6268283fc6 chore: 存一个「改完怎么验收怎么交付」的技能,省得下次重新踩
本次踩到两个纯靠猜出不来的坑,都写进技能里了:

* `RemoteFarm.snapshot()` 是本地缓存不发请求,非实时脚本轮询会一直读到同一份
  旧数据——服务器在推进却看起来像卡死,排查花了十几分钟。
* `git push` 报远端 `Failed to authenticate user` 其实是本地 GCM 拒绝交互
  (`Cannot prompt because user interactivity has been disabled`),
  加 `GCM_INTERACTIVE=always` 就好;再不行 `GCM_TRACE=1` 看日志。
  验证推送成功要看 `git ls-remote`,`git status -sb` 的 `[gone]` 是沙箱
  丢 `.git/refs/remotes/**` 写入导致的假象。

技能放在 .workbuddy/skills/simplefarm-verify-and-ship/,覆盖四步交付流程:
门禁 → 真机端到端验收(预置存档 + 后台任务起服务器)→ 写记忆 → 提交推送。
2026-09-19 17:06:15 +08:00

98 lines
5.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: simplefarm-verify-and-ship
description: Use when finishing ANY change to the SimpleFarm project (D:/SimpleFarm) — after adding or refactoring features, before declaring the task done or pushing to gitea.4i.hk. Covers the four-step delivery ritual — run the quality gate (ruff format/check + pyright + unittest), verify the change end-to-end on a REAL server with seeded saves, write memory notes, then commit and push. Triggers include "改完了", "验收", "跑测试", "提交推送", "推送到 gitea", or any turn where farm/*.py was edited. Also load it when a test fails after a behaviour change, when the server seems frozen, or when `git push` says "Failed to authenticate user".
agent_created: true
---
# SimpleFarm:改完之后的验收与交付
任何一次改动,走完这四步才算做完。别跳过第 2 步——单元测试全绿**不代表**功能真的能跑
(本项目已经出现过:测试全绿,但真实服务器上 `--pid-file pid.txt` 直接崩、
非实时客户端轮询永远读到旧状态)。
## 第 1 步:质量门禁(唯一可信口径)
```bash
uv run ruff format . && uv run ruff check . && uv run pyright
uv run python -m unittest discover -s tests
uv run python tools/bench.py --repeat 5 # 只有碰了热路径才需要
```
预期:`All checks passed!` / `0 errors, 0 warnings` / `OK (skipped=35)`。
`skipped=35` 是没装 httpx 时 `TestClient` 那批用例的正常跳过,不是失败。
bench 里带调用计数器,计数变红说明轻量路径被改回重路径了。
**别用管道**:本机 Git Bash 缺 `head`/`tail`/`grep`/`ls`/`mkdir`,任何 `|` 都会整条失败。
查看/删除文件一律写 Python 脚本。
## 第 2 步:真机端到端验收
单元测试覆盖不到的,是"服务器起来了吗、接口真的返回新字段了吗"。必须起真服务器验。
**关键:服务器要用后台任务跑,别用 `--daemon`。** 脱离父进程的守护进程会随工具调用
结束被回收,下一条命令就连不上了(表现为 `ConnectionRefusedError`)。用
`run_in_background: true` 跑前台服务器,验完再停掉。
```bash
# 1) 预置存档:想验"买得起"的路径,就先塞一份有钱的档。
# farm_id 直接映射到 <save-dir>/<farm_id>.json;字段缺失一律走默认值,
# 所以最小延迟 {version:2, state:{gold,level}} 就够。
# 必须在同一条命令里 mkdir -p 好目录再写文件。
# 2) 起服务器(tick 小一点,3 秒一天)
uv run main.py serve --port 8799 --tick 3 --save-dir .e2e-saves
# 3) 用 RemoteFarm 驱动,跑完删掉脚本和 .e2e-saves/
```
**两个必须知道的坑:**
- **`RemoteFarm.snapshot()` 是本地缓存,不发请求。** 脚本里写
`while True: client.snapshot()` 会永远读到同一份旧数据——服务器在推进,
看起来却像卡死。非实时客户端必须自己调 `client.refresh()` 再读 `snapshot()`。
TUI/网页走 SSE 所以没这个问题。
- **作物会缺水停长。** 天气随机消耗水分(晴 1 / 多云 0 / 干旱 2),
等成熟的循环里要盯水分补浇,否则 120 秒也等不到成熟。这也顺带证明了
天气→浇水这条链真的在起作用。
验收脚本该断言什么:新字段真的出现在 `/api/state` 和 `/api/catalog` 里、
动作回执带新字段、错误码符合预期(如 `nothing_to_sell` / `locked` / `unknown_building`)。
错误码去 `farm/engine.py` 里核对,别猜。
## 第 3 步:写记忆
- 当天日志追加到 `.workbuddy/memory/YYYY-MM-DD.md`(append-only)。
- 长期约定改 `.workbuddy/memory/MEMORY.md`(原地编辑)。
- 用 `read_bytes()` / `write_bytes()` 保 LF;同一个文件**一条消息里改多处**时,
写一个幂等补丁脚本(整文件读入 → 精确匹配替换 → 逐条打印 `ALREADY`/`APPLIED`/`MISS`),
跑完即删。**不要**在一条消息里对同一文件连发多个 Edit——会被静默覆盖。
## 第 4 步:提交并推送
```bash
git add -A && git commit -F - <<'MSG'
...多行中文 commit message...
MSG
GCM_INTERACTIVE=always git push origin main
```
- **`GCM_INTERACTIVE=always` 是必需的。** 默认(非交互)下 GCM 直接拒绝
`Cannot prompt because user interactivity has been disabled`,
报错却显示成远端的 `Failed to authenticate user`,很容易误判成 token 失效。
加上它之后 GCM 会静默续期。
- 还是失败就加 `GCM_TRACE=1` 重新推一次,日志会明确说
`No existing credentials found` 还是拿到了 token。别急着去翻 tea 的
`credentials.json.enc`(加密的,读不出来)。
- **验证推送成功看 `git ls-remote origin`**,不要看 `git status -sb`——
沙箱会丢掉 `.git/refs/remotes/**` 新建目录的写入,status 会谎报
`## main...origin/main [gone]`。`ls-remote` 的 sha 与 `git rev-parse HEAD`
一致就是推上去了,`[gone]` 是无害的本地缓存问题。
## 别踩的坑(本机特有)
- `os.makedirs(os.path.dirname(path))` 在 path 只有文件名时会崩(dirname 是空串)。
用 `farm/server/daemon.py` 的 `ensure_parent_dir()`。
- 写文件必须显式保 LF(`write_bytes` 或 `newline="\n"`),别让 `\n` 被转成 `\r\n`。
- 访问本机 HTTP 走 `farm/net.py` 的 `open_url()`,环境里有 `HTTP_PROXY` 但没有
`no_proxy`,直连会被代理拦成 502。
- 端口自检用"能不能连上"判断,别用 bind;uvicorn 设了 SO_REUSEADDR,
Windows 上第二个实例能绑上已被监听的端口并抢走新连接。