From a727d1d97690c9bb12cee5760e91cfd1aa7c017d Mon Sep 17 00:00:00 2001 From: "t3-code[bot]" <269035359+t3-code[bot]@users.noreply.github.com> Date: Sun, 27 Sep 2026 02:48:53 -0500 Subject: [PATCH 1/2] test(relay): remove constant-restating database mode test (#13932) --- infra/relay/src/dbConfig.test.ts | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 infra/relay/src/dbConfig.test.ts diff --git a/infra/relay/src/dbConfig.test.ts b/infra/relay/src/dbConfig.test.ts deleted file mode 100644 index e97b29a742de..000000000000 --- a/infra/relay/src/dbConfig.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { describe, expect, it } from "vite-plus/test"; - -import { relayDatabaseMode } from "./dbConfig.ts"; - -describe("relayDatabaseMode", () => { - it("uses the shared database only for production", () => { - expect(relayDatabaseMode("prod")).toBe("shared-database"); - expect(relayDatabaseMode("dev_julius")).toBe("stage-branch"); - expect(relayDatabaseMode("preview")).toBe("stage-branch"); - }); -}); From de251fc2971a884cb5b1305ba4daf309dc8cccb0 Mon Sep 17 00:00:00 2001 From: "t3-code[bot]" <269035359+t3-code[bot]@users.noreply.github.com> Date: Sun, 27 Sep 2026 06:54:56 -0300 Subject: [PATCH 2/2] fix(web): continue onboarding after incomplete history imports (#13935) Co-authored-by: t3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com> Co-authored-by: Exotic <118054752+extoci@users.noreply.github.com> Co-authored-by: Utkarsh Patil <73941998+UtkarshUsername@users.noreply.github.com> --- .../onboarding/WelcomeWizard.test.tsx | 209 ++++++++++++++++++ .../components/onboarding/WelcomeWizard.tsx | 68 +++--- apps/web/src/routes/welcome.tsx | 10 +- 3 files changed, 253 insertions(+), 34 deletions(-) create mode 100644 apps/web/src/components/onboarding/WelcomeWizard.test.tsx diff --git a/apps/web/src/components/onboarding/WelcomeWizard.test.tsx b/apps/web/src/components/onboarding/WelcomeWizard.test.tsx new file mode 100644 index 000000000000..92514d012b82 --- /dev/null +++ b/apps/web/src/components/onboarding/WelcomeWizard.test.tsx @@ -0,0 +1,209 @@ +// @vitest-environment jsdom +import { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { EnvironmentId, ProjectId } from "@t3tools/contracts"; +import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test"; + +const mocks = vi.hoisted(() => ({ + importThreads: vi.fn(), + createProject: vi.fn(), + complete: vi.fn(), + refresh: vi.fn(), + toast: vi.fn(), + projects: [] as Array<{ id: string; environmentId: string; workspaceRoot: string }>, +})); +vi.mock("../../state/agentSessions", () => ({ agentSessionImport: "import" })); +vi.mock("../../state/projects", () => ({ projectEnvironment: { create: "create" } })); +vi.mock("../../state/use-atom-command", () => ({ + useAtomCommand: (command: string) => + command === "import" + ? mocks.importThreads + : command === "create" + ? mocks.createProject + : mocks.refresh, +})); +vi.mock("../../onboarding/firstRun", () => ({ useCompleteOnboarding: () => mocks.complete })); +vi.mock("../../state/entities", () => ({ + useProjects: () => mocks.projects, + readProjects: () => mocks.projects, +})); +vi.mock("../../state/environments", () => { + const environment = { + environmentId: "test-env", + label: "Computer", + connection: { phase: "connected" }, + }; + return { + useEnvironments: () => ({ environments: [environment] }), + usePrimaryEnvironment: () => environment, + }; +}); +vi.mock("../../state/server", () => ({ + serverEnvironment: { + providersValueAtom: () => [], + configValueAtom: () => null, + refreshProviders: "refresh", + }, +})); +vi.mock("@effect/atom-react", () => ({ useAtomValue: (value: unknown) => value })); +vi.mock("../../onboarding/useProjectScans", () => ({ + useProjectScans: () => [ + { + environmentId: "test-env", + isPending: false, + error: null, + refresh: mocks.refresh, + data: { + truncated: false, + candidates: [ + { + path: "/project", + title: "project", + projectId: "test-project", + threadCount: 29, + lastActiveAt: new Date().toISOString(), + sources: ["codex"], + }, + ], + }, + }, + ], +})); +vi.mock("../../connection/onboarding", () => ({ connectPairing: vi.fn() })); +vi.mock("../../state/terminal", () => ({ terminalEnvironment: {} })); +vi.mock("../clerk/useT3ConnectAuthPrompt", () => ({ useT3ConnectAuthPrompt: vi.fn() })); +vi.mock("../../cloud/publicConfig", () => ({ hasCloudPublicConfig: () => false })); +vi.mock("../ThreadTerminalDrawer", () => ({ TerminalViewport: () => null })); +vi.mock("../cloud/CloudEnvironmentConnectList", () => ({ + CloudEnvironmentConnectRows: () => null, +})); +vi.mock("../ui/toast", () => ({ + toastManager: { add: mocks.toast, close: vi.fn(), update: vi.fn() }, +})); + +import { WelcomeWizard } from "./WelcomeWizard"; + +let root: Root; +let container: HTMLDivElement; + +beforeEach(() => { + vi.clearAllMocks(); + vi.stubGlobal( + "ResizeObserver", + class { + observe() {} + unobserve() {} + disconnect() {} + }, + ); + Object.defineProperty(Element.prototype, "getAnimations", { + configurable: true, + value: () => [], + }); + mocks.projects = [{ id: "test-project", environmentId: "test-env", workspaceRoot: "/project" }]; + mocks.complete.mockResolvedValue(undefined); + mocks.refresh.mockResolvedValue(undefined); + mocks.importThreads.mockResolvedValue({ + _tag: "Success", + value: { importedCount: 28, skippedCount: 1 }, + }); + vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); + container = document.createElement("div"); + document.body.append(container); + root = createRoot(container); +}); +afterEach(async () => { + await act(async () => root.unmount()); + container.remove(); +}); + +async function click(label: string) { + const button = [...document.querySelectorAll("button")].find( + (element) => element.textContent?.trim() === label, + ); + expect(button, `button ${label}`).toBeDefined(); + await act(async () => button!.click()); +} + +it("enters the workspace after a partial import and warns after navigation finishes", async () => { + let finishNavigation = () => {}; + const navigation = new Promise((resolve) => { + finishNavigation = resolve; + }); + const onDone = vi.fn(() => navigation); + await act(async () => root.render()); + await click("Continue"); + await click("Continue"); + await click("Import 1 project"); + expect(onDone).toHaveBeenCalledWith({ + environmentId: EnvironmentId.make("test-env"), + projectId: ProjectId.make("test-project"), + }); + expect(mocks.toast).not.toHaveBeenCalled(); + await act(async () => finishNavigation()); + expect(mocks.toast).toHaveBeenCalledWith( + expect.objectContaining({ + type: "warning", + description: "Imported 28 threads. 1 thread could not be imported.", + }), + ); + expect(mocks.toast.mock.invocationCallOrder[0]).toBeGreaterThan( + onDone.mock.invocationCallOrder[0]!, + ); +}); + +it.each([ + [0, 0, null], + [29, 0, null], + [1, 0, null], + [0, 1, "1 thread could not be imported."], + [0, 2, "2 threads could not be imported."], +] as const)( + "finishes setup with %i imported and %i skipped threads", + async (importedCount, skippedCount, warning) => { + mocks.importThreads.mockResolvedValue({ + _tag: "Success", + value: { importedCount, skippedCount }, + }); + const onDone = vi.fn(); + await act(async () => root.render()); + await click("Continue"); + await click("Continue"); + await click("Import 1 project"); + expect(onDone).toHaveBeenCalledOnce(); + if (warning === null && importedCount > 0) { + expect(mocks.toast).toHaveBeenCalledWith({ + type: "success", + title: `Imported ${importedCount} ${importedCount === 1 ? "thread" : "threads"}`, + }); + } else if (warning === null) { + expect(mocks.toast).not.toHaveBeenCalled(); + } else { + expect(mocks.toast).toHaveBeenCalledWith( + expect.objectContaining({ type: "warning", description: warning }), + ); + } + }, +); + +it("keeps setup open when saving completion fails and preserves the import warning on retry", async () => { + mocks.complete.mockRejectedValueOnce(new Error("settings unavailable")); + const onDone = vi.fn(); + await act(async () => root.render()); + await click("Continue"); + await click("Continue"); + await click("Import 1 project"); + expect(onDone).not.toHaveBeenCalled(); + expect(mocks.toast).toHaveBeenCalledWith( + expect.objectContaining({ type: "error", title: "Could not finish setup" }), + ); + await click("Do not import projects"); + expect(onDone).toHaveBeenCalledOnce(); + expect(mocks.importThreads).toHaveBeenCalledOnce(); + expect(mocks.toast).toHaveBeenLastCalledWith( + expect.objectContaining({ + type: "warning", + description: "Imported 28 threads. 1 thread could not be imported.", + }), + ); +}); diff --git a/apps/web/src/components/onboarding/WelcomeWizard.tsx b/apps/web/src/components/onboarding/WelcomeWizard.tsx index 66e99359abae..942ffcd57ef6 100644 --- a/apps/web/src/components/onboarding/WelcomeWizard.tsx +++ b/apps/web/src/components/onboarding/WelcomeWizard.tsx @@ -99,7 +99,7 @@ export function WelcomeWizard({ }: { /** Whether this client is authenticated to the server serving the app. */ readonly localAvailable: boolean; - readonly onDone: (projectRef?: ScopedProjectRef) => void; + readonly onDone: (projectRef?: ScopedProjectRef) => void | Promise; }) { const completeOnboarding = useCompleteOnboarding(); const [step, setStep] = useState("connection"); @@ -141,7 +141,7 @@ export function WelcomeWizard({ }; const stageIndex = step === "agents" ? 1 : step === "import" ? 2 : 0; const finish = useCallback( - (projectRef?: ScopedProjectRef) => { + (projectRef?: ScopedProjectRef, importWarning?: string, importedThreadCount = 0) => { if (finishingPromiseRef.current !== null) return finishingPromiseRef.current; if (completionErrorToastIdRef.current !== null) { toastManager.close(completionErrorToastIdRef.current); @@ -149,12 +149,25 @@ export function WelcomeWizard({ } const completion = completeOnboarding() - .then(() => { + .then(async () => { if (completionErrorToastIdRef.current !== null) { toastManager.close(completionErrorToastIdRef.current); completionErrorToastIdRef.current = null; } - onDone(projectRef); + await onDone(projectRef); + if (importWarning) { + toastManager.add({ + type: "warning", + title: "Some history was not imported", + description: importWarning, + timeout: 0, + }); + } else if (importedThreadCount > 0) { + toastManager.add({ + type: "success", + title: `Imported ${importedThreadCount} ${importedThreadCount === 1 ? "thread" : "threads"}`, + }); + } return true; }) .catch(() => { @@ -952,14 +965,19 @@ function ImportStep({ readonly scans: ReturnType; readonly isImporting: boolean; readonly setIsImporting: (value: boolean) => void; - readonly onDone: (projectRef?: ScopedProjectRef) => Promise; + readonly onDone: ( + projectRef?: ScopedProjectRef, + importWarning?: string, + importedThreadCount?: number, + ) => Promise; }) { const { environments } = useEnvironments(); const createProject = useAtomCommand(projectEnvironment.create, { reportFailure: false }); const importThreads = useAtomCommand(agentSessionImport, { reportFailure: false }); const projects = useProjects(); const [selectedPaths, setSelectedPaths] = useState | null>(null); - const [importError, setImportError] = useState(""); + const importWarningRef = useRef(""); + const importedThreadCountRef = useRef(0); const [landingProject, setLandingProject] = useState(null); // Keep project creation attempts separate from completed history imports so both can retry. const importedProjectsRef = useRef(new Map()); @@ -988,9 +1006,11 @@ function ImportStep({ ) ) { setLandingProject(null); - void onDone(landingProject).then((completed) => { - if (!completed) setIsImporting(false); - }); + void onDone(landingProject, importWarningRef.current, importedThreadCountRef.current).then( + (completed) => { + if (!completed) setIsImporting(false); + }, + ); } }, [landingProject, onDone, projects, setIsImporting]); @@ -1020,7 +1040,7 @@ function ImportStep({ importedProjectsRef.current, ); if (projectRef === undefined) { - void onDone(); + void onDone(undefined, importWarningRef.current, importedThreadCountRef.current); return; } setIsImporting(true); @@ -1034,7 +1054,8 @@ function ImportStep({ return; } setIsImporting(true); - setImportError(""); + importWarningRef.current = ""; + importedThreadCountRef.current = 0; lastImportSelectionRef.current = selection.map((candidate) => candidate.key); const importGeneration = importGenerationRef.current; const importedProjects = importedProjectsRef.current; @@ -1130,23 +1151,17 @@ function ImportStep({ if (refreshEnvironments.has(scan.environmentId)) scan.refresh(); } setIsImporting(false); + importedThreadCountRef.current = importedThreadCount; if (importedProjectsCount < selection.length) { if (importedThreadCount > 0 && skippedThreadCount > 0) { - setImportError( - `Imported ${importedThreadCount} ${importedThreadCount === 1 ? "thread" : "threads"}. ${skippedThreadCount} ${skippedThreadCount === 1 ? "thread" : "threads"} could not be imported.`, - ); + importWarningRef.current = `Imported ${importedThreadCount} ${importedThreadCount === 1 ? "thread" : "threads"}. ${skippedThreadCount} ${skippedThreadCount === 1 ? "thread" : "threads"} could not be imported.`; } else if (skippedThreadCount > 0) { - setImportError( - `${skippedThreadCount} ${skippedThreadCount === 1 ? "thread could" : "threads could"} not be imported.`, - ); + importWarningRef.current = `${skippedThreadCount} ${skippedThreadCount === 1 ? "thread could" : "threads could"} not be imported.`; } else if (importedThreadCount > 0) { - setImportError( - `Imported ${importedThreadCount} ${importedThreadCount === 1 ? "thread" : "threads"}. Some thread history could not be imported.`, - ); + importWarningRef.current = `Imported ${importedThreadCount} ${importedThreadCount === 1 ? "thread" : "threads"}. Some thread history could not be imported.`; } else { - setImportError("Could not import thread history."); + importWarningRef.current = "Could not import thread history."; } - return; } finishAfterImport(); }; @@ -1253,14 +1268,9 @@ function ImportStep({ })} - {importError ?

{importError}

: null}
-