import {
EnvironmentId,
type ProjectListEntriesResult,
type ProjectReadFileResult,
} from "@t3tools/contracts";
import * as Effect from "effect/Effect";
import { Atom, AtomRegistry } from "effect/unstable/reactivity";
import { beforeEach, describe, expect, it, vi } from "vite-plus/test";
const projectMocks = vi.hoisted(() => ({
listEntries: vi.fn(),
optimisticFile: vi.fn(),
readFile: vi.fn(),
}));
const atomHooks = vi.hoisted(() => ({
registry: null as {
get(atom: object): unknown;
refresh(atom: object): void;
} | null,
}));
const reactHooks = vi.hoisted(() => {
let cursor = 0;
let refs: Array<{ current: unknown }> = [];
const nextIndex = () => cursor++;
return {
beginRender() {
cursor = 0;
},
reset() {
cursor = 0;
refs = [];
},
useCallback(callback: A): A {
nextIndex();
return callback;
},
useEffect(effect: () => void): void {
nextIndex();
effect();
},
useRef(initialValue: A): { current: A } {
const index = nextIndex();
refs[index] ??= { current: initialValue };
return refs[index] as { current: A };
},
};
});
vi.mock("@effect/atom-react", () => ({
useAtomRefresh: (atom: object) => () => {
atomHooks.registry?.refresh(atom);
},
useAtomValue: (atom: object) => atomHooks.registry?.get(atom),
}));
vi.mock("react", async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
useCallback: reactHooks.useCallback,
useEffect: reactHooks.useEffect,
useRef: reactHooks.useRef,
};
});
vi.mock("~/state/projects", () => ({
projectEnvironment: projectMocks,
}));
vi.mock("~/state/queries", () => ({
useProjectPathSearch: vi.fn(),
}));
import { useWorkspaceMutationRefresh } from "~/hooks/useWorkspaceMutationRefresh";
import { useProjectEntriesQuery, useProjectFileQuery } from "./projectFilesQueryState";
const environmentId = EnvironmentId.make("environment-1");
function deferred() {
let resolve!: (value: A) => void;
const promise = new Promise((complete) => {
resolve = complete;
});
return { promise, resolve };
}
function file(contents: string): ProjectReadFileResult {
return {
relativePath: "src/preview.ts",
contents,
byteLength: contents.length,
truncated: false,
};
}
function projectEntries(paths: readonly string[]): ProjectListEntriesResult {
return {
entries: paths.map((path) => ({ path, kind: "file" })),
truncated: false,
};
}
async function flushEffects(): Promise {
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
}
describe("project query refresh", () => {
beforeEach(() => {
projectMocks.listEntries.mockReset();
projectMocks.optimisticFile.mockReset();
projectMocks.readFile.mockReset();
reactHooks.reset();
});
it("replaces an in-flight initial read when a workspace mutation arrives", async () => {
const requests: Array>> = [];
const readAtom = Atom.make(
Effect.promise(() => {
const request = deferred();
requests.push(request);
return request.promise;
}),
).pipe(Atom.swr({ staleTime: 30_000, revalidateOnMount: true }));
const registry = AtomRegistry.make();
const unmount = registry.mount(readAtom);
projectMocks.readFile.mockReturnValue(readAtom);
projectMocks.optimisticFile.mockReturnValue(Atom.make(null));
atomHooks.registry = registry;
let renderedContents: string | null = null;
const render = (mutationId: string | null) => {
reactHooks.beginRender();
const query = useProjectFileQuery(environmentId, "/repo", "src/preview.ts");
renderedContents = query.data?.contents ?? null;
useWorkspaceMutationRefresh({
mutationId,
refresh: query.refresh,
resourceKey: "file:environment-1:/repo:src/preview.ts",
});
};
try {
render(null);
await flushEffects();
expect(requests).toHaveLength(1);
render("mutation-1");
await flushEffects();
expect(requests).toHaveLength(2);
requests[1]!.resolve(file("fresh"));
await flushEffects();
render("mutation-1");
expect(renderedContents).toBe("fresh");
requests[0]!.resolve(file("stale"));
await flushEffects();
render("mutation-1");
expect(renderedContents).toBe("fresh");
} finally {
unmount();
registry.dispose();
atomHooks.registry = null;
}
});
it("revalidates cached entries when a workspace mutation is observed after mounting", async () => {
const requests: Array>> = [];
const entriesAtom = Atom.make(
Effect.promise(() => {
const request = deferred();
requests.push(request);
return request.promise;
}),
).pipe(Atom.swr({ staleTime: 30_000, revalidateOnMount: true }));
const registry = AtomRegistry.make();
const unmount = registry.mount(entriesAtom);
projectMocks.listEntries.mockReturnValue(entriesAtom);
atomHooks.registry = registry;
let renderedPaths: readonly string[] = [];
const render = (mutationId: string | null) => {
reactHooks.beginRender();
const query = useProjectEntriesQuery(environmentId, "/repo");
renderedPaths = query.data?.entries.map((entry) => entry.path) ?? [];
useWorkspaceMutationRefresh({
mutationId,
refresh: query.refresh,
resourceKey: "files:environment-1:/repo",
});
};
try {
await flushEffects();
expect(requests).toHaveLength(1);
requests[0]!.resolve(projectEntries(["src/old.ts"]));
await flushEffects();
render("mutation-1");
expect(renderedPaths).toEqual(["src/old.ts"]);
await flushEffects();
expect(requests).toHaveLength(2);
requests[1]!.resolve(projectEntries(["src/new.ts"]));
await flushEffects();
render("mutation-1");
expect(renderedPaths).toEqual(["src/new.ts"]);
expect(requests).toHaveLength(2);
} finally {
unmount();
registry.dispose();
atomHooks.registry = null;
}
});
it("does not issue a file read for a disabled image preview", async () => {
const requests: Array>> = [];
const readAtom = Atom.make(
Effect.promise(() => {
const request = deferred();
requests.push(request);
return request.promise;
}),
);
const registry = AtomRegistry.make();
projectMocks.readFile.mockReturnValue(readAtom);
projectMocks.optimisticFile.mockReturnValue(Atom.make(null));
atomHooks.registry = registry;
try {
reactHooks.beginRender();
const query = useProjectFileQuery(environmentId, "/repo", "preview.png", false);
useWorkspaceMutationRefresh({
enabled: false,
mutationId: "mutation-1",
refresh: query.refresh,
resourceKey: "file:environment-1:/repo:preview.png",
});
await flushEffects();
expect(projectMocks.readFile).not.toHaveBeenCalled();
expect(requests).toHaveLength(0);
} finally {
registry.dispose();
atomHooks.registry = null;
}
});
});