From a137ede7df6a3a0a72a1157549cc741cb7a4ec71 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 27 Jul 2026 22:02:50 +0200 Subject: [PATCH 1/2] Clear provider update action while updating - Add regression coverage for the loading toast state - Remove the stale Update action during provider updates --- ...ProviderUpdatePrimaryNotification.test.tsx | 134 ++++++++++++++++++ .../ProviderUpdatePrimaryNotification.tsx | 4 + 2 files changed, 138 insertions(+) create mode 100644 apps/web/src/components/ProviderUpdatePrimaryNotification.test.tsx diff --git a/apps/web/src/components/ProviderUpdatePrimaryNotification.test.tsx b/apps/web/src/components/ProviderUpdatePrimaryNotification.test.tsx new file mode 100644 index 00000000000..61000470e7c --- /dev/null +++ b/apps/web/src/components/ProviderUpdatePrimaryNotification.test.tsx @@ -0,0 +1,134 @@ +import type { ComponentPropsWithoutRef } from "react"; +import { beforeEach, describe, expect, it, vi } from "vite-plus/test"; +import { + type EnvironmentId, + ProviderDriverKind, + ProviderInstanceId, + type ServerProvider, +} from "@t3tools/contracts"; + +const testState = vi.hoisted(() => ({ + addToast: vi.fn(), + closeToast: vi.fn(), + dismissNotificationKey: vi.fn(), + navigate: vi.fn(), + providers: [] as ServerProvider[], + updateProvider: vi.fn(), + updateToast: vi.fn(), +})); + +vi.mock("react", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useCallback: (callback: T): T => callback, + useEffect: (effect: () => void | (() => void)) => { + effect(); + }, + useMemo: (factory: () => T): T => factory(), + useRef: (initialValue: T) => ({ current: initialValue }), + }; +}); + +vi.mock("react/compiler-runtime", () => ({ + c: (size: number) => Array.from({ length: size }, () => Symbol.for("react.memo_cache_sentinel")), +})); + +vi.mock("@tanstack/react-router", () => ({ + useNavigate: () => testState.navigate, +})); + +vi.mock("@effect/atom-react", () => ({ + useAtomValue: () => testState.providers, +})); + +vi.mock("../state/server", () => ({ + primaryServerProvidersAtom: Symbol("primaryServerProvidersAtom"), + serverEnvironment: { updateProvider: Symbol("updateProvider") }, +})); + +vi.mock("../state/environments", () => ({ + usePrimaryEnvironment: () => ({ + environmentId: "env-primary" as EnvironmentId, + }), +})); + +vi.mock("../providerUpdateDismissal", () => ({ + useDismissedProviderUpdateNotificationKeys: () => ({ + dismissedNotificationKeys: new Set(), + dismissNotificationKey: testState.dismissNotificationKey, + }), +})); + +vi.mock("./ui/toast", () => ({ + stackedThreadToast: (options: T): T => options, + toastManager: { + add: testState.addToast, + close: testState.closeToast, + update: testState.updateToast, + }, +})); + +vi.mock("../state/use-atom-command", () => ({ + useAtomCommand: () => testState.updateProvider, +})); + +import { ProviderUpdatePrimaryNotification } from "./ProviderUpdatePrimaryNotification"; + +function updateCandidate(): ServerProvider { + return { + instanceId: ProviderInstanceId.make("codex"), + driver: ProviderDriverKind.make("codex"), + enabled: true, + installed: true, + version: "1.0.0", + status: "ready", + auth: { status: "authenticated" }, + checkedAt: "2026-07-27T12:00:00.000Z", + models: [], + slashCommands: [], + skills: [], + versionAdvisory: { + status: "behind_latest", + currentVersion: "1.0.0", + latestVersion: "1.1.0", + updateCommand: "npm install -g @openai/codex@latest", + canUpdate: true, + checkedAt: "2026-07-27T12:00:00.000Z", + message: "Update available.", + }, + }; +} + +describe("ProviderUpdatePrimaryNotification", () => { + beforeEach(() => { + testState.addToast.mockReset().mockReturnValue("provider-update-toast"); + testState.closeToast.mockReset(); + testState.dismissNotificationKey.mockReset(); + testState.navigate.mockReset(); + testState.providers = [updateCandidate()]; + testState.updateProvider.mockReset().mockReturnValue(new Promise(() => {})); + testState.updateToast.mockReset(); + }); + + it("removes the prompt action while the provider update is running", () => { + ProviderUpdatePrimaryNotification(); + + const prompt = testState.addToast.mock.calls[0]?.[0] as { + readonly actionProps?: ComponentPropsWithoutRef<"button">; + }; + expect(prompt.actionProps?.children).toBe("Update"); + + prompt.actionProps?.onClick?.({} as never); + + expect(testState.updateProvider).toHaveBeenCalledOnce(); + expect(testState.updateToast).toHaveBeenCalledWith( + "provider-update-toast", + expect.objectContaining({ + type: "loading", + title: "Updating provider", + actionProps: undefined, + }), + ); + }); +}); diff --git a/apps/web/src/components/ProviderUpdatePrimaryNotification.tsx b/apps/web/src/components/ProviderUpdatePrimaryNotification.tsx index a7f7a2c1e69..64399679acf 100644 --- a/apps/web/src/components/ProviderUpdatePrimaryNotification.tsx +++ b/apps/web/src/components/ProviderUpdatePrimaryNotification.tsx @@ -68,6 +68,10 @@ function updateProviderUpdateToast(input: { title: input.view.title, description: input.view.description, timeout: 0, + // Base UI merges toast updates with the existing toast. Explicitly clear + // the prompt action so its guarded Update handler cannot linger as a + // visible no-op while the update is running (or after it succeeds). + actionProps: undefined, data: { hideCopyButton: true, ...(input.view.dismissAfterVisibleMs !== undefined From 7fef9ff35a75667eadb3acf86b118b61096a681a Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 27 Jul 2026 22:03:43 +0200 Subject: [PATCH 2/2] Delete apps/web/src/components/ProviderUpdatePrimaryNotification.test.tsx --- ...ProviderUpdatePrimaryNotification.test.tsx | 134 ------------------ 1 file changed, 134 deletions(-) delete mode 100644 apps/web/src/components/ProviderUpdatePrimaryNotification.test.tsx diff --git a/apps/web/src/components/ProviderUpdatePrimaryNotification.test.tsx b/apps/web/src/components/ProviderUpdatePrimaryNotification.test.tsx deleted file mode 100644 index 61000470e7c..00000000000 --- a/apps/web/src/components/ProviderUpdatePrimaryNotification.test.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import type { ComponentPropsWithoutRef } from "react"; -import { beforeEach, describe, expect, it, vi } from "vite-plus/test"; -import { - type EnvironmentId, - ProviderDriverKind, - ProviderInstanceId, - type ServerProvider, -} from "@t3tools/contracts"; - -const testState = vi.hoisted(() => ({ - addToast: vi.fn(), - closeToast: vi.fn(), - dismissNotificationKey: vi.fn(), - navigate: vi.fn(), - providers: [] as ServerProvider[], - updateProvider: vi.fn(), - updateToast: vi.fn(), -})); - -vi.mock("react", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - useCallback: (callback: T): T => callback, - useEffect: (effect: () => void | (() => void)) => { - effect(); - }, - useMemo: (factory: () => T): T => factory(), - useRef: (initialValue: T) => ({ current: initialValue }), - }; -}); - -vi.mock("react/compiler-runtime", () => ({ - c: (size: number) => Array.from({ length: size }, () => Symbol.for("react.memo_cache_sentinel")), -})); - -vi.mock("@tanstack/react-router", () => ({ - useNavigate: () => testState.navigate, -})); - -vi.mock("@effect/atom-react", () => ({ - useAtomValue: () => testState.providers, -})); - -vi.mock("../state/server", () => ({ - primaryServerProvidersAtom: Symbol("primaryServerProvidersAtom"), - serverEnvironment: { updateProvider: Symbol("updateProvider") }, -})); - -vi.mock("../state/environments", () => ({ - usePrimaryEnvironment: () => ({ - environmentId: "env-primary" as EnvironmentId, - }), -})); - -vi.mock("../providerUpdateDismissal", () => ({ - useDismissedProviderUpdateNotificationKeys: () => ({ - dismissedNotificationKeys: new Set(), - dismissNotificationKey: testState.dismissNotificationKey, - }), -})); - -vi.mock("./ui/toast", () => ({ - stackedThreadToast: (options: T): T => options, - toastManager: { - add: testState.addToast, - close: testState.closeToast, - update: testState.updateToast, - }, -})); - -vi.mock("../state/use-atom-command", () => ({ - useAtomCommand: () => testState.updateProvider, -})); - -import { ProviderUpdatePrimaryNotification } from "./ProviderUpdatePrimaryNotification"; - -function updateCandidate(): ServerProvider { - return { - instanceId: ProviderInstanceId.make("codex"), - driver: ProviderDriverKind.make("codex"), - enabled: true, - installed: true, - version: "1.0.0", - status: "ready", - auth: { status: "authenticated" }, - checkedAt: "2026-07-27T12:00:00.000Z", - models: [], - slashCommands: [], - skills: [], - versionAdvisory: { - status: "behind_latest", - currentVersion: "1.0.0", - latestVersion: "1.1.0", - updateCommand: "npm install -g @openai/codex@latest", - canUpdate: true, - checkedAt: "2026-07-27T12:00:00.000Z", - message: "Update available.", - }, - }; -} - -describe("ProviderUpdatePrimaryNotification", () => { - beforeEach(() => { - testState.addToast.mockReset().mockReturnValue("provider-update-toast"); - testState.closeToast.mockReset(); - testState.dismissNotificationKey.mockReset(); - testState.navigate.mockReset(); - testState.providers = [updateCandidate()]; - testState.updateProvider.mockReset().mockReturnValue(new Promise(() => {})); - testState.updateToast.mockReset(); - }); - - it("removes the prompt action while the provider update is running", () => { - ProviderUpdatePrimaryNotification(); - - const prompt = testState.addToast.mock.calls[0]?.[0] as { - readonly actionProps?: ComponentPropsWithoutRef<"button">; - }; - expect(prompt.actionProps?.children).toBe("Update"); - - prompt.actionProps?.onClick?.({} as never); - - expect(testState.updateProvider).toHaveBeenCalledOnce(); - expect(testState.updateToast).toHaveBeenCalledWith( - "provider-update-toast", - expect.objectContaining({ - type: "loading", - title: "Updating provider", - actionProps: undefined, - }), - ); - }); -});