diff --git a/apps/desktop/src/app/DesktopAppIdentity.test.ts b/apps/desktop/src/app/DesktopAppIdentity.test.ts index 5c39ff304b3b67445db5a46aaa8ba13d3bcb6574..71bcf5f7aef1c4b5753d2882e325180b9fdfd797 100644 --- a/apps/desktop/src/app/DesktopAppIdentity.test.ts +++ b/apps/desktop/src/app/DesktopAppIdentity.test.ts @@ -1,3 +1,4 @@ +import * as NodePath from "@effect/platform-node/NodePath"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, describe, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; @@ -88,6 +89,7 @@ const makeEnvironmentLayer = (overrides: TestEnvironmentInput = {}) => { Layer.provide( Layer.mergeAll( NodeServices.layer, + NodePath.layerPosix, DesktopConfig.layerTest({ ...env, }), diff --git a/apps/desktop/src/app/DesktopAssets.test.ts b/apps/desktop/src/app/DesktopAssets.test.ts index bb118d43d29a9d77d5685a4cc120d9851a12cf8f..78819d06e6cc9e4d644e73e06935f2beab4a0069 100644 --- a/apps/desktop/src/app/DesktopAssets.test.ts +++ b/apps/desktop/src/app/DesktopAssets.test.ts @@ -1,3 +1,4 @@ +import * as NodePath from "@effect/platform-node/NodePath"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, describe, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; @@ -20,7 +21,11 @@ const environmentLayer = DesktopEnvironment.layer({ isPackaged: true, resourcesPath: "/Applications/T3 Code.app/Contents/Resources", runningUnderArm64Translation: false, -}).pipe(Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({})))); +}).pipe( + Layer.provide( + Layer.mergeAll(NodeServices.layer, NodePath.layerPosix, DesktopConfig.layerTest({})), + ), +); describe("DesktopAssets", () => { it.effect("uses canonical source-tree icons for unpackaged development", () => @@ -39,6 +44,7 @@ describe("DesktopAssets", () => { Layer.provide( Layer.mergeAll( NodeServices.layer, + NodePath.layerPosix, DesktopConfig.layerTest({ VITE_DEV_SERVER_URL: "http://localhost:5733" }), ), ), diff --git a/apps/desktop/src/app/DesktopEnvironment.test.ts b/apps/desktop/src/app/DesktopEnvironment.test.ts index 218e2c3e4ba2dc62b5bc1f25ccd90dfdf0170db6..89cc592831a7bc67626cc964267239210cec504f 100644 --- a/apps/desktop/src/app/DesktopEnvironment.test.ts +++ b/apps/desktop/src/app/DesktopEnvironment.test.ts @@ -1,3 +1,4 @@ +import * as NodePath from "@effect/platform-node/NodePath"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, describe, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; @@ -26,7 +27,11 @@ const makeEnvironmentLayer = ( DesktopEnvironment.layer({ ...defaultInput, ...overrides, - }).pipe(Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest(env)))); + }).pipe( + Layer.provide( + Layer.mergeAll(NodeServices.layer, NodePath.layerPosix, DesktopConfig.layerTest(env)), + ), + ); const makeEnvironment = ( overrides: Partial = {}, diff --git a/apps/server/src/auth/ServerSecretStore.test.ts b/apps/server/src/auth/ServerSecretStore.test.ts index d4411fb9f3b7b8e9b1f5253ec405bd9b02d84a62..6b7850aa363bddcee46ea4fb22159fe315daebb0 100644 --- a/apps/server/src/auth/ServerSecretStore.test.ts +++ b/apps/server/src/auth/ServerSecretStore.test.ts @@ -109,7 +109,7 @@ const ConcurrentReadMissFileSystemLayer = Layer.effect( return { ...fileSystem, readFile: (path) => - String(path).endsWith("/session-signing-key.bin") + /[\\/]session-signing-key\.bin$/.test(String(path)) ? Ref.updateAndGet(readCountRef, (count) => count + 1).pipe( Effect.flatMap((count) => { if (count > 2) { @@ -219,7 +219,7 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => { yield* secretStore.set("session-signing-key", Uint8Array.from([1, 2, 3])); assert.isTrue( - chmodCalls.some((call) => call.mode === 0o700 && call.path.endsWith("/secrets")), + chmodCalls.some((call) => call.mode === 0o700 && /[\\/]secrets$/.test(call.path)), ); assert.isAtLeast(chmodCalls.filter((call) => call.mode === 0o600).length, 2); }).pipe(Effect.provide(NodeServices.layer)), diff --git a/apps/server/src/project/ProjectFaviconResolver.test.ts b/apps/server/src/project/ProjectFaviconResolver.test.ts index 2c7b0f7bdc62c9382dc46e549db943bce2f2aab4..b8d6b46da088921eebed968e9193d28ec3b66fa0 100644 --- a/apps/server/src/project/ProjectFaviconResolver.test.ts +++ b/apps/server/src/project/ProjectFaviconResolver.test.ts @@ -54,11 +54,12 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => { it.effect("serves repeated resolves from cache instead of re-walking candidates", () => Effect.gen(function* () { const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; + const path = yield* Path.Path; const cwd = yield* makeTempDir; yield* writeTextFile(cwd, "public/favicon.svg", "public"); const resolved = yield* resolver.resolvePath(cwd); - expect(resolved?.endsWith("public/favicon.svg")).toBe(true); + expect(resolved?.endsWith(path.join("public", "favicon.svg"))).toBe(true); // `favicon.svg` outranks `public/favicon.svg`, so a resolver that walked // the candidate list again would switch to it. Staying on the original @@ -71,7 +72,7 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => { yield* TestClock.adjust(Duration.minutes(11)); - expect((yield* resolver.resolvePath(cwd))?.endsWith("/favicon.svg")).toBe(true); + expect(yield* resolver.resolvePath(cwd)).toBe(path.join(cwd, "favicon.svg")); expect(yield* resolver.resolvePath(cwd)).not.toBe(resolved); }).pipe(Effect.provide(TestClock.layer())), ); diff --git a/apps/server/src/provider/Drivers/ClaudeSkills.test.ts b/apps/server/src/provider/Drivers/ClaudeSkills.test.ts index 3e46ba94df03f174ffb86aea01b91a5d42023529..d126a15c12b8a9757f30c395aab846d12cc8e212 100644 --- a/apps/server/src/provider/Drivers/ClaudeSkills.test.ts +++ b/apps/server/src/provider/Drivers/ClaudeSkills.test.ts @@ -1,3 +1,4 @@ +import * as NodePath from "@effect/platform-node/NodePath"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; @@ -492,7 +493,11 @@ it.layer(NodeServices.layer)("discoverClaudeSkills", (it) => { it.effect("lets the administrator's managed policy outrank every other settings file", () => Effect.gen(function* () { - const path = yield* Path.Path; + // The function is pure on its `path` argument, so hand it the + // implementation matching each platform under test rather than the + // host's. + const path = yield* Path.Path.pipe(Effect.provide(NodePath.layerPosix)); + const win32Path = yield* Path.Path.pipe(Effect.provide(NodePath.layerWin32)); for (const [platform, expected] of [ ["darwin", "/Library/Application Support/ClaudeCode/managed-settings.json"], @@ -508,14 +513,15 @@ it.layer(NodeServices.layer)("discoverClaudeSkills", (it) => { } assert.deepEqual( - skillOverrideSettingsPaths(path, "/home/.claude", undefined, "win32", { - PROGRAMDATA: "C:/ProgramData", + skillOverrideSettingsPaths(win32Path, "C:\\Users\\me\\.claude", undefined, "win32", { + PROGRAMDATA: "C:\\ProgramData", }).at(-1), - "C:/ProgramData/ClaudeCode/managed-settings.json", + "C:\\ProgramData\\ClaudeCode\\managed-settings.json", + ); + assert.deepEqual( + skillOverrideSettingsPaths(win32Path, "C:\\Users\\me\\.claude", undefined, "win32", {}), + ["C:\\Users\\me\\.claude\\settings.json"], ); - assert.deepEqual(skillOverrideSettingsPaths(path, "/home/.claude", undefined, "win32", {}), [ - "/home/.claude/settings.json", - ]); // Only the repository root's local file joins in, after the // workspace's own local file so it wins. diff --git a/packages/ssh/src/config.test.ts b/packages/ssh/src/config.test.ts index 0446370337ad8a0bae63838e8391f8a94c89fd4f..4fd3b7636d3f67c2ac238747f2e8234f6e5d742d 100644 --- a/packages/ssh/src/config.test.ts +++ b/packages/ssh/src/config.test.ts @@ -1,3 +1,4 @@ +import * as NodePath from "@effect/platform-node/NodePath"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, describe, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; @@ -133,6 +134,6 @@ describe("ssh config", () => { "/tmp/home", ); assert.equal(pattern, "/tmp/home/.ssh/config.d/*.conf"); - }).pipe(Effect.provide(NodeServices.layer)), + }).pipe(Effect.provide(NodePath.layerPosix)), ); }); diff --git a/packages/tailscale/src/tailscale.test.ts b/packages/tailscale/src/tailscale.test.ts index 09d9def2106645b776fb386bfbfa08a848fce0e8..102153f6e2046898314d0f35504452e49b56adcc 100644 --- a/packages/tailscale/src/tailscale.test.ts +++ b/packages/tailscale/src/tailscale.test.ts @@ -8,6 +8,7 @@ import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; import * as TestClock from "effect/testing/TestClock"; import { ChildProcessSpawner } from "effect/unstable/process"; +import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { buildTailscaleHttpsBaseUrl, @@ -98,14 +99,22 @@ function neverFinishingMockHandle() { }); } +// The executable name depends on the host platform (`tailscale.exe` on +// Windows), so pin it: these tests assert the posix spelling. +function spawnerLayer(spawner: ChildProcessSpawner.ChildProcessSpawner["Service"]) { + return Layer.merge( + Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner), + Layer.succeed(HostProcessPlatform, "linux"), + ); +} + function mockSpawnerLayer( handler: ( command: string, args: ReadonlyArray, ) => { stdout?: string; stderr?: string; code?: number }, ) { - return Layer.succeed( - ChildProcessSpawner.ChildProcessSpawner, + return spawnerLayer( ChildProcessSpawner.make((command) => { const childProcess = command as unknown as { readonly command: string; @@ -194,10 +203,7 @@ describe("tailscale", () => { method: "spawn", cause: systemCause, }); - const layer = Layer.succeed( - ChildProcessSpawner.ChildProcessSpawner, - ChildProcessSpawner.make(() => Effect.fail(cause)), - ); + const layer = spawnerLayer(ChildProcessSpawner.make(() => Effect.fail(cause))); return Effect.gen(function* () { const error = yield* readTailscaleStatus.pipe(Effect.flip, Effect.provide(layer)); @@ -218,8 +224,7 @@ describe("tailscale", () => { // inside an `Effect.callback` registration, so that throw arrives as a // defect rather than a typed error - the shape reproduced here. const defect = Object.assign(new Error("spawn tailscale ENOTDIR"), { code: "ENOTDIR" }); - const layer = Layer.succeed( - ChildProcessSpawner.ChildProcessSpawner, + const layer = spawnerLayer( ChildProcessSpawner.make(() => Effect.callback(() => { throw defect; @@ -297,10 +302,7 @@ describe("tailscale", () => { it.effect("times out tailscale status through TestClock", () => { const layer = Layer.merge( TestClock.layer(), - Layer.succeed( - ChildProcessSpawner.ChildProcessSpawner, - ChildProcessSpawner.make(() => Effect.succeed(neverFinishingMockHandle())), - ), + spawnerLayer(ChildProcessSpawner.make(() => Effect.succeed(neverFinishingMockHandle()))), ); return Effect.gen(function* () { diff --git a/scripts/mobile-showcase.test.ts b/scripts/mobile-showcase.test.ts index 2fd4c1dd8edd3233e309a744404f703e1cc20cfc..a3f11e3a37c7997ecfa9d116455e52d2ea795146 100644 --- a/scripts/mobile-showcase.test.ts +++ b/scripts/mobile-showcase.test.ts @@ -1,3 +1,5 @@ +// @effect-diagnostics nodeBuiltinImport:off - expectations mirror the host path joins the script under test performs. +import * as NodePath from "node:path"; import { assert, it } from "@effect/vitest"; import { PNG } from "pngjs"; @@ -167,8 +169,14 @@ it("expands both appearances into independent upload-ready directories", () => { directory: showcaseCaptureDirectory("/captures", capture), })), [ - { appearance: "light", directory: "/captures/apple/iphone-test/light/t3-code" }, - { appearance: "dark", directory: "/captures/apple/iphone-test/dark/t3-code" }, + { + appearance: "light", + directory: NodePath.join("/captures", "apple", "iphone-test", "light", "t3-code"), + }, + { + appearance: "dark", + directory: NodePath.join("/captures", "apple", "iphone-test", "dark", "t3-code"), + }, ], ); }); @@ -192,10 +200,10 @@ it("expands themes into independent upload-ready directories per appearance", () showcaseCaptureDirectory("/captures", capture), ), [ - "/captures/apple/iphone-test/light/ocean", - "/captures/apple/iphone-test/light/ember", - "/captures/apple/iphone-test/dark/ocean", - "/captures/apple/iphone-test/dark/ember", + NodePath.join("/captures", "apple", "iphone-test", "light", "ocean"), + NodePath.join("/captures", "apple", "iphone-test", "light", "ember"), + NodePath.join("/captures", "apple", "iphone-test", "dark", "ocean"), + NodePath.join("/captures", "apple", "iphone-test", "dark", "ember"), ], ); }); diff --git a/scripts/mobile-showcase.ts b/scripts/mobile-showcase.ts index d6ced75d5db2152cff4ad845f18da9aed74afa9c..6b6adebeb779edf75a690be7ecfd936b33c1b0b7 100644 --- a/scripts/mobile-showcase.ts +++ b/scripts/mobile-showcase.ts @@ -53,7 +53,8 @@ export function resolveAndroidSdkRoot( const configured = environment.ANDROID_HOME ?? environment.ANDROID_SDK_ROOT; if (configured) return configured; const home = environment.HOME ?? environment.USERPROFILE ?? ""; - return NodePath.join(home, platform === "darwin" ? "Library/Android/sdk" : "Android/Sdk"); + const path = platform === "win32" ? NodePath.win32 : NodePath.posix; + return path.join(home, platform === "darwin" ? "Library/Android/sdk" : "Android/Sdk"); } const ANDROID_SDK_ROOT = resolveAndroidSdkRoot(NodeProcess.env);