meticulous-increase-coverage

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Increase coverage for a Meticulous project

为Meticulous项目提升代码覆盖率

Run
meticulous-cli-update
first if you haven't already this conversation (it also covers authentication and project selection).
如果本次会话尚未执行,请先运行
meticulous-cli-update
(该命令还包含身份验证和项目选择功能)。

What you deliver

交付成果

Two separate outputs, both expected — neither substitutes for the other:
  1. One or more test runs from newly recorded sessions that provably extend coverage. "Provably" means a comparison against the baseline naming the files whose coverage went up, and by how much (Step 7). If a target turns out not to be coverable, say so rather than padding the list.
  2. A PR proposing
    .meticulousignore
    changes.
    Only include paths — most often whole directories — that you are confident are not coverable at all. Anything you merely failed to reach in one sitting does not belong there; leave it out and mention it in the PR description instead.
需交付两个独立输出,二者缺一不可:
  1. 基于新录制会话的一次或多次测试运行,可证明覆盖率得到扩展。「可证明」指与基线对比,明确列出覆盖率提升的文件及提升幅度(步骤7)。若目标文件确实无法被覆盖,请如实说明,不要凑数。
  2. 提议修改
    .meticulousignore
    的PR
    。仅包含你确信完全无法被覆盖的路径——通常是整个目录。任何只是本次未覆盖到的内容都不应列入;可在PR描述中提及这些内容,但不要加入忽略列表。

Before you start: run this on
main
, with a clean tree

开始前:在
main
分支执行,确保工作区干净

Every command below relies on the CLI resolving things from your local checkout:
js-coverage
defaults to the current git HEAD,
trigger-test-run
defaults to HEAD for the deployment and to the merge-base with the origin default branch for the base. On
main
with a clean tree those collapse to a single commit, which is exactly what you want — no diff, a head-only run, and a union in Step 7 that the API will actually accept.
Off
main
this breaks in ways that are tedious to unpick: union coverage is rejected unless every run executed the exact same commit, and a PR's merge commit is recomputed whenever its base branch moves, so a run triggered earlier against a since-advanced base no longer unions with a new one.
So: check out
main
, pull, and make sure
git status
is clean before Step 1.
Then confirm there is actually a test run to work from:
bash
meticulous agent test-run-for-commit
Keep the id it prints — Step 7 falls back to it. If it reports "No test run found for commit …", stop and report that to the user; you cannot baseline without it. The most common cause is the CLI pointing at the wrong project, so suggest they check with
meticulous auth get-project
.
meticulous auth set-project
only applies for OAuth tokens; API tokens are bound to one project (so
set-project
fails), and injected credentials leave no local token to select — in those cases the fix is a different credential, not
set-project
.
以下所有命令都依赖CLI从本地检出内容解析:
js-coverage
默认使用当前git HEAD,
trigger-test-run
默认使用HEAD进行部署,并使用与远程默认分支的合并基准作为基线。在干净的
main
分支上,这些都会指向同一个提交,这正是我们需要的状态——无差异、仅针对HEAD的运行,且步骤7中的合并操作能被API正常接受。
若不在
main
分支上,会出现难以排查的问题:合并覆盖率仅在所有运行执行完全相同的提交时才会被接受;PR的合并提交会随基准分支更新而重新计算,因此早期针对旧基准触发的运行无法与新运行合并。
因此:检出
main
分支、拉取最新代码,并确保
git status
显示工作区干净后再执行步骤1。
然后确认存在可用于工作的测试运行:
bash
meticulous agent test-run-for-commit
记录输出的id——步骤7会回退使用该id。若输出**"No test run found for commit …"**,请停止操作并告知用户;没有基线无法继续。最常见的原因是CLI指向了错误的项目,建议用户使用
meticulous auth get-project
检查。
meticulous auth set-project
仅适用于OAuth令牌;API令牌绑定到单个项目(因此
set-project
会失败),注入的凭证不会留下本地令牌供选择——这种情况下需要更换凭证,而非执行
set-project

Step 1 — Baseline coverage

步骤1 — 获取基线覆盖率

bash
meticulous agent js-coverage --includeAllFiles --includeCoveragePercentage \
  > /tmp/baseline-coverage.tsv
If this reports "No test run found for commit …" (it shouldn't, if the check above passed), stop and report to the user. Do not work around it by baselining against some other commit's run: Step 7's union requires your new run and the baseline to have executed the same commit.
bash
meticulous agent js-coverage --includeAllFiles --includeCoveragePercentage \
  > /tmp/baseline-coverage.tsv
若输出**"No test run found for commit …"**(如果之前的检查通过,这不应该发生),请停止操作并告知用户。不要通过基于其他提交的运行来获取基线:步骤7的合并操作要求新运行和基线运行执行的是同一个提交。

Step 2 — Separate dead code from real targets

步骤2 — 区分死代码与真实覆盖目标

You are looking for two different things in this file, and it helps to keep them apart:
  • .meticulousignore
    candidates
    — files that are uniformly at 0% across a whole directory, which suggests they never execute in a browser at all.
  • Coverage targets — files a real user flow could reach but no recorded session happens to. These are not only the 0% files. A file at 12% or 40% usually means one path through it runs and the rest doesn't, and those partial files are often the cheapest wins: the module already loads, so a single extra interaction can light up a large block. Sort ascending by percentage and work up from the bottom, rather than stopping at 0%.
Start with the ignore candidates, since they shrink the list. Break the 0%-coverage files down by top-level directory, so you are reasoning about groups rather than 100s of individual files. The exact command depends on how the repo is laid out — a monorepo wants the first two path segments, a single-app repo wants something deeper. For example, in a
packages/<name>/…
monorepo:
bash
undefined
你需要从该文件中找出两类内容,分开处理会更高效:
  • .meticulousignore
    候选路径
    ——整个目录下所有文件覆盖率均为0%,表明这些代码永远不会在浏览器中执行。
  • 覆盖目标——真实用户流程可触及,但现有录制会话未覆盖到的文件。这些不仅是0%覆盖率的文件。覆盖率为12%或40%的文件通常意味着部分代码路径已被执行,其余未执行;这类部分覆盖的文件往往是最容易提升的:模块已加载,只需额外一次交互就能覆盖大量代码块。按覆盖率升序排序,从最低的开始处理,不要只停留在0%的文件。
先处理忽略候选路径,因为这会缩小目标列表。将0%覆盖率的文件按顶级目录分组,这样你可以针对组而非数百个单个文件进行分析。具体命令取决于仓库结构——单仓库应用可能需要更深的路径段,而单体仓库可能只需要前两段。例如,在
packages/<name>/…
结构的单体仓库中:
bash
undefined

example only — adjust the segment depth to this repo's layout

仅为示例——根据仓库结构调整路径段深度

awk -F'\t' 'NR>1 && $2=="0.0" {split($1,a,"/"); print a[1]"/"a[2]}'
/tmp/baseline-coverage.tsv | sort | uniq -c | sort -rn

A directory where **every single file** is at 0% (not just some) is a strong
signal it never ships to the browser (backend, CLI tooling, docs, e2e test
harness). Those are your `.meticulousignore` candidates.

There is a second, stronger signal that doesn't depend on coverage data at
all and catches individual dead files scattered inside an otherwise-live
directory, which the uniformly-0% heuristic above misses entirely. For any
file sitting at 0%, grep the codebase for its actual exported symbol — not
just its filename:

```bash
awk -F'\t' 'NR>1 && $2=="0.0" {split($1,a,"/"); print a[1]"/"a[2]}'
/tmp/baseline-coverage.tsv | sort | uniq -c | sort -rn

**每个文件**覆盖率均为0%的目录(并非部分文件)是强烈信号,表明该目录下的代码不会被打包到浏览器(后端、CLI工具、文档、端到端测试工具等)。这些是你的`.meticulousignore`候选路径。

还有一种更可靠的信号,不依赖覆盖率数据,能捕获分散在活跃目录中的单个死文件,而上述全0%启发式方法会遗漏这类文件。对于任何覆盖率为0%的文件,在代码库中搜索其实际导出的符号——而非仅搜索文件名:

```bash

example only — adjust the source root and extensions to this repo

仅为示例——根据仓库调整源码根目录和文件扩展名

grep -rn "theActualExportedName" <src-root> --include=".ts" --include=".tsx"

If the only match is the file's own definition, nothing imports it, so no
session — however comprehensive — can ever execute it. That is proof of
unreachability, not an inference from silence, and it is worth checking even
when you already have a directory-level rule elsewhere in this file: dead
exports accumulate inside packages that are otherwise very much alive.

Some categories are **safe exclusions almost everywhere** and are worth
proposing without further tracing, provided the coverage data agrees they are
uniformly 0%:

- test files and their directories — `__tests__/`, `__mocks__/`, `*.test.*`,
  `*.spec.*`
- Storybook — `*.stories.*`, `__stories__/`, `.storybook/`
- test/mock harness directories — `testing/`, `mocks/`, fixtures
- build, lint and codegen config executed only by Node — `*.config.*`,
  `setupTests.*`, scripts directories

Note that some of these may already be outside the coverage report entirely;
check the baseline before adding a rule that does nothing.

Be suspicious of a directory showing 0% everywhere if you _know_ it is bundled
into the frontend (a shared component/utils library the main app imports).
That pattern is more likely a source-map/path-attribution gap than genuinely
dead code — leave it out of the ignore list and flag it as unresolved.

Now pick the coverage targets. Filter the generated/config noise out of the
real app package first, then order what's left by how little of it runs —
keeping the partially-covered files in, not just the 0% ones. The patterns are
repo-specific; inspect the actual paths in your baseline rather than copying
this verbatim:

```bash
grep -rn "theActualExportedName" <src-root> --include=".ts" --include=".tsx"

如果唯一匹配项是文件自身的定义,说明没有任何代码导入它,因此无论会话多么全面,都无法执行该文件。这是无法访问的证明,而非基于无数据的推断;即使目录层面已有忽略规则,也值得检查这类文件——活跃包中也会积累死导出。

某些类别几乎在所有项目中都是**安全排除项**,只要覆盖率数据显示它们全为0%,就可以直接提议忽略,无需进一步追踪:

- 测试文件及其目录——`__tests__/`、`__mocks__/`、`*.test.*`、`*.spec.*`
- Storybook相关——`*.stories.*`、`__stories__/`、`.storybook/`
- 测试/模拟工具目录——`testing/`、`mocks/`、fixtures
- 仅由Node执行的构建、lint和代码生成配置——`*.config.*`、`setupTests.*`、scripts目录

注意其中部分内容可能已不在覆盖率报告范围内;添加规则前先检查基线,避免添加无效规则。

如果你**确定**某个目录会被打包到前端(比如主应用导入的共享组件/工具库),但该目录下所有文件覆盖率均为0%,则更可能是源映射/路径归因问题,而非真正的死代码——不要将其加入忽略列表,而是标记为未解决问题。

现在选择覆盖目标。先过滤掉真实应用包中的生成/配置噪声,然后按覆盖率从低到高排序剩余内容——保留部分覆盖的文件,不要只选0%的文件。具体模式因仓库而异;请检查基线中的实际路径,不要直接复制以下命令:

```bash

example only — derive the patterns from the paths this repo actually has

仅为示例——根据仓库实际路径推导模式

grep -v "/tests/|.test.|.stories.|/testing/|/mock"
/tmp/baseline-coverage.tsv | sort -t$'\t' -k2 -g > /tmp/candidates.tsv

Group the candidates by feature area rather than picking the single worst
files: one recorded flow usually moves a whole cluster of related files at
once, so a directory sitting at 5-20% across a dozen files is a better target
than an isolated 0% file behind an obscure branch.
grep -v "/tests/|.test.|.stories.|/testing/|/mock"
/tmp/baseline-coverage.tsv | sort -t$'\t' -k2 -g > /tmp/candidates.tsv

按功能区域分组候选文件,而非只选覆盖率最低的单个文件:一次录制的流程通常会同时提升一组相关文件的覆盖率,因此某个目录下十几个文件覆盖率在5-20%之间,比孤立的0%文件(位于模糊分支后)更适合作为目标。

Step 3 — Trace, don't guess

步骤3 — 追踪而非猜测

For each candidate file, find its actual caller(s) — for example:
bash
undefined
对于每个候选文件,找到其实际调用者——例如:
bash
undefined

example only — adjust the source root and extensions to this repo

仅为示例——根据仓库调整源码根目录和文件扩展名

grep -rln "<ExportedThing" <src-root> --include="*.tsx" | grep -v test

Read the caller. Confirm:

- It's reachable via a simple, describable UI action (a specific button, a
  specific menu item) — not buried behind a feature flag, a disabled config
  (e.g. billing/SSO toggles that are off in this environment), or a
  conditional branch that only fires for certain object types.
- If the target is a **hook**, check every branch that calls it. Hooks are
  often called conditionally — one branch might route through a completely
  different mechanism (a plain router `<Link>` instead of the app's own
  navigation hook, a side-panel open instead of a full navigate). Confirm
  which branch your candidate action actually hits.
grep -rln "<ExportedThing" <src-root> --include="*.tsx" | grep -v test

查看调用者代码,确认:

- 它可通过简单、可描述的UI操作触达(特定按钮、特定菜单项)——而非隐藏在功能标志、禁用配置(如当前环境中关闭的计费/SSO开关)或仅针对特定对象类型触发的条件分支后。
- 如果目标是**hook**,检查所有调用它的分支。Hook通常会被条件调用——某个分支可能会通过完全不同的机制(比如普通路由`<Link>`而非应用自身的导航hook,侧边栏打开而非完整跳转)。确认候选操作实际触发的是哪个分支。

Step 4 — Drive the flow

步骤4 — 触发流程

Any of these three drivers records correctly — verified against two different apps:
  • Claude in Chrome — the only one that drives the user's own signed-in Chrome profile, so an authenticated app needs no login flow. Its input path is also the one that wedges (see below), so verify early and be ready to switch.
  • Playwright and
    agent-browser
    — CDP-launched browsers, faster and more scriptable, and reliable in every test here. The trade-off is a fresh profile each time, so you have to sign in.
    agent-browser
    additionally refuses a click when the target is covered by another element, naming the covering node, which catches a class of silent mis-click the other two will happily perform.
Set a realistic viewport whichever you pick. The CDP browsers default to something small (around 1280px wide) where a real Chrome window is often twice that. Responsive layouts render different components at different widths, so the default viewport can quietly cover different code — or hide the control you were aiming for.
What matters far more than the choice of tool is the one rule below.
Only trusted events are recorded. The recorder ignores anything synthesised in page JS, so
element.click()
, assigning
input.value
, or dispatching your own events all drive the app convincingly and record nothing. The page looks right, the session comes back empty. Drive everything through the tool's real input actions.
When something doesn't take effect, find out which half is broken before changing tactics — install a capturing probe and repeat the action:
js
window.__ev = [];
["pointerdown", "click", "keydown", "change"].forEach((t) =>
  window.addEventListener(
    t,
    (e) => window.__ev.push({ t, trusted: e.isTrusted }),
    true,
  ),
);
  • nothing captured → your input never reached the page; a different selector or coordinate won't help (see the wedged-extension note below)
  • captured but
    trusted: false
    → it reached the page but will not be recorded; you are synthesising somewhere
以下三种工具均可正确录制——已在两个不同应用中验证:
  • Claude in Chrome——唯一可使用用户自身已登录Chrome配置文件的工具,因此认证应用无需登录流程。但其输入路径可能出现阻塞(见下文),因此需提前验证,并准备好切换工具。
  • Playwright和**
    agent-browser
    **——基于CDP启动的浏览器,更快、更具脚本性,且在所有测试中都可靠。缺点是每次启动都是全新配置文件,因此需要登录。
    agent-browser
    还会在目标元素被其他元素覆盖时拒绝点击,并指明覆盖节点,这能捕获其他两种工具可能会静默执行的错误点击。
无论选择哪种工具,都要设置符合实际的视口。CDP浏览器默认视口较小(约1280px宽),而真实Chrome窗口通常是其两倍。响应式布局在不同宽度下会渲染不同组件,因此默认视口可能会静默覆盖不同代码——或隐藏你要操作的控件。
比工具选择更重要的是以下规则:
仅录制可信事件。录制器会忽略页面JS中合成的任何事件,因此
element.click()
、赋值
input.value
或自定义事件调度都能正常驱动应用,但不会被录制。页面显示正常,但会话内容为空。所有操作都要通过工具的真实输入动作触发。
当操作未生效时,在更换策略前先排查问题——安装捕获探针并重复操作:
js
window.__ev = [];
["pointerdown", "click", "keydown", "change"].forEach((t) =>
  window.addEventListener(
    t,
    (e) => window.__ev.push({ t, trusted: e.isTrusted }),
    true,
  ),
);
  • 未捕获到任何事件→你的输入从未到达页面;更换选择器或坐标也无济于事(见下文扩展程序阻塞说明)
  • 捕获到事件但
    trusted: false
    →输入到达页面但不会被录制;你在某个环节使用了合成事件

Three ways an action records as nothing

操作录制为空的三种情况

All three look like success in the browser, which is what makes them expensive — you find out from the coverage numbers, long after the fact.
Native
<select>
s.
Setting the value through a form-fill action, or assigning it in JS, fires a
change
with
isTrusted: false
— the app reacts and the value visibly updates, so it looks like it worked, but the recorder ignores it and the sort/filter never happens on replay. Clicking the
<select>
is no good either: that opens an OS-level popup the driver can't see. What works is to focus the element and press the first letter of the option's visible text (
"p"
→ "Priority"), which yields a trusted
keydown
and a trusted
change
. Repeat the letter to cycle options sharing an initial. Verify with a
change
listener reading
e.isTrusted
— the value updates either way, so the value alone tells you nothing.
Modifier shortcuts. Replay reproduces a modifier only if a discrete modifier keydown was recorded and is still held. Some drivers send one only for the base key: Claude in Chrome's
cmd+k
and
agent-browser
's
press Alt+ArrowRight
both record a single keydown with the modifier flag set and no separate modifier press, so on replay the flag is cleared and the handler body never runs — while working perfectly live. Playwright's
press('Alt+ArrowRight')
does record the discrete press and replays correctly (verified by coverage).
So: prefer the equivalent click target where one exists, and if you must use a chord, drive it with Playwright and confirm from coverage afterwards. If the line holding
if (… && event.metaKey)
is covered but the body is not, the keydown was delivered and the condition evaluated false — that is this.
Double-clicks. A double-click may be recorded as a single click, in which case the replay never fires
onDoubleClick
and every later event in that session targets UI that never opened — so coverage drops. Check the recorded event count looks like two press/release pairs, and treat any double-click-only feature as suspect until coverage confirms it.
这三种情况在浏览器中看起来都成功了,因此排查成本很高——你只能通过覆盖率数据发现问题,而此时已耗时很久。
原生
<select>
元素
。通过表单填充操作设置值,或在JS中赋值,会触发
isTrusted: false
change
事件——应用会响应,值也会明显更新,看起来操作成功,但录制器会忽略该事件,重放时排序/过滤不会生效。点击
<select>
也不行:这会打开驱动程序无法看到的系统级弹窗。可行的方法是聚焦元素,然后按下选项可见文本的首字母(例如
"p"
→"Priority"),这会产生可信的
keydown
change
事件。重复按首字母可循环选择首字母相同的选项。请使用监听
change
事件并读取
e.isTrusted
的方式验证——无论哪种方式值都会更新,因此仅看值无法判断是否成功。
修饰键快捷键。只有录制了独立的修饰键按下事件且重放时仍保持按下状态,修饰键才会在重放时生效。部分驱动程序仅针对基础按键发送一次事件:Claude in Chrome的
cmd+k
agent-browser
press Alt+ArrowRight
都会录制一个带有修饰键标志的单次按键事件,而没有独立的修饰键按下事件,因此重放时标志会被清除,处理函数主体永远不会执行——但实时操作完全正常。Playwright的
press('Alt+ArrowRight')
会录制独立的按下事件,且重放正常(已通过覆盖率验证)。
因此:优先使用等效的点击目标;如果必须使用组合键,请使用Playwright触发,并在之后通过覆盖率确认。如果包含
if (… && event.metaKey)
的代码行被覆盖,但主体未被覆盖,说明按键已送达,但条件判断为false——这就是上述情况。
双击。双击可能会被录制为单次点击,导致重放时永远不会触发
onDoubleClick
,该会话中后续所有事件都会针对从未打开的UI——因此覆盖率下降。检查录制的事件计数是否包含两次按下/释放对;任何仅支持双击的功能在覆盖率确认前都应视为可疑。

Some interactions may not survive agent-driven recording at all

某些交互可能无法通过Agent驱动的录制正常重放

Occasionally an interaction records cleanly and visibly works live, but the handler it's meant to trigger never fires on replay — with no error, and the affected file sitting at exactly its baseline percentage in Step 7's union. One case seen: typing a value into an input inside a dropdown's own portal-rendered content (a filter chip, a view rename behind a "..." menu). Plain clicks in the same portal, and the identical type-then-Enter sequence on an input in the main page tree, both replay fine — so this is specific to keyboard/text input inside a portal, not a driver issue.
Don't assume a typed-value target worked just because the live interaction did — check Step 7's diff. If a target only reproduces through this kind of interaction and won't move, that's a shortcoming of agent-driven recording for this flow: report it as unresolved and suggest a human drive that one flow manually, rather than continuing to pad the list with retries.
偶尔会出现这种情况:交互录制正常,实时操作也可见成功,但重放时预期触发的处理函数从未执行——无错误,且受影响文件在步骤7的合并对比中覆盖率与基线完全相同。已发现的案例:在下拉菜单的门户渲染内容中输入值(过滤芯片、「...」菜单后的视图重命名)。同一门户中的普通点击,以及主页面树中输入框的相同输入+回车序列,重放都正常——因此这是针对门户内键盘/文本输入的特定问题,而非驱动程序问题。
不要仅因为实时交互成功就认为输入目标已被覆盖——请检查步骤7的差异。如果某个目标只能通过这类交互触发且无法提升覆盖率,说明Agent驱动的录制在该流程中存在局限性:请如实报告为未解决问题,并建议人工手动触发该流程,不要继续重试凑数。

claude-in-chrome
specifics

claude-in-chrome
特定说明

  • Screenshots are downscaled (~0.6x), so they are not CSS pixels. Click coordinates read off the screenshot, or scale a
    getBoundingClientRect()
    centre by
    screenshotWidth / window.innerWidth
    . Re-derive after any resize, and re-screenshot rather than reusing coordinates from an earlier page — layout shifts, and a stale coordinate can land on the wrong element and record an interaction you did not intend.
  • Re-acquire a
    ref
    immediately before clicking it
    , and never reuse one across pages or tabs. Don't predict a number —
    read_page
    does not emit them in order. Refs and coordinates are equally reliable; pick whichever is convenient. This is not unique to
    claude-in-chrome
    agent-browser
    's
    snapshot
    refs go stale the same way, and reusing one silently clicks whatever now occupies that ref, not what you intended. It cost a real mistake once: a stale ref landed on a table's "Add New" affordance and created a blank record. Take a fresh snapshot immediately before every click when the DOM might have changed, and treat an unexpected page/record-count change right after a click as a sign a ref just misfired, not as an unrelated bug — clean up whatever it created before continuing.
  • Input delivery wedges intermittently. Every click and keystroke reports success, reads keep working, and nothing reaches the page. Refs and coordinates die together, so this is never a selector problem — and it is not cleared by a new tab, a fresh navigation, waiting, or retrying. A full Chrome restart helps but does not durably fix it. Check the probe after your first interaction, before driving a whole flow, and if input isn't landing switch to Playwright or
    agent-browser
    rather than switching selector method. Both stayed reliable throughout, including on the same page at the same moment that Claude in Chrome was dead.
  • 截图会被缩小(约0.6倍),因此不是CSS像素。请根据截图读取点击坐标,或按
    screenshotWidth / window.innerWidth
    缩放
    getBoundingClientRect()
    的中心坐标。调整窗口大小后重新计算坐标,页面变化后重新截图,不要使用之前页面的坐标——布局会发生偏移,过时的坐标可能会点击到错误元素,录制到非预期的交互。
  • 点击前立即重新获取
    ref
    ,不要跨页面或标签页复用
    ref
    。不要预测编号——
    read_page
    不会按顺序输出编号。
    ref
    和坐标同样可靠;选择方便的方式即可。这并非
    claude-in-chrome
    独有——
    agent-browser
    snapshot
    ref也会过时,复用会静默点击当前占据该ref的元素,而非预期元素。曾因此出现过真实错误:过时的ref点击到表格的「添加新项」按钮,创建了一条空白记录。DOM可能变化时,每次点击前都要获取新的快照;点击后页面/记录计数意外变化时,应视为ref错误触发的信号,而非无关bug——继续操作前先清理错误创建的内容。
  • 输入交付会间歇性阻塞。所有点击和按键都会报告成功,读取操作正常,但输入从未到达页面。
    ref
    和坐标同时失效,因此这永远不是选择器问题——且无法通过新建标签页、重新导航、等待或重试解决。完全重启Chrome可能会有所帮助,但无法彻底修复。在触发整个流程前,先检查第一个交互的探针;如果输入未到达页面,请切换到Playwright或
    agent-browser
    ,而非更换选择器方法。这两种工具在所有测试中都保持可靠,包括在Claude in Chrome失效的同一页面。

Drive it, then verify

触发流程并验证

  1. Navigate to the target URL and drive the real action.
  2. Confirm it worked against the DOM (e.g.
    document.body.innerText.includes(...)
    ), not just a screenshot — a tooltip appearing can look like success. Check you are still on the page you think you are: an app that has quietly redirected you to a login screen will absorb blind coordinate clicks into empty space and hand you a session with zero events.
  3. Close the tab.
Then sanity-check the recording, before you trigger anything. Wait ~10s after closing the tab, so the session is complete, and check that it captured something:
bash
meticulous agent sessions --limit 10 --excludeSyntheticSessions \
  --includeDurationSeconds --includeNumberUserEvents \
  --includeNumberUrlsVisited --includeStartUrl --includeAbandonedReason
Read the row you just produced:
  • no row at all — nothing was uploaded yet; wait a little longer before concluding the recording failed
  • numberUserEvents
    of 0
    — the recorder saw no user input. Your clicks were not reaching the page, or were synthesised rather than trusted; go back to the input-delivery probe above. Replaying this session is pointless.
  • numberUrlsVisited
    of 1 when you navigated several times
    — the later pages did not make it into this session. They either landed in their own sessions (fine, collect those ids too) or were swallowed as an unreplayable tail (see the hard-navigation note in Step 5).
  • durationSeconds
    over 300
    — everything past the 5-minute mark will be silently trimmed on replay (Step 5). Re-record the overflowing part as its own session rather than hoping it survives.
  • populated
    abandonedReason
    — the recorder gave up on the session (see the 10-minute cap in Step 5); it is not worth replaying.
  • startUrl
    that is not the page you drove
    — the navigation you cared about belongs to a different session than you assumed.
A session is only complete once its tab is closed. While the tab is open the row reflects only the chunks uploaded so far, so a low or zero
numberUserEvents
there means "not flushed yet", not "the recording failed". A session measured for this skill read 0 events with the tab open and 12 once it was closed. Judging it early nearly caused a perfectly good recording to be re-driven from scratch.
Events upload on a short interval (a few seconds), but anything still unflushed at unload is only stashed in
sessionStorage
and re-sent on a later page load to the same origin — so a session's tail can be delayed until the next visit. Prefer navigating away over hard-closing the browser, and never judge a recording immediately.
  1. 导航到目标URL并触发真实操作。
  2. 针对DOM确认操作成功(例如
    document.body.innerText.includes(...)
    ),不要仅依赖截图——工具提示出现可能看起来像成功。检查你是否仍在预期页面:应用可能已静默重定向到登录页面,此时盲目点击坐标会点击到空白区域,得到一个无事件的会话。
  3. 关闭标签页
然后在触发测试运行前,先检查录制内容。关闭标签页后等待约10秒,确保会话完整,然后检查是否捕获到内容:
bash
meticulous agent sessions --limit 10 --excludeSyntheticSessions \
  --includeDurationSeconds --includeNumberUserEvents \
  --includeNumberUrlsVisited --includeStartUrl --includeAbandonedReason
查看你刚生成的行:
  • 无任何行——内容尚未上传;等待一段时间后再判断录制失败
  • numberUserEvents
    为0
    ——录制器未捕获到用户输入。你的点击未到达页面,或使用了合成事件而非可信事件;回到上文的输入交付探针排查。重放该会话毫无意义。
  • numberUrlsVisited
    为1,但你导航了多个页面
    ——后续页面未被纳入本次会话。它们要么属于其他会话(没问题,收集这些id即可),要么被视为无法重放的尾部(见步骤5中的硬导航说明)。
  • durationSeconds
    超过300
    ——会话中超过5分钟的部分会在重放时被静默截断(步骤5)。将溢出部分重新录制为独立会话,不要指望它能被完整保留。
  • abandonedReason
    不为空
    ——录制器已放弃该会话(见步骤5中的10分钟上限);不值得重放。
  • startUrl
    不是你触发的页面
    ——你关注的导航属于另一个会话。
会话仅在标签页关闭后才完整。标签页打开时,行内容仅反映已上传的部分,因此此时
numberUserEvents
低或为0意味着「尚未刷新」,不是「录制失败」。某次测试中,标签页打开时会话显示0个事件,关闭后显示12个事件。过早判断可能会导致重新触发完全正常的录制。
事件会在短时间间隔(几秒)内上传,但卸载时仍未刷新的内容只会暂存到
sessionStorage
,并在后续同域名页面加载时重新发送——因此会话尾部可能会延迟到下次访问时才上传。优先使用导航离开而非直接关闭浏览器,不要立即判断录制结果。

Step 5 — Session-time budget and close discipline

步骤5 — 会话时间预算与关闭规范

  • Cloud replays cap at 5 minutes of session time. Everything recorded after that is silently trimmed from the replay — the run still succeeds and reports itself accurate, so the loss is invisible unless you look for it (snapshot routes stop early; far fewer allowed events than the session has clicks). Don't leave this to feel:
    agent sessions --includeDurationSeconds
    gives you the number, and anything over 300 is losing its tail.
  • Your interaction pace eats this budget. Each find/click/verify round trip is 10-60s of recorded session time. Plan the flow completely before opening the tab, batch your actions into as few round trips as possible with short waits between them, verify from the recording afterwards rather than mid-flow, and aim to stay under 4 minutes. Several short sessions beat one long sweep.
  • Direct URL navigation is a legitimate fast path to each target page and replays fine — prefer it over slow click-paths. But hard navigations split the recording into multiple sessions, so collect every resulting session id afterwards and pass them all to
    --sessionIds
    (Step 6). Mostly this is fine, each piece staying under the replay cap. The trap is navigating again too quickly: a page reached a second or two after the previous one gets appended as the tail of that session instead of starting its own, and tails frequently do not replay — so the page renders perfectly while you drive it and still contributes no coverage. Give each page you actually care about its own dwell time (~10s) before moving on, and check in Step 6 that it shows up as a
    startUrl
    in its own right.
  • Recorder limits: a 10-minute hard cap on tab-open time marks the whole session "abandoned"; uploads flush on a 5s interval — wait ~6-8s after the last interaction before closing the tab.
  • 云端重放会话时间上限为5分钟。超过5分钟的录制内容会在重放时被静默截断——测试运行仍会成功并报告准确,因此除非主动检查,否则无法发现内容丢失(快照路由提前停止;允许的事件数远少于会话中的点击数)。不要凭感觉判断:
    agent sessions --includeDurationSeconds
    会给出具体数值,超过300秒的会话会丢失尾部内容。
  • 你的交互节奏会消耗该预算。每次查找/点击/验证的往返会占用10-60秒的录制会话时间。打开标签页前完整规划流程,将操作批量整合到尽可能少的往返中,操作间等待时间短,录制后再验证,目标是保持在4分钟以内。多个短会话优于一次长扫描。
  • 直接URL导航是到达目标页面的合法快速路径,且重放正常——优先使用该方式而非缓慢的点击路径。但硬导航会将录制拆分为多个会话,因此后续需收集所有生成的会话id,并全部传入
    --sessionIds
    (步骤6)。通常这没问题,每个部分都在重放上限内。陷阱是过快导航:在前一个页面后1-2秒到达的页面会被追加为该会话的尾部,而非启动新会话,且尾部内容通常无法重放——因此你触发时页面渲染完全正常,但不会贡献任何覆盖率。为每个你关注的页面预留约10秒的停留时间,然后再继续;并在步骤6中检查该页面是否以独立
    startUrl
    出现。
  • 录制器限制:标签页打开时间超过10分钟会标记整个会话为「已放弃」;上传会每5秒刷新一次——最后一次操作后等待约6-8秒再关闭标签页。

Step 6 — Collect the session ids and trigger the test run

步骤6 — 收集会话id并触发测试运行

First list what you actually recorded, newest first:
bash
meticulous agent sessions --limit 20 --excludeSyntheticSessions \
  --includeDurationSeconds --includeNumberUserEvents \
  --includeNumberUrlsVisited --includeStartUrl
Skip any row with
numberUserEvents
of 0 — it will replay as nothing and only dilutes the run. Note any row with
durationSeconds
over 300: it will replay, but only its first five minutes, so treat coverage from its tail as absent rather than assuming the whole flow ran.
Identify your sessions by recorded-at time and
startUrl
. Be careful here: other people — and other apps pointed at the same project — record too, so never assume the newest N rows are yours.
--recordedSince
and
--visitedUrlFilter
are the quickest way to narrow it down when the list is busy.
Expect more sessions than pages you drove: a hard navigation usually ends one session and starts another, so a five-page sweep can produce five ids. Collect all of them. A page whose URL never shows up as a
startUrl
was probably swallowed as the tail of the previous session and will not replay — re-record it on its own if you need it covered.
Then trigger:
bash
meticulous agent trigger-test-run --sessionIds "<id1>,<id2>,..."
首先列出你实际录制的会话,按时间从新到旧排序:
bash
meticulous agent sessions --limit 20 --excludeSyntheticSessions \
  --includeDurationSeconds --includeNumberUserEvents \
  --includeNumberUrlsVisited --includeStartUrl
跳过任何
numberUserEvents
为0的行——重放这类会话毫无意义,只会稀释测试运行结果。注意任何
durationSeconds
超过300的行:它会被重放,但仅前5分钟有效,因此不要指望尾部内容能贡献覆盖率。
通过录制时间和
startUrl
识别你的会话。请注意:其他人——以及指向同一项目的其他应用——也会录制会话,因此不要想当然地认为最新的N行都是你的。当列表繁忙时,
--recordedSince
--visitedUrlFilter
是最快的筛选方式。
预期会话数量多于你触发的页面数量:硬导航通常会结束一个会话并启动另一个,因此5个页面的扫描可能会生成5个id。收集所有这些id。如果某个页面的URL从未作为
startUrl
出现,它可能被前一个会话的尾部吞噬,无法重放——如果需要覆盖该页面,请重新单独录制。
然后触发测试运行:
bash
meticulous agent trigger-test-run --sessionIds "<id1>,<id2>,..."

Step 7 — Compare with a union, not a raw diff

步骤7 — 使用合并结果对比,而非原始差异

bash
meticulous agent js-coverage --headPlusTestRunIds "<newRunId>" \
  --includeAllFiles --includeCoveragePercentage > /tmp/combined-coverage.tsv
This unions your new run into the baseline run resolved from HEAD — the same run Step 1 used — so it is baseline coverage plus your new sessions' coverage. Commit resolution skips runs over an explicit session set, so the run you just triggered won't be picked as the baseline. The union is needed because
--sessionIds
replaced the selected set for that run rather than adding to it, so your run on its own covers far less than the baseline and a raw diff would read as mass regressions. Diff the union against
/tmp/baseline-coverage.tsv
: you should see zero regressions, and only the files your new flow touched improve.
Pass only your new run — a run cannot be unioned with itself.
Two signs HEAD resolved to something other than the golden-set run: it is rejected with "is the run being queried", or every file has dropped (which means you unioned into another narrow session set, not a regression). The most likely cause is a pinned-session run predating the skip. Either way, name both sides explicitly, using the baseline id from Step 0:
bash
meticulous agent js-coverage --testRunIds "<baselineRunId>,<newRunId>" \
  --includeAllFiles --includeCoveragePercentage > /tmp/combined-coverage.tsv
If the union is rejected because the runs executed different commits, that is the
main
/clean-tree precondition biting — see the top of this skill.
bash
meticulous agent js-coverage --headPlusTestRunIds "<newRunId>" \
  --includeAllFiles --includeCoveragePercentage > /tmp/combined-coverage.tsv
该命令会将你的新运行合并到从HEAD解析的基线运行中——即步骤1使用的同一运行——因此结果是基线覆盖率加上新会话的覆盖率。提交解析会跳过显式会话集之外的运行,因此你刚触发的运行不会被选为基线。需要合并的原因是
--sessionIds
替换该运行的选定会话集,而非追加,因此单独运行你的会话覆盖范围远小于基线,原始差异会显示为大量回归。将合并结果与
/tmp/baseline-coverage.tsv
对比:你应该看不到任何回归,只有新流程触及的文件覆盖率提升。
仅传入你的新运行——运行无法与自身合并。
两种迹象表明HEAD解析的不是黄金集运行:命令被拒绝并提示"is the run being queried",或所有文件覆盖率下降(这意味着你合并到了另一个狭窄的会话集,而非真正的回归)。最可能的原因是存在早于跳过规则的固定会话运行。无论哪种情况,都明确指定两边的id,使用步骤0中的基线id:
bash
meticulous agent js-coverage --testRunIds "<baselineRunId>,<newRunId>" \
  --includeAllFiles --includeCoveragePercentage > /tmp/combined-coverage.tsv
如果合并被拒绝,原因是运行执行的提交不同,这就是
main
/干净工作区的前提条件未满足——见本文顶部说明。

Step 8 — Verify and report

步骤8 — 验证并报告

For each traced target file: did coverage move? For each presumed-dead file (the
.meticulousignore
candidates from Step 2): did it stay at 0% in the union comparison (confirming it's genuinely unreachable)?
If a well-traced target didn't move, don't assume the recording failed — re-check the session first (Step 4: does it exist, did it capture user events and URL visits, is it abandoned, is its
startUrl
the page you drove?), then re-check whether the click actually goes through the file you expected (Step 3) rather than a sibling/parent component. Report honestly if a target remains unresolved; don't claim success without the coverage number to back it.
When you report the gains, be clear about what they are not yet: the test run proves the coverage is reachable, but the project's own coverage figure will only improve once the next session selection picks these new sessions up into the selected set. Until then nothing changes for recurring runs.
对于每个追踪的目标文件:覆盖率是否提升?对于每个假定的死文件(步骤2中的
.meticulousignore
候选路径):在合并对比中是否仍保持0%(确认其确实无法访问)?
如果追踪充分的目标文件覆盖率未提升,不要想当然地认为录制失败——先重新检查会话(步骤4:会话是否存在,是否捕获到用户事件和URL访问,是否已放弃,
startUrl
是否是你触发的页面),然后重新检查点击是否确实经过了你预期的文件(步骤3),而非兄弟/父组件。如实报告未解决的目标;不要在没有覆盖率数据支持的情况下声称成功。
报告提升效果时,请明确说明当前的局限性:测试运行证明覆盖率可被触及,但项目自身的覆盖率数值只有在下次会话选择将这些新会话纳入选定集后才会提升。在此之前,定期运行的结果不会有任何变化。

Step 9 — Open the
.meticulousignore
PR

步骤9 — 提交
.meticulousignore
的PR

The second deliverable. Branch, commit the
.meticulousignore
change, and open a PR.
Only propose paths you are confident are not coverable at all. The bar is "no session could ever execute this", not "I didn't get to it today". Prefer directory-level rules over long lists of individual files — a directory rule stays correct as files are added, whereas a file list silently goes stale. In practice most entries come from the safe-exclusion categories in Step 2 plus whatever whole non-browser packages the baseline showed at a uniform 0%.
Confirm before you commit: every path you are about to ignore stayed at 0% in the Step 7 union. A path your own new sessions just covered obviously does not belong in the ignore list, and that check catches it.
A common structure is "ignore everything, then un-ignore what does run in the browser" — the pattern Meticulous's own monorepo uses:
undefined
这是第二个交付成果。创建分支、提交
.meticulousignore
的修改、并打开PR。
仅提议你确信完全无法覆盖的路径。标准是「任何会话都永远无法执行该代码」,而非「我今天没覆盖到它」。优先使用目录级规则,而非长文件列表——目录级规则在添加新文件时仍保持正确,而文件列表会静默过时。实际上,大多数条目来自步骤2中的安全排除类别,以及基线显示全0%的非浏览器包。
提交前确认:你要忽略的每个路径在步骤7的合并结果中仍保持0%。你自己的新会话刚覆盖的路径显然不应加入忽略列表,该检查能捕获这类情况。
常见的结构是「忽略所有内容,然后取消忽略在浏览器中执行的内容」——Meticulous自身的单体仓库就使用这种模式:
undefined

Ignore everything except packages that are executed in the

忽略所有内容,除了在浏览器中执行且有意义前端覆盖率的包。

browser and have meaningful frontend coverage.

packages/* !packages/<frontend-app>/ !packages/<frontend-app>/** !packages/<shared-ui-lib>/ !packages/<shared-ui-lib>/**

Note that `.meticulousignore` follows gitignore semantics, so a file cannot be
re-included once its parent directory is excluded — un-ignore the directories
(`!some/dir/**/`) as well as the files.

In the PR description, give the reasoning for each rule — _why_ this code
cannot run in a browser (it's a Node-only build script, a test harness, a
backend package). The baseline can only show you that something is at 0%
today, which is never proof it is unreachable, so the justification has to
come from what the code actually is. Also call out explicitly:

- anything you left **out** of the ignore list despite low coverage because
  you suspect a source-map/attribution gap rather than dead code (Step 2)
- anything unreachable only because of environment or feature-flag config
  rather than structurally — that is the reviewer's judgement call, not
  yours, so flag it instead of silently ignoring it
packages/* !packages/<frontend-app>/ !packages/<frontend-app>/** !packages/<shared-ui-lib>/ !packages/<shared-ui-lib>/**

注意`.meticulousignore`遵循gitignore语义,因此父目录被排除后,无法重新包含其子文件——需同时取消忽略目录(`!some/dir/**/`)和文件。

在PR描述中,说明每个规则的理由——**为什么**该代码无法在浏览器中执行(它是仅Node执行的构建脚本、测试工具、后端包)。基线只能显示当前覆盖率为0%,但这永远无法证明代码无法访问,因此理由必须基于代码的实际用途。还需明确指出:

- 尽管覆盖率低,但你怀疑是源映射/归因问题而非死代码,因此未加入忽略列表的内容(步骤2)
- 仅因环境或功能标志配置无法访问,而非结构上无法访问的内容——这需要评审者判断,因此请标记出来,不要静默忽略

Reference

参考

references/worked-example.md
runs the whole skill against a small app (kanban-demo), with the real coverage deltas. Worth reading for calibration: it has no 0% files at all, one target that gained coverage and one that recorded cleanly and then failed to replay — and it shows how to tell the difference from executed ranges.
references/worked-example.md
展示了针对小型应用(kanban-demo)执行整个流程的完整示例,包含真实的覆盖率变化。值得一读以校准操作:该示例中没有0%覆盖率的文件,一个目标覆盖率提升,另一个录制正常但重放失败——并展示了如何通过执行范围区分这两种情况。