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
44 changes: 38 additions & 6 deletions packages/gittensory-miner/lib/rejection-signal.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { resolveAiPolicyVerdict } from "@jsonbored/gittensory-engine";
// not assume a false result here means "no rejection signal of any kind."

const DEFAULT_RAW_CONTENT_BASE_URL = "https://raw.githubusercontent.com";
const MAX_POLICY_DOC_BYTES = 128 * 1024;

function parseRepoFullName(repoFullName) {
if (typeof repoFullName !== "string") return null;
Expand All @@ -33,13 +34,46 @@ function normalizeOptions(options = {}) {
};
}

async function readBoundedPolicyDoc(response) {
const contentLength = response.headers?.get?.("content-length");
if (contentLength !== undefined && contentLength !== null) {
const parsedLength = Number.parseInt(contentLength, 10);
if (Number.isFinite(parsedLength) && parsedLength > MAX_POLICY_DOC_BYTES) return null;
}

if (!response.body?.getReader) {
const text = await response.text();
return typeof text === "string" && Buffer.byteLength(text, "utf8") <= MAX_POLICY_DOC_BYTES ? text : null;
}

const reader = response.body.getReader();
const decoder = new TextDecoder();
let totalBytes = 0;
let text = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
totalBytes += value.byteLength;
if (totalBytes > MAX_POLICY_DOC_BYTES) {
await reader.cancel();
return null;
}
text += decoder.decode(value, { stream: true });
}
text += decoder.decode();
return text;
} finally {
reader.releaseLock();
}
}

async function fetchPolicyDoc(target, path, resolved) {
const url = `${resolved.rawContentBaseUrl}/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.repo)}/HEAD/${path}`;
try {
const response = await resolved.fetchImpl(url, { method: "GET", headers: { accept: "application/json", "user-agent": "loopover-miner" } });
if (!response.ok) return null;
const text = await response.text();
return typeof text === "string" ? text : null;
return await readBoundedPolicyDoc(response);
} catch {
return null;
}
Expand All @@ -59,10 +93,8 @@ export async function resolveRejectionSignaled(repoFullName, options = {}) {
if (!target) return false;
const resolved = normalizeOptions(options);

const [aiUsage, contributing] = await Promise.all([
fetchPolicyDoc(target, "AI-USAGE.md", resolved),
fetchPolicyDoc(target, "CONTRIBUTING.md", resolved),
]);
const aiUsage = await fetchPolicyDoc(target, "AI-USAGE.md", resolved);
const contributing = aiUsage && aiUsage.trim() ? null : await fetchPolicyDoc(target, "CONTRIBUTING.md", resolved);

const verdict = resolveAiPolicyVerdict({ aiUsage, contributing });
return !verdict.allowed;
Expand Down
138 changes: 138 additions & 0 deletions test/unit/miner-rejection-signal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,144 @@ describe("resolveRejectionSignaled (#5132)", () => {
expect(result).toBe(true);
});

it("does not fetch CONTRIBUTING.md when a non-empty AI-USAGE.md decides the policy", async () => {
const fetchImpl = vi.fn(async (url: string) => {
if (url.includes("AI-USAGE.md")) return textResponse("No AI-generated pull requests, please.");
return textResponse("Do not download me");
});

const result = await resolveRejectionSignaled("acme/widgets", { fetchImpl });

expect(result).toBe(true);
expect(fetchImpl).toHaveBeenCalledTimes(1);
expect(fetchImpl.mock.calls[0]?.[0]).toContain("AI-USAGE.md");
});

it("treats an oversized policy document as absent without reading its body", async () => {
const text = vi.fn(async () => "No AI-generated pull requests, please.");
const fetchImpl = routedFetch({
"AI-USAGE.md": () => ({
ok: true,
status: 200,
headers: new Headers({ "content-length": String(129 * 1024) }),
json: async (): Promise<unknown> => {
throw new Error("json() is unused by resolveRejectionSignaled");
},
text,
}),
"CONTRIBUTING.md": () => textResponse("Welcome, contributors!"),
});

const result = await resolveRejectionSignaled("acme/widgets", { fetchImpl });

expect(result).toBe(false);
expect(text).not.toHaveBeenCalled();
});

it("ignores a non-numeric content-length header and falls through to reading the body", async () => {
const fetchImpl = routedFetch({
"AI-USAGE.md": () => ({
ok: true,
status: 200,
headers: new Headers({ "content-length": "not-a-number" }),
json: async (): Promise<unknown> => {
throw new Error("json() is unused by resolveRejectionSignaled");
},
text: async () => "No AI-generated pull requests, please.",
}),
"CONTRIBUTING.md": () => textResponse("Welcome, contributors!"),
});

const result = await resolveRejectionSignaled("acme/widgets", { fetchImpl });

expect(result).toBe(true);
});

it("treats an oversized non-streamed policy document as absent", async () => {
const oversizedText = "a".repeat(129 * 1024);
const fetchImpl = routedFetch({
"AI-USAGE.md": () => ({
ok: true,
status: 200,
headers: new Headers(),
json: async (): Promise<unknown> => {
throw new Error("json() is unused by resolveRejectionSignaled");
},
text: async () => oversizedText,
}),
"CONTRIBUTING.md": () => textResponse("Do not submit AI-generated code."),
});

const result = await resolveRejectionSignaled("acme/widgets", { fetchImpl });

// AI-USAGE.md is treated as absent (oversized), so the verdict falls through to CONTRIBUTING.md's ban.
expect(result).toBe(true);
});

it("cancels a streamed policy document once it exceeds the byte limit", async () => {
let canceled = false;
const chunk = new Uint8Array(65 * 1024);
const stream = new ReadableStream({
start(controller) {
controller.enqueue(chunk);
controller.enqueue(chunk);
},
cancel() {
canceled = true;
},
});
const fetchImpl = routedFetch({
"AI-USAGE.md": () => ({
ok: true,
status: 200,
headers: new Headers(),
body: stream,
json: async (): Promise<unknown> => {
throw new Error("json() is unused by resolveRejectionSignaled");
},
text: async () => {
throw new Error("streaming responses should not call text()");
},
}),
"CONTRIBUTING.md": () => textResponse("Welcome, contributors!"),
});

const result = await resolveRejectionSignaled("acme/widgets", { fetchImpl });

expect(result).toBe(false);
expect(canceled).toBe(true);
});

it("reads a streamed policy document to completion when it stays within the byte limit", async () => {
const encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode("No AI-generated "));
controller.enqueue(encoder.encode("pull requests, please."));
controller.close();
},
});
const fetchImpl = routedFetch({
"AI-USAGE.md": () => ({
ok: true,
status: 200,
headers: new Headers(),
body: stream,
json: async (): Promise<unknown> => {
throw new Error("json() is unused by resolveRejectionSignaled");
},
text: async () => {
throw new Error("streaming responses should not call text()");
},
}),
"CONTRIBUTING.md": () => textResponse("Welcome, contributors!"),
});

const result = await resolveRejectionSignaled("acme/widgets", { fetchImpl });

expect(result).toBe(true);
});

it("fails open to false when both docs 404", async () => {
const fetchImpl = routedFetch({});
const result = await resolveRejectionSignaled("acme/widgets", { fetchImpl });
Expand Down