import { describe, expect, it } from "vitest"; import type { KanbanState, WorkflowSnapshotData } from "@/lib/state/slices/kanban/types"; import type { TaskStatusSummary } from "@/lib/types/task-status-summary"; import type { ThreadFilterClause, ThreadView } from "@/lib/state/slices/ui/thread-view-types"; import { queryThreadView, selectThreadCandidates } from "./thread-view-query"; type TaskOverrides = Partial & { id: string }; const REVIEW_STEP_ID = "step-review"; const SECOND_WORKFLOW_ID = "workflow-2"; const SAME_ACTIVITY = "2026-08-31T12:00:00Z"; function task(overrides: TaskOverrides): KanbanState["tasks"][number] { const { id, ...rest } = overrides; return { id, workspaceId: "workspace-1", workflowId: "workflow-1", workflowStepId: "step-build", title: `Task ${id}`, position: 0, state: "IN_PROGRESS", primarySessionId: `session-${id}`, primarySessionState: "RUNNING", updatedAt: "2026-08-31T10:00:00Z", createdAt: "2026-08-30T10:00:00Z", ...rest, }; } function summary(overrides: Partial = {}): TaskStatusSummary { return { revision: 1, updated_at: "2026-08-31T10:00:00Z", primary_session: { id: "session-default", state: "RUNNING" }, ...overrides, }; } function snapshot( tasks: KanbanState["tasks"], workflowId = "workflow-1", workflowName = "Delivery", ): WorkflowSnapshotData { return { workflowId, workflowName, steps: [ { id: REVIEW_STEP_ID, title: "Review", color: "green", position: 1 }, { id: "step-build", title: "Build", color: "blue", position: 0 }, ], tasks, }; } function view(overrides: Partial = {}): ThreadView { return { id: "view-all-threads", name: "All threads", taskScope: { mode: "all", taskIds: [] }, filters: [], sort: { key: "attention", direction: "asc" }, maxColumns: null, layout: "columns", autoHideComposer: false, ...overrides, }; } function clause( dimension: ThreadFilterClause["dimension"], value: ThreadFilterClause["value"], op: ThreadFilterClause["op"] = "is", ): ThreadFilterClause { return { id: `${dimension}-${op}`, dimension, op, value }; } function richTask(): KanbanState["tasks"][number] { return task({ id: "rich", workflowStepId: REVIEW_STEP_ID, title: "Fix checkout regression", priority: "critical", state: "REVIEW", blocked: true, labels: ["urgent", "frontend"], primaryAgentProfileId: "agent-profile-1", primaryAgentName: "Reviewer", primaryExecutorType: "ssh", foregroundActivity: "background", interrupted: true, sessionCount: 2, isPRReview: true, statusSummary: summary({ primary_session: { id: "session-rich", state: "WAITING_FOR_INPUT" }, pending_action: "permission", queued_prompt_count: 2, active_subagent_count: 1, active_error: { stamp: "error-1", occurred_at: "2026-08-31T09:00:00Z", preview: "The check failed", }, git: { changed_files: 2 }, foreground_activity: "background", pull_request: { count: 1, open_count: 1, attention: true, number: 42, state: "open", aggregate_state: "checks_failed", }, last_activity_at: "2026-08-31T11:00:00Z", }), }); } describe("selectThreadCandidates", () => { it("projects bounded task data without applying the active workflow filter", () => { const candidates = selectThreadCandidates({ "workflow-1": snapshot([richTask()]), [SECOND_WORKFLOW_ID]: snapshot( [ task({ id: "other-workflow", workflowId: SECOND_WORKFLOW_ID, workspaceId: "workspace-1", }), ], SECOND_WORKFLOW_ID, "Operations", ), }); expect(candidates.map((candidate) => candidate.taskId)).toEqual( expect.arrayContaining(["rich", "other-workflow"]), ); expect(candidates).toHaveLength(2); expect(candidates.find((candidate) => candidate.taskId === "rich")).toMatchObject({ workflowStepId: REVIEW_STEP_ID, taskState: "REVIEW", priority: "critical", blocked: true, labels: ["urgent", "frontend"], taskType: "pull_request_review", primaryAgentProfileId: "agent-profile-1", executorType: "ssh", repositoryIds: [], threadStatus: "needs_action", hasDiff: true, hasPR: true, prNeedsAttention: true, hasActiveError: true, hasMultipleSessions: true, foregroundActivity: "background", interrupted: true, isOnLastWorkflowStep: true, prInfo: { number: 42, state: "Open", aggregateState: "checks_failed", }, }); }); it("keeps selected IDs out of the result while they are ineligible", () => { const result = queryThreadView( { "workflow-1": snapshot([ task({ id: "selected", primarySessionState: "COMPLETED" }), task({ id: "running" }), ]), }, view({ taskScope: { mode: "selected", taskIds: ["selected", "running"] } }), ); expect(result.matchingCandidates.map((candidate) => candidate.taskId)).toEqual(["running"]); expect(result.candidates.map((candidate) => candidate.taskId)).toEqual(["running"]); }); }); describe("queryThreadView filters", () => { const filterCases: Array<{ dimension: ThreadFilterClause["dimension"]; value: ThreadFilterClause["value"]; op?: ThreadFilterClause["op"]; }> = [ { dimension: "threadStatus", value: "needs_action" }, { dimension: "pendingAction", value: "permission" }, { dimension: "taskState", value: "REVIEW" }, { dimension: "workflow", value: "workflow-1" }, { dimension: "workflowStep", value: REVIEW_STEP_ID }, { dimension: "repository", value: "repo-1" }, { dimension: "primaryAgent", value: "agent-profile-1" }, { dimension: "executorType", value: "ssh" }, { dimension: "priority", value: "critical" }, { dimension: "blocked", value: true }, { dimension: "hasQueuedPrompts", value: true }, { dimension: "hasActiveSubagents", value: true }, { dimension: "hasDiff", value: true }, { dimension: "hasPR", value: true }, { dimension: "prNeedsAttention", value: true }, { dimension: "taskType", value: "pull_request_review" }, { dimension: "titleMatch", value: "CHECKOUT", op: "matches" }, { dimension: "hasActiveError", value: true }, { dimension: "taskLabel", value: "urgent" }, { dimension: "taskOrigin", value: "automation_run" }, { dimension: "hasMultipleSessions", value: true }, ]; it.each(filterCases)("supports the $dimension dimension", ({ dimension, value, op }) => { const candidateTask = richTask(); candidateTask.repositories = [ { id: "task-repo-1", repository_id: "repo-1", base_branch: "main", position: 0, }, ]; candidateTask.metadata = { origin: "automation_run" }; const snapshots: Record = dimension === "workflow" ? { "workflow-1": snapshot([candidateTask]), [SECOND_WORKFLOW_ID]: snapshot( [task({ id: "plain", workflowId: SECOND_WORKFLOW_ID })], SECOND_WORKFLOW_ID, "Operations", ), } : { "workflow-1": snapshot([candidateTask, task({ id: "plain" })]) }; const result = queryThreadView( snapshots, view({ filters: [clause(dimension, value, op ?? "is")] }), ); expect(result.matchingCandidates.map((candidate) => candidate.taskId)).toEqual(["rich"]); }); it("uses AND between clauses and OR inside an in clause", () => { const result = queryThreadView( { "workflow-1": snapshot([ richTask(), task({ id: "plain", priority: "critical" }), task({ id: "other", priority: "low" }), ]), }, view({ filters: [ clause("priority", ["critical", "high"], "in"), clause("taskLabel", ["urgent", "missing"], "in"), ], }), ); expect(result.matchingCandidates.map((candidate) => candidate.taskId)).toEqual(["rich"]); }); it("supports negative and substring operators", () => { const result = queryThreadView( { "workflow-1": snapshot([richTask(), task({ id: "plain", title: "Small fix" })]) }, view({ filters: [ clause("titleMatch", "checkout", "matches"), clause("taskType", "standard", "not_matches"), ], }), ); expect(result.matchingCandidates).toHaveLength(1); expect(result.matchingCandidates[0].taskId).toBe("rich"); }); }); describe("queryThreadView attention sort", () => { it("keeps attention buckets and recency order for All threads", () => { const candidates = [ task({ id: "waiting-new", primarySessionState: "WAITING_FOR_INPUT", statusSummary: summary({ primary_session: { id: "session-waiting-new", state: "WAITING_FOR_INPUT" }, last_activity_at: "2026-08-31T13:00:00Z", }), }), task({ id: "review-old", state: "REVIEW", primarySessionState: "COMPLETED", statusSummary: summary({ primary_session: { id: "session-review-old", state: "COMPLETED" }, last_activity_at: "2026-08-31T10:00:00Z", }), }), task({ id: "running-new", primarySessionState: "RUNNING", statusSummary: summary({ primary_session: { id: "session-running-new", state: "RUNNING" }, last_activity_at: SAME_ACTIVITY, }), }), task({ id: "action-old", primarySessionState: "WAITING_FOR_INPUT", statusSummary: summary({ primary_session: { id: "session-action-old", state: "WAITING_FOR_INPUT" }, pending_action: "clarification", last_activity_at: "2026-08-31T09:00:00Z", }), }), task({ id: "action-new", primarySessionState: "WAITING_FOR_INPUT", statusSummary: summary({ primary_session: { id: "session-action-new", state: "WAITING_FOR_INPUT" }, pending_action: "permission", last_activity_at: "2026-08-31T11:00:00Z", }), }), ]; const result = queryThreadView({ "workflow-1": snapshot(candidates) }, view()); expect(result.matchingCandidates.map((candidate) => candidate.taskId)).toEqual([ "action-new", "action-old", "running-new", "review-old", "waiting-new", ]); }); }); describe("queryThreadView sort and admission", () => { it("sorts deterministically and limits the admitted candidates", () => { const result = queryThreadView( { "workflow-1": snapshot([ task({ id: "c", updatedAt: SAME_ACTIVITY }), task({ id: "a", updatedAt: SAME_ACTIVITY }), task({ id: "b", updatedAt: SAME_ACTIVITY }), ]), }, view({ sort: { key: "updatedAt", direction: "desc" }, maxColumns: 2 }), ); expect(result.matchingCandidates.map((candidate) => candidate.taskId)).toEqual(["a", "b", "c"]); expect(result.admittedCandidates.map((candidate) => candidate.taskId)).toEqual(["a", "b"]); expect(result.matchingCount).toBe(3); expect(result.hiddenCount).toBe(1); }); it("temporarily admits a valid hidden deep-link target without changing the view", () => { const activeView = view({ taskScope: { mode: "selected", taskIds: ["first", "second", "hidden"] }, maxColumns: 2, }); const result = queryThreadView( { "workflow-1": snapshot([ task({ id: "first" }), task({ id: "second" }), task({ id: "hidden" }), ]), }, activeView, { requestedTaskId: "hidden" }, ); expect(result.admittedCandidates.map((candidate) => candidate.taskId)).toEqual([ "first", "hidden", ]); expect(result.effectiveView).toEqual(activeView); }); it("admits an eligible deep-link target outside a selected scope", () => { const result = queryThreadView( { "workflow-1": snapshot([task({ id: "selected" }), task({ id: "linked" })]), }, view({ taskScope: { mode: "selected", taskIds: ["selected"] }, maxColumns: 1 }), { requestedTaskId: "linked" }, ); expect(result.matchingCandidates.map((candidate) => candidate.taskId)).toEqual(["selected"]); expect(result.admittedCandidates.map((candidate) => candidate.taskId)).toEqual(["linked"]); }); it("temporarily admits an eligible deep-link target hidden by a saved filter", () => { const result = queryThreadView( { "workflow-1": snapshot([ task({ id: "matching", title: "Matching task" }), task({ id: "filtered", title: "Filtered task" }), ]), }, view({ filters: [clause("titleMatch", "matching", "matches")], maxColumns: 1, }), { requestedTaskId: "filtered" }, ); expect(result.matchingCandidates.map((candidate) => candidate.taskId)).toEqual(["matching"]); expect(result.admittedCandidates.map((candidate) => candidate.taskId)).toEqual(["filtered"]); expect(result.matchingCount).toBe(1); expect(result.temporaryAdmissionCount).toBe(1); expect(result.hiddenCount).toBe(1); }); it("merges a matching draft without changing the saved view identity", () => { const activeView = view({ maxColumns: null }); const result = queryThreadView( { "workflow-1": snapshot([task({ id: "one" }), task({ id: "two" })]) }, activeView, { draft: { baseViewId: activeView.id, taskScope: { mode: "selected", taskIds: ["two"] }, filters: [], sort: { key: "title", direction: "asc" }, maxColumns: 1, layout: "columns", autoHideComposer: false, }, }, ); expect(result.admittedCandidates.map((candidate) => candidate.taskId)).toEqual(["two"]); expect(result.effectiveView).toMatchObject({ id: activeView.id, name: activeView.name }); expect(result.effectiveView.maxColumns).toBe(1); }); }); describe("thread view fingerprint", () => { // @covers AC-UI-THREADS-SAVED-VIEWS-005.7 it("applies draft presentation without changing admission or the query fingerprint", () => { const snapshots = { "workflow-1": snapshot([task({ id: "one" }), task({ id: "two" })]) }; const saved = view(); const before = queryThreadView(snapshots, saved); const after = queryThreadView(snapshots, saved, { draft: { ...saved, baseViewId: saved.id, layout: "grid", autoHideComposer: true }, }); expect(after.effectiveView).toMatchObject({ layout: "grid", autoHideComposer: true }); expect(after.fingerprint).toBe(before.fingerprint); expect(after.admittedCandidates).toEqual(before.admittedCandidates); expect(after.stableCandidates).toEqual(before.stableCandidates); }); it("changes when query inputs change and remains stable for equivalent views", () => { const snapshots = { "workflow-1": snapshot([task({ id: "one" })]) }; const first = queryThreadView(snapshots, view({ maxColumns: 2 })); const equivalent = queryThreadView(snapshots, view({ maxColumns: 2 })); const changed = queryThreadView(snapshots, view({ maxColumns: 3 })); expect(first.fingerprint).toBe(equivalent.fingerprint); expect(first.fingerprint).not.toBe(changed.fingerprint); }); }); // @covers AC-TASKS-THREADS-ACTIONS-003.7, AC-TASKS-THREADS-ACTIONS-003.8 describe("pending archive exclusion", () => { it("excludes pending tasks before scopes, counts and column limits", () => { const snapshots = { "workflow-1": snapshot([task({ id: "a" }), task({ id: "b" }), task({ id: "c" })]), }; const activeView = view({ taskScope: { mode: "selected", taskIds: ["a", "b", "c"] }, maxColumns: 1, }); const baseline = queryThreadView(snapshots, activeView); const result = queryThreadView(snapshots, activeView, { requestedTaskId: "a", excludedTaskIds: new Set(["a"]), }); for (const candidates of [ result.candidates, result.matchingCandidates, result.stableCandidates, ]) { expect(candidates.map(({ taskId }) => taskId)).toEqual(["b", "c"]); } expect(result.admittedCandidates.map(({ taskId }) => taskId)).toEqual(["b"]); expect(result).toMatchObject({ matchingCount: 2, temporaryAdmissionCount: 0, hiddenCount: 1 }); expect(result.fingerprint).toBe(baseline.fingerprint); expect(result.effectiveView).toEqual(activeView); expect(snapshots["workflow-1"].tasks.map(({ id }) => id)).toEqual(["a", "b", "c"]); }); it("does not temporarily admit an excluded deep link outside the saved filter", () => { const result = queryThreadView( { "workflow-1": snapshot([task({ id: "a" }), task({ id: "b" })]) }, view({ filters: [clause("titleMatch", "Task b", "matches")], maxColumns: 1 }), { requestedTaskId: "a", excludedTaskIds: new Set(["a"]) }, ); expect(result.admittedCandidates.map(({ taskId }) => taskId)).toEqual(["b"]); expect(result).toMatchObject({ matchingCount: 1, temporaryAdmissionCount: 0, hiddenCount: 0 }); }); it("excludes every covered descendant and returns an empty query when none survive", () => { const result = queryThreadView( { "workflow-1": snapshot([ task({ id: "parent" }), task({ id: "child", parentTaskId: "parent" }), ]), }, view(), { excludedTaskIds: new Set(["parent", "child"]) }, ); expect(result.candidates).toEqual([]); expect(result.stableCandidates).toEqual([]); expect(result.admittedCandidates).toEqual([]); expect(result).toMatchObject({ matchingCount: 0, temporaryAdmissionCount: 0, hiddenCount: 0 }); }); });