mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 17:32:41 -10:00
fix(ui): refetchOnWindowFocus=always so Explorer refresh ignores staleTime
Forward-fix #2 on walk-item 8 refocus path. velocity-qa VM verify on
131bf22 showed the manual refresh button worked but synthetic + real
refocus did NOT update the Explorer sidebar after a new slice folder
was created.
Root cause: `refetchOnWindowFocus: true` gates the refetch on the
react-query staleness predicate. With `staleTime: 15_000` on
useFilesList and `staleTime: 30_000` on useSlices, a refocus inside
that window returns cached data — no refetch fires. The VM operator's
refocus tests landed within seconds of the initial fetch, well inside
the stale window.
Fix: switch both hooks to `refetchOnWindowFocus: "always"`. The
'always' variant bypasses staleness and refetches on every focus
event, which is the actual intent of the walk-item 8 contract — the
operator SHOULD see slice / file folders they just created when they
come back to the tab, even if the list was fetched moments ago.
Hooks touched:
- useFilesList (the load-bearing hook for Explorer sidebar; consumed
by ProjectTreeView via useMissionDiscovery)
- useSlices (drives the slice rail in Explorer + Project surfaces)
Regression test at packages/ui/test/use-files-list-focus-refetch.test.tsx:
- Uses react-query's focusManager.setFocused(false/true) to simulate
blur/focus deterministically in jsdom.
- Asserts a second fetch fires on focus toggle, within the staleTime
window. With the pre-fix `true` value, the test fails (one call
only); with `'always'`, the test passes (two calls).
Discrimination verified by temporarily reverting both hooks back to
`true` and re-running: both tests FAIL. Restored to `'always'`, both
tests PASS. 2/2.
Full daemon suite: 3352 passed / 1 skipped / 0 failed (one of the
banked flaky-createDaemon-timeout cases hit on first run; resolved
on clean re-run pattern; not caused by this slice).
git diff --check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
131bf22626
commit
c9578b381f
3 files changed
+138
-8
No files matched your search
@@ -73,10 +73,17 @@ export function useFilesList(root: string | null, path: string | null) {
|
||||
queryFn: () => fetchList(root!, path ?? ""),
|
||||
enabled: !!root,
|
||||
staleTime: 15_000,
|
||||
// V0.3.1 slice 17 founder-walk-workspace-state-correctness — walk item 8 (Explorer auto-show): refetch on window focus so new files
|
||||
// / folders created while the operator was off-tab appear without a
|
||||
// manual refresh click.
|
||||
refetchOnWindowFocus: true,
|
||||
// V0.3.1 slice 17 walk-item 8 (Explorer auto-show): refetch on
|
||||
// window focus so new files / folders created while the operator
|
||||
// was off-tab appear without a manual refresh click.
|
||||
//
|
||||
// Forward-fix #2: 'always' instead of `true`. With staleTime:
|
||||
// 15_000, plain `true` gates the refetch on staleness — short
|
||||
// refocus within the 15-second window observed no refetch in the
|
||||
// VM proof. 'always' refetches on every focus regardless of
|
||||
// staleness. This is the load-bearing hook for the Explorer
|
||||
// sidebar (driven via useMissionDiscovery + ProjectTreeView).
|
||||
refetchOnWindowFocus: "always",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -81,10 +81,19 @@ export function useSlices(filter: SliceFilter, boundToWorkflow: BoundToWorkflowF
|
||||
],
|
||||
queryFn: () => fetchSlicesList(filter, boundToWorkflow),
|
||||
staleTime: 30_000,
|
||||
// V0.3.1 slice 17 founder-walk-workspace-state-correctness — walk item 8 (Explorer auto-show): refetch on window focus so an operator
|
||||
// who switches away to `mkdir slices/...` and comes back sees the
|
||||
// new folder without manually clicking refresh.
|
||||
refetchOnWindowFocus: true,
|
||||
// V0.3.1 slice 17 walk-item 8 (Explorer auto-show): refetch on
|
||||
// window focus so an operator who switches away to `mkdir slices/...`
|
||||
// and comes back sees the new folder without manually clicking
|
||||
// refresh.
|
||||
//
|
||||
// Forward-fix #2 (2026-05-11 velocity-qa VM verify CONCERNING):
|
||||
// the value MUST be 'always' instead of plain `true`. With this
|
||||
// query's local staleTime: 30_000 (30 seconds), plain `true` gates
|
||||
// the refetch on the staleness predicate — short refocus tests
|
||||
// within the stale window observed no refetch. The 'always'
|
||||
// variant ignores staleness and refetches on every focus, which
|
||||
// is the actual intent: see new folders the operator JUST created.
|
||||
refetchOnWindowFocus: "always",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
// V0.3.1 slice 17 walk-item 8 forward-fix #2 — focus-refetch regression test.
|
||||
//
|
||||
// The Explorer sidebar's auto-show behavior on window focus requires
|
||||
// useFilesList + useSlices to refetch when the operator switches away
|
||||
// and back, even if the last fetch was very recent. With staleTime
|
||||
// configured (15s for files, 30s for slices) AND
|
||||
// refetchOnWindowFocus: true (the slice 17 initial impl), react-query
|
||||
// gates the refetch on staleness — a refocus within the stale window
|
||||
// produces no refetch, which is exactly what velocity-qa observed in
|
||||
// the VM verify proof.
|
||||
//
|
||||
// Fix: `refetchOnWindowFocus: 'always'` bypasses staleness. This test
|
||||
// asserts the focus-triggered refetch fires regardless of staleness.
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { renderHook, waitFor, cleanup } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider, focusManager } from "@tanstack/react-query";
|
||||
import { createElement, type ReactNode } from "react";
|
||||
import { useFilesList } from "../src/hooks/useFiles.js";
|
||||
import { useSlices } from "../src/hooks/useSlices.js";
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
let fetchSpy: ReturnType<typeof vi.fn>;
|
||||
|
||||
function makeWrapper() {
|
||||
// Use the same staleTime defaults the production QueryClient ships
|
||||
// with so the test discriminates against the same gating predicate
|
||||
// production hits.
|
||||
const client = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false, staleTime: 5_000 } },
|
||||
});
|
||||
return ({ children }: { children: ReactNode }) =>
|
||||
createElement(QueryClientProvider, { client }, children);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
fetchSpy = vi.fn();
|
||||
globalThis.fetch = fetchSpy as unknown as typeof fetch;
|
||||
// Start focused so the initial query mount fires normally.
|
||||
focusManager.setFocused(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
globalThis.fetch = originalFetch;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("useFilesList focus-refetch (walk-item 8 forward-fix #2)", () => {
|
||||
it("refetches on focus regardless of staleTime (refetchOnWindowFocus: 'always')", async () => {
|
||||
fetchSpy.mockImplementation(async () =>
|
||||
new Response(JSON.stringify({
|
||||
root: "workspace",
|
||||
path: "missions",
|
||||
entries: [],
|
||||
}), { status: 200, headers: { "Content-Type": "application/json" } }),
|
||||
);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useFilesList("workspace", "missions"),
|
||||
{ wrapper: makeWrapper() },
|
||||
);
|
||||
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
||||
const initialCallCount = fetchSpy.mock.calls.filter(
|
||||
(c) => String(c[0]).startsWith("/api/files/list"),
|
||||
).length;
|
||||
expect(initialCallCount).toBe(1);
|
||||
|
||||
// Simulate tab blur + focus WITHIN the staleTime window. With
|
||||
// refetchOnWindowFocus: true (pre-fix), this would be no-op.
|
||||
// With 'always' (post-fix), a second fetch fires.
|
||||
focusManager.setFocused(false);
|
||||
focusManager.setFocused(true);
|
||||
|
||||
await waitFor(() => {
|
||||
const calls = fetchSpy.mock.calls.filter(
|
||||
(c) => String(c[0]).startsWith("/api/files/list"),
|
||||
);
|
||||
expect(calls.length).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("useSlices focus-refetch (walk-item 8 forward-fix #2)", () => {
|
||||
it("refetches on focus regardless of staleTime (refetchOnWindowFocus: 'always')", async () => {
|
||||
fetchSpy.mockImplementation(async () =>
|
||||
new Response(JSON.stringify({
|
||||
slices: [],
|
||||
totalCount: 0,
|
||||
filter: "all",
|
||||
}), { status: 200, headers: { "Content-Type": "application/json" } }),
|
||||
);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useSlices("all"),
|
||||
{ wrapper: makeWrapper() },
|
||||
);
|
||||
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
||||
const initialCallCount = fetchSpy.mock.calls.filter(
|
||||
(c) => String(c[0]).startsWith("/api/slices"),
|
||||
).length;
|
||||
expect(initialCallCount).toBe(1);
|
||||
|
||||
focusManager.setFocused(false);
|
||||
focusManager.setFocused(true);
|
||||
|
||||
await waitFor(() => {
|
||||
const calls = fetchSpy.mock.calls.filter(
|
||||
(c) => String(c[0]).startsWith("/api/slices"),
|
||||
);
|
||||
expect(calls.length).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user