Skip to content
Closed
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
@@ -1,5 +1,5 @@
import { CheckCircle2 } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";

import { StatusPill, type Status } from "@/components/site/control-primitives";
import { TableScroll } from "@/components/site/data-table";
Expand Down Expand Up @@ -61,13 +61,17 @@ export function ActivationPreview({ reviewability }: { reviewability: Array<{ pr
const [preview, setPreview] = useState<ActivationPreviewResponse | null>(null);
const [loading, setLoading] = useState(false);
const [loadError, setLoadError] = useState<string | null>(null);
// Stale-response generation (#7784): ignore out-of-order resolutions when the free-text repo picker races.
const requestGenerationRef = useRef(0);

const base = repoApiBase(repoFullName);
const hasRepos = repoOptions.length > 0;

const load = useCallback(async () => {
const generation = ++requestGenerationRef.current;
const apiBase = repoApiBase(repoFullName);
if (!apiBase) {
if (generation !== requestGenerationRef.current) return;
setPreview(null);
setLoadError(null);
return;
Expand All @@ -79,6 +83,7 @@ export function ActivationPreview({ reviewability }: { reviewability: Array<{ pr
credentials: "include",
silentStatus: true,
});
if (generation !== requestGenerationRef.current) return;
if (result.ok) {
setPreview(result.data);
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { KeyRound, Loader2, Save, Trash2 } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { StatusPill } from "@/components/site/control-primitives";
import { apiFetch } from "@/lib/api/request";
import { getApiOrigin } from "@/lib/api/origin";
Expand Down Expand Up @@ -57,11 +57,14 @@ export function AiReviewSettings({ reviewability }: { reviewability: Array<{ pr:
const [busy, setBusy] = useState(false);
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState<Message | null>(null);
// Stale-response generation (#7784): ignore out-of-order resolutions when the free-text repo picker races.
const requestGenerationRef = useRef(0);

const base = repoApiBase(repoFullName);
const hasRepos = repoOptions.length > 0;

const load = useCallback(async () => {
const generation = ++requestGenerationRef.current;
const apiBase = repoApiBase(repoFullName);
if (!apiBase) return;
setMessage(null);
Expand All @@ -78,6 +81,7 @@ export function AiReviewSettings({ reviewability }: { reviewability: Array<{ pr:
silentStatus: true,
}),
]);
if (generation !== requestGenerationRef.current) return;
if (settings.ok) {
setMode(settings.data.aiReviewMode ?? "off");
setByok(settings.data.aiReviewByok ?? false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,41 @@
});
expect(screen.getByText(/This view is unavailable for this repository\./i)).toBeTruthy();
});

it("ignores a stale response after the free-text repo picker races (#7784)", async () => {
let resolveFirst!: (value: { ok: true; data: typeof POPULATED_COMPARISON }) => void;
const first = new Promise<{ ok: true; data: typeof POPULATED_COMPARISON }>((resolve) => {
resolveFirst = resolve;
});
const secondComparison = {
...POPULATED_COMPARISON,
windowDays: 30,
checkedSubmitterCount: 2,
amsCohort: { ...POPULATED_COMPARISON.amsCohort, submitterCount: 9 },
};
apiFetch.mockImplementation((url: string) => {
if (String(url).includes("/acme/widgets/")) return first;
return Promise.resolve({ ok: true, data: secondComparison });
});

render(
<AmsMinerCohortCard

Check failure on line 160 in apps/loopover-ui/src/components/site/app-panels/ams-miner-cohort-card.test.tsx

View workflow job for this annotation

GitHub Actions / validate-code

Replace `⏎········reviewability={[{·pr:·"acme/widgets#1"·},·{·pr:·"other/repo#2"·}]}⏎·····` with `·reviewability={[{·pr:·"acme/widgets#1"·},·{·pr:·"other/repo#2"·}]}`
reviewability={[{ pr: "acme/widgets#1" }, { pr: "other/repo#2" }]}
/>,
);
// Initial load for acme/widgets is in flight (deferred). Type a different repo so a newer request starts.
fireEvent.change(screen.getByPlaceholderText("owner/repo"), {
target: { value: "other/repo" },
});
await waitFor(() => expect(screen.getByText(/Window: 30 days · checked 2 of/i)).toBeTruthy());

// Stale first response arrives late — must not overwrite the newer repo's state.
resolveFirst({ ok: true, data: POPULATED_COMPARISON });
await waitFor(() => expect(apiFetch).toHaveBeenCalledWith(

Check failure on line 172 in apps/loopover-ui/src/components/site/app-panels/ams-miner-cohort-card.test.tsx

View workflow job for this annotation

GitHub Actions / validate-code

Insert `⏎·····`
expect.stringContaining("/v1/repos/other/repo/ams-miner-cohort"),

Check failure on line 173 in apps/loopover-ui/src/components/site/app-panels/ams-miner-cohort-card.test.tsx

View workflow job for this annotation

GitHub Actions / validate-code

Insert `··`
expect.anything(),

Check failure on line 174 in apps/loopover-ui/src/components/site/app-panels/ams-miner-cohort-card.test.tsx

View workflow job for this annotation

GitHub Actions / validate-code

Replace `······` with `········`
));

Check failure on line 175 in apps/loopover-ui/src/components/site/app-panels/ams-miner-cohort-card.test.tsx

View workflow job for this annotation

GitHub Actions / validate-code

Replace `)` with `··),⏎····`
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
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";

import { StateBoundary } from "@/components/site/state-views";
import { apiFetch } from "@/lib/api/request";
Expand Down Expand Up @@ -94,13 +94,18 @@ export function AmsMinerCohortCard({ reviewability }: { reviewability: Array<{ p
const [comparison, setComparison] = useState<AmsMinerCohortComparison | null>(null);
const [loading, setLoading] = useState(false);
const [loadError, setLoadError] = useState<string | null>(null);
// Stale-response generation (#7784): every keystroke that changes repoFullName bumps this; a response
// whose generation no longer matches is ignored so an earlier in-flight fetch can't overwrite later state.
const requestGenerationRef = useRef(0);

const base = repoApiBase(repoFullName);
const hasRepos = repoOptions.length > 0;

const load = useCallback(async () => {
const generation = ++requestGenerationRef.current;
const apiBase = repoApiBase(repoFullName);
if (!apiBase) {
if (generation !== requestGenerationRef.current) return;
setComparison(null);
setLoadError(null);
return;
Expand All @@ -112,6 +117,7 @@ export function AmsMinerCohortCard({ reviewability }: { reviewability: Array<{ p
credentials: "include",
silentStatus: true,
});
if (generation !== requestGenerationRef.current) return;
if (result.ok) {
setComparison(result.data);
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FileCog, Loader2, Save } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";

import { apiFetch } from "@/lib/api/request";
import { getApiOrigin } from "@/lib/api/origin";
Expand Down Expand Up @@ -141,11 +141,14 @@ export function MaintainerSettings({ reviewability }: { reviewability: Array<{ p
const [loading, setLoading] = useState(false);
const [busy, setBusy] = useState(false);
const [message, setMessage] = useState<Message | null>(null);
// Stale-response generation (#7784): ignore out-of-order resolutions when the free-text repo picker races.
const requestGenerationRef = useRef(0);

const base = repoApiBase(repoFullName);
const hasRepos = repoOptions.length > 0;

const load = useCallback(async () => {
const generation = ++requestGenerationRef.current;
const apiBase = repoApiBase(repoFullName);
if (!apiBase) return;
setMessage(null);
Expand All @@ -155,6 +158,7 @@ export function MaintainerSettings({ reviewability }: { reviewability: Array<{ p
credentials: "include",
silentStatus: true,
});
if (generation !== requestGenerationRef.current) return;
// Default the agent-layer fields defensively so the editor renders even against an older response shape.
setSettings(
result.ok
Expand Down Expand Up @@ -519,8 +523,11 @@ function FocusManifestEditor({ base }: { base: string | null }) {
const [loading, setLoading] = useState(false);
const [busy, setBusy] = useState(false);
const [message, setMessage] = useState<Message | null>(null);
// Stale-response generation (#7784): ignore out-of-order resolutions when `base` changes mid-flight.
const requestGenerationRef = useRef(0);

const load = useCallback(async () => {
const generation = ++requestGenerationRef.current;
if (!base) return;
setLoading(true);
setMessage(null);
Expand All @@ -529,6 +536,7 @@ function FocusManifestEditor({ base }: { base: string | null }) {
credentials: "include",
silentStatus: true,
});
if (generation !== requestGenerationRef.current) return;
setText(result.ok ? JSON.stringify(result.data.manifest, null, 2) : "");
setLoading(false);
}, [base]);
Expand Down
Loading