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 sessionWaiting: false,
8 phase: "connected" as "connected" | "offline",
9 sessionAtom: {},
10 searchAtom: {},
11 searchRequests: vi.fn(),
12 contentAtom: {},
13 contentRequests: vi.fn(),
14 contentError: null as string | null,
15 contentData: {
16 matches: [{ path: "src/index.ts", lineNumber: 3, lineContent: "a match", matchRanges: [] }],
17 truncated: false,
18 },
19}));
20β
21vi.mock("react", async (importOriginal) => ({
22 ...(await importOriginal<typeof import("react")>()),
23 useCallback: <A>(callback: A) => callback,
24 useEffect: () => {},
25 useMemo: <A>(factory: () => A) => factory(),
26 useState: <A>(value: A) => [value, vi.fn()],
27}));
28vi.mock("./session", () => ({
29 environmentSession: { sessionStateAtom: () => state.sessionAtom },
30}));
31vi.mock("./presentation", () => ({
32 useEnvironmentPresentation: () => ({
33 isReady: true,
34 presentation: { connection: { phase: state.phase, error: null } },
35 }),
36}));
37vi.mock("./projects", () => ({
38 projectEnvironment: {
39 searchEntries: (target: unknown) => {
40 state.searchRequests(target);
41 return state.searchAtom;
42 },
43 },
44 projectContentSearch: (target: unknown) => {
45 state.contentRequests(target);
46 return state.contentAtom;
47 },
48}));
49vi.mock("../rpc/atomRegistry", () => ({ appAtomRegistry: {} }));
50vi.mock("./orchestration", () => ({ orchestrationEnvironment: {} }));
51vi.mock("./threads", () => ({ useEnvironmentThread: vi.fn() }));
52vi.mock("./vcs", () => ({ vcsEnvironment: {} }));
53vi.mock("./query", () => ({
54 useEnvironmentQuery: (atom: unknown) => ({
55 data:
56 atom === state.sessionAtom
57 ? state.session
58 : atom === state.searchAtom
59 ? { entries: [{ path: "src/index.ts", kind: "file" }] }
60 : atom === state.contentAtom
61 ? state.contentData
62 : null,
63 error:
64 atom === state.sessionAtom
65 ? state.sessionError
66 : atom === state.contentAtom
67 ? state.contentError
68 : null,
69 isPending:
70 atom === state.sessionAtom &&
71 (state.sessionWaiting || (state.session === null && state.sessionError === null)),
72 refresh: vi.fn(),
73 }),
74}));
75β
76import { useComposerPathSearch, useProjectContentSearch } from "./queries";
77β
78const target = {
79 environmentId: EnvironmentId.make("test-environment"),
80 cwd: "/repo",
81 query: "src",
82};
83β
84beforeEach(() => {
85 state.session = null;
86 state.sessionError = null;
87 state.sessionWaiting = false;
88 state.phase = "connected";
89 state.searchRequests.mockClear();
90 state.contentRequests.mockClear();
91 state.contentError = null;
92});
93β
94const contentTarget = {
95 ...target,
96 query: " a match ",
97 caseSensitive: true,
98 wholeWord: true,
99 useRegex: false,
100};
101β
102it.each([
103 { query: "src", expected: "src" },
104 { query: "a".repeat(256), expected: "a".repeat(256) },
105 {
106 query: ["a".repeat(86), "b".repeat(86), "c".repeat(86), "unique.ts"].join("/"),
107 expected: ["a".repeat(86), "b".repeat(86), "c".repeat(82)].join("/"),
108 },
109 { query: `${"a".repeat(255)}π/boundary.ts`, expected: "a".repeat(255) },
110 { query: `${"a".repeat(254)}π/boundary.ts`, expected: `${"a".repeat(254)}π` },
111])(
112 "bounds the path search request while preserving query identity: $query",
113 ({ query, expected }) => {
114 state.session = { authenticated: true, scopes: [AuthFilesystemReadScope] };
115 const result = useComposerPathSearch({ ...target, query: ` ${query} ` });
116β
117 expect(state.searchRequests).toHaveBeenCalledExactlyOnceWith({
118 environmentId: target.environmentId,
119 input: { cwd: target.cwd, query: expected, limit: 80 },
120 });
121 expect(result.searchedTarget).toMatchObject({ ...target, query });
122 expect(result.searchedQuery).toBe(query);
123 expect(result.isPending).toBe(false);
124 },
125);
126β
127it("explains an overlong content query instead of sending it", () => {
128 state.session = { authenticated: true, scopes: [AuthFilesystemReadScope] };
129 const result = useProjectContentSearch({ ...contentTarget, query: "a".repeat(257) });
130β
131 expect(state.contentRequests).not.toHaveBeenCalled();
132 expect(result.matches).toEqual([]);
133 expect(result.isPending).toBe(false);
134 expect(result.error).toMatch(/256/);
135});
136β
137it("shows content-search denial before typing and never issues an unauthorized request", () => {
138 state.session = { authenticated: true, scopes: [] };
139 for (const query of ["", contentTarget.query]) {
140 const result = useProjectContentSearch({ ...contentTarget, query });
141 expect(state.contentRequests).not.toHaveBeenCalled();
142 expect(result).toMatchObject({
143 canReadFiles: false,
144 isCheckingAccess: false,
145 matches: [],
146 error: "This connection cannot search host files.",
147 isPending: false,
148 });
149 }
150 expect(state.contentRequests).not.toHaveBeenCalled();
151});
152β
153it("keeps confirmed content access during revalidation and drops results when it is revoked", () => {
154 state.session = { authenticated: true, scopes: [AuthFilesystemReadScope] };
155 state.sessionWaiting = true;
156 expect(useProjectContentSearch(contentTarget)).toMatchObject({
157 matches: state.contentData.matches,
158 isPending: false,
159 });
160β
161 state.contentRequests.mockClear();
162 state.sessionWaiting = false;
163 state.session = { authenticated: true, scopes: [] };
164 const result = useProjectContentSearch(contentTarget);
165 expect(state.contentRequests).not.toHaveBeenCalled();
166 expect(result).toMatchObject({
167 canReadFiles: false,
168 matches: [],
169 error: "This connection cannot search host files.",
170 isPending: false,
171 });
172 expect(state.contentRequests).not.toHaveBeenCalled();
173});
174β
175it("fails closed after a session check fails, even with a cached grant and matches", () => {
176 state.session = { authenticated: true, scopes: [AuthFilesystemReadScope] };
177 state.sessionError = "The session has expired.";
178 const result = useProjectContentSearch(contentTarget);
179 expect(state.contentRequests).not.toHaveBeenCalled();
180 expect(result).toMatchObject({
181 canReadFiles: false,
182 matches: [],
183 error: state.sessionError,
184 isPending: false,
185 });
186 expect(state.contentRequests).not.toHaveBeenCalled();
187});