feat(FN-4521): complete Step 3 — unify active diff data source
Fusion-Task-Id: FN-4521 Fusion-Task-Lineage: 675472a2-480d-4b9e-8861-98d2d82680db
This commit is contained in:
@@ -1301,13 +1301,10 @@ function TaskCardComponent({
|
||||
if (task.column === "in-progress") {
|
||||
const activeDiffCount = diffStats?.filesChanged;
|
||||
const fallbackCount =
|
||||
activeDiffCount == null || activeDiffCount === 0
|
||||
activeDiffCount == null
|
||||
? task.modifiedFiles?.length
|
||||
: undefined;
|
||||
const displayCount =
|
||||
activeDiffCount != null && activeDiffCount > 0
|
||||
? activeDiffCount
|
||||
: fallbackCount;
|
||||
const displayCount = activeDiffCount ?? fallbackCount;
|
||||
if (displayCount == null || displayCount === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -1328,13 +1325,10 @@ function TaskCardComponent({
|
||||
if (task.column === "in-review") {
|
||||
const reviewDiffCount = diffStats?.filesChanged;
|
||||
const fallbackCount =
|
||||
reviewDiffCount == null || reviewDiffCount === 0
|
||||
reviewDiffCount == null
|
||||
? task.modifiedFiles?.length
|
||||
: undefined;
|
||||
const displayCount =
|
||||
reviewDiffCount != null && reviewDiffCount > 0
|
||||
? reviewDiffCount
|
||||
: fallbackCount;
|
||||
const displayCount = reviewDiffCount ?? fallbackCount;
|
||||
if (displayCount == null || displayCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -231,8 +231,8 @@ export function TaskChangesTab({ taskId, worktree, projectId, column, mergeDetai
|
||||
);
|
||||
}
|
||||
|
||||
// Non-done task without a worktree → show worktree empty state
|
||||
if (!isDone && !worktree) {
|
||||
// Non-done task without a worktree → only show fallback state when branch-fallback diff is empty.
|
||||
if (!isDone && !worktree && files.length === 0) {
|
||||
if (modifiedFiles && modifiedFiles.length > 0) {
|
||||
return renderModifiedFilesFallback(modifiedFiles, false);
|
||||
}
|
||||
|
||||
@@ -116,6 +116,28 @@ describe("TaskChangesTab — worktree-backed (non-done tasks)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("renders fetched file rows for active task without worktree when branch fallback has files", async () => {
|
||||
mockFetchTaskDiff.mockResolvedValue({
|
||||
files: [
|
||||
{ path: "a.ts", status: "modified", additions: 1, deletions: 0, patch: "@@ -1 +1,2 @@" },
|
||||
],
|
||||
stats: { filesChanged: 1, additions: 1, deletions: 0 },
|
||||
});
|
||||
|
||||
render(
|
||||
<TaskChangesTab
|
||||
taskId="FN-001"
|
||||
worktree={undefined}
|
||||
column="in-progress"
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("a.ts")).toBeTruthy();
|
||||
});
|
||||
expect(screen.queryByText("No worktree available for this task.")).toBeNull();
|
||||
});
|
||||
|
||||
it("shows modifiedFiles fallback when an active task has no worktree diff", async () => {
|
||||
mockFetchTaskDiff.mockResolvedValue({ files: [], stats: { filesChanged: 0, additions: 0, deletions: 0 } });
|
||||
|
||||
|
||||
@@ -53,25 +53,25 @@ describe("useTaskDiffStats", () => {
|
||||
expect(mockFetchTaskDiff).toHaveBeenCalledWith("FN-123", undefined, "proj-1");
|
||||
});
|
||||
|
||||
it("does not fetch for active columns without a worktree", async () => {
|
||||
it("fetches for active columns without a worktree", async () => {
|
||||
mockFetchTaskDiff.mockResolvedValue({
|
||||
files: [],
|
||||
stats: { filesChanged: 2, additions: 4, deletions: 1 },
|
||||
});
|
||||
|
||||
const { result: inProgress } = renderHook(() =>
|
||||
useTaskDiffStats("FN-123", "in-progress", "abc1234", undefined),
|
||||
);
|
||||
const { result: todo } = renderHook(() =>
|
||||
useTaskDiffStats("FN-123", "todo", "abc1234", undefined),
|
||||
);
|
||||
const { result: inReview } = renderHook(() =>
|
||||
useTaskDiffStats("FN-123", "in-review", "abc1234", undefined),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(inProgress.current.loading).toBe(false));
|
||||
await waitFor(() => expect(todo.current.loading).toBe(false));
|
||||
await waitFor(() => expect(inReview.current.loading).toBe(false));
|
||||
|
||||
expect(inProgress.current.stats).toBeNull();
|
||||
expect(todo.current.stats).toBeNull();
|
||||
expect(inReview.current.stats).toBeNull();
|
||||
expect(mockFetchTaskDiff).not.toHaveBeenCalled();
|
||||
expect(inProgress.current.stats).toEqual({ filesChanged: 2, additions: 4, deletions: 1 });
|
||||
expect(inReview.current.stats).toEqual({ filesChanged: 2, additions: 4, deletions: 1 });
|
||||
expect(mockFetchTaskDiff).toHaveBeenCalledWith("FN-123", undefined, undefined);
|
||||
});
|
||||
|
||||
it("fetches diff stats for active tasks with a worktree", async () => {
|
||||
@@ -169,6 +169,16 @@ describe("useTaskDiffStats", () => {
|
||||
expect(mockFetchTaskDiff).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("does not fetch for inactive columns", async () => {
|
||||
const { result: todo } = renderHook(() =>
|
||||
useTaskDiffStats("FN-123", "todo", "abc1234", undefined),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(todo.current.loading).toBe(false));
|
||||
expect(todo.current.stats).toBeNull();
|
||||
expect(mockFetchTaskDiff).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resets stats when column changes from done to non-done", async () => {
|
||||
mockFetchTaskDiff.mockResolvedValueOnce({
|
||||
files: [],
|
||||
|
||||
@@ -31,12 +31,12 @@ interface UseTaskDiffStatsOptions {
|
||||
const diffStatsCache = new Map<string, { stats: DiffStats; expiresAt: number }>();
|
||||
const CACHE_TTL_MS = 30_000; // 30 seconds
|
||||
|
||||
function getCacheKey(taskId: string, projectId?: string, worktree?: string, stepVersion?: string): string {
|
||||
return `${taskId}:${projectId ?? ""}:${worktree ?? ""}:${stepVersion ?? ""}`;
|
||||
function getCacheKey(taskId: string, projectId?: string, worktree?: string, stepVersion?: string, mode?: "done" | "active"): string {
|
||||
return `${taskId}:${projectId ?? ""}:${worktree ?? ""}:${stepVersion ?? ""}:${mode ?? ""}`;
|
||||
}
|
||||
|
||||
function getCachedStats(taskId: string, projectId?: string, worktree?: string, stepVersion?: string): DiffStats | null {
|
||||
const key = getCacheKey(taskId, projectId, worktree, stepVersion);
|
||||
function getCachedStats(taskId: string, projectId?: string, worktree?: string, stepVersion?: string, mode?: "done" | "active"): DiffStats | null {
|
||||
const key = getCacheKey(taskId, projectId, worktree, stepVersion, mode);
|
||||
const entry = diffStatsCache.get(key);
|
||||
|
||||
if (!entry) return null;
|
||||
@@ -50,8 +50,8 @@ function getCachedStats(taskId: string, projectId?: string, worktree?: string, s
|
||||
return entry.stats;
|
||||
}
|
||||
|
||||
function setCachedStats(taskId: string, projectId: string | undefined, worktree: string | undefined, stepVersion: string | undefined, stats: DiffStats): void {
|
||||
const key = getCacheKey(taskId, projectId, worktree, stepVersion);
|
||||
function setCachedStats(taskId: string, projectId: string | undefined, worktree: string | undefined, stepVersion: string | undefined, mode: "done" | "active", stats: DiffStats): void {
|
||||
const key = getCacheKey(taskId, projectId, worktree, stepVersion, mode);
|
||||
diffStatsCache.set(key, {
|
||||
stats,
|
||||
expiresAt: Date.now() + CACHE_TTL_MS,
|
||||
@@ -103,7 +103,7 @@ export function useTaskDiffStats(
|
||||
}
|
||||
|
||||
const shouldFetchDoneTask = column === "done";
|
||||
const shouldFetchActiveTask = (column === "in-progress" || column === "in-review") && Boolean(worktree);
|
||||
const shouldFetchActiveTask = column === "in-progress" || column === "in-review";
|
||||
|
||||
if (!taskId || (!shouldFetchDoneTask && !shouldFetchActiveTask)) {
|
||||
setStats(null);
|
||||
@@ -113,12 +113,13 @@ export function useTaskDiffStats(
|
||||
|
||||
const activeWorktree = shouldFetchActiveTask ? worktree : undefined;
|
||||
const stepVersionStr = stepVersion !== undefined ? String(stepVersion) : undefined;
|
||||
const mode: "done" | "active" = shouldFetchDoneTask ? "done" : "active";
|
||||
let cancelled = false;
|
||||
|
||||
async function load(forceRefresh = false) {
|
||||
// Check cache first - return immediately without loading flicker (unless force refresh)
|
||||
if (!forceRefresh) {
|
||||
const cached = getCachedStats(taskId, projectId, activeWorktree, stepVersionStr);
|
||||
const cached = getCachedStats(taskId, projectId, activeWorktree, stepVersionStr, mode);
|
||||
if (cached) {
|
||||
if (!cancelled) {
|
||||
setStats(cached);
|
||||
@@ -134,7 +135,7 @@ export function useTaskDiffStats(
|
||||
if (!cancelled) {
|
||||
setStats(data.stats);
|
||||
// Store in cache
|
||||
setCachedStats(taskId, projectId, activeWorktree, stepVersionStr, data.stats);
|
||||
setCachedStats(taskId, projectId, activeWorktree, stepVersionStr, mode, data.stats);
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
|
||||
Reference in New Issue
Block a user