commit f953ea526b412323648cd36109ba6b93545da69f
parent 7daa86515342c77a155a31d73011626fd700eaee
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Mon, 28 Sep 2026 03:06:38 -0400
hub: /ask waits for a live-chat refetch in flight; the front page stays ready through it (review low 1) — subsSettled = data || (errored && (progressive || !isFetching)); e2e: /ask after a front-page live-chat failure is not ready while its own mount refetches it
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
2 files changed, 43 insertions(+), 1 deletion(-)
diff --git a/common/components/SearchDataContext.tsx b/common/components/SearchDataContext.tsx
@@ -447,11 +447,21 @@ export function MultiSiteDataProvider({
// refetches, so `isError` would take the archive (and its videos) out of
// the search for the length of a live-chat Retry. With the count, the
// archive stays ready through it, still noted as missing its live chat.
+ //
+ // That holds for the PROGRESSIVE front page only. The count lives in the
+ // shared query client, so it also survives into a later refetch the front
+ // page never asked for: /ask's own mount (an errored query with no data is
+ // refetched when a new observer mounts), a scope toggle off and on, a
+ // whole-archive Retry. /ask grounds in what it was given, so it waits for
+ // such a refetch to settle rather than answer without live chat that is
+ // about to arrive.
const posts = postsQueries[i];
const postsSettled = !!posts && (posts.isSuccess || posts.isError);
const subs = subsQueries[i];
const subsErrored = !!subs && subs.data === undefined && subs.errorUpdateCount > 0;
- const subsSettled = !!subs && (subs.data !== undefined || subsErrored);
+ const subsSettled =
+ !!subs &&
+ (subs.data !== undefined || (subsErrored && (progressive || !subs.isFetching)));
if (
m?.isSuccess &&
pages.length === m.data.pageCount &&
diff --git a/export/e2e-hub/federated-search.spec.ts b/export/e2e-hub/federated-search.spec.ts
@@ -582,6 +582,38 @@ test.describe("hub federated search — scope, per-archive state, attribution",
expect(mocks.b.requests.slice(asked)).toEqual(["/subs/manifest.json"]);
});
+ test("/ask is not ready while it refetches a live chat that failed on the front page", async ({
+ page,
+ }) => {
+ // Origin B's live chat failed on the front page: its archive is ready
+ // there, noted as missing its live chat.
+ const mocks = await setup(page);
+ mocks.b.subs = "error";
+ await page.goto("/");
+ await expect(chip(page, B).getByTestId("hub-scope-live-chat")).toHaveText(
+ LIVE_CHAT_MISSING,
+ );
+
+ // To /ask in the same page life (the same query client): its own mount
+ // asks for the failed live chat again. Held, /ask is not ready — a question
+ // now would miss live chat that may be about to arrive.
+ mocks.b.subs = "hold";
+ await page
+ .getByRole("banner")
+ .getByRole("link", { name: "Ask AI", exact: true })
+ .click();
+ await page.waitForURL(/\/ask\/?$/);
+ await expect.poll(() => mocks.b.heldSubs.length).toBe(1);
+ await page.waitForTimeout(1_000);
+ await expect(page.getByPlaceholder("Loading transcripts…")).toBeVisible();
+
+ // It answers: /ask is ready, over both archives and B's live chat.
+ for (const r of mocks.b.heldSubs.splice(0)) await fulfillJson(r, SUBS_OK);
+ await expect(
+ page.getByPlaceholder(/^Ask about the transcripts/),
+ ).toBeVisible();
+ });
+
test("/ask with every archive switched off says so and cannot ask; with one back on, it asks", async ({
page,
}) => {