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) => {...@@ -57,6 +57,22 @@ vi.mock("node:fs", async (importOriginal) => {
5757
58const windowsHost = HostProcessPlatform.defaultValue() === "win32";58const windowsHost = HostProcessPlatform.defaultValue() === "win32";
59const nullDevice = windowsHost ? "\\\\.\\NUL" : "/dev/null";59const 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
60const TestEnvelopeSchema = Schema.Struct({ mode: Schema.String });76const TestEnvelopeSchema = Schema.Struct({ mode: Schema.String });
61const encodeTestEnvelopeSchema = Schema.encodeEffect(Schema.fromJsonString(TestEnvelopeSchema));77const encodeTestEnvelopeSchema = Schema.encodeEffect(Schema.fromJsonString(TestEnvelopeSchema));
6278
...@@ -71,10 +87,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {...@@ -71,10 +87,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
71 `${yield* encodeTestEnvelopeSchema({ mode: "desktop" })}\n`,87 `${yield* encodeTestEnvelopeSchema({ mode: "desktop" })}\n`,
72 );88 );
7389
74 const fd = yield* Effect.acquireRelease(90 const fd = yield* openBootstrapInputFd(filePath);
75 Effect.sync(() => NodeFS.openSync(filePath, "r")),
76 (fd) => Effect.sync(() => NodeFS.closeSync(fd)),
77 );
7891
79 const payload = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, { timeoutMs: 100 });92 const payload = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, { timeoutMs: 100 });
80 assertSome(payload, {93 assertSome(payload, {
...@@ -119,7 +132,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {...@@ -119,7 +132,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
119 const filePath = yield* fs.makeTempFileScoped({ prefix: "t3-bootstrap-", suffix: ".ndjson" });132 const filePath = yield* fs.makeTempFileScoped({ prefix: "t3-bootstrap-", suffix: ".ndjson" });
120 const fd = yield* Effect.acquireRelease(133 const fd = yield* Effect.acquireRelease(
121 Effect.sync(() => NodeFS.openSync(filePath, "r")),134 Effect.sync(() => NodeFS.openSync(filePath, "r")),
122 (fd) => Effect.sync(() => NodeFS.closeSync(fd)),135 (fd) => Effect.sync(() => closeIfOpen(fd)),
123 );136 );
124 const fdPath = `/proc/self/fd/${fd}`;137 const fdPath = `/proc/self/fd/${fd}`;
125138
...@@ -160,7 +173,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {...@@ -160,7 +173,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
160 Effect.gen(function* () {173 Effect.gen(function* () {
161 const fd = yield* Effect.acquireRelease(174 const fd = yield* Effect.acquireRelease(
162 Effect.sync(() => NodeFS.openSync(nullDevice, "r")),175 Effect.sync(() => NodeFS.openSync(nullDevice, "r")),
163 (fd) => Effect.sync(() => NodeFS.closeSync(fd)),176 (fd) => Effect.sync(() => closeIfOpen(fd)),
164 );177 );
165178
166 fstatSyncInterceptor.failFd = fd;179 fstatSyncInterceptor.failFd = fd;
...@@ -185,10 +198,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {...@@ -185,10 +198,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
185 const filePath = yield* fs.makeTempFileScoped({ prefix: "t3-bootstrap-", suffix: ".ndjson" });198 const filePath = yield* fs.makeTempFileScoped({ prefix: "t3-bootstrap-", suffix: ".ndjson" });
186 yield* fs.writeFileString(filePath, '{"mode":42}\n');199 yield* fs.writeFileString(filePath, '{"mode":42}\n');
187200
188 const fd = yield* Effect.acquireRelease(201 const fd = yield* openBootstrapInputFd(filePath);
189 Effect.sync(() => NodeFS.openSync(filePath, "r")),
190 (fd) => Effect.sync(() => NodeFS.closeSync(fd)),
191 );
192 const error = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, {202 const error = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, {
193 timeoutMs: 100,203 timeoutMs: 100,
194 }).pipe(Effect.flip);204 }).pipe(Effect.flip);
...@@ -228,7 +238,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {...@@ -228,7 +238,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
228238
229 const fd = yield* Effect.acquireRelease(239 const fd = yield* Effect.acquireRelease(
230 Effect.sync(() => NodeFS.openSync(fifoPath, "r")),240 Effect.sync(() => NodeFS.openSync(fifoPath, "r")),
231 (fd) => Effect.sync(() => NodeFS.closeSync(fd)),241 (fd) => Effect.sync(() => closeIfOpen(fd)),
232 );242 );
233243
234 const fiber = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, {244 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) => {...@@ -60,7 +60,16 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
60 yield* fs.writeFileString(filePath, `${encoded}\n`);60 yield* fs.writeFileString(filePath, `${encoded}\n`);
61 return yield* Effect.acquireRelease(61 return yield* Effect.acquireRelease(
62 Effect.sync(() => NodeFS.openSync(filePath, "r")),62 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 }),
64 );73 );
65 });74 });
6675
...@@ -281,13 +290,15 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {...@@ -281,13 +290,15 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
281290
282 it.effect("uses bootstrap envelope values as fallbacks when flags and env are absent", () =>291 it.effect("uses bootstrap envelope values as fallbacks when flags and env are absent", () =>
283 Effect.gen(function* () {292 Effect.gen(function* () {
284 const { join } = yield* Path.Path;293 const { join, resolve } = yield* Path.Path;
285 const baseDir = "/tmp/t3-bootstrap-home";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");
286 const fd = yield* openBootstrapFd(297 const fd = yield* openBootstrapFd(
287 makeDesktopBootstrap({298 makeDesktopBootstrap({
288 port: 4888,299 port: 4888,
289 host: "127.0.0.2",300 host: "127.0.0.2",
290 t3Home: baseDir,301 t3Home: "/tmp/t3-bootstrap-home",
291 noBrowser: true,302 noBrowser: true,
292 desktopBootstrapToken: "desktop-token",303 desktopBootstrapToken: "desktop-token",
293 desktopTelemetryFd: 4,304 desktopTelemetryFd: 4,
apps/server/src/cloud/bootService.test.ts+6-3
...@@ -529,11 +529,14 @@ it.layer(NodeServices.layer)("boot service install", (it) => {...@@ -529,11 +529,14 @@ it.layer(NodeServices.layer)("boot service install", (it) => {
529 it.effect("installs, reports current state, and uninstalls on macOS", () =>529 it.effect("installs, reports current state, and uninstalls on macOS", () =>
530 Effect.gen(function* () {530 Effect.gen(function* () {
531 const { service, fs, statePath, commands, timeouts } = yield* makeHarness("darwin");531 const { service, fs, statePath, commands, timeouts } = yield* makeHarness("darwin");
532 const path = yield* Path.Path;
532 const plan = yield* service.install();533 const plan = yield* service.install();
533534
534 expect(plan.unitPath.endsWith("Library/LaunchAgents/com.t3tools.t3code.service.plist")).toBe(535 expect(
535 true,536 plan.unitPath.endsWith(
536 );537 path.join("Library", "LaunchAgents", "com.t3tools.t3code.service.plist"),
538 ),
539 ).toBe(true);
537 expect(yield* fs.readFileString(plan.unitPath)).toContain(540 expect(yield* fs.readFileString(plan.unitPath)).toContain(
538 ` <key>PATH</key>\n <string>${macInstallerPath}:/usr/local/bin:/usr/sbin:/sbin</string>`,541 ` <key>PATH</key>\n <string>${macInstallerPath}:/usr/local/bin:/usr/sbin:/sbin</string>`,
539 );542 );
apps/server/src/provider/Layers/AntigravityAdapter.test.ts+2-1
...@@ -421,7 +421,8 @@ it.layer(layer)("AntigravityAdapter", (it) => {...@@ -421,7 +421,8 @@ it.layer(layer)("AntigravityAdapter", (it) => {
421 modelSelection: { instanceId, model: nativeAlternative },421 modelSelection: { instanceId, model: nativeAlternative },
422 });422 });
423 expect(second.model).toBe(nativeAlternative);423 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"));
425 expect(h.launches[1]?.resumeSessionId).toBe(nativeSessionId);426 expect(h.launches[1]?.resumeSessionId).toBe(nativeSessionId);
426 expect(h.calls).toEqual([427 expect(h.calls).toEqual([
427 "start",428 "start",
apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts+13-4
...@@ -24,6 +24,7 @@...@@ -24,6 +24,7 @@
24 */24 */
25import { describe, expect, it } from "@effect/vitest";25import { describe, expect, it } from "@effect/vitest";
26import * as NodeServices from "@effect/platform-node/NodeServices";26import * as NodeServices from "@effect/platform-node/NodeServices";
27import * as Path from "effect/Path";
27import {28import {
28 type ClaudeSettings,29 type ClaudeSettings,
29 type CodexSettings,30 type CodexSettings,
...@@ -214,15 +215,19 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => {...@@ -214,15 +215,19 @@ describe("ProviderInstanceRegistryLive — multi-instance codex slice", () => {
214 expect(personalSnapshot.instanceId).toBe(personalId);215 expect(personalSnapshot.instanceId).toBe(personalId);
215 expect(personalSnapshot.driver).toBe(codexDriverKind);216 expect(personalSnapshot.driver).toBe(codexDriverKind);
216 expect(personalSnapshot.enabled).toBe(false);217 expect(personalSnapshot.enabled).toBe(false);
218 // The layout resolves the configured home through the host Path.
219 const path = yield* Path.Path;
217 expect(personalSnapshot.continuation?.groupKey).toBe(220 expect(personalSnapshot.continuation?.groupKey).toBe(
218 "codex:home:/home/julius/.codex_personal",221 `codex:home:${path.resolve("/home/julius/.codex_personal")}`,
219 );222 );
220223
221 const workSnapshot = yield* work!.snapshot.getSnapshot;224 const workSnapshot = yield* work!.snapshot.getSnapshot;
222 expect(workSnapshot.instanceId).toBe(workId);225 expect(workSnapshot.instanceId).toBe(workId);
223 expect(workSnapshot.driver).toBe(codexDriverKind);226 expect(workSnapshot.driver).toBe(codexDriverKind);
224 expect(workSnapshot.enabled).toBe(false);227 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
227 // Nothing goes to the unavailable bucket — both drivers are registered.232 // Nothing goes to the unavailable bucket — both drivers are registered.
228 const unavailable = yield* registry.listUnavailable;233 const unavailable = yield* registry.listUnavailable;
...@@ -451,13 +456,17 @@ describe("ProviderInstanceRegistryLive — all drivers slice", () => {...@@ -451,13 +456,17 @@ describe("ProviderInstanceRegistryLive — all drivers slice", () => {
451 expect(codexSnapshot.instanceId).toBe(codexId);456 expect(codexSnapshot.instanceId).toBe(codexId);
452 expect(codexSnapshot.driver).toBe(codexDriverKind);457 expect(codexSnapshot.driver).toBe(codexDriverKind);
453 expect(codexSnapshot.enabled).toBe(false);458 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
456 const claudeSnapshot = yield* claude!.snapshot.getSnapshot;463 const claudeSnapshot = yield* claude!.snapshot.getSnapshot;
457 expect(claudeSnapshot.instanceId).toBe(claudeId);464 expect(claudeSnapshot.instanceId).toBe(claudeId);
458 expect(claudeSnapshot.driver).toBe(claudeDriverKind);465 expect(claudeSnapshot.driver).toBe(claudeDriverKind);
459 expect(claudeSnapshot.enabled).toBe(false);466 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
462 const cursorSnapshot = yield* cursor!.snapshot.getSnapshot;471 const cursorSnapshot = yield* cursor!.snapshot.getSnapshot;
463 expect(cursorSnapshot.instanceId).toBe(cursorId);472 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";...@@ -3,6 +3,7 @@ import { describe, it, assert } from "@effect/vitest";
3import * as DateTime from "effect/DateTime";3import * as DateTime from "effect/DateTime";
4import * as Deferred from "effect/Deferred";4import * as Deferred from "effect/Deferred";
5import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
6import * as Path from "effect/Path";
6import * as Exit from "effect/Exit";7import * as Exit from "effect/Exit";
7import * as Fiber from "effect/Fiber";8import * as Fiber from "effect/Fiber";
8import * as Layer from "effect/Layer";9import * as Layer from "effect/Layer";
...@@ -346,6 +347,21 @@ function makeMutableServerSettingsService(...@@ -346,6 +347,21 @@ function makeMutableServerSettingsService(
346 });347 });
347}348}
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
349it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), TestHttpClientLive))(365it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), TestHttpClientLive))(
350 "ProviderRegistry",366 "ProviderRegistry",
351 (it) => {367 (it) => {
...@@ -1752,18 +1768,10 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te...@@ -1752,18 +1768,10 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
1752 assert.deepStrictEqual((yield* registry.getProviders)[0]?.models, [1768 assert.deepStrictEqual((yield* registry.getProviders)[0]?.models, [
1753 ...initialProvider.models,1769 ...initialProvider.models,
1754 ]);1770 ]);
1771 const persisted = yield* awaitPersistedProvider(registry, refreshedProvider.checkedAt);
1755 yield* PubSub.publish(changes, refreshedProvider);1772 yield* PubSub.publish(changes, refreshedProvider);
17561773 yield* Fiber.join(persisted);
1757 let cachedProvider = yield* readProviderStatusCache(filePath);1774 const 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 }
17671775
1768 assert.deepStrictEqual(cachedProvider, {1776 assert.deepStrictEqual(cachedProvider, {
1769 ...refreshedProvider,1777 ...refreshedProvider,
...@@ -1878,31 +1886,23 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te...@@ -1878,31 +1886,23 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
1878 instanceId: openCodeInstanceId,1886 instanceId: openCodeInstanceId,
1879 });1887 });
18801888
1889 const authoritativePersisted = yield* awaitPersistedProvider(
1890 registry,
1891 authoritativeProvider.checkedAt,
1892 );
1881 yield* PubSub.publish(changes, authoritativeProvider);1893 yield* PubSub.publish(changes, authoritativeProvider);
18821894 yield* Fiber.join(authoritativePersisted);
1883 let cachedProvider = yield* readProviderStatusCache(filePath);1895 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
1894 assert.deepStrictEqual(cachedProvider?.models, [authoritativeProvider.models[0]!]);1897 assert.deepStrictEqual(cachedProvider?.models, [authoritativeProvider.models[0]!]);
18951898
1899 const failedPersisted = yield* awaitPersistedProvider(
1900 registry,
1901 failedProvider.checkedAt,
1902 );
1896 yield* PubSub.publish(changes, failedProvider);1903 yield* PubSub.publish(changes, failedProvider);
1897 for (1904 yield* Fiber.join(failedPersisted);
1898 let attempt = 0;1905 cachedProvider = yield* readProviderStatusCache(filePath);
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 }
19061906
1907 assert.deepStrictEqual(cachedProvider?.models, [authoritativeProvider.models[0]!]);1907 assert.deepStrictEqual(cachedProvider?.models, [authoritativeProvider.models[0]!]);
1908 assert.deepStrictEqual((yield* registry.getProviders)[0]?.models, [1908 assert.deepStrictEqual((yield* registry.getProviders)[0]?.models, [
...@@ -2715,9 +2715,10 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te...@@ -2715,9 +2715,10 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
2715 claudeCapabilities(),2715 claudeCapabilities(),
2716 );2716 );
2717 assert.strictEqual(status.status, "ready");2717 assert.strictEqual(status.status, "ready");
2718 // The home is resolved through the host Path before it reaches the env.
2718 assert.deepStrictEqual(2719 assert.deepStrictEqual(
2719 recorded.commands.map((command) => command.env?.CLAUDE_CONFIG_DIR),2720 recorded.commands.map((command) => command.env?.CLAUDE_CONFIG_DIR),
2720 [claudeConfigDir],2721 [(yield* Path.Path).resolve(claudeConfigDir)],
2721 );2722 );
2722 }).pipe(Effect.provide(recorded.layer));2723 }).pipe(Effect.provide(recorded.layer));
2723 });2724 });
apps/server/src/server.test.ts+10-1
...@@ -1855,12 +1855,18 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -1855,12 +1855,18 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
1855 const staticDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-static-replace-" });1855 const staticDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-static-replace-" });
1856 const beforeOpenPath = path.join(staticDir, "before-open.txt");1856 const beforeOpenPath = path.join(staticDir, "before-open.txt");
1857 const afterOpenPath = path.join(staticDir, "after-open.txt");1857 const afterOpenPath = path.join(staticDir, "after-open.txt");
1858 const afterOpenSnapshotPath = path.join(staticDir, "after-open-snapshot.txt");
1859 const windowsHost = HostProcessPlatform.defaultValue() === "win32";
1858 const original = "original bytes";1860 const original = "original bytes";
1859 const replacement = "replacement bytes with a different size";1861 const replacement = "replacement bytes with a different size";
1860 for (const filePath of [beforeOpenPath, afterOpenPath]) {1862 for (const filePath of [beforeOpenPath, afterOpenPath]) {
1861 yield* fileSystem.writeFileString(filePath, original);1863 yield* fileSystem.writeFileString(filePath, original);
1862 yield* fileSystem.writeFileString(`${filePath}.next`, replacement);1864 yield* fileSystem.writeFileString(`${filePath}.next`, replacement);
1863 }1865 }
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 }
1864 const replaced = new Set<string>();1870 const replaced = new Set<string>();
1865 const replaceOnce = Effect.fnUntraced(function* (filePath: string) {1871 const replaceOnce = Effect.fnUntraced(function* (filePath: string) {
1866 if (replaced.has(filePath)) return;1872 if (replaced.has(filePath)) return;
...@@ -1877,7 +1883,10 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -1877,7 +1883,10 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
1877 ),1883 ),
1878 open: (filePath, options) =>1884 open: (filePath, options) =>
1879 fileSystem1885 fileSystem
1880 .open(filePath, options)1886 .open(
1887 filePath === afterOpenPath && windowsHost ? afterOpenSnapshotPath : filePath,
1888 options,
1889 )
1881 .pipe(1890 .pipe(
1882 Effect.tap(() => (filePath === afterOpenPath ? replaceOnce(filePath) : Effect.void)),1891 Effect.tap(() => (filePath === afterOpenPath ? replaceOnce(filePath) : Effect.void)),
1883 ),1892 ),
apps/server/src/sourceControl/SourceControlRepositoryService.test.ts+3-1
...@@ -3,6 +3,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices";...@@ -3,6 +3,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
3import { assert, it } from "@effect/vitest";3import { assert, it } from "@effect/vitest";
4import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";5import * as FileSystem from "effect/FileSystem";
6import * as Path from "effect/Path";
6import * as Layer from "effect/Layer";7import * as Layer from "effect/Layer";
7import * as PlatformError from "effect/PlatformError";8import * as PlatformError from "effect/PlatformError";
8import { ChildProcessSpawner } from "effect/unstable/process";9import { ChildProcessSpawner } from "effect/unstable/process";
...@@ -153,10 +154,11 @@ it.effect("preserves provider failures without deriving the repository message f...@@ -153,10 +154,11 @@ it.effect("preserves provider failures without deriving the repository message f
153it.effect("clones a looked-up repository into the requested destination", () =>154it.effect("clones a looked-up repository into the requested destination", () =>
154 Effect.gen(function* () {155 Effect.gen(function* () {
155 const fs = yield* FileSystem.FileSystem;156 const fs = yield* FileSystem.FileSystem;
157 const path = yield* Path.Path;
156 const parent = yield* fs.makeTempDirectoryScoped({158 const parent = yield* fs.makeTempDirectoryScoped({
157 prefix: "t3-source-control-clone-parent-",159 prefix: "t3-source-control-clone-parent-",
158 });160 });
159 const destinationPath = `${parent}/t3code`;161 const destinationPath = path.join(parent, "t3code");
160 const cloneCalls: Array<{ cwd: string; args: ReadonlyArray<string> }> = [];162 const cloneCalls: Array<{ cwd: string; args: ReadonlyArray<string> }> = [];
161163
162 yield* Effect.gen(function* () {164 yield* Effect.gen(function* () {
apps/server/src/workspace/WorkspaceFileSystem.test.ts+23-19
...@@ -14,6 +14,7 @@ import * as VcsProcess from "../vcs/VcsProcess.ts";...@@ -14,6 +14,7 @@ import * as VcsProcess from "../vcs/VcsProcess.ts";
14import * as WorkspaceEntries from "./WorkspaceEntries.ts";14import * as WorkspaceEntries from "./WorkspaceEntries.ts";
15import * as WorkspaceFileSystem from "./WorkspaceFileSystem.ts";15import * as WorkspaceFileSystem from "./WorkspaceFileSystem.ts";
16import * as WorkspacePaths from "./WorkspacePaths.ts";16import * as WorkspacePaths from "./WorkspacePaths.ts";
17import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
17import { symlinksSupported } from "@t3tools/shared/testing/symlinks";18import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
1819
19const ProjectLayer = WorkspaceFileSystem.layer.pipe(20const ProjectLayer = WorkspaceFileSystem.layer.pipe(
...@@ -100,28 +101,31 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i...@@ -100,28 +101,31 @@ it.layer(TestLayer, { excludeTestServices: true })("WorkspaceFileSystemLive", (i
100 }),101 }),
101 );102 );
102103
103 it.effect("rejects a FIFO without blocking on open", () =>104 // Needs mkfifo; Windows has no FIFOs to reject.
104 Effect.gen(function* () {105 it.effect.skipIf(HostProcessPlatform.defaultValue() === "win32")(
105 const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem;106 "rejects a FIFO without blocking on open",
106 const path = yield* Path.Path;107 () =>
107 const cwd = yield* makeTempDir;108 Effect.gen(function* () {
108 const outsideDir = yield* makeTempDir;109 const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem;
109 const fifoPath = path.join(outsideDir, "pipe");110 const path = yield* Path.Path;
110 yield* Effect.promise(111 const cwd = yield* makeTempDir;
111 () =>112 const outsideDir = yield* makeTempDir;
112 new Promise<void>((resolve, reject) =>113 const fifoPath = path.join(outsideDir, "pipe");
113 NodeChildProcess.execFile("mkfifo", [fifoPath], (error) =>114 yield* Effect.promise(
114 error ? reject(error) : resolve(),115 () =>
116 new Promise<void>((resolve, reject) =>
117 NodeChildProcess.execFile("mkfifo", [fifoPath], (error) =>
118 error ? reject(error) : resolve(),
119 ),
115 ),120 ),
116 ),121 );
117 );
118122
119 const error = yield* workspaceFileSystem123 const error = yield* workspaceFileSystem
120 .readFile({ cwd, relativePath: fifoPath })124 .readFile({ cwd, relativePath: fifoPath })
121 .pipe(Effect.flip);125 .pipe(Effect.flip);
122126
123 expect(error).toBeInstanceOf(WorkspaceFileSystem.WorkspacePathNotFileError);127 expect(error).toBeInstanceOf(WorkspaceFileSystem.WorkspacePathNotFileError);
124 }),128 }),
125 );129 );
126130
127 it.effect("rejects reads outside the workspace root", () =>131 it.effect("rejects reads outside the workspace root", () =>