Production typecheck (tsconfig.json + tsconfig.app.json) was already
clean, but a third config that includes test files surfaced 661 errors
across 60+ test files — accumulated drift between mock fixtures and
production types. Six parallel typescript-pro agents fixed every one
without touching production code.
Per-scope before/after (errors → 0):
ChatView 183
Mailbox + Agent suite (5 files) 156
Task / Modal suite (6 files) 127
App + small components (12 files) 96
Hooks + api/auth (8 files) 48
Long tail (32 files) 51
-----------------------------------------------------------------
Total 661
Major fix categories:
- Untyped state objects inferring `never[]` / `null` literals (root
cause of ~120 errors in ChatView alone — added a single
`UseChatReturn` annotation)
- Mock objects missing fields that became required: `WorkflowStep.mode`,
`ChatMessage.thinkingOutput / metadata`, `ChatSession.projectId`,
`Task.log`, `ProjectHealth` fields, `PtyTerminalSessionInfo.createdAt`,
`Agent.metadata`, `InboxResponse.total`, etc.
- Mock objects with stale fields that no longer exist:
`AgentBudgetStatus.budgetPeriod`, `truncated` on log responses,
`OutboxResponse.unreadCount`, `MergeResult.source/target/details`
- Modal props that became required (e.g. `PlanningModeModal.onTasksCreated`)
- String literals not in narrowed unions (`Column`, `WorkflowStepPhase`,
`InsightStatus`, `AgentLogType`, etc.)
- `querySelector` returning `Element` cast to `HTMLElement` for
`@testing-library/react`'s `within()`
- Vitest mock typing: `.mock.calls` access needing `vi.mocked(...)`,
zero-param tuple handling, generic `vi.fn(() => [])` inferring
`never[]`
Helpers introduced in test files (no shared infra):
- `makeSettings(overrides)` in ModelSelectorTab.test.tsx
- `makePromptOverrides(overrides)` in AgentPromptsManager.test.tsx
- `FileBrowserTestOverrides` type alias in FileBrowser.test.tsx
- `makeInboxResponse / makeOutboxResponse` in MailboxView.test.tsx
Verification:
- tsc -p tsconfig.json: exit 0
- tsc -p tsconfig.app.json: exit 0
- tsc -p tsconfig.test-check.json (new — includes test files): exit 0
- vitest run: 9639 / 9641 (2 pre-existing failures
in terminal-mobile-keyboard-layout.test.ts
unrelated to this work; verified via
`git stash` + run on clean HEAD)
Adds packages/dashboard/tsconfig.test-check.json to keep this regression
guard available locally — same as tsconfig.app.json minus the test
exclude.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
247 lines
7.4 KiB
TypeScript
247 lines
7.4 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { loadAllAppCss } from "../../test/cssFixture";
|
|
import { fireEvent, render, screen } from "@testing-library/react";
|
|
import type { Agent, AiSessionSummary } from "../../api";
|
|
import type { Toast } from "../../hooks/useToast";
|
|
|
|
vi.mock("../../hooks/useExecutorStats", () => ({
|
|
useExecutorStats: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../../hooks/useLiveTranscript", () => ({
|
|
useLiveTranscript: vi.fn(() => ({
|
|
entries: [],
|
|
isConnected: false,
|
|
})),
|
|
}));
|
|
|
|
import { useExecutorStats } from "../../hooks/useExecutorStats";
|
|
import { BackgroundTasksIndicator } from "../BackgroundTasksIndicator";
|
|
import { ExecutorStatusBar } from "../ExecutorStatusBar";
|
|
import { ActiveAgentsPanel } from "../ActiveAgentsPanel";
|
|
import { ToastContainer } from "../ToastContainer";
|
|
|
|
|
|
function escapeRegExp(value: string): string {
|
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
}
|
|
|
|
function expectMobileRule(css: string, selector: string, declaration: string): void {
|
|
const pattern = new RegExp(
|
|
`@media\\s*\\(max-width:\\s*768px\\)\\s*\\{[\\s\\S]*?${escapeRegExp(selector)}\\s*\\{[\\s\\S]*?${escapeRegExp(declaration)}`,
|
|
);
|
|
expect(pattern.test(css)).toBe(true);
|
|
}
|
|
|
|
describe("Utility component mobile adaptations", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.mocked(useExecutorStats).mockReturnValue({
|
|
stats: {
|
|
runningTaskCount: 1,
|
|
blockedTaskCount: 2,
|
|
stuckTaskCount: 0,
|
|
queuedTaskCount: 3,
|
|
inReviewCount: 4,
|
|
executorState: "running",
|
|
maxConcurrent: 5,
|
|
lastActivityAt: new Date().toISOString(),
|
|
},
|
|
loading: false,
|
|
error: null,
|
|
refresh: vi.fn(),
|
|
});
|
|
});
|
|
|
|
it("renders BackgroundTasksIndicator pill when sessions exist", () => {
|
|
const sessions: AiSessionSummary[] = [
|
|
{
|
|
id: "sess-1",
|
|
type: "planning",
|
|
status: "generating",
|
|
title: "Refine onboarding flow",
|
|
projectId: "proj-1",
|
|
lockedByTab: null,
|
|
updatedAt: new Date().toISOString(),
|
|
},
|
|
];
|
|
|
|
render(
|
|
<BackgroundTasksIndicator
|
|
sessions={sessions}
|
|
generating={1}
|
|
needsInput={0}
|
|
onOpenSession={vi.fn()}
|
|
onDismissSession={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
expect(screen.getByRole("button", { name: /AI 1/i })).toBeTruthy();
|
|
});
|
|
|
|
it("renders BackgroundTasksIndicator popover on pill click", () => {
|
|
const sessions: AiSessionSummary[] = [
|
|
{
|
|
id: "sess-2",
|
|
type: "subtask",
|
|
status: "awaiting_input",
|
|
title: "Break down API tasks",
|
|
projectId: "proj-1",
|
|
lockedByTab: null,
|
|
updatedAt: new Date().toISOString(),
|
|
},
|
|
];
|
|
|
|
render(
|
|
<BackgroundTasksIndicator
|
|
sessions={sessions}
|
|
generating={0}
|
|
needsInput={1}
|
|
onOpenSession={vi.fn()}
|
|
onDismissSession={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: /AI 1/i }));
|
|
|
|
expect(screen.getByText("Background Tasks")).toBeTruthy();
|
|
expect(screen.getByText("Break down API tasks")).toBeTruthy();
|
|
});
|
|
|
|
it("returns null for BackgroundTasksIndicator with no sessions", () => {
|
|
const { container } = render(
|
|
<BackgroundTasksIndicator
|
|
sessions={[]}
|
|
generating={0}
|
|
needsInput={0}
|
|
onOpenSession={vi.fn()}
|
|
onDismissSession={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
expect(container.firstChild).toBeNull();
|
|
});
|
|
|
|
it("calls onOpenSession when clicking on a milestone_interview session item", () => {
|
|
const onOpenSession = vi.fn();
|
|
const sessions: AiSessionSummary[] = [
|
|
{
|
|
id: "sess-milestone-1",
|
|
type: "milestone_interview",
|
|
status: "awaiting_input",
|
|
title: "Plan milestone scope",
|
|
projectId: "proj-1",
|
|
lockedByTab: null,
|
|
updatedAt: new Date().toISOString(),
|
|
},
|
|
];
|
|
|
|
render(
|
|
<BackgroundTasksIndicator
|
|
sessions={sessions}
|
|
generating={0}
|
|
needsInput={1}
|
|
onOpenSession={onOpenSession}
|
|
onDismissSession={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: /AI 1/i }));
|
|
fireEvent.click(screen.getByText("Plan milestone scope"));
|
|
|
|
expect(onOpenSession).toHaveBeenCalledWith(sessions[0]);
|
|
});
|
|
|
|
it("calls onOpenSession when clicking on a slice_interview session item", () => {
|
|
const onOpenSession = vi.fn();
|
|
const sessions: AiSessionSummary[] = [
|
|
{
|
|
id: "sess-slice-1",
|
|
type: "slice_interview",
|
|
status: "error",
|
|
title: "Plan slice scope",
|
|
projectId: "proj-1",
|
|
lockedByTab: null,
|
|
updatedAt: new Date().toISOString(),
|
|
},
|
|
];
|
|
|
|
render(
|
|
<BackgroundTasksIndicator
|
|
sessions={sessions}
|
|
generating={0}
|
|
needsInput={0}
|
|
onOpenSession={onOpenSession}
|
|
onDismissSession={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: /AI 1/i }));
|
|
fireEvent.click(screen.getByText("Plan slice scope"));
|
|
|
|
expect(onOpenSession).toHaveBeenCalledWith(sessions[0]);
|
|
});
|
|
|
|
it("renders ExecutorStatusBar segments", () => {
|
|
render(<ExecutorStatusBar tasks={[]} />);
|
|
|
|
const bar = screen.getByRole("status");
|
|
expect(bar).toHaveTextContent("Running");
|
|
expect(bar).toHaveTextContent("Blocked");
|
|
expect(bar).toHaveTextContent("Queued");
|
|
expect(bar).toHaveTextContent("In Review");
|
|
});
|
|
|
|
it("renders ActiveAgentsPanel grid and cards when agents are provided", () => {
|
|
const agents: Agent[] = [
|
|
{
|
|
id: "agent-1",
|
|
name: "Live Agent",
|
|
role: "executor",
|
|
state: "active",
|
|
taskId: "FN-555",
|
|
lastHeartbeatAt: new Date().toISOString(),
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
metadata: {},
|
|
},
|
|
];
|
|
|
|
const { container } = render(<ActiveAgentsPanel agents={agents} />);
|
|
|
|
expect(container.querySelector(".active-agents-grid")).toBeTruthy();
|
|
expect(container.querySelectorAll(".live-agent-card").length).toBe(1);
|
|
});
|
|
|
|
it("returns null for ActiveAgentsPanel when no agents are active", () => {
|
|
const { container } = render(<ActiveAgentsPanel agents={[]} />);
|
|
expect(container.firstChild).toBeNull();
|
|
});
|
|
|
|
it("renders toasts in ToastContainer", () => {
|
|
const toasts: Toast[] = [
|
|
{ id: 1, message: "Saved", type: "success" },
|
|
{ id: 2, message: "Failed", type: "error" },
|
|
];
|
|
|
|
const { container } = render(<ToastContainer toasts={toasts} onRemove={vi.fn()} />);
|
|
|
|
expect(container.querySelector(".toast-container")).toBeTruthy();
|
|
expect(container.querySelector(".toast-success")).toBeTruthy();
|
|
expect(container.querySelector(".toast-error")).toBeTruthy();
|
|
});
|
|
|
|
it("contains mobile CSS overrides for adapted utility and layout components", () => {
|
|
const css = loadAllAppCss();
|
|
|
|
expectMobileRule(css, ".settings-layout", "flex-direction: column;");
|
|
expectMobileRule(css, ".agent-board", "grid-template-columns: 1fr;");
|
|
expectMobileRule(css, ".active-agents-grid", "grid-template-columns: 1fr;");
|
|
expectMobileRule(css, ".toast-container", "top: calc(var(--header-height, 57px) + env(safe-area-inset-top, 0px) + var(--space-sm));");
|
|
expectMobileRule(css, ".toast-container", "bottom: auto;");
|
|
expectMobileRule(css, ".toast-container", "right: var(--space-sm);");
|
|
expectMobileRule(css, ".toast-container", "left: var(--space-sm);");
|
|
expectMobileRule(css, ".background-tasks-indicator__popover", "position: fixed;");
|
|
});
|
|
});
|