| ... | @@ -0,0 +1,154 @@ |
| 1 | import { describe, expect, it, vi } from "vite-plus/test"; |
| 2 | |
| 3 | const testState = vi.hoisted(() => { |
| 4 | let completeProjectFileRead: (value: null) => void = () => undefined; |
| 5 | let projectFileRead = Promise.resolve<null>(null); |
| 6 | let storedDraft: { |
| 7 | readonly draftId: string; |
| 8 | readonly environmentId: string; |
| 9 | readonly promotedTo: null; |
| 10 | readonly threadId: string; |
| 11 | } | null = null; |
| 12 | const router = { |
| 13 | state: { |
| 14 | location: { href: "/" }, |
| 15 | matches: [{ params: {} }], |
| 16 | }, |
| 17 | navigate: vi.fn(async (request: { readonly params: { readonly draftId: string } }) => { |
| 18 | router.state.location.href = `/draft/${request.params.draftId}`; |
| 19 | }), |
| 20 | }; |
| 21 | const draftStore = { |
| 22 | getComposerDraft: vi.fn(() => ({})), |
| 23 | getDraftSessionByLogicalProjectKey: vi.fn(() => storedDraft), |
| 24 | getDraftSession: vi.fn(() => null), |
| 25 | getDraftThread: vi.fn(() => null), |
| 26 | applyStickyState: vi.fn(), |
| 27 | setDraftThreadContext: vi.fn(), |
| 28 | setLogicalProjectDraftThreadId: vi.fn(), |
| 29 | setModelSelection: vi.fn(), |
| 30 | }; |
| 31 | |
| 32 | return { |
| 33 | completeProjectFileRead: (value: null) => completeProjectFileRead(value), |
| 34 | draftStore, |
| 35 | get projectFileRead() { |
| 36 | return projectFileRead; |
| 37 | }, |
| 38 | reset(nextStoredDraft: typeof storedDraft) { |
| 39 | storedDraft = nextStoredDraft; |
| 40 | router.state.location.href = "/"; |
| 41 | router.navigate.mockClear(); |
| 42 | draftStore.setLogicalProjectDraftThreadId.mockClear(); |
| 43 | projectFileRead = new Promise<null>((resolve) => { |
| 44 | completeProjectFileRead = resolve; |
| 45 | }); |
| 46 | }, |
| 47 | router, |
| 48 | }; |
| 49 | }); |
| 50 | |
| 51 | vi.mock("@effect/atom-react", () => ({ |
| 52 | useAtomValue: () => ({ defaultThreadEnvMode: "local", newWorktreesStartFromOrigin: false }), |
| 53 | })); |
| 54 | vi.mock("@t3tools/client-runtime/environment", () => ({ |
| 55 | scopedProjectKey: () => "remote-project", |
| 56 | scopeProjectRef: (environmentId: string, projectId: string) => ({ environmentId, projectId }), |
| 57 | scopeThreadRef: (environmentId: string, threadId: string) => ({ environmentId, threadId }), |
| 58 | })); |
| 59 | vi.mock("@t3tools/contracts", () => ({ DEFAULT_RUNTIME_MODE: "default" })); |
| 60 | vi.mock("@t3tools/shared/threadEnvMode", () => ({ |
| 61 | resolveDefaultThreadEnvMode: (input: { |
| 62 | readonly projectFile: "local" | "worktree" | null; |
| 63 | readonly globalDefault: "local" | "worktree"; |
| 64 | }) => input.projectFile ?? input.globalDefault, |
| 65 | })); |
| 66 | vi.mock("@tanstack/react-router", () => ({ |
| 67 | useParams: () => null, |
| 68 | useRouter: () => testState.router, |
| 69 | })); |
| 70 | vi.mock("react", () => ({ |
| 71 | useCallback: <T>(callback: T) => callback, |
| 72 | useMemo: <T>(factory: () => T) => factory(), |
| 73 | })); |
| 74 | vi.mock("../components/Sidebar.logic", () => ({ orderItemsByPreferredIds: () => [] })); |
| 75 | vi.mock("../composerDraftStore", () => { |
| 76 | const useComposerDraftStore = Object.assign(() => null, { |
| 77 | getState: () => testState.draftStore, |
| 78 | }); |
| 79 | return { |
| 80 | composerDraftHasUserContent: () => false, |
| 81 | markPromotedDraftThreadByRef: vi.fn(), |
| 82 | useComposerDraftStore, |
| 83 | }; |
| 84 | }); |
| 85 | vi.mock("../lib/chatThreadActions", () => ({ |
| 86 | hasExplicitComposerModelSelection: () => false, |
| 87 | resolveNewDraftStartFromOrigin: () => false, |
| 88 | resolveNewThreadModelSelectionOverride: () => null, |
| 89 | })); |
| 90 | vi.mock("../lib/t3ProjectFileDefaults", () => ({ |
| 91 | readT3ProjectFileDefaultThreadEnvMode: () => testState.projectFileRead, |
| 92 | })); |
| 93 | vi.mock("../lib/utils", () => ({ |
| 94 | newDraftId: () => "draft-delayed", |
| 95 | newThreadId: () => "thread-delayed", |
| 96 | })); |
| 97 | vi.mock("../logicalProject", () => ({ |
| 98 | deriveLogicalProjectKeyFromSettings: () => "remote-project", |
| 99 | getProjectOrderKey: () => "remote-project", |
| 100 | selectProjectGroupingSettings: () => ({}), |
| 101 | })); |
| 102 | vi.mock("../state/entities", () => ({ |
| 103 | readProjects: () => [ |
| 104 | { |
| 105 | id: "project-remote", |
| 106 | environmentId: "environment-ssh", |
| 107 | workspaceRoot: "/remote/project", |
| 108 | defaultThreadEnvMode: null, |
| 109 | defaultModelSelection: null, |
| 110 | }, |
| 111 | ], |
| 112 | readThreadShell: () => null, |
| 113 | useProjects: () => [], |
| 114 | useThread: () => null, |
| 115 | })); |
| 116 | vi.mock("../state/server", () => ({ primaryServerSettingsAtom: {} })); |
| 117 | vi.mock("../threadRoutes", () => ({ resolveThreadRouteTarget: () => null })); |
| 118 | vi.mock("../uiStateStore", () => ({ |
| 119 | legacyProjectCwdPreferenceKey: () => "remote-project", |
| 120 | useUiStateStore: () => [], |
| 121 | })); |
| 122 | vi.mock("./useSettings", () => ({ useClientSettings: () => ({}) })); |
| 123 | |
| 124 | import { useNewThreadHandler } from "./useHandleNewThread"; |
| 125 | |
| 126 | describe("useNewThreadHandler", () => { |
| 127 | it.each([ |
| 128 | ["new", null], |
| 129 | [ |
| 130 | "reusable", |
| 131 | { |
| 132 | draftId: "draft-existing", |
| 133 | environmentId: "environment-ssh", |
| 134 | promotedTo: null, |
| 135 | threadId: "thread-existing", |
| 136 | }, |
| 137 | ], |
| 138 | ])("abandons a delayed %s draft open when the user navigates elsewhere", async (_, draft) => { |
| 139 | testState.reset(draft); |
| 140 | const openThread = useNewThreadHandler(); |
| 141 | const pendingOpen = openThread( |
| 142 | { environmentId: "environment-ssh", projectId: "project-remote" } as never, |
| 143 | { replace: true }, |
| 144 | ); |
| 145 | |
| 146 | testState.router.state.location.href = "/usage"; |
| 147 | testState.completeProjectFileRead(null); |
| 148 | await pendingOpen; |
| 149 | |
| 150 | expect(testState.router.state.location.href).toBe("/usage"); |
| 151 | expect(testState.router.navigate).not.toHaveBeenCalled(); |
| 152 | expect(testState.draftStore.setLogicalProjectDraftThreadId).not.toHaveBeenCalled(); |
| 153 | }); |
| 154 | }); |