Files
kandev/apps/web/lib/state/task-removal.test.ts

114 lines
3.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
advanceTaskNavigationRevision,
beginTaskRemoval,
createTaskRemovalState,
ownsTaskRemovalDeparture,
recordTaskRemovalResult,
releaseTaskRemoval,
taskRemovalCoversTask,
taskRemovalOwnsDepartureForTask,
} from "./task-removal";
const REMOVAL_ONE = "removal-1";
const WORKSPACE_ONE = "workspace-1";
const TASK_ONE = "task-1";
const TASK_TWO = "task-2";
const PARENT_TASK = "task-parent";
const CHILD_TASK = "task-child";
describe("task removal state", () => {
it("captures the complete removal set and rejects overlapping pending work", () => {
const initial = createTaskRemovalState();
const started = beginTaskRemoval(initial, {
token: REMOVAL_ONE,
action: "archive",
workspaceId: WORKSPACE_ONE,
taskIds: [PARENT_TASK, CHILD_TASK],
requestIds: [PARENT_TASK],
departure: {
taskId: CHILD_TASK,
sessionId: "session-child",
navigationRevision: 0,
origin: "detail",
},
});
expect(started).not.toBeNull();
expect(taskRemovalCoversTask(started!, CHILD_TASK)).toBe(true);
expect(started!.operationsByToken[REMOVAL_ONE].taskIds).toEqual(["task-parent", "task-child"]);
expect(
beginTaskRemoval(started!, {
token: "removal-2",
action: "delete",
workspaceId: WORKSPACE_ONE,
taskIds: [CHILD_TASK],
requestIds: [CHILD_TASK],
departure: null,
}),
).toBeNull();
});
it("invalidates automatic navigation after leaving and returning to a task", () => {
let state = beginTaskRemoval(createTaskRemovalState(), {
token: REMOVAL_ONE,
action: "delete",
workspaceId: WORKSPACE_ONE,
taskIds: [TASK_ONE],
requestIds: [TASK_ONE],
departure: { taskId: TASK_ONE, sessionId: null, navigationRevision: 0, origin: "detail" },
})!;
expect(ownsTaskRemovalDeparture(state, REMOVAL_ONE, 0)).toBe(true);
state = advanceTaskNavigationRevision(state);
state = advanceTaskNavigationRevision(state);
expect(state.navigationRevision).toBe(2);
expect(ownsTaskRemovalDeparture(state, REMOVAL_ONE, 0)).toBe(false);
expect(ownsTaskRemovalDeparture(state, REMOVAL_ONE, 2)).toBe(false);
});
it("keeps per-request outcomes until the coordinator releases the operation", () => {
let state = beginTaskRemoval(createTaskRemovalState(), {
token: REMOVAL_ONE,
action: "delete",
workspaceId: WORKSPACE_ONE,
taskIds: [TASK_ONE, TASK_TWO],
requestIds: [TASK_ONE, TASK_TWO],
departure: null,
})!;
state = recordTaskRemovalResult(state, REMOVAL_ONE, [TASK_ONE], "succeeded");
expect(state.operationsByToken[REMOVAL_ONE].outcomesByTaskId).toEqual({
[TASK_ONE]: "succeeded",
[TASK_TWO]: "pending",
});
expect(taskRemovalCoversTask(state, TASK_TWO)).toBe(true);
state = recordTaskRemovalResult(state, REMOVAL_ONE, [TASK_TWO], "failed");
expect(taskRemovalCoversTask(state, TASK_TWO)).toBe(true);
state = releaseTaskRemoval(state, REMOVAL_ONE);
expect(taskRemovalCoversTask(state, TASK_ONE)).toBe(false);
expect(taskRemovalCoversTask(state, TASK_TWO)).toBe(false);
});
it("recognizes local departure ownership for every task in a cascade", () => {
const state = beginTaskRemoval(createTaskRemovalState(), {
token: REMOVAL_ONE,
action: "archive",
workspaceId: "workspace-1",
taskIds: [PARENT_TASK, CHILD_TASK],
requestIds: [PARENT_TASK],
departure: {
taskId: PARENT_TASK,
sessionId: null,
navigationRevision: 0,
origin: "detail",
},
})!;
expect(taskRemovalOwnsDepartureForTask(state, PARENT_TASK)).toBe(true);
expect(taskRemovalOwnsDepartureForTask(state, CHILD_TASK)).toBe(true);
expect(taskRemovalOwnsDepartureForTask(undefined, PARENT_TASK)).toBe(false);
});
});