Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/web/src/app/projects/[projectId]/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,7 @@ describe("ProjectPage", () => {
expect(screen.getByText("This project's config failed to load")).toBeInTheDocument();
expect(screen.getByText("Local config failed validation")).toBeInTheDocument();
expect(screen.queryByTestId("dashboard")).not.toBeInTheDocument();
expect(screen.getByRole("link", { name: "Back to dashboard" })).toHaveAttribute("href", "/");
expect(screen.queryByRole("link", { name: "Edit settings" })).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,7 @@ describe("ProjectSettingsPage", () => {
).toBeInTheDocument();
expect(screen.getByText("Local config failed validation")).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Save changes" })).not.toBeInTheDocument();
expect(screen.getByRole("link", { name: "Back to dashboard" })).toHaveAttribute("href", "/");
expect(screen.queryByRole("link", { name: "Edit settings" })).not.toBeInTheDocument();
});
});
12 changes: 3 additions & 9 deletions packages/web/src/components/DegradedProjectState.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Link from "next/link";
import { projectDashboardPath } from "@/lib/routes";

import { RepairDegradedProjectButton } from "./RepairDegradedProjectButton";

interface DegradedProjectStateProps {
Expand Down Expand Up @@ -61,16 +61,10 @@ export function DegradedProjectState({

<div className="mt-6 flex flex-wrap gap-3">
<Link
href={projectDashboardPath(projectId)}
className="rounded-lg border border-[var(--color-border-default)] px-4 py-2 text-sm font-medium text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-bg-elevated-hover)]"
>
Back to project
</Link>
<Link
href={projectDashboardPath(projectId)}
href="/"
className="rounded-lg border border-[var(--color-border-default)] px-4 py-2 text-sm font-medium text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-bg-elevated-hover)]"
>
Open dashboard view
Back to dashboard
</Link>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { DegradedProjectState } from "@/components/DegradedProjectState";

vi.mock("next/link", () => ({
default: ({
children,
...props
}: React.PropsWithChildren<React.AnchorHTMLAttributes<HTMLAnchorElement>>) => (
<a {...props}>{children}</a>
),
}));

vi.mock("@/components/RepairDegradedProjectButton", () => ({
RepairDegradedProjectButton: ({ projectId }: { projectId: string }) => (
<button>Repair {projectId}</button>
),
}));

const baseProps = {
projectId: "my-project",
resolveError: "Local config at /tmp/my-project/agent-orchestrator.yaml failed validation: bad field",
projectPath: "/tmp/my-project",
};

describe("DegradedProjectState", () => {
it("renders the default heading", () => {
render(<DegradedProjectState {...baseProps} />);
expect(screen.getByText("This project's config failed to load")).toBeInTheDocument();
});

it("renders a custom heading when provided", () => {
render(<DegradedProjectState {...baseProps} heading="Custom heading" />);
expect(screen.getByText("Custom heading")).toBeInTheDocument();
});

it("displays the resolve error", () => {
render(<DegradedProjectState {...baseProps} />);
expect(screen.getByText(baseProps.resolveError)).toBeInTheDocument();
});

it("extracts and displays the config path from the resolve error", () => {
render(<DegradedProjectState {...baseProps} />);
expect(
screen.getByText("/tmp/my-project/agent-orchestrator.yaml"),
).toBeInTheDocument();
});

it("falls back to the project path when the config path cannot be parsed from the error", () => {
render(
<DegradedProjectState
{...baseProps}
resolveError="Something went wrong"
/>,
);
expect(
screen.getByText("/tmp/my-project/agent-orchestrator.yaml or .yml"),
).toBeInTheDocument();
});

it("renders 'Back to dashboard' link pointing to /", () => {
render(<DegradedProjectState {...baseProps} />);
const link = screen.getByRole("link", { name: "Back to dashboard" });
expect(link).toHaveAttribute("href", "/");
});

it("does not render an 'Edit settings' link", () => {
render(<DegradedProjectState {...baseProps} />);
expect(screen.queryByRole("link", { name: "Edit settings" })).not.toBeInTheDocument();
});

it("does not show the repair button for a generic validation error", () => {
render(<DegradedProjectState {...baseProps} />);
expect(screen.queryByRole("button", { name: /repair/i })).not.toBeInTheDocument();
});

it("shows the repair button when the error indicates a wrapped projects format", () => {
render(
<DegradedProjectState
{...baseProps}
resolveError="Local config at /tmp/x/agent-orchestrator.yaml still uses a wrapped projects: format"
/>,
);
expect(screen.getByRole("button", { name: /repair/i })).toBeInTheDocument();
});
});
Loading