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 [branchesError, setBranchesError] = useState<string | null>(null);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
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 = () => {
|
const reset = () => {
|
||||||
setSourceRepo("");
|
setSourceRepo("");
|
||||||
@@ -50,6 +55,7 @@ export function AddLaneModal({
|
|||||||
setBranchesError(null);
|
setBranchesError(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
|
setSetupResult(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Look up the repo's branches once the path settles - debounced so every
|
// Look up the repo's branches once the path settles - debounced so every
|
||||||
@@ -96,6 +102,32 @@ export function AddLaneModal({
|
|||||||
title: name,
|
title: name,
|
||||||
base: base || undefined,
|
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();
|
reset();
|
||||||
onAdded(result.lane);
|
onAdded(result.lane);
|
||||||
onClose();
|
onClose();
|
||||||
|
|||||||
@@ -18,7 +18,15 @@ import type { CwdSuggestion } from "../../../lib/api";
|
|||||||
import type { Lane } from "../../../lib/types";
|
import type { Lane } from "../../../lib/types";
|
||||||
|
|
||||||
vi.mock("../../../lib/api", () => ({
|
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 {
|
function laneFixture(over: Partial<Lane> = {}): Lane {
|
||||||
@@ -76,6 +84,12 @@ async function focusField(user: ReturnType<typeof userEvent.setup>, el: HTMLElem
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.mocked(api.lanes.branches).mockReset();
|
vi.mocked(api.lanes.branches).mockReset();
|
||||||
vi.mocked(api.lanes.worktree).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", () => {
|
describe("AddLaneModal", () => {
|
||||||
@@ -169,4 +183,51 @@ describe("AddLaneModal", () => {
|
|||||||
renderModal({ open: false });
|
renderModal({ open: false });
|
||||||
expect(screen.queryByRole("dialog")).toBeNull();
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2080,6 +2080,11 @@ export const api = {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: "{}",
|
body: "{}",
|
||||||
}),
|
}),
|
||||||
|
profileInit: (id: number, force = false) =>
|
||||||
|
request<{ scaffolded: boolean; written?: string[]; todos?: string[]; reason?: string }>(
|
||||||
|
`/lanes/${id}/profile/init`,
|
||||||
|
{ method: "POST", body: JSON.stringify({ force }) }
|
||||||
|
),
|
||||||
integration: (id: number, name: string) =>
|
integration: (id: number, name: string) =>
|
||||||
request<{ enabled: boolean }>(`/lanes/${id}/integrations/${encodeURIComponent(name)}`),
|
request<{ enabled: boolean }>(`/lanes/${id}/integrations/${encodeURIComponent(name)}`),
|
||||||
syncBaseCheck: (id: number, branch?: string) =>
|
syncBaseCheck: (id: number, branch?: string) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user