Files
kandev/apps/web/hooks/domains/session/use-commit-detail.test.ts
T
João Salavisa 88002a60a3 fix(i18n): localize date-fns distances and drop redundant t() test stubs (#2363)
Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 13:29:44 +02:00

206 lines
6.7 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import { act, cleanup, renderHook, waitFor } from "@testing-library/react";
import type { FileInfo } from "@/lib/state/store";
const mocks = vi.hoisted(() => ({
requestCommitDetail: vi.fn(),
toast: vi.fn(),
state: {
tasks: { activeSessionId: "session-1", activeTaskId: "task-1" },
taskSessions: { items: { "session-1": { task_id: "task-1" } } },
sessionAgentctl: { itemsBySessionId: { "session-1": { status: "ready" } } },
},
}));
vi.mock("@/components/state-provider", () => ({
useAppStore: (selector: (state: unknown) => unknown) => selector(mocks.state),
}));
vi.mock("@/components/toast-provider", () => ({
useToast: () => ({ toast: mocks.toast }),
}));
vi.mock("@/components/task/commit-detail-request", () => ({
CommitDetailProtocolError: class CommitDetailProtocolError extends Error {},
requestCommitDetail: mocks.requestCommitDetail,
}));
import { useCommitDetail } from "./use-commit-detail";
const remoteFiles: Record<string, FileInfo> = {
"src/remote.ts": {
path: "src/remote.ts",
status: "modified",
staged: false,
diff: "remote patch",
},
};
const remoteTarget = {
source: "github" as const,
sha: "remote123",
workspaceId: "workspace-1",
owner: "acme",
repo: "widget",
};
const nextRemoteTarget = { ...remoteTarget, sha: "remote456" };
function deferred<T>() {
let resolve!: (value: T) => void;
const promise = new Promise<T>((resolvePromise) => {
resolve = resolvePromise;
});
return { promise, resolve };
}
afterEach(() => {
cleanup();
mocks.requestCommitDetail.mockReset();
mocks.toast.mockReset();
mocks.state.sessionAgentctl.itemsBySessionId["session-1"].status = "ready";
});
describe("useCommitDetail", () => {
it("requests a GitHub target without adding local session routing", async () => {
mocks.requestCommitDetail.mockResolvedValue({
source: "github",
success: true,
files: remoteFiles,
commit: {
sha: "remote123",
message: "remote commit",
author_login: "octocat",
author_name: "Octo Cat",
author_date: "2026-08-04T12:00:00Z",
additions: 1,
deletions: 0,
files_changed: 1,
files: [],
},
});
const { result } = renderHook(() => useCommitDetail(remoteTarget));
await waitFor(() => expect(result.current.loading).toBe(false));
expect(mocks.requestCommitDetail).toHaveBeenCalledWith({ target: remoteTarget });
expect(result.current.files).toEqual(remoteFiles);
expect(result.current.commit?.message).toBe("remote commit");
});
it("passes local session readiness only for a local target", async () => {
mocks.requestCommitDetail.mockResolvedValue({
source: "local",
success: true,
files: remoteFiles,
});
const target = { source: "local" as const, sha: "local123", repo: "frontend" };
const { result } = renderHook(() => useCommitDetail(target));
await waitFor(() => expect(result.current.loading).toBe(false));
expect(mocks.requestCommitDetail).toHaveBeenCalledWith({
target,
local: {
sessionId: "session-1",
taskId: "task-1",
agentctlReady: true,
},
});
});
it("keeps a GitHub error on the remote path and exposes it to the panel", async () => {
mocks.requestCommitDetail.mockRejectedValue(new Error("GitHub unavailable"));
const { result } = renderHook(() => useCommitDetail(remoteTarget));
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.files).toBeNull();
expect(result.current.error).toBe("GitHub unavailable");
expect(mocks.toast).toHaveBeenCalled();
expect(mocks.requestCommitDetail).toHaveBeenCalledTimes(1);
});
it("turns a malformed remote response into a persistent error", async () => {
mocks.requestCommitDetail.mockResolvedValue({ source: "github", success: false });
const { result } = renderHook(() => useCommitDetail(remoteTarget));
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.error).toBe("Unexpected response from the server");
expect(result.current.files).toBeNull();
expect(mocks.toast).toHaveBeenCalled();
});
it("does not refetch a GitHub target when local readiness changes", async () => {
mocks.requestCommitDetail.mockResolvedValue({
source: "github",
success: true,
files: remoteFiles,
});
const { result, rerender } = renderHook(() => useCommitDetail(remoteTarget));
await waitFor(() => expect(result.current.loading).toBe(false));
expect(mocks.requestCommitDetail).toHaveBeenCalledTimes(1);
mocks.state.sessionAgentctl.itemsBySessionId["session-1"].status = "starting";
rerender();
await waitFor(() => expect(result.current.loading).toBe(false));
expect(mocks.requestCommitDetail).toHaveBeenCalledTimes(1);
});
it("refetches the same target on demand", async () => {
mocks.requestCommitDetail.mockResolvedValue({ source: "github", success: false });
const { result } = renderHook(() => useCommitDetail(remoteTarget));
await waitFor(() => expect(result.current.loading).toBe(false));
await act(async () => {
await result.current.refetch();
});
expect(mocks.requestCommitDetail).toHaveBeenCalledTimes(2);
});
});
describe("useCommitDetail target switching", () => {
it("hides the previous detail while switching targets", async () => {
const first = deferred<{
source: "github";
success: true;
files: Record<string, FileInfo>;
commit: { sha: string; message: string };
}>();
const second = deferred<{
source: "github";
success: true;
files: Record<string, FileInfo>;
commit: { sha: string; message: string };
}>();
mocks.requestCommitDetail.mockImplementation(({ target }: { target: { sha: string } }) =>
target.sha === remoteTarget.sha ? first.promise : second.promise,
);
const { result, rerender } = renderHook(({ target }) => useCommitDetail(target), {
initialProps: { target: remoteTarget },
});
await waitFor(() => expect(mocks.requestCommitDetail).toHaveBeenCalledTimes(1));
await act(async () => {
first.resolve({
source: "github",
success: true,
files: remoteFiles,
commit: { sha: remoteTarget.sha, message: "first commit" },
});
await first.promise;
});
await waitFor(() => expect(result.current.commit?.message).toBe("first commit"));
rerender({ target: nextRemoteTarget });
expect(result.current.files).toBeNull();
expect(result.current.commit).toBeNull();
expect(result.current.error).toBeNull();
expect(result.current.loading).toBe(true);
});
});