Skip to content

fix(app): gate healing on resolution failures and prefer same-environment baselines - #431

Open
PhenX wants to merge 7 commits into
mainfrom
claude/audit-quick-wins-server
Open

fix(app): gate healing on resolution failures and prefer same-environment baselines#431
PhenX wants to merge 7 commits into
mainfrom
claude/audit-quick-wins-server

Conversation

@PhenX

@PhenX PhenX commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

What & why

The server-analysis slice of the failing-run data audit's quick wins (audit §7 items 2, 3, 4, 10 and 13; findings in §3.5, §4.1 and Appendix A). One commit per item. Nothing from the audit document itself is on the branch.

Item 2 — gate locator healing on a resolution failure

A toHaveCount mismatch (locator resolved to 51 elements), a disabled or hidden element, or a page.goto timeout used to get a "Recommended fix" that rewrote a locator that already worked, and the run page showed a "Locator fix" signal built from ARIA guesses. classifyLocatorResolution (pure, in shared/) reads Playwright's call log: healing applies when the locator never resolved (waiting for <locator> with no later locator resolved to line), resolved to 0 elements, or hit a strict-mode violation; it does not when the locator resolved and the action or assertion failed afterwards, nor for navigation errors. resolveHealingForCase returns applicable: false + a one-line reason before any ladder rung (ARIA fallback included), so the batch variant, the run's failure groups, the auto-heal policy, the fix plan, the MCP tools and the AI context inherit the gate; the panel shows the reason as one line instead of a menu.

  • shared/locator-resolution.ts (new), packages/core/src/locator-healing-types.ts, server/utils/locator-healing.ts, shared/handlers/test-runs.ts, server/utils/heal/policy.ts, server/utils/fix-plan.ts, server/utils/ai-context.ts, server/utils/mcp/tools.ts, shared/mcp-tools.ts, app/demo/api/diagnosis-context.ts, app/components/shared/LocatorHealingPanel.vue, apps/docs/reporter.md, apps/docs/ai-diagnosis.md
  • Tests: tests/unit/locator-resolution.test.ts (new; the five call-log shapes plus navigation, no-locator and ANSI cases), locator-healing-resolve.test.ts, heal-policy.test.ts, demo-seed-consistency.test.ts (the demo's resolved-but-hidden cluster 9 is now asserted as not healed — the CSS is the bug there, not the selector)

Item 3 — baselines prefer the same environment, then the same branch

The environment diff picked the test's last pass on the same browser and nothing else, so a development failure was diffed against a production pass and the diff listed the environment label itself. The visual diff preferred the branch but ignored the environment. Both now rank candidates through one shared ordering (same environment → same branch → recency); when no same-environment pass exists the fallback still happens but carries a baselineNote ("compared with a production run; no passing development run of this test exists") that the cards and the AI context show, and the environment label is never a diff entry in that case.

  • shared/baseline-order.ts (new), server/utils/environment-diff.ts, server/utils/visual-diff.ts, shared/environment-diff.ts, server/utils/ai-context.ts, app/components/shared/EnvironmentDiffCard.vue, app/components/test-case/VisualDiffCard.vue, app/utils/help-content.ts, apps/docs/concepts.md
  • Tests: tests/unit/baseline-order.test.ts (new)
  • Follow-up, deliberately not in this PR: computeRegressionSignals and run insights remain environment-blind (branch-aware only), as the brief asked.

Item 4 — keep network request start times

The reporter sends startTime per request but network_requests had no column, so it was dropped and the AI context's t+Nms offset read a field that was always undefined. Nullable start_time (epoch ms) in both dialects with generated migrations (SQLite integer, PostgreSQL bigint, declared in the dialect-drift allowlist), persisted from the wire by the shared builder, exposed on the execution response and as firstStartTime / lastStartTime on the run-level endpoint summaries, live in the AI-context offset, seeded by the demo generator and simulator, and used to order the execution page's captured requests.

  • server/database/schema.sqlite.ts, server/database/schema.pg.ts, server/database/migrations/0054_stale_zzzax.sql, server/database/migrations-pg/0055_deep_lester.sql (+ journals/snapshots), server/utils/network-request-helpers.ts, server/utils/persist-run-cases.ts, app/demo/api/reporter.ts, shared/handlers/test-cases.ts, shared/handlers/test-runs.ts, types/api.ts, server/utils/ai-context.ts, scripts/generate-demo-seed.mjs, public/demo/seed.version.json, app/demo/simulator.ts, app/components/test-case/TestCaseNetworkRequests.vue, apps/docs/capture-fixtures.md
  • Tests: tests/unit/route-and-network.test.ts, tests/unit/schema-dialect-drift.test.ts

Item 10 — close the loop on fix verification

A cluster could read "Fix verified" and "Open" at once. A diagnosis-verified verdict now sets an open cluster to resolved, and regressed sets a resolved cluster back to open, each appending a system line to the triage note ("Resolved automatically: diagnosis verified in run #N" / "Reopened automatically: regressed in run #N"). stopped-failing alone changes nothing; ignored clusters are never touched; the verdict badge stays separate from the status as the docs describe. Two new events, cluster.fixed (every recorded fix, with the verdict in its payload) and cluster.regressed, are rendered for browser, Slack, email digests and webhooks. The demo validates events against the shared list, so it picks them up without a mirror change.

  • server/utils/fix-verification.ts, shared/notification-events.ts, server/utils/notifications/dispatch.ts, app/composables/useNotificationStream.ts, apps/docs/notifications.md, apps/docs/ai-diagnosis.md
  • Tests: tests/unit/fix-verification.test.ts (new; in-memory SQLite, SCM and emit mocked), tests/unit/notification-events.test.ts, tests/fix-verification.spec.ts (E2E: a cluster resolved by hand is reopened with the note kept)

Item 13 — deterministic cluster titles

Without an AI title, clusters displayed their raw masked signature as a name. describeCluster builds a title from the error type, the locator's primary target, the navigation route and the spec basename (Timeout on getByLabel('Email address') in checkout.spec.ts, toHaveCount mismatch on getByRole('row') in users.spec.ts, Navigation timeout on /users), never emitting <N>, <VALUE>, <URL>, <STR> or other mask tokens; the AI title still wins. Used as the fallback in the run failure-groups table, the project cluster list, the cluster page heading and <title>, the execution rail card, the test-case history list, run insights, the analytics landscape and the cluster.new alerts (Slack, email, browser), with the raw signature kept as secondary text and the cluster page reading title first, signature below.

  • shared/describe-cluster.ts (new), app/components/run/FailureGroups.vue, app/components/project/FailureClustersList.vue, app/components/cluster/ClusterSummary.vue, app/pages/failure-clusters/[id].vue, app/components/test-case/FailureClusterCard.vue, app/pages/test-run-cases/[id].vue, app/pages/test-cases/[id].vue, app/components/run/RunInsights.vue, app/components/analytics/ClusterLandscape.vue, shared/handlers/test-cases.ts, shared/handlers/run-insights.ts, shared/handlers/analytics/cluster-landscape.ts, shared/analytics/types.ts, server/utils/notifications/run-notifications.ts, server/utils/notifications/dispatch.ts, server/utils/email.ts, apps/docs/ai-diagnosis.md
  • Tests: tests/unit/describe-cluster.test.ts (new; includes every sample error from error-fingerprint.test.ts)

Nothing was left out. Not done: no new feature-screenshot scene was added for the one-line healing state or the retitled cluster heading.

How was it tested?

From apps/application/: npm run app:typecheck, npm run app:lint, npm run app:format:check, npm run app:test:unit (124 files, all green), npm run app:check:demo, npm run app:seed:demo (seed regenerated, seed.version.json committed), npm run db:migrate on a fresh SQLite database (both migrations generated with drizzle-kit, none hand-written), and the E2E specs failure-clusters, fix-verification, cluster-page-layout, insights-and-spec-health and notifications (38 passed, auth-only tests skipped outside CI). The PostgreSQL migration was generated but not applied against a live PostgreSQL instance.

Checklist

  • PR title follows Conventional Commits (type(scope): subject)
  • Tests added/updated for behavior changes
  • Docs updated if user-facing (apps/docs/, README, or reporter README)

🤖 Generated with Claude Code

https://claude.ai/code/session_011oW4GertDgvoFGdtGGoj5U


Generated by Claude Code

Healing suggestions only make sense when the failing locator never found its
element. A toHaveCount mismatch (locator resolved to 51 elements), a disabled
element, a hidden element or a page.goto timeout got a Recommended fix that
narrowed or replaced a locator that already worked, and the run page showed a
Locator fix signal built from ARIA guesses for navigation errors.

classifyLocatorResolution reads Playwright's call log: healing applies when
the locator never resolved, resolved to 0 elements, or hit a strict-mode
violation; it does not when a later 'locator resolved to' line (or an action
phase such as 'element is not enabled') shows it resolved, or when the error
is a navigation failure. resolveHealingForCase returns applicable: false with
a one-line reason before any ladder rung (the ARIA fallback included), so the
batch variant, the run's failure groups, the auto-heal policy, the fix plan,
the MCP tools and the AI context all inherit the gate. The panel shows the
reason as one line instead of a ranked menu.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oW4GertDgvoFGdtGGoj5U
… diffs

The environment diff picked the test's last passing execution on the same
browser and nothing else, so a development failure was diffed against a
production pass and the diff listed the environment label itself as the
change. The visual diff preferred the same branch but ignored the
environment.

Both now rank candidates with one shared, unit-tested ordering: same
environment first, then same branch, then recency. When no passing run from
the failing run's environment exists the fallback still happens, but the
result carries a baselineNote the cards and the AI context show, and the
environment label is never reported as a diff entry in that case. Regression
signals and run insights are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oW4GertDgvoFGdtGGoj5U
The reporter sends startTime per network request but network_requests had no
column for it, so the value was dropped on ingest and the AI context's
t+Nms offset for failed requests read a field that was always undefined.

network_requests gains a nullable start_time (Unix epoch ms) in both
dialects with generated migrations; the shared insert builder persists it
from the wire, the execution response and the run-level endpoint summaries
(firstStartTime / lastStartTime) expose it, the AI-context offset reads the
stored value, the demo seed and simulator stamp sequential start times, and
the execution page orders captured requests by start time when present.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oW4GertDgvoFGdtGGoj5U
A cluster could read Fix verified and Open at the same time: verification
recorded the verdict but never touched the triage status, and nothing told
anyone a fix had landed or come undone.

A diagnosis-verified verdict now resolves an open cluster and a regressed
verdict reopens a resolved one, each appending a system line to the triage
note (Resolved automatically: diagnosis verified in run #N / Reopened
automatically: regressed in run #N) so the status stays auditable and
overridable. Stopped-failing alone changes nothing and ignored clusters are
never touched; the verdict badge stays separate from the status. Two events
join the subscription list: cluster.fixed for every recorded fix (with the
verdict in its payload) and cluster.regressed, rendered for browser, Slack,
email digests and webhooks, and documented with the verdict rules.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oW4GertDgvoFGdtGGoj5U
describeCluster builds a human title from a cluster's error type, locator
target, navigation route and spec basename (Timeout on getByLabel('Email
address') in checkout.spec.ts; toHaveCount mismatch on getByRole('row') in
users.spec.ts; Navigation timeout on /users), never emitting a masking
placeholder, and defers to the AI title when one exists. Unit-tested against
the fingerprint suite's sample errors. The UI, notification and API call
sites that still show the raw signature as a name follow.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_011oW4GertDgvoFGdtGGoj5U
…the name

Without an AI title a cluster showed its raw normalized signature — a
Playwright error kind with masking placeholders — in the run failure-groups
table, the project cluster list, the cluster page heading and document
title, the execution rail card, the test-case history list, run insights,
the analytics landscape and the cluster.new alerts. Every one of them now
uses describeCluster as the fallback name (the AI title still wins), keeps
the raw signature as secondary text, and the cluster page reads title first
with the signature below. Handlers expose the title, locator and error type
the helper needs; cluster.new payloads carry the display title for Slack,
email and browser notifications.

Also types the two network-request builders with the shared
NetworkRequestBuilder shape (the inline copies lacked startTime) and
declares network_requests.start_time as an int/bigint dialect difference.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_011oW4GertDgvoFGdtGGoj5U
The automatic reopen note now contains the word regressed, so the row's
substring match found two elements.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_011oW4GertDgvoFGdtGGoj5U
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for Reporter (./packages/reporter)

Status Category Percentage Covered / Total
🔵 Lines 74.9% 2262 / 3020
🔵 Statements 73.24% 2573 / 3513
🔵 Functions 77.97% 393 / 504
🔵 Branches 67.36% 1810 / 2687
File CoverageNo changed files found.
Generated in workflow #1485 for commit e11ce42 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for Application (./apps/application)

Status Category Percentage Covered / Total
🔵 Lines 44.08% 5891 / 13362
🔵 Statements 43.28% 6850 / 15824
🔵 Functions 43.01% 1137 / 2643
🔵 Branches 39.37% 5145 / 13066
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
apps/application/app/composables/useNotificationStream.ts 0% 0% 0% 0% 5-216
apps/application/app/utils/help-content.ts 100% 100% 100% 100%
apps/application/server/utils/ai-context.ts 23.31% 19.27% 20.76% 24.57% 75-83, 171-438, 450-451, 453, 454, 455, 456, 457, 463-844, 863-962, 996-1168, 1227, 1247, 1293, 1300, 1320, 1362-1541, 1596-1616, 1667-1668, 1671, 1677, 1704, 1715, 1718, 1721-1731, 1741, 1746, 1753, 1763-1773, 1778-1782, 1804-1817, 1874, 1890, 1949, 1971-2039, 2071-2877
apps/application/server/utils/email.ts 3% 0% 0% 3.06% 34-86, 91-309
apps/application/server/utils/environment-diff.ts 4.34% 0% 0% 5% 46-132
apps/application/server/utils/fix-plan.ts 0% 0% 0% 0% 29-205
apps/application/server/utils/fix-verification.ts 91.42% 73.03% 100% 96.55% 73, 98, 112, 122, 240, 266, 273-274, 277
apps/application/server/utils/locator-healing.ts 64.63% 50.77% 73.8% 67.44% 49, 53, 80, 90-91, 95, 112, 113, 114, 116, 131, 139-140, 151-172, 292, 296, 378, 382-392, 426, 562, 581, 629, 669-671, 704, 708-709, 722-939
apps/application/server/utils/network-request-helpers.ts 52.63% 73.33% 66.66% 56.25% 36, 64-72
apps/application/server/utils/persist-run-cases.ts 0% 0% 0% 0% 39-469
apps/application/server/utils/visual-diff.ts 21.91% 20.51% 25% 22.22% 93-236
apps/application/server/utils/heal/policy.ts 21.56% 29.11% 5.26% 22.36% 102-250
apps/application/server/utils/mcp/tools.ts 0% 0% 0% 0% 73-1809
apps/application/server/utils/notifications/dispatch.ts 2.28% 0% 0% 2.68% 29-335
apps/application/server/utils/notifications/run-notifications.ts 0% 0% 0% 0% 4-166
apps/application/shared/baseline-order.ts 100% 100% 100% 100%
apps/application/shared/describe-cluster.ts 91.52% 79.41% 100% 94.11% 57-58, 68, 75, 101
apps/application/shared/environment-diff.ts 61.29% 50% 60% 61.53% 120-133, 150
apps/application/shared/locator-resolution.ts 100% 95.45% 100% 100%
apps/application/shared/mcp-tools.ts 100% 100% 100% 100%
apps/application/shared/notification-events.ts 95.87% 87.96% 100% 97.46% 211, 275, 327
apps/application/shared/analytics/types.ts 0% 0% 0% 0%
apps/application/shared/handlers/run-insights.ts 0% 0% 0% 0% 52-301
apps/application/shared/handlers/test-cases.ts 0% 0% 0% 0% 20-482
apps/application/shared/handlers/test-runs.ts 0% 0% 0% 0% 32-773
apps/application/shared/handlers/analytics/cluster-landscape.ts 96% 76.92% 100% 100% 23
Generated in workflow #1485 for commit e11ce42 by the Vitest Coverage Report Action

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants