Marketplace: pin the commit the user reviewed (final review I2)

Install and update pinned whatever the marketplace head was when the
click landed, so a background refresh between review and click could
pin content nobody saw (including a hook's shell commands).
install_marketplace_item and update_marketplace_item now take
expected_commit and refuse with "changed since you reviewed this item —
review it again" unless it is still the head. The UI passes the head the
selected item was read at (Browse), the head frozen with a pending hook
confirm (whose commands are frozen too), and the head of the accepted
diff (Installed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 10:11:19 -07:00
co-authored by Claude Opus 5.5
parent f2ebddd073
commit dd019cf2c0
13 changed files with 197 additions and 65 deletions
+15 -1
View File
@@ -9,6 +9,7 @@ const listMarketplaceUpdates = vi.fn();
const getSettings = vi.fn();
const listProjects = vi.fn();
const installMarketplaceItem = vi.fn();
const updateMarketplaceItem = vi.fn();
vi.mock("../lib/tauri-commands", () => ({
listMarketplaceSnapshots: () => listMarketplaceSnapshots(),
@@ -17,6 +18,7 @@ vi.mock("../lib/tauri-commands", () => ({
getSettings: () => getSettings(),
listProjects: () => listProjects(),
installMarketplaceItem: (...a: unknown[]) => installMarketplaceItem(...a),
updateMarketplaceItem: (...a: unknown[]) => updateMarketplaceItem(...a),
}));
let syncHandler: ((e: { payload: unknown }) => void) | null = null;
@@ -66,11 +68,23 @@ describe("useMarketplace", () => {
installMarketplaceItem.mockRejectedValue("boom");
const { result } = renderHook(() => useMarketplace());
const ok = await act(() =>
result.current.install({ marketplace_id: "m1", kind: "agent", key: "a" }, { type: "global" }),
result.current.install({ marketplace_id: "m1", kind: "agent", key: "a" }, { type: "global" }, "c".repeat(40)),
);
expect(ok).toBe(false);
expect(useAppState.getState().toasts[0]).toMatchObject({ kind: "error", detail: "boom" });
});
it("I2: passes the reviewed commit to install and update", async () => {
listMarketplaceSnapshots.mockResolvedValue([]);
installMarketplaceItem.mockResolvedValue({});
updateMarketplaceItem.mockResolvedValue(undefined);
const item = { marketplace_id: "m1", kind: "hook" as const, key: "h" };
const { result } = renderHook(() => useMarketplace());
await act(() => result.current.install(item, { type: "global" }, "c".repeat(40)));
expect(installMarketplaceItem).toHaveBeenCalledWith(item, { type: "global" }, "c".repeat(40));
await act(() => result.current.update(item, { type: "project", project_id: "p1" }, "d".repeat(40)));
expect(updateMarketplaceItem).toHaveBeenCalledWith(item, { type: "project", project_id: "p1" }, "d".repeat(40));
});
});
describe("useMarketplaceSyncToasts", () => {
+8 -6
View File
@@ -21,10 +21,12 @@ export interface MarketplaceApi {
refresh: (marketplaceId?: string) => Promise<void>;
/** Reload settings, projects and the update list after a mutation. */
reloadState: () => Promise<void>;
install: (item: MarketplaceItemRef, scope: InstallScope) => Promise<boolean>;
/** `commit`: the marketplace head the user reviewed (see `install_marketplace_item`). */
install: (item: MarketplaceItemRef, scope: InstallScope, commit: string) => Promise<boolean>;
uninstall: (item: MarketplaceItemRef, scope: InstallScope) => Promise<boolean>;
setDisabled: (projectId: string, item: MarketplaceItemRef, disabled: boolean) => Promise<boolean>;
update: (item: MarketplaceItemRef, scope: InstallScope) => Promise<boolean>;
/** `commit`: the head whose diff the user accepted. */
update: (item: MarketplaceItemRef, scope: InstallScope, commit: string) => Promise<boolean>;
forget: (marketplaceId: string) => Promise<boolean>;
remove: (marketplaceId: string) => Promise<boolean>;
}
@@ -146,16 +148,16 @@ export function useMarketplace(): MarketplaceApi {
load,
refresh,
reloadState,
install: (item, scope) =>
mutate(`Could not install ${item.key}`, () => commands.installMarketplaceItem(item, scope)),
install: (item, scope, commit) =>
mutate(`Could not install ${item.key}`, () => commands.installMarketplaceItem(item, scope, commit)),
uninstall: (item, scope) =>
mutate(`Could not remove ${item.key}`, () => commands.uninstallMarketplaceItem(item, scope)),
setDisabled: (projectId, item, disabled) =>
mutate(`Could not change ${item.key} for this project`, () =>
commands.setGlobalItemDisabled(projectId, item, disabled),
),
update: (item, scope) =>
mutate(`Could not update ${item.key}`, () => commands.updateMarketplaceItem(item, scope)),
update: (item, scope, commit) =>
mutate(`Could not update ${item.key}`, () => commands.updateMarketplaceItem(item, scope, commit)),
forget: (marketplaceId) =>
mutate("Could not forget those installs", () => commands.forgetMarketplaceInstalls(marketplaceId)),
remove: async (marketplaceId) => {