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

test: stop path and platform tests depending on the host OS (#9564)

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

10 files changed, 65 insertions(+), 33 deletions(-)

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