Unified · Split
| @@ -1,3 +1,4 @@ | ||
| 1 | +import * as NodePath from "@effect/platform-node/NodePath"; | |
| 1 | 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | 3 | import { assert, describe, it } from "@effect/vitest"; |
| 3 | 4 | import * as Effect from "effect/Effect"; |
| @@ -88,6 +89,7 @@ const makeEnvironmentLayer = (overrides: TestEnvironmentInput = {}) => { · 84 unmodified lines | ||
| 88 | 89 | Layer.provide( |
| 89 | 90 | Layer.mergeAll( |
| 90 | 91 | NodeServices.layer, |
| 92 | + NodePath.layerPosix, | |
| 91 | 93 | DesktopConfig.layerTest({ |
| 92 | 94 | ...env, |
| 93 | 95 | }), |
| @@ -1,3 +1,4 @@ | ||
| 1 | +import * as NodePath from "@effect/platform-node/NodePath"; | |
| 1 | 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | 3 | import { assert, describe, it } from "@effect/vitest"; |
| 3 | 4 | import * as Effect from "effect/Effect"; |
| @@ -20,7 +21,11 @@ const environmentLayer = DesktopEnvironment.layer({ · 16 unmodified lines | ||
| 20 | 21 | isPackaged: true, |
| 21 | 22 | resourcesPath: "/Applications/T3 Code.app/Contents/Resources", |
| 22 | 23 | runningUnderArm64Translation: false, |
| 23 | −}).pipe(Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({})))); | |
| 24 | +}).pipe( | |
| 25 | + Layer.provide( | |
| 26 | + Layer.mergeAll(NodeServices.layer, NodePath.layerPosix, DesktopConfig.layerTest({})), | |
| 27 | + ), | |
| 28 | +); | |
| 24 | 29 | |
| 25 | 30 | describe("DesktopAssets", () => { |
| 26 | 31 | it.effect("uses canonical source-tree icons for unpackaged development", () => |
| @@ -39,6 +44,7 @@ describe("DesktopAssets", () => { · 12 unmodified lines | ||
| 39 | 44 | Layer.provide( |
| 40 | 45 | Layer.mergeAll( |
| 41 | 46 | NodeServices.layer, |
| 47 | + NodePath.layerPosix, | |
| 42 | 48 | DesktopConfig.layerTest({ VITE_DEV_SERVER_URL: "http://localhost:5733" }), |
| 43 | 49 | ), |
| 44 | 50 | ), |
| @@ -1,3 +1,4 @@ | ||
| 1 | +import * as NodePath from "@effect/platform-node/NodePath"; | |
| 1 | 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | 3 | import { assert, describe, it } from "@effect/vitest"; |
| 3 | 4 | import * as Effect from "effect/Effect"; |
| @@ -26,7 +27,11 @@ const makeEnvironmentLayer = ( · 22 unmodified lines | ||
| 26 | 27 | DesktopEnvironment.layer({ |
| 27 | 28 | ...defaultInput, |
| 28 | 29 | ...overrides, |
| 29 | − }).pipe(Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest(env)))); | |
| 30 | + }).pipe( | |
| 31 | + Layer.provide( | |
| 32 | + Layer.mergeAll(NodeServices.layer, NodePath.layerPosix, DesktopConfig.layerTest(env)), | |
| 33 | + ), | |
| 34 | + ); | |
| 30 | 35 | |
| 31 | 36 | const makeEnvironment = ( |
| 32 | 37 | overrides: Partial<DesktopEnvironment.MakeDesktopEnvironmentInput> = {}, |
| @@ -109,7 +109,7 @@ const ConcurrentReadMissFileSystemLayer = Layer.effect( · 108 unmodified lines | ||
| 109 | 109 | return { |
| 110 | 110 | ...fileSystem, |
| 111 | 111 | readFile: (path) => |
| 112 | − String(path).endsWith("/session-signing-key.bin") | |
| 112 | + /[\\/]session-signing-key\.bin$/.test(String(path)) | |
| 113 | 113 | ? Ref.updateAndGet(readCountRef, (count) => count + 1).pipe( |
| 114 | 114 | Effect.flatMap((count) => { |
| 115 | 115 | if (count > 2) { |
| @@ -219,7 +219,7 @@ it.layer(NodeServices.layer)("ServerSecretStore.layer", (it) => { · 103 unmodified lines | ||
| 219 | 219 | yield* secretStore.set("session-signing-key", Uint8Array.from([1, 2, 3])); |
| 220 | 220 | |
| 221 | 221 | assert.isTrue( |
| 222 | − chmodCalls.some((call) => call.mode === 0o700 && call.path.endsWith("/secrets")), | |
| 222 | + chmodCalls.some((call) => call.mode === 0o700 && /[\\/]secrets$/.test(call.path)), | |
| 223 | 223 | ); |
| 224 | 224 | assert.isAtLeast(chmodCalls.filter((call) => call.mode === 0o600).length, 2); |
| 225 | 225 | }).pipe(Effect.provide(NodeServices.layer)), |
| @@ -54,11 +54,12 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => { · 53 unmodified lines | ||
| 54 | 54 | it.effect("serves repeated resolves from cache instead of re-walking candidates", () => |
| 55 | 55 | Effect.gen(function* () { |
| 56 | 56 | const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; |
| 57 | + const path = yield* Path.Path; | |
| 57 | 58 | const cwd = yield* makeTempDir; |
| 58 | 59 | yield* writeTextFile(cwd, "public/favicon.svg", "<svg>public</svg>"); |
| 59 | 60 | |
| 60 | 61 | const resolved = yield* resolver.resolvePath(cwd); |
| 61 | − expect(resolved?.endsWith("public/favicon.svg")).toBe(true); | |
| 62 | + expect(resolved?.endsWith(path.join("public", "favicon.svg"))).toBe(true); | |
| 62 | 63 | |
| 63 | 64 | // `favicon.svg` outranks `public/favicon.svg`, so a resolver that walked |
| 64 | 65 | // the candidate list again would switch to it. Staying on the original |
| @@ -71,7 +72,7 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => { · 6 unmodified lines | ||
| 71 | 72 | |
| 72 | 73 | yield* TestClock.adjust(Duration.minutes(11)); |
| 73 | 74 | |
| 74 | − expect((yield* resolver.resolvePath(cwd))?.endsWith("/favicon.svg")).toBe(true); | |
| 75 | + expect(yield* resolver.resolvePath(cwd)).toBe(path.join(cwd, "favicon.svg")); | |
| 75 | 76 | expect(yield* resolver.resolvePath(cwd)).not.toBe(resolved); |
| 76 | 77 | }).pipe(Effect.provide(TestClock.layer())), |
| 77 | 78 | ); |
| @@ -1,3 +1,4 @@ | ||
| 1 | +import * as NodePath from "@effect/platform-node/NodePath"; | |
| 1 | 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | 3 | import { assert, it } from "@effect/vitest"; |
| 3 | 4 | import * as Effect from "effect/Effect"; |
| @@ -492,7 +493,11 @@ it.layer(NodeServices.layer)("discoverClaudeSkills", (it) => { · 488 unmodified lines | ||
| 492 | 493 | |
| 493 | 494 | it.effect("lets the administrator's managed policy outrank every other settings file", () => |
| 494 | 495 | Effect.gen(function* () { |
| 495 | − const path = yield* Path.Path; | |
| 496 | + // The function is pure on its `path` argument, so hand it the | |
| 497 | + // implementation matching each platform under test rather than the | |
| 498 | + // host's. | |
| 499 | + const path = yield* Path.Path.pipe(Effect.provide(NodePath.layerPosix)); | |
| 500 | + const win32Path = yield* Path.Path.pipe(Effect.provide(NodePath.layerWin32)); | |
| 496 | 501 | |
| 497 | 502 | for (const [platform, expected] of [ |
| 498 | 503 | ["darwin", "/Library/Application Support/ClaudeCode/managed-settings.json"], |
| @@ -508,14 +513,15 @@ it.layer(NodeServices.layer)("discoverClaudeSkills", (it) => { · 9 unmodified lines | ||
| 508 | 513 | } |
| 509 | 514 | |
| 510 | 515 | assert.deepEqual( |
| 511 | − skillOverrideSettingsPaths(path, "/home/.claude", undefined, "win32", { | |
| 512 | − PROGRAMDATA: "C:/ProgramData", | |
| 516 | + skillOverrideSettingsPaths(win32Path, "C:\\Users\\me\\.claude", undefined, "win32", { | |
| 517 | + PROGRAMDATA: "C:\\ProgramData", | |
| 513 | 518 | }).at(-1), |
| 514 | − "C:/ProgramData/ClaudeCode/managed-settings.json", | |
| 519 | + "C:\\ProgramData\\ClaudeCode\\managed-settings.json", | |
| 520 | + ); | |
| 521 | + assert.deepEqual( | |
| 522 | + skillOverrideSettingsPaths(win32Path, "C:\\Users\\me\\.claude", undefined, "win32", {}), | |
| 523 | + ["C:\\Users\\me\\.claude\\settings.json"], | |
| 515 | 524 | ); |
| 516 | − assert.deepEqual(skillOverrideSettingsPaths(path, "/home/.claude", undefined, "win32", {}), [ | |
| 517 | − "/home/.claude/settings.json", | |
| 518 | − ]); | |
| 519 | 525 | |
| 520 | 526 | // Only the repository root's local file joins in, after the |
| 521 | 527 | // workspace's own local file so it wins. |
| @@ -1,3 +1,4 @@ | ||
| 1 | +import * as NodePath from "@effect/platform-node/NodePath"; | |
| 1 | 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | 3 | import { assert, describe, it } from "@effect/vitest"; |
| 3 | 4 | import * as Effect from "effect/Effect"; |
| @@ -133,6 +134,6 @@ describe("ssh config", () => { · 129 unmodified lines | ||
| 133 | 134 | "/tmp/home", |
| 134 | 135 | ); |
| 135 | 136 | assert.equal(pattern, "/tmp/home/.ssh/config.d/*.conf"); |
| 136 | − }).pipe(Effect.provide(NodeServices.layer)), | |
| 137 | + }).pipe(Effect.provide(NodePath.layerPosix)), | |
| 137 | 138 | ); |
| 138 | 139 | }); |
| @@ -8,6 +8,7 @@ import * as Sink from "effect/Sink"; · 7 unmodified lines | ||
| 8 | 8 | import * as Stream from "effect/Stream"; |
| 9 | 9 | import * as TestClock from "effect/testing/TestClock"; |
| 10 | 10 | import { ChildProcessSpawner } from "effect/unstable/process"; |
| 11 | +import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; | |
| 11 | 12 | |
| 12 | 13 | import { |
| 13 | 14 | buildTailscaleHttpsBaseUrl, |
| @@ -98,14 +99,22 @@ function neverFinishingMockHandle() { · 84 unmodified lines | ||
| 98 | 99 | }); |
| 99 | 100 | } |
| 100 | 101 | |
| 102 | +// The executable name depends on the host platform (`tailscale.exe` on | |
| 103 | +// Windows), so pin it: these tests assert the posix spelling. | |
| 104 | +function spawnerLayer(spawner: ChildProcessSpawner.ChildProcessSpawner["Service"]) { | |
| 105 | + return Layer.merge( | |
| 106 | + Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner), | |
| 107 | + Layer.succeed(HostProcessPlatform, "linux"), | |
| 108 | + ); | |
| 109 | +} | |
| 110 | + | |
| 101 | 111 | function mockSpawnerLayer( |
| 102 | 112 | handler: ( |
| 103 | 113 | command: string, |
| 104 | 114 | args: ReadonlyArray<string>, |
| 105 | 115 | ) => { stdout?: string; stderr?: string; code?: number }, |
| 106 | 116 | ) { |
| 107 | − return Layer.succeed( | |
| 108 | − ChildProcessSpawner.ChildProcessSpawner, | |
| 117 | + return spawnerLayer( | |
| 109 | 118 | ChildProcessSpawner.make((command) => { |
| 110 | 119 | const childProcess = command as unknown as { |
| 111 | 120 | readonly command: string; |
| @@ -194,10 +203,7 @@ describe("tailscale", () => { · 82 unmodified lines | ||
| 194 | 203 | method: "spawn", |
| 195 | 204 | cause: systemCause, |
| 196 | 205 | }); |
| 197 | − const layer = Layer.succeed( | |
| 198 | − ChildProcessSpawner.ChildProcessSpawner, | |
| 199 | − ChildProcessSpawner.make(() => Effect.fail(cause)), | |
| 200 | − ); | |
| 206 | + const layer = spawnerLayer(ChildProcessSpawner.make(() => Effect.fail(cause))); | |
| 201 | 207 | |
| 202 | 208 | return Effect.gen(function* () { |
| 203 | 209 | const error = yield* readTailscaleStatus.pipe(Effect.flip, Effect.provide(layer)); |
| @@ -218,8 +224,7 @@ describe("tailscale", () => { · 14 unmodified lines | ||
| 218 | 224 | // inside an `Effect.callback` registration, so that throw arrives as a |
| 219 | 225 | // defect rather than a typed error - the shape reproduced here. |
| 220 | 226 | const defect = Object.assign(new Error("spawn tailscale ENOTDIR"), { code: "ENOTDIR" }); |
| 221 | − const layer = Layer.succeed( | |
| 222 | − ChildProcessSpawner.ChildProcessSpawner, | |
| 227 | + const layer = spawnerLayer( | |
| 223 | 228 | ChildProcessSpawner.make(() => |
| 224 | 229 | Effect.callback<never, never>(() => { |
| 225 | 230 | throw defect; |
| @@ -297,10 +302,7 @@ describe("tailscale", () => { · 71 unmodified lines | ||
| 297 | 302 | it.effect("times out tailscale status through TestClock", () => { |
| 298 | 303 | const layer = Layer.merge( |
| 299 | 304 | TestClock.layer(), |
| 300 | − Layer.succeed( | |
| 301 | − ChildProcessSpawner.ChildProcessSpawner, | |
| 302 | − ChildProcessSpawner.make(() => Effect.succeed(neverFinishingMockHandle())), | |
| 303 | − ), | |
| 305 | + spawnerLayer(ChildProcessSpawner.make(() => Effect.succeed(neverFinishingMockHandle()))), | |
| 304 | 306 | ); |
| 305 | 307 | |
| 306 | 308 | return Effect.gen(function* () { |
| @@ -1,3 +1,5 @@ | ||
| 1 | +// @effect-diagnostics nodeBuiltinImport:off - expectations mirror the host path joins the script under test performs. | |
| 2 | +import * as NodePath from "node:path"; | |
| 1 | 3 | import { assert, it } from "@effect/vitest"; |
| 2 | 4 | import { PNG } from "pngjs"; |
| 3 | 5 | |
| @@ -167,8 +169,14 @@ it("expands both appearances into independent upload-ready directories", () => { · 163 unmodified lines | ||
| 167 | 169 | directory: showcaseCaptureDirectory("/captures", capture), |
| 168 | 170 | })), |
| 169 | 171 | [ |
| 170 | − { appearance: "light", directory: "/captures/apple/iphone-test/light/t3-code" }, | |
| 171 | − { appearance: "dark", directory: "/captures/apple/iphone-test/dark/t3-code" }, | |
| 172 | + { | |
| 173 | + appearance: "light", | |
| 174 | + directory: NodePath.join("/captures", "apple", "iphone-test", "light", "t3-code"), | |
| 175 | + }, | |
| 176 | + { | |
| 177 | + appearance: "dark", | |
| 178 | + directory: NodePath.join("/captures", "apple", "iphone-test", "dark", "t3-code"), | |
| 179 | + }, | |
| 172 | 180 | ], |
| 173 | 181 | ); |
| 174 | 182 | }); |
| @@ -192,10 +200,10 @@ it("expands themes into independent upload-ready directories per appearance", () · 17 unmodified lines | ||
| 192 | 200 | showcaseCaptureDirectory("/captures", capture), |
| 193 | 201 | ), |
| 194 | 202 | [ |
| 195 | − "/captures/apple/iphone-test/light/ocean", | |
| 196 | − "/captures/apple/iphone-test/light/ember", | |
| 197 | − "/captures/apple/iphone-test/dark/ocean", | |
| 198 | − "/captures/apple/iphone-test/dark/ember", | |
| 203 | + NodePath.join("/captures", "apple", "iphone-test", "light", "ocean"), | |
| 204 | + NodePath.join("/captures", "apple", "iphone-test", "light", "ember"), | |
| 205 | + NodePath.join("/captures", "apple", "iphone-test", "dark", "ocean"), | |
| 206 | + NodePath.join("/captures", "apple", "iphone-test", "dark", "ember"), | |
| 199 | 207 | ], |
| 200 | 208 | ); |
| 201 | 209 | }); |
| @@ -53,7 +53,8 @@ export function resolveAndroidSdkRoot( · 52 unmodified lines | ||
| 53 | 53 | const configured = environment.ANDROID_HOME ?? environment.ANDROID_SDK_ROOT; |
| 54 | 54 | if (configured) return configured; |
| 55 | 55 | const home = environment.HOME ?? environment.USERPROFILE ?? ""; |
| 56 | − return NodePath.join(home, platform === "darwin" ? "Library/Android/sdk" : "Android/Sdk"); | |
| 56 | + const path = platform === "win32" ? NodePath.win32 : NodePath.posix; | |
| 57 | + return path.join(home, platform === "darwin" ? "Library/Android/sdk" : "Android/Sdk"); | |
| 57 | 58 | } |
| 58 | 59 | |
| 59 | 60 | const ANDROID_SDK_ROOT = resolveAndroidSdkRoot(NodeProcess.env); |