Skip to content

fix(first-run-tour): stop promising a result once acceptance is lost - #15172

Merged
christian-byrne merged 3 commits into
fix/14619-tour-unaccepted-runfrom
fix/tour-acceptance-loss-watch
Aug 23, 2026
Merged

fix(first-run-tour): stop promising a result once acceptance is lost#15172
christian-byrne merged 3 commits into
fix/14619-tour-unaccepted-runfrom
fix/tour-acceptance-loss-watch

Conversation

@christian-byrne

@christian-byrne christian-byrne commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Stacked on #15091 — base is fix/14619-tour-unaccepted-run, merge after it.

#15091 gave the first-run tour a 15s acceptance deadline, which covers a submission the backend refuses outright. It left one hole: a run that was accepted and then loses its job without an outcome. Two live paths do that, and neither is the mid-run credits path this description originally led with — thanks @benjcooley for catching that:

path what it leaves behind covered before this PR?
accepted job dropped with no status ever written — the cloud "waiting for a machine" job cancelled or reconciled away nothing ❌ no branch can fire
handleServiceLevelError ("Job has stagnated") — drops the job, records a prompt error, never touches workflowStatus a stale running ❌ the stale running shadowed the error branch
mid-run credits (handleAccountPreconditionError) stale running already covered#15161 clears the status, so it ends via the existing undefined-after-running branch

In the uncovered cases runState sits on generating forever and the card keeps promising "Hang tight, your result lands right here the moment it's ready."

The controller had two disarms and no re-arm. This makes losing acceptance a signal in its own right: extend the existing tourRunAccepted watcher so true -> false fails the run when it is still generating.

watch(tourRunAccepted, (accepted) => {
  if (accepted) stopAcceptDeadline()
  else if (runState.value === 'generating') runState.value = 'failed'
})

No re-armed deadline — this is immediate, not a second timer.

The 'generating' gate is what keeps healthy runs safe: every run leaves the queue when it finishes. handleExecutionSuccess and handleExecutionError write the terminal status and call resetExecutionState in the same synchronous handler, and the status watcher is registered before this one, so a finished run has already left generating by the time this fires. Test 3 below covers exactly that tick.

Tests, in the existing 'a run behind a dropped socket' describe:

  • accepted job removed with no status -> failed
  • accepted job, status running, removed (the stagnation case) -> failed
  • accepted job, status completed written in the same tick as the removal -> stays succeeded
  • accepted job, status failed, then removed -> stays failed

The first two fail on the base branch (expected 'generating' to be 'failed'). Of the last two terminal-status guards, only the completed one discriminates — ungated, the failed case writes 'failed' over 'failed' and still passes. It is kept as documentation of intent, not as a guard; the earlier claim that both pinned the gate was wrong.

Two tests added in review:

  • stays failed when an unrelated workflow churns the status map — pins the transition gate below. Fails without it (expected 'generating' to be 'failed').
  • gives up on a stagnated job that leaves an error and a stale status — the other half of the stagnation path. Documents it; does not discriminate the gate.

Review fix: the status watcher's status === 'running' branch was unconditional, and its source re-evaluates whenever the workflowStatus map is replaced (mutateStatus swaps the whole map, so any workflow's change re-runs it) or an error flag flips. With a stale running sitting there, each re-evaluation put the card back on "your result lands right here" after this watcher had already failed the run. It is now gated on an actual transition into running, which also removes the registration-order dependency the old comment overclaimed away.

Complementary to #15161 (merged 2026-08-13), which fixes the same user-visible symptom from the store side by clearing the stale status. Independent of it: this is the controller refusing to promise regardless of whether the store cleans up. Both are wanted; neither duplicates the other.

Requested by @benjcooley in review on #15091 (acceptance-loss watch for the mid-run credits variant) and by CodeRabbit (acceptance-loss coverage, terminal statuses stay terminal).

Tests

  • 58 unit tests green in useFirstRunTourController.test.ts (54 existing + 4 new), full-file and each new test in isolation.
  • pnpm lint, pnpm typecheck, pnpm format:check pass.

A job dropped from queuedJobs without a terminal status left runState on
'generating' forever. resetExecutionState clears the job and the workflow
mapping but not the status, so handleAccountPreconditionError - the mid-run
INSUFFICIENT_CREDITS path - leaves the card promising a result that never
arrives.

Fail the run when acceptance goes true -> false while still generating.
Gated on 'generating' so the queue drop every healthy run makes on its way
out stays harmless: a finished run writes its terminal status in the same
tick, and the status watcher is registered first.
@christian-byrne
christian-byrne requested a review from a team as a code owner August 13, 2026 02:39
@dosubot dosubot Bot added the size:XS This PR changes 0-9 lines, ignoring generated files. label Aug 13, 2026
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

🎭 Playwright: ✅ 1802 passed, 0 failed · 3 flaky

📊 Browser Reports
  • chromium: View Report (✅ 1783 / ❌ 0 / ⚠️ 2 / ⏭️ 5)
  • chromium-2x: View Report (✅ 2 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • chromium-0.5x: View Report (✅ 1 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • mobile-chrome: View Report (✅ 16 / ❌ 0 / ⚠️ 1 / ⏭️ 0)

🎨 Storybook: ✅ Built — View Storybook

Details

⏰ Completed at: 08/17/2026, 04:51:31 PM UTC

Links

📦 Bundle Size

⏳ Size data collection in progress…

⚡ Performance Report

canvas-idle: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 67.2 MB heap
canvas-mouse-sweep: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 48.6 MB heap
canvas-zoom-sweep: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 70.0 MB heap
dom-widget-clipping: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 50.7 MB heap
large-graph-idle: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 62.2 MB heap
large-graph-pan: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 64.7 MB heap
large-graph-zoom: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 56.2 MB heap
minimap-idle: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 64.2 MB heap
subgraph-dom-widget-clipping: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 51.2 MB heap
subgraph-idle: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 67.2 MB heap
subgraph-mouse-sweep: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 51.2 MB heap
subgraph-transition-enter: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 124ms TBT · 94.7 MB heap
viewport-pan-sweep: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 70.7 MB heap
vue-large-graph-idle: · 57.1 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 3ms TBT · 160.4 MB heap
vue-large-graph-pan: · 56.3 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 457ms TBT · 164.9 MB heap
workflow-execution: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 64.6 MB heap

ℹ️ No baseline found — significance unavailable.

Absolute values
Metric Value
canvas-idle: avg frame time 17ms
canvas-idle: p95 frame time 17ms
canvas-idle: layout duration 0ms
canvas-idle: style recalc duration 7ms
canvas-idle: layout count 0
canvas-idle: style recalc count 9
canvas-idle: task duration 425ms
canvas-idle: script duration 16ms
canvas-idle: TBT 0ms
canvas-idle: heap used 67.2 MB
canvas-idle: DOM nodes 18
canvas-idle: event listeners 4
canvas-mouse-sweep: avg frame time 17ms
canvas-mouse-sweep: p95 frame time 17ms
canvas-mouse-sweep: layout duration 4ms
canvas-mouse-sweep: style recalc duration 41ms
canvas-mouse-sweep: layout count 12
canvas-mouse-sweep: style recalc count 78
canvas-mouse-sweep: task duration 966ms
canvas-mouse-sweep: script duration 123ms
canvas-mouse-sweep: TBT 0ms
canvas-mouse-sweep: heap used 48.6 MB
canvas-mouse-sweep: DOM nodes -281
canvas-mouse-sweep: event listeners -153
canvas-zoom-sweep: avg frame time 17ms
canvas-zoom-sweep: p95 frame time 17ms
canvas-zoom-sweep: layout duration 1ms
canvas-zoom-sweep: style recalc duration 16ms
canvas-zoom-sweep: layout count 6
canvas-zoom-sweep: style recalc count 32
canvas-zoom-sweep: task duration 374ms
canvas-zoom-sweep: script duration 18ms
canvas-zoom-sweep: TBT 0ms
canvas-zoom-sweep: heap used 70.0 MB
canvas-zoom-sweep: DOM nodes 78
canvas-zoom-sweep: event listeners 19
dom-widget-clipping: avg frame time 17ms
dom-widget-clipping: p95 frame time 17ms
dom-widget-clipping: layout duration 0ms
dom-widget-clipping: style recalc duration 8ms
dom-widget-clipping: layout count 0
dom-widget-clipping: style recalc count 12
dom-widget-clipping: task duration 381ms
dom-widget-clipping: script duration 57ms
dom-widget-clipping: TBT 0ms
dom-widget-clipping: heap used 50.7 MB
dom-widget-clipping: DOM nodes 20
dom-widget-clipping: event listeners 1
large-graph-idle: avg frame time 17ms
large-graph-idle: p95 frame time 17ms
large-graph-idle: layout duration 0ms
large-graph-idle: style recalc duration 7ms
large-graph-idle: layout count 0
large-graph-idle: style recalc count 9
large-graph-idle: task duration 664ms
large-graph-idle: script duration 91ms
large-graph-idle: TBT 0ms
large-graph-idle: heap used 62.2 MB
large-graph-idle: DOM nodes -281
large-graph-idle: event listeners -164
large-graph-pan: avg frame time 17ms
large-graph-pan: p95 frame time 17ms
large-graph-pan: layout duration 0ms
large-graph-pan: style recalc duration 14ms
large-graph-pan: layout count 0
large-graph-pan: style recalc count 68
large-graph-pan: task duration 1234ms
large-graph-pan: script duration 404ms
large-graph-pan: TBT 0ms
large-graph-pan: heap used 64.7 MB
large-graph-pan: DOM nodes -284
large-graph-pan: event listeners -163
large-graph-zoom: avg frame time 17ms
large-graph-zoom: p95 frame time 17ms
large-graph-zoom: layout duration 8ms
large-graph-zoom: style recalc duration 16ms
large-graph-zoom: layout count 60
large-graph-zoom: style recalc count 66
large-graph-zoom: task duration 1496ms
large-graph-zoom: script duration 516ms
large-graph-zoom: TBT 0ms
large-graph-zoom: heap used 56.2 MB
large-graph-zoom: DOM nodes -288
large-graph-zoom: event listeners -153
minimap-idle: avg frame time 17ms
minimap-idle: p95 frame time 17ms
minimap-idle: layout duration 0ms
minimap-idle: style recalc duration 6ms
minimap-idle: layout count 0
minimap-idle: style recalc count 8
minimap-idle: task duration 666ms
minimap-idle: script duration 93ms
minimap-idle: TBT 0ms
minimap-idle: heap used 64.2 MB
minimap-idle: DOM nodes -284
minimap-idle: event listeners -163
subgraph-dom-widget-clipping: avg frame time 17ms
subgraph-dom-widget-clipping: p95 frame time 17ms
subgraph-dom-widget-clipping: layout duration 0ms
subgraph-dom-widget-clipping: style recalc duration 11ms
subgraph-dom-widget-clipping: layout count 0
subgraph-dom-widget-clipping: style recalc count 48
subgraph-dom-widget-clipping: task duration 412ms
subgraph-dom-widget-clipping: script duration 127ms
subgraph-dom-widget-clipping: TBT 0ms
subgraph-dom-widget-clipping: heap used 51.2 MB
subgraph-dom-widget-clipping: DOM nodes 21
subgraph-dom-widget-clipping: event listeners 6
subgraph-idle: avg frame time 17ms
subgraph-idle: p95 frame time 17ms
subgraph-idle: layout duration 0ms
subgraph-idle: style recalc duration 7ms
subgraph-idle: layout count 0
subgraph-idle: style recalc count 10
subgraph-idle: task duration 468ms
subgraph-idle: script duration 13ms
subgraph-idle: TBT 0ms
subgraph-idle: heap used 67.2 MB
subgraph-idle: DOM nodes 20
subgraph-idle: event listeners 4
subgraph-mouse-sweep: avg frame time 17ms
subgraph-mouse-sweep: p95 frame time 17ms
subgraph-mouse-sweep: layout duration 4ms
subgraph-mouse-sweep: style recalc duration 34ms
subgraph-mouse-sweep: layout count 16
subgraph-mouse-sweep: style recalc count 78
subgraph-mouse-sweep: task duration 764ms
subgraph-mouse-sweep: script duration 84ms
subgraph-mouse-sweep: TBT 0ms
subgraph-mouse-sweep: heap used 51.2 MB
subgraph-mouse-sweep: DOM nodes -108
subgraph-mouse-sweep: event listeners -75
subgraph-transition-enter: avg frame time 17ms
subgraph-transition-enter: p95 frame time 17ms
subgraph-transition-enter: layout duration 11ms
subgraph-transition-enter: style recalc duration 29ms
subgraph-transition-enter: layout count 14
subgraph-transition-enter: style recalc count 18
subgraph-transition-enter: task duration 900ms
subgraph-transition-enter: script duration 36ms
subgraph-transition-enter: TBT 124ms
subgraph-transition-enter: heap used 94.7 MB
subgraph-transition-enter: DOM nodes 13673
subgraph-transition-enter: event listeners 2375
viewport-pan-sweep: avg frame time 17ms
viewport-pan-sweep: p95 frame time 17ms
viewport-pan-sweep: layout duration 0ms
viewport-pan-sweep: style recalc duration 40ms
viewport-pan-sweep: layout count 0
viewport-pan-sweep: style recalc count 250
viewport-pan-sweep: task duration 4509ms
viewport-pan-sweep: script duration 1442ms
viewport-pan-sweep: TBT 0ms
viewport-pan-sweep: heap used 70.7 MB
viewport-pan-sweep: DOM nodes -283
viewport-pan-sweep: event listeners -133
vue-large-graph-idle: avg frame time 17ms
vue-large-graph-idle: p95 frame time 17ms
vue-large-graph-idle: layout duration 0ms
vue-large-graph-idle: style recalc duration 0ms
vue-large-graph-idle: layout count 0
vue-large-graph-idle: style recalc count 0
vue-large-graph-idle: task duration 15446ms
vue-large-graph-idle: script duration 584ms
vue-large-graph-idle: TBT 3ms
vue-large-graph-idle: heap used 160.4 MB
vue-large-graph-idle: DOM nodes -8312
vue-large-graph-idle: event listeners -16390
vue-large-graph-pan: avg frame time 18ms
vue-large-graph-pan: p95 frame time 17ms
vue-large-graph-pan: layout duration 0ms
vue-large-graph-pan: style recalc duration 22ms
vue-large-graph-pan: layout count 0
vue-large-graph-pan: style recalc count 162
vue-large-graph-pan: task duration 22127ms
vue-large-graph-pan: script duration 948ms
vue-large-graph-pan: TBT 457ms
vue-large-graph-pan: heap used 164.9 MB
vue-large-graph-pan: DOM nodes -8312
vue-large-graph-pan: event listeners -16382
workflow-execution: avg frame time 17ms
workflow-execution: p95 frame time 17ms
workflow-execution: layout duration 1ms
workflow-execution: style recalc duration 20ms
workflow-execution: layout count 3
workflow-execution: style recalc count 13
workflow-execution: task duration 107ms
workflow-execution: script duration 8ms
workflow-execution: TBT 0ms
workflow-execution: heap used 64.6 MB
workflow-execution: DOM nodes 129
workflow-execution: event listeners 73
Raw data
{
  "timestamp": "2026-08-17T17:01:44.546Z",
  "gitSha": "7d5e23919582c07fe87a24b7e5df0c7ff41e143b",
  "branch": "fix/tour-acceptance-loss-watch",
  "measurements": [
    {
      "name": "canvas-idle",
      "durationMs": 2089.179999999999,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 7.32,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 390.14,
      "heapDeltaBytes": 5931676,
      "heapUsedBytes": 70271400,
      "domNodes": 18,
      "jsHeapTotalBytes": 24641536,
      "scriptDurationMs": 17.535000000000004,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-idle",
      "durationMs": 2026.7880000000105,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 6.181000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 458.92999999999995,
      "heapDeltaBytes": 5907588,
      "heapUsedBytes": 70701324,
      "domNodes": 18,
      "jsHeapTotalBytes": 24641536,
      "scriptDurationMs": 14.950999999999999,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 2072.938999999991,
      "styleRecalcs": 80,
      "styleRecalcDurationMs": 48.679,
      "layouts": 12,
      "layoutDurationMs": 5.010000000000001,
      "taskDurationMs": 1066.103,
      "heapDeltaBytes": -12653552,
      "heapUsedBytes": 51758644,
      "domNodes": -280,
      "jsHeapTotalBytes": 24088576,
      "scriptDurationMs": 134.015,
      "eventListeners": -153,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 1819.7299999999927,
      "styleRecalcs": 75,
      "styleRecalcDurationMs": 32.404,
      "layouts": 12,
      "layoutDurationMs": 3.379,
      "taskDurationMs": 866.84,
      "heapDeltaBytes": -14443880,
      "heapUsedBytes": 50235276,
      "domNodes": -282,
      "jsHeapTotalBytes": 23564288,
      "scriptDurationMs": 111.54599999999999,
      "eventListeners": -153,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1737.8209999999967,
      "styleRecalcs": 32,
      "styleRecalcDurationMs": 16.381,
      "layouts": 6,
      "layoutDurationMs": 0.7210000000000001,
      "taskDurationMs": 375.116,
      "heapDeltaBytes": 8778208,
      "heapUsedBytes": 73400024,
      "domNodes": 79,
      "jsHeapTotalBytes": 24379392,
      "scriptDurationMs": 18.342999999999996,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66999999999998,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1719.7949999999764,
      "styleRecalcs": 32,
      "styleRecalcDurationMs": 15.450000000000001,
      "layouts": 6,
      "layoutDurationMs": 0.7360000000000001,
      "taskDurationMs": 373.19800000000004,
      "heapDeltaBytes": 8767268,
      "heapUsedBytes": 73371456,
      "domNodes": 77,
      "jsHeapTotalBytes": 24641536,
      "scriptDurationMs": 18.33,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 624.9990000000025,
      "styleRecalcs": 13,
      "styleRecalcDurationMs": 8.662,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 385.964,
      "heapDeltaBytes": -11283432,
      "heapUsedBytes": 53120320,
      "domNodes": 22,
      "jsHeapTotalBytes": 25690112,
      "scriptDurationMs": 57.861999999999995,
      "eventListeners": 2,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.670000000000012,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 577.8889999999706,
      "styleRecalcs": 11,
      "styleRecalcDurationMs": 7.2940000000000005,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 376.00500000000005,
      "heapDeltaBytes": -11251380,
      "heapUsedBytes": 53136696,
      "domNodes": 18,
      "jsHeapTotalBytes": 24641536,
      "scriptDurationMs": 55.93100000000001,
      "eventListeners": 0,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2025.9629999999902,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 6.98,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 656.949,
      "heapDeltaBytes": 6908572,
      "heapUsedBytes": 66431252,
      "domNodes": -281,
      "jsHeapTotalBytes": 3506176,
      "scriptDurationMs": 90.13300000000002,
      "eventListeners": -149,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2024.8189999999795,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 6.938,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 672.0410000000002,
      "heapDeltaBytes": 4450260,
      "heapUsedBytes": 63962068,
      "domNodes": -281,
      "jsHeapTotalBytes": 3244032,
      "scriptDurationMs": 92.13000000000001,
      "eventListeners": -179,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2146.724000000006,
      "styleRecalcs": 68,
      "styleRecalcDurationMs": 14.186,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1233.6529999999998,
      "heapDeltaBytes": 18663808,
      "heapUsedBytes": 79712432,
      "domNodes": -282,
      "jsHeapTotalBytes": 2912256,
      "scriptDurationMs": 403.126,
      "eventListeners": -177,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66999999999998,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2152.0649999999932,
      "styleRecalcs": 68,
      "styleRecalcDurationMs": 14.039000000000003,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1233.781,
      "heapDeltaBytes": -5428480,
      "heapUsedBytes": 55892324,
      "domNodes": -285,
      "jsHeapTotalBytes": 5603328,
      "scriptDurationMs": 404.598,
      "eventListeners": -149,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3194.626000000028,
      "styleRecalcs": 66,
      "styleRecalcDurationMs": 16.242,
      "layouts": 60,
      "layoutDurationMs": 8.204,
      "taskDurationMs": 1484.9460000000001,
      "heapDeltaBytes": -2566824,
      "heapUsedBytes": 59563568,
      "domNodes": -287,
      "jsHeapTotalBytes": 5865472,
      "scriptDurationMs": 517.0749999999999,
      "eventListeners": -153,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66999999999998,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3178.9210000000594,
      "styleRecalcs": 65,
      "styleRecalcDurationMs": 15.270000000000003,
      "layouts": 60,
      "layoutDurationMs": 8.046,
      "taskDurationMs": 1507.566,
      "heapDeltaBytes": -3844236,
      "heapUsedBytes": 58313980,
      "domNodes": -288,
      "jsHeapTotalBytes": 6651904,
      "scriptDurationMs": 514.421,
      "eventListeners": -153,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "minimap-idle",
      "durationMs": 2029.504999999972,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 5.9430000000000005,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 668.7539999999999,
      "heapDeltaBytes": 6801504,
      "heapUsedBytes": 67616792,
      "domNodes": -284,
      "jsHeapTotalBytes": 3768320,
      "scriptDurationMs": 94.19100000000002,
      "eventListeners": -149,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "minimap-idle",
      "durationMs": 2036.8400000000975,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 6.512999999999998,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 663.2950000000001,
      "heapDeltaBytes": 6375760,
      "heapUsedBytes": 66968216,
      "domNodes": -284,
      "jsHeapTotalBytes": 4554752,
      "scriptDurationMs": 92.72899999999998,
      "eventListeners": -177,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 598.6409999999864,
      "styleRecalcs": 47,
      "styleRecalcDurationMs": 10.408000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 413.271,
      "heapDeltaBytes": -10640248,
      "heapUsedBytes": 53856356,
      "domNodes": 20,
      "jsHeapTotalBytes": 25952256,
      "scriptDurationMs": 129.423,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.670000000000012,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 592.8099999999858,
      "styleRecalcs": 48,
      "styleRecalcDurationMs": 11.813,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 409.837,
      "heapDeltaBytes": -10974268,
      "heapUsedBytes": 53525964,
      "domNodes": 22,
      "jsHeapTotalBytes": 25952256,
      "scriptDurationMs": 125.12200000000001,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-idle",
      "durationMs": 2039.3229999999676,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 7.826999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 452.37,
      "heapDeltaBytes": 5983104,
      "heapUsedBytes": 70457832,
      "domNodes": 20,
      "jsHeapTotalBytes": 24379392,
      "scriptDurationMs": 13.873999999999999,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66999999999998,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "subgraph-idle",
      "durationMs": 2015.8270000000016,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 6.860999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 484.079,
      "heapDeltaBytes": 5915708,
      "heapUsedBytes": 70403948,
      "domNodes": 20,
      "jsHeapTotalBytes": 24641536,
      "scriptDurationMs": 12.973,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.670000000000012,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1702.150999999958,
      "styleRecalcs": 76,
      "styleRecalcDurationMs": 34.809,
      "layouts": 16,
      "layoutDurationMs": 4.076,
      "taskDurationMs": 751.346,
      "heapDeltaBytes": -3762592,
      "heapUsedBytes": 60811260,
      "domNodes": 64,
      "jsHeapTotalBytes": 26214400,
      "scriptDurationMs": 83.658,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1741.2419999999997,
      "styleRecalcs": 79,
      "styleRecalcDurationMs": 33.892,
      "layouts": 16,
      "layoutDurationMs": 4.199,
      "taskDurationMs": 776.097,
      "heapDeltaBytes": -17889448,
      "heapUsedBytes": 46547356,
      "domNodes": -280,
      "jsHeapTotalBytes": 23564288,
      "scriptDurationMs": 84.066,
      "eventListeners": -153,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-transition-enter",
      "durationMs": 1400.8360000000266,
      "styleRecalcs": 18,
      "styleRecalcDurationMs": 28.711000000000002,
      "layouts": 14,
      "layoutDurationMs": 11.418000000000001,
      "taskDurationMs": 899.5830000000002,
      "heapDeltaBytes": 31038012,
      "heapUsedBytes": 99351972,
      "domNodes": 13673,
      "jsHeapTotalBytes": 15990784,
      "scriptDurationMs": 35.911000000000016,
      "eventListeners": 2375,
      "totalBlockingTimeMs": 124,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 8239.688000000002,
      "styleRecalcs": 250,
      "styleRecalcDurationMs": 39.584,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 4354.425,
      "heapDeltaBytes": 11819724,
      "heapUsedBytes": 71481196,
      "domNodes": -282,
      "jsHeapTotalBytes": 6320128,
      "scriptDurationMs": 1312.156,
      "eventListeners": -133,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.670000000000012,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 8225.202999999965,
      "styleRecalcs": 249,
      "styleRecalcDurationMs": 40.539,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 4662.804999999999,
      "heapDeltaBytes": 16950104,
      "heapUsedBytes": 76835672,
      "domNodes": -283,
      "jsHeapTotalBytes": 10514432,
      "scriptDurationMs": 1572.792,
      "eventListeners": -133,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 13436.350000000004,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 13399.739,
      "heapDeltaBytes": -55990768,
      "heapUsedBytes": 170540916,
      "domNodes": -8312,
      "jsHeapTotalBytes": -2826240,
      "scriptDurationMs": 601.7009999999999,
      "eventListeners": -16391,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.223333333333358,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 17505.62000000002,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 17492.032,
      "heapDeltaBytes": -36868348,
      "heapUsedBytes": 165880960,
      "domNodes": -8312,
      "jsHeapTotalBytes": -10424320,
      "scriptDurationMs": 566.63,
      "eventListeners": -16389,
      "totalBlockingTimeMs": 6,
      "frameDurationMs": 17.77333333333336,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 21966.953999999987,
      "styleRecalcs": 158,
      "styleRecalcDurationMs": 22.05300000000002,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 21932.564999999995,
      "heapDeltaBytes": -51228692,
      "heapUsedBytes": 168451752,
      "domNodes": -8312,
      "jsHeapTotalBytes": -7802880,
      "scriptDurationMs": 914.269,
      "eventListeners": -16379,
      "totalBlockingTimeMs": 460,
      "frameDurationMs": 17.776666666666642,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 22372.145000000048,
      "styleRecalcs": 165,
      "styleRecalcDurationMs": 22.46799999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 22321.267,
      "heapDeltaBytes": -28968816,
      "heapUsedBytes": 177330288,
      "domNodes": -8312,
      "jsHeapTotalBytes": -8138752,
      "scriptDurationMs": 981.3320000000001,
      "eventListeners": -16385,
      "totalBlockingTimeMs": 453,
      "frameDurationMs": 17.77333333333336,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "workflow-execution",
      "durationMs": 109.4229999999925,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 14.016999999999998,
      "layouts": 2,
      "layoutDurationMs": 0.6529999999999999,
      "taskDurationMs": 75.179,
      "heapDeltaBytes": 3085896,
      "heapUsedBytes": 66620660,
      "domNodes": 111,
      "jsHeapTotalBytes": 3407872,
      "scriptDurationMs": 5.812000000000001,
      "eventListeners": 49,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "workflow-execution",
      "durationMs": 516.2970000000087,
      "styleRecalcs": 18,
      "styleRecalcDurationMs": 26.526999999999997,
      "layouts": 4,
      "layoutDurationMs": 1.802,
      "taskDurationMs": 138.357,
      "heapDeltaBytes": 5180972,
      "heapUsedBytes": 68933616,
      "domNodes": 147,
      "jsHeapTotalBytes": 5242880,
      "scriptDurationMs": 10.829,
      "eventListeners": 97,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    }
  ]
}

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b89efd40-2c40-4b3d-bddf-690fd39b97b6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

…loss

The note claimed the status watcher wins because it is registered first.
Verified otherwise: moving it below the acceptance watch leaves all 58 tests
green, because it applies the terminal status from the same flush either way.
Order is not what makes this safe, so stop implying a reordering would break it.

Addresses review feedback:
#15091 (comment)
@christian-byrne christian-byrne self-assigned this Aug 13, 2026
@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

@@                       Coverage Diff                       @@
##           fix/14619-tour-unaccepted-run   #15172    +/-   ##
===============================================================
  Coverage                          81.34%   81.34%            
===============================================================
  Files                               1882     1882            
  Lines                             107863   107865     +2     
  Branches                           33860    33390   -470     
===============================================================
+ Hits                               87740    87742     +2     
  Misses                             19724    19724            
  Partials                             399      399            
Flag Coverage Δ
unit 72.56% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...ons/firstRunTour/tour/useFirstRunTourController.ts 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@benjcooley benjcooley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the one-line watcher change against executionStore (resetExecutionState, handleExecutionSuccess/Error, handleAccountPreconditionError, handleServiceLevelError, flushPendingWorkflowStatus) and the two watchers' flush ordering.

Verdict: the fix is sound and safe. The 'generating' gate holds on every real path I traced, and because the status watcher applies terminal statuses unconditionally, an early 'failed' self-corrects if an outcome lands in a later flush. No blockers.

Severity Count
BLOCKER 0
SHOULD FIX 3
TRIVIAL 1

What I'd want addressed:

  1. The stale-running entry can undo this line. Nothing clears workflowStatus on the stagnation path, and the status watcher's status === 'running' → 'generating' branch is unconditional and re-runs on any workflow's status change (mutateStatus replaces the whole map) or any error-flag flip. One extra condition on that branch closes it.
  2. The registration order is load-bearing and untested. In the stale-running case this watcher must run after the status watcher. Test 3 passes with either order, so nothing pins it.
  3. The stated motivation is stale. #15161 already clears the status on handleAccountPreconditionError, so the mid-run credits variant the description leads with is covered on main by the existing undefined-after-running branch. The hole this actually closes is (a) an accepted job dropped with no status ever written — the cloud "waiting for a machine" job that gets cancelled or reconciled away, which is genuinely new and worth having — and (b) handleServiceLevelError, which still leaves running behind. Worth correcting the description so the next reader doesn't re-derive it.

Nice work on the gate itself — pinning it with the same-tick completed case (test 3) is the right instinct, and tourRunAccepted keying on the queue rather than the status remains the correct call for cloud allocation.

*/
watch(tourRunAccepted, (accepted) => {
if (accepted) stopAcceptDeadline()
else if (runState.value === 'generating') runState.value = 'failed'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SHOULD FIX — the status watcher can undo this afterwards.

On the paths that drop a job without clearing its status, workflowStatus keeps a 'running' entry for the tour workflow forever. handleServiceLevelError ("Job has stagnated") is the live example: it writes the terminal status with showStatus: false (so applyWorkflowStatus is skipped), then calls resetExecutionState, which does not touch workflowStatus.

The status watcher's first branch is unconditional:

if (status === 'running') runState.value = 'generating'

and its source getter re-evaluates whenever the workflowStatus shallowRef is replaced — mutateStatus swaps the whole map, so any workflow's status change re-runs it, not just the tour's — or whenever hasNodeError / hasPromptError flips (clearAllErrors() at the top of the next queuePrompt, clearExecutionStartErrors() on any execution_start). Each of those re-reads the stale 'running' and puts the card back on "Hang tight, your result lands right here" after this line already failed it.

One-line fix in the status watcher — only enter generating on an actual transition:

if (status === 'running' && previous?.[0] !== 'running')
  runState.value = 'generating'

* `resetExecutionState` drops the job without writing an outcome on the
* mid-run credits path. A finished run leaves the queue too, but reports a
* terminal status in the same flush, which the watch above applies regardless
* of which of the two runs first.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SHOULD FIX — "regardless of which of the two runs first" is not true in the case this PR is actually for, and nothing tests the order.

It holds for completed/failed, because those branches overwrite unconditionally. It does not hold for the stale-running drop: there the status watcher takes status === 'running' → 'generating', so this watcher must run after it or the card stays promising. That works today only because this watch is registered second.

Test 3 doesn't pin it — with the two watchers swapped it still ends 'succeeded', since the terminal branch is unconditional. A test that would: status stays 'running', hasPromptError = true, and the job removed in the same tick → expect 'failed'. That is the stagnation path, and it is the only place the registration order is load-bearing.

it('stops promising a result when a running job is dropped mid-run', async () => {
// An API node that charges credits mid-run lands in
// `handleAccountPreconditionError`, which drops the job but leaves the
// stale `running` status behind, so no status change reports the end.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SHOULD FIX — wrong handler; this comment is already false on main.

#15161 (merged) added clearWorkflowStatus to handleAccountPreconditionError, so the credits path no longer leaves a stale running. It now goes running → undefined, and the existing status === undefined && previous?.[0] === 'running' branch already fails the run without this PR.

The scenario the test covers is still real, but via handleServiceLevelError ("Job has stagnated"): it sets the status with showStatus: false and never clears it, so running survives the drop. Retarget the comment — and the same claim in the PR description — or it reads as false the moment this rebases onto main.

).toBe('succeeded')
})

it('keeps a failed run that leaves the queue after reporting', async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TRIVIAL — this test cannot fail on the behaviour it claims to pin.

The description says the last two tests "both fail against an ungated else runState.value = 'failed' variant". This one doesn't: ungated, the removal writes 'failed' over a 'failed' and the assertion still passes. Only test 3 (completed) discriminates. Either drop it or reword the claim.

@benjcooley benjcooley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please address should fixes. But no blockers.

…nning

Addresses @benjcooley's three SHOULD FIX items.

The status watcher's `status === 'running'` branch was unconditional, and its
source re-evaluates whenever the `workflowStatus` map is replaced — which
`mutateStatus` does for *any* workflow — or whenever an error flag flips. On
paths that drop a job without clearing its status the stale `running` survives,
so each of those re-evaluations put the card back on "your result lands right
here" after the acceptance-loss watcher had already failed the run. Gated on an
actual transition.

The comments named the wrong handler. #15161 made
`handleAccountPreconditionError` clear the status, so the mid-run credits path
now ends via the existing `undefined`-after-`running` branch. The paths this
watcher actually covers are an accepted job dropped with no status ever written,
and `handleServiceLevelError`, which drops the job and records a prompt error
but never touches `workflowStatus`.

The gate also removes the registration-order dependency the old comment
overclaimed away: with it, the terminal branches are the only ones that write,
and they overwrite unconditionally.

Tests: 60 green. `stays failed when an unrelated workflow churns the status map`
fails without the gate (`expected 'generating' to be 'failed'`).
@dosubot dosubot Bot added size:S This PR changes 10-29 lines, ignoring generated files. and removed size:XS This PR changes 0-9 lines, ignoring generated files. labels Aug 17, 2026
@christian-byrne

Copy link
Copy Markdown
Contributor Author

All four addressed in 61bd7dd. Thanks — item 1 is a real bug I would not have found, and item 3 is the second time in this stack I have shipped a description that stopped being true underneath me.

1. SHOULD FIX — the status watcher can undo this. Fixed, and you are right about the mechanism:

if (status === 'running' && previous?.[0] !== 'running')
  runState.value = 'generating'

Pinned by a new test, stays failed when an unrelated workflow churns the status map: fail the run by dropping its job with a stale running in place, then write a status for a different workflow. Verified it fails without the gate — expected 'generating' to be 'failed' — rather than assuming it would.

2. SHOULD FIX — registration order is load-bearing and untested. The gate in item 1 dissolves the dependency rather than pinning it: once the running branch only fires on a transition, the terminal branches are the only ones that write, and those overwrite unconditionally. So order stops mattering, and the comment is now true as written instead of aspirational. If you would still rather have a test asserting the order explicitly, say so and I will add one, but I would rather not pin a property the code no longer relies on.

3. SHOULD FIX — the stated motivation is stale. Corrected in the description, the test comment, and the controller comment. You are right that #15161 covers the credits path via the existing undefined-after-running branch. The description now has a table naming the two paths this actually closes and explicitly striking out the credits one.

One correction to your description of the mechanism, which does not change the conclusion: handleServiceLevelError never calls setWorkflowStatus at all — no terminal write, no showStatus: false. It calls clearInitializationByJobId, resetExecutionState and recordPromptError. The stale running is the one handleExecutionStart wrote, which nothing clears. Same outcome, one fewer step.

4. TRIVIAL — the failed test cannot fail on what it claims. Correct. Kept it as documentation of intent and fixed the claim instead: only the completed case discriminates the gate. The description said both did; that was wrong.

60 tests green, oxfmt --check and oxlint --type-aware clean, vue-tsc reports nothing on either file.

Still stacked on #15091, which is blocked on @MaanilVerma's changes-requested, so this cannot merge ahead of it.

@christian-byrne
christian-byrne merged commit 70704d9 into fix/14619-tour-unaccepted-run Aug 23, 2026
71 of 72 checks passed
@christian-byrne
christian-byrne deleted the fix/tour-acceptance-loss-watch branch August 23, 2026 01:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S This PR changes 10-29 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants