341 lines
11 KiB
TypeScript
341 lines
11 KiB
TypeScript
import { renderHook, waitFor } from "@testing-library/react";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { UserSettingsState } from "@/lib/state/slices/settings/types";
|
|
|
|
type PendingTaskCreateLastUsed = {
|
|
repositoryId?: string;
|
|
branch?: string;
|
|
agentProfileId?: string;
|
|
executorProfileId?: string;
|
|
};
|
|
|
|
const mockFetchUserSettings = vi.fn();
|
|
const mockSetUserSettings = vi.fn((settings: UserSettingsState) => {
|
|
mockState.userSettings = settings;
|
|
});
|
|
const mockReadQueuedTaskCreateLastUsedState = vi.fn<() => PendingTaskCreateLastUsed>(() => ({
|
|
repositoryId: undefined,
|
|
branch: undefined,
|
|
agentProfileId: undefined,
|
|
executorProfileId: undefined,
|
|
}));
|
|
|
|
type MockState = {
|
|
userSettings: UserSettingsState;
|
|
setUserSettings: typeof mockSetUserSettings;
|
|
};
|
|
|
|
let mockState: MockState;
|
|
|
|
vi.mock("@/components/state-provider", () => ({
|
|
useAppStore: (selector: (state: MockState) => unknown) => selector(mockState),
|
|
useAppStoreApi: () => ({
|
|
getState: () => mockState,
|
|
setState: vi.fn(),
|
|
}),
|
|
}));
|
|
|
|
vi.mock("@/lib/api/domains/settings-api", () => ({
|
|
fetchUserSettings: (...args: unknown[]) => mockFetchUserSettings(...args),
|
|
}));
|
|
|
|
vi.mock("@/components/task-create-dialog-handlers", () => ({
|
|
readQueuedTaskCreateLastUsedState: () => mockReadQueuedTaskCreateLastUsedState(),
|
|
}));
|
|
|
|
import {
|
|
__resetEnsureUserSettingsForTests,
|
|
useEnsureUserSettings,
|
|
} from "./use-ensure-user-settings";
|
|
|
|
/** Builds a full default user-settings state marked as not loaded. */
|
|
function makeUnloadedSettings(): UserSettingsState {
|
|
return {
|
|
revision: null,
|
|
workspaceId: null,
|
|
workflowId: null,
|
|
kanbanViewMode: null,
|
|
startupPage: "task_overview",
|
|
repositoryIds: [],
|
|
tasksListSort: "updated_desc",
|
|
tasksListGroup: "state",
|
|
tasksListShowDetails: false,
|
|
preferredShell: null,
|
|
shellOptions: [],
|
|
defaultEditorId: null,
|
|
enablePreviewOnClick: false,
|
|
chatSubmitKey: "cmd_enter",
|
|
reviewAutoMarkOnScroll: true,
|
|
confirmTaskArchive: true,
|
|
preventAutoStartAgentOnOpen: false,
|
|
unreadDivider: true,
|
|
agentGeneratedTaskTitles: false,
|
|
autoFocusNewTasks: true,
|
|
mcpTaskAgentProfileDefault: "current_task",
|
|
showAnchoredPromptBar: false,
|
|
showScrollToLastPrompt: true,
|
|
showScrollToStart: false,
|
|
showTranscriptAutoScrollControl: true,
|
|
showTodoListPanel: false,
|
|
showTodoListPanelOnlyWhenNotEmpty: false,
|
|
showReleaseNotification: true,
|
|
releaseNotesLastSeenVersion: null,
|
|
savedLayouts: [],
|
|
sidebarViews: [],
|
|
sidebarViewsByWorkspace: {},
|
|
sidebarLayoutsByWorkspace: {},
|
|
sidebarActiveViewId: null,
|
|
sidebarDraft: null,
|
|
threadViews: [],
|
|
threadActiveViewId: null,
|
|
threadViewDraft: null,
|
|
sidebarTaskPrefs: { pinnedTaskIds: [], orderedTaskIds: [], subtaskOrderByParentId: {} },
|
|
sidebarTaskColorAutomation: { enabled: false, rules: [] },
|
|
sidebarTaskColors: {},
|
|
taskCreateLastUsed: {
|
|
repositoryId: null,
|
|
branch: null,
|
|
agentProfileId: null,
|
|
executorProfileId: null,
|
|
workflowIdsByWorkspace: {},
|
|
},
|
|
jiraSavedViews: undefined,
|
|
jiraTaskPresets: undefined,
|
|
githubSavedPresets: undefined,
|
|
githubDefaultQueryPresets: undefined,
|
|
gitlabSavedPresets: undefined,
|
|
azureDevOpsBrowsePreferences: undefined,
|
|
defaultUtilityAgentId: null,
|
|
defaultUtilityAgentProfileId: null,
|
|
keyboardShortcuts: {},
|
|
terminalLinkBehavior: "new_tab",
|
|
terminalFontFamily: null,
|
|
terminalFontSize: null,
|
|
changesPanelLayout: "tree",
|
|
lastSeenDisplay: "absolute",
|
|
systemMetricsDisplay: { showInTopbar: false, simplified: false },
|
|
sidebarHoverEnabled: true,
|
|
sidebarHoverDelayMs: 500,
|
|
appStatusBarEnabled: false,
|
|
resolveSessionHostnames: false,
|
|
appStatusBarOrder: { leftItemIds: [], rightItemIds: [] },
|
|
quickChatTabOrderByWorkspace: {},
|
|
lspAutoStartLanguages: [],
|
|
lspAutoInstallLanguages: [],
|
|
lspServerConfigs: {},
|
|
lspStatusLocation: "toolbar",
|
|
hiddenWorkflowStepIds: {},
|
|
workflowIdsWithAutoHideEmptySteps: [],
|
|
kanbanSort: "created_desc",
|
|
kanbanPriorityFilterTokens: [],
|
|
loaded: false,
|
|
};
|
|
}
|
|
|
|
/** Builds a mock settings API response wrapping the given task-create last-used payload. */
|
|
function userSettingsResponse(taskCreateLastUsed = {}) {
|
|
return {
|
|
shell_options: [],
|
|
settings: {
|
|
task_create_last_used: taskCreateLastUsed,
|
|
},
|
|
};
|
|
}
|
|
|
|
beforeEach(() => {
|
|
__resetEnsureUserSettingsForTests();
|
|
vi.clearAllMocks();
|
|
mockState = {
|
|
userSettings: makeUnloadedSettings(),
|
|
setUserSettings: mockSetUserSettings,
|
|
};
|
|
});
|
|
|
|
describe("useEnsureUserSettings", () => {
|
|
it("fetches and stores user settings on first enabled mount", async () => {
|
|
mockFetchUserSettings.mockResolvedValue(
|
|
userSettingsResponse({ repository_id: "repo-1", branch: "main" }),
|
|
);
|
|
|
|
renderHook(() => useEnsureUserSettings(true));
|
|
|
|
await waitFor(() => expect(mockSetUserSettings).toHaveBeenCalled());
|
|
expect(mockFetchUserSettings).toHaveBeenCalledWith({ cache: "no-store" });
|
|
expect(mockSetUserSettings.mock.calls[0]![0].loaded).toBe(true);
|
|
expect(mockSetUserSettings.mock.calls[0]![0].taskCreateLastUsed).toMatchObject({
|
|
repositoryId: "repo-1",
|
|
branch: "main",
|
|
});
|
|
});
|
|
|
|
it("joins an in-flight settings fetch instead of starting a duplicate request", async () => {
|
|
let resolveFetch: (value: unknown) => void = () => undefined;
|
|
mockFetchUserSettings.mockReturnValue(
|
|
new Promise((resolve) => {
|
|
resolveFetch = resolve;
|
|
}),
|
|
);
|
|
|
|
renderHook(() => useEnsureUserSettings(true));
|
|
renderHook(() => useEnsureUserSettings(true));
|
|
|
|
await waitFor(() => expect(mockFetchUserSettings).toHaveBeenCalledTimes(1));
|
|
resolveFetch(userSettingsResponse());
|
|
await waitFor(() => expect(mockSetUserSettings).toHaveBeenCalled());
|
|
});
|
|
});
|
|
|
|
describe("useEnsureUserSettings — fetched settings overlays", () => {
|
|
it("merges only defined queued task-create fields over fetched settings", async () => {
|
|
mockReadQueuedTaskCreateLastUsedState.mockReturnValue({
|
|
repositoryId: undefined,
|
|
branch: undefined,
|
|
agentProfileId: "agent-2",
|
|
executorProfileId: undefined,
|
|
});
|
|
mockFetchUserSettings.mockResolvedValue(
|
|
userSettingsResponse({
|
|
repository_id: "repo-1",
|
|
branch: "main",
|
|
agent_profile_id: "agent-1",
|
|
executor_profile_id: "exec-profile-1",
|
|
}),
|
|
);
|
|
|
|
renderHook(() => useEnsureUserSettings(true));
|
|
|
|
await waitFor(() => expect(mockSetUserSettings).toHaveBeenCalled());
|
|
expect(mockSetUserSettings.mock.calls[0]![0].taskCreateLastUsed).toEqual({
|
|
repositoryId: "repo-1",
|
|
branch: "main",
|
|
agentProfileId: "agent-2",
|
|
executorProfileId: "exec-profile-1",
|
|
workflowIdsByWorkspace: {},
|
|
synced: true,
|
|
});
|
|
});
|
|
|
|
it("preserves queued task-create fields when multiple callers join the same fetch", async () => {
|
|
let resolveFetch: (value: unknown) => void = () => undefined;
|
|
mockReadQueuedTaskCreateLastUsedState.mockReturnValue({
|
|
repositoryId: undefined,
|
|
branch: "feature",
|
|
agentProfileId: undefined,
|
|
executorProfileId: undefined,
|
|
});
|
|
mockFetchUserSettings.mockReturnValue(
|
|
new Promise((resolve) => {
|
|
resolveFetch = resolve;
|
|
}),
|
|
);
|
|
|
|
renderHook(() => useEnsureUserSettings(true));
|
|
renderHook(() => useEnsureUserSettings(true));
|
|
resolveFetch(
|
|
userSettingsResponse({
|
|
repository_id: "repo-1",
|
|
branch: "main",
|
|
agent_profile_id: "agent-1",
|
|
}),
|
|
);
|
|
|
|
await waitFor(() => expect(mockSetUserSettings).toHaveBeenCalledTimes(2));
|
|
expect(mockSetUserSettings.mock.calls.at(-1)![0].taskCreateLastUsed).toMatchObject({
|
|
repositoryId: "repo-1",
|
|
branch: "feature",
|
|
agentProfileId: "agent-1",
|
|
});
|
|
});
|
|
|
|
it("does not fetch while disabled", () => {
|
|
const { result } = renderHook(() => useEnsureUserSettings(false));
|
|
|
|
expect(mockFetchUserSettings).not.toHaveBeenCalled();
|
|
expect(result.current.loaded).toBe(false);
|
|
});
|
|
|
|
it("settles a failed fetch for the current mount but retries on the next mount", async () => {
|
|
mockFetchUserSettings
|
|
.mockRejectedValueOnce(new Error("temporary"))
|
|
.mockResolvedValueOnce(userSettingsResponse({ repository_id: "repo-2" }));
|
|
|
|
const first = renderHook(() => useEnsureUserSettings(true));
|
|
|
|
await waitFor(() => expect(first.result.current.loaded).toBe(true));
|
|
expect(mockSetUserSettings).not.toHaveBeenCalled();
|
|
first.unmount();
|
|
|
|
renderHook(() => useEnsureUserSettings(true));
|
|
|
|
await waitFor(() => expect(mockFetchUserSettings).toHaveBeenCalledTimes(2));
|
|
await waitFor(() => expect(mockSetUserSettings).toHaveBeenCalled());
|
|
expect(mockSetUserSettings.mock.calls[0]![0].taskCreateLastUsed.repositoryId).toBe("repo-2");
|
|
});
|
|
});
|
|
|
|
describe("useEnsureUserSettings — loaded settings overlay", () => {
|
|
it("applies queued task-create fields when settings are already loaded", () => {
|
|
mockState.userSettings = {
|
|
...makeUnloadedSettings(),
|
|
loaded: true,
|
|
taskCreateLastUsed: {
|
|
repositoryId: "repo-1",
|
|
branch: "main",
|
|
agentProfileId: "agent-1",
|
|
executorProfileId: "exec-1",
|
|
workflowIdsByWorkspace: {},
|
|
},
|
|
};
|
|
mockReadQueuedTaskCreateLastUsedState.mockReturnValue({
|
|
repositoryId: "repo-2",
|
|
branch: "feature",
|
|
agentProfileId: undefined,
|
|
executorProfileId: undefined,
|
|
});
|
|
|
|
const { result } = renderHook(() => useEnsureUserSettings(true));
|
|
|
|
expect(mockFetchUserSettings).not.toHaveBeenCalled();
|
|
expect(result.current.userSettings.taskCreateLastUsed).toEqual({
|
|
repositoryId: "repo-2",
|
|
branch: "feature",
|
|
agentProfileId: "agent-1",
|
|
executorProfileId: "exec-1",
|
|
workflowIdsByWorkspace: {},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("useEnsureUserSettings — stale settings fetches", () => {
|
|
it("keeps task-create edits made while a settings fetch is in flight", async () => {
|
|
let resolveFetch: (value: unknown) => void = () => undefined;
|
|
mockFetchUserSettings.mockReturnValue(
|
|
new Promise((resolve) => {
|
|
resolveFetch = resolve;
|
|
}),
|
|
);
|
|
|
|
renderHook(() => useEnsureUserSettings(true));
|
|
await waitFor(() => expect(mockFetchUserSettings).toHaveBeenCalledTimes(1));
|
|
|
|
mockReadQueuedTaskCreateLastUsedState.mockReturnValue({
|
|
repositoryId: "repo-2",
|
|
branch: "feature",
|
|
agentProfileId: undefined,
|
|
executorProfileId: undefined,
|
|
});
|
|
resolveFetch(
|
|
userSettingsResponse({
|
|
repository_id: "repo-1",
|
|
branch: "main",
|
|
}),
|
|
);
|
|
|
|
await waitFor(() => expect(mockSetUserSettings).toHaveBeenCalled());
|
|
expect(mockSetUserSettings.mock.calls[0]![0].taskCreateLastUsed).toMatchObject({
|
|
repositoryId: "repo-2",
|
|
branch: "feature",
|
|
});
|
|
});
|
|
});
|