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
9 changes: 8 additions & 1 deletion src/signals/focus-manifest-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import type { LocalManifestLoadResult } from "../selfhost/private-config";
export const REPO_FOCUS_MANIFEST_SIGNAL = "repo-focus-manifest";
export const REPO_PUBLIC_FOCUS_MANIFEST_SIGNAL = "repo-public-focus-manifest";
export const REPO_FOCUS_MANIFEST_MAX_AGE_MS = 6 * 60 * 60 * 1000;
// Per-request ceiling for each raw-content candidate fetch (#7071), matching the bounded-fetch convention in
// src/review/** (e.g. alerts.ts's AbortSignal.timeout(10_000)) so a slow github.com/ghraw response
// can't stall manifest resolution on the cold-cache path.
const MANIFEST_FETCH_TIMEOUT_MS = 10_000;
export const REPO_FOCUS_MANIFEST_MAX_CONCURRENT_LOADS = 4;

/**
Expand Down Expand Up @@ -101,7 +105,10 @@ export async function fetchRepoFocusManifestFile(repoFullName: string): Promise<
for (const path of MANIFEST_FILE_CANDIDATES) {
const url = `https://github.com/ghraw/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/HEAD/${path}`;
try {
const response = await fetch(url, { headers: { Accept: "application/json", "User-Agent": "loopover" } });
const response = await fetch(url, {
headers: { Accept: "application/json", "User-Agent": "loopover" },
signal: AbortSignal.timeout(MANIFEST_FETCH_TIMEOUT_MS),
});
if (response.ok) {
const text = await readBoundedResponseText(response);
if (text !== null) return text;
Expand Down
13 changes: 13 additions & 0 deletions test/unit/focus-manifest-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,19 @@ describe("focus-manifest loader", () => {
expect(fetchSpy).toHaveBeenCalledTimes(1); // first candidate in MANIFEST_FILE_CANDIDATES is a 200, no fallback needed
});

it("bounds each candidate fetch with an AbortSignal timeout (#7071)", async () => {
// A slow github.com/ghraw response can't stall manifest resolution: every candidate fetch must
// carry a time bound, matching the bounded-fetch convention elsewhere in src/review/**.
const fetchSpy = vi
.spyOn(globalThis, "fetch")
.mockImplementation(async () => new Response("not found", { status: 404 }));
await fetchRepoFocusManifestFile("owner/repo");
expect(fetchSpy).toHaveBeenCalled();
for (const call of fetchSpy.mock.calls) {
expect((call[1] as RequestInit | undefined)?.signal).toBeInstanceOf(AbortSignal);
}
});

it("does not read public manifest responses when Content-Length is too large", async () => {
const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(async (url) => {
const stringUrl = String(url);
Expand Down