mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
fix: ignore stale GitHub Skill Sync previews
This commit is contained in:
@@ -6,7 +6,7 @@ import { Input } from "./ui/input";
|
||||
import { Label } from "./ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
||||
|
||||
export type GitHubSkillSyncPublisherOption = {
|
||||
type GitHubSkillSyncPublisherOption = {
|
||||
publisher: {
|
||||
_id: Id<"publishers">;
|
||||
handle: string;
|
||||
@@ -27,7 +27,7 @@ export type GitHubSkillSyncRepository = {
|
||||
unavailableReason: "disabled" | null;
|
||||
};
|
||||
|
||||
export type GitHubSkillSyncPreviewItem = {
|
||||
type GitHubSkillSyncPreviewItem = {
|
||||
slug: string;
|
||||
displayName: string;
|
||||
path: string;
|
||||
@@ -297,6 +297,7 @@ function classificationLabel(classification: GitHubSkillSyncPreviewItem["classif
|
||||
case "ownership-conflict":
|
||||
return "Ownership conflict";
|
||||
}
|
||||
return assertNever(classification);
|
||||
}
|
||||
|
||||
function classificationTone(classification: GitHubSkillSyncPreviewItem["classification"]) {
|
||||
@@ -309,6 +310,11 @@ function classificationTone(classification: GitHubSkillSyncPreviewItem["classifi
|
||||
case "ownership-conflict":
|
||||
return "text-status-error-fg";
|
||||
}
|
||||
return assertNever(classification);
|
||||
}
|
||||
|
||||
function assertNever(value: never): never {
|
||||
throw new Error(`Unsupported GitHub Skill Sync classification: ${String(value)}`);
|
||||
}
|
||||
|
||||
function previewReasonLabel(reason: string) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* @vitest-environment jsdom */
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { getFunctionName } from "convex/server";
|
||||
import type { FunctionReturnType } from "convex/server";
|
||||
import type { ReactNode } from "react";
|
||||
@@ -801,6 +801,93 @@ describe("Settings", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("ignores a preview response after the selected repository changes", async () => {
|
||||
const listRepositories = vi.fn().mockResolvedValue({
|
||||
publisher: { _id: "publisher_patrick", handle: "patrick", kind: "user" },
|
||||
page: 1,
|
||||
perPage: 100,
|
||||
hasMore: false,
|
||||
repositories: [
|
||||
{
|
||||
repositoryId: "1",
|
||||
repo: "patrick-erichsen/skills",
|
||||
ownerId: "123",
|
||||
ownerLogin: "patrick-erichsen",
|
||||
defaultBranch: "main",
|
||||
archived: false,
|
||||
disabled: false,
|
||||
fork: false,
|
||||
pushedAt: "2026-07-23T12:00:00Z",
|
||||
selectable: true,
|
||||
unavailableReason: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
let resolvePreview: ((value: unknown) => void) | undefined;
|
||||
const previewRepository = vi.fn(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolvePreview = resolve;
|
||||
}),
|
||||
);
|
||||
useActionMock.mockImplementation((action) => {
|
||||
const actionName = getFunctionName(action);
|
||||
if (actionName === "githubSkillSyncSettings:listRepositories") return listRepositories;
|
||||
if (actionName === "githubSkillSyncSettings:previewRepository") return previewRepository;
|
||||
return vi.fn();
|
||||
});
|
||||
mockSignedInSettings({
|
||||
search: { view: "githubSources" },
|
||||
memberships: [personalMembership],
|
||||
});
|
||||
|
||||
render(<Settings />);
|
||||
|
||||
await waitFor(() => expect(listRepositories).toHaveBeenCalled());
|
||||
fireEvent.click(screen.getByRole("button", { name: "Preview repository" }));
|
||||
await waitFor(() => expect(previewRepository).toHaveBeenCalled());
|
||||
|
||||
fireEvent.change(screen.getByLabelText("Repository URL"), {
|
||||
target: { value: "patrick-erichsen/other-skills" },
|
||||
});
|
||||
await act(async () => {
|
||||
resolvePreview?.({
|
||||
publisher: { _id: "publisher_patrick", handle: "patrick", kind: "user" },
|
||||
repository: {
|
||||
requestedRepo: "patrick-erichsen/skills",
|
||||
repositoryId: "1",
|
||||
repo: "patrick-erichsen/skills",
|
||||
redirected: false,
|
||||
defaultBranch: "main",
|
||||
commit: "a".repeat(40),
|
||||
},
|
||||
summary: {
|
||||
total: 1,
|
||||
newDestinations: 1,
|
||||
replacements: 0,
|
||||
unavailable: 0,
|
||||
conflicts: 0,
|
||||
},
|
||||
items: [
|
||||
{
|
||||
slug: "html",
|
||||
displayName: "HTML",
|
||||
path: "skills/html",
|
||||
contentHash: "hash-html",
|
||||
classification: "new-destination",
|
||||
eligible: true,
|
||||
destination: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
expect(screen.getByLabelText<HTMLInputElement>("Repository URL").value).toBe(
|
||||
"patrick-erichsen/other-skills",
|
||||
);
|
||||
expect(screen.queryByRole("heading", { name: "Repository preview" })).toBeNull();
|
||||
});
|
||||
|
||||
it("shows synced repos as separate cards and lets owners delete a source", async () => {
|
||||
const deleteSource = vi.fn().mockResolvedValue({ ok: true, deletedSkills: 0 });
|
||||
useMutationMock.mockImplementation((mutation) =>
|
||||
|
||||
+14
-2
@@ -36,6 +36,7 @@ import {
|
||||
type FormEvent,
|
||||
type ReactNode,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -331,6 +332,7 @@ export function Settings() {
|
||||
const [isLoadingGitHubRepositories, setIsLoadingGitHubRepositories] = useState(false);
|
||||
const [githubSyncPreview, setGitHubSyncPreview] = useState<GitHubSkillSyncPreview | null>(null);
|
||||
const [isPreviewingGitHubSource, setIsPreviewingGitHubSource] = useState(false);
|
||||
const githubSyncPreviewRequestId = useRef(0);
|
||||
const [deletingSourceId, setDeletingSourceId] = useState<Id<"githubSkillSources"> | null>(null);
|
||||
const [sourceToDelete, setSourceToDelete] = useState<GitHubSkillSource | null>(null);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
@@ -472,7 +474,7 @@ export function Settings() {
|
||||
) {
|
||||
setGitHubRepositories([]);
|
||||
setGitHubRepositoriesError(null);
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
let cancelled = false;
|
||||
setIsLoadingGitHubRepositories(true);
|
||||
@@ -788,6 +790,8 @@ export function Settings() {
|
||||
if (!selectedSourcePublisher) return;
|
||||
const repo = parseGitHubRepoInput(githubRepo);
|
||||
if (!repo) return;
|
||||
const requestId = githubSyncPreviewRequestId.current + 1;
|
||||
githubSyncPreviewRequestId.current = requestId;
|
||||
setIsPreviewingGitHubSource(true);
|
||||
setGitHubSyncPreview(null);
|
||||
try {
|
||||
@@ -795,12 +799,16 @@ export function Settings() {
|
||||
publisherId: selectedSourcePublisher.publisher._id,
|
||||
repo,
|
||||
});
|
||||
if (githubSyncPreviewRequestId.current !== requestId) return;
|
||||
setGithubRepo(result.repository.repo);
|
||||
setGitHubSyncPreview(result as GitHubSkillSyncPreview);
|
||||
} catch (error) {
|
||||
if (githubSyncPreviewRequestId.current !== requestId) return;
|
||||
toast.error(getUserFacingConvexError(error, "GitHub repository could not be previewed."));
|
||||
} finally {
|
||||
setIsPreviewingGitHubSource(false);
|
||||
if (githubSyncPreviewRequestId.current === requestId) {
|
||||
setIsPreviewingGitHubSource(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1706,17 +1714,21 @@ export function Settings() {
|
||||
publisherOptions={githubSourcePublishers}
|
||||
selectedPublisherId={selectedSourcePublisher.publisher._id}
|
||||
onPublisherChange={(publisherId) => {
|
||||
githubSyncPreviewRequestId.current += 1;
|
||||
setSelectedSourcePublisherId(publisherId);
|
||||
setGithubRepo("");
|
||||
setGitHubSyncPreview(null);
|
||||
setIsPreviewingGitHubSource(false);
|
||||
}}
|
||||
repositories={githubRepositories}
|
||||
repositoriesError={githubRepositoriesError}
|
||||
isLoadingRepositories={isLoadingGitHubRepositories}
|
||||
githubRepo={githubRepo}
|
||||
onGithubRepoChange={(repo) => {
|
||||
githubSyncPreviewRequestId.current += 1;
|
||||
setGithubRepo(repo);
|
||||
setGitHubSyncPreview(null);
|
||||
setIsPreviewingGitHubSource(false);
|
||||
}}
|
||||
onPreview={onPreviewGitHubSource}
|
||||
isPreviewing={isPreviewingGitHubSource}
|
||||
|
||||
Reference in New Issue
Block a user