From 56044e408097a8885b93ae902d7609ee97ba37c0 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Tue, 3 Nov 2026 18:14:20 -0800 Subject: [PATCH 1/2] test(server): AgentSessionScanner blocked-repo test no longer races the git-read timeout The test advanced the TestClock as soon as the blocked repo's config was opened. On a slow runner the neighbours' `cat` children were still running, so the jump fired their deadlines too and they came back with no git identity. The test now waits until both neighbour reads have returned. Co-Authored-By: Claude Opus 5.5 --- .../src/project/AgentSessionScanner.test.ts | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/apps/server/src/project/AgentSessionScanner.test.ts b/apps/server/src/project/AgentSessionScanner.test.ts index ce1830b8c5710..5528d8c2d94ca 100644 --- a/apps/server/src/project/AgentSessionScanner.test.ts +++ b/apps/server/src/project/AgentSessionScanner.test.ts @@ -22,11 +22,13 @@ import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; +import * as Queue from "effect/Queue"; import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; import * as TestClock from "effect/testing/TestClock"; import * as ServerConfig from "../config.ts"; +import * as ProcessRunner from "../processRunner.ts"; import * as ProjectStore from "../orchestration-v2/ProjectStore.ts"; import * as ServerSettings from "../serverSettings.ts"; import * as AgentSessionScanner from "./AgentSessionScanner.ts"; @@ -56,10 +58,16 @@ interface ScannerTestInput { readonly importedWorkspaceRoots?: ReadonlyArray; readonly configBaseDir?: string; readonly providerInstances?: ContractServerSettings["providerInstances"]; + readonly processRunner?: Layer.Layer; } const layerScannerTest = (input: ScannerTestInput) => - AgentSessionScanner.layer.pipe( + (input.processRunner === undefined + ? AgentSessionScanner.layer + : Layer.effect(AgentSessionScanner.AgentSessionScanner, AgentSessionScanner.make).pipe( + Layer.provide(input.processRunner), + ) + ).pipe( Layer.provide( Layer.mergeAll( ServerSettings.layerTest({ @@ -1959,11 +1967,38 @@ describe.skipIf(HostProcess.Platform.defaultValue() === "win32")( }); } + // The deadline fires for every read still in flight, so on a slow runner the + // neighbours' `cat` children must have exited before the clock moves. + const neighbourConfigs = new Set( + [repos[0], repos[2]].map((repo) => path.join(repo, ".git", "config")), + ); + const finishedNeighbourReads = yield* Queue.unbounded(); + const processRunner = Layer.effect( + ProcessRunner.ProcessRunner, + Effect.gen(function* () { + const runner = yield* ProcessRunner.ProcessRunner; + return ProcessRunner.ProcessRunner.of({ + run: (input) => { + const filePath = input.args.at(-1) ?? ""; + return neighbourConfigs.has(filePath) + ? runner + .run(input) + .pipe(Effect.tap(() => Queue.offer(finishedNeighbourReads, filePath))) + : runner.run(input); + }, + }); + }), + ).pipe(Layer.provide(ProcessRunner.layer), Layer.provide(NodeServices.layer)); + for (let attempt = 1; attempt <= 2; attempt++) { - const scan = yield* Effect.forkChild(runScan({ claudeHomePath, codexHomePath })); + const scan = yield* Effect.forkChild( + runScan({ claudeHomePath, codexHomePath, processRunner }), + ); const result = yield* Effect.scoped( Effect.gen(function* () { yield* awaitGateReader(path.join(repos[1], ".git", "config")); + yield* Queue.take(finishedNeighbourReads); + yield* Queue.take(finishedNeighbourReads); yield* TestClock.adjust("2 seconds"); return yield* Fiber.join(scan); }), From 256580b07b05ea6e84b99f44be62dbcb5faedf54 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Tue, 3 Nov 2026 18:24:53 -0800 Subject: [PATCH 2/2] test(server): signal neighbour reads that fail too Effect.tap skipped the queue offer when a read failed, so the test could wait until Vitest's timeout instead of failing on its assertion. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/project/AgentSessionScanner.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/project/AgentSessionScanner.test.ts b/apps/server/src/project/AgentSessionScanner.test.ts index 5528d8c2d94ca..0844e3cd71834 100644 --- a/apps/server/src/project/AgentSessionScanner.test.ts +++ b/apps/server/src/project/AgentSessionScanner.test.ts @@ -1983,7 +1983,7 @@ describe.skipIf(HostProcess.Platform.defaultValue() === "win32")( return neighbourConfigs.has(filePath) ? runner .run(input) - .pipe(Effect.tap(() => Queue.offer(finishedNeighbourReads, filePath))) + .pipe(Effect.ensuring(Queue.offer(finishedNeighbourReads, filePath))) : runner.run(input); }, });