# 自动化测试流程(AI 迭代闭环) > 目标:AI 改完代码后,可自动完成「构建 → 烧录 → 抓取串口调试信息 → 校验 → 迭代」,无需人工开终端/串口助手。 ## 总体流程 ``` @test 命令触发 │ ├─ Step 0 预检 .venv + serial_tool(selftest/ports 确认 COM) ├─ Step 1 构建验证 keil-build skill(keil_build.py) (0 错误 0 警告才继续) ├─ Step 2 烧录 + 采集(一条命令编排:开串口 + 清日志 → 烧录 → 固件身份戳 → wait-idle → 提取 → 回读校验) │ .dsh/skills/serial-boot-test/scripts/run_boot_test.py (内置 serial_tool + stlink-flash skill 的 flash/cmp + verify_log.py) ├─ Step 3 判定: │ PASS → 报告,结束 │ FAIL → 依据 [MISS]/[HIT] + 日志尾部,修改代码后回到 Step 1 ``` ## 命令与脚本清单 | 名称 | 位置 | 作用 | |---|---|---| | `@build` | `keil-build` skill(`.dsh/skills/keil-build/`,自带 `keil_build.py`) | 仅编译验证(0 错误 0 警告) | | `@download` | `stlink-flash` skill(`.dsh/skills/stlink-flash/`,自带 `stlink_flash.py`) | 编译并烧录(ST-Link) | | `@test` | `serial-boot-test` skill(`.dsh/skills/serial-boot-test/`) | 完整闭环:build → capture+download → verify → iterate | | `.dsh/skills/serial-boot-test/scripts/run_boot_test.py` | `.dsh/skills/serial-boot-test/scripts/run_boot_test.py` | **一键闭环**:open + clear → 烧录 → 固件身份戳(从 hex 抠 `fw build:`)→ wait-idle(静默 8s/超时 30s/首字节 12s)→ 提取 `[RX]`(每行加 `[+Nms]` 时基)→ 回读校验(`stlink_flash.py cmp`)→ verify;`--keep-out` 清旧产物;退出码 0=PASS/1=FAIL/2=采集·烧录·回读错误/3=用法错误 | | `.dsh/skills/stlink-flash/scripts/stlink_flash.py` | `stlink-flash` skill | ST-LINK CLI 封装:`flash [hex]`(烧 + `-Rst -Run`)/ **`cmp [hex]`(回读比对,2026-10-10 加)**/ `reset`(只复位)/ `hard`(nRST 脉冲,CLI 拒 `RM=` 自动降级)/ `halt`/`run`/`list`;原 `MDK-ARM/reset.bat` 已于 2026-10-10 删除并全并入本 skill | | `.dsh/skills/serial-assistant/scripts/serial_tool.py` | `.dsh/skills/serial-assistant/` | Python 串口助手(常驻 daemon + 薄 CLI):ports/open/send/status/log/clear/wait-idle/selftest;全量日志 `state/serial.log`(20MB 自动截头) | | `.dsh/skills/serial-boot-test/scripts/verify_log.py` | `.dsh/skills/serial-boot-test/scripts/verify_log.py` | 对日志做标记校验:Required/Forbidden(**命中逐行打印行号+内容**)、启动锚点计数 `--max-boot-count`、原因白名单 `--expect-reset-cause`、标记顺序 `--require-order`、开机耗时 `--done-marker`/`--max-boot-ms`、`--json` 结构化输出、**整行锚定 `--exact` / 跨复位配对 `--pair` / 崩溃腿 `--allow-residual`+`--head-char` / 判别自检 `--selftest`(22 条,含三计数分类器)**;退出码 **0=PASS / 1=设备侧真有坏消息 / 2=测量没成立**(读不到日志,或开了锚点计数却零锚点——采集断了不算设备故障)。子串 Required 照不出"行首被吃掉"那一类行级损坏 → 行级完整性一律用 `--exact`,口径与固定动作见 `启动日志原子性复验流程.md` | ## 判定标准 - **构建**:`@build` 退出码 0(0 错误 0 警告),否则不进入硬件环节。 - **烧录**:`@download` 退出码 0(`run_boot_test.py` 内部调用)。 - **日志**:`verify_log.py` 退出码 - `0` = PASS:所有 `-Required` 标记均出现,且无 `-Forbidden` 模式命中 - `1` = FAIL:存在缺失标记(`[MISS]`)或违规模式(`[HIT]`) - `2` = 日志文件不可读 - **固件身份戳(2026-10-10 加)**:`App/app/app_main.c` 开机打印 `fw build: <__DATE__> <__TIME__>`(编译期展开成一段**连续 ASCII**,`.hex` 里可原样搜到)。`run_boot_test.py` 从**待烧的 hex** 里抠出该串并追加成 Required 标记 → 芯片里跑的必须就是这份构建。反例实测:把 hex 里的戳改成 `Oct 9 2026 01:00:00` 再跑,其余标记全中但 `[MISS] fw build: Oct 9 2026 01:00:00` → FAIL——这正是过去"反复重下载、芯片一直是旧镜像"在闭环里**完全隐形**的场景。`--no-fw-check` 关闭;hex 里没有该串(旧构建)只 WARN 跳过、不假判。 - **戳覆盖构建配置(2026-10-11 加)**:`__DATE__`/`__TIME__` 只在**该 TU 被重编**时才刷新——增量构建下只切 `test/board/test_config.h` 的套件宏**不会**重编 `app_main.c`,于是产品镜像与测试镜像顶着同一个戳(实测:切宏后的测试构建与产品构建戳同为 `Oct 11 2026 14:44:11`,只有回读校验才发现镜像不同)。两条防线:① `app_main.c` `#include "test_config.h"` 建立头依赖 → 改宏必重编本 TU、戳必刷新;② 戳串追加构建变体 ` suite= dstr=`(`FW_BUILD_VARIANT`)→ 产品 = `suite=NONE dstr=off`、CH395F 测试 = `suite=CH395F dstr=on`,**即便戳时间相同也能一眼分辨**。实测:切宏前后 `Oct 11 2026 20:36:11 suite=NONE dstr=off` → `Oct 11 2026 20:36:40 suite=CH395F dstr=on`;`@test` 闭环对新格式匹配通过(Required 行就是完整戳串,脚本按"标记后连续可打印 ASCII"抠取,无需改脚本)。 - **烧录回读(2026-10-10 加)**:采集结束后自动跑 `stlink_flash.py cmp`(= `-CmpFile` 全片逐字节比对),判定按**行首** `No difference found`(进度条与帮助文本里也含该短语,故不能全文匹配);不一致 → `RUN_TEST_ERROR: flash readback mismatch` + 退出码 2(本次运行作废:日志是"某个未知镜像"跑出来的)。⚠ **顺序不能颠倒**——`-CmpFile` 连接时会复位目标(实测板子当场重打一遍开机日志)。`--no-verify-flash` 关闭。 - **启动锚点计数(2026-10-10 加)**:`reset cause:` 在一次采集里出现 **0 次**(采集没从复位开始,标记全中也可能只是上一轮残留)或 **>1 次**(板子中途自己复位:IWDG 咬狗 / Fault 后 `NVIC_SystemReset` / 掉电重启)→ FAIL;`--max-boot-count N` 放宽(跑故意复位的套件时用),`--expect-reset-cause 'software;power-on'` 对第一个锚点做原因白名单(SWD 软复位实测为 `software (NVIC_SystemReset)`,CSR=0x14000000)。 - **判定可读性 + 时序(2026-10-10 第三批)**: - **Forbidden 命中行**:命中时逐行打印 `L<行号>: <内容>`(最多 6 行,剩余报数)。旧实现只说 `forbidden pattern: [ERR] (3 line(s))`,还得人再 grep 一遍才知道是哪几行。 - **`[+Nms]` 相对时基**:`run_boot_test.py` 给 boot_log 每行加行首 `[+Nms]`(daemon 毫秒时基,本次第一行 = +0ms;`--raw-log` 出纯载荷)。MCU 侧时间戳只有秒级、开机头几行还没时钟(`----/--/--`),量不了耗时。 - **开机耗时**:`--done-marker 'net init: OK'`(默认)+ `--max-boot-ms N`。实测基线 **+537 ms**(2026-10-10 台架,`stlink_flash.py flash` 后);默认 `--max-boot-ms 0` = 只报告不判,避免网络重试抖动造成假 FAIL。 - **标记顺序**:`--require-order 'App init OK>Network Init>net init: OK'` 按首次出现的**行序**校验;标记全中但顺序颠倒(屏障失序 / 重试补跑)此前完全不可见。 - **`--json`**:同一套判定以 JSON 输出(`required_missing` / `forbidden_hits[].lines` / `boot` / `timing` / `tail` / `verdict` / `exit`),退出码语义不变,供 CI 与上层脚本消费。 - **产物保留**:`--keep-out N`(默认 20)自动清理 `--out-dir` 里的旧 `boot_log_*.txt` / `selftest_log_*.log`(本机曾堆到 31+7 份)。 ### 自检结果判定(S7,`--require-selftest-pass`) `run_boot_test.py` / `verify_log.py` 支持 `--require-selftest-pass`:除串口标记外,还判定设备侧自检结论 (v1 只记故障、不记通过)。判定对象(20 处自检描述串)见 `实机测试实施指南.md` §3.2。 **判据默认只看「本次启动新增」**(`--selftest-scope this-run`,2026-10-22 改)。原因: `/log/selftest.log` 是 **512 条环形累计历史**,而旧判据是"文件非空即 FAIL"——用它判一次启动, 在任何做过故障注入或长跑的设备上**永远不可能 PASS**(本仓台架实测:抓到的 29 条记录时间戳 全是 2026-10-10 ~ 10-14 留下的,没有一条来自本次)。新流程 = 烧录**前**先经 FTP 取一次基线、 启动后再取一次,只把"基线最后一条之后"的记录交给判定: | 情形 | 含义 | 判定 | |---|---|---| | 设备侧无此文件(450/550) | 台账干净("仅故障才写"语义下本就可能不存在) | 无新增 → **PASS** | | 抓到文件、本次无新增 | 本次启动没产生故障 | **PASS** | | 抓到文件、本次有新增 | 本次启动有故障 | **FAIL**,只打印新增条目(落 `selftest_log_<时刻>_new.log`) | | 连不上 / 登录被拒 / FTP 故障 | 结果**未知** | **FAIL**——旧行为是 WARN 后当"无记录"放过,那就是假绿 | | 既没有 `device_ip` 也没有 `--selftest-log` | 要求判定却没有数据源 | **FAIL**(同上,不再 WARN 放行) | | 基线最后一条在台账里找不到 | 512 条环已覆盖基线(更新量 ≥ 环容),无法证明 | **FAIL**(不拿"看不到"当"没有") | - ⚠ **采集窗口内仍不要跑 FTP**:每条 FTP 命令都会打到串口上(日志变长、开机耗时指标被污染—— 实测抓基线排在采集武装之后时,`net init: OK` 从 612 ms 被拉到 7997 ms)。基线抓取因此固定在 `open/clear` **之前**。(原先更要命的是客户端退出会打 `[ERR] [FTP] error reading next command` → 直接命中 Forbidden 里的 `[ERR]`;该措辞与级别已在 2026-10-22 归入"对端行为"级,见 `../架构设计/STM32F4-Base存储架构说明.md` §10.3。注意:RST 硬拆仍会留一条 `[ERR][NET] sockN` `TIMEOUT`——那是网络自愈架构要读的套接字超时锚点,不在降级范围内。 - `--selftest-scope all` = 保留旧的累计口径(任一记录即 FAIL),用于**审计**或第一次建基线。 - 给了 `--selftest-log`(本地副本)就没有基线可比 → 自动降级为累计口径并打印一行说明。 - 抓基线同样要 FTP 凭据:`--ftp-user` / `--ftp-pass`(config `ftp_user` / `ftp_pass`,默认出厂 `admin`/`admin`);设备**没有匿名通道** → `FTP_Test_Guide.md` §1。 ### 默认开机标记(来自固件) | 标记 | 来源 | |---|---| | `App init OK` | `App/app/app_main.c` | | `RS485 init: OK` | `App/app/app_main.c` | | `NAND init:` | `App/app/app_main.c` | | `ADC init:` | `App/app/app_main.c` | | `SD2506 ID:` / `time:` / `temp:` / `battery:` | `App/app/app_main.c` | | `Network Init` / `net init: OK` / `MAC:` / `PHY:` | `App/task/net_task.c` | | `phasor(first)` / `phasor(hb)` | `App/task/adc_task.c`(L2 相量层,2026-10-11 起常开;`hb` 每 10 s 一次,采集窗口需 ≥10 s——默认 `max_wait 30 s` 覆盖) | **默认 Forbidden 模式**:`FAIL;ERROR;HardFault;[ERR]` > ⚠ 固件错误级日志前缀是 `[ERR]`(`DBG_ERROR` → `[时间][ERR][TAG] 消息`),与 `ERROR` > 子串**并不匹配**。2026-09-21 起默认列表显式加入 `[ERR]`——此前驱动/模块的错误行 > 不会被自动闭环拦截(历史日志中的 `[ERR]` 全是漏检)。产品构建正常开机应为零 `[ERR]`。 ### 失败模式应对 | 现象 | 处理 | |---|---| | `selftest` FAIL / `ModuleNotFoundError: pyserial` | venv 未建或依赖缺失:按 AGENTS.md「Python 环境」重建 `.venv` + `pip install -r requirements.txt` | | daemon 无响应 / 自动拉起失败 | 查 `.dsh/skills/serial-assistant/scripts/state/daemon.log`;`serial_tool.py restart` 重建 daemon;检查 COM 是否被其他程序占用 | | 抓到日志缺 `Network Init` | `-SettleMs` 太小(boot 任务间有 ~3-4s 自然静默间隙),用默认 8000ms;2026-10-10 起首字节到达前不启动静默计时(`--first-wait-ms` 默认 12000),首行晚到不再被提前收尾 | | 烧录后无数据(`no_data`) | 2026-10-10 起 `no_data` = **本次等待期间零字节**(旧实现比 daemon 累计字节,常驻 daemon 下永不触发,只能靠"日志里没有 [RX] 行"兜底);开串口 + clear 必须在烧录**前**完成(`run_boot_test.py` 已保证);核对 COM 号、板子供电,看 `state/serial.log` 有无 SYS 异常行 | | `[MISS] fw build: <戳>`(其余标记全中) | 芯片里跑的不是这份构建:烧录没落地 / 烧错文件 / hex 未重编译。处置:`stlink_flash.py flash ` 重烧并看 `READBACK: OK`;确认 `@build` 已重跑(戳取自编译时刻)。⚠ 若戳**命中**但里面的 `suite=` 与本次要跑的套件不符(想跑 CH395F 却显示 `suite=NONE dstr=off`)→ 烧的是产品镜像,`@build` 不会替你切宏(→ `board-test-suite` skill) | | `READBACK: FAIL` / `flash readback mismatch` | 芯片内容与 hex 逐字节不一致(写入未落地、选项字节/RDP 异常、烧错目标)。处置:按 `docs/测试说明/ST-LINK_CLI_复位使用说明.md` §7 排查;`stlink_flash.py flash` 重烧后闭环会自动复核 | | `boot banner x2 > allowed 1` | 采集窗口内板子自己复位过(IWDG 咬狗 / Fault 后 `NVIC_SystemReset` / 掉电重启)。处置:看判定里逐条打印的 `reset cause:` 定位;跑**故意复位**的套件用 `--max-boot-count N` 放宽 | | `marker order violated` | 标记全中但**先后顺序颠倒** = 某步在另一步之后补做(就绪屏障失序 / 失败重试补跑)。处置:看判定打印的各标记首次行号定位;`--require-order` 只在需要强约束时启用 | | `boot time N ms > limit` | 能开机但慢(mount 重试 / PHY 重协商 / 复位重试)。实测基线:`net init: OK` 在 **+537ms**(2026-10-10 台架);默认只报告不判(`--max-boot-ms 0`),要设门禁再给预算 | | 端口被占用(`port busy`) | 先 `serial_tool.py close` 或 `restart`;检查其他程序占用;Keil ST-Link 不占 COM | | 配置里的 COM 号失效(换口/换机) | `serial_tool.py ports` 现回 `configured_present` + `hint`(系统里只有一个候选时直接给出口名),`open` 的报错里也带 `[hint: ...]`;确认后只改 `.serialtest/config.json` | | 日志中无 UI 相关输出 | 见下节「UI 自动化」 | | 日志出现 `[ERR]` 行 | 固件错误级日志(`DBG_ERROR`),默认 Forbidden 会判 FAIL:按 TAG 查对应模块的 `docs/问题记录/*_Trap_Records.md`;若属测试套件的**预期**错误(守卫负路径、非法名校验等),用 `--forbidden` 放宽 | ## UI 屏幕自动化 - **初始化回归**:`App/task/ui_task.c` 已有 `UI port init: OK` / `UI menu init: OK` / `UI net sink: started` 打印(见 `@test` 命令示例);将其加入 `-Required` 即可经串口自动回归 UI 初始化。 - **屏幕内容回归**:由 LVGL 模拟器像素回归覆盖(文本级断言 + 53 页黄金参考 + 池水位门禁, 见 `docs/UI设计/UI_自动化测试流程.md`);上机屏幕仍需人工目检(L3 层,见 `测试开发标准流程.md` §1)。 ## 扩展用法 - **单模块回归**:`@test` 后接 `-Required` 定制,例如网络回归用 `-Required "App init OK;Network Init;net init: OK;PHY: 100M FULL"`。 - **测试套件构建**:`test/board/test_config.h` 开启某个 `TEST_SUITE_*` 后,把套件打印的 `=== ... Test:` 标题作为 Required 标记,配合 `test/` 下各用例输出判断。 **破坏性用例默认不跑**:`test_config.h` 的 `ENABLE_DESTRUCTIVE_TESTS`(默认关闭)控制写/擦 NAND、 格式化、坏块注入与 FatFS 写类用例;关闭时套件为只读模式(开机打印 `mode: READ-ONLY`), 可随时在在用设备上执行且不改动卷内容。首次上电/产线验证时才打开(打印 `mode: DESTRUCTIVE`)。 **注意**:套件内**预期**的错误日志会打印 `[ERR]`(GD5F 的 `TC-GD5F-704/705` DMA 守卫 负路径与 FTL 运行期坏块、FatFs 非法名校验等),与默认 Forbidden 冲突,须显式放宽: `--forbidden "FAIL;HardFault"`(`[FAIL]` 仍会拦截真实断言失败); 例:GD5F 只读 `--required "=== GD5F2GQ5UE Tests: 16/16 PASSED"`,破坏性 `68/68`。 - **长时间采集**:daemon 全量持续写入 `state/serial.log`(20MB 上限自动截头保尾 10MB),直接读文件即可;或用 `log --grep <标记>` 过滤(2026-10-10 起先过滤后截尾,不必再配 `--tail 0`)。 ## 变更记录 | 日期 | 变更 | |---|---| | 2026-09-13 | 「UI 屏幕自动化(当前缺口)」更新:UI 初始化 DBG 打印已实现(`ui_task.c`),屏幕内容已纳入模拟器像素回归;测试分层与门禁见 `测试开发标准流程.md` | | 2026-09-15 | 采集机制整体切换:serial-mcp-server(采集窗口/first-byte/EOF 重试)→ 仓库内置 Python 串口助手(daemon 常驻全量日志 + `wait-idle` 静默判据 `-SettleMs`/`-MaxWaitMs`);`serial_capture.ps1` 删除,流程/脚本清单/失败模式表同步 | | 2026-09-21 | 默认 Forbidden 增补 `[ERR]`:固件 `DBG_ERROR` 前缀是 `[ERR]`(≠ `ERROR` 子串),此前驱动/模块错误日志不会被自动闭环拦截;`verify_log.py` / `run_boot_test.py` 默认值同步,测试套件运行需显式放宽 | | 2026-10-10 | 采集链可靠性修复(serial_tool 第一批):`wait-idle` 的 `no_data` 改按本次增量判(旧实现常驻 daemon 下永不触发,"烧录后无数据"这一失败模式实际测不出来)+ 首字节门 `--first-timeout-ms`(`run_boot_test.py` 新增 `--first-wait-ms`,默认 12000);`log --grep` 改先过滤后截尾(旧顺序导致 marker 必然查不到)并加 `-i`/`--regex`;`wait-idle` CLI 超时自动推导,不再需手工 `--timeout`;HTTP 口独占绑定修正(旧选项名/数值均无效,实际未生效) | | 2026-10-10 | **第二批:闭环补上"烧进去的是哪份镜像"与"跑的过程中有没有偷偷复位"**:① 固件身份戳——`App/app/app_main.c` 开机打印 `fw build: <__DATE__> <__TIME__>`,`run_boot_test.py` 从待烧 hex 里抠出该串追加成 Required 标记(旧镜像/错镜像此前所有固定标记照样全中);② 烧录回读校验进闭环——`reset.bat` 新增 `cmp` 子命令(`-CmpFile` 全片比对),采集后自动跑,不一致 exit 2;实测 `-CmpFile` 连接时会复位目标,故必须排在采集之后;③ 启动锚点计数——`verify_log.py` 新增 `--max-boot-count`/`--boot-marker`/`--expect-reset-cause`,`reset cause:` 出现 0 次或 >1 次判 FAIL;④ 修 `--flash-cmd` 带参数时预检必 missing、以及 `cmd /c` 对含空格路径(本仓库目录名 `DTU Code`)的引号解析把回读假判成"芯片不一致"(改为直接 CreateProcess 跑 .bat)。真机验证:完整烧录闭环 PASS(含戳匹配 + `No difference found` + 锚点 x1);伪造戳 → 其余标记全中仍 FAIL | | 2026-10-10 | **第三批:判定可读性 + 时序 + 采集卫生**:① Forbidden 命中逐行打印行号+内容(旧只报条数);② boot_log 每行加 `[+Nms]` daemon 毫秒时基(`--raw-log` 关闭),据此量开机耗时(实测 `net init: OK` = **+537ms**;`--max-boot-ms` 默认 0 只报告);③ `--require-order` 标记先后顺序校验(全中但失序此前不可见);④ `--json` 结构化判定(退出码不变);⑤ `--keep-out`(默认 20)清理 out 目录旧产物(本机曾堆到 31+7 份);⑥ daemon 侧:无换行残行 64KB 封顶 + open/close/clear 会话边界丢弃残行 + 截头按行边界对齐(旧实现截出来的首行是半行)+ `open` 不带 `--dtr/--rts` 时保持现状、显式给则现场应用;⑦ `ports` 回 `configured_present`/`hint`,`status` 回 `rx_age_ms`。selftest 23/23、verify_log 离线回归 17/17、真机闭环 PASS | | 2026-10-11 | **身份戳覆盖测试宏**(N-31 复测时发现):切 `test/board/test_config.h` 的套件宏后 `app_main.c` 未被重编,测试镜像与产品镜像顶着同一个 `fw build:` 戳 → 戳校验对"两种构建互换"失明(当时只有 `-CmpFile` 回读发现镜像不同)。修复:`app_main.c` `#include "test_config.h"`(头依赖 → 改宏必重编、戳必刷新)+ 戳串追加 ` suite= dstr=`(`FW_BUILD_VARIANT`,戳内容本身区分构建)。实测:`20:36:11 suite=NONE dstr=off` ↔ `20:36:40 suite=CH395F dstr=on`;`@test` 闭环对新格式 PASS(脚本无需改动,戳按"标记后连续可打印 ASCII"抠取)。失败模式表补一行:戳命中但 `suite=` 与要跑的套件不符 = 烧了产品镜像 | | 2026-10-22 | **自检台账判据改为「只看本次启动新增」**(修两处叠出来的假绿):① 抓取用的 FTP 登录还写着匿名,而设备自鉴权落地起就没有匿名通道 → 抓取**必然** 530 失败,旧实现又把"抓取失败"和"设备侧无此文件"都返回空、一律按"无记录 → PASS"处理 → `--require-selftest-pass` 从写下那天起没真正看过文件;② 就算抓到了,`/log/selftest.log` 是 512 条环形累计历史,"非空即 FAIL"在任何做过故障注入/长跑的设备上永远不可能 PASS(本仓台架实测 29 条记录全是 2026-10-10 ~ 10-14 留下的)。现在:烧录前取基线、启动后只判新增;抓取失败 / 无数据源 / 基线被环覆盖 **一律 FAIL**;`--selftest-scope all` 保留累计口径用于审计与首次建基线;FTP 凭据 `--ftp-user/--ftp-pass`(默认出厂 admin/admin)。凭据与判据两处都过真机:`this-run` → PASS(无新增)、`all` → FAIL(累计 29 条)、无 `device_ip` → FAIL(无数据源)。 | | 2026-10-27 | 内容页黄金参考 **40 → 47 页**(遥测死区与系数 2 页落地,`simulator/tests/ref/` 实测 47 张:7 张新增、存量 40 张逐字节零漂移);本文件的当前态计数同步改 47,历史行 27 → 40 原样保留 | | 2026-10-27 | 内容页黄金参考 **47 → 53 页**(通讯设置页落地并绑 `#page=comm`;判据 = `simulator/tests/ref/` 入库 `shot_*.bin` 实数 53 张,`git diff --name-status 31ca4a1f HEAD -- simulator/tests/ref` 实测只有 6 个 `A`、零 `M` ⇒ 存量 47 张逐字节零漂移);本文件的当前态计数已同步 | | 2026-10-26 | UI 屏幕自动化段的页数口径修正为 **27 页**黄金参考(与 `simulator/tests/ref/` 入库的 27 个 `shot_*.bin` 对齐;旧写 11)| | 2026-10-26 | UI 屏幕自动化段的黄金参考页数 **27 → 40 页**(`simulator/tests/ref/` 入库 `shot_*.bin` 实测 40 个);历史行保留 |