authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-04 13:33:23-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-04 20:33:23+00:00
logb123cbb317ca47ff080a15b44c4bc0c99126f729
treeb8e86d7d8b1878a70acaafc03cddaa9014bcc6e7
parent5f4c7161fa7782ea93b63fe934d43229742e7ec8
signature Signed by PGP key B5690EEEBB952194

test(server): make Windows path and async fixtures deterministic (#9576)

Co-authored-by: Claude Code <noreply@anthropic.com>

9 files changed, 125 insertions(+), 75 deletions(-)

apps/server/src/bootstrap.test.ts+21-11
......@@ -57,6 +57,22 @@ vi.mock("node:fs", async (importOriginal) => {
5757
5858const windowsHost = HostProcessPlatform.defaultValue() === "win32";
5959const nullDevice = windowsHost ? "\\\\.\\NUL" : "/dev/null";
60const closeIfOpen = (fd: number) => {
61 try {
62 NodeFS.closeSync(fd);
63 } catch (error) {
64 if ((error as NodeJS.ErrnoException).code !== "EBADF") throw error;
65 }
66};
67
68// A successful Windows read streams the inherited fd with autoClose. POSIX
69// reopens the fd through /proc or /dev, so the test still owns the original.
70const openBootstrapInputFd = (filePath: string) =>
71 Effect.acquireRelease(
72 Effect.sync(() => NodeFS.openSync(filePath, "r")),
73 (fd) => (windowsHost ? Effect.void : Effect.sync(() => closeIfOpen(fd))),
74 );
75
6076const TestEnvelopeSchema = Schema.Struct({ mode: Schema.String });
6177const encodeTestEnvelopeSchema = Schema.encodeEffect(Schema.fromJsonString(TestEnvelopeSchema));
6278
......@@ -71,10 +87,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
7187 `${yield* encodeTestEnvelopeSchema({ mode: "desktop" })}\n`,
7288 );
7389
74 const fd = yield* Effect.acquireRelease(
75 Effect.sync(() => NodeFS.openSync(filePath, "r")),
76 (fd) => Effect.sync(() => NodeFS.closeSync(fd)),
77 );
90 const fd = yield* openBootstrapInputFd(filePath);
7891
7992 const payload = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, { timeoutMs: 100 });
8093 assertSome(payload, {
......@@ -119,7 +132,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
119132 const filePath = yield* fs.makeTempFileScoped({ prefix: "t3-bootstrap-", suffix: ".ndjson" });
120133 const fd = yield* Effect.acquireRelease(
121134 Effect.sync(() => NodeFS.openSync(filePath, "r")),
122 (fd) => Effect.sync(() => NodeFS.closeSync(fd)),
135 (fd) => Effect.sync(() => closeIfOpen(fd)),
123136 );
124137 const fdPath = `/proc/self/fd/${fd}`;
125138
......@@ -160,7 +173,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
160173 Effect.gen(function* () {
161174 const fd = yield* Effect.acquireRelease(
162175 Effect.sync(() => NodeFS.openSync(nullDevice, "r")),
163 (fd) => Effect.sync(() => NodeFS.closeSync(fd)),
176 (fd) => Effect.sync(() => closeIfOpen(fd)),
164177 );
165178
166179 fstatSyncInterceptor.failFd = fd;
......@@ -185,10 +198,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
185198 const filePath = yield* fs.makeTempFileScoped({ prefix: "t3-bootstrap-", suffix: ".ndjson" });
186199 yield* fs.writeFileString(filePath, '{"mode":42}\n');
187200
188 const fd = yield* Effect.acquireRelease(
189 Effect.sync(() => NodeFS.openSync(filePath, "r")),
190 (fd) => Effect.sync(() => NodeFS.closeSync(fd)),
191 );
201 const fd = yield* openBootstrapInputFd(filePath);
192202 const error = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, {
193203 timeoutMs: 100,
194204 }).pipe(Effect.flip);
......@@ -228,7 +238,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
228238
229239 const fd = yield* Effect.acquireRelease(
230240 Effect.sync(() => NodeFS.openSync(fifoPath, "r")),
231 (fd) => Effect.sync(() => NodeFS.closeSync(fd)),
241 (fd) => Effect.sync(() => closeIfOpen(fd)),
232242 );
233243
234244 const fiber = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, {
apps/server/src/cli/config.test.ts+15-4
......@@ -60,7 +60,16 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
6060 yield* fs.writeFileString(filePath, `${encoded}\n`);
6161 return yield* Effect.acquireRelease(
6262 Effect.sync(() => NodeFS.openSync(filePath, "r")),
63 (fd) => Effect.sync(() => NodeFS.closeSync(fd)),
63 // Without a /proc or /dev/fd path to reopen, the reader consumes the fd
64 // itself (autoClose), so on Windows it is already closed here.
65 (fd) =>
66 Effect.sync(() => {
67 try {
68 NodeFS.closeSync(fd);
69 } catch (error) {
70 if ((error as NodeJS.ErrnoException).code !== "EBADF") throw error;
71 }
72 }),
6473 );
6574 });
6675
......@@ -281,13 +290,15 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
281290
282291 it.effect("uses bootstrap envelope values as fallbacks when flags and env are absent", () =>
283292 Effect.gen(function* () {
284 const { join } = yield* Path.Path;
285 const baseDir = "/tmp/t3-bootstrap-home";
293 const { join, resolve } = yield* Path.Path;
294 // The resolver absolutises the configured home, so the expectation must
295 // carry the host's drive on Windows.
296 const baseDir = resolve("/tmp/t3-bootstrap-home");
286297 const fd = yield* openBootstrapFd(
287298 makeDesktopBootstrap({
288299 port: 4888,
289300 host: "127.0.0.2",
290 t3Home: baseDir,
301 t3Home: "/tmp/t3-bootstrap-home",
291302 noBrowser: true,
292303 desktopBootstrapToken: "desktop-token",
293304 desktopTelemetryFd: 4,
apps/server/src/cloud/bootService.test.ts+6-3
......@@ -529,11 +529,14 @@ it.layer(NodeServices.layer)("boot service install", (it) => {
529529 it.effect("installs, reports current state, and uninstalls on macOS", () =>
530530 Effect.gen(function* () {
531531 const { service, fs, statePath, commands, timeouts } = yield* makeHarness("darwin");
532 const path = yield* Path.Path;
532533 const plan = yield* service.install();
533534
534 expect(plan.unitPath.endsWith("Library/LaunchAgents/com.t3tools.t3code.service.plist")).toBe(
535 true,
536 );
535 expect(
536 plan.unitPath.endsWith(
537 path.join("Library", "LaunchAgents", "com.t3tools.t3code.service.plist"),
538 ),
539 ).toBe(true);
537540 expect(yield* fs.readFileString(plan.unitPath)).toContain(
538541 ` <key>PATH</key>\n <string>${macInstallerPath}:/usr/local/bin:/usr/sbin:/sbin</string>`,
539542 );
apps/server/src/provider/Layers/AntigravityAdapter.test.ts+2-1
......@@ -421,7 +421,8 @@ it.layer(layer)("AntigravityAdapter", (it) => {
421421 modelSelection: { instanceId, model: nativeAlternative },
422422 });
423423 expect(second.model).toBe(nativeAlternative);
424 expect(second.cwd).toBe("/tmp");
424 // The adapter resolves the cwd it was given through the host Path.
425 expect(second.cwd).toBe((yield* Path.Path).resolve("/tmp"));
425426 expect(h.launches[1]?.resumeSessionId).toBe(nativeSessionId);
426427 expect(h.calls).toEqual([
427428 "start",
apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts+13-4
......@@ -24,6 +24,7 @@
2424 */
2525import { describe, expect, it } from "@effect/vitest";
2626import * as NodeServices from "@effect/platform-node/NodeServices";
27import * as Path from "effect/Path";
2728import {
2829 type ClaudeSettings,
2930 type CodexSettings,
......@@ -214,15 +215,19 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => {
214215 expect(personalSnapshot.instanceId).toBe(personalId);
215216 expect(personalSnapshot.driver).toBe(codexDriverKind);
216217 expect(personalSnapshot.enabled).toBe(false);
218 // The layout resolves the configured home through the host Path.
219 const path = yield* Path.Path;
217220 expect(personalSnapshot.continuation?.groupKey).toBe(
218 "codex:home:/home/julius/.codex_personal",
221 `codex:home:${path.resolve("/home/julius/.codex_personal")}`,
219222 );
220223
221224 const workSnapshot = yield* work!.snapshot.getSnapshot;
222225 expect(workSnapshot.instanceId).toBe(workId);
223226 expect(workSnapshot.driver).toBe(codexDriverKind);
224227 expect(workSnapshot.enabled).toBe(false);
225 expect(workSnapshot.continuation?.groupKey).toBe("codex:home:/home/julius/.codex");
228 expect(workSnapshot.continuation?.groupKey).toBe(
229 `codex:home:${path.resolve("/home/julius/.codex")}`,
230 );
226231
227232 // Nothing goes to the unavailable bucket — both drivers are registered.
228233 const unavailable = yield* registry.listUnavailable;
......@@ -451,13 +456,17 @@ describe("ProviderInstanceRegistryLive — all drivers slice", () => {
451456 expect(codexSnapshot.instanceId).toBe(codexId);
452457 expect(codexSnapshot.driver).toBe(codexDriverKind);
453458 expect(codexSnapshot.enabled).toBe(false);
454 expect(codexSnapshot.continuation?.groupKey).toBe("codex:home:/home/julius/.codex");
459 expect(codexSnapshot.continuation?.groupKey).toBe(
460 `codex:home:${(yield* Path.Path).resolve("/home/julius/.codex")}`,
461 );
455462
456463 const claudeSnapshot = yield* claude!.snapshot.getSnapshot;
457464 expect(claudeSnapshot.instanceId).toBe(claudeId);
458465 expect(claudeSnapshot.driver).toBe(claudeDriverKind);
459466 expect(claudeSnapshot.enabled).toBe(false);
460 expect(claudeSnapshot.continuation?.groupKey).toBe("claude:home:/home/julius/.claude-work");
467 expect(claudeSnapshot.continuation?.groupKey).toBe(
468 `claude:home:${(yield* Path.Path).resolve("/home/julius/.claude-work")}`,
469 );
461470
462471 const cursorSnapshot = yield* cursor!.snapshot.getSnapshot;
463472 expect(cursorSnapshot.instanceId).toBe(cursorId);
apps/server/src/provider/Layers/ProviderRegistry.test.ts+32-31
......@@ -3,6 +3,7 @@ import { describe, it, assert } from "@effect/vitest";
33import * as DateTime from "effect/DateTime";
44import * as Deferred from "effect/Deferred";
55import * as Effect from "effect/Effect";
6import * as Path from "effect/Path";
67import * as Exit from "effect/Exit";
78import * as Fiber from "effect/Fiber";
89import * as Layer from "effect/Layer";
......@@ -346,6 +347,21 @@ function makeMutableServerSettingsService(
346347 });
347348}
348349
350// The registry writes the status cache and only then publishes the change, so
351// a subscriber that sees `checkedAt` on the stream knows the file is on disk.
352// Subscribed before the publish that triggers it; a spin on the file would
353// race the write and lose on a slow host.
354const awaitPersistedProvider = (
355 registry: ProviderRegistry.ProviderRegistry["Service"],
356 checkedAt: string,
357) =>
358 registry.streamChanges.pipe(
359 Stream.filter((providers) => providers.some((provider) => provider.checkedAt === checkedAt)),
360 Stream.take(1),
361 Stream.runDrain,
362 Effect.forkScoped,
363 );
364
349365it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), TestHttpClientLive))(
350366 "ProviderRegistry",
351367 (it) => {
......@@ -1752,18 +1768,10 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
17521768 assert.deepStrictEqual((yield* registry.getProviders)[0]?.models, [
17531769 ...initialProvider.models,
17541770 ]);
1771 const persisted = yield* awaitPersistedProvider(registry, refreshedProvider.checkedAt);
17551772 yield* PubSub.publish(changes, refreshedProvider);
1756
1757 let cachedProvider = yield* readProviderStatusCache(filePath);
1758 for (
1759 let attempt = 0;
1760 attempt < 50 && cachedProvider?.checkedAt !== refreshedProvider.checkedAt;
1761 attempt += 1
1762 ) {
1763 yield* TestClock.adjust("10 millis");
1764 yield* Effect.yieldNow;
1765 cachedProvider = yield* readProviderStatusCache(filePath);
1766 }
1773 yield* Fiber.join(persisted);
1774 const cachedProvider = yield* readProviderStatusCache(filePath);
17671775
17681776 assert.deepStrictEqual(cachedProvider, {
17691777 ...refreshedProvider,
......@@ -1878,31 +1886,23 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
18781886 instanceId: openCodeInstanceId,
18791887 });
18801888
1889 const authoritativePersisted = yield* awaitPersistedProvider(
1890 registry,
1891 authoritativeProvider.checkedAt,
1892 );
18811893 yield* PubSub.publish(changes, authoritativeProvider);
1882
1894 yield* Fiber.join(authoritativePersisted);
18831895 let cachedProvider = yield* readProviderStatusCache(filePath);
1884 for (
1885 let attempt = 0;
1886 attempt < 50 && cachedProvider?.checkedAt !== authoritativeProvider.checkedAt;
1887 attempt += 1
1888 ) {
1889 yield* TestClock.adjust("10 millis");
1890 yield* Effect.yieldNow;
1891 cachedProvider = yield* readProviderStatusCache(filePath);
1892 }
18931896
18941897 assert.deepStrictEqual(cachedProvider?.models, [authoritativeProvider.models[0]!]);
18951898
1899 const failedPersisted = yield* awaitPersistedProvider(
1900 registry,
1901 failedProvider.checkedAt,
1902 );
18961903 yield* PubSub.publish(changes, failedProvider);
1897 for (
1898 let attempt = 0;
1899 attempt < 50 && cachedProvider?.checkedAt !== failedProvider.checkedAt;
1900 attempt += 1
1901 ) {
1902 yield* TestClock.adjust("10 millis");
1903 yield* Effect.yieldNow;
1904 cachedProvider = yield* readProviderStatusCache(filePath);
1905 }
1904 yield* Fiber.join(failedPersisted);
1905 cachedProvider = yield* readProviderStatusCache(filePath);
19061906
19071907 assert.deepStrictEqual(cachedProvider?.models, [authoritativeProvider.models[0]!]);
19081908 assert.deepStrictEqual((yield* registry.getProviders)[0]?.models, [
......@@ -2715,9 +2715,10 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
27152715 claudeCapabilities(),
27162716 );
27172717 assert.strictEqual(status.status, "ready");
2718 // The home is resolved through the host Path before it reaches the env.
27182719 assert.deepStrictEqual(
27192720 recorded.commands.map((command) => command.env?.CLAUDE_CONFIG_DIR),
2720 [claudeConfigDir],
2721 [(yield* Path.Path).resolve(claudeConfigDir)],
27212722 );
27222723 }).pipe(Effect.provide(recorded.layer));
27232724 });
apps/server/src/server.test.ts+10-1
......@@ -1855,12 +1855,18 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
18551855 const staticDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-static-replace-" });
18561856 const beforeOpenPath = path.join(staticDir, "before-open.txt");
18571857 const afterOpenPath = path.join(staticDir, "after-open.txt");
1858 const afterOpenSnapshotPath = path.join(staticDir, "after-open-snapshot.txt");
1859 const windowsHost = HostProcessPlatform.defaultValue() === "win32";
18581860 const original = "original bytes";
18591861 const replacement = "replacement bytes with a different size";
18601862 for (const filePath of [beforeOpenPath, afterOpenPath]) {
18611863 yield* fileSystem.writeFileString(filePath, original);
18621864 yield* fileSystem.writeFileString(`${filePath}.next`, replacement);
18631865 }
1866 if (windowsHost) {
1867 // Windows cannot replace an open destination, so model the race with its original handle.
1868 yield* fileSystem.writeFileString(afterOpenSnapshotPath, original);
1869 }
18641870 const replaced = new Set<string>();
18651871 const replaceOnce = Effect.fnUntraced(function* (filePath: string) {
18661872 if (replaced.has(filePath)) return;
......@@ -1877,7 +1883,10 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
18771883 ),
18781884 open: (filePath, options) =>
18791885 fileSystem
1880 .open(filePath, options)
1886 .open(
1887 filePath === afterOpenPath && windowsHost ? afterOpenSnapshotPath : filePath,
1888 options,
1889 )
18811890 .pipe(
18821891 Effect.tap(() => (filePath === afterOpenPath ? replaceOnce(filePath) : Effect.void)),
18831892 ),
apps/server/src/sourceControl/SourceControlRepositoryService.test.ts+3-1
......@@ -3,6 +3,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
33import { assert, it } from "@effect/vitest";
44import * as Effect from "effect/Effect";
55import * as FileSystem from "effect/FileSystem";
6import * as Path from "effect/Path";
67import * as Layer from "effect/Layer";
78import * as PlatformError from "effect/PlatformError";
89import { ChildProcessSpawner } from "effect/unstable/process";
......@@ -153,10 +154,11 @@ it.effect("preserves provider failures without deriving the repository message f
153154it.effect("clones a looked-up repository into the requested destination", () =>
154155 Effect.gen(function* () {
155156 const fs = yield* FileSystem.FileSystem;
157 const path = yield* Path.Path;
156158 const parent = yield* fs.makeTempDirectoryScoped({
157159 prefix: "t3-source-control-clone-parent-",
158160 });
159 const destinationPath = `${parent}/t3code`;
161 const destinationPath = path.join(parent, "t3code");
160162 const cloneCalls: Array<{ cwd: string; args: ReadonlyArray<string> }> = [];
161163
162164 yield* Effect.gen(function* () {
apps/server/src/workspace/WorkspaceFileSystem.test.ts+23-19
......@@ -14,6 +14,7 @@ import * as VcsProcess from "../vcs/VcsProcess.ts";
1414import * as WorkspaceEntries from "./WorkspaceEntries.ts";
1515import * as WorkspaceFileSystem from "./WorkspaceFileSystem.ts";
1616import * as WorkspacePaths from "./WorkspacePaths.ts";
17import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
1718import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
1819
1920const ProjectLayer = WorkspaceFileSystem.layer.pipe(
......@@ -100,28 +101,31 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i
100101 }),
101102 );
102103
103 it.effect("rejects a FIFO without blocking on open", () =>
104 Effect.gen(function* () {
105 const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem;
106 const path = yield* Path.Path;
107 const cwd = yield* makeTempDir;
108 const outsideDir = yield* makeTempDir;
109 const fifoPath = path.join(outsideDir, "pipe");
110 yield* Effect.promise(
111 () =>
112 new Promise<void>((resolve, reject) =>
113 NodeChildProcess.execFile("mkfifo", [fifoPath], (error) =>
114 error ? reject(error) : resolve(),
104 // Needs mkfifo; Windows has no FIFOs to reject.
105 it.effect.skipIf(HostProcessPlatform.defaultValue() === "win32")(
106 "rejects a FIFO without blocking on open",
107 () =>
108 Effect.gen(function* () {
109 const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem;
110 const path = yield* Path.Path;
111 const cwd = yield* makeTempDir;
112 const outsideDir = yield* makeTempDir;
113 const fifoPath = path.join(outsideDir, "pipe");
114 yield* Effect.promise(
115 () =>
116 new Promise<void>((resolve, reject) =>
117 NodeChildProcess.execFile("mkfifo", [fifoPath], (error) =>
118 error ? reject(error) : resolve(),
119 ),
115120 ),
116 ),
117 );
121 );
118122
119 const error = yield* workspaceFileSystem
120 .readFile({ cwd, relativePath: fifoPath })
121 .pipe(Effect.flip);
123 const error = yield* workspaceFileSystem
124 .readFile({ cwd, relativePath: fifoPath })
125 .pipe(Effect.flip);
122126
123 expect(error).toBeInstanceOf(WorkspaceFileSystem.WorkspacePathNotFileError);
124 }),
127 expect(error).toBeInstanceOf(WorkspaceFileSystem.WorkspacePathNotFileError);
128 }),
125129 );
126130
127131 it.effect("rejects reads outside the workspace root", () =>