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 @@...@@ -1,3 +1,4 @@
1import "vite-plus/test/config";
1import { defineConfig } from "vite-plus";2import { defineConfig } from "vite-plus";
23
3import { loadRepoEnv } from "../../scripts/lib/public-config.ts";4import { loadRepoEnv } from "../../scripts/lib/public-config.ts";
...@@ -87,5 +88,6 @@ export default defineConfig({...@@ -87,5 +88,6 @@ export default defineConfig({
87 // The Windows lane runs workspace suites concurrently; filesystem-heavy88 // The Windows lane runs workspace suites concurrently; filesystem-heavy
88 // desktop integration tests can exceed Vitest's 5 second default there.89 // desktop integration tests can exceed Vitest's 5 second default there.
89 testTimeout: 15_000,90 testTimeout: 15_000,
91 setupFiles: ["../../packages/shared/src/testing/longTempDir.ts"],
90 },92 },
91});93});
apps/server/src/bootstrap.test.ts+38-33
...@@ -55,6 +55,8 @@ vi.mock("node:fs", async (importOriginal) => {...@@ -55,6 +55,8 @@ vi.mock("node:fs", async (importOriginal) => {
55 };55 };
56});56});
5757
58const windowsHost = HostProcessPlatform.defaultValue() === "win32";
59const nullDevice = windowsHost ? "\\\\.\\NUL" : "/dev/null";
58const TestEnvelopeSchema = Schema.Struct({ mode: Schema.String });60const TestEnvelopeSchema = Schema.Struct({ mode: Schema.String });
59const encodeTestEnvelopeSchema = Schema.encodeEffect(Schema.fromJsonString(TestEnvelopeSchema));61const encodeTestEnvelopeSchema = Schema.encodeEffect(Schema.fromJsonString(TestEnvelopeSchema));
6062
...@@ -146,7 +148,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {...@@ -146,7 +148,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
146148
147 it.effect("returns none when the fd is unavailable", () =>149 it.effect("returns none when the fd is unavailable", () =>
148 Effect.gen(function* () {150 Effect.gen(function* () {
149 const fd = NodeFS.openSync("/dev/null", "r");151 const fd = NodeFS.openSync(nullDevice, "r");
150 NodeFS.closeSync(fd);152 NodeFS.closeSync(fd);
151153
152 const payload = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, { timeoutMs: 100 });154 const payload = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, { timeoutMs: 100 });
...@@ -157,7 +159,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {...@@ -157,7 +159,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
157 it.effect("preserves fd and cause when stat fails for a non-availability reason", () =>159 it.effect("preserves fd and cause when stat fails for a non-availability reason", () =>
158 Effect.gen(function* () {160 Effect.gen(function* () {
159 const fd = yield* Effect.acquireRelease(161 const fd = yield* Effect.acquireRelease(
160 Effect.sync(() => NodeFS.openSync("/dev/null", "r")),162 Effect.sync(() => NodeFS.openSync(nullDevice, "r")),
161 (fd) => Effect.sync(() => NodeFS.closeSync(fd)),163 (fd) => Effect.sync(() => NodeFS.closeSync(fd)),
162 );164 );
163165
...@@ -201,40 +203,43 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {...@@ -201,40 +203,43 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => {
201 }),203 }),
202 );204 );
203205
204 it.effect("returns none when the bootstrap read times out before any value arrives", () =>206 // Needs a FIFO, which mkfifo creates; Windows has neither.
205 Effect.gen(function* () {207 it.effect.skipIf(windowsHost)(
206 const fs = yield* FileSystem.FileSystem;208 "returns none when the bootstrap read times out before any value arrives",
207 const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-bootstrap-" });209 () =>
208 const fifoPath = NodePath.join(tempDir, "bootstrap.pipe");210 Effect.gen(function* () {
209211 const fs = yield* FileSystem.FileSystem;
210 yield* Effect.sync(() => NodeChildProcess.execFileSync("mkfifo", [fifoPath]));212 const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-bootstrap-" });
211213 const fifoPath = NodePath.join(tempDir, "bootstrap.pipe");
212 const _writer = yield* Effect.acquireRelease(214
213 Effect.sync(() =>215 yield* Effect.sync(() => NodeChildProcess.execFileSync("mkfifo", [fifoPath]));
214 NodeChildProcess.spawn("sh", ["-c", 'exec 3>"$1"; sleep 60', "sh", fifoPath], {216
215 stdio: ["ignore", "ignore", "ignore"],217 const _writer = yield* Effect.acquireRelease(
216 }),218 Effect.sync(() =>
217 ),219 NodeChildProcess.spawn("sh", ["-c", 'exec 3>"$1"; sleep 60', "sh", fifoPath], {
218 (writer) =>220 stdio: ["ignore", "ignore", "ignore"],
219 Effect.sync(() => {221 }),
220 writer.kill("SIGKILL");222 ),
221 }),223 (writer) =>
222 );224 Effect.sync(() => {
225 writer.kill("SIGKILL");
226 }),
227 );
223228
224 const fd = yield* Effect.acquireRelease(229 const fd = yield* Effect.acquireRelease(
225 Effect.sync(() => NodeFS.openSync(fifoPath, "r")),230 Effect.sync(() => NodeFS.openSync(fifoPath, "r")),
226 (fd) => Effect.sync(() => NodeFS.closeSync(fd)),231 (fd) => Effect.sync(() => NodeFS.closeSync(fd)),
227 );232 );
228233
229 const fiber = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, {234 const fiber = yield* readBootstrapEnvelope(TestEnvelopeSchema, fd, {
230 timeoutMs: 100,235 timeoutMs: 100,
231 }).pipe(Effect.forkScoped);236 }).pipe(Effect.forkScoped);
232237
233 yield* Effect.yieldNow;238 yield* Effect.yieldNow;
234 yield* TestClock.adjust(Duration.millis(100));239 yield* TestClock.adjust(Duration.millis(100));
235240
236 const payload = yield* Fiber.join(fiber);241 const payload = yield* Fiber.join(fiber);
237 assertNone(payload);242 assertNone(payload);
238 }).pipe(Effect.provide(TestClock.layer())),243 }).pipe(Effect.provide(TestClock.layer())),
239 );244 );
240});245});
apps/server/vite.config.ts+1
...@@ -78,6 +78,7 @@ export default mergeConfig(...@@ -78,6 +78,7 @@ export default mergeConfig(
78 // The server suite exercises sqlite, git, temp worktrees, and orchestration78 // The server suite exercises sqlite, git, temp worktrees, and orchestration
79 // runtimes heavily. Running files in parallel introduces load-sensitive flakes.79 // runtimes heavily. Running files in parallel introduces load-sensitive flakes.
80 fileParallelism: false,80 fileParallelism: false,
81 // Appended to the root setup, which mergeConfig concatenates.
81 setupFiles: ["./src/testUtils/gitConfig.setup.ts"],82 setupFiles: ["./src/testUtils/gitConfig.setup.ts"],
82 // Server integration tests exercise sqlite, git, and orchestration together.83 // Server integration tests exercise sqlite, git, and orchestration together.
83 // Under package-wide runs they can exceed the default budget on loaded CI hosts.84 // 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 = {...@@ -80,6 +80,7 @@ const unitTestProject = {
80 // run, those async tests can exceed Vitest's default 5s budget.80 // run, those async tests can exceed Vitest's default 5s budget.
81 hookTimeout: 15_000,81 hookTimeout: 15_000,
82 testTimeout: 15_000,82 testTimeout: 15_000,
83 setupFiles: ["../../packages/shared/src/testing/longTempDir.ts"],
83 },84 },
84} satisfies TestProjectInlineConfiguration;85} satisfies TestProjectInlineConfiguration;
8586
packages/client-runtime/vite.config.ts+1
...@@ -5,5 +5,6 @@ export default defineConfig({...@@ -5,5 +5,6 @@ export default defineConfig({
5 test: {5 test: {
6 environment: "node",6 environment: "node",
7 include: ["src/**/*.test.ts"],7 include: ["src/**/*.test.ts"],
8 setupFiles: ["../shared/src/testing/longTempDir.ts"],
8 },9 },
9});10});
packages/shared/package.json+4
...@@ -223,6 +223,10 @@...@@ -223,6 +223,10 @@
223 "types": "./src/hostProcess.ts",223 "types": "./src/hostProcess.ts",
224 "import": "./src/hostProcess.ts"224 "import": "./src/hostProcess.ts"
225 },225 },
226 "./testing/longTempDir": {
227 "types": "./src/testing/longTempDir.ts",
228 "import": "./src/testing/longTempDir.ts"
229 },
226 "./testing/symlinks": {230 "./testing/symlinks": {
227 "types": "./src/testing/symlinks.ts",231 "types": "./src/testing/symlinks.ts",
228 "import": "./src/testing/symlinks.ts"232 "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({...@@ -19,6 +19,11 @@ export default defineConfig({
19 ],19 ],
20 hookTimeout: 60_000,20 hookTimeout: 60_000,
21 testTimeout: 60_000,21 testTimeout: 60_000,
22 setupFiles: [
23 NodeURL.fileURLToPath(
24 new URL("./packages/shared/src/testing/longTempDir.ts", import.meta.url),
25 ),
26 ],
22 },27 },
23 staged: {28 staged: {
24 // Formatter only for now — no lint or typecheck on commit.29 // Formatter only for now — no lint or typecheck on commit.