Files
kandev/apps/web/lib/threads/thread-session-status.test.ts
2026-09-15 23:14:56 +01:00

103 lines
3.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { TaskSession } from "@/lib/types/http";
import { sessionId, taskId } from "@/lib/types/ids";
import {
FAILED_TASK_STATUS,
resolveThreadColumnStatus,
resolveThreadSessionStatus,
} from "./thread-session-status";
function session(overrides: Partial<TaskSession> = {}): TaskSession {
return {
id: sessionId("session-1"),
task_id: taskId("task-1"),
state: "IDLE",
started_at: "2026-08-27T10:00:00Z",
updated_at: "2026-08-27T12:00:00Z",
...overrides,
};
}
describe("resolveThreadSessionStatus", () => {
it("gives permission the highest precedence", () => {
expect(
resolveThreadSessionStatus(session({ state: "RUNNING", pending_action: "permission" })),
).toMatchObject({ kind: "permission", labelKey: "threads:statusPermissionNeeded" });
});
it("uses a question status only for an explicit clarification", () => {
expect(
resolveThreadSessionStatus(
session({ state: "WAITING_FOR_INPUT", pending_action: "clarification" }),
),
).toMatchObject({ kind: "clarification", labelKey: "threads:statusQuestionFromAgent" });
});
it("does not turn plain WAITING_FOR_INPUT into a question", () => {
expect(resolveThreadSessionStatus(session({ state: "WAITING_FOR_INPUT" }))).toMatchObject({
kind: "waiting",
hasAttention: false,
});
});
it("distinguishes starting, foreground work, and a finished turn", () => {
expect(resolveThreadSessionStatus(session({ state: "STARTING" })).kind).toBe("starting");
expect(resolveThreadSessionStatus(session({ state: "RUNNING" })).kind).toBe("working");
expect(resolveThreadSessionStatus(session({ state: "IDLE" })).kind).toBe("finished");
});
it("lets a foreground activity keep an idle session working", () => {
expect(
resolveThreadSessionStatus(session({ state: "IDLE", foreground_activity: "background" }))
.kind,
).toBe("working");
});
it("keeps terminal errors distinct", () => {
expect(resolveThreadSessionStatus(session({ state: "FAILED" })).kind).toBe("failed");
expect(resolveThreadSessionStatus(session({ state: "CANCELLED" })).kind).toBe("cancelled");
});
});
describe("resolveThreadColumnStatus", () => {
it("shows review readiness only when no session needs explicit action", () => {
expect(
resolveThreadColumnStatus({
taskState: "REVIEW",
session: session({ state: "COMPLETED" }),
}),
).toMatchObject({ kind: "review-ready", labelKey: "threads:statusReadyForReview" });
});
it("keeps session attention ahead of review readiness", () => {
expect(
resolveThreadColumnStatus({
taskState: "REVIEW",
session: session({ state: "WAITING_FOR_INPUT", pending_action: "clarification" }),
}).kind,
).toBe("clarification");
});
it("keeps task-wide attention separate from the selected session action", () => {
expect(
resolveThreadColumnStatus({
taskPendingAction: "permission",
session: session({ state: "WAITING_FOR_INPUT", pending_action: null }),
}),
).toMatchObject({ kind: "needs-you", hasAttention: true });
});
});
// AC-UI-INBOX-FAILED-001.20: the Failed tab has no session to resolve status
// from, so it reaches the shared vocabulary's "failed" entry directly rather
// than fabricating a stand-in session for resolveThreadSessionStatus.
describe("FAILED_TASK_STATUS", () => {
it("is exactly the same entry resolveThreadSessionStatus returns for a failed session", () => {
expect(FAILED_TASK_STATUS).toEqual(resolveThreadSessionStatus(session({ state: "FAILED" })));
});
it("carries no attention flag", () => {
expect(FAILED_TASK_STATUS.hasAttention).toBe(false);
});
});