单元测试抓不住 autosave 失效(dirty 被高频清零这件事测试里复现 不了真机时序),所以技能里加一条:动了存档/会话层,必须 0 客户端 在线做一个动作、等 60s、直接查库那一行有没有更新。
6.4 KiB
name, description, agent_created
| name | description | agent_created |
|---|---|---|
| simplefarm-verify-and-ship | 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". | true |
SimpleFarm:改完之后的验收与交付
任何一次改动,走完这四步才算做完。别跳过第 2 步——单元测试全绿不代表功能真的能跑
(本项目已经出现过:测试全绿,但真实服务器上 --pid-file pid.txt 直接崩、
非实时客户端轮询永远读到旧状态)。
第 1 步:质量门禁(唯一可信口径)
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 跑前台服务器,验完再停掉。
# 1) 预置存档(SQLite,v2 起):想验"买得起"的路径,就先塞一行有钱的档。
# python -c "
# from farm.models import GameState
# from farm.storage import save_state
# st = GameState(); st.gold = 5000; st.level = 5
# save_state(st, '.e2e-saves/farm.db', 'boss')"
# 字段缺失一律走默认值;老 JSON 档也可以直接丢进 --save-dir,启动时自动迁移。
# 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 所以没这个问题。- 端口上可能有别人的服务器。 8765 是默认端口,用户自己(或另一个 checkout)
起的实例可能正在上面跑、里面还有玩家进度。动手前先看
/api/farms里有没有 不认识的活跃牧场;有就换端口验,别 stop 它。判断一个实例是不是本工作区 的代码,看它的存档目录在不在这个项目里(saves/farm.db的 mtime 动不动)。 - 作物会缺水停长。 天气随机消耗水分(晴 1 / 多云 0 / 干旱 2), 等成熟的循环里要盯水分补浇,否则 120 秒也等不到成熟。这也顺带证明了 天气→浇水这条链真的在起作用。
验收脚本该断言什么:新字段真的出现在 /api/state 和 /api/catalog 里、
动作回执带新字段、错误码符合预期(如 nothing_to_sell / locked / unknown_building)。
错误码去 farm/engine.py 里核对,别猜。
动了存档/会话层必须加验一条:只走 HTTP(不连 SSE、0 客户端在线)做一个动作,
等 60 秒自动存档周期过去,直接查 saves/farm.db 那一行有没有更新。
单元测试抓不住 autosave 失效——2026-09-23 就漏过一次:dirty 被推送循环高频清零,
autosave 60s 才看一眼,等于永不存盘,全量测试照样绿。
第 3 步:写记忆
- 当天日志追加到
.workbuddy/memory/YYYY-MM-DD.md(append-only)。 - 长期约定改
.workbuddy/memory/MEMORY.md(原地编辑)。 - 用
read_bytes()/write_bytes()保 LF;同一个文件一条消息里改多处时, 写一个幂等补丁脚本(整文件读入 → 精确匹配替换 → 逐条打印ALREADY/APPLIED/MISS), 跑完即删。不要在一条消息里对同一文件连发多个 Edit——会被静默覆盖。
第 4 步:提交并推送
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 上第二个实例能绑上已被监听的端口并抢走新连接。