fix(billing): resolve terminal promise even if success side effects throw - #14964
fix(billing): resolve terminal promise even if success side effects throw#14964christian-byrne wants to merge 4 commits into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThe billing success handler now resolves terminal operations even when post-success cleanup, reconciliation, or UI side effects throw. Tests cover top-up toast failures and subscription reconciliation failures. ChangesBilling success handling
Estimated code review effort: 2 (Simple) | ~10 minutes Suggested reviewers: Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 inconclusive)
✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
🎨 Storybook: ✅ Built — View Storybook🎭 Playwright: ✅ 1794 passed, 0 failed · 6 flaky📊 Browser Reports
📦 Bundle: 8.69 MB gzip 🔴 +64 BDetailsSummary
Category Glance App Entry Points — 3.67 kB (baseline 3.67 kB) • ⚪ 0 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.36 MB (baseline 1.36 MB) • ⚪ 0 BGraph editor runtime, canvas, workflow orchestration
Status: 2 added / 2 removed / 1 unchanged Views & Navigation — 124 kB (baseline 124 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 13 added / 13 removed / 4 unchanged Panels & Settings — 565 kB (baseline 565 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 10 added / 10 removed / 16 unchanged User & Accounts — 27 kB (baseline 27 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 6 added / 6 removed / 4 unchanged Editors & Dialogs — 125 kB (baseline 125 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 7 added / 7 removed / 1 unchanged UI Components — 67.1 kB (baseline 67.1 kB) • ⚪ 0 BReusable component library chunks
Status: 6 added / 6 removed / 8 unchanged Data & Services — 3.5 MB (baseline 3.5 MB) • 🔴 +71 BStores, services, APIs, and repositories
Status: 14 added / 14 removed / 3 unchanged Utilities & Hooks — 550 kB (baseline 550 kB) • ⚪ 0 BHelpers, composables, and utility bundles
Status: 18 added / 18 removed / 20 unchanged Vendor & Third-Party — 16.3 MB (baseline 16.3 MB) • ⚪ 0 BExternal libraries and shared vendor chunks Status: 17 unchanged Other — 14 MB (baseline 14 MB) • ⚪ 0 BBundles that do not match a named category
Status: 68 added / 68 removed / 217 unchanged ⚡ Performance Report
Show regressions
All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-08-11T22:52:49.985Z",
"gitSha": "79cc6153fb556837c2992b33268d8f1122007f84",
"branch": "test/billing-op-store-coverage",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2051.4840000000163,
"styleRecalcs": 10,
"styleRecalcDurationMs": 8.036999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 518.826,
"heapDeltaBytes": 5759672,
"heapUsedBytes": 70311324,
"domNodes": 20,
"jsHeapTotalBytes": 24379392,
"scriptDurationMs": 21.648,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-idle",
"durationMs": 2037.4990000000253,
"styleRecalcs": 10,
"styleRecalcDurationMs": 7.122999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 445.83199999999994,
"heapDeltaBytes": 5390824,
"heapUsedBytes": 69132040,
"domNodes": 20,
"jsHeapTotalBytes": 25427968,
"scriptDurationMs": 18.358000000000004,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1922.1569999999701,
"styleRecalcs": 79,
"styleRecalcDurationMs": 40.033,
"layouts": 12,
"layoutDurationMs": 3.865,
"taskDurationMs": 885.005,
"heapDeltaBytes": -12828816,
"heapUsedBytes": 51683428,
"domNodes": -283,
"jsHeapTotalBytes": 22519808,
"scriptDurationMs": 112.43900000000001,
"eventListeners": -153,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1849.62900000005,
"styleRecalcs": 75,
"styleRecalcDurationMs": 36.443000000000005,
"layouts": 12,
"layoutDurationMs": 3.7670000000000003,
"taskDurationMs": 887.783,
"heapDeltaBytes": -12000448,
"heapUsedBytes": 52297544,
"domNodes": -277,
"jsHeapTotalBytes": 23306240,
"scriptDurationMs": 114.77700000000002,
"eventListeners": -153,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1724.5349999999462,
"styleRecalcs": 30,
"styleRecalcDurationMs": 15.803999999999998,
"layouts": 6,
"layoutDurationMs": 0.8340000000000001,
"taskDurationMs": 377.512,
"heapDeltaBytes": 8752136,
"heapUsedBytes": 73294572,
"domNodes": 76,
"jsHeapTotalBytes": 24379392,
"scriptDurationMs": 20.340999999999998,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1739.465999999993,
"styleRecalcs": 31,
"styleRecalcDurationMs": 16.244,
"layouts": 6,
"layoutDurationMs": 0.665,
"taskDurationMs": 392.38800000000003,
"heapDeltaBytes": 8785360,
"heapUsedBytes": 73177484,
"domNodes": 78,
"jsHeapTotalBytes": 24641536,
"scriptDurationMs": 22.757,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "dom-widget-clipping",
"durationMs": 626.2679999999818,
"styleRecalcs": 12,
"styleRecalcDurationMs": 6.931000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 385.127,
"heapDeltaBytes": -11247764,
"heapUsedBytes": 53128360,
"domNodes": 20,
"jsHeapTotalBytes": 25690112,
"scriptDurationMs": 57.13999999999999,
"eventListeners": 0,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 563.8890000000174,
"styleRecalcs": 12,
"styleRecalcDurationMs": 6.956999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 358.188,
"heapDeltaBytes": -11228308,
"heapUsedBytes": 53038016,
"domNodes": 20,
"jsHeapTotalBytes": 25165824,
"scriptDurationMs": 51.30500000000001,
"eventListeners": 0,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-idle",
"durationMs": 2028.9510000000064,
"styleRecalcs": 7,
"styleRecalcDurationMs": 4.944999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 657.9079999999999,
"heapDeltaBytes": 7518120,
"heapUsedBytes": 67341492,
"domNodes": -284,
"jsHeapTotalBytes": 3510272,
"scriptDurationMs": 93.481,
"eventListeners": -149,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2060.1219999999785,
"styleRecalcs": 9,
"styleRecalcDurationMs": 6.856000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 652.4540000000001,
"heapDeltaBytes": 7435836,
"heapUsedBytes": 67390456,
"domNodes": -282,
"jsHeapTotalBytes": 3510272,
"scriptDurationMs": 91.081,
"eventListeners": -147,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-pan",
"durationMs": 2150.9190000000444,
"styleRecalcs": 69,
"styleRecalcDurationMs": 15.109000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1196.746,
"heapDeltaBytes": 4274880,
"heapUsedBytes": 64726108,
"domNodes": -283,
"jsHeapTotalBytes": 3440640,
"scriptDurationMs": 374.168,
"eventListeners": -147,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-pan",
"durationMs": 2138.6759999999185,
"styleRecalcs": 68,
"styleRecalcDurationMs": 14.111999999999997,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1252.3549999999998,
"heapDeltaBytes": 4653548,
"heapUsedBytes": 65962532,
"domNodes": -286,
"jsHeapTotalBytes": 3440640,
"scriptDurationMs": 409.696,
"eventListeners": -149,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3252.301999999986,
"styleRecalcs": 66,
"styleRecalcDurationMs": 17.403,
"layouts": 60,
"layoutDurationMs": 8.765,
"taskDurationMs": 1502.86,
"heapDeltaBytes": 249352,
"heapUsedBytes": 62827472,
"domNodes": -287,
"jsHeapTotalBytes": 5607424,
"scriptDurationMs": 473.65000000000003,
"eventListeners": -153,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3219.8600000000397,
"styleRecalcs": 66,
"styleRecalcDurationMs": 17.169000000000004,
"layouts": 60,
"layoutDurationMs": 9.037999999999998,
"taskDurationMs": 1445.1799999999998,
"heapDeltaBytes": 24277600,
"heapUsedBytes": 86887044,
"domNodes": 14,
"jsHeapTotalBytes": 7340032,
"scriptDurationMs": 479.21000000000004,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "minimap-idle",
"durationMs": 2042.5000000000182,
"styleRecalcs": 8,
"styleRecalcDurationMs": 6.541999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 656.25,
"heapDeltaBytes": 6835008,
"heapUsedBytes": 67504464,
"domNodes": -285,
"jsHeapTotalBytes": 2985984,
"scriptDurationMs": 94.45400000000001,
"eventListeners": -149,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2033.9389999999185,
"styleRecalcs": 8,
"styleRecalcDurationMs": 6.402999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 674.604,
"heapDeltaBytes": 6804372,
"heapUsedBytes": 67294472,
"domNodes": -282,
"jsHeapTotalBytes": 4034560,
"scriptDurationMs": 97.58,
"eventListeners": -149,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 599.2750000000342,
"styleRecalcs": 48,
"styleRecalcDurationMs": 11.687000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 406.83,
"heapDeltaBytes": -10702008,
"heapUsedBytes": 53837924,
"domNodes": 22,
"jsHeapTotalBytes": 25690112,
"scriptDurationMs": 118.723,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 568.7430000000404,
"styleRecalcs": 47,
"styleRecalcDurationMs": 10.016,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 393.492,
"heapDeltaBytes": -11599308,
"heapUsedBytes": 52169764,
"domNodes": 20,
"jsHeapTotalBytes": 26214400,
"scriptDurationMs": 112.486,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-idle",
"durationMs": 2027.2360000000162,
"styleRecalcs": 11,
"styleRecalcDurationMs": 7.964000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 461.687,
"heapDeltaBytes": 5904876,
"heapUsedBytes": 70536112,
"domNodes": 22,
"jsHeapTotalBytes": 24379392,
"scriptDurationMs": 18.31,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 2014.6540000000641,
"styleRecalcs": 11,
"styleRecalcDurationMs": 7.949999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 497.29499999999996,
"heapDeltaBytes": -17414424,
"heapUsedBytes": 47071028,
"domNodes": -278,
"jsHeapTotalBytes": 22519808,
"scriptDurationMs": 17.229000000000003,
"eventListeners": -153,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1693.930000000023,
"styleRecalcs": 76,
"styleRecalcDurationMs": 37.229,
"layouts": 16,
"layoutDurationMs": 5.163,
"taskDurationMs": 683.1859999999999,
"heapDeltaBytes": -3856840,
"heapUsedBytes": 60729680,
"domNodes": 62,
"jsHeapTotalBytes": 24903680,
"scriptDurationMs": 89.05,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1711.6959999999608,
"styleRecalcs": 76,
"styleRecalcDurationMs": 35.492000000000004,
"layouts": 16,
"layoutDurationMs": 4.615,
"taskDurationMs": 801.9879999999999,
"heapDeltaBytes": -17726740,
"heapUsedBytes": 46735032,
"domNodes": -279,
"jsHeapTotalBytes": 23306240,
"scriptDurationMs": 88.245,
"eventListeners": -153,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-transition-enter",
"durationMs": 1401.927999999998,
"styleRecalcs": 19,
"styleRecalcDurationMs": 27.804000000000002,
"layouts": 15,
"layoutDurationMs": 10.567,
"taskDurationMs": 880.5920000000001,
"heapDeltaBytes": 3476984,
"heapUsedBytes": 74681668,
"domNodes": 13673,
"jsHeapTotalBytes": 15990784,
"scriptDurationMs": 33.594,
"eventListeners": 2375,
"totalBlockingTimeMs": 130,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8187.780000000089,
"styleRecalcs": 249,
"styleRecalcDurationMs": 44.503,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4357.313999999999,
"heapDeltaBytes": 8729140,
"heapUsedBytes": 68166836,
"domNodes": -283,
"jsHeapTotalBytes": 5799936,
"scriptDurationMs": 1259.857,
"eventListeners": -133,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8200.531999999952,
"styleRecalcs": 250,
"styleRecalcDurationMs": 43.544,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4547.716,
"heapDeltaBytes": 14656284,
"heapUsedBytes": 74874048,
"domNodes": -284,
"jsHeapTotalBytes": 8945664,
"scriptDurationMs": 1430.204,
"eventListeners": -133,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 14428.174000000014,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14412.078999999998,
"heapDeltaBytes": -31541520,
"heapUsedBytes": 166520588,
"domNodes": -8312,
"jsHeapTotalBytes": -10428416,
"scriptDurationMs": 520.475,
"eventListeners": -16387,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 14576.70100000007,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14549.262999999999,
"heapDeltaBytes": -51501236,
"heapUsedBytes": 166135348,
"domNodes": -8312,
"jsHeapTotalBytes": -7540736,
"scriptDurationMs": 533.1740000000001,
"eventListeners": -16387,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-pan",
"durationMs": 17489.65999999996,
"styleRecalcs": 121,
"styleRecalcDurationMs": 17.574999999999978,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 17464.413,
"heapDeltaBytes": -49302624,
"heapUsedBytes": 156533212,
"domNodes": -8312,
"jsHeapTotalBytes": -8855552,
"scriptDurationMs": 884.055,
"eventListeners": -16385,
"totalBlockingTimeMs": 24,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 17530.54599999996,
"styleRecalcs": 120,
"styleRecalcDurationMs": 18.062999999999967,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 17499.284,
"heapDeltaBytes": -72511556,
"heapUsedBytes": 153471328,
"domNodes": -8312,
"jsHeapTotalBytes": -10166272,
"scriptDurationMs": 876.437,
"eventListeners": -16383,
"totalBlockingTimeMs": 49,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "workflow-execution",
"durationMs": 474.6549999999843,
"styleRecalcs": 14,
"styleRecalcDurationMs": 21.238999999999997,
"layouts": 3,
"layoutDurationMs": 1.6119999999999999,
"taskDurationMs": 126.72299999999997,
"heapDeltaBytes": 5063040,
"heapUsedBytes": 68749036,
"domNodes": 126,
"jsHeapTotalBytes": 5242880,
"scriptDurationMs": 11.094,
"eventListeners": 99,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "workflow-execution",
"durationMs": 469.9319999999716,
"styleRecalcs": 12,
"styleRecalcDurationMs": 18.973000000000003,
"layouts": 3,
"layoutDurationMs": 0.8059999999999998,
"taskDurationMs": 110.624,
"heapDeltaBytes": 5045044,
"heapUsedBytes": 68532612,
"domNodes": 123,
"jsHeapTotalBytes": 4718592,
"scriptDurationMs": 9.302999999999999,
"eventListeners": 97,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.700000000000728
}
]
} |
…hrow A thrown error in the success-path side-effects block (dialog close, settings show, or toast add) silently skips resolveTerminal, leaving any caller awaiting startOperation() hanging forever. poll()'s catch-all swallowed the exception, masking the hang. Adds a try/finally so resolveTerminal runs unconditionally after the success side-effects block. Adds a regression test that throws from the second mockToastAdd call and asserts the terminal promise still resolves to succeeded. Found in the review of #14421. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. @@ Coverage Diff @@
## main #14964 +/- ##
==========================================
- Coverage 81.30% 81.20% -0.10%
==========================================
Files 1871 1873 +2
Lines 106640 107810 +1170
Branches 31425 32208 +783
==========================================
+ Hits 86701 87548 +847
- Misses 19591 19884 +293
- Partials 348 378 +30
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 59 files with indirect coverage changes 🚀 New features to boost your workflow:
|
2bbb3af to
e3442d9
Compare
dante01yoon
left a comment
There was a problem hiding this comment.
The change correctly prevents the terminal promise from hanging when the dialog/toast success effects throw. The regression test covers the reported failure path. I left one non-blocking suggestion about broadening the guarantee to the other post-success effects.
| useDialogStore().closeDialog({ key: 'top-up-credits' }) | ||
| useSettingsDialog().show('workspace') | ||
| } | ||
| try { |
There was a problem hiding this comment.
suggestion (non-blocking): Could we place this try/finally around all post-success effects after the operation is marked succeeded? As written, a synchronous throw from telemetry, billing reconciliation setup, or the cancel workspace update can still leave the terminal promise unresolved. This patch fixes the reported dialog/toast path, so this does not need to block the PR.
The cancel path's updateActiveWorkspace call and dialog/toast calls are now all guarded by the same try/finally, so resolveTerminal fires even if any synchronous side effect throws. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/platform/workspace/stores/billingOperationStore.ts`:
- Around line 416-441: Move the existing try/finally boundary in the terminal
operation flow to immediately follow updateOperationStatus, enclosing cleanup,
telemetry, reconciliation, and the current success handling so
resolveTerminal(opId) always executes. Add a regression test that makes a
reconciliation method throw synchronously and verifies the terminal promise
still resolves.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: effeceb8-512f-4b0c-b879-0b5803bbb825
📒 Files selected for processing (2)
src/platform/workspace/stores/billingOperationStore.test.tssrc/platform/workspace/stores/billingOperationStore.ts
| try { | ||
| if (operation.type === 'cancel') { | ||
| useTeamWorkspaceStore().updateActiveWorkspace({ isSubscribed: false }) | ||
| return | ||
| } | ||
|
|
||
| const toastStore = useToastStore() | ||
| const messageKey = | ||
| operation.type === 'subscription' | ||
| ? 'billingOperation.subscriptionSuccess' | ||
| : 'billingOperation.topupSuccess' | ||
| // A subscription checkout shows its own success step in the pricing dialog, | ||
| // so leave it open. Top-ups have no such step: close and surface settings. | ||
| if (operation.type === 'topup') { | ||
| useDialogStore().closeDialog({ key: 'top-up-credits' }) | ||
| useSettingsDialog().show('workspace') | ||
| } | ||
|
|
||
| toastStore.add({ | ||
| severity: 'success', | ||
| summary: t(messageKey), | ||
| life: 5000 | ||
| }) | ||
| const toastStore = useToastStore() | ||
| const messageKey = | ||
| operation.type === 'subscription' | ||
| ? 'billingOperation.subscriptionSuccess' | ||
| : 'billingOperation.topupSuccess' | ||
|
|
||
| resolveTerminal(opId) | ||
| toastStore.add({ | ||
| severity: 'success', | ||
| summary: t(messageKey), | ||
| life: 5000 | ||
| }) | ||
| } finally { | ||
| resolveTerminal(opId) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Expand the terminal-resolution boundary.
cleanup, telemetry, and reconciliation run before this try/finally. If one throws synchronously, resolveTerminal does not run. poll then returns from its catch path because updateOperationStatus replaced the operation object, so the terminal promise remains pending.
Start the try/finally immediately after updateOperationStatus. Include cleanup, telemetry, and reconciliation. Add a regression test that makes a reconciliation method throw synchronously.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/platform/workspace/stores/billingOperationStore.ts` around lines 416 -
441, Move the existing try/finally boundary in the terminal operation flow to
immediately follow updateOperationStatus, enclosing cleanup, telemetry,
reconciliation, and the current success handling so resolveTerminal(opId) always
executes. Add a regression test that makes a reconciliation method throw
synchronously and verifies the terminal promise still resolves.
…ciliation resolveTerminal now fires even if any synchronous side effect in handleSuccess throws — including cleanup, telemetry dispatches, billing context reconciliation, or the cancel workspace update. Adds a regression test verifying the terminal promise resolves when reconcileSubscriptionSuccess throws synchronously. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/platform/workspace/stores/billingOperationStore.ts`:
- Around line 408-416: Update the success-side effects around
billingContext.reconcileSubscriptionSuccess, fetchStatus, and fetchBalance to
catch and log any rejected error before preserving the existing propagation or
return behavior. Keep the finally-based resolveTerminal flow unchanged, and use
the store’s existing logging mechanism.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 698f6984-e300-4643-9327-e4f2a7af1e1f
📒 Files selected for processing (2)
src/platform/workspace/stores/billingOperationStore.test.tssrc/platform/workspace/stores/billingOperationStore.ts
| const billingContext = useBillingContext() | ||
| if (operation.type === 'subscription') { | ||
| await Promise.allSettled([billingContext.reconcileSubscriptionSuccess()]) | ||
| } else { | ||
| await Promise.allSettled([ | ||
| billingContext.fetchStatus(), | ||
| billingContext.fetchBalance() | ||
| ]) | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Consider logging the swallowed side-effect error.
The finally block guarantees resolveTerminal, but the original error still propagates out of handleSuccess. poll awaits handleSuccess inside its own try, so the rejection reaches the catch at line 258. There, currentOperation !== operation is true because updateOperationStatus replaced the map entry, so poll returns and discards the error.
Result: a reconciliation or telemetry failure leaves no diagnostic signal. Add a catch that logs the error before rethrowing or before returning, so failures in the success path stay observable.
♻️ Proposed observability improvement
} finally {
resolveTerminal(opId)
}+ } catch (error) {
+ console.error(`Billing operation ${opId} success handling failed`, error)
} finally {
resolveTerminal(opId)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const billingContext = useBillingContext() | |
| if (operation.type === 'subscription') { | |
| await Promise.allSettled([billingContext.reconcileSubscriptionSuccess()]) | |
| } else { | |
| await Promise.allSettled([ | |
| billingContext.fetchStatus(), | |
| billingContext.fetchBalance() | |
| ]) | |
| } | |
| const billingContext = useBillingContext() | |
| if (operation.type === 'subscription') { | |
| await Promise.allSettled([billingContext.reconcileSubscriptionSuccess()]) | |
| } else { | |
| await Promise.allSettled([ | |
| billingContext.fetchStatus(), | |
| billingContext.fetchBalance() | |
| ]) | |
| } | |
| } catch (error) { | |
| console.error(`Billing operation ${opId} success handling failed`, error) | |
| } finally { | |
| resolveTerminal(opId) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/platform/workspace/stores/billingOperationStore.ts` around lines 408 -
416, Update the success-side effects around
billingContext.reconcileSubscriptionSuccess, fetchStatus, and fetchBalance to
catch and log any rejected error before preserving the existing propagation or
return behavior. Keep the finally-based resolveTerminal flow unchanged, and use
the store’s existing logging mechanism.
|
Verified the current state — the |
|
Heads up — this now has a merge conflict against |
Summary
closeDialog,settingsDialog.show,toastStore.add) in atry/finallysoresolveTerminalruns unconditionally — even if one of those calls throws.mockToastAddcall (the success toast) and asserts the terminal promise still resolves to{ status: 'succeeded' }.Root cause
resolveTerminal(opId)was the last line ofhandleSuccess, aftertoastStore.add(...). A thrown error in any of the dialog/toast calls skippedresolveTerminal, leaving any callerawaitingstartOperation()hanging forever.poll()'scatchblock swallowed the exception from outside, masking the hang entirely.The fix was identified during the review of #14421. That PR's second commit (
428c614) fixed exactly this bug in its version of the store — but the bug survived inmainbecause subsequent billing PRs re-introduced the same call structure without carrying thetry/finallyforward.Relationship to #14421
PR #14421 (
refactor: convert billingOperationStore to discriminated union) can be closed — its implementation work was absorbed by the billing PRs that landed onmainbetween July 30–August 9 (3DS support, cancel op type, workspace-scoping, terminal promise, richer telemetry). This PR backfills the one remaining piece: thetry/finallybug fix and its regression test.Test plan
pnpm test:unit src/platform/workspace/stores/billingOperationStore.test.ts— 70 passed (was 69, new test is the regression)mainbefore this fix (times out at 5000ms), passes after🤖 Generated with Claude Code