From 4d414ca2d31cd732c624cad7ff34bd5d699cfaf4 Mon Sep 17 00:00:00 2001 From: dante01yoon Date: Thu, 11 Jun 2026 13:54:27 +0900 Subject: [PATCH 1/3] fix(billing): route subscription/sign-in/credit preconditions to modal, out of error panel (FE-878) --- .../useAccountPreconditionDialog.test.ts | 58 ++++++++ .../useAccountPreconditionDialog.ts | 38 +++++ .../accountPreconditionRouting.test.ts | 132 ++++++++++++++++++ .../accountPreconditionRouting.ts | 65 +++++++++ src/scripts/app.ts | 23 ++- src/stores/executionStore.test.ts | 69 +++++++++ src/stores/executionStore.ts | 19 +++ 7 files changed, 390 insertions(+), 14 deletions(-) create mode 100644 src/platform/cloud/subscription/composables/useAccountPreconditionDialog.test.ts create mode 100644 src/platform/cloud/subscription/composables/useAccountPreconditionDialog.ts create mode 100644 src/platform/errorCatalog/accountPreconditionRouting.test.ts create mode 100644 src/platform/errorCatalog/accountPreconditionRouting.ts diff --git a/src/platform/cloud/subscription/composables/useAccountPreconditionDialog.test.ts b/src/platform/cloud/subscription/composables/useAccountPreconditionDialog.test.ts new file mode 100644 index 00000000000..64f102bf8dd --- /dev/null +++ b/src/platform/cloud/subscription/composables/useAccountPreconditionDialog.test.ts @@ -0,0 +1,58 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { useAccountPreconditionDialog } from './useAccountPreconditionDialog' + +const mockDialogService = { + showApiNodesSignInDialog: vi.fn(), + showSubscriptionRequiredDialog: vi.fn(), + showTopUpCreditsDialog: vi.fn() +} + +vi.mock('@/services/dialogService', () => ({ + useDialogService: vi.fn(() => mockDialogService) +})) + +describe('useAccountPreconditionDialog', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('routes a sign-in precondition to the API sign-in dialog with the node type', () => { + useAccountPreconditionDialog().open('sign_in', { nodeType: 'ApiNode' }) + + expect(mockDialogService.showApiNodesSignInDialog).toHaveBeenCalledWith([ + 'ApiNode' + ]) + expect( + mockDialogService.showSubscriptionRequiredDialog + ).not.toHaveBeenCalled() + expect(mockDialogService.showTopUpCreditsDialog).not.toHaveBeenCalled() + }) + + it('routes a sign-in precondition with no node type to an empty list', () => { + useAccountPreconditionDialog().open('sign_in') + + expect(mockDialogService.showApiNodesSignInDialog).toHaveBeenCalledWith([]) + }) + + it('routes a subscription precondition to the subscription dialog', () => { + useAccountPreconditionDialog().open('subscription') + + expect( + mockDialogService.showSubscriptionRequiredDialog + ).toHaveBeenCalledTimes(1) + expect(mockDialogService.showApiNodesSignInDialog).not.toHaveBeenCalled() + expect(mockDialogService.showTopUpCreditsDialog).not.toHaveBeenCalled() + }) + + it('routes a credit precondition to the top-up dialog', () => { + useAccountPreconditionDialog().open('credits', { nodeType: 'PartnerNode' }) + + expect(mockDialogService.showTopUpCreditsDialog).toHaveBeenCalledWith({ + isInsufficientCredits: true + }) + expect( + mockDialogService.showSubscriptionRequiredDialog + ).not.toHaveBeenCalled() + }) +}) diff --git a/src/platform/cloud/subscription/composables/useAccountPreconditionDialog.ts b/src/platform/cloud/subscription/composables/useAccountPreconditionDialog.ts new file mode 100644 index 00000000000..1828bd2e2cb --- /dev/null +++ b/src/platform/cloud/subscription/composables/useAccountPreconditionDialog.ts @@ -0,0 +1,38 @@ +import type { AccountPrecondition } from '@/platform/errorCatalog/accountPreconditionRouting' +import { useDialogService } from '@/services/dialogService' + +export interface AccountPreconditionContext { + /** Node type that triggered the precondition, used as modal context. */ + nodeType?: string +} + +// Routes a resolved account precondition to its dedicated modal. This is the +// single seam where FE-978 attaches role-aware (member vs owner) subscription +// content: the `subscription` branch resolves to the subscription dialog, whose +// inner content FE-978 specializes for cancelled/inactive team states. +export function useAccountPreconditionDialog() { + const dialogService = useDialogService() + + function open( + precondition: AccountPrecondition, + context: AccountPreconditionContext = {} + ): void { + switch (precondition) { + case 'sign_in': + void dialogService.showApiNodesSignInDialog( + context.nodeType ? [context.nodeType] : [] + ) + return + case 'subscription': + void dialogService.showSubscriptionRequiredDialog() + return + case 'credits': + void dialogService.showTopUpCreditsDialog({ + isInsufficientCredits: true + }) + return + } + } + + return { open } +} diff --git a/src/platform/errorCatalog/accountPreconditionRouting.test.ts b/src/platform/errorCatalog/accountPreconditionRouting.test.ts new file mode 100644 index 00000000000..cfe6390575c --- /dev/null +++ b/src/platform/errorCatalog/accountPreconditionRouting.test.ts @@ -0,0 +1,132 @@ +import { describe, expect, it } from 'vitest' + +import { + isAccountPreconditionCatalogId, + preconditionForCatalogId, + resolveAccountPrecondition, + selectHighestPrecedencePrecondition +} from './accountPreconditionRouting' +import { + EXECUTION_FAILED_CATALOG_ID, + INSUFFICIENT_CREDITS_CATALOG_ID, + SIGN_IN_REQUIRED_CATALOG_ID, + SUBSCRIPTION_REQUIRED_CATALOG_ID, + SUBSCRIPTION_UPGRADE_REQUIRED_CATALOG_ID, + WORKSPACE_INSUFFICIENT_CREDITS_CATALOG_ID +} from './catalogIds' + +describe('resolveAccountPrecondition', () => { + it('classifies a sign-in error', () => { + expect( + resolveAccountPrecondition({ + exceptionType: 'RuntimeError', + exceptionMessage: 'Unauthorized: Please login first to use this node.' + }) + ).toBe('sign_in') + }) + + it('classifies an inactive-subscription error', () => { + expect( + resolveAccountPrecondition({ + exceptionType: 'InactiveSubscriptionError', + exceptionMessage: + 'User has no active subscription. Please subscribe to a plan to continue.' + }) + ).toBe('subscription') + }) + + it('classifies a subscription-upgrade error as a subscription precondition', () => { + expect( + resolveAccountPrecondition({ + exceptionType: 'RuntimeError', + exceptionMessage: + 'the following private models require a subscription upgrade: flux-pro' + }) + ).toBe('subscription') + }) + + it('classifies an account credit error', () => { + expect( + resolveAccountPrecondition({ + exceptionType: 'InsufficientFundsError', + exceptionMessage: + 'Payment Required: Please add credits to your account to use this node.' + }) + ).toBe('credits') + }) + + it('classifies a workspace credit error', () => { + expect( + resolveAccountPrecondition({ + exceptionType: 'RuntimeError', + exceptionMessage: + 'Payment Required: Please add credits to your workspace to continue.' + }) + ).toBe('credits') + }) + + it('returns undefined for an ordinary workflow error', () => { + expect( + resolveAccountPrecondition({ + exceptionType: 'RuntimeError', + exceptionMessage: 'CUDA out of memory' + }) + ).toBeUndefined() + }) +}) + +describe('preconditionForCatalogId / isAccountPreconditionCatalogId', () => { + it('maps every precondition catalog id', () => { + expect(preconditionForCatalogId(SIGN_IN_REQUIRED_CATALOG_ID)).toBe( + 'sign_in' + ) + expect(preconditionForCatalogId(SUBSCRIPTION_REQUIRED_CATALOG_ID)).toBe( + 'subscription' + ) + expect( + preconditionForCatalogId(SUBSCRIPTION_UPGRADE_REQUIRED_CATALOG_ID) + ).toBe('subscription') + expect(preconditionForCatalogId(INSUFFICIENT_CREDITS_CATALOG_ID)).toBe( + 'credits' + ) + expect( + preconditionForCatalogId(WORKSPACE_INSUFFICIENT_CREDITS_CATALOG_ID) + ).toBe('credits') + }) + + it('does not treat a workflow error catalog id as a precondition', () => { + expect( + preconditionForCatalogId(EXECUTION_FAILED_CATALOG_ID) + ).toBeUndefined() + expect(isAccountPreconditionCatalogId(EXECUTION_FAILED_CATALOG_ID)).toBe( + false + ) + expect(isAccountPreconditionCatalogId(undefined)).toBe(false) + }) +}) + +describe('selectHighestPrecedencePrecondition', () => { + it('prefers sign-in over subscription and credits', () => { + expect( + selectHighestPrecedencePrecondition([ + 'credits', + 'subscription', + 'sign_in' + ]) + ).toBe('sign_in') + }) + + it('prefers subscription over credits', () => { + expect( + selectHighestPrecedencePrecondition(['credits', 'subscription']) + ).toBe('subscription') + }) + + it('returns the only precondition present', () => { + expect(selectHighestPrecedencePrecondition(['credits'])).toBe('credits') + }) + + it('returns undefined when none are present', () => { + expect(selectHighestPrecedencePrecondition([])).toBeUndefined() + }) +}) diff --git a/src/platform/errorCatalog/accountPreconditionRouting.ts b/src/platform/errorCatalog/accountPreconditionRouting.ts new file mode 100644 index 00000000000..f38773b3af6 --- /dev/null +++ b/src/platform/errorCatalog/accountPreconditionRouting.ts @@ -0,0 +1,65 @@ +import { + INSUFFICIENT_CREDITS_CATALOG_ID, + SIGN_IN_REQUIRED_CATALOG_ID, + SUBSCRIPTION_REQUIRED_CATALOG_ID, + SUBSCRIPTION_UPGRADE_REQUIRED_CATALOG_ID, + WORKSPACE_INSUFFICIENT_CREDITS_CATALOG_ID +} from './catalogIds' +import { resolveRuntimeCatalogMatch } from './runtimeErrorMatcher' + +// Account preconditions are gating states (sign-in, subscription, credits) that +// must open their own modal instead of surfacing as a workflow error. They are +// excluded from the error panel and the error count. +export type AccountPrecondition = 'sign_in' | 'subscription' | 'credits' + +interface AccountPreconditionInfo { + exceptionType: string + exceptionMessage: string +} + +// Lower index wins. Sign-in must resolve before a subscription prompt, and any +// payment precondition takes precedence over a credits top-up. +const PRECONDITION_PRECEDENCE: AccountPrecondition[] = [ + 'sign_in', + 'subscription', + 'credits' +] + +const CATALOG_ID_TO_PRECONDITION = new Map([ + [SIGN_IN_REQUIRED_CATALOG_ID, 'sign_in'], + [SUBSCRIPTION_REQUIRED_CATALOG_ID, 'subscription'], + [SUBSCRIPTION_UPGRADE_REQUIRED_CATALOG_ID, 'subscription'], + [INSUFFICIENT_CREDITS_CATALOG_ID, 'credits'], + [WORKSPACE_INSUFFICIENT_CREDITS_CATALOG_ID, 'credits'] +]) + +export function preconditionForCatalogId( + catalogId: string | undefined +): AccountPrecondition | undefined { + if (!catalogId) return undefined + return CATALOG_ID_TO_PRECONDITION.get(catalogId) +} + +export function isAccountPreconditionCatalogId( + catalogId: string | undefined +): boolean { + return preconditionForCatalogId(catalogId) !== undefined +} + +// Classifies a single runtime error payload into the account precondition it +// represents, or `undefined` when it is an ordinary workflow error. +export function resolveAccountPrecondition( + info: AccountPreconditionInfo +): AccountPrecondition | undefined { + const match = resolveRuntimeCatalogMatch(info) + return preconditionForCatalogId(match?.catalogId) +} + +// Resolves the winning precondition when several could co-occur. Ordering +// follows sign-in -> subscription -> credits. +export function selectHighestPrecedencePrecondition( + preconditions: Iterable +): AccountPrecondition | undefined { + const present = new Set(preconditions) + return PRECONDITION_PRECEDENCE.find((candidate) => present.has(candidate)) +} diff --git a/src/scripts/app.ts b/src/scripts/app.ts index 2345b0871ae..5c369a4f499 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -60,6 +60,8 @@ import { ComponentWidgetImpl, DOMWidgetImpl } from '@/scripts/domWidget' +import { useAccountPreconditionDialog } from '@/platform/cloud/subscription/composables/useAccountPreconditionDialog' +import { resolveAccountPrecondition } from '@/platform/errorCatalog/accountPreconditionRouting' import { useDialogService } from '@/services/dialogService' import { useExtensionService } from '@/services/extensionService' import { useLitegraphService } from '@/services/litegraphService' @@ -770,20 +772,13 @@ export class ComfyApp { }) api.addEventListener('execution_error', ({ detail }) => { - // Check if this is an auth-related error or credits-related error - if ( - detail.exception_message?.includes( - 'Unauthorized: Please login first to use this node.' - ) - ) { - useDialogService().showApiNodesSignInDialog([detail.node_type]) - } else if ( - detail.exception_message?.includes( - 'Payment Required: Please add credits to your account to use this node.' - ) - ) { - useDialogService().showTopUpCreditsDialog({ - isInsufficientCredits: true + const precondition = resolveAccountPrecondition({ + exceptionType: detail.exception_type ?? '', + exceptionMessage: detail.exception_message ?? '' + }) + if (precondition) { + useAccountPreconditionDialog().open(precondition, { + nodeType: detail.node_type }) } else if (useSettingStore().get('Comfy.RightSidePanel.ShowErrorsTab')) { useExecutionErrorStore().showErrorOverlay() diff --git a/src/stores/executionStore.test.ts b/src/stores/executionStore.test.ts index 18b32fd2837..e8da1b0ba78 100644 --- a/src/stores/executionStore.test.ts +++ b/src/stores/executionStore.test.ts @@ -1229,6 +1229,75 @@ describe('useExecutionStore - WebSocket event handlers', () => { exception_message: 'CUDA OOM' }) }) + + it('keeps a subscription precondition (no node_id) out of the error panel and count', () => { + const errorStore = useExecutionErrorStore() + + fire('execution_error', { + prompt_id: 'job-1', + node_id: null, + exception_type: 'InactiveSubscriptionError', + exception_message: + 'User has no active subscription. Please subscribe to a plan to continue.', + traceback: [] + }) + + expect(errorStore.lastExecutionError).toBeNull() + expect(errorStore.lastPromptError).toBeNull() + expect(errorStore.lastNodeErrors).toBeNull() + expect(errorStore.totalErrorCount).toBe(0) + }) + + it('keeps a sign-in precondition out of the error panel and count', () => { + const errorStore = useExecutionErrorStore() + + fire('execution_error', { + prompt_id: 'job-1', + node_id: 'n1', + node_type: 'ApiNode', + exception_type: 'RuntimeError', + exception_message: 'Unauthorized: Please login first to use this node.', + traceback: [] + }) + + expect(errorStore.lastExecutionError).toBeNull() + expect(errorStore.lastPromptError).toBeNull() + expect(errorStore.totalErrorCount).toBe(0) + }) + + it('keeps a runtime credit precondition at a node out of the error panel and count', () => { + const errorStore = useExecutionErrorStore() + + fire('execution_error', { + prompt_id: 'job-1', + node_id: 'n1', + node_type: 'PartnerApiNode', + exception_type: 'InsufficientFundsError', + exception_message: + 'Payment Required: Please add credits to your account to use this node.', + traceback: [] + }) + + expect(errorStore.lastExecutionError).toBeNull() + expect(errorStore.lastPromptError).toBeNull() + expect(errorStore.totalErrorCount).toBe(0) + }) + + it('still routes an ordinary node runtime error to the error panel', () => { + const errorStore = useExecutionErrorStore() + + fire('execution_error', { + prompt_id: 'job-1', + node_id: 'n1', + node_type: 'KSampler', + exception_type: 'RuntimeError', + exception_message: 'Something unrelated broke', + traceback: [] + }) + + expect(errorStore.lastExecutionError).not.toBeNull() + expect(errorStore.totalErrorCount).toBe(1) + }) }) describe('notification', () => { diff --git a/src/stores/executionStore.ts b/src/stores/executionStore.ts index f19eb77b8e0..69cec40b922 100644 --- a/src/stores/executionStore.ts +++ b/src/stores/executionStore.ts @@ -3,6 +3,7 @@ import { computed, ref, shallowRef } from 'vue' import { useNodeProgressText } from '@/composables/node/useNodeProgressText' import { isCloud } from '@/platform/distribution/types' +import { resolveAccountPrecondition } from '@/platform/errorCatalog/accountPreconditionRouting' import { useTelemetry } from '@/platform/telemetry' import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore' import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' @@ -423,6 +424,10 @@ export const useExecutionStore = defineStore('execution', () => { if (handleCloudValidationError(e.detail)) return } + // Account preconditions (sign-in, subscription, credits) open their own + // modal and must stay out of the error panel and error count. + if (handleAccountPreconditionError(e.detail)) return + // Service-level errors (e.g. "Job has stagnated") have no associated node. // Route them as job errors if (handleServiceLevelError(e.detail)) return @@ -433,6 +438,20 @@ export const useExecutionStore = defineStore('execution', () => { resetExecutionState(e.detail.prompt_id) } + function handleAccountPreconditionError( + detail: ExecutionErrorWsMessage + ): boolean { + const precondition = resolveAccountPrecondition({ + exceptionType: detail.exception_type ?? '', + exceptionMessage: detail.exception_message ?? '' + }) + if (!precondition) return false + + clearInitializationByJobId(detail.prompt_id) + resetExecutionState(detail.prompt_id) + return true + } + function handleServiceLevelError(detail: ExecutionErrorWsMessage): boolean { const nodeId = detail.node_id if (nodeId !== null && nodeId !== undefined && String(nodeId) !== '') From dc6c48c6dd838872fa0bc29f9020257715364657 Mon Sep 17 00:00:00 2001 From: dante01yoon Date: Mon, 15 Jun 2026 12:36:23 +0900 Subject: [PATCH 2/3] fix(billing): route queue paywall (402) to modal, out of error panel (FE-878) The free-tier queue paywall arrives on the POST /prompt path as { type: PAYMENT_REQUIRED, message: "Subscription required to queue workflows" }, which the execution_error-only routing did not cover, so it still surfaced as a raw error in the panel. Teach the runtime matcher this message and short-circuit the queuePrompt catch to open the precondition modal, keeping it out of the error panel and error count. Add an e2e regression test for the queue paywall plus a control for ordinary queue errors. Fixes #12840 --- .../tests/subscriptionPaywallError.spec.ts | 68 +++++++++++++++++++ .../accountPreconditionRouting.test.ts | 9 +++ .../errorCatalog/runtimeErrorMatcher.ts | 3 +- src/scripts/app.ts | 18 +++++ 4 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 browser_tests/tests/subscriptionPaywallError.spec.ts diff --git a/browser_tests/tests/subscriptionPaywallError.spec.ts b/browser_tests/tests/subscriptionPaywallError.spec.ts new file mode 100644 index 00000000000..f900bb74894 --- /dev/null +++ b/browser_tests/tests/subscriptionPaywallError.spec.ts @@ -0,0 +1,68 @@ +import type { Page } from '@playwright/test' +import { expect } from '@playwright/test' + +import type { PromptResponse } from '@/schemas/apiSchema' + +import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage' +import { TestIds } from '@e2e/fixtures/selectors' + +// Regression for #12840: a free-tier paywall on queue (`POST /prompt` 402 with +// `{ error: { type: 'PAYMENT_REQUIRED', message: 'Subscription required to +// queue workflows' } }`) is an account precondition. It must open its own modal +// and stay out of the error panel, instead of surfacing the raw backend string +// with non-actionable Find-on-GitHub / Copy actions. +test.describe('Subscription paywall on queue', { tag: '@ui' }, () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.settings.setSetting( + 'Comfy.RightSidePanel.ShowErrorsTab', + true + ) + }) + + async function mockQueueError(page: Page, error: PromptResponse['error']) { + const body: PromptResponse = { node_errors: {}, error } + await page.route('**/api/prompt', async (route) => { + await route.fulfill({ + status: 402, + contentType: 'application/json', + body: JSON.stringify(body) + }) + }) + } + + test('keeps the subscription paywall out of the error panel', async ({ + comfyPage + }) => { + await mockQueueError(comfyPage.page, { + type: 'PAYMENT_REQUIRED', + message: 'Subscription required to queue workflows', + details: '' + }) + + const queued = comfyPage.page.waitForResponse('**/api/prompt') + await comfyPage.actionbar.queueButton.primaryButton.click() + await queued + + await expect( + comfyPage.page.getByTestId(TestIds.dialogs.errorOverlay) + ).toBeHidden() + }) + + test('still surfaces ordinary queue errors in the error panel', async ({ + comfyPage + }) => { + await mockQueueError(comfyPage.page, { + type: 'server_error', + message: 'The server exploded', + details: '' + }) + + const queued = comfyPage.page.waitForResponse('**/api/prompt') + await comfyPage.actionbar.queueButton.primaryButton.click() + await queued + + await expect( + comfyPage.page.getByTestId(TestIds.dialogs.errorOverlay) + ).toBeVisible() + }) +}) diff --git a/src/platform/errorCatalog/accountPreconditionRouting.test.ts b/src/platform/errorCatalog/accountPreconditionRouting.test.ts index cfe6390575c..be69340187d 100644 --- a/src/platform/errorCatalog/accountPreconditionRouting.test.ts +++ b/src/platform/errorCatalog/accountPreconditionRouting.test.ts @@ -35,6 +35,15 @@ describe('resolveAccountPrecondition', () => { ).toBe('subscription') }) + it('classifies the queue paywall (PAYMENT_REQUIRED) as a subscription precondition', () => { + expect( + resolveAccountPrecondition({ + exceptionType: 'PAYMENT_REQUIRED', + exceptionMessage: 'Subscription required to queue workflows' + }) + ).toBe('subscription') + }) + it('classifies a subscription-upgrade error as a subscription precondition', () => { expect( resolveAccountPrecondition({ diff --git a/src/platform/errorCatalog/runtimeErrorMatcher.ts b/src/platform/errorCatalog/runtimeErrorMatcher.ts index 76e13ef811e..773172045ad 100644 --- a/src/platform/errorCatalog/runtimeErrorMatcher.ts +++ b/src/platform/errorCatalog/runtimeErrorMatcher.ts @@ -37,7 +37,8 @@ const WORKSPACE_INSUFFICIENT_CREDITS_MESSAGES = new Set([ ]) const SUBSCRIPTION_REQUIRED_MESSAGES = new Set([ 'Workspace has no active subscription. Please subscribe to a plan to continue.', - 'User has no active subscription. Please subscribe to a plan to continue.' + 'User has no active subscription. Please subscribe to a plan to continue.', + 'Subscription required to queue workflows' ]) const SUBSCRIPTION_UPGRADE_REQUIRED_PREFIX = 'the following private models require a subscription upgrade:' diff --git a/src/scripts/app.ts b/src/scripts/app.ts index 5c369a4f499..41ae0e73181 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -1672,6 +1672,24 @@ export class ComfyApp { this.canvas.draw(true, true) } } catch (error: unknown) { + const preconditionResponseError = + error instanceof PromptExecutionError && + typeof error.response.error === 'object' + ? error.response.error + : undefined + const promptPrecondition = preconditionResponseError + ? resolveAccountPrecondition({ + exceptionType: preconditionResponseError.type, + exceptionMessage: preconditionResponseError.message + }) + : undefined + // Account preconditions (sign-in, subscription, credits) open their + // own modal and must stay out of the error panel and error count. + if (promptPrecondition) { + useAccountPreconditionDialog().open(promptPrecondition) + console.error(error) + break + } if ( error instanceof PromptExecutionError && typeof error.response.error === 'object' && From 75131c2614bbf0598a80b7873000da8f536ba9f3 Mon Sep 17 00:00:00 2001 From: dante01yoon Date: Wed, 17 Jun 2026 11:25:47 +0900 Subject: [PATCH 3/3] refactor(billing): drop unused precedence helper, reuse RuntimeErrorInfo (FE-878) --- .../accountPreconditionRouting.test.ts | 29 +------------------ .../accountPreconditionRouting.ts | 25 ++-------------- .../errorCatalog/runtimeErrorMatcher.ts | 2 +- 3 files changed, 4 insertions(+), 52 deletions(-) diff --git a/src/platform/errorCatalog/accountPreconditionRouting.test.ts b/src/platform/errorCatalog/accountPreconditionRouting.test.ts index be69340187d..3082f902861 100644 --- a/src/platform/errorCatalog/accountPreconditionRouting.test.ts +++ b/src/platform/errorCatalog/accountPreconditionRouting.test.ts @@ -3,8 +3,7 @@ import { describe, expect, it } from 'vitest' import { isAccountPreconditionCatalogId, preconditionForCatalogId, - resolveAccountPrecondition, - selectHighestPrecedencePrecondition + resolveAccountPrecondition } from './accountPreconditionRouting' import { EXECUTION_FAILED_CATALOG_ID, @@ -113,29 +112,3 @@ describe('preconditionForCatalogId / isAccountPreconditionCatalogId', () => { expect(isAccountPreconditionCatalogId(undefined)).toBe(false) }) }) - -describe('selectHighestPrecedencePrecondition', () => { - it('prefers sign-in over subscription and credits', () => { - expect( - selectHighestPrecedencePrecondition([ - 'credits', - 'subscription', - 'sign_in' - ]) - ).toBe('sign_in') - }) - - it('prefers subscription over credits', () => { - expect( - selectHighestPrecedencePrecondition(['credits', 'subscription']) - ).toBe('subscription') - }) - - it('returns the only precondition present', () => { - expect(selectHighestPrecedencePrecondition(['credits'])).toBe('credits') - }) - - it('returns undefined when none are present', () => { - expect(selectHighestPrecedencePrecondition([])).toBeUndefined() - }) -}) diff --git a/src/platform/errorCatalog/accountPreconditionRouting.ts b/src/platform/errorCatalog/accountPreconditionRouting.ts index f38773b3af6..a3f6338268d 100644 --- a/src/platform/errorCatalog/accountPreconditionRouting.ts +++ b/src/platform/errorCatalog/accountPreconditionRouting.ts @@ -5,6 +5,7 @@ import { SUBSCRIPTION_UPGRADE_REQUIRED_CATALOG_ID, WORKSPACE_INSUFFICIENT_CREDITS_CATALOG_ID } from './catalogIds' +import type { RuntimeErrorInfo } from './runtimeErrorMatcher' import { resolveRuntimeCatalogMatch } from './runtimeErrorMatcher' // Account preconditions are gating states (sign-in, subscription, credits) that @@ -12,19 +13,6 @@ import { resolveRuntimeCatalogMatch } from './runtimeErrorMatcher' // excluded from the error panel and the error count. export type AccountPrecondition = 'sign_in' | 'subscription' | 'credits' -interface AccountPreconditionInfo { - exceptionType: string - exceptionMessage: string -} - -// Lower index wins. Sign-in must resolve before a subscription prompt, and any -// payment precondition takes precedence over a credits top-up. -const PRECONDITION_PRECEDENCE: AccountPrecondition[] = [ - 'sign_in', - 'subscription', - 'credits' -] - const CATALOG_ID_TO_PRECONDITION = new Map([ [SIGN_IN_REQUIRED_CATALOG_ID, 'sign_in'], [SUBSCRIPTION_REQUIRED_CATALOG_ID, 'subscription'], @@ -49,17 +37,8 @@ export function isAccountPreconditionCatalogId( // Classifies a single runtime error payload into the account precondition it // represents, or `undefined` when it is an ordinary workflow error. export function resolveAccountPrecondition( - info: AccountPreconditionInfo + info: RuntimeErrorInfo ): AccountPrecondition | undefined { const match = resolveRuntimeCatalogMatch(info) return preconditionForCatalogId(match?.catalogId) } - -// Resolves the winning precondition when several could co-occur. Ordering -// follows sign-in -> subscription -> credits. -export function selectHighestPrecedencePrecondition( - preconditions: Iterable -): AccountPrecondition | undefined { - const present = new Set(preconditions) - return PRECONDITION_PRECEDENCE.find((candidate) => present.has(candidate)) -} diff --git a/src/platform/errorCatalog/runtimeErrorMatcher.ts b/src/platform/errorCatalog/runtimeErrorMatcher.ts index 773172045ad..933b3751236 100644 --- a/src/platform/errorCatalog/runtimeErrorMatcher.ts +++ b/src/platform/errorCatalog/runtimeErrorMatcher.ts @@ -121,7 +121,7 @@ const PREPROCESSING_FAILED_PREFIXES = [ 'Failed to complete preparation:' ] -interface RuntimeErrorInfo { +export interface RuntimeErrorInfo { exceptionType: string exceptionMessage: string }