Marketplace: explain, don't offer, updates that would be refused (PR re-review)
ItemUpdate gains invalid_at_head: compute_updates records why the item cannot be installed at head (catalog invalid, or gone) — the rule update_marketplace_item applies — read once per marketplace from the snapshot at head, else from the catalog parsed at head. The Installed row shows that reason instead of a Review button, and the update counts in the Marketplace tab and Settings count only applicable updates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -73,7 +73,7 @@ describe("InstalledPane", () => {
|
||||
|
||||
it("badges and accepts an update for the matching install", async () => {
|
||||
const mp = api({
|
||||
updates: [{ item: { marketplace_id: "m1", kind: "agent", key: "rev" }, pinned: A, head: B }],
|
||||
updates: [{ item: { marketplace_id: "m1", kind: "agent", key: "rev" }, pinned: A, head: B, invalid_at_head: null }],
|
||||
});
|
||||
render(<InstalledPane mp={mp} />);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Review update for rev" }));
|
||||
@@ -99,7 +99,7 @@ describe("InstalledPane", () => {
|
||||
] as unknown as Project[],
|
||||
});
|
||||
const mp = api({
|
||||
updates: [{ item: { marketplace_id: "m1", kind: "agent", key: "rev" }, pinned: A, head: B }],
|
||||
updates: [{ item: { marketplace_id: "m1", kind: "agent", key: "rev" }, pinned: A, head: B, invalid_at_head: null }],
|
||||
});
|
||||
render(<InstalledPane mp={mp} />);
|
||||
const global = screen.getByTestId("installed-global");
|
||||
@@ -108,13 +108,30 @@ describe("InstalledPane", () => {
|
||||
expect(within(proj).queryByRole("button", { name: "Review update for rev" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("re-review round 2: an update that would be refused shows why instead of a Review button", () => {
|
||||
const mp = api({
|
||||
updates: [
|
||||
{
|
||||
item: { marketplace_id: "m1", kind: "agent", key: "rev" },
|
||||
pinned: A,
|
||||
head: B,
|
||||
invalid_at_head: 'unknown hook event "PreFoo"',
|
||||
},
|
||||
],
|
||||
});
|
||||
render(<InstalledPane mp={mp} />);
|
||||
const global = screen.getByTestId("installed-global");
|
||||
expect(within(global).queryByRole("button", { name: "Review update for rev" })).not.toBeInTheDocument();
|
||||
expect(within(global).getByText(/Update to bbbbbbbb cannot be installed: unknown hook event "PreFoo"/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("I2: accepts the head that was reviewed even if the update list moves on", async () => {
|
||||
const C = "c".repeat(40);
|
||||
const item = { marketplace_id: "m1", kind: "agent" as const, key: "rev" };
|
||||
const mp = api({ updates: [{ item, pinned: A, head: B }] });
|
||||
const mp = api({ updates: [{ item, pinned: A, head: B, invalid_at_head: null }] });
|
||||
const { rerender } = render(<InstalledPane mp={mp} />);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Review update for rev" }));
|
||||
rerender(<InstalledPane mp={{ ...mp, updates: [{ item, pinned: A, head: C }] }} />);
|
||||
rerender(<InstalledPane mp={{ ...mp, updates: [{ item, pinned: A, head: C, invalid_at_head: null }] }} />);
|
||||
fireEvent.click(screen.getByRole("button", { name: "accept diff" }));
|
||||
await waitFor(() => expect(mp.update).toHaveBeenCalledWith(item, { type: "global" }, B));
|
||||
});
|
||||
|
||||
@@ -100,9 +100,16 @@ export default function InstalledPane({ mp }: { mp: MarketplaceApi }) {
|
||||
{KIND_LABELS[i.kind].replace(/s$/, "").toLowerCase()} · {nameOf(i.marketplace_id)} · {i.commit.slice(0, 8)}
|
||||
</span>
|
||||
{gone && <span className="ml-2 text-[var(--warning)]">Source removed</span>}
|
||||
{upd?.invalid_at_head && !gone && (
|
||||
// The update would be refused (re-review round 2), so it is
|
||||
// explained rather than offered.
|
||||
<span className="block text-[var(--warning)]">
|
||||
Update to {upd.head.slice(0, 8)} cannot be installed: {upd.invalid_at_head}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-1 flex-shrink-0">
|
||||
{upd && !gone && (
|
||||
{upd && !upd.invalid_at_head && !gone && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useMarketplace } from "../../hooks/useMarketplace";
|
||||
import { applicableUpdates } from "../../lib/marketplace";
|
||||
import BrowsePane from "./BrowsePane";
|
||||
import InstalledPane from "./InstalledPane";
|
||||
import AccountsPane from "./AccountsPane";
|
||||
@@ -49,9 +50,9 @@ export default function MarketplaceView({ active }: Props) {
|
||||
}`}
|
||||
>
|
||||
{t.label}
|
||||
{t.id === "installed" && mp.updates.length > 0 && (
|
||||
{t.id === "installed" && applicableUpdates(mp.updates).length > 0 && (
|
||||
<span className="ml-1.5 px-1 rounded-[4px] text-[10px] bg-[var(--accent-muted)] text-[var(--accent)]">
|
||||
{mp.updates.length}
|
||||
{applicableUpdates(mp.updates).length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
@@ -25,7 +25,19 @@ describe("MarketplaceSettings", () => {
|
||||
} as unknown as AppSettings,
|
||||
});
|
||||
listMarketplaceUpdates.mockResolvedValue([
|
||||
{ item: { marketplace_id: "m1", kind: "agent", key: "a" }, pinned: "a".repeat(40), head: "b".repeat(40) },
|
||||
{
|
||||
item: { marketplace_id: "m1", kind: "agent", key: "a" },
|
||||
pinned: "a".repeat(40),
|
||||
head: "b".repeat(40),
|
||||
invalid_at_head: null,
|
||||
},
|
||||
// Not applicable (re-review round 2): not counted as available.
|
||||
{
|
||||
item: { marketplace_id: "m1", kind: "hook", key: "h" },
|
||||
pinned: "a".repeat(40),
|
||||
head: "b".repeat(40),
|
||||
invalid_at_head: 'unknown hook event "PreFoo"',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useAppState } from "../../store/appState";
|
||||
import { listMarketplaceUpdates } from "../../lib/tauri-commands";
|
||||
import { applicableUpdates } from "../../lib/marketplace";
|
||||
import Button from "../ui/Button";
|
||||
|
||||
const plural = (n: number, one: string, many: string) => `${n} ${n === 1 ? one : many}`;
|
||||
@@ -14,7 +15,7 @@ export default function MarketplaceSettings() {
|
||||
let cancelled = false;
|
||||
listMarketplaceUpdates()
|
||||
.then((u) => {
|
||||
if (!cancelled) setUpdateCount(u.length);
|
||||
if (!cancelled) setUpdateCount(applicableUpdates(u).length);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setUpdateCount(null);
|
||||
|
||||
Reference in New Issue
Block a user