cruise 0.1.76

YAML-driven coding agent workflow orchestrator
Documentation
import { render, cleanup } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { ConfigEntry } from "../types";
import { ConfigSelect } from "./ConfigSelect";

afterEach(() => cleanup());

const BASE_DIR = "/home/user/project";
const USER_DIR = "/home/user/.config/cruise";

const LOCAL_ROOT: ConfigEntry = {
  path: `${BASE_DIR}/cruise.yaml`,
  name: "cruise.yaml",
  source: "local",
};

const LOCAL_NESTED: ConfigEntry = {
  path: `${BASE_DIR}/.cruise/team.yaml`,
  name: "team.yaml",
  source: "local",
};

const USER_ENTRY: ConfigEntry = {
  path: `${USER_DIR}/default.yaml`,
  name: "default.yaml",
  description: "default config",
  source: "user",
};

const NO_SOURCE_ENTRY: ConfigEntry = {
  path: "/repos/other-project/cruise.yaml",
  name: "cruise.yaml",
};

function getOptgroups(container: HTMLElement): HTMLOptGroupElement[] {
  return Array.from(container.querySelectorAll("optgroup"));
}

describe("ConfigSelect", () => {
  it("renders the 'Auto' option first with the corrected label", () => {
    // Given: any set of configs
    const { container } = render(
      <ConfigSelect
        id="config-select"
        value=""
        onChange={vi.fn()}
        configs={[LOCAL_ROOT]}
        baseDir={BASE_DIR}
      />,
    );

    // When: inspecting the first option
    const select = container.querySelector("select");
    const firstOption = select?.querySelector("option");

    // Then: it is the Auto entry with the corrected copy
    expect(firstOption).not.toBeNull();
    expect(firstOption).toHaveValue("");
    expect(firstOption?.textContent).toBe("Auto (base dir / user config / builtin)");
  });

  it("groups local entries under a 'Base dir (...)' optgroup", () => {
    // Given: a local-sourced entry at the repo root
    const { container } = render(
      <ConfigSelect
        id="config-select"
        value=""
        onChange={vi.fn()}
        configs={[LOCAL_ROOT]}
        baseDir={BASE_DIR}
      />,
    );

    // When: inspecting the optgroups
    const groups = getOptgroups(container);
    const localGroup = groups.find((g) => g.label === `Base dir (${BASE_DIR})`);

    // Then: the local group exists and contains the entry
    expect(localGroup).toBeDefined();
    expect(localGroup?.querySelectorAll("option")).toHaveLength(1);
    expect(localGroup?.querySelector("option")?.value).toBe(LOCAL_ROOT.path);
  });

  it("groups user entries under a 'User config (...)' optgroup derived from the entry path", () => {
    // Given: a user-sourced entry
    const { container } = render(
      <ConfigSelect
        id="config-select"
        value=""
        onChange={vi.fn()}
        configs={[USER_ENTRY]}
        baseDir={BASE_DIR}
      />,
    );

    // When: inspecting the optgroups
    const groups = getOptgroups(container);
    const userGroup = groups.find((g) => g.label === `User config (${USER_DIR})`);

    // Then: the user group exists and contains the entry, with description appended
    expect(userGroup).toBeDefined();
    const option = userGroup?.querySelector("option");
    expect(option?.value).toBe(USER_ENTRY.path);
    expect(option?.textContent).toBe("default.yaml — default config");
  });

  it("displays a root-level local entry as its bare filename", () => {
    // Given: a local entry at the base dir root
    const { container } = render(
      <ConfigSelect
        id="config-select"
        value=""
        onChange={vi.fn()}
        configs={[LOCAL_ROOT]}
        baseDir={BASE_DIR}
      />,
    );

    // When: reading the option text for the root entry
    const option = container.querySelector(`option[value="${LOCAL_ROOT.path}"]`);

    // Then: it shows only the filename
    expect(option?.textContent).toBe("cruise.yaml");
  });

  it("displays a local entry under .cruise/ with its relative path", () => {
    // Given: a local entry nested under .cruise/
    const { container } = render(
      <ConfigSelect
        id="config-select"
        value=""
        onChange={vi.fn()}
        configs={[LOCAL_NESTED]}
        baseDir={BASE_DIR}
      />,
    );

    // When: reading the option text for the nested entry
    const option = container.querySelector(`option[value="${LOCAL_NESTED.path}"]`);

    // Then: it shows the path relative to baseDir
    expect(option?.textContent).toBe(".cruise/team.yaml");
  });

  it("places entries without a source into an 'Other' optgroup", () => {
    // Given: an entry with no `source` field (synthetic "current config" entry)
    const { container } = render(
      <ConfigSelect
        id="config-select"
        value=""
        onChange={vi.fn()}
        configs={[NO_SOURCE_ENTRY]}
        baseDir={BASE_DIR}
      />,
    );

    // When: inspecting the optgroups
    const groups = getOptgroups(container);
    const otherGroup = groups.find((g) => g.label === "Other");

    // Then: the entry appears under "Other"
    expect(otherGroup).toBeDefined();
    expect(otherGroup?.querySelector("option")?.value).toBe(NO_SOURCE_ENTRY.path);
  });

  it("calls onChange with the selected path when the user picks an option", () => {
    // Given: a rendered select with a local entry
    const onChange = vi.fn();
    const { container } = render(
      <ConfigSelect
        id="config-select"
        value=""
        onChange={onChange}
        configs={[LOCAL_ROOT]}
        baseDir={BASE_DIR}
      />,
    );

    // When: the user selects the local entry
    const select = container.querySelector("select") as HTMLSelectElement;
    select.value = LOCAL_ROOT.path;
    select.dispatchEvent(new Event("change", { bubbles: true }));

    // Then: onChange fires with the new path
    expect(onChange).toHaveBeenCalledWith(LOCAL_ROOT.path);
  });

  it("applies the given id to the underlying select", () => {
    // Given: a custom id
    const { container } = render(
      <ConfigSelect
        id="session-config-select"
        value=""
        onChange={vi.fn()}
        configs={[]}
        baseDir={BASE_DIR}
      />,
    );

    // When: locating the select by id
    const select = container.querySelector("#session-config-select");

    // Then: it exists
    expect(select).not.toBeNull();
  });

  it("disables the select when disabled=true", () => {
    // Given: disabled=true
    const { container } = render(
      <ConfigSelect
        id="config-select"
        value=""
        onChange={vi.fn()}
        configs={[]}
        baseDir={BASE_DIR}
        disabled
      />,
    );

    // Then: the select is disabled
    const select = container.querySelector("select");
    expect(select).toBeDisabled();
  });

  it("falls back to the bare filename and a '.' group label when baseDir is omitted", () => {
    // Given: no baseDir prop at all
    const { container } = render(
      <ConfigSelect id="config-select" value="" onChange={vi.fn()} configs={[LOCAL_ROOT]} />,
    );

    // When: inspecting the group label and option text
    const groups = getOptgroups(container);
    const localGroup = groups.find((g) => g.label === "Base dir (.)");
    const option = container.querySelector(`option[value="${LOCAL_ROOT.path}"]`);

    // Then: the group falls back to "." and the option shows the bare filename
    expect(localGroup).toBeDefined();
    expect(option?.textContent).toBe("cruise.yaml");
  });

  it("lists every entry when a group contains more than one", () => {
    // Given: two local entries in the same group
    const { container } = render(
      <ConfigSelect
        id="config-select"
        value=""
        onChange={vi.fn()}
        configs={[LOCAL_ROOT, LOCAL_NESTED]}
        baseDir={BASE_DIR}
      />,
    );

    // When: inspecting the local optgroup
    const groups = getOptgroups(container);
    const localGroup = groups.find((g) => g.label === `Base dir (${BASE_DIR})`);
    const options = localGroup?.querySelectorAll("option");

    // Then: both entries are rendered with their own relative labels
    expect(options).toHaveLength(2);
    expect(Array.from(options ?? []).map((o) => o.textContent)).toEqual([
      "cruise.yaml",
      ".cruise/team.yaml",
    ]);
  });

  it("strips a trailing slash from baseDir before matching entry paths", () => {
    // Given: baseDir as produced by DirectoryPicker's autocomplete/Browse (trailing slash)
    const { container } = render(
      <ConfigSelect
        id="config-select"
        value=""
        onChange={vi.fn()}
        configs={[LOCAL_ROOT, LOCAL_NESTED]}
        baseDir={`${BASE_DIR}/`}
      />,
    );

    // Then: relative paths are still resolved instead of falling back to bare filenames
    const rootOption = container.querySelector(`option[value="${LOCAL_ROOT.path}"]`);
    const nestedOption = container.querySelector(`option[value="${LOCAL_NESTED.path}"]`);
    expect(rootOption?.textContent).toBe("cruise.yaml");
    expect(nestedOption?.textContent).toBe(".cruise/team.yaml");
  });
});