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

test: resolve the Windows temp directory to its long name (#9573)

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

8 files changed, 71 insertions(+), 33 deletions(-)

apps/desktop/vite.config.ts+2
......@@ -1,3 +1,4 @@
1import "vite-plus/test/config";
12import { defineConfig } from "vite-plus";
23
34import { loadRepoEnv } from "../../scripts/lib/public-config.ts";
......@@ -87,5 +88,6 @@ export default defineConfig({
8788 // The Windows lane runs workspace suites concurrently; filesystem-heavy
8889 // desktop integration tests can exceed Vitest's 5 second default there.
8990 testTimeout: 15_000,
91 setupFiles: ["../../packages/shared/src/testing/longTempDir.ts"],
9092 },
9193});
apps/server/src/bootstrap.test.ts+38-33
......@@ -55,6 +55,8 @@ vi.mock("node:fs", async (importOriginal) => {
5555 };
5656});
5757
58const windowsHost = HostProcessPlatform.defaultValue() === "win32";
59const nullDevice = windowsHost ? "\\\\.\\NUL" : "/dev/null";
5860const TestEnvelopeSchema = Schema.Struct({ mode: Schema.String });
5961const encodeTestEnvelopeSchema = Schema.encodeEffect(Schema.fromJsonString(TestEnvelopeSchema));
6062
......@@ -146,7 +148,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
146148
147149 it.effect("returns none when the fd is unavailable", () =>
148150 Effect.gen(function* () {
149 const fd = NodeFS.openSync("/dev/null", "r");
151 const fd = NodeFS.openSync(nullDevice, "r");
150152 NodeFS.closeSync(fd);
151153
152154 const payload = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, { timeoutMs: 100 });
......@@ -157,7 +159,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
157159 it.effect("preserves fd and cause when stat fails for a non-availability reason", () =>
158160 Effect.gen(function* () {
159161 const fd = yield* Effect.acquireRelease(
160 Effect.sync(() => NodeFS.openSync("/dev/null", "r")),
162 Effect.sync(() => NodeFS.openSync(nullDevice, "r")),
161163 (fd) => Effect.sync(() => NodeFS.closeSync(fd)),
162164 );
163165
......@@ -201,40 +203,43 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
201203 }),
202204 );
203205
204 it.effect("returns none when the bootstrap read times out before any value arrives", () =>
205 Effect.gen(function* () {
206 const fs = yield* FileSystem.FileSystem;
207 const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-bootstrap-" });
208 const fifoPath = NodePath.join(tempDir, "bootstrap.pipe");
209
210 yield* Effect.sync(() => NodeChildProcess.execFileSync("mkfifo", [fifoPath]));
211
212 const _writer = yield* Effect.acquireRelease(
213 Effect.sync(() =>
214 NodeChildProcess.spawn("sh", ["-c", 'exec 3>"$1"; sleep 60', "sh", fifoPath], {
215 stdio: ["ignore", "ignore", "ignore"],
216 }),
217 ),
218 (writer) =>
219 Effect.sync(() => {
220 writer.kill("SIGKILL");
221 }),
222 );
206 // Needs a FIFO, which mkfifo creates; Windows has neither.
207 it.effect.skipIf(windowsHost)(
208 "returns none when the bootstrap read times out before any value arrives",
209 () =>
210 Effect.gen(function* () {
211 const fs = yield* FileSystem.FileSystem;
212 const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-bootstrap-" });
213 const fifoPath = NodePath.join(tempDir, "bootstrap.pipe");
214
215 yield* Effect.sync(() => NodeChildProcess.execFileSync("mkfifo", [fifoPath]));
216
217 const _writer = yield* Effect.acquireRelease(
218 Effect.sync(() =>
219 NodeChildProcess.spawn("sh", ["-c", 'exec 3>"$1"; sleep 60', "sh", fifoPath], {
220 stdio: ["ignore", "ignore", "ignore"],
221 }),
222 ),
223 (writer) =>
224 Effect.sync(() => {
225 writer.kill("SIGKILL");
226 }),
227 );
223228
224 const fd = yield* Effect.acquireRelease(
225 Effect.sync(() => NodeFS.openSync(fifoPath, "r")),
226 (fd) => Effect.sync(() => NodeFS.closeSync(fd)),
227 );
229 const fd = yield* Effect.acquireRelease(
230 Effect.sync(() => NodeFS.openSync(fifoPath, "r")),
231 (fd) => Effect.sync(() => NodeFS.closeSync(fd)),
232 );
228233
229 const fiber = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, {
230 timeoutMs: 100,
231 }).pipe(Effect.forkScoped);
234 const fiber = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, {
235 timeoutMs: 100,
236 }).pipe(Effect.forkScoped);
232237
233 yield* Effect.yieldNow;
234 yield* TestClock.adjust(Duration.millis(100));
238 yield* Effect.yieldNow;
239 yield* TestClock.adjust(Duration.millis(100));
235240
236 const payload = yield* Fiber.join(fiber);
237 assertNone(payload);
238 }).pipe(Effect.provide(TestClock.layer())),
241 const payload = yield* Fiber.join(fiber);
242 assertNone(payload);
243 }).pipe(Effect.provide(TestClock.layer())),
239244 );
240245});
apps/server/vite.config.ts+1
......@@ -78,6 +78,7 @@ export default mergeConfig(
7878 // The server suite exercises sqlite, git, temp worktrees, and orchestration
7979 // runtimes heavily. Running files in parallel introduces load-sensitive flakes.
8080 fileParallelism: false,
81 // Appended to the root setup, which mergeConfig concatenates.
8182 setupFiles: ["./src/testUtils/gitConfig.setup.ts"],
8283 // Server integration tests exercise sqlite, git, and orchestration together.
8384 // Under package-wide runs they can exceed the default budget on loaded CI hosts.
apps/web/vite.config.ts+1
......@@ -80,6 +80,7 @@ const unitTestProject = {
8080 // run, those async tests can exceed Vitest's default 5s budget.
8181 hookTimeout: 15_000,
8282 testTimeout: 15_000,
83 setupFiles: ["../../packages/shared/src/testing/longTempDir.ts"],
8384 },
8485} satisfies TestProjectInlineConfiguration;
8586
packages/client-runtime/vite.config.ts+1
......@@ -5,5 +5,6 @@ export default defineConfig({
55 test: {
66 environment: "node",
77 include: ["src/**/*.test.ts"],
8 setupFiles: ["../shared/src/testing/longTempDir.ts"],
89 },
910});
packages/shared/package.json+4
......@@ -223,6 +223,10 @@
223223 "types": "./src/hostProcess.ts",
224224 "import": "./src/hostProcess.ts"
225225 },
226 "./testing/longTempDir": {
227 "types": "./src/testing/longTempDir.ts",
228 "import": "./src/testing/longTempDir.ts"
229 },
226230 "./testing/symlinks": {
227231 "types": "./src/testing/symlinks.ts",
228232 "import": "./src/testing/symlinks.ts"
packages/shared/src/testing/longTempDir.ts created+19
......@@ -0,0 +1,19 @@
1// @effect-diagnostics nodeBuiltinImport:off - runs once at test setup, outside any Effect runtime.
2import * as NodeFS from "node:fs";
3import * as NodeOS from "node:os";
4import { HostProcessPlatform } from "../hostProcess.ts";
5
6// GitHub's Windows runners hand out the temp directory by its 8.3 short name
7// (C:\Users\RUNNER~1\...). Anything that canonicalises a path, such as git or
8// realpath, reports the long form, so equality checks between a temp path and
9// its canonical form fail. Node reads TEMP/TMP on every os.tmpdir() call, so
10// pointing them at the long form fixes every temp directory the suite makes.
11if (HostProcessPlatform.defaultValue() === "win32") {
12 try {
13 const longForm = NodeFS.realpathSync.native(NodeOS.tmpdir());
14 process.env.TEMP = longForm;
15 process.env.TMP = longForm;
16 } catch {
17 // Leave the host's value alone if it cannot be resolved.
18 }
19}
vite.config.ts+5
......@@ -19,6 +19,11 @@ export default defineConfig({
1919 ],
2020 hookTimeout: 60_000,
2121 testTimeout: 60_000,
22 setupFiles: [
23 NodeURL.fileURLToPath(
24 new URL("./packages/shared/src/testing/longTempDir.ts", import.meta.url),
25 ),
26 ],
2227 },
2328 staged: {
2429 // Formatter only for now — no lint or typecheck on commit.