| author | |
| committer | |
| log | 9fa54eeb4c481b444c68e38d00066d707af5f677 |
| tree | 2532fa12aab7dda81f451d398ba4f1cb8564c26b |
| parent | 9af5139f5974281a59f5cd62c11d918d62126313 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 @@ |
| 1 | import "vite-plus/test/config"; | ||
| 1 | import { defineConfig } from "vite-plus"; | 2 | import { defineConfig } from "vite-plus"; |
| 2 | 3 | ||
| 3 | import { loadRepoEnv } from "../../scripts/lib/public-config.ts"; | 4 | import { 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-heavy | 88 | // 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 | }); |
| 57 | 57 | ||
| 58 | const windowsHost = HostProcessPlatform.defaultValue() === "win32"; | ||
| 59 | const nullDevice = windowsHost ? "\\\\.\\NUL" : "/dev/null"; | ||
| 58 | const TestEnvelopeSchema = Schema.Struct({ mode: Schema.String }); | 60 | const TestEnvelopeSchema = Schema.Struct({ mode: Schema.String }); |
| 59 | const encodeTestEnvelopeSchema = Schema.encodeEffect(Schema.fromJsonString(TestEnvelopeSchema)); | 61 | const encodeTestEnvelopeSchema = Schema.encodeEffect(Schema.fromJsonString(TestEnvelopeSchema)); |
| 60 | 62 | ||
| ... | @@ -146,7 +148,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => { | ... | @@ -146,7 +148,7 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => { |
| 146 | 148 | ||
| 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); |
| 151 | 153 | ||
| 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 | ); |
| 163 | 165 | ||
| ... | @@ -201,40 +203,43 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => { | ... | @@ -201,40 +203,43 @@ it.layer(NodeServices.layer)("readBootstrapEnvelope", (it) => { |
| 201 | }), | 203 | }), |
| 202 | ); | 204 | ); |
| 203 | 205 | ||
| 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* () { |
| 209 | 211 | const fs = yield* FileSystem.FileSystem; | |
| 210 | yield* Effect.sync(() => NodeChildProcess.execFileSync("mkfifo", [fifoPath])); | 212 | const tempDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-bootstrap-" }); |
| 211 | 213 | 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 | ); | ||
| 223 | 228 | ||
| 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 | ); |
| 228 | 233 | ||
| 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); |
| 232 | 237 | ||
| 233 | yield* Effect.yieldNow; | 238 | yield* Effect.yieldNow; |
| 234 | yield* TestClock.adjust(Duration.millis(100)); | 239 | yield* TestClock.adjust(Duration.millis(100)); |
| 235 | 240 | ||
| 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 orchestration | 78 | // 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; |
| 85 | 86 |
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. | ||
| 2 | import * as NodeFS from "node:fs"; | ||
| 3 | import * as NodeOS from "node:os"; | ||
| 4 | import { 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. | ||
| 11 | if (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. |