fix(skills): preserve bulk flow after conflict resolution

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
J
2026-06-10 15:24:48 +08:00
parent c8bf74c1f3
commit f810e9bc2b
2 changed files with 89 additions and 2 deletions

View File

@@ -453,4 +453,79 @@ describe("RuntimeLocalSkillImportPanel", () => {
expect(await screen.findByText("Updated")).toBeInTheDocument();
});
it("keeps bulk completion behavior when conflict resolution leaves one success", async () => {
mockRuntimeLocalSkillsOptions.mockReturnValue({
queryKey: ["runtimes", "local-skills", "runtime-1"],
queryFn: () =>
Promise.resolve({
supported: true,
skills: [MOCK_SKILL_A, MOCK_SKILL_B],
}),
});
mockResolveRuntimeLocalSkillImport
.mockResolvedValueOnce({
status: "conflict",
conflict: {
existing_skill_id: "existing-skill-1",
existing_created_by: "user-1",
can_overwrite: true,
},
})
.mockRejectedValueOnce(new Error("daemon failed"))
.mockResolvedValueOnce({
status: "updated",
skill: {
...MOCK_IMPORTED_SKILL_A,
id: "existing-skill-1",
},
});
const onImported = vi.fn();
const onBulkDone = vi.fn();
renderPanel({ onImported, onBulkDone });
expect(
await screen.findByText("Review Helper", {}, { timeout: 5000 }),
).toBeInTheDocument();
const selectAllLabel = screen.getByText(/Select all/i);
const selectAllCheckbox = selectAllLabel
.closest("label")!
.querySelector("input[type='checkbox']")!;
fireEvent.click(selectAllCheckbox);
const importButton = screen.getByRole("button", {
name: /Import 2 Skills/i,
});
await waitFor(() => expect(importButton).not.toBeDisabled(), {
timeout: 5000,
});
fireEvent.click(importButton);
expect(
await screen.findByText(/A skill with this name already exists/i),
).toBeInTheDocument();
const applyButton = screen.getByRole("button", {
name: /Apply decisions/i,
});
await waitFor(() => expect(applyButton).not.toBeDisabled(), {
timeout: 5000,
});
fireEvent.click(applyButton);
await waitFor(
() => {
expect(
screen.getByRole("button", { name: /Done/i }),
).toBeInTheDocument();
},
{ timeout: 10000 },
);
fireEvent.click(screen.getByRole("button", { name: /Done/i }));
expect(onBulkDone).toHaveBeenCalledTimes(1);
expect(onImported).not.toHaveBeenCalled();
});
});

View File

@@ -68,6 +68,7 @@ type BulkImportState = {
phase: "idle" | "importing" | "resolving" | "done" | "cancelled";
total: number;
completed: number;
selectedCount: number;
results: BulkImportResult[];
};
@@ -82,6 +83,7 @@ const INITIAL_BULK_STATE: BulkImportState = {
phase: "idle",
total: 0,
completed: 0,
selectedCount: 0,
results: [],
};
@@ -622,7 +624,13 @@ export function RuntimeLocalSkillImportPanel({
const total = skillsToImport.length;
cancelRef.current = false;
setBulkState({ phase: "importing", total, completed: 0, results: [] });
setBulkState({
phase: "importing",
total,
completed: 0,
selectedCount: total,
results: [],
});
const results: BulkImportResult[] = [];
@@ -1029,7 +1037,11 @@ export function RuntimeLocalSkillImportPanel({
);
// Single-import flow: navigate to the imported skill detail page.
// Multi-import flow: close the dialog even if only one succeeded.
if (bulkState.total === 1 && succeeded.length === 1 && succeeded[0]!.skill) {
if (
bulkState.selectedCount === 1 &&
succeeded.length === 1 &&
succeeded[0]!.skill
) {
onImported?.(succeeded[0]!.skill);
} else {
onBulkDone?.();