f8654880f3
* fix(web): stop kanban spinner when primary session is terminal The kanban card spinner was gated only on `task.state` being IN_PROGRESS or SCHEDULING. Workflows leave a task in the IN_PROGRESS column after the agent's session reaches a terminal state (COMPLETED/FAILED/CANCELLED) so the user can review and move it manually, which made the loading icon spin indefinitely. The frontend already receives `primary_session_state` over SSR and WS; gate the spinner on that not being terminal. Fixes #985. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(orchestrator): write task.state=REVIEW after engine on_turn_complete transition The non-engine path (setSessionWaitingForInput → writeTaskReviewState) has always written tasks.state = REVIEW when the agent's session paused for input. The workflow-engine on_turn_complete path forgot the task-state write: it persisted the new workflow step + flipped the session to WAITING_FOR_INPUT but left tasks.state stale at IN_PROGRESS. That left the kanban card stuck in IN_PROGRESS — with a spinning loader — even after the workflow had moved it into the "Review" column. Mirror the non-engine path: call writeTaskReviewState inside the RUNNING/STARTING branch of applyEngineTransition. Add a regression test that asserts tasks.state = REVIEW after an engine-driven on_turn_complete transition. Also tighten the frontend gate added in the previous commit to suppress the spinner for any non-active primary-session state (WAITING_FOR_INPUT, IDLE in addition to COMPLETED/FAILED/CANCELLED). The backend fix is the real correction; the broader frontend gate stays as defense-in-depth against future state-sync drift. Refs #985. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(orchestrator): drop redundant time.Sleep in engine on_turn_complete test writeTaskReviewState and ApplyTransition both run synchronously inside applyEngineTransition (before processOnEnter's goroutine), so the task-state and step assertions need no async wait. assertSessionState already has its own 500ms polling loop for the WAITING_FOR_INPUT flip. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
69 lines
3.3 KiB
TypeScript
69 lines
3.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { isValidElement, type ReactNode } from "react";
|
|
import { IconCheck, IconMessageQuestion } from "@tabler/icons-react";
|
|
import { getTaskStateIcon, shouldShowTaskRunningSpinner } from "./state-icons";
|
|
|
|
function iconType(node: ReactNode) {
|
|
if (!isValidElement(node)) throw new Error("Expected React element");
|
|
return node.type;
|
|
}
|
|
|
|
describe("getTaskStateIcon", () => {
|
|
it("uses the question icon for waiting-for-input task state", () => {
|
|
expect(iconType(getTaskStateIcon("WAITING_FOR_INPUT"))).toBe(IconMessageQuestion);
|
|
});
|
|
|
|
it("uses the question icon when there is a pending clarification", () => {
|
|
expect(iconType(getTaskStateIcon("REVIEW", undefined, true))).toBe(IconMessageQuestion);
|
|
});
|
|
|
|
it("keeps review task state as the review check without pending clarification", () => {
|
|
expect(iconType(getTaskStateIcon("REVIEW", undefined, false))).toBe(IconCheck);
|
|
});
|
|
});
|
|
|
|
describe("shouldShowTaskRunningSpinner", () => {
|
|
it("returns false for non-loading task states regardless of session state", () => {
|
|
expect(shouldShowTaskRunningSpinner("COMPLETED")).toBe(false);
|
|
expect(shouldShowTaskRunningSpinner("FAILED")).toBe(false);
|
|
expect(shouldShowTaskRunningSpinner("CANCELLED")).toBe(false);
|
|
expect(shouldShowTaskRunningSpinner("REVIEW")).toBe(false);
|
|
expect(shouldShowTaskRunningSpinner("TODO")).toBe(false);
|
|
expect(shouldShowTaskRunningSpinner("REVIEW", "RUNNING")).toBe(false);
|
|
});
|
|
|
|
it("returns true for SCHEDULING with no primary session yet", () => {
|
|
expect(shouldShowTaskRunningSpinner("SCHEDULING")).toBe(true);
|
|
expect(shouldShowTaskRunningSpinner("SCHEDULING", null)).toBe(true);
|
|
expect(shouldShowTaskRunningSpinner("SCHEDULING", undefined)).toBe(true);
|
|
});
|
|
|
|
it("returns true for IN_PROGRESS when the primary session is actively running", () => {
|
|
expect(shouldShowTaskRunningSpinner("IN_PROGRESS", "RUNNING")).toBe(true);
|
|
expect(shouldShowTaskRunningSpinner("IN_PROGRESS", "STARTING")).toBe(true);
|
|
expect(shouldShowTaskRunningSpinner("IN_PROGRESS", "CREATED")).toBe(true);
|
|
});
|
|
|
|
it("returns true for IN_PROGRESS when no primary session is attached yet", () => {
|
|
expect(shouldShowTaskRunningSpinner("IN_PROGRESS", undefined)).toBe(true);
|
|
expect(shouldShowTaskRunningSpinner("IN_PROGRESS", null)).toBe(true);
|
|
});
|
|
|
|
it("suppresses the spinner when the primary session is terminal", () => {
|
|
// Repro from issue #985: agent finishes (session → COMPLETED) but the
|
|
// workflow leaves the task in IN_PROGRESS for review/manual move. The
|
|
// spinner must not keep spinning forever.
|
|
expect(shouldShowTaskRunningSpinner("IN_PROGRESS", "COMPLETED")).toBe(false);
|
|
expect(shouldShowTaskRunningSpinner("IN_PROGRESS", "FAILED")).toBe(false);
|
|
expect(shouldShowTaskRunningSpinner("IN_PROGRESS", "CANCELLED")).toBe(false);
|
|
expect(shouldShowTaskRunningSpinner("SCHEDULING", "COMPLETED")).toBe(false);
|
|
});
|
|
|
|
it("suppresses the spinner when the primary session is paused (waiting/idle)", () => {
|
|
// Same desync class, paused branch: agent stopped to wait for input or
|
|
// was torn down (office IDLE). The spinner is misleading.
|
|
expect(shouldShowTaskRunningSpinner("IN_PROGRESS", "WAITING_FOR_INPUT")).toBe(false);
|
|
expect(shouldShowTaskRunningSpinner("IN_PROGRESS", "IDLE")).toBe(false);
|
|
});
|
|
});
|