Files
kandev/apps/web/components/task/task-select-helpers.test.ts
T

141 lines
5.4 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { prepareAndSwitchTask } 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", () => ({
releaseLayoutToDefault: vi.fn(),
useDockviewStore: { getState: () => ({ api: null, buildDefaultLayout: vi.fn() }) },
}));
vi.mock("@/lib/state/layout-manager", () => ({
INTENT_PR_REVIEW: "pr-review",
}));
vi.mock("@/lib/links", () => ({
replaceTaskUrl: vi.fn(),
}));
import { launchSession } from "@/lib/services/session-launch-service";
import { releaseLayoutToDefault } from "@/lib/state/dockview-store";
import type { StoreApi } from "zustand";
import type { AppState } from "@/lib/state/store";
const NEW_TASK_ID = "task-new";
const OLD_SESSION_ID = "old-session";
/**
* Regression: switching from a task with env-scoped panels open (file-editor,
* diff-viewer, commit-detail, browser, vscode, pr-detail) to a task that
* needs an env prepared previously left those panels mounted in the dockview
* for the entire `await launchSession(...)` round trip. The user saw stray
* tabs (e.g. a diff panel) from the old task on the new task's page while the
* env was still being prepared.
*
* Fix: release the outgoing env (drops env-scoped portals + falls back to a
* default layout) BEFORE awaiting `launchSession`, so the user sees a clean
* slate during preparation. The new env is adopted in the usual way once
* its session id is known.
*/
describe("prepareAndSwitchTask — outgoing-env panel cleanup", () => {
function makeStore(activeSessionId: string | null): StoreApi<AppState> {
const state = {
tasks: { activeSessionId },
taskPRs: { byTaskId: {} as Record<string, unknown[]> },
environmentIdBySessionId: activeSessionId ? { [activeSessionId]: "env-old" } : {},
};
return {
getState: () => state as unknown as AppState,
setState: vi.fn(),
subscribe: vi.fn(),
} as unknown as StoreApi<AppState>;
}
beforeEach(() => {
vi.clearAllMocks();
});
it("releases the outgoing env's panels before awaiting launchSession", async () => {
// Make launchSession return a deferred we control so we can observe
// synchronous side effects that happen before the await resolves.
let resolveLaunch: (v: { session_id: string }) => void = () => {};
vi.mocked(launchSession).mockImplementation(
() =>
new Promise((res) => {
resolveLaunch = res;
}),
);
const store = makeStore(OLD_SESSION_ID);
const switchToSession = vi.fn();
const setPreparingTaskId = vi.fn();
const promise = prepareAndSwitchTask(NEW_TASK_ID, store, switchToSession, setPreparingTaskId);
// The outgoing env release must have already happened — without this the
// diff/file-editor panels from the previous task stay visible until
// launchSession resolves and the WS env-id mapping arrives.
expect(releaseLayoutToDefault).toHaveBeenCalledTimes(1);
expect(switchToSession).not.toHaveBeenCalled();
resolveLaunch({ session_id: "new-session" });
const result = await promise;
// Happy-path coverage: switchToSession must run with the new session id and
// a null oldSessionId (releaseLayoutToDefault already saved + released the
// outgoing env; passing the real oldSessionId would trigger a second
// saveOutgoingEnv that overwrites envA's correct layout with the default).
expect(result).toBe(true);
expect(switchToSession).toHaveBeenCalledTimes(1);
expect(switchToSession).toHaveBeenCalledWith(NEW_TASK_ID, "new-session", null);
expect(setPreparingTaskId).toHaveBeenLastCalledWith(null);
});
/**
* Failure-path regression: launch errors / empty responses go through
* selectTaskWithLayout's no-session fallback. Releasing the outgoing env
* exactly once — here, before the await — keeps the originating task's
* saved layout intact. A second release on the failure tail would overwrite
* it with the default layout (the api state after the first release).
*/
it("returns false and does not call switchToSession when launchSession throws", async () => {
vi.mocked(launchSession).mockRejectedValue(new Error("network"));
const store = makeStore(OLD_SESSION_ID);
const switchToSession = vi.fn();
const setPreparingTaskId = vi.fn();
const result = await prepareAndSwitchTask(
NEW_TASK_ID,
store,
switchToSession,
setPreparingTaskId,
);
expect(result).toBe(false);
expect(releaseLayoutToDefault).toHaveBeenCalledTimes(1);
expect(switchToSession).not.toHaveBeenCalled();
expect(setPreparingTaskId).toHaveBeenLastCalledWith(null);
});
it("returns false and does not call switchToSession when session_id is absent", async () => {
vi.mocked(launchSession).mockResolvedValue({} as never);
const store = makeStore(OLD_SESSION_ID);
const switchToSession = vi.fn();
const setPreparingTaskId = vi.fn();
const result = await prepareAndSwitchTask(
NEW_TASK_ID,
store,
switchToSession,
setPreparingTaskId,
);
expect(result).toBe(false);
expect(releaseLayoutToDefault).toHaveBeenCalledTimes(1);
expect(switchToSession).not.toHaveBeenCalled();
expect(setPreparingTaskId).toHaveBeenLastCalledWith(null);
});
});