Files
kandev/apps/web/lib/state/workflow-session-focus.test.ts
2026-09-19 21:24:01 +01:00

491 lines
18 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { createAppStore } from "./store";
import {
acknowledgeWorkflowSessionFocus,
bindWorkflowSessionFocus,
cancelWorkflowSessionFocus,
createWorkflowSessionFocusState,
beginWorkflowSessionFocus,
reconcileWorkflowSessionFocus,
} from "./workflow-session-focus";
const START = {
taskId: "task-1",
workflowId: "workflow-1",
destinationStepId: "step-implement",
presentationToken: 7,
navigationRevision: 3,
};
const SOURCE_SESSION_ID = "session-astra";
const DESTINATION_SESSION_ID = "session-luna";
const NEWER_SESSION_ID = "session-newer";
const LATER_STEP_ID = "step-review";
const LIVE_UPDATED_AT = "2026-09-19T10:00:02Z";
const RESPONSE_UPDATED_AT = "2026-09-19T10:00:01Z";
const FOCUS_INTENT_ERROR = "test focus intent did not start";
const COMMITTED_ROUTE = {
operation_id: "operation-1",
destination_step_id: "step-implement",
entry_identity: "entry:00000000000000000042",
destination_session_id: DESTINATION_SESSION_ID,
phase: "committed",
};
function started(): {
state: ReturnType<typeof createWorkflowSessionFocusState>;
requestId: number;
} {
const result = beginWorkflowSessionFocus(createWorkflowSessionFocusState(), START);
if (result.requestId === null) throw new Error(FOCUS_INTENT_ERROR);
return { state: result.state, requestId: result.requestId };
}
// eslint-disable-next-line max-lines-per-function -- state-machine scenarios share one transition fixture
describe("workflow session focus state", () => {
it("applies the resolved recipient atomically in the app store and clears the old pin", () => {
const store = createAppStore();
store.setState((state) => {
state.tasks.activeTaskId = START.taskId;
state.tasks.activeSessionId = SOURCE_SESSION_ID;
state.tasks.pinnedSessionId = SOURCE_SESSION_ID;
state.taskRemoval.navigationRevision = START.navigationRevision;
state.kanban.tasks = [
{
id: START.taskId,
workflowStepId: START.destinationStepId,
metadata: { workflow_session_route: COMMITTED_ROUTE },
},
] as never;
state.taskSessionsByTask.itemsByTaskId[START.taskId] = [
{ id: SOURCE_SESSION_ID },
{ id: DESTINATION_SESSION_ID },
] as never;
state.taskSessions.items[DESTINATION_SESSION_ID] = {
id: DESTINATION_SESSION_ID,
task_id: START.taskId,
} as never;
});
const requestId = store.getState().beginWorkflowSessionFocus(START);
expect(requestId).toBe(1);
if (requestId === null) throw new Error(FOCUS_INTENT_ERROR);
store.getState().bindWorkflowSessionFocus({
requestId,
presentationToken: START.presentationToken,
entryIdentity: COMMITTED_ROUTE.entry_identity,
});
store.getState().reconcileWorkflowSessionFocus(START.taskId);
expect(store.getState().tasks.activeSessionId).toBe(DESTINATION_SESSION_ID);
expect(store.getState().tasks.pinnedSessionId).toBeNull();
expect(store.getState().tasks.lastSessionByTaskId[START.taskId]).toBe(DESTINATION_SESSION_ID);
expect(store.getState().workflowSessionFocus.request?.sessionId).toBe(DESTINATION_SESSION_ID);
store.getState().setActiveSession(START.taskId, SOURCE_SESSION_ID);
expect(store.getState().workflowSessionFocus.request).toBeNull();
});
it("reconsiders the cached live route when a move response is delayed", () => {
const store = createAppStore();
store.setState((state) => {
state.tasks.activeTaskId = START.taskId;
state.tasks.activeSessionId = SOURCE_SESSION_ID;
state.taskRemoval.navigationRevision = START.navigationRevision;
state.kanban.tasks = [
{
id: START.taskId,
workflowStepId: START.destinationStepId,
updatedAt: LIVE_UPDATED_AT,
metadata: { workflow_session_route: COMMITTED_ROUTE },
},
] as never;
state.taskSessionsByTask.itemsByTaskId[START.taskId] = [
{ id: SOURCE_SESSION_ID },
{ id: DESTINATION_SESSION_ID },
] as never;
});
const requestId = store.getState().beginWorkflowSessionFocus(START);
if (requestId === null) throw new Error(FOCUS_INTENT_ERROR);
store.getState().bindWorkflowSessionFocus({
requestId,
presentationToken: START.presentationToken,
entryIdentity: COMMITTED_ROUTE.entry_identity,
});
store.getState().reconcileWorkflowSessionFocus(START.taskId, {
metadata: { workflow_session_route: { ...COMMITTED_ROUTE, phase: "prepared" } },
updatedAt: RESPONSE_UPDATED_AT,
entryIdentity: COMMITTED_ROUTE.entry_identity,
});
expect(store.getState().tasks.activeSessionId).toBe(DESTINATION_SESSION_ID);
expect(store.getState().workflowSessionFocus.request?.sessionId).toBe(DESTINATION_SESSION_ID);
});
it("accepts the response projection when the live task is still on the source step", () => {
const startedState = started();
const bound = bindWorkflowSessionFocus(startedState.state, {
requestId: startedState.requestId,
presentationToken: START.presentationToken,
entryIdentity: COMMITTED_ROUTE.entry_identity,
});
const result = reconcileWorkflowSessionFocus(bound, {
activeTaskId: START.taskId,
navigationRevision: START.navigationRevision,
workflowStepId: "step-plan",
routeMetadata: undefined,
routeUpdatedAt: "2026-09-19T10:00:00Z",
responseProjection: {
metadata: { workflow_session_route: COMMITTED_ROUTE },
updatedAt: RESPONSE_UPDATED_AT,
workflowStepId: START.destinationStepId,
entryIdentity: COMMITTED_ROUTE.entry_identity,
},
knownSessionIds: [DESTINATION_SESSION_ID],
});
expect(result.sessionId).toBe(DESTINATION_SESSION_ID);
});
it("keeps an intent pending when its response route is older than a newer live entry", () => {
const store = createAppStore();
const newerLiveRoute = {
...COMMITTED_ROUTE,
entry_identity: "entry:00000000000000000043",
destination_session_id: NEWER_SESSION_ID,
};
store.setState((state) => {
state.tasks.activeTaskId = START.taskId;
state.tasks.activeSessionId = SOURCE_SESSION_ID;
state.taskRemoval.navigationRevision = START.navigationRevision;
state.kanban.tasks = [
{
id: START.taskId,
workflowStepId: START.destinationStepId,
updatedAt: LIVE_UPDATED_AT,
metadata: { workflow_session_route: newerLiveRoute },
},
] as never;
state.taskSessionsByTask.itemsByTaskId[START.taskId] = [
{ id: SOURCE_SESSION_ID },
{ id: DESTINATION_SESSION_ID },
{ id: NEWER_SESSION_ID },
] as never;
});
const requestId = store.getState().beginWorkflowSessionFocus(START);
if (requestId === null) throw new Error(FOCUS_INTENT_ERROR);
store.getState().bindWorkflowSessionFocus({
requestId,
presentationToken: START.presentationToken,
entryIdentity: COMMITTED_ROUTE.entry_identity,
});
store.getState().reconcileWorkflowSessionFocus(START.taskId, {
metadata: { workflow_session_route: COMMITTED_ROUTE },
updatedAt: RESPONSE_UPDATED_AT,
entryIdentity: COMMITTED_ROUTE.entry_identity,
});
expect(store.getState().tasks.activeSessionId).toBe(SOURCE_SESSION_ID);
expect(store.getState().workflowSessionFocus.intent).not.toBeNull();
expect(store.getState().workflowSessionFocus.request).toBeNull();
});
it("binds a response entry and resolves it only for the committed task-owned recipient", () => {
const startedState = started();
expect(startedState.requestId).toBe(1);
const bound = bindWorkflowSessionFocus(startedState.state, {
requestId: startedState.requestId,
presentationToken: START.presentationToken,
entryIdentity: COMMITTED_ROUTE.entry_identity,
});
const unresolved = reconcileWorkflowSessionFocus(bound, {
activeTaskId: START.taskId,
navigationRevision: START.navigationRevision,
workflowStepId: START.destinationStepId,
routeMetadata: { workflow_session_route: COMMITTED_ROUTE },
knownSessionIds: [SOURCE_SESSION_ID],
});
expect(unresolved.sessionId).toBeNull();
expect(unresolved.state.intent).not.toBeNull();
const resolved = reconcileWorkflowSessionFocus(bound, {
activeTaskId: START.taskId,
navigationRevision: START.navigationRevision,
workflowStepId: START.destinationStepId,
routeMetadata: { workflow_session_route: COMMITTED_ROUTE },
knownSessionIds: [SOURCE_SESSION_ID, DESTINATION_SESSION_ID],
});
expect(resolved.sessionId).toBe(DESTINATION_SESSION_ID);
expect(resolved.state.intent).toBeNull();
expect(resolved.state.request).toEqual({
requestId: startedState.requestId,
presentationToken: START.presentationToken,
taskId: START.taskId,
sessionId: DESTINATION_SESSION_ID,
});
});
it("rejects prepared, foreign, stale, and mismatched routes", () => {
const startedState = started();
const bound = bindWorkflowSessionFocus(startedState.state, {
requestId: startedState.requestId,
presentationToken: START.presentationToken,
entryIdentity: COMMITTED_ROUTE.entry_identity,
});
for (const routeMetadata of [
{ workflow_session_route: { ...COMMITTED_ROUTE, phase: "prepared" } },
{ workflow_session_route: { ...COMMITTED_ROUTE, entry_identity: "entry:other" } },
{ workflow_session_route: { ...COMMITTED_ROUTE, destination_step_id: "step-review" } },
{ workflow_session_route: { ...COMMITTED_ROUTE, destination_session_id: "session-other" } },
{ workflow_session_route: { ...COMMITTED_ROUTE, operation_id: "" } },
]) {
const result = reconcileWorkflowSessionFocus(bound, {
activeTaskId: START.taskId,
navigationRevision: START.navigationRevision,
workflowStepId: START.destinationStepId,
routeMetadata,
knownSessionIds: [DESTINATION_SESSION_ID],
});
expect(result.sessionId).toBeNull();
expect(result.state.intent).not.toBeNull();
}
});
it("cancels on a later navigation, newer request, and a missing response identity", () => {
const first = started();
const canceled = cancelWorkflowSessionFocus(first.state, {
taskId: START.taskId,
presentationToken: START.presentationToken,
});
expect(canceled.intent).toBeNull();
const second = beginWorkflowSessionFocus(canceled, { ...START, presentationToken: 8 });
const newer = beginWorkflowSessionFocus(second.state, {
...START,
destinationStepId: LATER_STEP_ID,
});
expect(newer.requestId).toBe(3);
expect(newer.state.intent?.destinationStepId).toBe(LATER_STEP_ID);
if (newer.requestId === null) throw new Error("test focus intent did not restart");
const missingIdentity = bindWorkflowSessionFocus(newer.state, {
requestId: newer.requestId,
presentationToken: START.presentationToken,
entryIdentity: "",
});
expect(missingIdentity.intent).toBeNull();
expect(missingIdentity.request).toBeNull();
});
it("keeps a focus request until its desktop or phone consumer acknowledges it", () => {
const startedState = started();
const bound = bindWorkflowSessionFocus(startedState.state, {
requestId: startedState.requestId,
presentationToken: START.presentationToken,
entryIdentity: COMMITTED_ROUTE.entry_identity,
});
const resolved = reconcileWorkflowSessionFocus(bound, {
activeTaskId: START.taskId,
navigationRevision: START.navigationRevision,
workflowStepId: START.destinationStepId,
routeMetadata: { workflow_session_route: COMMITTED_ROUTE },
knownSessionIds: [DESTINATION_SESSION_ID],
});
const acknowledged = acknowledgeWorkflowSessionFocus(resolved.state, startedState.requestId);
expect(acknowledged.request).toBeNull();
});
it("uses the live committed route when the response arrives after its task event", () => {
const startedState = started();
const bound = bindWorkflowSessionFocus(startedState.state, {
requestId: startedState.requestId,
presentationToken: START.presentationToken,
entryIdentity: COMMITTED_ROUTE.entry_identity,
});
const resolved = reconcileWorkflowSessionFocus(bound, {
activeTaskId: START.taskId,
navigationRevision: START.navigationRevision,
workflowStepId: START.destinationStepId,
routeMetadata: { workflow_session_route: COMMITTED_ROUTE },
routeUpdatedAt: LIVE_UPDATED_AT,
responseProjection: {
metadata: {
workflow_session_route: { ...COMMITTED_ROUTE, phase: "prepared" },
},
updatedAt: RESPONSE_UPDATED_AT,
entryIdentity: COMMITTED_ROUTE.entry_identity,
},
knownSessionIds: [DESTINATION_SESSION_ID],
});
expect(resolved.sessionId).toBe(DESTINATION_SESSION_ID);
});
it("rejects an obsolete response route after a newer live entry", () => {
const startedState = started();
const bound = bindWorkflowSessionFocus(startedState.state, {
requestId: startedState.requestId,
presentationToken: START.presentationToken,
entryIdentity: COMMITTED_ROUTE.entry_identity,
});
const newerLiveRoute = {
...COMMITTED_ROUTE,
entry_identity: "entry:00000000000000000043",
destination_session_id: NEWER_SESSION_ID,
};
const result = reconcileWorkflowSessionFocus(bound, {
activeTaskId: START.taskId,
navigationRevision: START.navigationRevision,
workflowStepId: START.destinationStepId,
routeMetadata: { workflow_session_route: newerLiveRoute },
routeUpdatedAt: LIVE_UPDATED_AT,
responseProjection: {
metadata: { workflow_session_route: COMMITTED_ROUTE },
updatedAt: RESPONSE_UPDATED_AT,
entryIdentity: COMMITTED_ROUTE.entry_identity,
},
knownSessionIds: [DESTINATION_SESSION_ID, NEWER_SESSION_ID],
});
expect(result.sessionId).toBeNull();
expect(result.state.intent).not.toBeNull();
});
it("accepts a committed response only when its entry identity matches the bound move", () => {
const startedState = started();
const bound = bindWorkflowSessionFocus(startedState.state, {
requestId: startedState.requestId,
presentationToken: START.presentationToken,
entryIdentity: COMMITTED_ROUTE.entry_identity,
});
const result = reconcileWorkflowSessionFocus(bound, {
activeTaskId: START.taskId,
navigationRevision: START.navigationRevision,
workflowStepId: START.destinationStepId,
routeMetadata: undefined,
responseProjection: {
metadata: { workflow_session_route: COMMITTED_ROUTE },
updatedAt: RESPONSE_UPDATED_AT,
entryIdentity: "entry:other",
},
knownSessionIds: [DESTINATION_SESSION_ID],
});
expect(result.sessionId).toBeNull();
expect(result.state.intent).not.toBeNull();
});
it("rejects a committed route after the task reaches a later workflow step", () => {
const startedState = started();
const bound = bindWorkflowSessionFocus(startedState.state, {
requestId: startedState.requestId,
presentationToken: START.presentationToken,
entryIdentity: COMMITTED_ROUTE.entry_identity,
});
const result = reconcileWorkflowSessionFocus(bound, {
activeTaskId: START.taskId,
navigationRevision: START.navigationRevision,
workflowStepId: LATER_STEP_ID,
routeMetadata: { workflow_session_route: COMMITTED_ROUTE },
knownSessionIds: [DESTINATION_SESSION_ID],
});
expect(result.sessionId).toBeNull();
expect(result.state.intent).toBeNull();
});
it.each(["0", "2026-02-30T10:00:00Z"])(
"ignores malformed response timestamp %s when choosing a live route",
(updatedAt) => {
const startedState = started();
const bound = bindWorkflowSessionFocus(startedState.state, {
requestId: startedState.requestId,
presentationToken: START.presentationToken,
entryIdentity: COMMITTED_ROUTE.entry_identity,
});
const result = reconcileWorkflowSessionFocus(bound, {
activeTaskId: START.taskId,
navigationRevision: START.navigationRevision,
workflowStepId: START.destinationStepId,
routeMetadata: { workflow_session_route: COMMITTED_ROUTE },
routeUpdatedAt: LIVE_UPDATED_AT,
responseProjection: {
metadata: {
workflow_session_route: { ...COMMITTED_ROUTE, phase: "prepared" },
},
updatedAt,
entryIdentity: COMMITTED_ROUTE.entry_identity,
},
knownSessionIds: [DESTINATION_SESSION_ID],
});
expect(result.sessionId).toBe(DESTINATION_SESSION_ID);
},
);
it("selects a valid snapshot over a normalized-invalid task timestamp", () => {
const store = createAppStore();
const wrongRoute = {
...COMMITTED_ROUTE,
destination_session_id: NEWER_SESSION_ID,
};
store.setState((state) => {
state.tasks.activeTaskId = START.taskId;
state.tasks.activeSessionId = SOURCE_SESSION_ID;
state.taskRemoval.navigationRevision = START.navigationRevision;
state.kanban.tasks = [
{
id: START.taskId,
workflowStepId: START.destinationStepId,
updatedAt: "2026-02-30T10:00:00Z",
metadata: { workflow_session_route: wrongRoute },
},
] as never;
state.kanbanMulti.snapshots = {
[START.workflowId]: {
workflowId: START.workflowId,
workflowName: "Workflow",
steps: [],
tasks: [
{
id: START.taskId,
workflowStepId: START.destinationStepId,
updatedAt: "2026-03-01T10:00:00Z",
metadata: { workflow_session_route: COMMITTED_ROUTE },
},
],
},
} as never;
state.taskSessionsByTask.itemsByTaskId[START.taskId] = [
{ id: SOURCE_SESSION_ID },
{ id: DESTINATION_SESSION_ID },
{ id: NEWER_SESSION_ID },
] as never;
});
const requestId = store.getState().beginWorkflowSessionFocus(START);
if (requestId === null) throw new Error(FOCUS_INTENT_ERROR);
store.getState().bindWorkflowSessionFocus({
requestId,
presentationToken: START.presentationToken,
entryIdentity: COMMITTED_ROUTE.entry_identity,
});
store.getState().reconcileWorkflowSessionFocus(START.taskId);
expect(store.getState().tasks.activeSessionId).toBe(DESTINATION_SESSION_ID);
});
});