feat(lanes): auto-setup (profile/agents/mcp) after Add Lane (F5)
This commit is contained in:
@@ -41,6 +41,11 @@ export function AddLaneModal({
|
||||
const [branchesError, setBranchesError] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [setupResult, setSetupResult] = useState<{
|
||||
profile: "scaffolded" | "skipped" | "failed";
|
||||
agents: "ok" | "failed";
|
||||
mcp: "ok" | "failed";
|
||||
} | null>(null);
|
||||
|
||||
const reset = () => {
|
||||
setSourceRepo("");
|
||||
@@ -50,6 +55,7 @@ export function AddLaneModal({
|
||||
setBranchesError(null);
|
||||
setError(null);
|
||||
setBusy(false);
|
||||
setSetupResult(null);
|
||||
};
|
||||
|
||||
// Look up the repo's branches once the path settles - debounced so every
|
||||
@@ -96,6 +102,32 @@ export function AddLaneModal({
|
||||
title: name,
|
||||
base: base || undefined,
|
||||
});
|
||||
|
||||
const [profileOutcome, agentsOutcome, mcpOutcome] = await Promise.allSettled([
|
||||
api.lanes.profileInit(result.lane.id),
|
||||
api.lanes.agentsInstall(result.lane.id),
|
||||
api.lanes.mcpSync(result.lane.id),
|
||||
]);
|
||||
setSetupResult({
|
||||
profile:
|
||||
profileOutcome.status === "fulfilled"
|
||||
? profileOutcome.value.scaffolded
|
||||
? "scaffolded"
|
||||
: "skipped"
|
||||
: "failed",
|
||||
agents: agentsOutcome.status === "fulfilled" ? "ok" : "failed",
|
||||
mcp: mcpOutcome.status === "fulfilled" ? "ok" : "failed",
|
||||
});
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.info("[add-lane] auto-setup result:", {
|
||||
profile:
|
||||
profileOutcome.status === "fulfilled" ? profileOutcome.value : profileOutcome.reason,
|
||||
agents: agentsOutcome.status === "fulfilled" ? agentsOutcome.value : agentsOutcome.reason,
|
||||
mcp: mcpOutcome.status === "fulfilled" ? mcpOutcome.value : mcpOutcome.reason,
|
||||
});
|
||||
}
|
||||
|
||||
reset();
|
||||
onAdded(result.lane);
|
||||
onClose();
|
||||
|
||||
@@ -18,7 +18,15 @@ import type { CwdSuggestion } from "../../../lib/api";
|
||||
import type { Lane } from "../../../lib/types";
|
||||
|
||||
vi.mock("../../../lib/api", () => ({
|
||||
api: { lanes: { branches: vi.fn(), worktree: vi.fn() } },
|
||||
api: {
|
||||
lanes: {
|
||||
branches: vi.fn(),
|
||||
worktree: vi.fn(),
|
||||
profileInit: vi.fn(),
|
||||
agentsInstall: vi.fn(),
|
||||
mcpSync: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
function laneFixture(over: Partial<Lane> = {}): Lane {
|
||||
@@ -76,6 +84,12 @@ async function focusField(user: ReturnType<typeof userEvent.setup>, el: HTMLElem
|
||||
beforeEach(() => {
|
||||
vi.mocked(api.lanes.branches).mockReset();
|
||||
vi.mocked(api.lanes.worktree).mockReset();
|
||||
vi.mocked(api.lanes.profileInit).mockResolvedValue({
|
||||
scaffolded: false,
|
||||
reason: "no detectable Node.js project",
|
||||
});
|
||||
vi.mocked(api.lanes.agentsInstall).mockResolvedValue({ installed: [] });
|
||||
vi.mocked(api.lanes.mcpSync).mockResolvedValue({ servers: [], profilesSeeded: [] });
|
||||
});
|
||||
|
||||
describe("AddLaneModal", () => {
|
||||
@@ -169,4 +183,51 @@ describe("AddLaneModal", () => {
|
||||
renderModal({ open: false });
|
||||
expect(screen.queryByRole("dialog")).toBeNull();
|
||||
});
|
||||
|
||||
it("fires profileInit, agentsInstall, and mcpSync after a successful worktree call", async () => {
|
||||
vi.mocked(api.lanes.branches).mockResolvedValue({ branches: ["main"], current: "main" });
|
||||
vi.mocked(api.lanes.worktree).mockResolvedValue({
|
||||
lane: { id: 42, title: "demo", cwd: "/lanes/demo", status: "provisioning" } as Lane,
|
||||
});
|
||||
const onAdded = vi.fn();
|
||||
renderModal({ onAdded });
|
||||
const user = userEvent.setup();
|
||||
|
||||
const repoField = screen.getByLabelText("Source repository");
|
||||
await focusField(user, repoField);
|
||||
await user.type(repoField, "/Users/tester/projects/repo");
|
||||
await screen.findByLabelText("Branch to fork from");
|
||||
await user.type(screen.getByLabelText("Title"), "demo");
|
||||
await user.click(screen.getByRole("button", { name: "Add lane" }));
|
||||
|
||||
await waitFor(() => expect(api.lanes.worktree).toHaveBeenCalled());
|
||||
await waitFor(() => expect(api.lanes.profileInit).toHaveBeenCalledWith(42));
|
||||
expect(api.lanes.agentsInstall).toHaveBeenCalledWith(42);
|
||||
expect(api.lanes.mcpSync).toHaveBeenCalledWith(42);
|
||||
await waitFor(() => expect(onAdded).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it("still calls onAdded and closes even when every setup call fails", async () => {
|
||||
vi.mocked(api.lanes.branches).mockResolvedValue({ branches: ["main"], current: "main" });
|
||||
vi.mocked(api.lanes.worktree).mockResolvedValue({
|
||||
lane: { id: 43, title: "demo2", cwd: "/lanes/demo2", status: "provisioning" } as Lane,
|
||||
});
|
||||
vi.mocked(api.lanes.profileInit).mockRejectedValue(new Error("boom"));
|
||||
vi.mocked(api.lanes.agentsInstall).mockRejectedValue(new Error("boom"));
|
||||
vi.mocked(api.lanes.mcpSync).mockRejectedValue(new Error("boom"));
|
||||
const onAdded = vi.fn();
|
||||
const onClose = vi.fn();
|
||||
renderModal({ onAdded, onClose });
|
||||
const user = userEvent.setup();
|
||||
|
||||
const repoField = screen.getByLabelText("Source repository");
|
||||
await focusField(user, repoField);
|
||||
await user.type(repoField, "/Users/tester/projects/repo");
|
||||
await screen.findByLabelText("Branch to fork from");
|
||||
await user.type(screen.getByLabelText("Title"), "demo2");
|
||||
await user.click(screen.getByRole("button", { name: "Add lane" }));
|
||||
|
||||
await waitFor(() => expect(onAdded).toHaveBeenCalled());
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user