Skip to content

fix(billing): scope operation completion to workspace - #15431

Open
dante01yoon wants to merge 3 commits into
dante/fe-1584-guard-prompt-workspacefrom
dante/fe-1584-scope-billing-completion
Open

fix(billing): scope operation completion to workspace#15431
dante01yoon wants to merge 3 commits into
dante/fe-1584-guard-prompt-workspacefrom
dante/fe-1584-scope-billing-completion

Conversation

@dante01yoon

@dante01yoon dante01yoon commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Why

Local/Desktop can switch billing workspaces without reloading. Billing requests and post-success reconciliation are asynchronous, so a completion originating in Workspace A must never refresh state or show success/failure UI in Workspace B.

Root cause

The original guard checked workspace ownership only before and after useBillingContext() reconciliation. That context dynamically selects the active adapter between awaits, leaving a TOCTOU window where A's completion could refresh B. The synchronous top-up and Team subscription paths also handled successful A responses against whichever workspace was active when the response returned. Timeout handling ran before ownership checks, so an inactive A operation could show failure UI in B.

Change

  • Check operation ownership before timeout/failure UI and defer inactive operation polling until its workspace is active.
  • Revalidate ownership between status and balance reconciliation calls.
  • Make subscription reconciliation snapshot workspace ID + transition generation internally, while preserving same-workspace billing-rail migration.
  • Reject top-up and synchronous Team subscription responses after A→B or A→B→A transitions.
  • Keep stale completions terminal without refreshing balance/status, closing dialogs, opening settings, or showing completion UI.
  • Add deterministic regression tests for reconciliation, inactive timeout, top-up, synchronous subscription, and A→B→A races.

AS IS / TO BE

AS IS: Workspace A completion could refresh or present success/failure state in Workspace B when the user switched during an awaited request.

TO BE: Each completion remains scoped to its originating workspace identity; stale responses are ignored and cannot mutate or present UI in another workspace.

No visual layout changes are introduced, so screenshots would not demonstrate these timing-only differences. Delayed-response tests capture the before/after behavior.

Test plan

  • pnpm test:unit src/platform/workspace/stores/billingOperationStore.test.ts — 72 passed
  • pnpm test:unit src/platform/workspace/components/TopUpCreditsDialogContentWorkspace.test.ts — 20 passed
  • pnpm test:unit src/platform/workspace/composables/useSubscriptionCheckout.test.ts — 103 passed
  • pnpm test:unit src/composables/billing/useBillingContext.test.ts — 41 passed
  • pnpm typecheck
  • pnpm knip --cache

Stack

@dosubot dosubot Bot added the size:S This PR changes 10-29 lines, ignoring generated files. label Aug 19, 2026
@coderabbitai

coderabbitai Bot commented Aug 19, 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: 51939060-0500-45b0-a9ae-9cddb2c225f7

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

🎨 Storybook: ✅ Built — View Storybook

Details

⏰ Completed at: 08/22/2026, 05:02:54 PM UTC

Links

📦 Bundle Size

⏳ Size data collection in progress…

⚡ Performance Report

canvas-idle: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 65.2 MB heap
canvas-mouse-sweep: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 56.5 MB heap
canvas-zoom-sweep: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 60.6 MB heap
dom-widget-clipping: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 67.4 MB heap
large-graph-idle: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 66.5 MB heap
large-graph-pan: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 77.2 MB heap
large-graph-zoom: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 61.1 MB heap
legacy-node-drag: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 71.4 MB heap
minimap-idle: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 72.7 MB heap
subgraph-dom-widget-clipping: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 68.8 MB heap
subgraph-idle: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 58.6 MB heap
subgraph-mouse-sweep: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 65.9 MB heap
subgraph-transition-enter: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 80ms TBT · 92.1 MB heap
viewport-pan-sweep: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 78.9 MB heap
vue-large-graph-idle: · 57.1 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 169.1 MB heap
vue-large-graph-pan: · 56.3 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 19ms TBT · 184.5 MB heap
workflow-execution: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 60.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 404ms
canvas-idle: script duration 6ms
canvas-idle: TBT 0ms
canvas-idle: heap used 65.2 MB
canvas-idle: DOM nodes -282
canvas-idle: event listeners -151
canvas-mouse-sweep: avg frame time 17ms
canvas-mouse-sweep: p95 frame time 17ms
canvas-mouse-sweep: layout duration 3ms
canvas-mouse-sweep: style recalc duration 28ms
canvas-mouse-sweep: layout count 12
canvas-mouse-sweep: style recalc count 71
canvas-mouse-sweep: task duration 700ms
canvas-mouse-sweep: script duration 83ms
canvas-mouse-sweep: TBT 0ms
canvas-mouse-sweep: heap used 56.5 MB
canvas-mouse-sweep: DOM nodes -282
canvas-mouse-sweep: event listeners -150
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 14ms
canvas-zoom-sweep: layout count 6
canvas-zoom-sweep: style recalc count 31
canvas-zoom-sweep: task duration 287ms
canvas-zoom-sweep: script duration 8ms
canvas-zoom-sweep: TBT 0ms
canvas-zoom-sweep: heap used 60.6 MB
canvas-zoom-sweep: DOM nodes 77
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 7ms
dom-widget-clipping: layout count 0
dom-widget-clipping: style recalc count 13
dom-widget-clipping: task duration 294ms
dom-widget-clipping: script duration 45ms
dom-widget-clipping: TBT 0ms
dom-widget-clipping: heap used 67.4 MB
dom-widget-clipping: DOM nodes 20
dom-widget-clipping: event listeners 2
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 10
large-graph-idle: task duration 493ms
large-graph-idle: script duration 13ms
large-graph-idle: TBT 0ms
large-graph-idle: heap used 66.5 MB
large-graph-idle: DOM nodes -270
large-graph-idle: event listeners -163
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 69
large-graph-pan: task duration 906ms
large-graph-pan: script duration 254ms
large-graph-pan: TBT 0ms
large-graph-pan: heap used 77.2 MB
large-graph-pan: DOM nodes -272
large-graph-pan: event listeners -164
large-graph-zoom: avg frame time 17ms
large-graph-zoom: p95 frame time 17ms
large-graph-zoom: layout duration 7ms
large-graph-zoom: style recalc duration 14ms
large-graph-zoom: layout count 60
large-graph-zoom: style recalc count 67
large-graph-zoom: task duration 1137ms
large-graph-zoom: script duration 329ms
large-graph-zoom: TBT 0ms
large-graph-zoom: heap used 61.1 MB
large-graph-zoom: DOM nodes -284
large-graph-zoom: event listeners -168
legacy-node-drag: avg frame time 17ms
legacy-node-drag: p95 frame time 17ms
legacy-node-drag: layout duration 0ms
legacy-node-drag: style recalc duration 9ms
legacy-node-drag: layout count 0
legacy-node-drag: style recalc count 47
legacy-node-drag: task duration 1118ms
legacy-node-drag: script duration 372ms
legacy-node-drag: TBT 0ms
legacy-node-drag: heap used 71.4 MB
legacy-node-drag: DOM nodes -126
legacy-node-drag: event listeners 184
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 9
minimap-idle: task duration 488ms
minimap-idle: script duration 13ms
minimap-idle: TBT 0ms
minimap-idle: heap used 72.7 MB
minimap-idle: DOM nodes -266
minimap-idle: event listeners -149
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 9ms
subgraph-dom-widget-clipping: layout count 0
subgraph-dom-widget-clipping: style recalc count 47
subgraph-dom-widget-clipping: task duration 314ms
subgraph-dom-widget-clipping: script duration 96ms
subgraph-dom-widget-clipping: TBT 0ms
subgraph-dom-widget-clipping: heap used 68.8 MB
subgraph-dom-widget-clipping: DOM nodes 20
subgraph-dom-widget-clipping: event listeners 7
subgraph-idle: avg frame time 17ms
subgraph-idle: p95 frame time 17ms
subgraph-idle: layout duration 0ms
subgraph-idle: style recalc duration 6ms
subgraph-idle: layout count 0
subgraph-idle: style recalc count 11
subgraph-idle: task duration 385ms
subgraph-idle: script duration 5ms
subgraph-idle: TBT 0ms
subgraph-idle: heap used 58.6 MB
subgraph-idle: DOM nodes -281
subgraph-idle: event listeners -151
subgraph-mouse-sweep: avg frame time 17ms
subgraph-mouse-sweep: p95 frame time 17ms
subgraph-mouse-sweep: layout duration 3ms
subgraph-mouse-sweep: style recalc duration 27ms
subgraph-mouse-sweep: layout count 16
subgraph-mouse-sweep: style recalc count 75
subgraph-mouse-sweep: task duration 625ms
subgraph-mouse-sweep: script duration 66ms
subgraph-mouse-sweep: TBT 0ms
subgraph-mouse-sweep: heap used 65.9 MB
subgraph-mouse-sweep: DOM nodes -279
subgraph-mouse-sweep: event listeners -150
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 24ms
subgraph-transition-enter: layout count 15
subgraph-transition-enter: style recalc count 20
subgraph-transition-enter: task duration 702ms
subgraph-transition-enter: script duration 11ms
subgraph-transition-enter: TBT 80ms
subgraph-transition-enter: heap used 92.1 MB
subgraph-transition-enter: DOM nodes 13673
subgraph-transition-enter: event listeners 2373
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 38ms
viewport-pan-sweep: layout count 0
viewport-pan-sweep: style recalc count 250
viewport-pan-sweep: task duration 3186ms
viewport-pan-sweep: script duration 801ms
viewport-pan-sweep: TBT 0ms
viewport-pan-sweep: heap used 78.9 MB
viewport-pan-sweep: DOM nodes -260
viewport-pan-sweep: event listeners -133
vue-large-graph-idle: avg frame time 18ms
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 13958ms
vue-large-graph-idle: script duration 90ms
vue-large-graph-idle: TBT 0ms
vue-large-graph-idle: heap used 169.1 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 16ms
vue-large-graph-pan: layout count 0
vue-large-graph-pan: style recalc count 155
vue-large-graph-pan: task duration 17051ms
vue-large-graph-pan: script duration 346ms
vue-large-graph-pan: TBT 19ms
vue-large-graph-pan: heap used 184.5 MB
vue-large-graph-pan: DOM nodes -8312
vue-large-graph-pan: event listeners -16386
workflow-execution: avg frame time 17ms
workflow-execution: p95 frame time 17ms
workflow-execution: layout duration 3ms
workflow-execution: style recalc duration 13ms
workflow-execution: layout count 2
workflow-execution: style recalc count 7
workflow-execution: task duration 73ms
workflow-execution: script duration 5ms
workflow-execution: TBT 0ms
workflow-execution: heap used 60.6 MB
workflow-execution: DOM nodes 118
workflow-execution: event listeners 37
Raw data
{
  "timestamp": "2026-08-22T17:12:41.324Z",
  "gitSha": "050904b46335b251e0b5d676741a159cb83dc0b6",
  "branch": "dante/fe-1584-scope-billing-completion",
  "measurements": [
    {
      "name": "canvas-idle",
      "durationMs": 2054.7900000000254,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 9.042000000000002,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 427.264,
      "heapDeltaBytes": 17212336,
      "heapUsedBytes": 78721644,
      "domNodes": -282,
      "jsHeapTotalBytes": 3923968,
      "scriptDurationMs": 6.571000000000001,
      "eventListeners": -151,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-idle",
      "durationMs": 2006.9179999999847,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 5.8900000000000015,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 381.655,
      "heapDeltaBytes": -3371584,
      "heapUsedBytes": 58044316,
      "domNodes": -282,
      "jsHeapTotalBytes": 4972544,
      "scriptDurationMs": 5.951,
      "eventListeners": -151,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 1720.119000000011,
      "styleRecalcs": 71,
      "styleRecalcDurationMs": 28.880000000000003,
      "layouts": 12,
      "layoutDurationMs": 3.439,
      "taskDurationMs": 727.261,
      "heapDeltaBytes": -4168624,
      "heapUsedBytes": 56333572,
      "domNodes": -282,
      "jsHeapTotalBytes": 4710400,
      "scriptDurationMs": 85.28099999999999,
      "eventListeners": -149,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 1712.1980000000008,
      "styleRecalcs": 71,
      "styleRecalcDurationMs": 27.169,
      "layouts": 12,
      "layoutDurationMs": 2.979,
      "taskDurationMs": 673.2230000000001,
      "heapDeltaBytes": 1841252,
      "heapUsedBytes": 62116544,
      "domNodes": -282,
      "jsHeapTotalBytes": 4448256,
      "scriptDurationMs": 80.263,
      "eventListeners": -151,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1660.4839999999967,
      "styleRecalcs": 31,
      "styleRecalcDurationMs": 14.139000000000001,
      "layouts": 6,
      "layoutDurationMs": 0.642,
      "taskDurationMs": 285.55299999999994,
      "heapDeltaBytes": 3107424,
      "heapUsedBytes": 63926056,
      "domNodes": 77,
      "jsHeapTotalBytes": 4718592,
      "scriptDurationMs": 8.408999999999999,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1730.731999999989,
      "styleRecalcs": 30,
      "styleRecalcDurationMs": 13.252000000000002,
      "layouts": 6,
      "layoutDurationMs": 0.6169999999999999,
      "taskDurationMs": 287.817,
      "heapDeltaBytes": 2798932,
      "heapUsedBytes": 63156900,
      "domNodes": 76,
      "jsHeapTotalBytes": 4718592,
      "scriptDurationMs": 8.475000000000001,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 545.880000000011,
      "styleRecalcs": 11,
      "styleRecalcDurationMs": 6.430999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 304.924,
      "heapDeltaBytes": 9975344,
      "heapUsedBytes": 70665792,
      "domNodes": 18,
      "jsHeapTotalBytes": 4980736,
      "scriptDurationMs": 45.45300000000001,
      "eventListeners": 2,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.670000000000012,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 467.27700000002415,
      "styleRecalcs": 14,
      "styleRecalcDurationMs": 7.561,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 283.922,
      "heapDeltaBytes": 9980392,
      "heapUsedBytes": 70659932,
      "domNodes": 22,
      "jsHeapTotalBytes": 4980736,
      "scriptDurationMs": 43.721,
      "eventListeners": 2,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000273
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2026.9819999999754,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 7.283999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 505.65700000000004,
      "heapDeltaBytes": 2337632,
      "heapUsedBytes": 76421820,
      "domNodes": -262,
      "jsHeapTotalBytes": -2887680,
      "scriptDurationMs": 13.883,
      "eventListeners": -149,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2020.9300000000212,
      "styleRecalcs": 11,
      "styleRecalcDurationMs": 6.794999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 480.095,
      "heapDeltaBytes": -11227604,
      "heapUsedBytes": 62958280,
      "domNodes": -278,
      "jsHeapTotalBytes": -2887680,
      "scriptDurationMs": 12.508,
      "eventListeners": -177,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2082.3180000000434,
      "styleRecalcs": 70,
      "styleRecalcDurationMs": 14.043999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 894.933,
      "heapDeltaBytes": 152076,
      "heapUsedBytes": 75071040,
      "domNodes": -260,
      "jsHeapTotalBytes": -2138112,
      "scriptDurationMs": 248.74800000000002,
      "eventListeners": -149,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2077.4189999999635,
      "styleRecalcs": 68,
      "styleRecalcDurationMs": 13.937999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 916.767,
      "heapDeltaBytes": 12791824,
      "heapUsedBytes": 86838608,
      "domNodes": -283,
      "jsHeapTotalBytes": 5201920,
      "scriptDurationMs": 259.365,
      "eventListeners": -179,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3170.7490000000007,
      "styleRecalcs": 67,
      "styleRecalcDurationMs": 14.376999999999997,
      "layouts": 60,
      "layoutDurationMs": 7.174999999999999,
      "taskDurationMs": 1138.5149999999999,
      "heapDeltaBytes": -10419944,
      "heapUsedBytes": 66062752,
      "domNodes": -283,
      "jsHeapTotalBytes": 5500928,
      "scriptDurationMs": 318.66,
      "eventListeners": -153,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3110.3489999999283,
      "styleRecalcs": 66,
      "styleRecalcDurationMs": 13.948000000000002,
      "layouts": 60,
      "layoutDurationMs": 6.853,
      "taskDurationMs": 1135.174,
      "heapDeltaBytes": -13764348,
      "heapUsedBytes": 61974192,
      "domNodes": -285,
      "jsHeapTotalBytes": 4976640,
      "scriptDurationMs": 339.606,
      "eventListeners": -183,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "legacy-node-drag",
      "durationMs": 2121.9900000000393,
      "styleRecalcs": 48,
      "styleRecalcDurationMs": 10.262999999999998,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1118.0590000000002,
      "heapDeltaBytes": 9176520,
      "heapUsedBytes": 86096328,
      "domNodes": 16,
      "jsHeapTotalBytes": 7270400,
      "scriptDurationMs": 374.927,
      "eventListeners": 184,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "legacy-node-drag",
      "durationMs": 2090.9580000000005,
      "styleRecalcs": 45,
      "styleRecalcDurationMs": 7.696999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1117.7130000000002,
      "heapDeltaBytes": 1223356,
      "heapUsedBytes": 63654008,
      "domNodes": -268,
      "jsHeapTotalBytes": 5242880,
      "scriptDurationMs": 369.08599999999996,
      "eventListeners": 184,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "minimap-idle",
      "durationMs": 2051.0509999999726,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 5.817000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 508.54999999999995,
      "heapDeltaBytes": -5282492,
      "heapUsedBytes": 75303216,
      "domNodes": -270,
      "jsHeapTotalBytes": 3403776,
      "scriptDurationMs": 13.339,
      "eventListeners": -149,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66999999999998,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "minimap-idle",
      "durationMs": 2036.4769999999908,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 5.203000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 467.209,
      "heapDeltaBytes": -3617692,
      "heapUsedBytes": 77240068,
      "domNodes": -262,
      "jsHeapTotalBytes": 3997696,
      "scriptDurationMs": 12.555999999999997,
      "eventListeners": -149,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 532.1799999999826,
      "styleRecalcs": 47,
      "styleRecalcDurationMs": 9.177,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 324.75399999999996,
      "heapDeltaBytes": 10983100,
      "heapUsedBytes": 71714084,
      "domNodes": 20,
      "jsHeapTotalBytes": 5242880,
      "scriptDurationMs": 99.01499999999999,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666682,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 514.3419999999423,
      "styleRecalcs": 47,
      "styleRecalcDurationMs": 8.506,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 303.399,
      "heapDeltaBytes": 11811152,
      "heapUsedBytes": 72666052,
      "domNodes": 20,
      "jsHeapTotalBytes": 4718592,
      "scriptDurationMs": 92.93900000000001,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.663333333333338,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-idle",
      "durationMs": 1991.5100000000052,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 6.5889999999999995,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 384.074,
      "heapDeltaBytes": 546296,
      "heapUsedBytes": 61710440,
      "domNodes": -280,
      "jsHeapTotalBytes": 4448256,
      "scriptDurationMs": 5.1880000000000015,
      "eventListeners": -151,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-idle",
      "durationMs": 2009.5230000000015,
      "styleRecalcs": 11,
      "styleRecalcDurationMs": 6.188999999999998,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 385.014,
      "heapDeltaBytes": 385700,
      "heapUsedBytes": 61130844,
      "domNodes": -281,
      "jsHeapTotalBytes": 4710400,
      "scriptDurationMs": 5.232999999999998,
      "eventListeners": -151,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1691.2319999999568,
      "styleRecalcs": 75,
      "styleRecalcDurationMs": 28.79,
      "layouts": 16,
      "layoutDurationMs": 3.680999999999999,
      "taskDurationMs": 633.664,
      "heapDeltaBytes": -2642880,
      "heapUsedBytes": 57966392,
      "domNodes": -279,
      "jsHeapTotalBytes": 5758976,
      "scriptDurationMs": 68.34400000000001,
      "eventListeners": -151,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1694.8359999999525,
      "styleRecalcs": 75,
      "styleRecalcDurationMs": 25.364,
      "layouts": 16,
      "layoutDurationMs": 3.1380000000000003,
      "taskDurationMs": 616.7560000000001,
      "heapDeltaBytes": 19116748,
      "heapUsedBytes": 80174104,
      "domNodes": -278,
      "jsHeapTotalBytes": 4448256,
      "scriptDurationMs": 63.26699999999999,
      "eventListeners": -149,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-transition-enter",
      "durationMs": 866.9399999999996,
      "styleRecalcs": 20,
      "styleRecalcDurationMs": 24.290999999999993,
      "layouts": 15,
      "layoutDurationMs": 10.663999999999998,
      "taskDurationMs": 702.2870000000001,
      "heapDeltaBytes": -467036,
      "heapUsedBytes": 96523592,
      "domNodes": 13673,
      "jsHeapTotalBytes": 10485760,
      "scriptDurationMs": 11.275999999999994,
      "eventListeners": 2373,
      "totalBlockingTimeMs": 80,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 8183.670000000006,
      "styleRecalcs": 251,
      "styleRecalcDurationMs": 42.098,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 3236.084,
      "heapDeltaBytes": 7493160,
      "heapUsedBytes": 81587856,
      "domNodes": -256,
      "jsHeapTotalBytes": -827392,
      "scriptDurationMs": 801.0690000000001,
      "eventListeners": -133,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.670000000000012,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 8086.802000000034,
      "styleRecalcs": 249,
      "styleRecalcDurationMs": 34.61,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 3136.108,
      "heapDeltaBytes": 9512780,
      "heapUsedBytes": 83935660,
      "domNodes": -264,
      "jsHeapTotalBytes": -1351680,
      "scriptDurationMs": 801.055,
      "eventListeners": -133,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 14678.184999999985,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 13970.756,
      "heapDeltaBytes": -53534856,
      "heapUsedBytes": 178967660,
      "domNodes": -8312,
      "jsHeapTotalBytes": -14573568,
      "scriptDurationMs": 91.63300000000001,
      "eventListeners": -16389,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.780000000000047,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 14681.015000000003,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 13945.519,
      "heapDeltaBytes": -31871468,
      "heapUsedBytes": 175652228,
      "domNodes": -8312,
      "jsHeapTotalBytes": -16191488,
      "scriptDurationMs": 87.86299999999999,
      "eventListeners": -16391,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.223333333333358,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 17456.657000000007,
      "styleRecalcs": 156,
      "styleRecalcDurationMs": 15.86499999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 16930.715,
      "heapDeltaBytes": -24188500,
      "heapUsedBytes": 196223224,
      "domNodes": -8312,
      "jsHeapTotalBytes": -10231808,
      "scriptDurationMs": 342.508,
      "eventListeners": -16385,
      "totalBlockingTimeMs": 9,
      "frameDurationMs": 17.776666666666642,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 17734.118000000082,
      "styleRecalcs": 154,
      "styleRecalcDurationMs": 15.54899999999998,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 17171.287,
      "heapDeltaBytes": -28409784,
      "heapUsedBytes": 190676732,
      "domNodes": -8312,
      "jsHeapTotalBytes": -12066816,
      "scriptDurationMs": 348.55400000000003,
      "eventListeners": -16387,
      "totalBlockingTimeMs": 28,
      "frameDurationMs": 17.776666666666642,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "workflow-execution",
      "durationMs": 84.87400000001344,
      "styleRecalcs": 6,
      "styleRecalcDurationMs": 10.375999999999998,
      "layouts": 2,
      "layoutDurationMs": 0.39100000000000007,
      "taskDurationMs": 59.198,
      "heapDeltaBytes": 3156728,
      "heapUsedBytes": 63567748,
      "domNodes": 109,
      "jsHeapTotalBytes": 524288,
      "scriptDurationMs": 4.621,
      "eventListeners": 49,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.663333333333338,
      "p95FrameDurationMs": 16.700000000000273
    },
    {
      "name": "workflow-execution",
      "durationMs": 114.59500000000844,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 15.754000000000001,
      "layouts": 2,
      "layoutDurationMs": 5.841000000000001,
      "taskDurationMs": 86.15700000000001,
      "heapDeltaBytes": 3035444,
      "heapUsedBytes": 63429220,
      "domNodes": 126,
      "jsHeapTotalBytes": 0,
      "scriptDurationMs": 4.7010000000000005,
      "eventListeners": 25,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000273
    }
  ]
}

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

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

@github-actions

Copy link
Copy Markdown

🎭 Playwright: ⏳ Running...

@github-actions github-actions Bot added the risk:R3 PR risk grade (advisory shadow check; grader-owned) label Aug 19, 2026
@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.33333% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...platform/workspace/stores/billingOperationStore.ts 60.00% 6 Missing ⚠️
src/composables/billing/useBillingContext.ts 91.66% 1 Missing ⚠️
@@                           Coverage Diff                           @@
##             dante/fe-1584-guard-prompt-workspace   #15431   +/-   ##
=======================================================================
  Coverage                                        ?   80.98%           
=======================================================================
  Files                                           ?     1883           
  Lines                                           ?   123036           
  Branches                                        ?    37873           
=======================================================================
  Hits                                            ?    99640           
  Misses                                          ?    22895           
  Partials                                        ?      501           
Flag Coverage Δ
unit 73.01% <83.33%> (?)

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

Files with missing lines Coverage Δ
.../components/TopUpCreditsDialogContentWorkspace.vue 66.04% <100.00%> (ø)
...m/workspace/composables/useSubscriptionCheckout.ts 96.78% <100.00%> (ø)
src/composables/billing/useBillingContext.ts 77.88% <91.66%> (ø)
...platform/workspace/stores/billingOperationStore.ts 65.26% <60.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.

@dante01yoon
dante01yoon force-pushed the dante/fe-1584-guard-prompt-workspace branch from 7ab4c1a to 6d0ad50 Compare August 19, 2026 18:05
@dante01yoon
dante01yoon force-pushed the dante/fe-1584-scope-billing-completion branch from ec5211b to 679dd13 Compare August 19, 2026 18:06
@dante01yoon
dante01yoon force-pushed the dante/fe-1584-guard-prompt-workspace branch from 6d0ad50 to 398664a Compare August 19, 2026 18:09
@dante01yoon
dante01yoon force-pushed the dante/fe-1584-scope-billing-completion branch from 679dd13 to 92cd477 Compare August 19, 2026 18:09
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:S This PR changes 10-29 lines, ignoring generated files. labels Aug 22, 2026

@christian-byrne christian-byrne 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.

This is careful work. Snapshotting workspace ID plus transition generation and revalidating between the status and balance calls is the right fix for the TOCTOU window, and splitting the Promise.allSettled pair so the second call cannot land after a switch is a detail that is easy to miss. The A→B→A coverage is good.

One thing I think is a real problem, in pollOperation:

if (operation.workspaceId !== workspaceStore.activeWorkspaceId) {
  scheduleNextPoll(opId)
  return
}

if (stopIfTimedOut(opId, operation)) return

Moving the ownership check ahead of the timeout check is right for the UI goal, since A's failure should not surface in B. But it also means stopIfTimedOut is unreachable for as long as the operation's workspace is inactive, and the same reordering is in the catch branch.

scheduleNextPoll has exponential backoff capped at MAX_INTERVAL_MS and no attempt cap, and the only terminal conditions are the operation being removed, its status leaving pending, or stopIfTimedOut firing. So an operation started in A, where the user switches to B and stays there, polls forever at the max interval and never reaches a terminal state. It holds a setTimeout chain and a map entry for the rest of the session.

I think the intent is to defer presenting the timeout, not to defer evaluating it. Could the inactive path still evaluate the timeout and resolve terminal silently, and only suppress the failure UI? Something like marking it timed out without the toast, so a user who never returns to A does not leave a poll loop running.

Worth a test for the case where the user switches away and never returns, since the current suite covers switching away and coming back.

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

Labels

risk:R3 PR risk grade (advisory shadow check; grader-owned) size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants