91bdee3d3a
* feat(plugins): show source repo link on installed plugin list and settings Installed plugins carried no repository URL — a "Repo" link existed only on marketplace catalog cards, whose repo_url is registry-derived from plugins.yaml and never reaches an installed record. Let the manifest declare an optional repo_url so sideloaded and directly-installed plugins surface the same link. - manifest: add optional RepoURL (repo_url); validate http(s)-only at registration so a javascript: scheme can't reach the operator's session. - Record embeds the manifest, so repo_url flows through JSON to the frontend PluginRecord with no extra plumbing. - frontend: extract the shared, guarded PluginRepoLink (+ isHttpUrl) from the marketplace row and render it on the installed plugin row and the plugin detail header. - fixture + docs: exercise repo_url in the e2e fixture manifest and document the field in the public manifest reference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(plugins): harden repo_url handling per review - PluginRepoLink accepts string|undefined|null and isHttpUrl null-checks before trim, so it can't throw on an absent repo_url (CodeRabbit); drop the now-redundant truthiness guards at the call sites. - validateRepoURL trims m.RepoURL in place so the stored/serialised value matches what was validated (Greptile). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
125 lines
3.2 KiB
TypeScript
125 lines
3.2 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
import { PluginRow } from "./plugin-row";
|
|
import type { MarketplaceEntry, PluginRecord } from "@/lib/types/plugins";
|
|
|
|
afterEach(() => cleanup());
|
|
|
|
function plugin(overrides: Partial<PluginRecord> = {}): PluginRecord {
|
|
return {
|
|
id: "acme",
|
|
api_version: 1,
|
|
version: "1.0.0",
|
|
display_name: "Acme",
|
|
description: "",
|
|
author: "acme",
|
|
categories: [],
|
|
capabilities: {},
|
|
status: "active",
|
|
install_path: "/p",
|
|
signed: true,
|
|
installed_at: "2026-01-01T00:00:00Z",
|
|
restart_count: 0,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function updateEntry(): MarketplaceEntry {
|
|
return {
|
|
id: "acme",
|
|
name: "Acme",
|
|
description: "",
|
|
author: "acme",
|
|
categories: [],
|
|
icon_url: "",
|
|
repo_url: "",
|
|
version: "2.0.0",
|
|
min_kandev_version: "",
|
|
package_url: "https://ex/acme-2.0.0.tar.gz",
|
|
package_sha256: "",
|
|
stars: 0,
|
|
updated_at: "",
|
|
install_state: "update_available",
|
|
source_id: "official",
|
|
source_name: "Kandev Official",
|
|
};
|
|
}
|
|
|
|
const noop = () => undefined;
|
|
|
|
describe("PluginRow update button", () => {
|
|
it("shows an Update button with the new version and fires onUpdate", () => {
|
|
const onUpdate = vi.fn();
|
|
render(
|
|
<PluginRow
|
|
plugin={plugin()}
|
|
busy={false}
|
|
update={updateEntry()}
|
|
onEnable={noop}
|
|
onDisable={noop}
|
|
onUninstall={noop}
|
|
onUpdate={onUpdate}
|
|
/>,
|
|
);
|
|
const button = screen.getByTestId("plugin-update-acme");
|
|
expect(button.textContent).toContain("Update to v2.0.0");
|
|
fireEvent.click(button);
|
|
expect(onUpdate).toHaveBeenCalledWith(updateEntry());
|
|
});
|
|
|
|
it("renders no Update button when there is no pending update", () => {
|
|
render(
|
|
<PluginRow
|
|
plugin={plugin()}
|
|
busy={false}
|
|
onEnable={noop}
|
|
onDisable={noop}
|
|
onUninstall={noop}
|
|
/>,
|
|
);
|
|
expect(screen.queryByTestId("plugin-update-acme")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("PluginRow repo link", () => {
|
|
it("renders a Repo link when the plugin declares an http(s) repo_url", () => {
|
|
render(
|
|
<PluginRow
|
|
plugin={plugin({ repo_url: "https://github.com/kdlbs/kandev-plugin-acme" })}
|
|
busy={false}
|
|
onEnable={noop}
|
|
onDisable={noop}
|
|
onUninstall={noop}
|
|
/>,
|
|
);
|
|
const link = screen.getByTestId("plugin-repo-link");
|
|
expect(link.getAttribute("href")).toBe("https://github.com/kdlbs/kandev-plugin-acme");
|
|
});
|
|
|
|
it("renders no Repo link when the plugin declares no repo_url", () => {
|
|
render(
|
|
<PluginRow
|
|
plugin={plugin()}
|
|
busy={false}
|
|
onEnable={noop}
|
|
onDisable={noop}
|
|
onUninstall={noop}
|
|
/>,
|
|
);
|
|
expect(screen.queryByTestId("plugin-repo-link")).toBeNull();
|
|
});
|
|
|
|
it("renders no Repo link for a non-http(s) repo_url scheme", () => {
|
|
render(
|
|
<PluginRow
|
|
plugin={plugin({ repo_url: "javascript:alert(document.cookie)" })}
|
|
busy={false}
|
|
onEnable={noop}
|
|
onDisable={noop}
|
|
onUninstall={noop}
|
|
/>,
|
|
);
|
|
expect(screen.queryByTestId("plugin-repo-link")).toBeNull();
|
|
});
|
|
});
|