# dsh-mp-automator

**WeChat Mini Program automated testing for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh)**
**微信小程序自动化测试 · DeepSeek Harness 插件**

[![npm](https://img.shields.io/badge/npm-dsh--mp--automator-cb3837)](https://www.npmjs.com/package/dsh-mp-automator)
[![license](https://img.shields.io/badge/license-MIT-blue)](https://github.com/VincentJiang06/dsh-wechatmpdev/blob/main/LICENSE)
[![tests](https://img.shields.io/badge/tests-51%2F51-brightgreen)](https://github.com/VincentJiang06/dsh-wechatmpdev/blob/main/docs/evidence/green-run.txt)

Eight `mp_*` tools that let a dsh agent drive a real Mini Program in WeChat
DevTools — inspect pages, act on elements, capture screenshots, read the
console — with the correctness discipline built **into the tools** instead of
into prompts the model may ignore.

让 dsh 智能体驱动微信开发者工具里真实运行的小程序：读取页面、操作元素、截图、
看控制台。正确性纪律**内建在工具里**，而不是写在模型可能无视的提示词里。

---

## Why this exists · 为什么做这个

Testing a Mini Program through an LLM agent fails in three quiet ways, and
each one produces a **green result that means nothing**:

1. **Stale element handles.** The DevTools automation layer renumbers element
   uids on reconnect, navigation, snapshot, and whenever a second client
   touches it — invisibly. An agent that replays a remembered uid taps the
   wrong element, *and nothing errors*.
2. **Stale builds.** If the compiled `.js` is older than its `.ts`, every
   assertion runs against code nobody intends to ship.
3. **Invisible screenshots.** A text-only model "takes" a screenshot it can
   never see, then quietly describes what it *imagines*.

三种静默失败，每一种都产出**毫无意义的绿色结果**：uid 被运行时无声重编号（点错
元素不报错）；编译产物比源码旧（断言的是没人要发布的代码）；纯文本模型"截"了一张
自己永远看不见的图（然后凭想象描述它）。

This plugin's answer, respectively · 对应的三个答案：

| Failure · 失败 | Mechanism · 机制 |
|---|---|
| stale uid | **Selector-addressed actions** — `mp_act` re-resolves the element *inside one exclusive tool call*; no uid ever crosses a call boundary. 选择器寻址：查找与操作在同一次独占调用内背靠背完成，uid 永不跨调用存活 |
| stale build | **Freshness gate** — act-class tools refuse when `.js` is older than `.ts`, and say honestly when freshness is *unknowable* (plain-JS projects) instead of pretending. 新鲜度门：产物过期直接拒绝执行；无从判断时如实警告，绝不伪装 |
| invisible screenshot | **Dual path** — every screenshot yields a geometry fact table (works on ANY model); vision-declared routes additionally get the real PNG as an image block, and the result always says which happened (`<imageStatus>`). 双路径：几何事实表全模型可用；声明视觉的路由额外附真图，结果永远写明发生了哪种 |

## Install · 安装

```bash
# 1. the automation CLI this plugin drives (owns the DevTools connection)
npm i -g vince-mp-cli

# 2. the plugin, into your dsh profile
dsh plugin --profile web add dsh-mp-automator
```

Requirements · 前置：macOS + [WeChat DevTools](https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html)
(automation/CLI port enabled in 设置→安全设置) · Node ≥ 20 · dsh ≥ 0.1.0-rc.5.

Open a dsh session **inside your Mini Program project** (the directory with
`project.config.json`) — the tools resolve the project from the session's
working directory. 会话必须开在小程序项目目录内。

## The tools · 工具

| tool | what it does · 作用 |
|---|---|
| `mp_session` | start / status / stop / restart / reconnect the persistent DevTools session（其余工具会自动起会话）|
| `mp_doctor` | project health: DevTools cli, `tsc --noEmit`, compiled-JS freshness — feeds the gate · 健康检查，喂给新鲜度门 |
| `mp_inspect` | page / stack / data(+path) / sysinfo / snapshot（元素事实表）|
| `mp_query` | selector → geometry fact table: `fully-visible / partial / offscreen / read-failed` flags + overlap candidates（0.2.0 起相对**当前滚动窗口**判定，事实表披露 `scrollTop=`；scroll-aware since 0.2.0）|
| `mp_act` | tap / input / longpress **by selector** · nav / switchTab / reLaunch · camera-less `scan` |
| `mp_screenshot` | PNG to `captures/` + fact table; real image block on vision routes; `<imageStatus>` says which path fired |
| `mp_console` | errors first, then the **most recent** entries（自动翻到缓冲区尾部）|
| `mp_eval` | escape hatch: evaluate JS in the page context — freshness-gated, config kill-switch |

Every result is byte-clamped under the host's compaction pruner and is
self-contained — long test sessions survive context compaction without the
results rotting into "see above". 所有结果自带字节上限且自包含，长会话压缩后依然可读。

## A test run looks like · 一次测试长这样

```
you › 测试首页的"立刻开始"按钮能进入扫码页

agent › mp_doctor            → freshness: fresh — freshness verified: 12 .ts/.js pair(s) checked, typecheck passed
agent › mp_query .start-btn  → matched=1 · button_3 | button | 24,180,342,96 | fully-visible | 立刻开始 简单扫码
agent › mp_act tap .start-btn → tap on .start-btn[0] (resolved button_3, 1 match(es)) | route now: pages/scan/index
agent › mp_console           → console: total=2 errors=0
agent › ✅ 按钮可见、可点、路由跳转正确、无控制台错误
```

And when the build is stale · 构建过期时：

```
agent › mp_act tap .start-btn
      ✗ refusing to act on this runtime: 1 compiled file(s) are older than their
        TypeScript source (e.g. utils/debounce.ts) — rebuild the project (or let
        DevTools finish compiling) so the runtime matches the source
```

## Vision routes · 视觉路由

Declare `input: [text, image]` on any pi-ai provider model and
`mp_screenshot` attaches the real PNG — verified live with
`kimi-k2.7-code`, which read button text and page colors straight from the
pixels ([evidence](./docs/evidence/live-matrix.md)). Text-only routes (e.g.
DeepSeek V4) get the same geometry table plus an explicit *"the PNG is on
disk but NOT in context"* note, so the model can never pretend it saw one.

给任意 pi-ai provider 的模型声明 `input: [text, image]`，截图就会作为真实
image block 进入上下文（已用 kimi-k2.7-code 实测：从像素读出按钮文字与配色）。
纯文本路由拿到同样的几何表 + 一行明示"图在磁盘、不在上下文"，模型无法假装看过图。

Provider template · 配置模板：[`docs/PROVIDER-TEMPLATE.yaml`](./docs/PROVIDER-TEMPLATE.yaml)

## Configuration · 配置

```yaml
# in your profile's cordis.patch.yml
- id: mp-automator
  config:
    freshnessMode: block   # block(默认) | warn | off
    enableEval: true       # mp_eval kill-switch
    imageBudget: 3         # max ATTACHED screenshots per session (vision routes) — each
                           # attached image keeps billing on every later request; unchanged
                           # screens are never re-attached · 视觉路由的图片附加预算
    screenshotDir: captures
    binPath: vince-mp      # absolute path if not on PATH
```

## Design notes · 设计笔记

This plugin was built by adversarial iteration: a five-lens attack battery
(plus a cross-vendor DeepSeek attacker) destroyed the first design — 9 P1
findings, four root causes — and v3 eliminated the root causes structurally
rather than patching symptoms. The full ledger, the red→green test evidence,
and the live test matrix live in the repo's [`docs/`](./docs/) (kept out of the npm tarball to stay lean).

本插件经对抗性迭代产出：五透镜攻击电池（外加跨厂商 DeepSeek 攻击手）击穿了第一版
设计（9 个 P1、四个根因），v3 用结构性设计消灭根因而非打补丁。完整台账、红绿测试
证据与真机测试矩阵见仓库 [`docs/`](./docs/)（未打进 npm 包以保持精简）。

The four structural decisions · 四个结构性决策：

- **Don't mirror shared state — change the addressing model.** The uid map
  is daemon-owned and renumbers invisibly; no plugin-side counter can win.
  不镜像共享状态，改寻址模型。
- **Fail closed everywhere.** Only strict `ok === true` is success; a missing
  doctor document refuses instead of assuming. 一切解析默认失败。
- **stderr is half the contract.** The CLI prints thrown errors to stderr;
  both streams are parsed. stderr 是契约的另一半。
- **Budgets live in code.** Byte-clamps with disclosed truncation, not prose
  promises. 预算写进代码，截断必声明。

## Companion skill · 配套技能

[`skill/mp-testing/SKILL.md`](./skill/mp-testing/SKILL.md) carries the
judgment layer (inspect→act→verify rhythm, selector discipline, the
assertion recipe). Load it into any skill root dsh reads — the tools work
without it, but test *quality* comes from doctrine.

工具携带能力与门，技能携带打法（先看后动再验证、选择器纪律、断言配方）。

## Relationship to vince-mp-cli · 与 CLI 的关系

This plugin is a thin shell: [`vince-mp-cli`](https://www.npmjs.com/package/vince-mp-cli)
owns the automation truth (DevTools connection, element resolution, path
policy); the plugin owns agent-facing discipline. Version negotiation is
explicit — the plugin verifies `vince-mp-cli >=0.2.0 <0.3.0` at first use
and refuses with an upgrade hint outside that window.

薄壳架构：CLI 拥有自动化事实，插件拥有面向智能体的纪律；首次调用时协商版本，
窗口外拒绝并给出升级指引。

## License

MIT © Vincent Jiang
