1import { AuthFilesystemReadScope, EnvironmentId, type AuthSessionState } from "@t3tools/contracts";
2import { beforeEach, expect, it, vi } from "vite-plus/test";
3
4const state = vi.hoisted(() => ({
5 session: null as Pick<AuthSessionState, "authenticated" | "scopes"> | null,
6 sessionError: null as string | null,
7 phase: "connected" as "connected" | "offline",
8 sessionAtom: {},
9 searchAtom: {},
10}));
11
12vi.mock("react", async (importOriginal) => ({
13 ...(await importOriginal<typeof import("react")>()),
14 useCallback: <A>(callback: A) => callback,
15 useEffect: () => {},
16 useMemo: <A>(factory: () => A) => factory(),
17 useState: <A>(value: A) => [value, vi.fn()],
18}));
19vi.mock("./session", () => ({
20 environmentSession: { sessionStateAtom: () => state.sessionAtom },
21}));
22vi.mock("./presentation", () => ({
23 useEnvironmentPresentation: () => ({
24 isReady: true,
25 presentation: { connection: { phase: state.phase, error: null } },
26 }),
27}));
28vi.mock("./pull-requests", () => ({ composerPullRequests: {} }));
29vi.mock("./projects", () => ({
30 projectEnvironment: { searchEntries: () => state.searchAtom },
31}));
32vi.mock("./atom-registry", () => ({ appAtomRegistry: {} }));
33vi.mock("./orchestration", () => ({ orchestrationEnvironment: {} }));
34vi.mock("./threads", () => ({ useEnvironmentThread: vi.fn() }));
35vi.mock("./vcs", () => ({ vcsEnvironment: {} }));
36vi.mock("./query", () => ({
37 useEnvironmentQuery: (atom: unknown) => ({
38 data:
39 atom === state.sessionAtom
40 ? state.session
41 : atom === state.searchAtom
42 ? { entries: [{ path: "src/index.ts", kind: "file" }] }
43 : null,
44 error: atom === state.sessionAtom ? state.sessionError : null,
45 isPending: atom === state.sessionAtom && state.session === null && state.sessionError === null,
46 refresh: vi.fn(),
47 }),
48}));
49
50import { useComposerPathSearch } from "./queries";
51
52const target = {
53 environmentId: EnvironmentId.make("test-environment"),
54 cwd: "/repo",
55 query: "src",
56};
57
58beforeEach(() => {
59 state.session = null;
60 state.sessionError = null;
61 state.phase = "connected";
62});
63
64it("keeps a file search pending until its grant loads, then shows matches", () => {
65 expect(useComposerPathSearch(target)).toMatchObject({
66 entries: [],
67 error: null,
68 isPending: true,
69 });
70 state.session = { authenticated: true, scopes: [AuthFilesystemReadScope] };
71 expect(useComposerPathSearch(target)).toMatchObject({
72 entries: [{ path: "src/index.ts", kind: "file" }],
73 error: null,
74 isPending: false,
75 });
76});
77
78it("shows a confirmed denial and a connection failure separately", () => {
79 state.session = { authenticated: true, scopes: [] };
80 expect(useComposerPathSearch(target)).toMatchObject({
81 entries: [],
82 error: "This connection cannot search host files.",
83 isPending: false,
84 });
85 state.session = null;
86 state.phase = "offline";
87 expect(useComposerPathSearch(target)).toMatchObject({
88 entries: [],
89 error: "This environment is not connected.",
90 isPending: false,
91 });
92});
93
94it("leaves an inactive search idle while its grant loads", () => {
95 expect(useComposerPathSearch({ ...target, cwd: null, query: null })).toMatchObject({
96 entries: [],
97 error: null,
98 isPending: false,
99 });
100});