为什么:自动化建筑只有水井和喂食器,中期以后每天的操作只剩浇水喂食 的重复劳动。新设施把"替代行动点动作"的思路推到完整流水线: 浇水(井/喷灌)→ 收割机收熟地 → 磨坊把陈粮变饲料 → 喂食器喂牲口 → 售货机趁高价清库存,人不在也能持续产出。 - 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 全绿。
150 lines
8.7 KiB
Markdown
150 lines
8.7 KiB
Markdown
---
|
||
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 上第二个实例能绑上已被监听的端口并抢走新连接。
|