Files
SimpleFarm/.workbuddy/skills/simplefarm-verify-and-ship/SKILL.md
T
snowy-yang cb4c1e7800 feat(engine): 四座自动化设施——喷灌 / 收割机 / 售货机 / 磨坊
为什么:自动化建筑只有水井和喂食器,中期以后每天的操作只剩浇水喂食
的重复劳动。新设施把"替代行动点动作"的思路推到完整流水线:
浇水(井/喷灌)→ 收割机收熟地 → 磨坊把陈粮变饲料 → 喂食器喂牲口
→ 售货机趁高价清库存,人不在也能持续产出。

- config.BUILDINGS 注册 4 座:喷灌系统 2800 金(全量浇水)、
  自动收割机 900 金 ×3(每天各收 2 块熟地,刻意不给经验——机器收的
  不算劳动,给手动玩法留价值)、自动售货机 1200 金(市价 > 基准价才
  出手、每天上限 5 份、越贵越先卖)、饲料磨坊 500 金(1 份最便宜的
  作物磨 3 份饲料)。
- engine._run_buildings 扩成六段流水线,顺序即依赖:磨坊排在喂食器前,
  早上磨的饲料当天就能喂上;收割机遇满仓停摆、作物留地(同喂食器
  缺料停摆哲学——自动系统绝不硬来,更绝不自动花钱)。
- webui / build 动作 / catalog 全部由 config.BUILDINGS 驱动,零改动。
- 测试 +6(喷灌全量浇水、收割入库且无经验、满仓停摆留作物、磨坊挑
  最便宜、磨坊→喂食器同日链路、售货机阈值与配额)。修复断言时确认
  两个既有行为:引擎自带 START_FEED=5;advance_day 末尾会把动物 fed
  复位("喂到了"的证据是 pending 累计而非 fed 本身)。
- 门禁:534 项 OK(skip 39)、ruff/pyright 全绿。
2026-09-25 21:05:27 +08:00

150 lines
8.7 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) 预置存档(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 才看一眼,等于永不存盘,全量测试照样绿。
**动了 webui 前端必须加验一条**:API 正常 ≠ 页面能用(window.prompt 被
sandbox 屏蔽、中文用户名炸 fetch 头,都是 API 测不出来的)。真浏览器验收:
```bash
# 1) 起 headless Chrome(后台任务保活;--executable-path 直起会 exit 0 连不上)
uv run python -c "
import subprocess, time
subprocess.Popen([r'C:\Program Files\Google\Chrome\Application\chrome.exe',
'--headless=new','--remote-debugging-port=9333',
'--user-data-dir='+__import__('os').environ['TEMP']+r'\abchrome',
'--no-first-run','about:blank'])
while True: time.sleep(3600)"
# 2) agent-browser 走 CDP 连接(每条命令都要带 --cdp)
PATH="/c/Program Files/nodejs:$PATH" agent-browser --cdp 9333 open http://127.0.0.1:8765/
PATH="/c/Program Files/nodejs:$PATH" agent-browser --cdp 9333 eval "..." # 页面内点按钮、读 toast
```
注意:系统 Chromium(Chrome 153+ / Edge)`--executable-path` 模式起不来,
必须自己起进程再 `--cdp` 连;`find text "登录"` 会点到标题而非按钮,
用 `eval` 精确 `document.getElementById('dologin').click()`。
## 第 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
NO_PROXY=gitea.4i.hk no_proxy=gitea.4i.hk GCM_INTERACTIVE=always git push origin main
```
- **`NO_PROXY=gitea.4i.hk`(大小写都要)是必需的。** 环境代理对它返回 502,
直连秒通。缺了这个会误判成认证问题。
- **push 卡死几分钟不动 = system 级 `credential.helper=helper-selector` 在
等人点选择框**(tasklist 里能看到 git-credential-helper-selector.exe)。
已修:global 里用**空 helper 重置列表**顶掉它,再依次挂 wincred 和 GCM:
```bash
git config --global --get-all credential.helper
# 应该是(顺序重要):空串 → wincred → GCM
# "" 、 !"C:/Users/Administrator/.workbuddy/binaries/PortableGit/versions/1.2.0/mingw64/bin/git-credential-wincred.exe"
# 、 !"C:/Program Files/Git/mingw64/bin/git-credential-manager.exe"
```
GCM 路径**必须带 `!` 前缀和引号**(`!"C:/Program Files/..."`)——
否则空格截断成 `C:/Program`,报 "No such file or directory"。
- **`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:https://refresh_token.gitea.4i.hk`(GCM 的刷新令牌)。
- **验证推送成功看 `git ls-remote origin`**,不要看 `git status -sb`——
沙箱会丢掉 `.git/refs/remotes/**` 新建目录的写入,status 会谎报
`## main...origin/main [gone]`。`ls-remote` 的 sha 与 `git rev-parse HEAD`
一致就是推上去了,`[gone]` 是无害的本地缓存问题。
- 被杀掉的 push 会留下 git.exe / git-credential-manager.exe 僵尸进程,
占住 GCM 的单例锁让后续 push 排队挂死。推送反复挂时先
`taskkill /F /IM git.exe` 等清场再试。
## 别踩的坑(本机特有)
- `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 上第二个实例能绑上已被监听的端口并抢走新连接。