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:
@@ -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", () => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user