cruise 0.1.78

YAML-driven coding agent workflow orchestrator
Documentation
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, waitFor, cleanup, act } from "@testing-library/react";
import type { MutableRefObject } from "react";
import { SessionSidebar } from "../components/SessionSidebar";
import type { Session } from "../types";
import * as commands from "../lib/commands";

vi.mock("@tauri-apps/api/app", () => ({
  getVersion: vi.fn().mockResolvedValue("0.0.0"),
}));

vi.mock("../lib/updater", () => ({
  checkForUpdate: vi.fn().mockResolvedValue(null),
  downloadAndInstall: vi.fn(),
}));

vi.mock("../lib/commands", () => ({
  listSessions: vi.fn(),
  cleanSessions: vi.fn(),
  getUpdateReadiness: vi.fn().mockResolvedValue({ canAutoUpdate: true }),
  getSessionDag: vi.fn(),
}));

// --- Helpers ---

function makeSession(overrides: Partial<Session> = {}): Session {
  return {
    id: "session-1",
    phase: "Planned",
    configSource: "default.yaml",
    baseDir: "/home/user/project",
    input: "test task",
    createdAt: "2026-01-01T00:00:00Z",
    workspaceMode: "Worktree",
    skippedSteps: [],
    ...overrides,
  };
}

const defaultProps = {
  selectedId: null as string | null,
  onSelect: vi.fn(),
  onNewSession: vi.fn(),
  onRunAll: vi.fn(),
};

// --- Tests ---

describe("SessionSidebar: onOptimisticRemoveRef", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.mocked(commands.listSessions).mockResolvedValue([]);
  });

  afterEach(() => {
    cleanup();
    vi.restoreAllMocks();
    vi.useRealTimers();
  });

  it("wires a remove function to the ref after mount", async () => {
    // Given
    const optimisticRemoveRef: MutableRefObject<((id: string) => void) | null> = {
      current: null,
    };

    // When
    render(
      <SessionSidebar
        {...defaultProps}
        onOptimisticRemoveRef={optimisticRemoveRef}
      />
    );

    // Then: ref is wired once the component mounts and loads
    await waitFor(() => {
      expect(optimisticRemoveRef.current).toBeTypeOf("function");
    });
  });

  it("removes the matching session from the rendered list", async () => {
    // Given: sidebar with two sessions
    const optimisticRemoveRef: MutableRefObject<((id: string) => void) | null> = {
      current: null,
    };
    vi.mocked(commands.listSessions).mockResolvedValue([
      makeSession({ id: "session-1", input: "first task" }),
      makeSession({ id: "session-2", input: "second task" }),
    ]);

    render(
      <SessionSidebar
        {...defaultProps}
        onOptimisticRemoveRef={optimisticRemoveRef}
      />
    );
    await waitFor(() => screen.getByText("first task"));
    await waitFor(() => expect(optimisticRemoveRef.current).toBeTypeOf("function"));

    // When: optimistic remove is called for session-1
    act(() => {
      optimisticRemoveRef.current!("session-1");
    });

    // Then: session-1 disappears, session-2 remains
    expect(screen.queryByText("first task")).toBeNull();
    expect(screen.getByText("second task")).toBeInTheDocument();
  });

  it("leaves the list unchanged when called with an unknown ID", async () => {
    // Given: sidebar with one session
    const optimisticRemoveRef: MutableRefObject<((id: string) => void) | null> = {
      current: null,
    };
    vi.mocked(commands.listSessions).mockResolvedValue([
      makeSession({ id: "session-1", input: "only task" }),
    ]);

    render(
      <SessionSidebar
        {...defaultProps}
        onOptimisticRemoveRef={optimisticRemoveRef}
      />
    );
    await waitFor(() => screen.getByText("only task"));
    await waitFor(() => expect(optimisticRemoveRef.current).toBeTypeOf("function"));

    // When: called with an ID not in the list
    act(() => {
      optimisticRemoveRef.current!("nonexistent-session");
    });

    // Then: the existing session is still displayed
    expect(screen.getByText("only task")).toBeInTheDocument();
  });

  it("sets the ref to null when the component unmounts", async () => {
    // Given
    const optimisticRemoveRef: MutableRefObject<((id: string) => void) | null> = {
      current: null,
    };

    const { unmount } = render(
      <SessionSidebar
        {...defaultProps}
        onOptimisticRemoveRef={optimisticRemoveRef}
      />
    );
    await waitFor(() => expect(optimisticRemoveRef.current).toBeTypeOf("function"));

    // When: component unmounts
    unmount();

    // Then: ref is cleared to prevent stale calls
    expect(optimisticRemoveRef.current).toBeNull();
  });

  it("renders normally when onOptimisticRemoveRef is omitted", async () => {
    // When: render without the optional prop
    render(<SessionSidebar {...defaultProps} />);

    // Then: no error, listSessions is still called
    await waitFor(() => {
      expect(commands.listSessions).toHaveBeenCalledOnce();
    });
  });
});