607 lines
20 KiB
TypeScript
607 lines
20 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { StoreApi } from "zustand";
|
|
import type { AppState } from "@/lib/state/store";
|
|
import type { TaskPendingAction, TaskSession } from "@/lib/types/http";
|
|
import { launchSession } from "@/lib/services/session-launch-service";
|
|
import { releaseLayoutToDefault } from "@/lib/state/dockview-store";
|
|
import { replaceTaskUrl } from "@/lib/links";
|
|
import { selectTaskWithLayout } from "./task-select-helpers";
|
|
|
|
vi.mock("@/lib/services/session-launch-service", () => ({ launchSession: vi.fn() }));
|
|
vi.mock("@/lib/services/session-launch-helpers", () => ({
|
|
buildPrepareRequest: vi.fn(() => ({ request: { taskId: "task-new" } })),
|
|
}));
|
|
vi.mock("@/lib/state/dockview-store", () => ({
|
|
performLayoutSwitch: vi.fn(),
|
|
releaseLayoutToDefault: vi.fn(),
|
|
}));
|
|
vi.mock("@/lib/links", () => ({ replaceTaskUrl: vi.fn() }));
|
|
|
|
const ORIGINAL_TASK_ID = "task-original";
|
|
const PENDING_TASK_ID = "task-pending";
|
|
const ORIGINAL_SESSION_ID = "sess-original";
|
|
const PENDING_SESSION_ID = "sess-pending";
|
|
const ORIGINAL_ENV_ID = "env-original";
|
|
const SUMMARY_UPDATED_AT = "2026-08-15T15:00:00Z";
|
|
const NEWER_SUMMARY_UPDATED_AT = "2026-08-15T15:00:01Z";
|
|
|
|
function makeSelectionHarness(args: {
|
|
activeTaskId: string;
|
|
activeSessionId: string | null;
|
|
envIds?: Record<string, string>;
|
|
sessions?: Record<string, { id: string; task_id: string }>;
|
|
taskRows?: Array<{
|
|
id: string;
|
|
primarySessionId?: string | null;
|
|
taskPendingAction?: TaskPendingAction | null;
|
|
statusSummary?: {
|
|
revision: number;
|
|
updated_at: string;
|
|
pending_action?: TaskPendingAction | null;
|
|
} | null;
|
|
}>;
|
|
}) {
|
|
const listeners: Array<(state: AppState, previousState: AppState) => void> = [];
|
|
const state = {
|
|
tasks: {
|
|
activeTaskId: args.activeTaskId,
|
|
activeSessionId: args.activeSessionId,
|
|
lastSessionByTaskId: {},
|
|
},
|
|
taskPRs: { byTaskId: {} as Record<string, unknown[]> },
|
|
environmentIdBySessionId: args.envIds ?? {},
|
|
taskSessions: { items: (args.sessions ?? {}) as Record<string, TaskSession> },
|
|
kanban: { tasks: args.taskRows ?? [] },
|
|
kanbanMulti: { snapshots: {} },
|
|
};
|
|
const snapshot = () => ({ ...state, tasks: { ...state.tasks } }) as unknown as AppState;
|
|
const notify = (previousState: AppState) => {
|
|
for (const listener of listeners) listener(state as unknown as AppState, previousState);
|
|
};
|
|
const store = {
|
|
getState: () => state as unknown as AppState,
|
|
setState: vi.fn(),
|
|
subscribe: vi.fn((listener: (state: AppState, previousState: AppState) => void) => {
|
|
listeners.push(listener);
|
|
return () => {
|
|
const index = listeners.indexOf(listener);
|
|
if (index >= 0) listeners.splice(index, 1);
|
|
};
|
|
}),
|
|
} as unknown as StoreApi<AppState>;
|
|
const setActiveTask = vi.fn((taskId: string) => {
|
|
const previousState = snapshot();
|
|
state.tasks.activeTaskId = taskId;
|
|
state.tasks.activeSessionId = null;
|
|
notify(previousState);
|
|
});
|
|
return { state, store, setActiveTask, getListenerCount: () => listeners.length };
|
|
}
|
|
|
|
function makeDeferredSessionLoader() {
|
|
let resolveLoad: (sessions: TaskSession[]) => void = () => {};
|
|
let rejectLoad: (error: Error) => void = () => {};
|
|
const loadTaskSessionsForTask = vi.fn(
|
|
() =>
|
|
new Promise<TaskSession[]>((resolve, reject) => {
|
|
resolveLoad = resolve;
|
|
rejectLoad = reject;
|
|
}),
|
|
);
|
|
return {
|
|
loadTaskSessionsForTask,
|
|
resolveLoad: (sessions: TaskSession[]) => resolveLoad(sessions),
|
|
rejectLoad: (error: Error) => rejectLoad(error),
|
|
};
|
|
}
|
|
|
|
async function flushTaskSelection() {
|
|
await Promise.resolve();
|
|
await Promise.resolve();
|
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
}
|
|
|
|
describe("selectTaskWithLayout pending selection races", () => {
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
it("ignores a sessionless selection after another task is selected", async () => {
|
|
const sessionlessTaskId = "task-sessionless";
|
|
const otherTaskId = "task-other";
|
|
const { state, store, setActiveTask } = makeSelectionHarness({
|
|
activeTaskId: sessionlessTaskId,
|
|
activeSessionId: null,
|
|
envIds: { "sess-other": "env-other" },
|
|
sessions: { "sess-other": { id: "sess-other", task_id: otherTaskId } },
|
|
});
|
|
const { loadTaskSessionsForTask, resolveLoad } = makeDeferredSessionLoader();
|
|
|
|
selectTaskWithLayout({
|
|
taskId: sessionlessTaskId,
|
|
task: undefined,
|
|
store,
|
|
switchToSession: vi.fn(),
|
|
loadTaskSessionsForTask,
|
|
setActiveTask,
|
|
setPreparingTaskId: vi.fn(),
|
|
});
|
|
state.tasks.activeTaskId = otherTaskId;
|
|
state.tasks.activeSessionId = "sess-other";
|
|
selectTaskWithLayout({
|
|
taskId: otherTaskId,
|
|
task: { primarySessionId: "sess-other" },
|
|
store,
|
|
switchToSession: vi.fn(),
|
|
loadTaskSessionsForTask: vi.fn(async () => []),
|
|
setActiveTask,
|
|
setPreparingTaskId: vi.fn(),
|
|
});
|
|
resolveLoad([]);
|
|
await flushTaskSelection();
|
|
|
|
expect(setActiveTask).not.toHaveBeenCalledWith(sessionlessTaskId);
|
|
expect(releaseLayoutToDefault).not.toHaveBeenCalled();
|
|
expect(replaceTaskUrl).not.toHaveBeenCalledWith(sessionlessTaskId);
|
|
});
|
|
|
|
it("ignores an old pending selection after returning to the original task", async () => {
|
|
const { store, setActiveTask } = makeSelectionHarness({
|
|
activeTaskId: ORIGINAL_TASK_ID,
|
|
activeSessionId: ORIGINAL_SESSION_ID,
|
|
envIds: { [ORIGINAL_SESSION_ID]: ORIGINAL_ENV_ID },
|
|
sessions: {
|
|
[ORIGINAL_SESSION_ID]: { id: ORIGINAL_SESSION_ID, task_id: ORIGINAL_TASK_ID },
|
|
},
|
|
});
|
|
const { loadTaskSessionsForTask, resolveLoad } = makeDeferredSessionLoader();
|
|
|
|
selectTaskWithLayout({
|
|
taskId: PENDING_TASK_ID,
|
|
task: undefined,
|
|
store,
|
|
switchToSession: vi.fn(),
|
|
loadTaskSessionsForTask,
|
|
setActiveTask,
|
|
setPreparingTaskId: vi.fn(),
|
|
});
|
|
selectTaskWithLayout({
|
|
taskId: ORIGINAL_TASK_ID,
|
|
task: { primarySessionId: ORIGINAL_SESSION_ID },
|
|
store,
|
|
switchToSession: vi.fn(),
|
|
loadTaskSessionsForTask: vi.fn(async () => []),
|
|
setActiveTask,
|
|
setPreparingTaskId: vi.fn(),
|
|
});
|
|
resolveLoad([]);
|
|
await flushTaskSelection();
|
|
|
|
expect(setActiveTask).not.toHaveBeenCalledWith(PENDING_TASK_ID);
|
|
expect(replaceTaskUrl).not.toHaveBeenCalledWith(PENDING_TASK_ID);
|
|
});
|
|
});
|
|
|
|
describe("selectTaskWithLayout pending summary races", () => {
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
it("falls back to the task route when the pending action changes", async () => {
|
|
const initialTask = {
|
|
id: PENDING_TASK_ID,
|
|
primarySessionId: PENDING_SESSION_ID,
|
|
statusSummary: {
|
|
revision: 1,
|
|
updated_at: SUMMARY_UPDATED_AT,
|
|
pending_action: "clarification" as const,
|
|
},
|
|
};
|
|
const { state, store, setActiveTask } = makeSelectionHarness({
|
|
activeTaskId: ORIGINAL_TASK_ID,
|
|
activeSessionId: ORIGINAL_SESSION_ID,
|
|
taskRows: [initialTask],
|
|
});
|
|
const switchToSession = vi.fn();
|
|
const { loadTaskSessionsForTask, resolveLoad } = makeDeferredSessionLoader();
|
|
|
|
selectTaskWithLayout({
|
|
taskId: PENDING_TASK_ID,
|
|
task: initialTask,
|
|
store,
|
|
switchToSession,
|
|
loadTaskSessionsForTask,
|
|
setActiveTask,
|
|
setPreparingTaskId: vi.fn(),
|
|
});
|
|
state.kanban.tasks[0] = {
|
|
...initialTask,
|
|
statusSummary: {
|
|
revision: 2,
|
|
updated_at: NEWER_SUMMARY_UPDATED_AT,
|
|
pending_action: "permission",
|
|
},
|
|
};
|
|
resolveLoad([
|
|
{
|
|
id: PENDING_SESSION_ID,
|
|
task_id: PENDING_TASK_ID,
|
|
state: "WAITING_FOR_INPUT",
|
|
pending_action: "clarification",
|
|
} as TaskSession,
|
|
]);
|
|
await vi.waitFor(() => expect(setActiveTask).toHaveBeenCalledWith(PENDING_TASK_ID));
|
|
expect(loadTaskSessionsForTask).toHaveBeenCalledWith(PENDING_TASK_ID, { force: true });
|
|
expect(switchToSession).not.toHaveBeenCalled();
|
|
expect(replaceTaskUrl).toHaveBeenCalledWith(PENDING_TASK_ID);
|
|
});
|
|
});
|
|
|
|
describe("selectTaskWithLayout same-action summary race", () => {
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
it("falls back to the task route when a same-action revision can have a new owner", async () => {
|
|
const initialTask = {
|
|
id: PENDING_TASK_ID,
|
|
primarySessionId: PENDING_SESSION_ID,
|
|
statusSummary: {
|
|
revision: 1,
|
|
updated_at: SUMMARY_UPDATED_AT,
|
|
pending_action: "clarification" as const,
|
|
},
|
|
};
|
|
const { state, store, setActiveTask } = makeSelectionHarness({
|
|
activeTaskId: ORIGINAL_TASK_ID,
|
|
activeSessionId: ORIGINAL_SESSION_ID,
|
|
taskRows: [initialTask],
|
|
});
|
|
const switchToSession = vi.fn();
|
|
const { loadTaskSessionsForTask, resolveLoad } = makeDeferredSessionLoader();
|
|
|
|
selectTaskWithLayout({
|
|
taskId: PENDING_TASK_ID,
|
|
task: initialTask,
|
|
store,
|
|
switchToSession,
|
|
loadTaskSessionsForTask,
|
|
setActiveTask,
|
|
setPreparingTaskId: vi.fn(),
|
|
});
|
|
state.kanban.tasks[0] = {
|
|
...initialTask,
|
|
statusSummary: {
|
|
revision: 2,
|
|
updated_at: NEWER_SUMMARY_UPDATED_AT,
|
|
pending_action: "clarification",
|
|
},
|
|
};
|
|
resolveLoad([
|
|
{
|
|
id: PENDING_SESSION_ID,
|
|
task_id: PENDING_TASK_ID,
|
|
state: "WAITING_FOR_INPUT",
|
|
pending_action: "clarification",
|
|
} as TaskSession,
|
|
]);
|
|
await vi.waitFor(() => expect(setActiveTask).toHaveBeenCalledWith(PENDING_TASK_ID));
|
|
expect(switchToSession).not.toHaveBeenCalled();
|
|
expect(replaceTaskUrl).toHaveBeenCalledWith(PENDING_TASK_ID);
|
|
});
|
|
});
|
|
|
|
describe("selectTaskWithLayout deleted-task race", () => {
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
it("leaves selection inert when the pending task projection disappears", async () => {
|
|
const initialTask = {
|
|
id: PENDING_TASK_ID,
|
|
primarySessionId: PENDING_SESSION_ID,
|
|
statusSummary: {
|
|
revision: 1,
|
|
updated_at: SUMMARY_UPDATED_AT,
|
|
pending_action: "clarification" as const,
|
|
},
|
|
};
|
|
const { state, store, setActiveTask } = makeSelectionHarness({
|
|
activeTaskId: ORIGINAL_TASK_ID,
|
|
activeSessionId: ORIGINAL_SESSION_ID,
|
|
taskRows: [initialTask],
|
|
});
|
|
const switchToSession = vi.fn();
|
|
const { loadTaskSessionsForTask, resolveLoad } = makeDeferredSessionLoader();
|
|
|
|
selectTaskWithLayout({
|
|
taskId: PENDING_TASK_ID,
|
|
task: initialTask,
|
|
store,
|
|
switchToSession,
|
|
loadTaskSessionsForTask,
|
|
setActiveTask,
|
|
setPreparingTaskId: vi.fn(),
|
|
});
|
|
state.kanban.tasks = [];
|
|
resolveLoad([
|
|
{
|
|
id: PENDING_SESSION_ID,
|
|
task_id: PENDING_TASK_ID,
|
|
state: "WAITING_FOR_INPUT",
|
|
pending_action: "clarification",
|
|
} as TaskSession,
|
|
]);
|
|
await flushTaskSelection();
|
|
|
|
expect(switchToSession).not.toHaveBeenCalled();
|
|
expect(setActiveTask).not.toHaveBeenCalled();
|
|
expect(replaceTaskUrl).not.toHaveBeenCalledWith(PENDING_TASK_ID);
|
|
});
|
|
});
|
|
|
|
describe("selectTaskWithLayout rejected pending-load races", () => {
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
for (const primarySessionId of [PENDING_SESSION_ID, null]) {
|
|
it(`falls back after a rejected ${primarySessionId ? "primary" : "sessionless"} load when the summary changes`, async () => {
|
|
const initialTask = {
|
|
id: PENDING_TASK_ID,
|
|
primarySessionId,
|
|
statusSummary: {
|
|
revision: 1,
|
|
updated_at: SUMMARY_UPDATED_AT,
|
|
pending_action: "clarification" as const,
|
|
},
|
|
};
|
|
const { state, store, setActiveTask } = makeSelectionHarness({
|
|
activeTaskId: ORIGINAL_TASK_ID,
|
|
activeSessionId: ORIGINAL_SESSION_ID,
|
|
taskRows: [initialTask],
|
|
});
|
|
const switchToSession = vi.fn();
|
|
const { loadTaskSessionsForTask, rejectLoad } = makeDeferredSessionLoader();
|
|
|
|
selectTaskWithLayout({
|
|
taskId: PENDING_TASK_ID,
|
|
task: initialTask,
|
|
store,
|
|
switchToSession,
|
|
loadTaskSessionsForTask,
|
|
setActiveTask,
|
|
setPreparingTaskId: vi.fn(),
|
|
});
|
|
state.kanban.tasks[0] = {
|
|
...initialTask,
|
|
statusSummary: {
|
|
revision: 2,
|
|
updated_at: NEWER_SUMMARY_UPDATED_AT,
|
|
pending_action: "permission",
|
|
},
|
|
};
|
|
rejectLoad(new Error("load failed"));
|
|
await vi.waitFor(() => expect(setActiveTask).toHaveBeenCalledWith(PENDING_TASK_ID));
|
|
expect(switchToSession).not.toHaveBeenCalled();
|
|
expect(replaceTaskUrl).toHaveBeenCalledWith(PENDING_TASK_ID);
|
|
});
|
|
}
|
|
});
|
|
|
|
describe("selectTaskWithLayout external active-task changes", () => {
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
function pendingHarness() {
|
|
return makeSelectionHarness({
|
|
activeTaskId: ORIGINAL_TASK_ID,
|
|
activeSessionId: ORIGINAL_SESSION_ID,
|
|
envIds: { [ORIGINAL_SESSION_ID]: ORIGINAL_ENV_ID },
|
|
sessions: {
|
|
[PENDING_SESSION_ID]: { id: PENDING_SESSION_ID, task_id: PENDING_TASK_ID },
|
|
[ORIGINAL_SESSION_ID]: { id: ORIGINAL_SESSION_ID, task_id: ORIGINAL_TASK_ID },
|
|
},
|
|
});
|
|
}
|
|
|
|
it("ignores a pending selection after an external active-task change", async () => {
|
|
const switchToSession = vi.fn();
|
|
const { state, store, setActiveTask } = pendingHarness();
|
|
const { loadTaskSessionsForTask, resolveLoad } = makeDeferredSessionLoader();
|
|
selectTaskWithLayout({
|
|
taskId: PENDING_TASK_ID,
|
|
task: { primarySessionId: PENDING_SESSION_ID },
|
|
store,
|
|
switchToSession,
|
|
loadTaskSessionsForTask,
|
|
setActiveTask,
|
|
setPreparingTaskId: vi.fn(),
|
|
});
|
|
|
|
state.tasks.activeTaskId = "task-other";
|
|
state.tasks.activeSessionId = null;
|
|
resolveLoad([
|
|
{ id: PENDING_SESSION_ID, task_id: PENDING_TASK_ID, is_primary: true } as TaskSession,
|
|
]);
|
|
await flushTaskSelection();
|
|
|
|
expect(switchToSession).not.toHaveBeenCalled();
|
|
expect(replaceTaskUrl).not.toHaveBeenCalledWith(PENDING_TASK_ID);
|
|
});
|
|
|
|
it("remembers an external switch even when it returns to the original task", async () => {
|
|
const switchToSession = vi.fn();
|
|
const { store, setActiveTask } = pendingHarness();
|
|
const { loadTaskSessionsForTask, resolveLoad } = makeDeferredSessionLoader();
|
|
selectTaskWithLayout({
|
|
taskId: PENDING_TASK_ID,
|
|
task: { primarySessionId: PENDING_SESSION_ID },
|
|
store,
|
|
switchToSession,
|
|
loadTaskSessionsForTask,
|
|
setActiveTask,
|
|
setPreparingTaskId: vi.fn(),
|
|
});
|
|
|
|
setActiveTask("task-other");
|
|
setActiveTask(ORIGINAL_TASK_ID);
|
|
resolveLoad([
|
|
{ id: PENDING_SESSION_ID, task_id: PENDING_TASK_ID, is_primary: true } as TaskSession,
|
|
]);
|
|
await flushTaskSelection();
|
|
|
|
expect(switchToSession).not.toHaveBeenCalled();
|
|
expect(replaceTaskUrl).not.toHaveBeenCalledWith(PENDING_TASK_ID);
|
|
});
|
|
|
|
it("ignores an off-route pending selection after unrelated navigation", async () => {
|
|
const switchToSession = vi.fn();
|
|
const navigateToTask = vi.fn();
|
|
const selectionController = new AbortController();
|
|
const { store, setActiveTask } = pendingHarness();
|
|
const { loadTaskSessionsForTask, resolveLoad } = makeDeferredSessionLoader();
|
|
selectTaskWithLayout({
|
|
taskId: PENDING_TASK_ID,
|
|
task: {
|
|
primarySessionId: PENDING_SESSION_ID,
|
|
taskPendingAction: "clarification",
|
|
},
|
|
store,
|
|
switchToSession,
|
|
loadTaskSessionsForTask,
|
|
setActiveTask,
|
|
setPreparingTaskId: vi.fn(),
|
|
navigateToTask,
|
|
selectionSignal: selectionController.signal,
|
|
});
|
|
|
|
expect(loadTaskSessionsForTask).toHaveBeenCalledWith(PENDING_TASK_ID, {
|
|
force: true,
|
|
signal: selectionController.signal,
|
|
});
|
|
|
|
selectionController.abort();
|
|
resolveLoad([
|
|
{
|
|
id: PENDING_SESSION_ID,
|
|
task_id: PENDING_TASK_ID,
|
|
state: "WAITING_FOR_INPUT",
|
|
pending_action: "clarification",
|
|
} as TaskSession,
|
|
]);
|
|
await flushTaskSelection();
|
|
|
|
expect(switchToSession).not.toHaveBeenCalled();
|
|
expect(setActiveTask).not.toHaveBeenCalledWith(PENDING_TASK_ID);
|
|
expect(navigateToTask).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("selectTaskWithLayout selection guard cleanup", () => {
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
for (const task of [
|
|
{ label: "primary", value: { primarySessionId: PENDING_SESSION_ID } },
|
|
{ label: "sessionless", value: undefined },
|
|
]) {
|
|
it(`disposes the guard when loading a ${task.label} task rejects`, async () => {
|
|
const { store, setActiveTask, getListenerCount } = makeSelectionHarness({
|
|
activeTaskId: ORIGINAL_TASK_ID,
|
|
activeSessionId: ORIGINAL_SESSION_ID,
|
|
envIds: { [ORIGINAL_SESSION_ID]: ORIGINAL_ENV_ID },
|
|
});
|
|
selectTaskWithLayout({
|
|
taskId: PENDING_TASK_ID,
|
|
task: task.value,
|
|
store,
|
|
switchToSession: vi.fn(),
|
|
loadTaskSessionsForTask: vi.fn(async () => {
|
|
throw new Error("load failed");
|
|
}),
|
|
setActiveTask,
|
|
setPreparingTaskId: vi.fn(),
|
|
});
|
|
|
|
expect(getListenerCount()).toBe(1);
|
|
await flushTaskSelection();
|
|
expect(getListenerCount()).toBe(0);
|
|
});
|
|
}
|
|
});
|
|
|
|
describe("selectTaskWithLayout old-session changes", () => {
|
|
beforeEach(() => vi.clearAllMocks());
|
|
|
|
it("keeps a pending selection alive when only the old session id changes", async () => {
|
|
vi.mocked(launchSession).mockResolvedValue({} as never);
|
|
const sessionlessTaskId = "task-sessionless";
|
|
const { state, store, setActiveTask } = makeSelectionHarness({
|
|
activeTaskId: "task-old",
|
|
activeSessionId: "sess-old",
|
|
});
|
|
const { loadTaskSessionsForTask, resolveLoad } = makeDeferredSessionLoader();
|
|
selectTaskWithLayout({
|
|
taskId: sessionlessTaskId,
|
|
task: undefined,
|
|
store,
|
|
switchToSession: vi.fn(),
|
|
loadTaskSessionsForTask,
|
|
setActiveTask,
|
|
setPreparingTaskId: vi.fn(),
|
|
});
|
|
|
|
state.tasks.activeSessionId = "sess-old-replaced";
|
|
resolveLoad([]);
|
|
await vi.waitFor(() => expect(setActiveTask).toHaveBeenCalledWith(sessionlessTaskId));
|
|
expect(replaceTaskUrl).toHaveBeenCalledWith(sessionlessTaskId);
|
|
});
|
|
|
|
it("uses the current old session after a primary-session load resolves", async () => {
|
|
const replacementSessionId = "sess-old-replaced";
|
|
const switchToSession = vi.fn();
|
|
const { state, store, setActiveTask } = makeSelectionHarness({
|
|
activeTaskId: ORIGINAL_TASK_ID,
|
|
activeSessionId: ORIGINAL_SESSION_ID,
|
|
});
|
|
const { loadTaskSessionsForTask, resolveLoad } = makeDeferredSessionLoader();
|
|
selectTaskWithLayout({
|
|
taskId: PENDING_TASK_ID,
|
|
task: { primarySessionId: PENDING_SESSION_ID },
|
|
store,
|
|
switchToSession,
|
|
loadTaskSessionsForTask,
|
|
setActiveTask,
|
|
setPreparingTaskId: vi.fn(),
|
|
});
|
|
|
|
state.tasks.activeSessionId = replacementSessionId;
|
|
resolveLoad([{ id: PENDING_SESSION_ID, task_id: PENDING_TASK_ID } as TaskSession]);
|
|
await vi.waitFor(() =>
|
|
expect(switchToSession).toHaveBeenCalledWith(
|
|
PENDING_TASK_ID,
|
|
PENDING_SESSION_ID,
|
|
replacementSessionId,
|
|
),
|
|
);
|
|
});
|
|
|
|
it("uses the current old session after a primary-session load rejects", async () => {
|
|
const replacementSessionId = "sess-old-replaced";
|
|
const switchToSession = vi.fn();
|
|
const { state, store, setActiveTask } = makeSelectionHarness({
|
|
activeTaskId: ORIGINAL_TASK_ID,
|
|
activeSessionId: ORIGINAL_SESSION_ID,
|
|
});
|
|
const { loadTaskSessionsForTask, rejectLoad } = makeDeferredSessionLoader();
|
|
selectTaskWithLayout({
|
|
taskId: PENDING_TASK_ID,
|
|
task: { primarySessionId: PENDING_SESSION_ID },
|
|
store,
|
|
switchToSession,
|
|
loadTaskSessionsForTask,
|
|
setActiveTask,
|
|
setPreparingTaskId: vi.fn(),
|
|
});
|
|
|
|
state.tasks.activeSessionId = replacementSessionId;
|
|
rejectLoad(new Error("load failed"));
|
|
await vi.waitFor(() =>
|
|
expect(switchToSession).toHaveBeenCalledWith(
|
|
PENDING_TASK_ID,
|
|
PENDING_SESSION_ID,
|
|
replacementSessionId,
|
|
),
|
|
);
|
|
});
|
|
});
|