Merge story-29: Backfill tests for maximum coverage

Adds 57 Rust tests and 60 frontend tests across 4 batches:
- Batch 1: store, search, workflow
- Batch 2: fs, shell, http/workflow
- Batch 3: usePathCompletion, api/client, api/workflow
- Batch 4: App, GatePanel, ReviewPanel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dave
2026-02-19 14:46:42 +00:00
13 changed files with 1648 additions and 2 deletions

View File

@@ -0,0 +1,145 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { GatePanel } from "./GatePanel";
const baseProps = {
gateState: null,
gateStatusLabel: "Unknown",
gateStatusColor: "#aaa",
isGateLoading: false,
gateError: null,
lastGateRefresh: null,
onRefresh: vi.fn(),
};
describe("GatePanel", () => {
it("shows 'no workflow data' when gateState is null", () => {
render(<GatePanel {...baseProps} />);
expect(screen.getByText("No workflow data yet.")).toBeInTheDocument();
});
it("shows loading message when isGateLoading is true", () => {
render(<GatePanel {...baseProps} isGateLoading={true} />);
expect(
screen.getByText("Loading workflow gates..."),
).toBeInTheDocument();
});
it("shows error with retry button", async () => {
const onRefresh = vi.fn();
render(
<GatePanel
{...baseProps}
gateError="Connection failed"
onRefresh={onRefresh}
/>,
);
expect(screen.getByText("Connection failed")).toBeInTheDocument();
const retryButton = screen.getByRole("button", { name: "Retry" });
await userEvent.click(retryButton);
expect(onRefresh).toHaveBeenCalledOnce();
});
it("shows gate status label and color", () => {
render(
<GatePanel
{...baseProps}
gateStatusLabel="Blocked"
gateStatusColor="#ff7b72"
/>,
);
expect(screen.getByText("Blocked")).toBeInTheDocument();
});
it("shows test summary when gateState is provided", () => {
render(
<GatePanel
{...baseProps}
gateState={{
canAccept: true,
reasons: [],
warning: null,
summary: { total: 5, passed: 5, failed: 0 },
missingCategories: [],
}}
gateStatusLabel="Ready to accept"
/>,
);
expect(
screen.getByText(/5\/5 passing, 0 failing/),
).toBeInTheDocument();
});
it("shows missing categories", () => {
render(
<GatePanel
{...baseProps}
gateState={{
canAccept: false,
reasons: [],
warning: null,
summary: { total: 0, passed: 0, failed: 0 },
missingCategories: ["unit", "integration"],
}}
/>,
);
expect(
screen.getByText("Missing: unit, integration"),
).toBeInTheDocument();
});
it("shows warning text", () => {
render(
<GatePanel
{...baseProps}
gateState={{
canAccept: false,
reasons: [],
warning: "Multiple tests failing — fix one at a time.",
summary: { total: 4, passed: 2, failed: 2 },
missingCategories: [],
}}
/>,
);
expect(
screen.getByText("Multiple tests failing — fix one at a time."),
).toBeInTheDocument();
});
it("shows reasons as list items", () => {
render(
<GatePanel
{...baseProps}
gateState={{
canAccept: false,
reasons: ["No approved test plan.", "Tests are failing."],
warning: null,
summary: { total: 2, passed: 1, failed: 1 },
missingCategories: [],
}}
/>,
);
expect(
screen.getByText("No approved test plan."),
).toBeInTheDocument();
expect(screen.getByText("Tests are failing.")).toBeInTheDocument();
});
it("calls onRefresh when Refresh button is clicked", async () => {
const onRefresh = vi.fn();
render(<GatePanel {...baseProps} onRefresh={onRefresh} />);
await userEvent.click(
screen.getByRole("button", { name: "Refresh" }),
);
expect(onRefresh).toHaveBeenCalledOnce();
});
it("disables Refresh button when loading", () => {
render(<GatePanel {...baseProps} isGateLoading={true} />);
expect(screen.getByRole("button", { name: "Refresh" })).toBeDisabled();
});
});

View File

@@ -0,0 +1,157 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import type { ReviewStory } from "../api/workflow";
import { ReviewPanel } from "./ReviewPanel";
const readyStory: ReviewStory = {
story_id: "29_backfill_tests",
can_accept: true,
reasons: [],
warning: null,
summary: { total: 5, passed: 5, failed: 0 },
missing_categories: [],
};
const blockedStory: ReviewStory = {
story_id: "26_tdd_gates",
can_accept: false,
reasons: ["2 tests are failing."],
warning: "Multiple tests failing — fix one at a time.",
summary: { total: 5, passed: 3, failed: 2 },
missing_categories: [],
};
const baseProps = {
reviewQueue: [] as ReviewStory[],
isReviewLoading: false,
reviewError: null,
proceedingStoryId: null,
storyId: "",
isGateLoading: false,
proceedError: null,
proceedSuccess: null,
lastReviewRefresh: null,
onRefresh: vi.fn(),
onProceed: vi.fn().mockResolvedValue(undefined),
};
describe("ReviewPanel", () => {
it("shows empty state when no stories", () => {
render(<ReviewPanel {...baseProps} />);
expect(
screen.getByText("No stories waiting for review."),
).toBeInTheDocument();
});
it("shows loading state", () => {
render(<ReviewPanel {...baseProps} isReviewLoading={true} />);
expect(screen.getByText("Loading review queue...")).toBeInTheDocument();
});
it("shows error with retry button", async () => {
const onRefresh = vi.fn();
render(
<ReviewPanel
{...baseProps}
reviewError="Network error"
onRefresh={onRefresh}
/>,
);
expect(
screen.getByText(/Network error.*Use Refresh to try again\./),
).toBeInTheDocument();
await userEvent.click(screen.getByRole("button", { name: "Retry" }));
expect(onRefresh).toHaveBeenCalledOnce();
});
it("renders ready story with Proceed button", () => {
render(<ReviewPanel {...baseProps} reviewQueue={[readyStory]} />);
expect(screen.getByText("29_backfill_tests")).toBeInTheDocument();
expect(screen.getByText("Ready")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Proceed" })).toBeEnabled();
});
it("renders blocked story with disabled button", () => {
render(<ReviewPanel {...baseProps} reviewQueue={[blockedStory]} />);
expect(screen.getByText("26_tdd_gates")).toBeInTheDocument();
expect(screen.getAllByText("Blocked")).toHaveLength(2);
expect(screen.getByRole("button", { name: "Blocked" })).toBeDisabled();
});
it("shows failing badge with count", () => {
render(<ReviewPanel {...baseProps} reviewQueue={[blockedStory]} />);
expect(screen.getByText("Failing 2")).toBeInTheDocument();
});
it("shows warning badge", () => {
render(<ReviewPanel {...baseProps} reviewQueue={[blockedStory]} />);
expect(screen.getByText("Warning")).toBeInTheDocument();
});
it("shows test summary per story", () => {
render(<ReviewPanel {...baseProps} reviewQueue={[readyStory]} />);
expect(screen.getByText(/5\/5 passing,\s*0 failing/)).toBeInTheDocument();
});
it("shows missing categories", () => {
const missingStory: ReviewStory = {
...blockedStory,
missing_categories: ["unit", "integration"],
};
render(<ReviewPanel {...baseProps} reviewQueue={[missingStory]} />);
expect(screen.getByText("Missing: unit, integration")).toBeInTheDocument();
});
it("calls onProceed when Proceed is clicked", async () => {
const onProceed = vi.fn().mockResolvedValue(undefined);
render(
<ReviewPanel
{...baseProps}
reviewQueue={[readyStory]}
onProceed={onProceed}
/>,
);
await userEvent.click(screen.getByRole("button", { name: "Proceed" }));
expect(onProceed).toHaveBeenCalledWith("29_backfill_tests");
});
it("shows queue counts in header", () => {
render(
<ReviewPanel {...baseProps} reviewQueue={[readyStory, blockedStory]} />,
);
expect(screen.getByText(/1 ready \/ 2 total/)).toBeInTheDocument();
});
it("shows proceedError message", () => {
render(
<ReviewPanel
{...baseProps}
proceedError="Acceptance blocked: tests failing"
/>,
);
expect(
screen.getByText("Acceptance blocked: tests failing"),
).toBeInTheDocument();
});
it("shows proceedSuccess message", () => {
render(
<ReviewPanel
{...baseProps}
proceedSuccess="Story accepted successfully"
/>,
);
expect(screen.getByText("Story accepted successfully")).toBeInTheDocument();
});
it("shows reasons as list items", () => {
render(<ReviewPanel {...baseProps} reviewQueue={[blockedStory]} />);
expect(screen.getByText("2 tests are failing.")).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,160 @@
import { act, renderHook, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import type { FileEntry } from "./usePathCompletion";
import {
getCurrentPartial,
isFuzzyMatch,
usePathCompletion,
} from "./usePathCompletion";
describe("isFuzzyMatch", () => {
it("matches when query is empty", () => {
expect(isFuzzyMatch("anything", "")).toBe(true);
});
it("matches exact prefix", () => {
expect(isFuzzyMatch("Documents", "Doc")).toBe(true);
});
it("matches fuzzy subsequence", () => {
expect(isFuzzyMatch("Documents", "dms")).toBe(true);
});
it("is case insensitive", () => {
expect(isFuzzyMatch("Documents", "DOCU")).toBe(true);
});
it("rejects when chars not found in order", () => {
expect(isFuzzyMatch("abc", "acb")).toBe(false);
});
it("rejects completely unrelated", () => {
expect(isFuzzyMatch("hello", "xyz")).toBe(false);
});
});
describe("getCurrentPartial", () => {
it("returns empty for empty input", () => {
expect(getCurrentPartial("")).toBe("");
});
it("returns empty when input ends with slash", () => {
expect(getCurrentPartial("/home/user/")).toBe("");
});
it("returns last segment", () => {
expect(getCurrentPartial("/home/user/Doc")).toBe("Doc");
});
it("returns full input when no slash", () => {
expect(getCurrentPartial("Doc")).toBe("Doc");
});
it("trims then evaluates: trailing-slash input returns empty", () => {
// " /home/user/ " trims to "/home/user/" which ends with slash
expect(getCurrentPartial(" /home/user/ ")).toBe("");
});
it("trims then returns last segment", () => {
expect(getCurrentPartial(" /home/user/Doc ")).toBe("Doc");
});
});
describe("usePathCompletion hook", () => {
const mockListDir = vi.fn<(path: string) => Promise<FileEntry[]>>();
beforeEach(() => {
mockListDir.mockReset();
});
it("returns empty matchList for empty input", async () => {
const { result } = renderHook(() =>
usePathCompletion({
pathInput: "",
setPathInput: vi.fn(),
homeDir: "/home/user",
listDirectoryAbsolute: mockListDir,
debounceMs: 0,
}),
);
// Allow effect + setTimeout(0) to fire
await waitFor(() => {
expect(mockListDir).not.toHaveBeenCalled();
});
expect(result.current.matchList).toEqual([]);
});
it("fetches directory listing and returns matches", async () => {
mockListDir.mockResolvedValue([
{ name: "Documents", kind: "dir" },
{ name: "Downloads", kind: "dir" },
{ name: ".bashrc", kind: "file" },
]);
const { result } = renderHook(() =>
usePathCompletion({
pathInput: "/home/user/",
setPathInput: vi.fn(),
homeDir: "/home/user",
listDirectoryAbsolute: mockListDir,
debounceMs: 0,
}),
);
await waitFor(() => {
expect(result.current.matchList.length).toBe(2);
});
expect(result.current.matchList[0].name).toBe("Documents");
expect(result.current.matchList[1].name).toBe("Downloads");
expect(result.current.matchList.every((m) => m.path.endsWith("/"))).toBe(
true,
);
});
it("filters by fuzzy match on partial input", async () => {
mockListDir.mockResolvedValue([
{ name: "Documents", kind: "dir" },
{ name: "Downloads", kind: "dir" },
{ name: "Desktop", kind: "dir" },
]);
const { result } = renderHook(() =>
usePathCompletion({
pathInput: "/home/user/Doc",
setPathInput: vi.fn(),
homeDir: "/home/user",
listDirectoryAbsolute: mockListDir,
debounceMs: 0,
}),
);
await waitFor(() => {
expect(result.current.matchList.length).toBe(1);
});
expect(result.current.matchList[0].name).toBe("Documents");
});
it("calls setPathInput when acceptMatch is invoked", () => {
const setPathInput = vi.fn();
const { result } = renderHook(() =>
usePathCompletion({
pathInput: "/home/",
setPathInput,
homeDir: "/home",
listDirectoryAbsolute: mockListDir,
debounceMs: 0,
}),
);
act(() => {
result.current.acceptMatch("/home/user/Documents/");
});
expect(setPathInput).toHaveBeenCalledWith("/home/user/Documents/");
});
});

View File

@@ -30,7 +30,7 @@ export interface UsePathCompletionResult {
closeSuggestions: () => void;
}
function isFuzzyMatch(candidate: string, query: string) {
export function isFuzzyMatch(candidate: string, query: string) {
if (!query) return true;
const lowerCandidate = candidate.toLowerCase();
const lowerQuery = query.toLowerCase();
@@ -43,7 +43,7 @@ function isFuzzyMatch(candidate: string, query: string) {
return true;
}
function getCurrentPartial(input: string) {
export function getCurrentPartial(input: string) {
const trimmed = input.trim();
if (!trimmed) return "";
if (trimmed.endsWith("/")) return "";