authorgravatar for nmharmon8@gmail.comNathan Harmon <nmharmon8@gmail.com> 2026-04-16 22:19:14-06:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-16 21:19:14-07:00
loged6b7fbf27973c8a5180e7074ba448d912c3cce4
tree4f3d35b7ec6546b80f832a965fb87f33792033fb
parentb991b9b9e635a55facda987c74957fab932e842e
signature Signed by PGP key B5690EEEBB952194

fix(server): honor gitignored files in workspace search (#2078)

Co-authored-by: Julius Marminge <julius0216@outlook.com>

2 files changed, 88 insertions(+), 10 deletions(-)

apps/server/src/server.test.ts+76-5
...@@ -370,9 +370,32 @@ const buildAppUnderTest = (options?: {...@@ -370,9 +370,32 @@ const buildAppUnderTest = (options?: {
370 ...options?.config,370 ...options?.config,
371 };371 };
372 const layerConfig = Layer.succeed(ServerConfig, config);372 const layerConfig = Layer.succeed(ServerConfig, config);
373 const gitCoreLayer = Layer.mock(GitCore)({
374 isInsideWorkTree: () => Effect.succeed(false),
375 listWorkspaceFiles: () =>
376 Effect.succeed({
377 paths: [],
378 truncated: false,
379 }),
380 filterIgnoredPaths: (_cwd, relativePaths) => Effect.succeed(relativePaths),
381 ...options?.layers?.gitCore,
382 });
373 const gitManagerLayer = Layer.mock(GitManager)({383 const gitManagerLayer = Layer.mock(GitManager)({
374 ...options?.layers?.gitManager,384 ...options?.layers?.gitManager,
375 });385 });
386 const workspaceEntriesLayer = WorkspaceEntriesLive.pipe(
387 Layer.provide(WorkspacePathsLive),
388 Layer.provideMerge(gitCoreLayer),
389 );
390 const workspaceAndProjectServicesLayer = Layer.mergeAll(
391 WorkspacePathsLive,
392 workspaceEntriesLayer,
393 WorkspaceFileSystemLive.pipe(
394 Layer.provide(WorkspacePathsLive),
395 Layer.provide(workspaceEntriesLayer),
396 ),
397 ProjectFaviconResolverLive,
398 );
376 const gitStatusBroadcasterLayer = options?.layers?.gitStatusBroadcaster399 const gitStatusBroadcasterLayer = options?.layers?.gitStatusBroadcaster
377 ? Layer.mock(GitStatusBroadcaster)({400 ? Layer.mock(GitStatusBroadcaster)({
378 ...options.layers.gitStatusBroadcaster,401 ...options.layers.gitStatusBroadcaster,
...@@ -416,11 +439,7 @@ const buildAppUnderTest = (options?: {...@@ -416,11 +439,7 @@ const buildAppUnderTest = (options?: {
416 ...options?.layers?.open,439 ...options?.layers?.open,
417 }),440 }),
418 ),441 ),
419 Layer.provide(442 Layer.provide(gitCoreLayer),
420 Layer.mock(GitCore)({
421 ...options?.layers?.gitCore,
422 }),
423 ),
424 Layer.provide(gitManagerLayer),443 Layer.provide(gitManagerLayer),
425 Layer.provideMerge(gitStatusBroadcasterLayer),444 Layer.provideMerge(gitStatusBroadcasterLayer),
426 Layer.provide(445 Layer.provide(
...@@ -2017,6 +2036,58 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -2017,6 +2036,58 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
2017 }).pipe(Effect.provide(NodeHttpServer.layerTest)),2036 }).pipe(Effect.provide(NodeHttpServer.layerTest)),
2018 );2037 );
20192038
2039 it.effect("routes websocket rpc projects.searchEntries excludes gitignored files", () =>
2040 Effect.gen(function* () {
2041 const fs = yield* FileSystem.FileSystem;
2042 const path = yield* Path.Path;
2043 const workspaceDir = yield* fs.makeTempDirectoryScoped({
2044 prefix: "t3-ws-project-search-gitignored-",
2045 });
2046 yield* fs.writeFileString(path.join(workspaceDir, ".gitignore"), ".venv/\n");
2047 yield* fs.makeDirectory(path.join(workspaceDir, ".venv", "lib"), { recursive: true });
2048 yield* fs.writeFileString(
2049 path.join(workspaceDir, ".venv", "lib", "ignored-search-target.ts"),
2050 "export const ignored = true;",
2051 );
2052 yield* fs.makeDirectory(path.join(workspaceDir, "src"), { recursive: true });
2053 yield* fs.writeFileString(
2054 path.join(workspaceDir, "src", "tracked.ts"),
2055 "export const ok = 1;",
2056 );
2057
2058 yield* buildAppUnderTest({
2059 layers: {
2060 gitCore: {
2061 isInsideWorkTree: () => Effect.succeed(true),
2062 listWorkspaceFiles: () =>
2063 Effect.succeed({
2064 paths: ["src/tracked.ts"],
2065 truncated: false,
2066 }),
2067 filterIgnoredPaths: (_cwd, relativePaths) =>
2068 Effect.succeed(
2069 relativePaths.filter((relativePath) => !relativePath.startsWith(".venv/")),
2070 ),
2071 },
2072 },
2073 });
2074
2075 const wsUrl = yield* getWsServerUrl("/ws");
2076 const response = yield* Effect.scoped(
2077 withWsRpcClient(wsUrl, (client) =>
2078 client[WS_METHODS.projectsSearchEntries]({
2079 cwd: workspaceDir,
2080 query: "ignored-search-target",
2081 limit: 10,
2082 }),
2083 ),
2084 );
2085
2086 assert.equal(response.entries.length, 0);
2087 assert.equal(response.truncated, false);
2088 }).pipe(Effect.provide(NodeHttpServer.layerTest)),
2089 );
2090
2020 it.effect("routes websocket rpc projects.searchEntries errors", () =>2091 it.effect("routes websocket rpc projects.searchEntries errors", () =>
2021 Effect.gen(function* () {2092 Effect.gen(function* () {
2022 yield* buildAppUnderTest();2093 yield* buildAppUnderTest();
apps/server/src/server.ts+12-5
...@@ -185,13 +185,20 @@ const GitLayerLive = Layer.empty.pipe(...@@ -185,13 +185,20 @@ const GitLayerLive = Layer.empty.pipe(
185185
186const TerminalLayerLive = TerminalManagerLive.pipe(Layer.provide(PtyAdapterLive));186const TerminalLayerLive = TerminalManagerLive.pipe(Layer.provide(PtyAdapterLive));
187187
188const WorkspaceEntriesLayerLive = WorkspaceEntriesLive.pipe(
189 Layer.provide(WorkspacePathsLive),
190 Layer.provideMerge(GitCoreLive),
191);
192
193const WorkspaceFileSystemLayerLive = WorkspaceFileSystemLive.pipe(
194 Layer.provide(WorkspacePathsLive),
195 Layer.provide(WorkspaceEntriesLayerLive),
196);
197
188const WorkspaceLayerLive = Layer.mergeAll(198const WorkspaceLayerLive = Layer.mergeAll(
189 WorkspacePathsLive,199 WorkspacePathsLive,
190 WorkspaceEntriesLive.pipe(Layer.provide(WorkspacePathsLive)),200 WorkspaceEntriesLayerLive,
191 WorkspaceFileSystemLive.pipe(201 WorkspaceFileSystemLayerLive,
192 Layer.provide(WorkspacePathsLive),
193 Layer.provide(WorkspaceEntriesLive.pipe(Layer.provide(WorkspacePathsLive))),
194 ),
195);202);
196203
197const AuthLayerLive = ServerAuthLive.pipe(204const AuthLayerLive = ServerAuthLive.pipe(