- Server /tasks/:id/diff and /tasks/:id/file-diffs now both use
resolveDiffBase() for consistent file lists across card and modal
- Both endpoints handle done tasks via mergeDetails.commitSha,
returning structured { files, stats } from the merge commit
- TaskChangesTab uses fetchTaskDiff for all columns (no more
fetchCommitDiff/parsePatch client-side path for done tasks)
- useChangedFiles hook simplified — server handles done tasks
- Done task cards show "N files changed" button using mergeDetails
- Updated tests for new unified data flow
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
134 lines
4.7 KiB
TypeScript
134 lines
4.7 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { renderHook, waitFor, act } from "@testing-library/react";
|
|
import { useChangedFiles } from "../useChangedFiles";
|
|
import * as api from "../../api";
|
|
|
|
vi.mock("../../api", () => ({
|
|
fetchTaskFileDiffs: vi.fn(),
|
|
}));
|
|
|
|
const mockFetchTaskFileDiffs = vi.mocked(api.fetchTaskFileDiffs);
|
|
|
|
describe("useChangedFiles", () => {
|
|
beforeEach(() => {
|
|
mockFetchTaskFileDiffs.mockReset();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("fetches changed files for active tasks with a worktree", async () => {
|
|
mockFetchTaskFileDiffs.mockResolvedValueOnce([
|
|
{ path: "src/a.ts", status: "modified", diff: "diff --git a/src/a.ts b/src/a.ts" },
|
|
{ path: "src/b.ts", status: "added", diff: "diff --git a/src/b.ts b/src/b.ts" },
|
|
]);
|
|
|
|
const { result } = renderHook(() => useChangedFiles("KB-651", "/repo/.worktrees/kb-651", "in-progress"));
|
|
|
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
|
|
expect(result.current.error).toBeNull();
|
|
expect(result.current.files).toHaveLength(2);
|
|
// Hook no longer auto-selects; component handles selection
|
|
expect(result.current.selectedFile).toBeNull();
|
|
expect(mockFetchTaskFileDiffs).toHaveBeenCalledWith("KB-651", undefined);
|
|
});
|
|
|
|
it("does not fetch for tasks in inactive columns", async () => {
|
|
const { result: triage } = renderHook(() => useChangedFiles("KB-651", "/repo/.worktrees/kb-651", "triage"));
|
|
const { result: todo } = renderHook(() => useChangedFiles("KB-651", "/repo/.worktrees/kb-651", "todo"));
|
|
|
|
await waitFor(() => expect(triage.current.loading).toBe(false));
|
|
await waitFor(() => expect(todo.current.loading).toBe(false));
|
|
|
|
expect(triage.current.files).toEqual([]);
|
|
expect(todo.current.files).toEqual([]);
|
|
expect(mockFetchTaskFileDiffs).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("returns an error state on fetch failure", async () => {
|
|
mockFetchTaskFileDiffs.mockRejectedValueOnce(new Error("boom"));
|
|
|
|
const { result } = renderHook(() => useChangedFiles("KB-651", "/repo/.worktrees/kb-651", "in-review"));
|
|
|
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
|
|
expect(result.current.files).toEqual([]);
|
|
expect(result.current.selectedFile).toBeNull();
|
|
expect(result.current.error).toBe("boom");
|
|
});
|
|
|
|
it("allows selecting a different file after data loads", async () => {
|
|
mockFetchTaskFileDiffs.mockResolvedValueOnce([
|
|
{ path: "src/a.ts", status: "modified", diff: "first" },
|
|
{ path: "src/b.ts", status: "added", diff: "second" },
|
|
]);
|
|
|
|
const { result } = renderHook(() => useChangedFiles("KB-651", "/repo/.worktrees/kb-651", "in-progress"));
|
|
|
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
|
|
act(() => {
|
|
result.current.setSelectedFile(result.current.files[1]!);
|
|
});
|
|
|
|
expect(result.current.selectedFile?.path).toBe("src/b.ts");
|
|
});
|
|
|
|
it("preserves selection when refetching finds a matching file", async () => {
|
|
const fileA = { path: "src/a.ts", status: "modified" as const, diff: "first" };
|
|
const fileB = { path: "src/b.ts", status: "added" as const, diff: "second" };
|
|
|
|
mockFetchTaskFileDiffs.mockResolvedValueOnce([fileA, fileB]);
|
|
|
|
const { result } = renderHook(() => useChangedFiles("KB-651", "/repo/.worktrees/kb-651", "in-progress"));
|
|
|
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
|
|
// Manually select file B
|
|
act(() => {
|
|
result.current.setSelectedFile(fileB);
|
|
});
|
|
|
|
expect(result.current.selectedFile?.path).toBe("src/b.ts");
|
|
});
|
|
|
|
it("provides resetSelection that clears selectedFile", async () => {
|
|
mockFetchTaskFileDiffs.mockResolvedValueOnce([
|
|
{ path: "src/a.ts", status: "modified", diff: "first" },
|
|
]);
|
|
|
|
const { result } = renderHook(() => useChangedFiles("KB-651", "/repo/.worktrees/kb-651", "in-progress"));
|
|
|
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
|
|
// Select a file
|
|
act(() => {
|
|
result.current.setSelectedFile(result.current.files[0]!);
|
|
});
|
|
|
|
expect(result.current.selectedFile?.path).toBe("src/a.ts");
|
|
|
|
// Reset selection
|
|
act(() => {
|
|
result.current.resetSelection();
|
|
});
|
|
|
|
expect(result.current.selectedFile).toBeNull();
|
|
});
|
|
|
|
it("fetches changed files for done tasks", async () => {
|
|
mockFetchTaskFileDiffs.mockResolvedValueOnce([
|
|
{ path: "src/a.ts", status: "modified", diff: "diff" },
|
|
]);
|
|
|
|
const { result } = renderHook(() => useChangedFiles("KB-651", undefined, "done"));
|
|
|
|
await waitFor(() => expect(result.current.loading).toBe(false));
|
|
|
|
expect(result.current.files).toHaveLength(1);
|
|
expect(mockFetchTaskFileDiffs).toHaveBeenCalledWith("KB-651", undefined);
|
|
});
|
|
});
|