diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts index f0829d4e654f882040c95d580bb83abbb5ef804c..294a02030a83f2e4e4da0bc08e6be6208bb31290 100644 --- a/apps/desktop/vite.config.ts +++ b/apps/desktop/vite.config.ts @@ -1,3 +1,4 @@ +import "vite-plus/test/config"; import { defineConfig } from "vite-plus"; import { loadRepoEnv } from "../../scripts/lib/public-config.ts"; @@ -87,5 +88,6 @@ export default defineConfig({ // The Windows lane runs workspace suites concurrently; filesystem-heavy // desktop integration tests can exceed Vitest's 5 second default there. testTimeout: 15_000, + setupFiles: ["../../packages/shared/src/testing/longTempDir.ts"], }, }); diff --git a/apps/server/src/bootstrap.test.ts b/apps/server/src/bootstrap.test.ts index 05155f32ec4cadcf0ab004953f6b35e6800f72d6..722eb21a24612cbbb1612a2d7e99a04270481740 100644 --- a/apps/server/src/bootstrap.test.ts +++ b/apps/server/src/bootstrap.test.ts @@ -55,6 +55,8 @@ vi.mock("node:fs", async (importOriginal) => { }; }); +const windowsHost = HostProcessPlatform.defaultValue() === "win32"; +const nullDevice = windowsHost ? "\\\\.\\NUL" : "/dev/null"; const TestEnvelopeSchema = Schema.Struct({ mode: Schema.String }); const encodeTestEnvelopeSchema = Schema.encodeEffect(Schema.fromJsonString(TestEnvelopeSchema)); @@ -146,7 +148,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => { it.effect("returns none when the fd is unavailable", () => Effect.gen(function* () { - const fd = NodeFS.openSync("/dev/null", "r"); + const fd = NodeFS.openSync(nullDevice, "r"); NodeFS.closeSync(fd); const payload = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, { timeoutMs: 100 }); @@ -157,7 +159,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => { it.effect("preserves fd and cause when stat fails for a non-availability reason", () => Effect.gen(function* () { const fd = yield* Effect.acquireRelease( - Effect.sync(() => NodeFS.openSync("/dev/null", "r")), + Effect.sync(() => NodeFS.openSync(nullDevice, "r")), (fd) => Effect.sync(() => NodeFS.closeSync(fd)), ); @@ -201,40 +203,43 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => { }), ); - it.effect("returns none when the bootstrap read times out before any value arrives", () => - Effect.gen(function* () { - const fs = yield* FileSystem.FileSystem; - const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-bootstrap-" }); - const fifoPath = NodePath.join(tempDir, "bootstrap.pipe"); + // Needs a FIFO, which mkfifo creates; Windows has neither. + it.effect.skipIf(windowsHost)( + "returns none when the bootstrap read times out before any value arrives", + () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-bootstrap-" }); + const fifoPath = NodePath.join(tempDir, "bootstrap.pipe"); - yield* Effect.sync(() => NodeChildProcess.execFileSync("mkfifo", [fifoPath])); + yield* Effect.sync(() => NodeChildProcess.execFileSync("mkfifo", [fifoPath])); - const _writer = yield* Effect.acquireRelease( - Effect.sync(() => - NodeChildProcess.spawn("sh", ["-c", 'exec 3>"$1"; sleep 60', "sh", fifoPath], { - stdio: ["ignore", "ignore", "ignore"], - }), - ), - (writer) => - Effect.sync(() => { - writer.kill("SIGKILL"); - }), - ); + const _writer = yield* Effect.acquireRelease( + Effect.sync(() => + NodeChildProcess.spawn("sh", ["-c", 'exec 3>"$1"; sleep 60', "sh", fifoPath], { + stdio: ["ignore", "ignore", "ignore"], + }), + ), + (writer) => + Effect.sync(() => { + writer.kill("SIGKILL"); + }), + ); - const fd = yield* Effect.acquireRelease( - Effect.sync(() => NodeFS.openSync(fifoPath, "r")), - (fd) => Effect.sync(() => NodeFS.closeSync(fd)), - ); + const fd = yield* Effect.acquireRelease( + Effect.sync(() => NodeFS.openSync(fifoPath, "r")), + (fd) => Effect.sync(() => NodeFS.closeSync(fd)), + ); - const fiber = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, { - timeoutMs: 100, - }).pipe(Effect.forkScoped); + const fiber = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, { + timeoutMs: 100, + }).pipe(Effect.forkScoped); - yield* Effect.yieldNow; - yield* TestClock.adjust(Duration.millis(100)); + yield* Effect.yieldNow; + yield* TestClock.adjust(Duration.millis(100)); - const payload = yield* Fiber.join(fiber); - assertNone(payload); - }).pipe(Effect.provide(TestClock.layer())), + const payload = yield* Fiber.join(fiber); + assertNone(payload); + }).pipe(Effect.provide(TestClock.layer())), ); }); diff --git a/apps/server/vite.config.ts b/apps/server/vite.config.ts index 139373a94dcace08ef559d33699dac4f54463286..88c8c2f4d37fca8cf1be7b212a8226a0c887dd63 100644 --- a/apps/server/vite.config.ts +++ b/apps/server/vite.config.ts @@ -78,6 +78,7 @@ export default mergeConfig( // The server suite exercises sqlite, git, temp worktrees, and orchestration // runtimes heavily. Running files in parallel introduces load-sensitive flakes. fileParallelism: false, + // Appended to the root setup, which mergeConfig concatenates. setupFiles: ["./src/testUtils/gitConfig.setup.ts"], // Server integration tests exercise sqlite, git, and orchestration together. // Under package-wide runs they can exceed the default budget on loaded CI hosts. diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 6661736f062f6791047a19d7148f4522c56a4283..702c1884098311aeb1eb07bbc0886219ec1f00c9 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -80,6 +80,7 @@ const unitTestProject = { // run, those async tests can exceed Vitest's default 5s budget. hookTimeout: 15_000, testTimeout: 15_000, + setupFiles: ["../../packages/shared/src/testing/longTempDir.ts"], }, } satisfies TestProjectInlineConfiguration; diff --git a/packages/client-runtime/vite.config.ts b/packages/client-runtime/vite.config.ts index fcdb97512160cd70bf08f39a514c50ac37bf5b92..7f4ff74eca29b8fc804f80a7dda2a9b2f7f20560 100644 --- a/packages/client-runtime/vite.config.ts +++ b/packages/client-runtime/vite.config.ts @@ -5,5 +5,6 @@ export default defineConfig({ test: { environment: "node", include: ["src/**/*.test.ts"], + setupFiles: ["../shared/src/testing/longTempDir.ts"], }, }); diff --git a/packages/shared/package.json b/packages/shared/package.json index a5ce349a4c0081367f4b5c9a87192c238f3400a5..efbc62320a525dcdb4d1a6e257df5401175999b2 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -223,6 +223,10 @@ "types": "./src/hostProcess.ts", "import": "./src/hostProcess.ts" }, + "./testing/longTempDir": { + "types": "./src/testing/longTempDir.ts", + "import": "./src/testing/longTempDir.ts" + }, "./testing/symlinks": { "types": "./src/testing/symlinks.ts", "import": "./src/testing/symlinks.ts" diff --git a/packages/shared/src/testing/longTempDir.ts b/packages/shared/src/testing/longTempDir.ts new file mode 100644 index 0000000000000000000000000000000000000000..8a18abb8ce42aa6cc33dabd5794e00102812c9e0 --- /dev/null +++ b/packages/shared/src/testing/longTempDir.ts @@ -0,0 +1,19 @@ +// @effect-diagnostics nodeBuiltinImport:off - runs once at test setup, outside any Effect runtime. +import * as NodeFS from "node:fs"; +import * as NodeOS from "node:os"; +import { HostProcessPlatform } from "../hostProcess.ts"; + +// GitHub's Windows runners hand out the temp directory by its 8.3 short name +// (C:\Users\RUNNER~1\...). Anything that canonicalises a path, such as git or +// realpath, reports the long form, so equality checks between a temp path and +// its canonical form fail. Node reads TEMP/TMP on every os.tmpdir() call, so +// pointing them at the long form fixes every temp directory the suite makes. +if (HostProcessPlatform.defaultValue() === "win32") { + try { + const longForm = NodeFS.realpathSync.native(NodeOS.tmpdir()); + process.env.TEMP = longForm; + process.env.TMP = longForm; + } catch { + // Leave the host's value alone if it cannot be resolved. + } +} diff --git a/vite.config.ts b/vite.config.ts index d3aec8e9764b22467edfca14c272ce47ed827af6..258555e04b5a3243050272340b16eb288f39a317 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -19,6 +19,11 @@ export default defineConfig({ ], hookTimeout: 60_000, testTimeout: 60_000, + setupFiles: [ + NodeURL.fileURLToPath( + new URL("./packages/shared/src/testing/longTempDir.ts", import.meta.url), + ), + ], }, staged: { // Formatter only for now — no lint or typecheck on commit.