Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,13 @@ jobs:

- name: Test
run: bun run test
env:
VITE_CONVEX_URL: https://example.invalid

- name: Coverage
run: bun run coverage
env:
VITE_CONVEX_URL: https://example.invalid

- name: ClawHub CLI Verify
run: bun run --cwd packages/clawhub verify
Expand Down
35 changes: 35 additions & 0 deletions convex/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,41 @@ export const getByHandle = query({
},
});

/** Lightweight stats for user hover tooltips. Uses the skills by_owner index. */
export const getHoverStats = query({
args: { userId: v.id("users") },
handler: async (ctx, args) => {
const skills = [];
let cursor: string | null = null;

for (;;) {
const page = await ctx.db
.query("skills")
.withIndex("by_owner", (q) => q.eq("ownerUserId", args.userId))
.paginate({ cursor, numItems: 100 });
skills.push(...page.page);
if (page.isDone) {
break;
}
cursor = page.continueCursor;
}

const active = skills.filter((s) => !s.softDeletedAt);
let totalStars = 0;
let totalDownloads = 0;
for (const s of active) {
totalStars += s.stats?.stars ?? 0;
totalDownloads += s.stats?.downloads ?? 0;
}

return {
publishedSkills: active.length,
totalStars,
totalDownloads,
};
},
});

export const getReservedHandleInternal = internalQuery({
args: { handle: v.string() },
handler: async (ctx, args) => {
Expand Down
Binary file modified public/clawd-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/clawd-mark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/favicon.ico
Binary file not shown.
45 changes: 45 additions & 0 deletions public/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/logo192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/logo512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 8 additions & 7 deletions src/__tests__/header.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ vi.mock("@convex-dev/auth/react", () => ({
}),
}));

const authStatusMock = vi.fn(() => ({
isAuthenticated: false,
isLoading: false,
me: null,
}));

vi.mock("../lib/useAuthStatus", () => ({
useAuthStatus: () => ({
isAuthenticated: false,
isLoading: false,
me: null,
}),
useAuthStatus: () => authStatusMock(),
}));

vi.mock("../lib/theme", () => ({
Expand Down Expand Up @@ -125,5 +127,4 @@ describe("Header", () => {
expect(screen.getAllByText("Users")).toHaveLength(2);
expect(screen.getByPlaceholderText("Search skills, plugins, users")).toBeTruthy();
expect(convexQueryMock).not.toHaveBeenCalled();
});
});
});});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove stray closing token in header test file

The file now ends with });});, which is a syntax error, so Vitest cannot parse this test module and bun run test will fail before executing the suite. This blocks CI and hides regressions in unrelated tests until the extra }); is removed.

Useful? React with 👍 / 👎.

1 change: 1 addition & 0 deletions src/__tests__/import.route.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const useAuthStatusMock = vi.fn();
let useActionCallCount = 0;

vi.mock("convex/react", () => ({
ConvexReactClient: class {},
useQuery: (...args: unknown[]) => useQueryMock(...args),
useAction: () => {
const action = [previewImport, previewCandidate, importSkill][useActionCallCount % 3];
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/packages-publish-route.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const useAuthStatusMock = vi.fn();
const originalFetch = globalThis.fetch;

vi.mock("convex/react", () => ({
ConvexReactClient: class {},
useMutation: () => generateUploadUrl,
useAction: () => publishRelease,
useQuery: () => undefined,
Expand Down
8 changes: 8 additions & 0 deletions src/__tests__/search-route.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { describe, expect, it, vi } from "vitest";

process.env.VITE_CONVEX_URL = process.env.VITE_CONVEX_URL ?? "https://example.convex.cloud";


vi.mock("../convex/client", () => ({
convex: {},
convexHttp: { query: vi.fn() },
}));

vi.mock("@tanstack/react-router", () => ({
createFileRoute: () => (config: { validateSearch?: unknown; component?: unknown }) => ({
__config: config,
Expand Down
13 changes: 13 additions & 0 deletions src/__tests__/skill-detail-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ import { SkillDetailPage } from "../components/SkillDetailPage";
const navigateMock = vi.fn();
const useAuthStatusMock = vi.fn();

process.env.VITE_CONVEX_URL = process.env.VITE_CONVEX_URL ?? "https://example.convex.cloud";


vi.mock("../components/UserBadge", () => ({
UserBadge: () => null,
}));

vi.mock("../convex/client", () => ({
convex: {},
convexHttp: { query: vi.fn() },
}));

vi.mock("@tanstack/react-router", () => ({
Link: ({ children }: { children: unknown }) => children,
useNavigate: () => navigateMock,
Expand All @@ -15,6 +27,7 @@ const useQueryMock = vi.fn();
const getReadmeMock = vi.fn();

vi.mock("convex/react", () => ({
ConvexReactClient: class {},
useQuery: (...args: unknown[]) => useQueryMock(...args),
useMutation: () => vi.fn(),
useAction: () => getReadmeMock,
Expand Down
22 changes: 22 additions & 0 deletions src/__tests__/skill-route-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ vi.mock("@tanstack/react-router", () => ({
createFileRoute:
() =>
(config: {
beforeLoad?: (args: { params: { owner: string; slug: string } }) => unknown;
loader?: (args: { params: { owner: string; slug: string } }) => Promise<unknown>;
component?: unknown;
head?: unknown;
Expand All @@ -31,6 +32,7 @@ vi.mock("../lib/skillPage", () => ({
async function loadRoute() {
return (await import("../routes/$owner/$slug")).Route as unknown as {
__config: {
beforeLoad?: (args: { params: { owner: string; slug: string } }) => unknown;
loader?: (args: { params: { owner: string; slug: string } }) => Promise<unknown>;
head?: (args: {
params: { owner: string; slug: string };
Expand All @@ -45,6 +47,14 @@ async function loadRoute() {
};
}

async function runBeforeLoad(params: { owner: string; slug: string }) {
const route = await loadRoute();
const beforeLoad = route.__config.beforeLoad as ((args: {
params: { owner: string; slug: string };
}) => unknown) | undefined;
return beforeLoad?.({ params });
}

async function runLoader(params: { owner: string; slug: string }) {
const route = await loadRoute();
const loader = route.__config.loader as (args: {
Expand All @@ -71,6 +81,18 @@ function runHead(
}

describe("skill route loader", () => {
it("allows numeric owner handles in beforeLoad", () => {
expect(() => runBeforeLoad({ owner: "123abc", slug: "weather" })).not.toThrow();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Await beforeLoad assertions in route guard tests

runBeforeLoad is async, but these checks use expect(() => runBeforeLoad(...)).not.toThrow(), which only verifies synchronous throws. If beforeLoad rejects (e.g., notFound()), the promise rejection is not asserted here, so these tests can pass even when the guard behavior regresses.

Useful? React with 👍 / 👎.

});

it("allows raw owner ids in beforeLoad", () => {
expect(() => runBeforeLoad({ owner: "users:abc123", slug: "weather" })).not.toThrow();
});

it("allows raw publisher ids in beforeLoad", () => {
expect(() => runBeforeLoad({ owner: "publishers:abc123", slug: "weather" })).not.toThrow();
});

beforeEach(() => {
fetchSkillPageDataMock.mockReset();
});
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/skills-index-load-more.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ vi.mock("@tanstack/react-router", () => ({
}));

vi.mock("convex/react", () => ({
ConvexReactClient: class {},
useAction: (...args: unknown[]) => convexReactMocks.useAction(...args),
useQuery: (...args: unknown[]) => convexReactMocks.useQuery(...args),
}));
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/skills-index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ vi.mock("@tanstack/react-router", () => ({
}));

vi.mock("convex/react", () => ({
ConvexReactClient: class {},
useAction: (...args: unknown[]) => convexReactMocks.useAction(...args),
useQuery: (...args: unknown[]) => convexReactMocks.useQuery(...args),
}));
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/upload.route.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const useAuthStatusMock = vi.fn();
let useActionCallCount = 0;

vi.mock("convex/react", () => ({
ConvexReactClient: class {},
useQuery: (...args: unknown[]) => useQueryMock(...args),
useMutation: () => generateUploadUrl,
useAction: () => {
Expand Down
11 changes: 7 additions & 4 deletions src/components/AppProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useEffect, useRef } from "react";
import { convex } from "../convex/client";
import { getUserFacingAuthError, normalizeAuthErrorMessage } from "../lib/authErrorMessage";
import { clearAuthError, setAuthError } from "../lib/useAuthError";
import { TooltipProvider } from "./ui/tooltip";
import { UserBootstrap } from "./UserBootstrap";

function getPendingAuthCode() {
Expand Down Expand Up @@ -82,10 +83,12 @@ export function AuthErrorHandler() {
export function AppProviders({ children }: { children: React.ReactNode }) {
return (
<ConvexAuthProvider client={convex} shouldHandleCode={false}>
<AuthCodeHandler />
<AuthErrorHandler />
<UserBootstrap />
{children}
<TooltipProvider delayDuration={400}>
<AuthCodeHandler />
<AuthErrorHandler />
<UserBootstrap />
{children}
</TooltipProvider>
</ConvexAuthProvider>
);
}
12 changes: 1 addition & 11 deletions src/components/DeploymentDriftBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,7 @@ function DeploymentDriftBannerContent() {
return (
<div
role="alert"
style={{
margin: "16px auto 0",
width: "min(1100px, calc(100vw - 32px))",
border: "1px solid #f59e0b",
background: "#fff7ed",
color: "#9a3412",
borderRadius: "14px",
padding: "12px 16px",
fontSize: "0.95rem",
lineHeight: 1.4,
}}
className="mx-auto mt-4 w-[min(1100px,calc(100vw-32px))] rounded-[14px] border border-status-warning-fg/40 bg-status-warning-bg px-4 py-3 text-[0.95rem] leading-[1.4] text-status-warning-fg"
>
Deploy mismatch detected. Frontend expects backend build <code>{drift.expectedBuildSha}</code>{" "}
but Convex reports <code>{drift.actualBuildSha}</code>.
Expand Down
Loading