Files
kandev/apps/web/components/task/changes-panel-focus.test.ts
T
2026-08-31 21:42:12 +00:00

637 lines
17 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import {
activateChangesPanel,
applyChangesPanelAutoFocusState,
markInactiveChangesIncreases,
migrateEnvironmentKeys,
selectChangesMarkerByEnvironment,
shouldClearPendingChangesFocus,
} from "./changes-panel-focus";
import type { FileInfo, GitStatusEntry } from "@/lib/state/slices/session-runtime/types";
import { RIGHT_TOP_GROUP } from "@/lib/state/layout-manager";
import { useDockviewStore } from "@/lib/state/dockview-store";
const hashDiffMock = vi.hoisted(() => vi.fn((diff: string) => `hash:${diff}`));
vi.mock("@/lib/utils/hash", () => ({
djb2Hash: hashDiffMock,
}));
const TEST_TIMESTAMP = "2026-06-29T00:00:00Z";
const INITIAL_FINGERPRINT = "repo:path:initial";
const UPDATED_FINGERPRINT = "repo:path:updated";
beforeEach(() => {
hashDiffMock.mockClear();
useDockviewStore.setState({
activeLayoutProfile: { kind: "built-in", id: "default" },
});
});
function activationApi(groupId: string, panelIds: string[]) {
const setActive = vi.fn();
const changesPanel = {
id: "changes",
group: { id: groupId, panels: panelIds.map((id) => ({ id })) },
api: { setActive },
};
const api = {
getPanel: vi.fn().mockReturnValue(changesPanel),
} as unknown as Parameters<typeof activateChangesPanel>[0];
return { api, setActive };
}
// @covers AC-UI-TASK-LAYOUT-PROFILES-001.11
describe("activateChangesPanel", () => {
it("activates Changes in the Default Files and Changes group", () => {
const { api, setActive } = activationApi(RIGHT_TOP_GROUP, ["files", "changes"]);
expect(activateChangesPanel(api)).toBe("activated");
expect(setActive).toHaveBeenCalledOnce();
});
it("does not activate Changes in a copied Default custom profile", () => {
useDockviewStore.setState({
activeLayoutProfile: { kind: "custom", id: "layout-copied-default" },
});
const { api, setActive } = activationApi(RIGHT_TOP_GROUP, ["files", "changes"]);
expect(activateChangesPanel(api)).toBe("blocked-layout");
expect(setActive).not.toHaveBeenCalled();
});
it.each([
["a non-Default group", "group-center", ["files", "changes"]],
["a Default group with an extra VS Code tab", RIGHT_TOP_GROUP, ["files", "changes", "vscode"]],
])("does not activate Changes in %s", (_name, groupId, panelIds) => {
const { api, setActive } = activationApi(groupId, panelIds);
expect(activateChangesPanel(api)).toBe("blocked-layout");
expect(setActive).not.toHaveBeenCalled();
});
});
function gitStatus(files: string[], timestamp = TEST_TIMESTAMP, diff = ""): GitStatusEntry {
return {
branch: "main",
remote_branch: null,
modified: [],
added: [],
deleted: [],
untracked: files,
renamed: [],
ahead: 0,
behind: 0,
files: Object.fromEntries(
files.map((path) => [path, { path, status: "untracked", staged: false, diff }]),
),
timestamp,
};
}
describe("selectChangesMarkerByEnvironment cache", () => {
it("reuses cached fingerprints for unchanged git status objects", () => {
const file: FileInfo = {
path: "one.ts",
status: "modified",
staged: false,
diff: "large diff body",
};
const status: GitStatusEntry = {
branch: "main",
remote_branch: null,
modified: ["one.ts"],
added: [],
deleted: [],
untracked: [],
renamed: [],
ahead: 0,
behind: 0,
files: { "one.ts": file },
timestamp: TEST_TIMESTAMP,
};
const state = {
gitStatus: {
byEnvironmentId: {},
byEnvironmentRepo: {
envA: {
repo1: status,
},
},
},
sessionCommits: {
loading: {},
refetchTrigger: {},
byEnvironmentId: {},
},
};
const baseMarker = selectChangesMarkerByEnvironment(state).envA;
const nextMarker = selectChangesMarkerByEnvironment(state).envA;
expect(nextMarker).toEqual(baseMarker);
expect(hashDiffMock).toHaveBeenCalledTimes(1);
});
});
describe("selectChangesMarkerByEnvironment", () => {
it("ignores timestamp-only git refreshes", () => {
const baseState = {
gitStatus: {
byEnvironmentId: {},
byEnvironmentRepo: {
envA: {
repo1: gitStatus(["one.ts"], "2026-06-29T00:00:00Z"),
},
},
},
sessionCommits: {
loading: {},
refetchTrigger: {},
byEnvironmentId: {},
},
};
const nextState = {
...baseState,
gitStatus: {
byEnvironmentId: {},
byEnvironmentRepo: {
envA: {
repo1: gitStatus(["one.ts"], "2026-06-29T00:01:00Z"),
},
},
},
};
const baseMarker = selectChangesMarkerByEnvironment(baseState).envA;
const nextMarker = selectChangesMarkerByEnvironment(nextState).envA;
expect(nextMarker.count).toBe(baseMarker.count);
expect(nextMarker.fingerprint).toBe(baseMarker.fingerprint);
});
it("changes fingerprint for diff-only git updates with the same count", () => {
const baseState = {
gitStatus: {
byEnvironmentId: {},
byEnvironmentRepo: {
envA: {
repo1: gitStatus(["one.ts"], TEST_TIMESTAMP, "old diff"),
},
},
},
sessionCommits: {
loading: {},
refetchTrigger: {},
byEnvironmentId: {},
},
};
const nextState = {
...baseState,
gitStatus: {
byEnvironmentId: {},
byEnvironmentRepo: {
envA: {
repo1: gitStatus(["one.ts"], TEST_TIMESTAMP, "new diff"),
},
},
},
};
const baseMarker = selectChangesMarkerByEnvironment(baseState).envA;
const nextMarker = selectChangesMarkerByEnvironment(nextState).envA;
expect(nextMarker.count).toBe(baseMarker.count);
expect(nextMarker.fingerprint).not.toBe(baseMarker.fingerprint);
});
});
function signal(count: number, fingerprint: string): string {
return `${count}\u0000${fingerprint}`;
}
describe("applyChangesPanelAutoFocusState fallback session keys", () => {
it("activates pending fallback session keys before the env id mapping arrives", () => {
const previousMarkers = {
"session-B": { count: 1, fingerprint: INITIAL_FINGERPRINT },
};
const pendingEnvKeys = new Set(["session-B"]);
let activateCalls = 0;
const previousActiveEnvKey = applyChangesPanelAutoFocusState({
signalsByEnv: {
"session-B": signal(1, INITIAL_FINGERPRINT),
},
activeEnvKey: "session-B",
previousActiveEnvKey: "envA",
environmentIdBySessionId: {},
previousMarkers,
pendingEnvKeys,
isRestoringLayout: false,
isMaximized: false,
activate: () => {
activateCalls += 1;
return "activated";
},
});
expect(previousActiveEnvKey).toBe("session-B");
expect(activateCalls).toBe(1);
expect(pendingEnvKeys.size).toBe(0);
});
it("does not queue updates for the active fallback session key", () => {
const previousMarkers = {
"session-A": { count: 1, fingerprint: INITIAL_FINGERPRINT },
};
const pendingEnvKeys = new Set<string>();
let activateCalls = 0;
const previousActiveEnvKey = applyChangesPanelAutoFocusState({
signalsByEnv: {
"session-A": signal(2, UPDATED_FINGERPRINT),
},
activeEnvKey: "session-A",
previousActiveEnvKey: "session-A",
environmentIdBySessionId: {},
previousMarkers,
pendingEnvKeys,
isRestoringLayout: false,
isMaximized: false,
activate: () => {
activateCalls += 1;
return "activated";
},
});
expect(previousActiveEnvKey).toBe("session-A");
expect(activateCalls).toBe(0);
expect(pendingEnvKeys.size).toBe(0);
expect(previousMarkers["session-A"]).toEqual({
count: 2,
fingerprint: UPDATED_FINGERPRINT,
});
});
});
describe("applyChangesPanelAutoFocusState returning environments", () => {
it("activates pending changes when returning from another environment", () => {
const previousMarkers = {
envB: { count: 1, fingerprint: "b1" },
};
const pendingEnvKeys = new Set(["envB"]);
let activateCalls = 0;
applyChangesPanelAutoFocusState({
signalsByEnv: {
envB: signal(1, "b1"),
},
activeEnvKey: "envB",
previousActiveEnvKey: "envA",
environmentIdBySessionId: {},
previousMarkers,
pendingEnvKeys,
isRestoringLayout: false,
isMaximized: false,
activate: () => {
activateCalls += 1;
return "activated";
},
});
expect(activateCalls).toBe(1);
expect(pendingEnvKeys.size).toBe(0);
});
it("keeps pending changes while maximized and activates them after exit", () => {
const previousMarkers = {
envB: { count: 1, fingerprint: "b1" },
};
const pendingEnvKeys = new Set(["envB"]);
let activateCalls = 0;
const baseArgs = {
signalsByEnv: {
envB: signal(1, "b1"),
},
activeEnvKey: "envB",
previousActiveEnvKey: "envA",
environmentIdBySessionId: {},
previousMarkers,
pendingEnvKeys,
isRestoringLayout: false,
};
applyChangesPanelAutoFocusState({
...baseArgs,
isMaximized: true,
activate: () => {
activateCalls += 1;
return "no-panel" as const;
},
});
const whileMaximized = {
activateCalls,
pendingEnvKeys: [...pendingEnvKeys],
};
applyChangesPanelAutoFocusState({
...baseArgs,
isMaximized: false,
activate: () => {
activateCalls += 1;
return "activated" as const;
},
});
expect({
whileMaximized,
afterExit: {
activateCalls,
pendingEnvKeys: [...pendingEnvKeys],
},
}).toEqual({
whileMaximized: {
activateCalls: 1,
pendingEnvKeys: ["envB"],
},
afterExit: {
activateCalls: 2,
pendingEnvKeys: [],
},
});
});
});
describe("applyChangesPanelAutoFocusState", () => {
it("migrates keys before queuing, defers during restore, and clears after activation", () => {
const previousMarkers = {};
const pendingEnvKeys = new Set<string>();
let previousActiveEnvKey: string | null = "envA";
let activateCalls = 0;
previousActiveEnvKey = applyChangesPanelAutoFocusState({
signalsByEnv: {
"session-B": signal(1, INITIAL_FINGERPRINT),
},
activeEnvKey: "envA",
previousActiveEnvKey,
environmentIdBySessionId: {},
previousMarkers,
pendingEnvKeys,
isRestoringLayout: false,
isMaximized: false,
activate: () => {
activateCalls += 1;
return "activated";
},
});
expect(pendingEnvKeys.size).toBe(0);
previousActiveEnvKey = applyChangesPanelAutoFocusState({
signalsByEnv: {
envB: signal(12, UPDATED_FINGERPRINT),
},
activeEnvKey: "envA",
previousActiveEnvKey,
environmentIdBySessionId: { "session-B": "envB" },
previousMarkers,
pendingEnvKeys,
isRestoringLayout: false,
isMaximized: false,
activate: () => {
activateCalls += 1;
return "activated";
},
});
expect([...pendingEnvKeys]).toEqual(["envB"]);
expect(previousMarkers).toEqual({
envB: { count: 12, fingerprint: UPDATED_FINGERPRINT },
});
previousActiveEnvKey = applyChangesPanelAutoFocusState({
signalsByEnv: {
envB: signal(12, UPDATED_FINGERPRINT),
},
activeEnvKey: "envB",
previousActiveEnvKey,
environmentIdBySessionId: { "session-B": "envB" },
previousMarkers,
pendingEnvKeys,
isRestoringLayout: true,
isMaximized: false,
activate: () => {
activateCalls += 1;
return "activated";
},
});
expect(activateCalls).toBe(0);
expect([...pendingEnvKeys]).toEqual(["envB"]);
previousActiveEnvKey = applyChangesPanelAutoFocusState({
signalsByEnv: {
envB: signal(12, UPDATED_FINGERPRINT),
},
activeEnvKey: "envB",
previousActiveEnvKey,
environmentIdBySessionId: { "session-B": "envB" },
previousMarkers,
pendingEnvKeys,
isRestoringLayout: false,
isMaximized: false,
activate: () => {
activateCalls += 1;
return "activated";
},
});
expect(previousActiveEnvKey).toBe("envB");
expect(activateCalls).toBe(1);
expect(pendingEnvKeys.size).toBe(0);
});
});
describe("markInactiveChangesIncreases", () => {
it("baselines first observations and queues only inactive environment increases", () => {
const previousMarkers = {};
const pendingEnvKeys = new Set<string>();
markInactiveChangesIncreases({
markersByEnv: {
envA: { count: 1, fingerprint: "a1" },
envB: { count: 0, fingerprint: "b0" },
},
activeEnvKey: "envA",
previousActiveEnvKey: "envA",
previousMarkers,
pendingEnvKeys,
});
expect(pendingEnvKeys.size).toBe(0);
markInactiveChangesIncreases({
markersByEnv: {
envA: { count: 2, fingerprint: "a2" },
envB: { count: 1, fingerprint: "b1" },
},
activeEnvKey: "envA",
previousActiveEnvKey: "envA",
previousMarkers,
pendingEnvKeys,
});
expect([...pendingEnvKeys]).toEqual(["envB"]);
expect(previousMarkers).toEqual({
envA: { count: 2, fingerprint: "a2" },
envB: { count: 1, fingerprint: "b1" },
});
});
it("queues a same-batch update for the previously inactive environment", () => {
const previousMarkers = {
envB: { count: 0, fingerprint: "b0" },
};
const pendingEnvKeys = new Set<string>();
markInactiveChangesIncreases({
markersByEnv: {
envB: { count: 1, fingerprint: "b1" },
},
activeEnvKey: "envB",
previousActiveEnvKey: "envA",
previousMarkers,
pendingEnvKeys,
});
expect([...pendingEnvKeys]).toEqual(["envB"]);
});
it("queues count-neutral meaningful updates for inactive environments", () => {
const previousMarkers = {
envB: { count: 1, fingerprint: "b1" },
};
const pendingEnvKeys = new Set<string>();
markInactiveChangesIncreases({
markersByEnv: {
envB: { count: 1, fingerprint: "b2" },
},
activeEnvKey: "envA",
previousActiveEnvKey: "envA",
previousMarkers,
pendingEnvKeys,
});
expect([...pendingEnvKeys]).toEqual(["envB"]);
});
});
describe("markInactiveChangesIncreases pending cleanup", () => {
it("queues first observed inactive changes after bootstrap", () => {
const previousMarkers = {
envA: { count: 0, fingerprint: "a0" },
};
const pendingEnvKeys = new Set<string>();
markInactiveChangesIncreases({
markersByEnv: {
envA: { count: 0, fingerprint: "a0" },
envB: { count: 1, fingerprint: "b1" },
},
activeEnvKey: "envA",
previousActiveEnvKey: "envA",
previousMarkers,
pendingEnvKeys,
queueFirstObservedInactiveChanges: true,
});
expect([...pendingEnvKeys]).toEqual(["envB"]);
});
it("clears pending focus when changes disappear or the environment disappears", () => {
const previousMarkers = {
envB: { count: 1, fingerprint: "b1" },
envC: { count: 1, fingerprint: "c1" },
};
const pendingEnvKeys = new Set(["envB", "envC"]);
markInactiveChangesIncreases({
markersByEnv: {
envB: { count: 0, fingerprint: "b0" },
},
activeEnvKey: "envA",
previousActiveEnvKey: "envA",
previousMarkers,
pendingEnvKeys,
queueFirstObservedInactiveChanges: true,
});
expect(pendingEnvKeys.size).toBe(0);
expect(previousMarkers).toEqual({
envB: { count: 0, fingerprint: "b0" },
});
});
});
describe("applyChangesPanelAutoFocusState restore races", () => {
it("keeps pending focus when a stale restore flag produces no panel", () => {
const previousMarkers = {
envB: { count: 1, fingerprint: "b1" },
};
const pendingEnvKeys = new Set(["envB"]);
let restoring = false;
applyChangesPanelAutoFocusState({
signalsByEnv: {
envB: signal(1, "b1"),
},
activeEnvKey: "envB",
previousActiveEnvKey: "envA",
environmentIdBySessionId: {},
previousMarkers,
pendingEnvKeys,
isRestoringLayout: false,
isMaximized: false,
getIsRestoringLayout: () => restoring,
activate: () => {
restoring = true;
return "no-panel";
},
});
expect([...pendingEnvKeys]).toEqual(["envB"]);
});
});
describe("migrateEnvironmentKeys", () => {
it("migrates pending and previous fallback session keys to environment keys", () => {
const previousMarkers = {
"session-B": { count: 1, fingerprint: "b1" },
};
const pendingEnvKeys = new Set(["session-B"]);
migrateEnvironmentKeys({
environmentIdBySessionId: { "session-B": "envB" },
previousMarkers,
pendingEnvKeys,
});
expect([...pendingEnvKeys]).toEqual(["envB"]);
expect(previousMarkers).toEqual({
envB: { count: 1, fingerprint: "b1" },
});
});
});
describe("shouldClearPendingChangesFocus", () => {
it("keeps retryable activation results pending", () => {
expect(shouldClearPendingChangesFocus("activated")).toBe(true);
expect(shouldClearPendingChangesFocus("no-panel")).toBe(true);
expect(shouldClearPendingChangesFocus("blocked-agent-group")).toBe(false);
expect(shouldClearPendingChangesFocus("blocked-layout")).toBe(false);
expect(shouldClearPendingChangesFocus("no-api")).toBe(false);
});
});