diff --git a/packages/views/skills/components/runtime-local-skill-import-panel.test.tsx b/packages/views/skills/components/runtime-local-skill-import-panel.test.tsx index dab605bdf..b6c7c2dbb 100644 --- a/packages/views/skills/components/runtime-local-skill-import-panel.test.tsx +++ b/packages/views/skills/components/runtime-local-skill-import-panel.test.tsx @@ -15,6 +15,13 @@ const TEST_RESOURCES = { const mockResolveRuntimeLocalSkillImport = vi.hoisted(() => vi.fn()); const mockRuntimeListOptions = vi.hoisted(() => vi.fn()); const mockRuntimeLocalSkillsOptions = vi.hoisted(() => vi.fn()); +const mockListMembers = vi.hoisted(() => vi.fn()); + +vi.mock("@multica/core/api", () => ({ + api: { + listMembers: (...args: unknown[]) => mockListMembers(...args), + }, +})); vi.mock("@multica/core/hooks", () => ({ useWorkspaceId: () => "ws-1", @@ -151,6 +158,10 @@ describe("RuntimeLocalSkillImportPanel", () => { status: "created", skill: MOCK_IMPORTED_SKILL_A, }); + mockListMembers.mockResolvedValue([ + { user_id: "user-1", name: "Alice", email: "alice@example.com" }, + { user_id: "user-2", name: "Bob", email: "bob@example.com" }, + ]); }); it("imports a single skill when selected via checkbox", async () => { @@ -528,4 +539,74 @@ describe("RuntimeLocalSkillImportPanel", () => { expect(onBulkDone).toHaveBeenCalledTimes(1); expect(onImported).not.toHaveBeenCalled(); }); + + it("renders the creator's display name for non-overwritable conflicts", async () => { + mockResolveRuntimeLocalSkillImport.mockResolvedValueOnce({ + status: "conflict", + conflict: { + existing_skill_id: "existing-skill-1", + existing_created_by: "user-2", + can_overwrite: false, + }, + }); + + renderPanel(); + + expect( + await screen.findByText("Review Helper", {}, { timeout: 5000 }), + ).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("button", { name: /Review Helper/i })); + const importButton = screen.getByRole("button", { + name: /Import to Workspace/i, + }); + await waitFor(() => expect(importButton).not.toBeDisabled(), { + timeout: 5000, + }); + fireEvent.click(importButton); + + // Bob is user-2 in the mocked member list. The locked message must show + // the resolved name, never the raw UUID. + expect( + await screen.findByText(/created by Bob/i, {}, { timeout: 5000 }), + ).toBeInTheDocument(); + expect(screen.queryByText(/user-2/)).not.toBeInTheDocument(); + }); + + it("falls back to the unbranded locked message when the creator left the workspace", async () => { + mockResolveRuntimeLocalSkillImport.mockResolvedValueOnce({ + status: "conflict", + conflict: { + existing_skill_id: "existing-skill-1", + existing_created_by: "user-gone", + can_overwrite: false, + }, + }); + + renderPanel(); + + expect( + await screen.findByText("Review Helper", {}, { timeout: 5000 }), + ).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("button", { name: /Review Helper/i })); + const importButton = screen.getByRole("button", { + name: /Import to Workspace/i, + }); + await waitFor(() => expect(importButton).not.toBeDisabled(), { + timeout: 5000, + }); + fireEvent.click(importButton); + + // user-gone is not in the workspace; the UI must not leak the UUID and + // should render the no-creator variant of the message. + expect( + await screen.findByText( + /Only the creator can overwrite this skill/i, + {}, + { timeout: 5000 }, + ), + ).toBeInTheDocument(); + expect(screen.queryByText(/user-gone/)).not.toBeInTheDocument(); + }); }); diff --git a/packages/views/skills/components/runtime-local-skill-import-panel.tsx b/packages/views/skills/components/runtime-local-skill-import-panel.tsx index 7a902a349..799b29079 100644 --- a/packages/views/skills/components/runtime-local-skill-import-panel.tsx +++ b/packages/views/skills/components/runtime-local-skill-import-panel.tsx @@ -28,6 +28,7 @@ import { resolveRuntimeLocalSkillImport, } from "@multica/core/runtimes"; import { + memberListOptions, skillDetailOptions, workspaceKeys, } from "@multica/core/workspace/queries"; @@ -316,6 +317,8 @@ function ConflictResolutionPanel({ onSkipAll: () => void; }) { const { t } = useT("skills"); + const wsId = useWorkspaceId(); + const { data: members = [] } = useQuery(memberListOptions(wsId)); const single = conflicts.length === 1; const canOverwriteAny = conflicts.some((r) => r.conflict?.can_overwrite); @@ -366,7 +369,10 @@ function ConflictResolutionPanel({ action: r.conflict?.can_overwrite ? "overwrite" : "rename", renameName: defaultRenameName(r.name), } satisfies ConflictResolutionState); - const creator = r.conflict?.existing_created_by; + const creatorId = r.conflict?.existing_created_by; + const creatorName = creatorId + ? members.find((m) => m.user_id === creatorId)?.name + : undefined; return (
- {creator + {creatorName ? t(($) => $.runtime_import.conflict_locked_creator, { - creator, + creator: creatorName, }) : t(($) => $.runtime_import.conflict_locked)}