Files
fusion/packages/dashboard/src/__tests__/agent-onboarding.test.ts
Fusion f51ec4d3b5 feat(FN-3554): expand AI agent interview onboarding flow
Reuses the experimental onboarding modal for both create and edit, makes
onboarding mode-aware, and plumbs draft handling through the agent detail
view, new agent dialog, and import/export/generation routes.

Fusion-Task-Id: FN-3554
2026-05-07 08:49:18 -07:00

309 lines
9.3 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const { mockCreateFnAgent } = vi.hoisted(() => ({
mockCreateFnAgent: vi.fn(),
}));
vi.mock("@fusion/engine", () => ({
createFnAgent: mockCreateFnAgent,
}));
import {
__resetAgentOnboardingState,
cancelAgentOnboardingSession,
createAgentOnboardingSessionPrompt,
getAgentOnboardingSession,
getAgentOnboardingSummary,
InvalidSessionStateError,
parseAgentOnboardingResponse,
respondToAgentOnboarding,
SessionNotFoundError,
startAgentOnboardingSession,
} from "../agent-onboarding.js";
function createMockAgent(responses: string[]) {
const queue = [...responses];
const messages: Array<{ role: string; content: string }> = [];
return {
session: {
state: { messages },
prompt: vi.fn(async () => {
const response = queue.shift() ?? queue[queue.length - 1] ?? "{}";
messages.push({ role: "assistant", content: response });
}),
dispose: vi.fn(),
},
};
}
async function waitFor(check: () => boolean, timeoutMs = 2000): Promise<void> {
const start = Date.now();
while (!check()) {
if (Date.now() - start > timeoutMs) {
throw new Error("Timed out waiting for condition");
}
await new Promise((resolve) => setTimeout(resolve, 10));
}
}
describe("agent-onboarding", () => {
beforeEach(() => {
vi.clearAllMocks();
__resetAgentOnboardingState();
});
afterEach(() => {
__resetAgentOnboardingState();
});
it("parses question responses", () => {
const parsed = parseAgentOnboardingResponse(
JSON.stringify({
type: "question",
data: {
id: "q1",
type: "text",
question: "What should this agent focus on?",
},
}),
);
expect(parsed.type).toBe("question");
if (parsed.type === "question") {
expect(parsed.data.id).toBe("q1");
}
});
it("parses complete summary responses", () => {
const parsed = parseAgentOnboardingResponse(
JSON.stringify({
type: "complete",
data: {
name: "Docs Reviewer",
role: "reviewer",
instructionsText: "Review docs for clarity and accuracy.",
thinkingLevel: "medium",
maxTurns: 20,
},
}),
);
expect(parsed.type).toBe("complete");
if (parsed.type === "complete") {
expect(parsed.data.name).toBe("Docs Reviewer");
expect(parsed.data.maxTurns).toBe(20);
}
});
it("rejects invalid complete summary", () => {
expect(() =>
parseAgentOnboardingResponse(
JSON.stringify({
type: "complete",
data: {
name: "",
role: "reviewer",
instructionsText: "",
thinkingLevel: "medium",
maxTurns: 0,
},
}),
),
).toThrow(/Invalid summary/);
});
it("builds compact onboarding context prompt for create mode", () => {
const prompt = createAgentOnboardingSessionPrompt({
mode: "create",
intent: "Need a reviewer for docs",
existingAgents: [{ id: "a1", name: "Alpha", role: "reviewer" }],
templates: [{ id: "t1", label: "Reviewer Template", description: "General reviewer" }],
});
expect(prompt).toContain("Need a reviewer for docs");
expect(prompt).toContain("a1:Alpha(reviewer)");
expect(prompt).toContain("t1:Reviewer Template");
expect(prompt).not.toContain("Current agent configuration:");
});
it("appends current agent configuration in edit mode prompt", () => {
const prompt = createAgentOnboardingSessionPrompt({
mode: "edit",
intent: "Improve this agent",
existingAgents: [{ id: "a1", name: "Alpha", role: "reviewer" }],
templates: [{ id: "t1", label: "Reviewer Template", description: "General reviewer" }],
existingAgentConfig: {
name: "Alpha",
role: "reviewer",
title: "Senior Reviewer",
instructionsText: "Current instructions",
soul: "Calm",
memory: "Team context",
reportsTo: "mgr-1",
skills: ["linting"],
model: "openai/gpt-5-mini",
thinkingLevel: "low",
maxTurns: 40,
runtimeHint: "gpu",
heartbeatIntervalMs: 30000,
heartbeatTimeoutMs: 120000,
maxConcurrentRuns: 2,
messageResponseMode: "immediate",
},
});
expect(prompt).toContain("Current agent configuration:");
expect(prompt).toContain("name: Alpha");
expect(prompt).toContain("instructionsText: Current instructions");
expect(prompt).toContain("messageResponseMode: immediate");
});
it("progresses through start -> question -> response -> final summary", async () => {
mockCreateFnAgent.mockResolvedValueOnce(
createMockAgent([
JSON.stringify({
type: "question",
data: { id: "goal", type: "text", question: "What is the primary goal?" },
}),
JSON.stringify({
type: "complete",
data: {
name: "Repo Steward",
role: "engineer",
instructionsText: "Keep the repo healthy and triage drift.",
thinkingLevel: "low",
maxTurns: 18,
templateId: "eng-template",
rationale: "Template path selected",
},
}),
]),
);
const sessionId = await startAgentOnboardingSession(
"127.0.0.1",
{
intent: "I need an engineer for repository hygiene",
existingAgents: [{ id: "agent-1", name: "Alpha", role: "engineer" }],
templates: [{ id: "eng-template", label: "Engineer template" }],
},
process.cwd(),
);
await waitFor(() => Boolean(getAgentOnboardingSession(sessionId)?.currentQuestion));
const session = getAgentOnboardingSession(sessionId);
expect(session?.currentQuestion?.id).toBe("goal");
await respondToAgentOnboarding(sessionId, { goal: "Keep CI green" });
await waitFor(() => Boolean(getAgentOnboardingSummary(sessionId)));
const summary = getAgentOnboardingSummary(sessionId);
expect(summary?.name).toBe("Repo Steward");
expect(summary?.templateId).toBe("eng-template");
});
it("defaults onboarding sessions to create mode", async () => {
mockCreateFnAgent.mockResolvedValueOnce(
createMockAgent([
JSON.stringify({
type: "question",
data: { id: "q1", type: "text", question: "What should this agent do?" },
}),
]),
);
const sessionId = await startAgentOnboardingSession(
"127.0.0.1",
{ intent: "create", existingAgents: [], templates: [] },
process.cwd(),
);
await waitFor(() => Boolean(getAgentOnboardingSession(sessionId)));
expect(getAgentOnboardingSession(sessionId)?.mode).toBe("create");
});
it("stores edit mode sessions and includes current config in agent prompt", async () => {
mockCreateFnAgent.mockResolvedValueOnce(
createMockAgent([
JSON.stringify({
type: "question",
data: { id: "q1", type: "text", question: "What should change?" },
}),
]),
);
const sessionId = await startAgentOnboardingSession(
"127.0.0.1",
{
mode: "edit",
intent: "Improve this agent",
existingAgents: [],
templates: [],
existingAgentConfig: {
name: "Editor",
instructionsText: "Current instructions",
messageResponseMode: "on-heartbeat",
},
},
process.cwd(),
);
await waitFor(() => Boolean(getAgentOnboardingSession(sessionId)?.currentQuestion));
expect(getAgentOnboardingSession(sessionId)?.mode).toBe("edit");
expect(getAgentOnboardingSession(sessionId)?.contextPrompt).toContain("Current agent configuration:");
expect(getAgentOnboardingSession(sessionId)?.contextPrompt).toContain("name: Editor");
});
it("throws InvalidSessionStateError when responding without an active question", async () => {
mockCreateFnAgent.mockResolvedValueOnce(
createMockAgent([
JSON.stringify({
type: "complete",
data: {
name: "Direct Summary",
role: "reviewer",
instructionsText: "Review with no follow-up",
thinkingLevel: "minimal",
maxTurns: 12,
},
}),
]),
);
const sessionId = await startAgentOnboardingSession(
"127.0.0.1",
{ intent: "quick", existingAgents: [], templates: [] },
process.cwd(),
);
await waitFor(() => Boolean(getAgentOnboardingSummary(sessionId)));
await expect(respondToAgentOnboarding(sessionId, { followup: "anything" })).rejects.toBeInstanceOf(
InvalidSessionStateError,
);
});
it("cancels session and subsequent access fails", async () => {
mockCreateFnAgent.mockResolvedValueOnce(
createMockAgent([
JSON.stringify({
type: "question",
data: { id: "q1", type: "text", question: "Question before cancel" },
}),
]),
);
const sessionId = await startAgentOnboardingSession(
"127.0.0.1",
{ intent: "cancel flow", existingAgents: [], templates: [] },
process.cwd(),
);
await waitFor(() => Boolean(getAgentOnboardingSession(sessionId)?.currentQuestion));
await cancelAgentOnboardingSession(sessionId);
expect(getAgentOnboardingSession(sessionId)).toBeUndefined();
await expect(respondToAgentOnboarding(sessionId, { q1: "x" })).rejects.toBeInstanceOf(SessionNotFoundError);
});
});