feat(FN-3559): cover launch flow and summary handoff (+1 more)
Merges agent onboarding test coverage (FN-3559: lock contract for modal/dialog flows, launch flow, summary handoff), adds heartbeat readonly gating with action-gate reload endpoints (FN-3734/FN-3730), fixes a service worker blank-page issue on first install, and documents dependency graph behaviors. Fusion-Task-Id: FN-3559
This commit is contained in:
@@ -1767,7 +1767,7 @@ describe("agent onboarding API wrappers", () => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it("starts onboarding streaming session with context payload", async () => {
|
||||
it("starts onboarding streaming session with create context payload", async () => {
|
||||
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, { sessionId: "onb-1" }, 201));
|
||||
|
||||
const result = await startAgentOnboardingStreaming(
|
||||
@@ -1775,6 +1775,7 @@ describe("agent onboarding API wrappers", () => {
|
||||
{
|
||||
existingAgents: [{ id: "agent-1", name: "Reviewer", role: "reviewer" }],
|
||||
templates: [{ id: "preset-1", label: "Reviewer preset" }],
|
||||
mode: "create",
|
||||
},
|
||||
"proj-123",
|
||||
{ planningModelProvider: "openai", planningModelId: "gpt-4o" },
|
||||
@@ -1789,13 +1790,67 @@ describe("agent onboarding API wrappers", () => {
|
||||
context: {
|
||||
existingAgents: [{ id: "agent-1", name: "Reviewer", role: "reviewer" }],
|
||||
templates: [{ id: "preset-1", label: "Reviewer preset" }],
|
||||
mode: "create",
|
||||
},
|
||||
mode: "create",
|
||||
planningModelProvider: "openai",
|
||||
planningModelId: "gpt-4o",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it("starts onboarding streaming session with edit context payload", async () => {
|
||||
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, { sessionId: "onb-edit" }, 201));
|
||||
|
||||
const result = await startAgentOnboardingStreaming(
|
||||
"Tighten this agent's review quality",
|
||||
{
|
||||
existingAgents: [{ id: "agent-1", name: "Reviewer", role: "reviewer" }],
|
||||
templates: [{ id: "preset-1", label: "Reviewer preset" }],
|
||||
mode: "edit",
|
||||
existingAgentConfig: {
|
||||
name: "Reviewer",
|
||||
role: "reviewer",
|
||||
instructionsText: "Current instructions",
|
||||
runtimeHint: "openclaw",
|
||||
heartbeatIntervalMs: 30000,
|
||||
},
|
||||
},
|
||||
"proj-123",
|
||||
);
|
||||
|
||||
expect(result.sessionId).toBe("onb-edit");
|
||||
expect(globalThis.fetch).toHaveBeenCalledWith(
|
||||
"/api/agents/onboarding/start-streaming?projectId=proj-123",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
intent: "Tighten this agent's review quality",
|
||||
context: {
|
||||
existingAgents: [{ id: "agent-1", name: "Reviewer", role: "reviewer" }],
|
||||
templates: [{ id: "preset-1", label: "Reviewer preset" }],
|
||||
mode: "edit",
|
||||
existingAgentConfig: {
|
||||
name: "Reviewer",
|
||||
role: "reviewer",
|
||||
instructionsText: "Current instructions",
|
||||
runtimeHint: "openclaw",
|
||||
heartbeatIntervalMs: 30000,
|
||||
},
|
||||
},
|
||||
mode: "edit",
|
||||
existingAgentConfig: {
|
||||
name: "Reviewer",
|
||||
role: "reviewer",
|
||||
instructionsText: "Current instructions",
|
||||
runtimeHint: "openclaw",
|
||||
heartbeatIntervalMs: 30000,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("posts onboarding response/retry/stop/cancel endpoints", async () => {
|
||||
globalThis.fetch = vi.fn()
|
||||
.mockReturnValueOnce(mockFetchResponse(true, { type: "question", data: { id: "q1", type: "text", question: "?" } }))
|
||||
|
||||
@@ -1884,6 +1884,22 @@ describe("AgentDetailView", () => {
|
||||
expect((screen.getByLabelText("Role") as HTMLSelectElement).value).toBe("reviewer");
|
||||
});
|
||||
expect(mockUpdateAgent).not.toHaveBeenCalled();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Save Settings" }));
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateAgent).toHaveBeenCalledWith(
|
||||
"agent-001",
|
||||
expect.objectContaining({
|
||||
name: "Interviewed Agent",
|
||||
role: "reviewer",
|
||||
title: "Draft Title",
|
||||
reportsTo: "agent-002",
|
||||
runtimeConfig: expect.objectContaining({ model: "openai/gpt-4o" }),
|
||||
metadata: { skills: ["skill-1"] },
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows settings delete control for idle and paused agents", async () => {
|
||||
|
||||
@@ -30,6 +30,33 @@ vi.mock("../../api", async (importOriginal) => {
|
||||
});
|
||||
});
|
||||
|
||||
vi.mock("../ExperimentalAgentOnboardingModal", () => ({
|
||||
ExperimentalAgentOnboardingModal: ({ isOpen, onClose, onUseDraft }: { isOpen: boolean; onClose: () => void; onUseDraft: (draft: any) => void }) => {
|
||||
if (!isOpen) return null;
|
||||
return (
|
||||
<div role="dialog" aria-label="AI Interview">
|
||||
<p>Draft ready for review</p>
|
||||
<button type="button" onClick={onClose}>Cancel</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onUseDraft({
|
||||
name: "Interview Draft Agent",
|
||||
role: "reviewer",
|
||||
title: "Drafted Title",
|
||||
instructionsText: "Drafted instructions",
|
||||
thinkingLevel: "low",
|
||||
maxTurns: 10,
|
||||
})
|
||||
}
|
||||
>
|
||||
Apply draft to agent form
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../AgentDetailView", () => ({
|
||||
AgentDetailView: ({ agentId, inline, onClose, showInlineBackButton, initialTab, initialRunId, preferActiveRun, onMutationSuccess }: { agentId: string; inline?: boolean; onClose?: () => void; showInlineBackButton?: boolean; initialTab?: string; initialRunId?: string | null; preferActiveRun?: boolean; onMutationSuccess?: (context: { agentId: string; deleted?: boolean }) => void | Promise<void> }) => (
|
||||
<div data-testid="agent-detail-view" data-inline={inline ? "true" : "false"} data-initial-tab={initialTab ?? "dashboard"} data-initial-run-id={initialRunId ?? ""} data-prefer-active-run={preferActiveRun ? "true" : "false"}>
|
||||
@@ -1633,6 +1660,41 @@ describe("AgentsView", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("launches interview from AgentsView and only applies draft after review confirmation", async () => {
|
||||
render(<AgentsView addToast={mockAddToast} agentOnboardingEnabled={true} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("New Agent")).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText("New Agent"));
|
||||
fireEvent.click(screen.getByRole("button", { name: "AI Interview" }));
|
||||
|
||||
const interviewDialog = await screen.findByRole("dialog", { name: "AI Interview" });
|
||||
expect(screen.getByText("Draft ready for review")).toBeTruthy();
|
||||
expect(mockCreateAgent).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.click(within(interviewDialog).getByRole("button", { name: "Cancel" }));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("dialog", { name: "AI Interview" })).toBeNull();
|
||||
});
|
||||
expect(mockCreateAgent).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "AI Interview" }));
|
||||
await screen.findByRole("dialog", { name: "AI Interview" });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Apply draft to agent form" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Back" })).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Back" }));
|
||||
fireEvent.click(screen.getByRole("tab", { name: "Custom agent" }));
|
||||
const nameInput = screen.getByLabelText(/Name/) as HTMLInputElement;
|
||||
expect(nameInput.value).toBe("Interview Draft Agent");
|
||||
expect(mockCreateAgent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not allow proceeding with empty name", async () => {
|
||||
render(<AgentsView addToast={mockAddToast} />);
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ const mockStartAgentOnboardingStreaming = vi.mocked(apiModule.startAgentOnboardi
|
||||
describe("ExperimentalAgentOnboardingModal", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
streamHandlers = undefined;
|
||||
});
|
||||
|
||||
it("renders draft review and only applies after explicit confirmation", async () => {
|
||||
@@ -166,6 +167,35 @@ describe("ExperimentalAgentOnboardingModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not apply draft when cancelled from question step", async () => {
|
||||
const onClose = vi.fn();
|
||||
const onUseDraft = vi.fn();
|
||||
|
||||
render(
|
||||
<ExperimentalAgentOnboardingModal
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
onUseDraft={onUseDraft}
|
||||
existingAgents={[]}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByLabelText("What should this new agent own?"), { target: { value: "Review docs" } });
|
||||
fireEvent.click(screen.getByText("Start onboarding"));
|
||||
|
||||
await screen.findByText("What should this agent primarily help with?");
|
||||
expect(screen.queryByText("Draft ready for review")).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: "Apply draft to agent form" })).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByText("Cancel"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCancel).toHaveBeenCalledWith("onb-1", undefined);
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
expect(onUseDraft).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("cancels server session on close", async () => {
|
||||
const onClose = vi.fn();
|
||||
const onUseDraft = vi.fn();
|
||||
|
||||
@@ -40,9 +40,17 @@ vi.mock("../ExperimentalAgentOnboardingModal", () => ({
|
||||
name: "Interview Draft",
|
||||
role: "reviewer",
|
||||
title: "Interview Title",
|
||||
icon: "🤖",
|
||||
reportsTo: "agent-manager-1",
|
||||
instructionsText: "Interview instructions",
|
||||
soul: "Interview soul",
|
||||
memory: "Interview memory",
|
||||
skills: ["skill-1"],
|
||||
heartbeatProcedurePath: ".fusion/agents/interview/HEARTBEAT.md",
|
||||
runtimeHint: "openclaw",
|
||||
thinkingLevel: "low",
|
||||
maxTurns: 12,
|
||||
heartbeatIntervalMs: 45000,
|
||||
})}
|
||||
>
|
||||
Apply Interview Draft
|
||||
@@ -305,15 +313,24 @@ describe("NewAgentDialog", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onPrefillDraft).toHaveBeenCalledWith(expect.objectContaining({ name: "Interview Draft" }));
|
||||
expect(screen.getByRole("button", { name: "Model" })).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Runtime")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(mockCreateAgent).not.toHaveBeenCalled();
|
||||
expect(screen.getByLabelText("Runtime")).toBeInTheDocument();
|
||||
expect((screen.getByLabelText("Runtime") as HTMLSelectElement).value).toBe("openclaw");
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Back" }));
|
||||
expect((getStepZeroField(/Name/) as HTMLInputElement).value).toBe("Interview Draft");
|
||||
expect((getStepZeroField(/Title/) as HTMLInputElement).value).toBe("Interview Title");
|
||||
expect((getStepZeroField(/Icon/) as HTMLInputElement).value).toBe("🤖");
|
||||
expect((getStepZeroField(/Reports To/) as HTMLSelectElement).value).toBe("agent-manager-1");
|
||||
expect((getStepZeroField(/Soul/) as HTMLTextAreaElement).value).toBe("Interview soul");
|
||||
expect((getStepZeroField(/Agent Memory/) as HTMLTextAreaElement).value).toBe("Interview memory");
|
||||
expect((getStepZeroField(/Heartbeat Procedure Path/) as HTMLInputElement).value).toBe(".fusion/agents/interview/HEARTBEAT.md");
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Next" }));
|
||||
expect((screen.getByTestId("skill-multiselect-value")).textContent).toContain("skill-1");
|
||||
await user.click(screen.getByRole("button", { name: "Next" }));
|
||||
expect(mockCreateAgent).not.toHaveBeenCalled();
|
||||
await user.click(screen.getByRole("button", { name: "Create" }));
|
||||
@@ -321,6 +338,17 @@ describe("NewAgentDialog", () => {
|
||||
await waitFor(() => {
|
||||
expect(mockCreateAgent).toHaveBeenCalledOnce();
|
||||
});
|
||||
expect(mockCreateAgent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: "Interview Draft",
|
||||
role: "reviewer",
|
||||
reportsTo: "agent-manager-1",
|
||||
heartbeatProcedurePath: ".fusion/agents/interview/HEARTBEAT.md",
|
||||
runtimeConfig: expect.objectContaining({ runtimeHint: "openclaw", thinkingLevel: "low", maxTurns: 12 }),
|
||||
metadata: { skills: ["skill-1"] },
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("closing interview leaves current form state unchanged and does not create agent", async () => {
|
||||
|
||||
Reference in New Issue
Block a user