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
Original file line number Diff line number Diff line change
Expand Up @@ -65,31 +65,41 @@ export function ActivationPreview({ reviewability }: { reviewability: Array<{ pr
const base = repoApiBase(repoFullName);
const hasRepos = repoOptions.length > 0;

const load = useCallback(async () => {
const apiBase = repoApiBase(repoFullName);
if (!apiBase) {
setPreview(null);
const load = useCallback(
async (opts?: { cancelled?: () => boolean }) => {
const isCancelled = opts?.cancelled ?? (() => false);
const apiBase = repoApiBase(repoFullName);
if (!apiBase) {
setPreview(null);
setLoadError(null);
return;
}
setLoadError(null);
return;
}
setLoadError(null);
setLoading(true);
const result = await apiFetch<ActivationPreviewResponse>(`${apiBase}/activation-preview`, {
label: "Activation preview",
credentials: "include",
silentStatus: true,
});
if (result.ok) {
setPreview(result.data);
} else {
setPreview(null);
setLoadError(result.message);
}
setLoading(false);
}, [repoFullName]);
setLoading(true);
const result = await apiFetch<ActivationPreviewResponse>(`${apiBase}/activation-preview`, {
label: "Activation preview",
credentials: "include",
silentStatus: true,
});
// Ignore responses after a newer repoFullName keyed a fresh load (#7784).
if (isCancelled()) return;
if (result.ok) {
setPreview(result.data);
} else {
setPreview(null);
setLoadError(result.message);
}
setLoading(false);
},
[repoFullName],
);

useEffect(() => {
void load();
let cancelled = false;
void load({ cancelled: () => cancelled });
return () => {
cancelled = true;
};
}, [load]);

return (
Expand Down Expand Up @@ -148,8 +158,8 @@ export function ActivationPreview({ reviewability }: { reviewability: Array<{ pr
isLoading={Boolean(base) && loading}
isError={Boolean(base) && !loading && loadError !== null}
isEmpty={Boolean(base) && !loading && preview !== null && preview.evaluatedCount === 0}
onRetry={load}
onRefresh={load}
onRetry={() => void load()}
onRefresh={() => void load()}
loadingTitle="Building activation preview…"
errorTitle="Couldn't load the activation preview"
errorDescription={loadError ?? undefined}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,35 +61,45 @@ export function AiReviewSettings({ reviewability }: { reviewability: Array<{ pr:
const base = repoApiBase(repoFullName);
const hasRepos = repoOptions.length > 0;

const load = useCallback(async () => {
const apiBase = repoApiBase(repoFullName);
if (!apiBase) return;
setMessage(null);
setLoading(true);
const [settings, key] = await Promise.all([
apiFetch<RepoSettingsResponse>(`${apiBase}/settings`, {
label: "AI review settings",
credentials: "include",
silentStatus: true,
}),
apiFetch<AiKeyStatus>(`${apiBase}/ai-key`, {
label: "AI key status",
credentials: "include",
silentStatus: true,
}),
]);
if (settings.ok) {
setMode(settings.data.aiReviewMode ?? "off");
setByok(settings.data.aiReviewByok ?? false);
setProvider(settings.data.aiReviewProvider ?? "anthropic");
setModel(settings.data.aiReviewModel ?? "");
}
setKeyStatus(key.ok ? key.data : null);
setLoading(false);
}, [repoFullName]);
const load = useCallback(
async (opts?: { cancelled?: () => boolean }) => {
const isCancelled = opts?.cancelled ?? (() => false);
const apiBase = repoApiBase(repoFullName);
if (!apiBase) return;
setMessage(null);
setLoading(true);
const [settings, key] = await Promise.all([
apiFetch<RepoSettingsResponse>(`${apiBase}/settings`, {
label: "AI review settings",
credentials: "include",
silentStatus: true,
}),
apiFetch<AiKeyStatus>(`${apiBase}/ai-key`, {
label: "AI key status",
credentials: "include",
silentStatus: true,
}),
]);
// Ignore responses after a newer repoFullName keyed a fresh load (#7784).
if (isCancelled()) return;
if (settings.ok) {
setMode(settings.data.aiReviewMode ?? "off");
setByok(settings.data.aiReviewByok ?? false);
setProvider(settings.data.aiReviewProvider ?? "anthropic");
setModel(settings.data.aiReviewModel ?? "");
}
setKeyStatus(key.ok ? key.data : null);
setLoading(false);
},
[repoFullName],
);

useEffect(() => {
void load();
let cancelled = false;
void load({ cancelled: () => cancelled });
return () => {
cancelled = true;
};
}, [load]);

async function saveKey() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,38 @@ describe("AmsMinerCohortCard", () => {
});
expect(screen.getByText(/This view is unavailable for this repository\./i)).toBeTruthy();
});

it("drops a superseded response when repoFullName changes before the first fetch resolves (#7784)", async () => {
let resolveStale!: (value: unknown) => void;
let resolveFresh!: (value: unknown) => void;
apiFetch
.mockImplementationOnce(() => new Promise((resolve) => (resolveStale = resolve)))
.mockImplementationOnce(() => new Promise((resolve) => (resolveFresh = resolve)));

render(<AmsMinerCohortCard reviewability={REVIEWABILITY} />);
expect(screen.getByText(/Loading AMS contributor mix/i)).toBeTruthy();

// Keystroke races a second request while the first is still in flight.
fireEvent.change(screen.getByPlaceholderText("owner/repo"), {
target: { value: "acme/other" },
});
await waitFor(() => expect(apiFetch).toHaveBeenCalledTimes(2));

const freshComparison = {
...POPULATED_COMPARISON,
windowDays: 30,
totalSubmitterCount: 2,
checkedSubmitterCount: 2,
};
// Newer request resolves first and must win.
resolveFresh({ ok: true, data: freshComparison });
await waitFor(() => expect(screen.getByText(/Window: 30 days · checked 2 of/i)).toBeTruthy());

// Stale request resolves last — must not overwrite the fresh window.
resolveStale({ ok: true, data: POPULATED_COMPARISON });
await Promise.resolve();
await Promise.resolve();
expect(screen.getByText(/Window: 30 days · checked 2 of/i)).toBeTruthy();
expect(screen.queryByText(/Window: 90 days · checked 5 of/i)).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -98,31 +98,41 @@ export function AmsMinerCohortCard({ reviewability }: { reviewability: Array<{ p
const base = repoApiBase(repoFullName);
const hasRepos = repoOptions.length > 0;

const load = useCallback(async () => {
const apiBase = repoApiBase(repoFullName);
if (!apiBase) {
setComparison(null);
const load = useCallback(
async (opts?: { cancelled?: () => boolean }) => {
const isCancelled = opts?.cancelled ?? (() => false);
const apiBase = repoApiBase(repoFullName);
if (!apiBase) {
setComparison(null);
setLoadError(null);
return;
}
setLoadError(null);
return;
}
setLoadError(null);
setLoading(true);
const result = await apiFetch<AmsMinerCohortComparison>(`${apiBase}/ams-miner-cohort`, {
label: "AMS miner cohort comparison",
credentials: "include",
silentStatus: true,
});
if (result.ok) {
setComparison(result.data);
} else {
setComparison(null);
setLoadError(result.message);
}
setLoading(false);
}, [repoFullName]);
setLoading(true);
const result = await apiFetch<AmsMinerCohortComparison>(`${apiBase}/ams-miner-cohort`, {
label: "AMS miner cohort comparison",
credentials: "include",
silentStatus: true,
});
// Ignore responses after a newer repoFullName keyed a fresh load (#7784).
if (isCancelled()) return;
if (result.ok) {
setComparison(result.data);
} else {
setComparison(null);
setLoadError(result.message);
}
setLoading(false);
},
[repoFullName],
);

useEffect(() => {
void load();
let cancelled = false;
void load({ cancelled: () => cancelled });
return () => {
cancelled = true;
};
}, [load]);

return (
Expand Down Expand Up @@ -171,8 +181,8 @@ export function AmsMinerCohortCard({ reviewability }: { reviewability: Array<{ p
isLoading={Boolean(base) && loading}
isError={Boolean(base) && !loading && loadError !== null}
isEmpty={Boolean(base) && !loading && comparison !== null && !comparison.present}
onRetry={load}
onRefresh={load}
onRetry={() => void load()}
onRefresh={() => void load()}
loadingTitle="Loading AMS contributor mix…"
errorTitle="Couldn't load the AMS contributor mix"
errorDescription={loadError ?? undefined}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,32 +145,42 @@ export function MaintainerSettings({ reviewability }: { reviewability: Array<{ p
const base = repoApiBase(repoFullName);
const hasRepos = repoOptions.length > 0;

const load = useCallback(async () => {
const apiBase = repoApiBase(repoFullName);
if (!apiBase) return;
setMessage(null);
setLoading(true);
const result = await apiFetch<MaintainerSettings>(`${apiBase}/settings`, {
label: "Repository settings",
credentials: "include",
silentStatus: true,
});
// Default the agent-layer fields defensively so the editor renders even against an older response shape.
setSettings(
result.ok
? {
...result.data,
autonomy: result.data.autonomy ?? {},
agentPaused: result.data.agentPaused ?? false,
agentDryRun: result.data.agentDryRun ?? false,
}
: null,
);
setLoading(false);
}, [repoFullName]);
const load = useCallback(
async (opts?: { cancelled?: () => boolean }) => {
const isCancelled = opts?.cancelled ?? (() => false);
const apiBase = repoApiBase(repoFullName);
if (!apiBase) return;
setMessage(null);
setLoading(true);
const result = await apiFetch<MaintainerSettings>(`${apiBase}/settings`, {
label: "Repository settings",
credentials: "include",
silentStatus: true,
});
// Ignore responses after a newer repoFullName keyed a fresh load (#7784).
if (isCancelled()) return;
// Default the agent-layer fields defensively so the editor renders even against an older response shape.
setSettings(
result.ok
? {
...result.data,
autonomy: result.data.autonomy ?? {},
agentPaused: result.data.agentPaused ?? false,
agentDryRun: result.data.agentDryRun ?? false,
}
: null,
);
setLoading(false);
},
[repoFullName],
);

useEffect(() => {
void load();
let cancelled = false;
void load({ cancelled: () => cancelled });
return () => {
cancelled = true;
};
}, [load]);

function setField<K extends keyof MaintainerSettings>(key: K, value: MaintainerSettings[K]) {
Expand Down Expand Up @@ -520,21 +530,31 @@ function FocusManifestEditor({ base }: { base: string | null }) {
const [busy, setBusy] = useState(false);
const [message, setMessage] = useState<Message | null>(null);

const load = useCallback(async () => {
if (!base) return;
setLoading(true);
setMessage(null);
const result = await apiFetch<FocusManifestResponse>(`${base}/focus-manifest`, {
label: "Focus manifest",
credentials: "include",
silentStatus: true,
});
setText(result.ok ? JSON.stringify(result.data.manifest, null, 2) : "");
setLoading(false);
}, [base]);
const load = useCallback(
async (opts?: { cancelled?: () => boolean }) => {
const isCancelled = opts?.cancelled ?? (() => false);
if (!base) return;
setLoading(true);
setMessage(null);
const result = await apiFetch<FocusManifestResponse>(`${base}/focus-manifest`, {
label: "Focus manifest",
credentials: "include",
silentStatus: true,
});
// Ignore responses after a newer base keyed a fresh load (#7784).
if (isCancelled()) return;
setText(result.ok ? JSON.stringify(result.data.manifest, null, 2) : "");
setLoading(false);
},
[base],
);

useEffect(() => {
void load();
let cancelled = false;
void load({ cancelled: () => cancelled });
return () => {
cancelled = true;
};
}, [load]);

async function save() {
Expand Down
Loading