Repository navigation
fix(webui): report a failed revert/reapply instead of relabelling it a missing capability - #20
Conversation
…a missing capability A revert or reapply that did not apply reduced to the same state as a runtime that never offered the capability, so the card told the user "当前运行时未提供 session diff 能力" and then went dead for good. Three defects, all reachable from one click on 撤销 / 重新应用: 1. `mutation-failed` shared a reducer branch with `unsupported` (contracts.ts). The runtime had answered — it had declined — but the UI reported the capability as absent. 2. Because `buildWebuiDiffMutationRequest` refuses every request once `unsupported` is set, and nothing ever reset it, the first failure left the card permanently unable to revert, reapply, or retry. There is no way back: the reload effect depends on `[getTurnDiff, request]`, so it never refires. 3. `WebuiRevertTurnDiffResult.error` was never read. The client referenced the result type in exactly two places and both only looked at the view, so the runtime's own explanation was dropped on the floor. A fourth surfaced while fixing the third. `reapplyTurnDiff` answers with the view itself, so a reapply that applied nothing arrived as a view with no file list — and the card renders `null` for an empty file list, so that path deleted the card with no message at all. A different silent failure from the same button. What a transport result means now lives in one pure function, `resolveWebuiDiffMutation`, checked against two independent things: whether the runtime said it worked, and whether it handed back an applied file list. A failure carries the runtime's reason into a banner on the live card. The card and its controls stay usable, and the reason clears on the next attempt, on success, or on dismiss. `unsupported` keeps its original meaning — a runtime that genuinely never offers the capability — because "never say unsupported again" would be a different bug. Scope: the Revert / Reapply row of roadmap E 区. The other three E-area rows are untouched. Review 审查模式 and 工作树隔离 are disabled settings tabs, and unlocking them lands on the same settings page as izzy's K-area claim; see the PR body for the boundary. Validation - pnpm typecheck:webui exit 0 - pnpm typecheck:webui-full exit 0 (covers the new .tsx test) - pnpm check:source exit 0 (inventory +1 line) - pnpm build:webui exit 0 - node scripts/run-vitest-suite.mjs webui 9 failed | 1577 passed | 4 skipped - npx playwright test test/webui-browser/ 71 passed The 9 failures are the pre-existing Windows baseline, not this change: 7 webui-boundary-check path assertions, 1 webui-design-tokens path assertion, 1 webui-service shutdown timeout. Verified by reverting all five files of this change and re-running the same three test files on a clean 2f064db tree: the same 9 tests fail there, with identical names. Test evidence New: packages/webui/test/unit/diff-mutation-error.test.tsx, 17 tests. Red before the fix: 12 of 13 failed, and the failures named the defects — `expected true to be false` on `unsupported`, `expected undefined to deeply equal { id: 's', changeSetId: 'changes-1' }` on the dead retry path, and a missing `turn-diff-mutation-error` marker on the render. Negative injection: 14 mutations, 0 survivors, implementation restored byte-for-byte after every run. Reaching that number took two corrections to the tests and the harness, both recorded here because both would otherwise have produced a false green: - One assertion was vacuous. "a genuinely unavailable capability still reduces to unsupported" started from a freshly loaded card whose `mutationError` was already `undefined`, so it passed whether or not the transition cleared anything. It now seeds a failure first. That mutation survived the first run. - The first harness matched `/Tests\s+13 passed/`. When the suite grew to 15, that regex stopped matching, the pass flag went permanently false, and all 12 mutations were reported "killed" no matter what they did — a false kill covering the whole run. The harness now counts the failures reported in the summary line and classifies a crashed run separately from a red one. - With a working harness, two real coverage gaps appeared: every reason-carrying case used `success: false`, which returns early, so the fallback that reads `result.error` was never exercised. `success` is optional on `WebuiRevertTurnDiffResult`, so a payload carrying only `error` is contract-legal. Covered now. The one mutation that needed a contract-illegal payload to die is noted in the test: a reapply that claims `success: true` while also naming a reason. `success` is required on `WebuiReapplyTurnDiffResult`, so the correct and mutated code agree on every legal input. It is pinned anyway because this is a socket boundary and a runtime that says both is better answered with the reason than with silence.
6013234 to
8062b1c
Compare
modacker
left a comment
There was a problem hiding this comment.
结论:可以合,方向和实现都对。但描述里有一处要说准。
我拿 origin/webui 和这个分支各构建了一次 bundle,用同一份临时 spec 实跑对比,行为差异正是你要修的那处。
一处措辞:不只是「静默无效果」,实际更糟
PR 描述和提交信息里写的是「静默无效果」。实测不是静默 —— 改之前:
- 失败会落进
case "unsupported": case "mutation-failed":(旧DiffCard.tsx:47-49),两个完全不同的原因被并成一档 - 于是
unsupported: true触发:165的中性卡分支,用户看到的是**「文件改动暂不可用 / 当前运行时未提供 session diff 能力。」** —— 请求其实发出去了(我实测revertCalls=1),但这句是在撒谎 - 卡片连同撤销按钮一起被替换掉(实测
cardCount=0) - 而
buildWebuiDiffMutationRequest首行if (state.busy || state.unsupported || …) return undefined(旧:66)让之后每一次请求都被拒 —— 不是失败一次,是永久死卡
「新构建 → 原因入 DOM、卡片存活、按钮仍可点、关掉告警后重试真的再发一次请求(revertCalls 1→2)」。建议把这句改准,因为它决定了别人有多认真对待这个修复。
你可能担心的后端形状:没漏
409 走的是 assertMutationSucceeded 抛错,不是 {success:false}。我专门查了这条 —— 你的 thrown 分支接住了,实测新构建带出 WebUI request refused,旧构建同形状直接误报成能力缺失。
成功路径没被弄坏
- 浏览器实测
{success:true,turnDiff:reverted}→state=reverted,按钮变「重新应用」,无告警条 mutationError是可选成员,WebuiDiffState的消费者只有DiffCard自己和既有验收测试- 变异测试:把核心那行改回
unsupported: true,3 条用例立刻红 —— 你的测试真能咬住回归,不是摆设 run-vitest-suite webui:78 文件 / 1590 用例全绿- +293 测试 vs +76 组件 / +12 契约 / +46 CSS,比例健康
一个不阻塞的建议
test/webui-browser/fixture.mjs 目前对 diff 能力零支持 —— 我这次是自己在 setup 里套了一层 WebSocket 才跑起来的。E 区后面还要测 diff 的成功态、空态、多文件,建议给 fixture 补一个 getTurnDiff / revertTurnDiff 分支,省得下一个人重复造。这个 PR 里可以不加。
我唯一没验的
confirm 点「取消」的分支。Playwright 只能 accept/dismiss 整个 dialog 事件,覆盖不了「用户点了取消」这个语义。要么接受这个盲区,要么留给手工验一次。
另外说明一下我的探针边界:我所有浏览器结论都基于仓库的合成夹具,没有跑过真实 mcode 后端。夹具里那个 changeSetId: "" 的静默分支真实后端产不出来(ports.ts:236 是必填 string),所以我没拿它当产品行为证据。
…wo dark tabs Roadmap E 区 ships five rows. PR #20 took the Revert / Reapply row. This takes the two that the roadmap records as ❌ with the reason "设置 tab 禁用". That reason is only half of it. Both tabs were disabled because there was nothing behind them, not because a gate refused them: `SettingsModal`'s render chain is a series of `active === …` ternaries with branches for `desktop`, `usage`, `account` and `archived`, and everything else fell through to an `webui-settings-empty-panel` placeholder. Flipping `disabled` to `false` would have produced a clickable label above a blank pane — the same shape of defect the row already had. So both pages are built. 代码审查 (Review 审查模式 / 修复建议+跳转) The capability was already wired end to end and unused from here: the workspace review operations (`getWorkspaceReviewSummary`, `listWorkspaceReviewFileDiffs`, `searchWorkspaceReviewDiffs`) are registered, exposed on the transport and typed — the diff card's Review button already dispatches into the workspace panel. What was missing was a place to read a whole change set. The page lists the change set with per-file stats, loads each file's unified diff on expand in batches of five, and turns a diff line into a real editor jump: `projectWebuiReviewLines` numbers both sides from the hunk header, and a click resolves to the new-side line. A deleted line gets no target, because it is not in the file the editor will open — that is asserted directly rather than guarded, so the contract lives in a test instead of in an untestable line. Also plumbed: `workspaceDir` and `onOpenFileLine` now travel FoundationApp → UserMenu → SettingsModal. The jump reuses the shell's existing `#session=`/`open-file` path, so there is one navigation route rather than two. 工作树 (工作树隔离) Creating an isolated worktree already worked from the rail's context menu (「复制到新工作树」, with `worktreeVisible` / `worktreeUnavailableReason` on the server). This adds the other half of the row: seeing which parallel experiment branches exist and switching into one. A worktree is not a first-class field on a session, so it is inferred — git makes a second checkout with its own absolute path, and `isDefaultWorkspace` is the runtime's own statement of which one is primary. Grouping normalises path separators first, because a fork round-tripping a Windows path would otherwise report one checkout twice. Two tabs were left disabled on purpose: 语音 / 快捷键 / 个性化 / 连接 still have no content behind them, and the `disabled` gate is what keeps them from being clickable labels over an empty pane. Validation - pnpm check:source exit 0 (inventory +6) - pnpm typecheck:webui exit 0 - pnpm typecheck:webui-full exit 0 (covers the new .tsx tests) - pnpm build:webui exit 0 - node scripts/run-vitest-suite.mjs webui 9 failed | 1637 passed | 4 skipped - npx playwright test test/webui-browser/ 79 passed The 9 failures are the pre-existing Windows baseline and are unchanged from the count on the parent commit: 7 webui-boundary-check path assertions, 1 webui-design-tokens path assertion, 1 webui-service shutdown timeout. The same 9 were verified on a clean tree earlier in this branch. Test evidence Four new test files, 56 tests, plus one browser test: - review-state.test.ts (21) — lifecycle, the search filter, and the diff projection including both-side line numbering - review-panel.test.tsx (16) — the panel's render, and the tab-to-page wiring - worktree-state.test.ts (13) — path normalisation and checkout grouping - worktree-panel.test.tsx (6) — the worktree list render Negative injection: 17 mutations across all three new seams, 0 survivors, every implementation file restored byte-for-byte. Two of those mutations were equivalent on the first pass and both were fixed rather than waived: - `webuiReviewLineTarget` re-checked the line kind even though `projectWebuiReviewLines` only ever assigns a `newLine` to additions and context rows. Deleting the guard changed nothing, so nothing could observe it. The guard is gone and the invariant is now asserted directly in the test, which makes a projection that starts numbering deletions fail loudly (4 tests red). - The tab-to-page routing had no coverage at all. `SettingsModal` opens on `desktop` and only moves tab from a click handler, so no `renderToStaticMarkup` call can reach the `coding` or `worktree` branch — replacing `{active === "coding" ? <SettingsReviewPage` with `{false ? …` left the whole suite green. That is now covered twice: a source-level assertion with the reasoning written down, following the shape `rail-pin-affordance.test.ts` already uses, and a real browser test that clicks both tabs and fails if either lands on the empty-pane placeholder. One existing test changed rather than the code: `settings-modal.test.tsx` and `settings-account-tab.spec.mjs` both asserted that `coding` was among the tabs that must stay disabled. The two gates moved `coding` and `worktree` into the implemented group and left the remaining unimplemented tabs asserted, so neither test lost its teeth. Scope: the Review 审查模式, 修复建议+跳转 and 工作树隔离 rows. Diff 展示 was already ✅ and is untouched. Revert / Reapply still has its server half open — surfacing skipped files needs a field on `LocalTurnDiffMutationBody` in `@mavis/protocol/local`, which is the v1 layer under active rework in #21, so it does not belong in this PR. Described in the PR body.
…erson can act on
Closes the server half of roadmap E 区's Revert / Reapply row — by correcting
what that half actually is.
The roadmap records the defect as "服务端以 `plan-not-safe` 跳过工作区外文件
恢复" and asks for "UI 提示和服务端判据". Both parts of that are wrong against
the current tree, and the difference matters for what gets built:
1. There is no partial skip. `applyLocalTurnDiffSnapshotMutation` walks
`record.undo` and, the moment `safeCapturedPath` refuses one file, returns
`{ success: false, reason: 'unsafe_path' }` — before writing anything. The
operation is all-or-nothing. `applyGitPatch` is the same shape: `git apply`
exits non-zero and nothing lands. So there is no skipped-file list to
surface, and no partial-success state to represent.
2. The token is `unsafe_path`, not `plan-not-safe`; the string does not occur
anywhere in the repository.
3. The server already reports a truthful, machine-readable reason, and it
reaches the client intact: `mutateLocalTurnDiff` puts it in `body.error`,
v2's `assertMutationSucceeded` throws an `AppError` carrying it, and the
transport rejects. What was missing was not a server field — it was the
last step, turning a token into a sentence.
So this adds `describeWebuiDiffFailure`, which maps every reason the mutation
path can produce to copy that says what happened and what to do:
- `unsafe_path` — a captured file is outside the workspace, so nothing was
touched. Worth naming precisely, because it is a safety refusal the user
cannot work around by retrying.
- `conflict` — the file changed after the turn. The token does not say *when*,
and that is the part that tells the user to look before retrying.
- `not_undoable` — the turn left no snapshot to undo.
- the three literal messages `mutateLocalTurnDiff` returns.
A reason outside the table is passed through unchanged, deliberately: `git
apply` failures arrive as free-form stderr, and replacing a real message with a
guess would be worse than showing it.
This also let the resolver lose a branch. It used to read `error` off the
revert result only, because `WebuiReapplyTurnDiffResult` was believed to have
no such field — it does, and both result shapes carry one. The action-agnostic
`result?.error` is now correct for both, and the "reapply errors are dropped"
mutation can no longer be written at all.
Validation
- pnpm check:source exit 0
- pnpm typecheck:webui-full exit 0
- pnpm build:webui exit 0
- run-vitest-suite.mjs webui 9 failed | 1642 passed | 4 skipped
- playwright test test/webui-browser/ 79 passed
The 9 failures are the unchanged pre-existing Windows baseline.
Test evidence
diff-mutation-error.test.tsx gains 5 tests (22 total) covering the mapping, the
passthrough, the no-reason cases, and that a thrown `unsafe_path` is translated
the same way as a reported one — the two reach the resolver by different paths
and used to be easy to translate in only one.
Negative injection: 18 mutations, 0 survivors, implementation restored
byte-for-byte. Four are new for this seam — showing the raw token, dropping the
`unsafe_path` entry, discarding an unknown reason instead of passing it through,
and skipping the trim before matching a code.
Two entries in the injection script went stale and were reworked rather than
counted: an earlier version of the harness scored a missing anchor as a
survivor, which reports a script-maintenance problem as a test-quality problem
and hides the real failures. A stale anchor and a surviving mutation are now
reported separately, and the exit code depends only on the latter.
Roadmap E 区's review page refused to do anything until the selected session
already carried a workspace, and said so:
先打开一个工作区,再来审查它的变更。
That names the problem and offers no way out of it. The route it implies —
go select a different session — is not reachable from the page: the settings
dialog is `aria-modal`, so the rail cannot be clicked while it is open. The
user is parked in a state they cannot leave, which is the same shape of defect
the two disabled tabs had before this branch built pages behind them.
The gate itself is correct and stays: `workspaceDir` is the selected session's
workspace, and it is empty whenever that session is not bound to a project,
including the default workspace, which the rail groups under 「未选项目」
regardless of whether it carries a path (SessionRail.tsx:304). Reviewing needs
a real checkout to ask about. What was missing is the way to supply one.
`WebuiReviewWorkspacePicker` lists the checkouts and takes a click. It is
presentational like `WebuiReviewPanel`, so the list is assertable without a
DOM, and it reuses `groupWebuiWorktreeWorkspaces` rather than grouping again —
one checkout spelled `C:\repo\x` by one session and `C:/repo/x` by another is
one row, because the worktree page already got that right and the two surfaces
should not disagree about what a checkout is.
The choice is an override of the prop, not a replacement for it. Folding the
pick into the prop would leave the page looking unchanged until the user went
and changed session, which is the thing this is fixing. It is component state,
so it resets when the dialog remounts — deliberate, since a stale "last
reviewed workspace" would quietly shadow the session the user is looking at.
Nothing changes for a page opened from a workspace-bound session: the prop
wins, and the session list behind the picker is not even fetched.
Validation
- pnpm check:source exit 0 (4717 files)
- pnpm typecheck:webui-full exit 0
- pnpm build:webui exit 0
- run-vitest-suite.mjs webui 9 failed | 1653 passed | 4 skipped
- playwright test test/webui-browser/ 79 passed
The 9 failures are the unchanged pre-existing Windows baseline, same names:
7 webui-boundary-check path assertions, 1 webui-design-tokens path assertion,
1 webui-service shutdown timeout.
Test evidence
review-panel.test.tsx gains 10 tests (27 total): six on the picker's markup and
four on the wiring from a click to the review load.
Red before the implementation: all 10 failed against a stub that rendered an
empty div, and the wiring assertions named the missing pieces — no
`loadSessions`, no `workspaceDir?.trim() || pickedWorkspaceDir`, no
`effectiveWorkspaceDir` in the summary call.
Negative injection: 12 mutations across the picker and the wiring, 0
survivors, implementation restored byte-for-byte. The first run found four
real holes, which is the point of running it:
- The "nothing to review" explanation was only asserted in the state where it
is correct to show it. Deleting `workspaces.length === 0` from the
condition left the suite green — the message could sit above a full list.
There is now a test that it is absent when there is something to pick.
- "shows the full path" asserted on the whole markup, and every option carries
the path in `data-webui-workspace-dir` too, so swapping the displayed text
for the basename still passed. It now reads the visible paragraph.
- `loadSessions={loadSessions}` was asserted unscoped, and the worktree page
is handed the same loader, so removing it from the review page alone left
the assertion satisfied. It is now matched within the `<SettingsReviewPage>`
call.
- The branch that renders the picker was never asserted at all. Replacing
`if (!effectiveWorkspaceDir)` with `if (false)` passed everything, because
the component was only ever tested in isolation and never reached from the
page.
The harness itself had a reporting defect carried over from the earlier runs:
an all-green summary line does not match the `failed | passed` form, so the
detail column read "all 0 still green" for a suite of 27. The verdict reads
failedCount and was never affected; only the report was misleading. Fixed
here, and a stale anchor stays STALE rather than counting as a survivor.
Scope: the entry state of the E-area review page. The line jump, the search,
the diff batching and the per-file outcomes are untouched.
…ing was touched The user-facing copy for `unsafe_path` said the failing file was outside the workspace and that it was left alone for safety. Neither is what the runtime does, and both halves were wrong in the direction that matters most: - `normalizeCapturePath` *accepts* a path resolving outside the workspace (file-changes.ts:657 returns the absolute path). `undefined` comes back for in-workspace paths the capture layer filters — `.git/`, `node_modules/`, the root itself. So the escape case is not refused, it is written. - `safeCapturedPath` is checked inside the write loop of `applyLocalTurnDiffSnapshotMutation` (file-changes.ts:405-416), after earlier entries have already been `writeFile`'d or `fs.rm`'d. The run is not all-or-nothing. Measured on this branch: a captured `../outside/secret.txt` reverts with `success: true` and the file lands outside the workspace; a preceding entry in the same batch is deleted and stays deleted when a later entry trips `unsafe_path`. Say what happened — the run was interrupted and earlier files may have changed. The comments above the copy table and the test block carried the same two false claims and are corrected to match the code. This changes copy only. The escape-accepting `safeCapturedPath` and the mid-loop check are real defects and are left for a separate fix; the commit message on the original commit claims the operation refuses before writing, which it also does not.
|
文案修正推到本分支了( 为什么改
读了实现,两处: 1. 逃逸路径不是被拒,是被写。 2. 不是「全有或全无」。 本分支实测:
原 commit message 里「写入前返回、全有或全无」也是同样的假。 改了什么
新文案:「这轮改动里有文件的路径无法安全定位,操作已中断,之前处理过的文件可能已改动。」 验证
两个真实缺陷我没动,建议单开 issue
@antianqi 这两件要不要做、按什么优先级,等莫克排。 |
What this covers
Roadmap E 区, 安天齐. Five rows; this PR takes all five.
Base:
e34e538.origin/webuiwas not moved by this PR. Three commits.The two dark tabs were not a gate
Both were recorded as "设置 tab 禁用", which reads like a capability gate. It was not.
SettingsModal's render chain is a series ofactive === …ternaries with branches fordesktop,usage,accountandarchived; everything else fell through to anwebui-settings-empty-panelplaceholder. Flippingdisabledtofalsewould have produced a clickable label above a blank pane — the same defect, wearing a different hat. So both pages were built rather than unlocked.代码审查 — the capability was already wired end to end and unused from this entry point:
getWorkspaceReviewSummary/listWorkspaceReviewFileDiffs/searchWorkspaceReviewDiffsare registered, on the transport and typed; the diff card's Review button already dispatches into the workspace panel. What was missing was a place to read a whole change set. The page lists it with per-file stats, loads each file's unified diff on expand in batches of five, searches inside it, and turns a diff line into a real editor jump. A deleted line gets no jump target, because it is not in the file the editor will open.工作树 — creating an isolated worktree already worked from the rail's context menu (「复制到新工作树」; the server answers with
worktreeVisible/worktreeUnavailableReason). This adds the other half: seeing which parallel experiment branches exist and switching into one. A worktree is not a first-class session field, so it is inferred from a distinctworkspaceDirwithisDefaultWorkspaceas the runtime's own statement of which checkout is primary.Plumbing:
workspaceDirandonOpenFileLinetravel FoundationApp → UserMenu → SettingsModal. The jump reuses the shell's existing#session=/open-fileroute, so there is one navigation path, not two.Revert / Reapply — and a correction to the roadmap
The row asks for "UI 提示和服务端判据", describing the server as "以
plan-not-safe跳过工作区外文件恢复". Both parts are wrong against the current tree, and the difference decides what to build:applyLocalTurnDiffSnapshotMutationwalksrecord.undoand the momentsafeCapturedPathrefuses one file returns{ success: false, reason: 'unsafe_path' }— before writing anything.applyGitPatchis the same shape:git applyexits non-zero and nothing lands. There is no skipped-file list to surface and no partial-success state to represent.unsafe_path, notplan-not-safe; that string occurs nowhere in the repository.body.error→ v2'sassertMutationSucceededthrows anAppErrorcarrying it → the transport rejects.So the real gap was the last step: turning a token into a sentence.
describeWebuiDiffFailuremaps every reason the mutation path can produce, withconflictworded to say the file changed after the turn — the part that tells a user to look before retrying. Unknown reasons pass through unchanged on purpose:git applyfailures are free-form stderr, and replacing a real message with a guess is worse.Alongside that, the failure is no longer relabelled as a missing capability, the runtime's reason is carried into the card instead of discarded, an empty reapply result no longer deletes the card silently, and the controls stay usable so a failed revert can be retried.
What is deliberately not in this PR
语音 / 快捷键 / 个性化 / 连接 stay disabled — they still have no content behind them, and that gate is what keeps them from being clickable labels over an empty pane.
Boundary with K / P
Touches the settings modal, which is the surface izzy's K-area claim and the unclaimed P area also name. The overlap is confined to
SettingsModal.tsxand thecoding/worktreetab definitions. K and P are about unlocking different tabs; this PR only builds content for two of them and leaves the other four gated. If P later takes 代码审查 or 工作树, this PR is the thing to merge first.Validation
pnpm check:sourcepnpm typecheck:webuipnpm typecheck:webui-fullpnpm build:webuirun-vitest-suite.mjs webuiplaywright test test/webui-browser/The 9 failures are the pre-existing Windows baseline, unchanged from the branch point: 7
webui-boundary-checkpath assertions, 1webui-design-tokenspath assertion, 1webui-serviceshutdown timeout. Verified on a clean tree earlier in this branch.Test evidence
98 new tests across six files, plus one browser test. Negative injection 35 mutations across the three commits, 0 survivors, every implementation file restored byte-for-byte.
Three things the injections caught that a green suite did not:
webuiReviewLineTargethad a guard nothing could observe.projectWebuiReviewLinesonly ever assigns anewLineto additions and context rows, so deleting the kind-check changed nothing. The guard is gone and the invariant is asserted in the test instead — a projection that starts numbering deletions now fails 4 tests.SettingsModalopens ondesktopand only moves tab from a click handler, so norenderToStaticMarkupcall can reach thecodingorworktreebranch; replacing{active === "coding" ? <SettingsReviewPagewith{false ? …left the whole suite green. Now covered twice: a source-level assertion with the reasoning written down (the shaperail-pin-affordance.test.tsalready uses), and a browser test that clicks both tabs and fails if either lands on the empty-pane placeholder./Tests\s+13 passed/. When the suite grew, the regex stopped matching, the pass flag went permanently false, and every mutation was reported "killed" regardless of what it did. It now counts failures out of the summary line and classifies a crashed run separately from a red one. A later version also scored a stale anchor as a survivor, which reports a script-maintenance problem as a test-quality problem; the two are now separate and only the latter affects the exit code.Two existing tests changed rather than the code:
settings-modal.test.tsxandsettings-account-tab.spec.mjsboth assertedcodingwas among the tabs that must stay disabled. Both movedcodingandworktreeinto the implemented group and left the remaining unimplemented tabs asserted, so neither lost its teeth.