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
7 changes: 7 additions & 0 deletions apps/gittensory-ui/src/components/site/app-shell.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Link, Outlet, useLocation, useNavigate, useRouterState } from "@tanstack/react-router";

Check warning on line 1 in apps/gittensory-ui/src/components/site/app-shell.tsx

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #792.

Check notice on line 1 in apps/gittensory-ui/src/components/site/app-shell.tsx

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #792.

Check notice on line 1 in apps/gittensory-ui/src/components/site/app-shell.tsx

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Titles/paths share 6 meaningful terms.

Check notice on line 1 in apps/gittensory-ui/src/components/site/app-shell.tsx

View check run for this annotation

Deleted GitHub App / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.

Check notice on line 1 in apps/gittensory-ui/src/components/site/app-shell.tsx

View check run for this annotation

Deleted GitHub App / Gittensory Context

Open PR queue is busy

This repo has a busy open PR queue in the local Gittensory cache.
import {
Activity,
BarChart3,
Expand All @@ -7,6 +7,7 @@
LayoutGrid,
Loader2,
LogOut,
ScrollText,
TerminalSquare,
Wrench,
Workflow,
Expand Down Expand Up @@ -68,6 +69,12 @@
icon: Activity,
roles: ["miner", "maintainer", "owner", "operator"],
},
{
to: "/app/audit",
label: "Skip audit",
icon: ScrollText,
roles: ["maintainer", "owner", "operator"],
},
],
},
{
Expand Down
124 changes: 124 additions & 0 deletions apps/gittensory-ui/src/components/site/audit-feed-model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
export type SkippedPrAuditReason =

Check warning on line 1 in apps/gittensory-ui/src/components/site/audit-feed-model.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #792.

Check notice on line 1 in apps/gittensory-ui/src/components/site/audit-feed-model.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #792.

Check notice on line 1 in apps/gittensory-ui/src/components/site/audit-feed-model.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Titles/paths share 6 meaningful terms.

Check notice on line 1 in apps/gittensory-ui/src/components/site/audit-feed-model.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.

Check notice on line 1 in apps/gittensory-ui/src/components/site/audit-feed-model.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Open PR queue is busy

This repo has a busy open PR queue in the local Gittensory cache.
| "surface_off"
| "missing_author"
| "bot_author"
| "maintainer_author"
| "miner_detection_unavailable"
| "not_official_gittensor_miner";

export type SkippedPrAuditItem = {
repoFullName: string;
pullNumber: number;
reason: string;
timestamp: string;
remediation: string;
};

export type SkippedPrAuditExport = {
generatedAt: string;
limit: number;
hasMore: boolean;
filters: {
repoFullName: string | null;
reason: SkippedPrAuditReason | null;
since: string | null;
};
items: SkippedPrAuditItem[];
};

export const SKIP_REASON_OPTIONS: Array<{ value: "" | SkippedPrAuditReason; label: string }> = [
{ value: "", label: "All reasons" },
{ value: "surface_off", label: "Surface off" },
{ value: "missing_author", label: "Missing author" },
{ value: "bot_author", label: "Bot author" },
{ value: "maintainer_author", label: "Maintainer author" },
{ value: "miner_detection_unavailable", label: "Miner detection unavailable" },
{ value: "not_official_gittensor_miner", label: "Not official Gittensor miner" },
];

export function buildSkippedPrAuditPath(options: {
limit: number;
repoFullName?: string;
reason?: SkippedPrAuditReason;
since?: string;
}): string {
const params = new URLSearchParams();
params.set("limit", String(options.limit));
if (options.repoFullName?.trim()) params.set("repoFullName", options.repoFullName.trim());
if (options.reason) params.set("reason", options.reason);
if (options.since?.trim()) params.set("since", options.since.trim());
return `/v1/app/skipped-pr-audit?${params.toString()}`;
}

/** Parse a datetime-local or ISO-ish value without throwing from Apply Filters. */
export function normalizeSinceInput(input: string): string {
const trimmed = input.trim();
if (!trimmed) return "";
const parsed = Date.parse(trimmed);
if (!Number.isFinite(parsed)) return "";
const date = new Date(parsed);
if (Number.isNaN(date.getTime())) return "";
try {
return date.toISOString();
} catch {
return "";
}
}

export function normalizeSkippedPrAuditExport(data: unknown): SkippedPrAuditExport | null {
if (!data || typeof data !== "object") return null;
const raw = data as Partial<SkippedPrAuditExport>;
if (typeof raw.generatedAt !== "string" || !Array.isArray(raw.items)) return null;
const items = raw.items.filter(
(item): item is SkippedPrAuditItem =>
item != null &&
typeof item === "object" &&
typeof item.repoFullName === "string" &&
typeof item.pullNumber === "number" &&
typeof item.reason === "string" &&
typeof item.timestamp === "string" &&
typeof item.remediation === "string",
);
const filters = raw.filters;
return {
generatedAt: raw.generatedAt,
limit: typeof raw.limit === "number" ? raw.limit : items.length,
hasMore: Boolean(raw.hasMore),
filters: {
repoFullName:
filters && typeof filters.repoFullName === "string" ? filters.repoFullName : null,
reason:
filters && typeof filters.reason === "string"
? (filters.reason as SkippedPrAuditReason)
: null,
since: filters && typeof filters.since === "string" ? filters.since : null,
},
items,
};
}

export function formatSkipReason(reason: string): string {
const match = SKIP_REASON_OPTIONS.find((option) => option.value === reason);
if (match && match.value) return match.label;
return reason.replaceAll("_", " ");
}

export function formatAuditTimestamp(timestamp: string): string {
const parsed = Date.parse(timestamp);
if (!Number.isFinite(parsed)) return timestamp;
return new Date(parsed).toLocaleString(undefined, {
dateStyle: "medium",
timeStyle: "short",
});
}

export function pullRequestHref(repoFullName: string, pullNumber: number): string {
return `https://github.com/${repoFullName}/pull/${pullNumber}`;
}

export function skipReasonTone(reason: string): "ready" | "info" | "warn" | "degraded" {
if (reason === "bot_author" || reason === "not_official_gittensor_miner") return "info";
if (reason === "surface_off" || reason === "maintainer_author") return "warn";
if (reason === "miner_detection_unavailable" || reason === "missing_author") return "degraded";
return "ready";
}
211 changes: 211 additions & 0 deletions apps/gittensory-ui/src/components/site/audit-feed.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";

Check warning on line 1 in apps/gittensory-ui/src/components/site/audit-feed.test.tsx

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #792.

Check notice on line 1 in apps/gittensory-ui/src/components/site/audit-feed.test.tsx

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #792.

Check notice on line 1 in apps/gittensory-ui/src/components/site/audit-feed.test.tsx

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Titles/paths share 6 meaningful terms.

Check notice on line 1 in apps/gittensory-ui/src/components/site/audit-feed.test.tsx

View check run for this annotation

Deleted GitHub App / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.

Check notice on line 1 in apps/gittensory-ui/src/components/site/audit-feed.test.tsx

View check run for this annotation

Deleted GitHub App / Gittensory Context

Open PR queue is busy

This repo has a busy open PR queue in the local Gittensory cache.
import { beforeEach, describe, expect, it, vi } from "vitest";

const { apiFetch } = vi.hoisted(() => ({ apiFetch: vi.fn() }));
vi.mock("@/lib/api/request", () => ({ apiFetch: (...args: unknown[]) => apiFetch(...args) }));
vi.mock("@/lib/api/origin", () => ({ getApiOrigin: () => "https://api.test" }));

import {
buildSkippedPrAuditPath,
formatSkipReason,
normalizeSinceInput,
normalizeSkippedPrAuditExport,
pullRequestHref,
} from "@/components/site/audit-feed-model";
import { AuditFeed } from "@/components/site/audit-feed";

const SAMPLE: {
generatedAt: string;
limit: number;
hasMore: boolean;
filters: { repoFullName: null; reason: null; since: null };
items: Array<{
repoFullName: string;
pullNumber: number;
reason: string;
timestamp: string;
remediation: string;
}>;
} = {
generatedAt: "2026-05-28T00:00:05.000Z",
limit: 50,
hasMore: false,
filters: { repoFullName: null, reason: null, since: null },
items: [
{
repoFullName: "repo-owner/owned-repo",
pullNumber: 6,
reason: "surface_off",
timestamp: "2026-05-28T00:00:04.000Z",
remediation: "Enable a PR public surface in repository settings.",
},
],
};

describe("audit feed helpers", () => {
it("builds query paths for skipped PR audit filters", () => {
expect(buildSkippedPrAuditPath({ limit: 25 })).toBe("/v1/app/skipped-pr-audit?limit=25");
expect(
buildSkippedPrAuditPath({
limit: 50,
repoFullName: "repo-owner/owned-repo",
reason: "bot_author",
since: "2026-05-28T00:00:00.000Z",
}),
).toBe(
"/v1/app/skipped-pr-audit?limit=50&repoFullName=repo-owner%2Fowned-repo&reason=bot_author&since=2026-05-28T00%3A00%3A00.000Z",
);
});

it("formats skip reasons and pull request links", () => {
expect(formatSkipReason("surface_off")).toBe("Surface off");
expect(formatSkipReason("legacy_skip_reason")).toBe("legacy skip reason");
expect(pullRequestHref("repo-owner/owned-repo", 6)).toBe(
"https://github.com/repo-owner/owned-repo/pull/6",
);
});

it("normalizes since input without throwing on invalid dates", () => {
expect(normalizeSinceInput("")).toBe("");
expect(normalizeSinceInput(" ")).toBe("");
expect(normalizeSinceInput("not-a-date")).toBe("");
expect(normalizeSinceInput("2026-05-28T00:00:00.000Z")).toBe("2026-05-28T00:00:00.000Z");
expect(() => normalizeSinceInput("definitely-not-a-date")).not.toThrow();
});

it("normalizes skipped-pr audit exports and rejects malformed payloads", () => {
expect(normalizeSkippedPrAuditExport(SAMPLE)).toEqual(SAMPLE);
expect(normalizeSkippedPrAuditExport({ ...SAMPLE, items: [] })).toMatchObject({ items: [] });
expect(normalizeSkippedPrAuditExport(null)).toBeNull();
expect(normalizeSkippedPrAuditExport({ generatedAt: "2026-05-28T00:00:05.000Z" })).toBeNull();
expect(
normalizeSkippedPrAuditExport({
...SAMPLE,
items: [
{
repoFullName: "x/y",
pullNumber: 1,
reason: "bot_author",
timestamp: "t",
remediation: "r",
},
null,
"bad",
],
}),
).toMatchObject({ items: [{ repoFullName: "x/y", pullNumber: 1 }] });
});
});

describe("AuditFeed", () => {
beforeEach(() => {
apiFetch.mockReset();
apiFetch.mockResolvedValue({ ok: true, data: SAMPLE });
});

it("renders populated audit rows from the skipped-pr-audit API", async () => {
render(<AuditFeed />);
expect(await screen.findByText("repo-owner/owned-repo")).toBeTruthy();
expect(screen.getByText("Enable a PR public surface in repository settings.")).toBeTruthy();
expect(screen.getByRole("link", { name: /#6/i }).getAttribute("href")).toBe(
"https://github.com/repo-owner/owned-repo/pull/6",
);
expect(apiFetch).toHaveBeenCalledWith(
"https://api.test/v1/app/skipped-pr-audit?limit=50",
expect.objectContaining({ credentials: "include" }),
);
});

it("shows an empty state when the audit export has no items", async () => {
apiFetch.mockResolvedValue({ ok: true, data: { ...SAMPLE, items: [] } });
render(<AuditFeed />);
expect(await screen.findByText("No skipped PR events")).toBeTruthy();
});

it("shows an error state when the audit request fails", async () => {
apiFetch.mockResolvedValue({ ok: false, message: "insufficient_role" });
render(<AuditFeed />);
expect(await screen.findByText("Couldn't load skip audit")).toBeTruthy();
expect(screen.getByText("insufficient_role")).toBeTruthy();
});

it("applies repository filters to subsequent audit requests", async () => {
render(<AuditFeed />);
await screen.findByText("repo-owner/owned-repo");
apiFetch.mockClear();
apiFetch.mockResolvedValue({ ok: true, data: SAMPLE });

fireEvent.change(screen.getByPlaceholderText("owner/repo"), {
target: { value: "repo-owner/owned-repo" },
});
fireEvent.click(screen.getByRole("button", { name: /apply filters/i }));

await waitFor(() =>
expect(apiFetch).toHaveBeenCalledWith(
expect.stringContaining("repoFullName=repo-owner%2Fowned-repo"),
expect.any(Object),
),
);
});

it("ignores invalid since values when applying filters", async () => {
render(<AuditFeed />);
await screen.findByText("repo-owner/owned-repo");
apiFetch.mockClear();
apiFetch.mockResolvedValue({ ok: true, data: SAMPLE });

fireEvent.change(screen.getByLabelText(/^since$/i), {
target: { value: "definitely-not-a-date" },
});
fireEvent.change(screen.getByPlaceholderText("owner/repo"), {
target: { value: "repo-owner/owned-repo" },
});
expect(() =>
fireEvent.click(screen.getByRole("button", { name: /apply filters/i })),
).not.toThrow();

await waitFor(() =>
expect(apiFetch).toHaveBeenCalledWith(
expect.stringContaining("repoFullName=repo-owner%2Fowned-repo"),
expect.any(Object),
),
);
expect(apiFetch.mock.calls.some(([url]) => String(url).includes("since="))).toBe(false);
});

it("shows a role error when the feed is disabled", async () => {
render(<AuditFeed enabled={false} />);
expect(await screen.findByText("Couldn't load skip audit")).toBeTruthy();
expect(screen.getByText("This audit feed is unavailable for your current role.")).toBeTruthy();
expect(apiFetch).not.toHaveBeenCalled();
});

it("loads more rows until the maximum page size", async () => {
apiFetch.mockResolvedValue({ ok: true, data: { ...SAMPLE, hasMore: true } });
render(<AuditFeed />);
await screen.findByText("repo-owner/owned-repo");
apiFetch.mockClear();
apiFetch.mockResolvedValue({ ok: true, data: { ...SAMPLE, hasMore: true, limit: 100 } });

fireEvent.click(screen.getByRole("button", { name: /load more/i }));

await waitFor(() =>
expect(apiFetch).toHaveBeenCalledWith(
"https://api.test/v1/app/skipped-pr-audit?limit=100",
expect.any(Object),
),
);

expect(screen.getByText(/maximum page size \(100\)/i)).toBeTruthy();
expect(screen.queryByRole("button", { name: /load more/i })).toBeNull();
});

it("shows an error state when the audit response is malformed", async () => {
apiFetch.mockResolvedValue({ ok: true, data: { generatedAt: "2026-05-28T00:00:05.000Z" } });
render(<AuditFeed />);
expect(await screen.findByText("Couldn't load skip audit")).toBeTruthy();
expect(
screen.getByText("The skipped PR audit endpoint returned an unexpected response."),
).toBeTruthy();
});
});
Loading
Loading