${escapeHtml(panel.label || "Panel")}
${escapeHtml(panel.badge || "live")}
@@ -49,7 +104,6 @@ async function load(container, target) {
`,
)
.join("");
- renderActions(body, response.payload?.actions);
}
function escapeHtml(value) {
@@ -73,15 +127,15 @@ function renderActions(body, actions) {
const list = Array.isArray(actions) ? actions : [];
if (list.length === 0) return;
const container = document.createElement("section");
- container.className = "gittensory-overlay__panel";
+ container.className = "gittensory-overlay__panel gittensory-overlay__panel--private";
container.innerHTML = `
Actions
extension
-
+
`;
- const actionsNode = container.querySelector(".gittensory-overlay__actions");
+ const actionsNode = container.querySelector(".gittensory-overlay__action-buttons");
if (!actionsNode) return;
for (const action of list) {
if (action?.id === "copy_public_safe_packet" && typeof action?.markdown === "string") {
@@ -119,3 +173,14 @@ function renderActions(body, actions) {
}
body.appendChild(container);
}
+
+if (globalThis.__GITTENSORY_EXTENSION_TEST__) {
+ globalThis.__gittensoryContentInternals = {
+ matchPullRequestTarget,
+ renderPullContext,
+ renderSection,
+ renderLegacyPanels,
+ renderActions,
+ escapeHtml,
+ };
+}
diff --git a/apps/gittensory-extension/styles.css b/apps/gittensory-extension/styles.css
index b7268d0e9a..4070d136c5 100644
--- a/apps/gittensory-extension/styles.css
+++ b/apps/gittensory-extension/styles.css
@@ -1,4 +1,18 @@
.gittensory-overlay {
+ border: 1px solid rgba(126, 231, 188, 0.45);
+ border-radius: 8px;
+ background: #0f1117;
+ color: #f4f7f5;
+ font: 13px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+}
+
+.gittensory-overlay--sidebar {
+ width: 100%;
+ margin-bottom: 16px;
+ overflow: hidden;
+}
+
+.gittensory-overlay--floating {
position: fixed;
right: 16px;
bottom: 16px;
@@ -6,12 +20,7 @@
width: min(360px, calc(100vw - 32px));
max-height: min(560px, calc(100vh - 32px));
overflow: auto;
- border: 1px solid rgba(126, 231, 188, 0.45);
- border-radius: 8px;
- background: #0f1117;
- color: #f4f7f5;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45);
- font: 13px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.gittensory-overlay__header {
@@ -23,6 +32,17 @@
font-weight: 650;
}
+.gittensory-overlay__privacy {
+ border: 1px solid rgba(126, 231, 188, 0.35);
+ border-radius: 999px;
+ color: #7ee7bc;
+ font-size: 10px;
+ font-weight: 700;
+ line-height: 1;
+ padding: 3px 6px;
+ text-transform: uppercase;
+}
+
.gittensory-overlay__mark {
display: inline-flex;
width: 22px;
@@ -56,6 +76,18 @@
padding: 10px;
}
+.gittensory-overlay__panel--good {
+ border-color: rgba(126, 231, 188, 0.35);
+}
+
+.gittensory-overlay__panel--warn {
+ border-color: rgba(255, 189, 105, 0.45);
+}
+
+.gittensory-overlay__panel--private {
+ border-color: rgba(145, 181, 255, 0.38);
+}
+
.gittensory-overlay__panel + .gittensory-overlay__panel {
margin-top: 8px;
}
@@ -77,6 +109,16 @@
text-transform: uppercase;
}
+.gittensory-overlay__panel--warn .gittensory-overlay__panel-head span {
+ border-color: rgba(255, 189, 105, 0.45);
+ color: #ffbd69;
+}
+
+.gittensory-overlay__panel--private .gittensory-overlay__panel-head span {
+ border-color: rgba(145, 181, 255, 0.45);
+ color: #91b5ff;
+}
+
.gittensory-overlay dl {
margin: 0;
}
@@ -96,8 +138,69 @@
margin: 0;
color: rgba(244, 247, 245, 0.9);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ text-align: right;
+ overflow-wrap: anywhere;
+}
+
+.gittensory-overlay__list,
+.gittensory-overlay__actions {
+ margin: 8px 0 0;
+ padding-left: 18px;
+}
+
+.gittensory-overlay__list li,
+.gittensory-overlay__actions li {
+ color: rgba(244, 247, 245, 0.78);
+ margin-top: 4px;
+}
+
+.gittensory-overlay__actions li {
+ color: rgba(244, 247, 245, 0.9);
+}
+
+.gittensory-overlay__action-buttons {
+ display: grid;
+ gap: 8px;
+}
+
+.gittensory-overlay__action-buttons button,
+.gittensory-overlay__action-buttons details {
+ border: 1px solid rgba(145, 181, 255, 0.32);
+ border-radius: 6px;
+ background: rgba(145, 181, 255, 0.08);
+ color: #f4f7f5;
+ font: inherit;
+}
+
+.gittensory-overlay__action-buttons button {
+ cursor: pointer;
+ padding: 7px 9px;
+ text-align: left;
+}
+
+.gittensory-overlay__action-buttons details {
+ padding: 7px 9px;
+}
+
+.gittensory-overlay__action-buttons summary {
+ cursor: pointer;
+ font-weight: 650;
+}
+
+.gittensory-overlay__action-buttons ul {
+ margin: 6px 0 0;
+ padding-left: 18px;
+}
+
+.gittensory-overlay__action-buttons li {
+ color: rgba(244, 247, 245, 0.82);
+ margin-top: 4px;
}
.gittensory-overlay__error {
color: #ffb4a8;
}
+
+.gittensory-overlay__empty {
+ color: rgba(244, 247, 245, 0.72);
+}
diff --git a/scripts/write-ui-openapi.ts b/scripts/write-ui-openapi.ts
index 52c571afc8..0d73a3d411 100644
--- a/scripts/write-ui-openapi.ts
+++ b/scripts/write-ui-openapi.ts
@@ -11,10 +11,12 @@ const checkOnly = process.argv.includes("--check");
const spec = buildOpenApiSpec();
spec.servers = [{ url: "https://gittensory-api.aethereal.dev", description: "Production" }];
-const next = `${JSON.stringify(spec, null, 2)}\n`;
+const current = await readFile(target, "utf8").catch(() => "");
+const currentSpec = parseCurrentSpec(current);
+const orderedSpec = currentSpec ? preserveExistingObjectOrder(spec, currentSpec) : spec;
+const next = `${JSON.stringify(orderedSpec, null, 2)}\n`;
if (checkOnly) {
- const current = await readFile(target, "utf8").catch(() => "");
if (current !== next) {
console.error("apps/gittensory-ui/public/openapi.json is stale; run npm run ui:openapi.");
process.exit(1);
@@ -24,3 +26,30 @@ if (checkOnly) {
await writeFile(target, next);
console.log("wrote apps/gittensory-ui/public/openapi.json");
}
+
+function parseCurrentSpec(currentText: string): Record
| null {
+ try {
+ return JSON.parse(currentText) as Record;
+ } catch {
+ return null;
+ }
+}
+
+function preserveExistingObjectOrder(next: T, current: unknown): T {
+ if (Array.isArray(next)) return next.map((item, index) => preserveExistingObjectOrder(item, Array.isArray(current) ? current[index] : undefined)) as T;
+ if (!isPlainObject(next)) return next;
+
+ const currentObject = isPlainObject(current) ? current : {};
+ const ordered: Record = {};
+ for (const key of Object.keys(currentObject)) {
+ if (key in next) ordered[key] = preserveExistingObjectOrder(next[key], currentObject[key]);
+ }
+ for (const key of Object.keys(next)) {
+ if (!(key in ordered)) ordered[key] = preserveExistingObjectOrder(next[key], currentObject[key]);
+ }
+ return ordered as T;
+}
+
+function isPlainObject(value: unknown): value is Record {
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
+}
diff --git a/src/api/routes.ts b/src/api/routes.ts
index a611313922..0b032b6ab8 100644
--- a/src/api/routes.ts
+++ b/src/api/routes.ts
@@ -162,13 +162,17 @@ import {
buildMaintainerCutReadiness,
buildMaintainerLaneReport,
buildPullRequestMaintainerPacket,
+ buildRoleContext,
buildPreflightResult,
buildQueueHealth,
buildRegistryChangeReport,
+ type ContributorOutcomeHistory,
+ type PullRequestMaintainerPacket,
+ type RoleContext,
} from "../signals/engine";
import { attachDataQuality, buildCoreSignalFidelity, buildFreshnessSloReport, buildRepoDataQuality, buildSignalFidelity } from "../signals/data-quality";
import { buildContributorOpenPrMonitor } from "../signals/contributor-open-pr-monitor";
-import { buildPullRequestReviewability } from "../signals/reward-risk";
+import { buildPullRequestReviewability, type PullRequestReviewability } from "../signals/reward-risk";
import { buildLocalBranchAnalysis, findCurrentBranchPullRequest } from "../signals/local-branch";
import { MAX_LOCAL_SCORER_WARNING_CHARS, MAX_LOCAL_SCORER_WARNING_COUNT } from "../signals/local-scorer-diagnostics";
import { loadRepoFocusManifest } from "../signals/focus-manifest-loader";
@@ -1147,7 +1151,7 @@ export function createApp() {
]);
const contributor = pullRequest?.authorLogin;
const contributorContext = contributor ? await loadContributorFastContext(c.env, contributor).catch(() => null) : null;
- const reviewability = buildPullRequestReviewability({
+ const signalArgs = {
repo,
pullRequest,
issues,
@@ -1160,6 +1164,16 @@ export function createApp() {
pullNumber,
profile: contributorContext?.profile,
outcomeHistory: contributorContext?.outcomeHistory,
+ };
+ const packet = buildPullRequestMaintainerPacket(signalArgs);
+ const reviewability = buildPullRequestReviewability(signalArgs);
+ const roleContext = buildRoleContext({
+ login: contributor ?? contributorContext?.profile.login ?? "unknown",
+ repo,
+ repoFullName: fullName,
+ pullRequests,
+ issues,
+ profile: contributorContext?.profile,
});
const publicSafePacketMarkdown = buildExtensionPublicSafePacket({
repoFullName: fullName,
@@ -1189,32 +1203,20 @@ export function createApp() {
clientName: "browser_extension",
metadata: { hasContributorContext: Boolean(contributorContext), hasCachedPullRequest: Boolean(pullRequest) },
});
- return c.json({
- generatedAt: nowIso(),
- repoFullName: fullName,
- pullNumber,
- reviewability,
- actions: [
- {
- id: "copy_public_safe_packet",
- label: "Copy public-safe packet",
- visibility: "public_safe",
- markdown: publicSafePacketMarkdown,
- },
- {
- id: "view_private_blockers",
- label: "View private blockers",
- visibility: "private",
- requiresAuth: true,
- blockers: privateBlockers,
- },
- ],
- panels: [
- { label: "Reviewability", badge: reviewability.action, rows: [{ k: "action", v: reviewability.action }, { k: "score", v: String(reviewability.score) }] },
- { label: "Contributor", badge: contributor ?? "unknown", rows: [{ k: "author", v: contributor ?? "unknown" }, { k: "prs", v: String(contributorContext?.contributorPullRequests.length ?? 0) }] },
- { label: "Boundary", badge: "private", rows: [{ k: "surface", v: "browser extension" }, { k: "public", v: "no" }] },
- ],
- });
+ return c.json(
+ buildExtensionPullContextPayload({
+ fullName,
+ pullNumber,
+ pullRequest,
+ contributorContext,
+ packet,
+ reviewability,
+ roleContext,
+ pullRequests,
+ publicSafePacketMarkdown,
+ privateBlockers,
+ }),
+ );
});
app.get("/v1/registry/snapshot", async (c) => {
@@ -2922,6 +2924,316 @@ async function loadContributorFastContext(env: Env, login: string) {
};
}
+type ExtensionContributorContext = Awaited> | null;
+
+type ExtensionPullContextSection = {
+ id: string;
+ label: string;
+ badge: string;
+ tone: "good" | "warn" | "neutral" | "private";
+ rows: Array<{ label: string; value: string }>;
+ items: string[];
+ actions: string[];
+};
+
+type ExtensionQueueLevel = "low" | "medium" | "high" | "unknown";
+
+const EXTENSION_REVIEWABILITY_TONES: Record = {
+ review_now: "good",
+ needs_author: "warn",
+ likely_duplicate: "warn",
+ close_or_redirect: "warn",
+ watch: "neutral",
+ maintainer_lane: "private",
+};
+
+const EXTENSION_QUEUE_TONES: Record = {
+ low: "good",
+ medium: "warn",
+ high: "warn",
+ unknown: "neutral",
+};
+
+const EXTENSION_QUEUE_DETAILS: Record = {
+ low: "Cached repo and author queue pressure are low enough for normal review flow.",
+ medium: "Some open PR pressure is visible; check queue hygiene before encouraging more work from the same lane.",
+ high: "Resolve open PR pressure before encouraging more work from the same lane.",
+ unknown: "Author repo-history context is unavailable; use cached repo open PR count as a lightweight pressure signal.",
+};
+
+function buildExtensionPullContextPayload(args: {
+ fullName: string;
+ pullNumber: number;
+ pullRequest: PullRequestRecord | null;
+ contributorContext: ExtensionContributorContext;
+ packet: PullRequestMaintainerPacket;
+ reviewability: PullRequestReviewability;
+ roleContext: RoleContext;
+ pullRequests: PullRequestRecord[];
+ publicSafePacketMarkdown: string;
+ privateBlockers: ReturnType;
+}) {
+ const contributor = args.pullRequest?.authorLogin ?? args.contributorContext?.profile.login ?? "unknown";
+ const minerStatus = extensionMinerStatus(args.contributorContext);
+ const repoOutcome = args.contributorContext?.outcomeHistory.repoOutcomes.find((outcome) => outcome.repoFullName.toLowerCase() === args.fullName.toLowerCase());
+ const repoOpenPullRequests = args.pullRequests.filter((pull) => pull.repoFullName === args.fullName && pull.state === "open").length;
+ const queue = extensionQueuePressure(repoOpenPullRequests, repoOutcome);
+ const linkedIssues = args.packet.reviewSignals.linkedIssues;
+ const duplicateCount = args.packet.reviewSignals.collisionClusters;
+ const publicActions = uniqueStrings([...args.reviewability.maintainerNextSteps, ...args.packet.contributorNextSteps]).slice(0, 5).map(sanitizeExtensionPrivateText);
+ const sections: ExtensionPullContextSection[] = [
+ cleanExtensionSection({
+ id: "miner-context",
+ label: "Miner Context",
+ badge: minerStatus.badge,
+ tone: minerStatus.tone,
+ rows: [
+ { label: "author", value: contributor },
+ { label: "status", value: minerStatus.label },
+ { label: "source", value: minerStatus.source },
+ ],
+ items: [minerStatus.detail],
+ actions: [],
+ }),
+ cleanExtensionSection({
+ id: "lane-fit",
+ label: "Lane Fit",
+ badge: args.roleContext.maintainerLane ? "maintainer lane" : args.roleContext.role,
+ tone: args.roleContext.maintainerLane ? "private" : args.roleContext.role === "outside_contributor" ? "good" : "neutral",
+ rows: [
+ { label: "role", value: args.roleContext.role },
+ { label: "normal evidence", value: args.roleContext.normalContributorEvidenceAllowed ? "allowed" : "separate lane" },
+ { label: "source", value: args.roleContext.source },
+ ],
+ items: uniqueStrings([args.roleContext.guidance, ...args.roleContext.reasons]).slice(0, 4),
+ actions: [],
+ }),
+ cleanExtensionSection({
+ id: "duplicate-risk",
+ label: "Duplicate Risk",
+ badge: duplicateCount > 0 ? "check overlap" : "clear",
+ tone: duplicateCount > 0 ? "warn" : "good",
+ rows: [
+ { label: "clusters", value: String(duplicateCount) },
+ { label: "action", value: duplicateCount > 0 ? "compare before review" : "no cached overlap" },
+ ],
+ items:
+ duplicateCount > 0
+ ? ["Compare linked issues, active PRs, and recent merges before detailed review."]
+ : ["No duplicate or WIP collision cluster includes this PR in cached metadata."],
+ actions: [],
+ }),
+ cleanExtensionSection({
+ id: "linked-issue-state",
+ label: "Linked Issue State",
+ badge: linkedIssues.length > 0 ? "linked" : "missing",
+ tone: linkedIssues.length > 0 ? "good" : "warn",
+ rows: [
+ { label: "issues", value: linkedIssues.length > 0 ? linkedIssues.map((issue) => `#${issue}`).join(", ") : "none cached" },
+ { label: "policy", value: linkedIssues.length > 0 ? "review traceable" : "ask for context" },
+ ],
+ items:
+ linkedIssues.length > 0
+ ? [`Cached PR body links ${linkedIssues.map((issue) => `#${issue}`).join(", ")}.`]
+ : ["Ask for a linked issue or a clear no-issue rationale before deep review."],
+ actions: [],
+ }),
+ cleanExtensionSection({
+ id: "queue-pressure",
+ label: "Queue Pressure",
+ badge: queue.level,
+ tone: queue.tone,
+ rows: [
+ { label: "repo open PRs", value: String(repoOpenPullRequests) },
+ { label: "author open PRs", value: queue.authorOpenPullRequests },
+ { label: "author merged", value: queue.authorMergedPullRequests },
+ ],
+ items: [queue.detail],
+ actions: [],
+ }),
+ cleanExtensionSection({
+ id: "public-safe-actions",
+ label: "Public-Safe Packet Actions",
+ badge: args.reviewability.action,
+ tone: EXTENSION_REVIEWABILITY_TONES[args.reviewability.action],
+ rows: [
+ { label: "priority", value: args.packet.reviewPriority },
+ { label: "checks", value: `${args.packet.reviewSignals.checkFailureCount} failing` },
+ { label: "reviews", value: `${args.packet.reviewSignals.reviewCount} cached` },
+ ],
+ items: args.reviewability.whyThisHelps.slice(0, 3),
+ actions: publicActions,
+ }),
+ cleanExtensionSection({
+ id: "boundary",
+ label: "Boundary",
+ badge: "private",
+ tone: "private",
+ rows: [
+ { label: "surface", value: "browser extension" },
+ { label: "public posting", value: "none" },
+ { label: "source upload", value: "none" },
+ ],
+ items: ["This panel is maintainer-private context and does not create comments, labels, checks, or source uploads."],
+ actions: [],
+ }),
+ ];
+
+ return {
+ generatedAt: nowIso(),
+ repoFullName: args.fullName,
+ pullNumber: args.pullNumber,
+ contributor: {
+ login: sanitizeExtensionPrivateText(contributor),
+ minerStatus: minerStatus.status,
+ role: sanitizeExtensionPrivateText(args.roleContext.role),
+ maintainerLane: args.roleContext.maintainerLane,
+ },
+ privacy: {
+ surface: "browser_extension",
+ publicPosting: false,
+ sourceUpload: false,
+ githubMutations: false,
+ },
+ reviewability: args.reviewability,
+ actions: [
+ {
+ id: "copy_public_safe_packet",
+ label: "Copy public-safe packet",
+ visibility: "public_safe",
+ markdown: args.publicSafePacketMarkdown,
+ },
+ {
+ id: "view_private_blockers",
+ label: "View private blockers",
+ visibility: "private",
+ requiresAuth: true,
+ blockers: args.privateBlockers,
+ },
+ ],
+ sections,
+ panels: [
+ {
+ label: "Reviewability",
+ badge: sanitizeExtensionPrivateText(args.reviewability.action),
+ rows: [
+ { k: "action", v: sanitizeExtensionPrivateText(args.reviewability.action) },
+ { k: "score", v: String(args.reviewability.score) },
+ ],
+ },
+ {
+ label: "Contributor",
+ badge: sanitizeExtensionPrivateText(contributor),
+ rows: [
+ { k: "author", v: sanitizeExtensionPrivateText(contributor) },
+ { k: "prs", v: String(args.contributorContext?.contributorPullRequests.length ?? 0) },
+ ],
+ },
+ {
+ label: "Boundary",
+ badge: "private",
+ rows: [
+ { k: "surface", v: "browser extension" },
+ { k: "public", v: "no" },
+ ],
+ },
+ ],
+ };
+}
+
+function extensionMinerStatus(context: ExtensionContributorContext): {
+ status: "confirmed" | "not_found" | "unavailable";
+ badge: string;
+ label: string;
+ source: string;
+ detail: string;
+ tone: ExtensionPullContextSection["tone"];
+} {
+ if (!context) {
+ return {
+ status: "unavailable",
+ badge: "unavailable",
+ label: "official context unavailable",
+ source: "unavailable",
+ detail: "Official contributor context is unavailable; this panel does not guess or post publicly.",
+ tone: "neutral",
+ };
+ }
+ if (context.profile.gittensor) {
+ return {
+ status: "confirmed",
+ badge: "confirmed",
+ label: "confirmed miner",
+ source: "official Gittensor API",
+ detail: "Official miner context is available for private maintainer triage without exposing wallet or key material.",
+ tone: "good",
+ };
+ }
+ return {
+ status: "not_found",
+ badge: "non-miner",
+ label: "no confirmed miner record",
+ source: context.profile.source,
+ detail: "No confirmed miner record is cached for this GitHub login; use normal PR review signals.",
+ tone: "neutral",
+ };
+}
+
+function extensionQueuePressure(
+ repoOpenPullRequests: number,
+ repoOutcome: ContributorOutcomeHistory["repoOutcomes"][number] | undefined,
+): { level: ExtensionQueueLevel; tone: ExtensionPullContextSection["tone"]; authorOpenPullRequests: string; authorMergedPullRequests: string; detail: string } {
+ if (!repoOutcome) {
+ const level = repoOpenPullRequests >= 6 ? "medium" : "unknown";
+ return {
+ level,
+ tone: EXTENSION_QUEUE_TONES[level],
+ authorOpenPullRequests: "unknown",
+ authorMergedPullRequests: "unknown",
+ detail: EXTENSION_QUEUE_DETAILS[level],
+ };
+ }
+ const authorOpenPullRequests = repoOutcome.openPullRequests;
+ const level = extensionQueueLevel(repoOpenPullRequests, authorOpenPullRequests);
+ return {
+ level,
+ tone: EXTENSION_QUEUE_TONES[level],
+ authorOpenPullRequests: String(authorOpenPullRequests),
+ authorMergedPullRequests: String(repoOutcome.mergedPullRequests),
+ detail: EXTENSION_QUEUE_DETAILS[level],
+ };
+}
+
+function extensionQueueLevel(repoOpenPullRequests: number, authorOpenPullRequests: number): "low" | "medium" | "high" {
+ if (repoOpenPullRequests >= 8 || authorOpenPullRequests >= 4) return "high";
+ if (repoOpenPullRequests >= 4 || authorOpenPullRequests >= 2) return "medium";
+ return "low";
+}
+
+function cleanExtensionSection(section: ExtensionPullContextSection): ExtensionPullContextSection {
+ return {
+ id: section.id,
+ label: sanitizeExtensionPrivateText(section.label),
+ badge: sanitizeExtensionPrivateText(section.badge),
+ tone: section.tone,
+ rows: section.rows.map((row) => ({ label: sanitizeExtensionPrivateText(row.label), value: sanitizeExtensionPrivateText(row.value) })),
+ items: section.items.map(sanitizeExtensionPrivateText),
+ actions: section.actions.map(sanitizeExtensionPrivateText),
+ };
+}
+
+function sanitizeExtensionPrivateText(value: unknown): string {
+ const text = String(value).replace(
+ /\b(wallets?|hotkeys?|coldkeys?|seed phrases?|mnemonics?|private keys?|raw trust scores?|trust scores?|raw rankings?|private rankings?|reward estimates?|payouts?|farming)\b|github_pat_[A-Za-z0-9_]+|gh[pousr]_[A-Za-z0-9_]+/gi,
+ "private signal",
+ );
+ return text.replace(/\s+/g, " ").trim();
+}
+
+function uniqueStrings(values: string[]): string[] {
+ return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
+}
+
async function loadCheckSummariesForPullRequests(env: Env, repoFullName: string, input: Parameters[0], pullRequests: Parameters[1]) {
const currentPullRequest = findCurrentBranchPullRequest(input, pullRequests);
return currentPullRequest ? listCheckSummaries(env, repoFullName, currentPullRequest.number) : [];
diff --git a/test/integration/api.test.ts b/test/integration/api.test.ts
index facdf5f6bd..aaad3ca6ba 100644
--- a/test/integration/api.test.ts
+++ b/test/integration/api.test.ts
@@ -447,6 +447,12 @@ describe("api routes", () => {
if (url.includes("/users/oktofeesh1/repos")) {
return Response.json([{ language: "TypeScript" }, { language: "Python" }, { language: "TypeScript" }]);
}
+ if (url.endsWith("/users/other")) {
+ return Response.json({ login: "other", public_repos: 4, followers: 1 });
+ }
+ if (url.includes("/users/other/repos")) {
+ return Response.json([{ language: "TypeScript" }]);
+ }
return new Response("not found", { status: 404 });
});
@@ -800,6 +806,14 @@ describe("api routes", () => {
);
expect(invalidQueueIntelligence.status).toBe(400);
+ const missingQueuePullRequests = await app.request(
+ "/v1/internal/queue-intelligence",
+ { method: "POST", headers: internalHeaders(env), body: JSON.stringify({}) },
+ env,
+ );
+ expect(missingQueuePullRequests.status).toBe(400);
+ await expect(missingQueuePullRequests.json()).resolves.toMatchObject({ error: "invalid_request", detail: "pullRequests array required" });
+
const invalidRepoContext = await app.request(
"/v1/internal/queue-intelligence",
{
@@ -2410,20 +2424,47 @@ describe("api routes", () => {
const extensionPayload = (await extensionContext.json()) as {
repoFullName: string;
pullNumber: number;
+ contributor: { login: string; minerStatus: string };
+ privacy: { surface: string; publicPosting: boolean; sourceUpload: boolean; githubMutations: boolean };
reviewability: { repoFullName: string; pullNumber: number };
actions: Array<{ id: string; markdown?: string; blockers?: Array<{ detail: string }> }>;
panels: Array<{ label: string }>;
+ sections: Array<{ id: string; label: string; badge?: string }>;
};
expect(extensionPayload).toMatchObject({
repoFullName: "entrius/allways-ui",
pullNumber: 12,
+ contributor: { login: "oktofeesh1", minerStatus: "confirmed" },
+ privacy: { surface: "browser_extension", publicPosting: false, sourceUpload: false, githubMutations: false },
reviewability: { repoFullName: "entrius/allways-ui", pullNumber: 12 },
actions: expect.arrayContaining([
expect.objectContaining({ id: "copy_public_safe_packet", visibility: "public_safe" }),
expect.objectContaining({ id: "view_private_blockers", visibility: "private", requiresAuth: true }),
]),
panels: expect.arrayContaining([expect.objectContaining({ label: "Reviewability" }), expect.objectContaining({ label: "Boundary" })]),
+ sections: expect.arrayContaining([
+ expect.objectContaining({ id: "miner-context", label: "Miner Context", badge: "confirmed" }),
+ expect.objectContaining({ id: "lane-fit", label: "Lane Fit" }),
+ expect.objectContaining({ id: "duplicate-risk", label: "Duplicate Risk", badge: "check overlap" }),
+ expect.objectContaining({ id: "linked-issue-state", label: "Linked Issue State", badge: "linked" }),
+ expect.objectContaining({ id: "queue-pressure", label: "Queue Pressure" }),
+ expect.objectContaining({ id: "public-safe-actions", label: "Public-Safe Packet Actions" }),
+ expect.objectContaining({ id: "boundary", label: "Boundary", badge: "private" }),
+ ]),
});
+ expect(JSON.stringify(extensionPayload)).not.toMatch(/wallet|hotkey|coldkey|raw trust|private ranking|github_pat|ghp_|payout|reward estimate|farming/i);
+
+ const nonMinerExtensionContext = await app.request(
+ "/v1/extension/pull-context?owner=entrius&repo=allways-ui&pullNumber=13",
+ { headers: { authorization: `Bearer ${extensionSessionBody.token}` } },
+ env,
+ );
+ expect(nonMinerExtensionContext.status).toBe(200);
+ await expect(nonMinerExtensionContext.json()).resolves.toMatchObject({
+ contributor: { login: "other", minerStatus: "not_found" },
+ sections: expect.arrayContaining([expect.objectContaining({ id: "miner-context", badge: "non-miner" })]),
+ });
+
const packet = extensionPayload.actions.find((action) => action.id === "copy_public_safe_packet")?.markdown ?? "";
expect(packet).toContain("# Public-safe PR packet");
expect(packet).not.toMatch(/wallet|hotkey|coldkey|reward estimate|payout|farming|raw trust score|estimated score|score estimate|private reviewability/i);
@@ -2440,8 +2481,14 @@ describe("api routes", () => {
await expect(missingPullContext.json()).resolves.toMatchObject({
repoFullName: "entrius/allways-ui",
pullNumber: 99,
+ contributor: { login: "unknown", minerStatus: "unavailable" },
actions: expect.arrayContaining([expect.objectContaining({ id: "copy_public_safe_packet" }), expect.objectContaining({ id: "view_private_blockers" })]),
panels: expect.arrayContaining([expect.objectContaining({ label: "Contributor", badge: "unknown" })]),
+ sections: expect.arrayContaining([
+ expect.objectContaining({ id: "miner-context", badge: "unavailable" }),
+ expect.objectContaining({ id: "duplicate-risk", badge: "clear" }),
+ expect.objectContaining({ id: "linked-issue-state", badge: "missing" }),
+ ]),
});
const expiringExtensionSession = await app.request("/v1/auth/extension/session", { method: "POST", headers: cookieHeaders }, env);
diff --git a/test/unit/extension-content.test.ts b/test/unit/extension-content.test.ts
new file mode 100644
index 0000000000..563cad626a
--- /dev/null
+++ b/test/unit/extension-content.test.ts
@@ -0,0 +1,82 @@
+import { readFileSync } from "node:fs";
+import { Script, createContext } from "node:vm";
+import { describe, expect, it, vi } from "vitest";
+
+const contentScript = readFileSync("apps/gittensory-extension/content.js", "utf8");
+
+describe("extension content script", () => {
+ it("matches only GitHub pull request routes", () => {
+ const internals = loadContentInternals();
+
+ expect(internals.matchPullRequestTarget("/JSONbored/gittensory/pull/146")).toEqual({
+ owner: "JSONbored",
+ repo: "gittensory",
+ pullNumber: 146,
+ });
+ expect(internals.matchPullRequestTarget("/JSONbored/gittensory/pull/146/files")).toEqual({
+ owner: "JSONbored",
+ repo: "gittensory",
+ pullNumber: 146,
+ });
+ expect(internals.matchPullRequestTarget("/JSONbored/gittensory/issues/146")).toBeNull();
+ expect(internals.matchPullRequestTarget("/JSONbored/gittensory")).toBeNull();
+ });
+
+ it("renders private pull-context sections and escapes API text", () => {
+ const internals = loadContentInternals();
+
+ const html = internals.renderPullContext({
+ sections: [
+ {
+ label: "Miner ",
+ badge: "confirmed",
+ tone: "good",
+ rows: [{ label: "author", value: "alice