drive-desktop-app

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Drive a desktop app and prove what happened

驱动桌面应用并验证操作结果

A desktop app reaches its backend over IPC, not HTTP. Patching
fetch
/
XHR
cannot see that, so a browser-shaped tool is blind to every backend call the app makes: the network log reads empty, an action has no in-flight request to settle on, and asserting on the network is vacuously true. That is a false green by construction.
Reticle observes the renderer and the IPC boundary, so a desktop verdict means what a web one does. Not installed?
RETICLE_INSTALL_SOURCE=npx_skill npx @reticlehq/server@latest init
, then the
install-and-verify
skill.
桌面应用通过IPC而非HTTP与后端通信。修补
fetch
/
XHR
无法监测到这类通信,因此类浏览器工具对应用发起的所有后端调用都一无所知:网络日志为空,操作没有可追踪的进行中请求,基于网络的断言毫无意义。这本质上是一种虚假的测试通过结果。
Reticle可以监测渲染进程和IPC边界,因此桌面应用的测试结果和Web应用的一样可靠。还未安装?执行
RETICLE_INSTALL_SOURCE=npx_skill npx @reticlehq/server@latest init
,然后使用
install-and-verify
技能。

Electron: two lines, none in your app code

Electron:仅需两行代码,无需修改应用业务代码

ts
// vite.config.ts — desktop:true also runs the plugin for `vite build`, because a packaged
// renderer is a production build with no dev server
export default defineConfig({
  base: './', // file:// needs relative asset paths
  plugins: [react(), reticle({ desktop: true })],
});
js
// electron/preload.cjs — FIRST line. This is what makes main-process IPC visible.
require('@reticlehq/electron/preload');
It must be in the preload and it must be first.
contextBridge.exposeInMainWorld
hands the renderer a deeply frozen object, so nothing in the page can instrument it afterwards. The preload is the last point where
ipcRenderer.invoke
is still writable, and the shim has to run before your preload captures its own reference.
A sandboxed preload cannot resolve
node_modules
, so the bare
require
fails. Either bundle the preload (electron-vite and Forge do by default) or set
sandbox: false
.
ts
// vite.config.ts — desktop:true 也会在 `vite build` 时运行该插件,因为打包后的
// 渲染进程是没有开发服务器的生产构建版本
export default defineConfig({
  base: './', // file:// 协议需要相对资源路径
  plugins: [react(), reticle({ desktop: true })],
});
js
// electron/preload.cjs — 第一行。这是让主进程IPC可见的关键。
require('@reticlehq/electron/preload');
这行代码必须放在预加载脚本的第一行。
contextBridge.exposeInMainWorld
向渲染进程提供一个深度冻结的对象,因此页面中的任何代码都无法在后续对其进行插桩。预加载脚本是
ipcRenderer.invoke
仍可被修改的最后环节,因此该垫片必须在你的预加载脚本获取自身引用前运行。
沙箱化的预加载脚本无法解析
node_modules
,因此直接使用
require
会失败。要么打包预加载脚本(electron-vite和Forge默认支持),要么设置
sandbox: false

Tauri: the CSP step is required and its failure is silent

Tauri:必须配置CSP,否则会静默失败

The frontend is the same as any web app. The part people miss is that Tauri's default CSP blocks the bridge WebSocket before it opens, so the app runs perfectly and simply never connects:
json
{
  "app": {
    "security": {
      "csp": "default-src 'self' ipc: http://ipc.localhost; connect-src 'self' ipc: http://ipc.localhost ws://localhost:4400 ws://127.0.0.1:4400"
    }
  }
}
Keep
ipc: http://ipc.localhost
: Tauri v2 needs it for
invoke
itself. Dev-only; drop the
ws://
entries from your release config.
IPC observation needs nothing on the Rust side: an
invoke('load_todos')
already reaches Reticle as
ipc://load_todos
. The
reticle-tauri
crate is only for screenshots and headless, and it is versioned independently of the npm packages.
Also: use a hash router. A packaged renderer is served from
file://
, where history-based routing does not resolve.
前端部分与普通Web应用完全一致。容易被忽略的是,Tauri默认的CSP会阻止桥接WebSocket建立连接,因此应用运行完全正常但始终无法连接Reticle
json
{
  "app": {
    "security": {
      "csp": "default-src 'self' ipc: http://ipc.localhost; connect-src 'self' ipc: http://ipc.localhost ws://localhost:4400 ws://127.0.0.1:4400"
    }
  }
}
保留
ipc: http://ipc.localhost
:Tauri v2自身的
invoke
调用需要该配置。这仅适用于开发环境;发布配置中需移除
ws://
相关条目。
IPC监测无需在Rust端做任何配置:
invoke('load_todos')
会自动以
ipc://load_todos
的形式被Reticle捕获。
reticle-tauri
crate仅用于截图和无头运行,其版本与npm包独立维护。
另外:使用哈希路由。打包后的渲染进程通过
file://
协议加载,基于history的路由无法正常解析。

Verify

验证环节

Same loop as the web, with IPC in the predicates:
reticle_act_and_wait({ sessionId, ref, action: "click", until: { kind: "allOf", predicates: [
  { kind: "net",     urlContains: "ipc://todos:archive", status: 200 },
  { kind: "element", query: { testid: "..." } },
  { kind: "console", level: "error", absent: true },
]}})
IPC has no status code.
200
/
500
are synthetic, mapped from whether the command succeeded, precisely so the same predicates keep working. On Tauri you will see
status: 500
next to
statusText: "OK"
. That is not a bug: the transport answered fine and the
500
is the command's own verdict.
ok
is authoritative.
reticle_state
reads the live store exactly as on the web.
reticle_screenshot
and
reticle_visual_diff
work once the platform's capture step is wired. Electron needs nothing extra; Tauri needs the crate. Headless on Tauri is
RETICLE_HEADLESS=1
, and screenshots keep working because the capture renders the webview rather than the screen.
与Web应用的测试流程一致,只是断言中包含IPC相关条件:
reticle_act_and_wait({ sessionId, ref, action: "click", until: { kind: "allOf", predicates: [
  { kind: "net",     urlContains: "ipc://todos:archive", status: 200 },
  { kind: "element", query: { testid: "..." } },
  { kind: "console", level: "error", absent: true },
]}})
IPC本身没有状态码
200
/
500
是合成的状态码,根据命令是否成功映射而来,这样就能复用相同的断言逻辑。在Tauri中,你可能会看到
status: 500
statusText: "OK"
同时出现。这并非bug:传输层响应正常,
500
是命令自身的执行结果。
ok
字段才是权威的判断依据。
reticle_state
可以像在Web应用中一样读取实时状态。配置好平台的捕获步骤后,
reticle_screenshot
reticle_visual_diff
即可正常工作。Electron无需额外配置;Tauri需要安装对应的crate。在Tauri中启用无头模式只需设置
RETICLE_HEADLESS=1
,截图功能依然可用,因为捕获的是webview的渲染内容而非屏幕画面。

What a missing observer looks like

缺失监测器的表现

A missing Electron preload is declared, not silent: verdicts come back with
coverage: partial
naming the line you did not add, instead of reading clean over a blind spot. If you see that, add the preload line before trusting anything.
If IPC calls never appear while the app works fine: on Electron, the shim's
require
is not first. On Tauri,
invoke
from
@tauri-apps/api/core
is observed, but a hand-rolled
postMessage
protocol is not.
如果缺少Electron预加载脚本中的监测代码,系统会明确提示而非静默失败:测试结果会返回
coverage: partial
,并指出你未添加的代码行,而不是忽略监测盲区给出看似正常的结果。如果看到该提示,务必先添加预加载代码,再信任测试结果。
如果应用运行正常但IPC调用从未出现在监测中:在Electron中,可能是垫片的
require
语句不是预加载脚本的第一行;在Tauri中,
@tauri-apps/api/core
中的
invoke
会被监测,但自定义的
postMessage
协议不会被监测。

Honesty

测试原则

unknown
is not a pass on the desktop either. And do not weaken an IPC assertion to make a red verdict green: a desktop false green is the exact failure this wiring exists to remove.

Full desktop reference:
curl https://docs.reticle.sh/desktop.md
. Everything else:
curl https://docs.reticle.sh/llms.txt
.
在桌面应用测试中,
unknown
状态也不代表测试通过。不要为了让红色测试结果变绿而弱化IPC断言:桌面应用中的虚假测试通过正是这套监测机制要解决的核心问题。

完整桌面应用参考文档:执行
curl https://docs.reticle.sh/desktop.md
获取。其他内容:执行
curl https://docs.reticle.sh/llms.txt
获取。