218 lines
6.8 KiB
TypeScript
218 lines
6.8 KiB
TypeScript
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import type { Message } from "@/lib/types/http";
|
|
import type { PrepareStepInfo } from "@/lib/state/slices/session-runtime/types";
|
|
|
|
let mockSteps: PrepareStepInfo[] = [];
|
|
let mockPrepareStatus: "preparing" | "completed" | "failed" = "preparing";
|
|
let mockSessionState: string = "STARTING";
|
|
let mockMessages: Message[] = [];
|
|
let mockPrepareError: string | undefined;
|
|
const CREATE_WORKTREE = "Create worktree";
|
|
|
|
vi.mock("@/components/state-provider", () => ({
|
|
useAppStore: (selector: (state: Record<string, unknown>) => unknown) =>
|
|
selector({
|
|
prepareProgress: {
|
|
bySessionId: {
|
|
"session-1": {
|
|
sessionId: "session-1",
|
|
status: mockPrepareStatus,
|
|
steps: mockSteps,
|
|
errorMessage: mockPrepareError,
|
|
},
|
|
},
|
|
},
|
|
taskSessions: {
|
|
items: {
|
|
"session-1": {
|
|
id: "session-1",
|
|
state: mockSessionState,
|
|
},
|
|
},
|
|
},
|
|
sessionAgentctl: {
|
|
itemsBySessionId: {},
|
|
},
|
|
messages: {
|
|
bySession: {
|
|
"session-1": mockMessages,
|
|
},
|
|
},
|
|
}),
|
|
}));
|
|
|
|
import { PrepareProgress } from "./prepare-progress";
|
|
|
|
describe("PrepareProgress", () => {
|
|
afterEach(() => {
|
|
cleanup();
|
|
mockMessages = [];
|
|
mockPrepareError = undefined;
|
|
});
|
|
|
|
it("hides skipped steps that have no useful details", () => {
|
|
mockPrepareStatus = "preparing";
|
|
mockSessionState = "STARTING";
|
|
mockSteps = [
|
|
{
|
|
name: "Uploading credentials",
|
|
status: "skipped",
|
|
},
|
|
{
|
|
name: "Waiting for agent controller",
|
|
status: "completed",
|
|
},
|
|
];
|
|
|
|
render(<PrepareProgress sessionId="session-1" />);
|
|
|
|
expect(screen.queryByText("Uploading credentials")).toBeNull();
|
|
expect(screen.getByText("Waiting for agent controller")).toBeTruthy();
|
|
});
|
|
|
|
it("keeps the fallback notice row visible because it carries a warning", () => {
|
|
mockPrepareStatus = "preparing";
|
|
mockSessionState = "STARTING";
|
|
mockSteps = [
|
|
{
|
|
name: "Reconnecting cloud sandbox",
|
|
status: "skipped",
|
|
warning:
|
|
"Previous sandbox is no longer available; provisioning a fresh one for this branch.",
|
|
warningDetail: "Old sandbox could not be reached.",
|
|
output: "Old sandbox: kandev-old\nNew sandbox: kandev-new\nBranch: feature/foo",
|
|
},
|
|
];
|
|
|
|
render(<PrepareProgress sessionId="session-1" />);
|
|
|
|
expect(screen.getByText("Reconnecting cloud sandbox")).toBeTruthy();
|
|
expect(
|
|
screen.getByText(
|
|
"Previous sandbox is no longer available; provisioning a fresh one for this branch.",
|
|
),
|
|
).toBeTruthy();
|
|
});
|
|
|
|
it("relabels the header when the only warning is the fallback notice", () => {
|
|
mockPrepareStatus = "completed";
|
|
mockSessionState = "RUNNING";
|
|
mockSteps = [
|
|
{
|
|
name: "Reconnecting cloud sandbox",
|
|
status: "skipped",
|
|
warning:
|
|
"Previous sandbox is no longer available; provisioning a fresh one for this branch.",
|
|
},
|
|
{ name: "Creating cloud sandbox", status: "completed" },
|
|
{ name: "Waiting for agent controller", status: "completed" },
|
|
];
|
|
|
|
render(<PrepareProgress sessionId="session-1" />);
|
|
|
|
expect(screen.getByText("Environment prepared on a fresh sandbox")).toBeTruthy();
|
|
expect(screen.queryByText("Environment prepared with warnings")).toBeNull();
|
|
});
|
|
|
|
it("uses the generic warnings header when warnings are unrelated to fallback", () => {
|
|
mockPrepareStatus = "completed";
|
|
mockSessionState = "RUNNING";
|
|
mockSteps = [
|
|
{
|
|
name: "Uploading credentials",
|
|
status: "completed",
|
|
warning: "Some credentials skipped",
|
|
},
|
|
];
|
|
|
|
render(<PrepareProgress sessionId="session-1" />);
|
|
|
|
expect(screen.getByText("Environment prepared with warnings")).toBeTruthy();
|
|
expect(screen.queryByText("Environment prepared on a fresh sandbox")).toBeNull();
|
|
});
|
|
|
|
it("keeps failed preparation diagnostics collapsed until requested", () => {
|
|
mockPrepareStatus = "failed";
|
|
mockSessionState = "FAILED";
|
|
mockPrepareError =
|
|
"branch feature/very-long-name not found locally or on remote: fatal: could not resolve host";
|
|
mockSteps = [
|
|
{
|
|
name: CREATE_WORKTREE,
|
|
status: "failed",
|
|
error: "fatal: could not resolve host github.com",
|
|
output: "git fetch origin feature/very-long-name",
|
|
},
|
|
];
|
|
|
|
render(<PrepareProgress sessionId="session-1" />);
|
|
|
|
expect(screen.getByText("Environment setup failed")).toBeTruthy();
|
|
expect(screen.queryByText(/branch feature\/very-long-name not found/)).toBeNull();
|
|
expect(screen.queryByText(CREATE_WORKTREE)).toBeNull();
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Show preparation details" }));
|
|
|
|
expect(screen.getByText(/branch feature\/very-long-name not found/)).toBeTruthy();
|
|
expect(screen.getByText(CREATE_WORKTREE)).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
function makeSetupScriptMessage(overrides: Partial<Message["metadata"] & object> = {}): Message {
|
|
return {
|
|
id: "msg-1",
|
|
task_id: "task-1" as Message["task_id"],
|
|
session_id: "session-1" as Message["session_id"],
|
|
author_type: "agent",
|
|
content: "Installing deps...\nDone.",
|
|
type: "script_execution",
|
|
created_at: "2026-05-27T19:06:51Z",
|
|
metadata: {
|
|
script_type: "setup",
|
|
command: "make install",
|
|
status: "exited",
|
|
exit_code: 0,
|
|
started_at: "2026-05-27T19:06:51Z",
|
|
completed_at: "2026-05-27T19:06:54Z",
|
|
...overrides,
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("PrepareProgress per-repo setup script", () => {
|
|
afterEach(() => {
|
|
cleanup();
|
|
mockMessages = [];
|
|
});
|
|
|
|
it("renders the per-repo setup script as a step inside the panel", () => {
|
|
// `preparing` keeps the panel auto-expanded so step rows render.
|
|
mockPrepareStatus = "preparing";
|
|
mockSessionState = "STARTING";
|
|
mockSteps = [
|
|
{ name: "Validate repository", status: "completed" },
|
|
{ name: CREATE_WORKTREE, status: "completed" },
|
|
];
|
|
mockMessages = [makeSetupScriptMessage()];
|
|
|
|
render(<PrepareProgress sessionId="session-1" />);
|
|
|
|
expect(screen.getByText("Run repository setup script")).toBeTruthy();
|
|
expect(screen.getByText("make install")).toBeTruthy();
|
|
});
|
|
|
|
it("marks a failed setup script as a failed step with an error message", () => {
|
|
// `failed` keeps the panel auto-expanded.
|
|
mockPrepareStatus = "failed";
|
|
mockSessionState = "FAILED";
|
|
mockSteps = [{ name: CREATE_WORKTREE, status: "completed" }];
|
|
mockMessages = [makeSetupScriptMessage({ exit_code: 2 })];
|
|
|
|
render(<PrepareProgress sessionId="session-1" />);
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Show preparation details" }));
|
|
expect(screen.getByText("Script exited with code 2")).toBeTruthy();
|
|
});
|
|
});
|