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

test: skip POSIX mode-bit assertions on a Windows host (#9571)

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

5 files changed, 180 insertions(+), 153 deletions(-)

apps/server/scripts/t3-sqlite-state.test.ts+3-1
...@@ -7,6 +7,7 @@ import * as SqlClient from "effect/unstable/sql/SqlClient";...@@ -7,6 +7,7 @@ import * as SqlClient from "effect/unstable/sql/SqlClient";
77
8import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient";8import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient";
9import { runSqliteState } from "./t3-sqlite-state.ts";9import { runSqliteState } from "./t3-sqlite-state.ts";
10import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
10import { symlinksSupported } from "@t3tools/shared/testing/symlinks";11import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
1112
12const createFixtureDatabase = Effect.fn("createSqliteStateFixtureDatabase")(function* (13const createFixtureDatabase = Effect.fn("createSqliteStateFixtureDatabase")(function* (
...@@ -89,7 +90,8 @@ it.layer(NodeServices.layer)("t3-sqlite-state", (it) => {...@@ -89,7 +90,8 @@ it.layer(NodeServices.layer)("t3-sqlite-state", (it) => {
89 sql: "INSERT INTO fixtures (id, label) VALUES (2, 'seeded')",90 sql: "INSERT INTO fixtures (id, label) VALUES (2, 'seeded')",
90 });91 });
91 assert.equal(mutation.operation, "exec");92 assert.equal(mutation.operation, "exec");
92 if (mutation.operation === "exec") {93 if (mutation.operation === "exec" && (yield* HostProcessPlatform) !== "win32") {
94 // NTFS has no POSIX mode bits to report.
93 assert.equal((yield* fs.stat(mutation.backup)).mode & 0o777, 0o600);95 assert.equal((yield* fs.stat(mutation.backup)).mode & 0o777, 0o600);
94 }96 }
9597
apps/server/src/cli/theme.test.ts+9-4
...@@ -14,6 +14,11 @@ import { Command } from "effect/unstable/cli";...@@ -14,6 +14,11 @@ import { Command } from "effect/unstable/cli";
1414
15import { cli } from "../bin.ts";15import { cli } from "../bin.ts";
16import { symlinksSupported } from "@t3tools/shared/testing/symlinks";16import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
17import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
18
19// These force a failure with chmod, which Windows ignores for directories and
20// cannot use to make a file unreadable, so the failure never happens there.
21const windowsHost = HostProcessPlatform.defaultValue() === "win32";
1722
18const runCli = (args: ReadonlyArray<string>) =>23const runCli = (args: ReadonlyArray<string>) =>
19 Command.runWith(cli, { version: "0.0.0" })(args).pipe(24 Command.runWith(cli, { version: "0.0.0" })(args).pipe(
...@@ -161,7 +166,7 @@ describe("t3 theme", () => {...@@ -161,7 +166,7 @@ describe("t3 theme", () => {
161 // rolled back rather than left mutating the environment's theme set. The166 // rolled back rather than left mutating the environment's theme set. The
162 // userdata directory is made read-only while themes stays writable, so the167 // userdata directory is made read-only while themes stays writable, so the
163 // failure lands after the publish -- the case the rollback exists for.168 // failure lands after the publish -- the case the rollback exists for.
164 it.effect("rolls back a publish when the default cannot be written", () =>169 it.effect.skipIf(windowsHost)("rolls back a publish when the default cannot be written", () =>
165 Effect.gen(function* () {170 Effect.gen(function* () {
166 const baseDir = makeBaseDir();171 const baseDir = makeBaseDir();
167 writeSettings(baseDir, {});172 writeSettings(baseDir, {});
...@@ -229,7 +234,7 @@ describe("t3 theme", () => {...@@ -229,7 +234,7 @@ describe("t3 theme", () => {
229 // Rollback moves the previous directory entry aside and back, so even an234 // Rollback moves the previous directory entry aside and back, so even an
230 // entry the watcher would never publish -- here a symlink -- comes back235 // entry the watcher would never publish -- here a symlink -- comes back
231 // exactly as it was when the set fails.236 // exactly as it was when the set fails.
232 it.effect.skipIf(!symlinksSupported)(237 it.effect.skipIf(!symlinksSupported || windowsHost)(
233 "restores a non-theme destination entry when the set fails",238 "restores a non-theme destination entry when the set fails",
234 () =>239 () =>
235 Effect.gen(function* () {240 Effect.gen(function* () {
...@@ -255,7 +260,7 @@ describe("t3 theme", () => {...@@ -255,7 +260,7 @@ describe("t3 theme", () => {
255 }),260 }),
256 );261 );
257262
258 it.effect("restores the previous theme when a re-publish fails to set", () =>263 it.effect.skipIf(windowsHost)("restores the previous theme when a re-publish fails to set", () =>
259 Effect.gen(function* () {264 Effect.gen(function* () {
260 const baseDir = makeBaseDir();265 const baseDir = makeBaseDir();
261 writeSettings(baseDir, {});266 writeSettings(baseDir, {});
...@@ -352,7 +357,7 @@ describe("t3 theme", () => {...@@ -352,7 +357,7 @@ describe("t3 theme", () => {
352357
353 // An unreadable settings file must never read as "no settings": writing a358 // An unreadable settings file must never read as "no settings": writing a
354 // fresh sparse file over it would discard every key the user had.359 // fresh sparse file over it would discard every key the user had.
355 it.effect("refuses to write when the settings file cannot be read", () =>360 it.effect.skipIf(windowsHost)("refuses to write when the settings file cannot be read", () =>
356 Effect.gen(function* () {361 Effect.gen(function* () {
357 const baseDir = makeBaseDir();362 const baseDir = makeBaseDir();
358 writeSettings(baseDir, { enableProviderUpdateChecks: false });363 writeSettings(baseDir, { enableProviderUpdateChecks: false });
apps/server/src/keybindings.test.ts+26-22
...@@ -12,6 +12,7 @@ import * as Schema from "effect/Schema";...@@ -12,6 +12,7 @@ import * as Schema from "effect/Schema";
12import * as ServerConfig from "./config.ts";12import * as ServerConfig from "./config.ts";
13import * as Keybindings from "./keybindings.ts";13import * as Keybindings from "./keybindings.ts";
14import { KeybindingsConfigError } from "@t3tools/contracts";14import { KeybindingsConfigError } from "@t3tools/contracts";
15import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
1516
16const KeybindingsConfigJson = Schema.fromJsonString(KeybindingsConfig);17const KeybindingsConfigJson = Schema.fromJsonString(KeybindingsConfig);
17const encodeKeybindingsConfigJson = Schema.encodeEffect(KeybindingsConfigJson);18const encodeKeybindingsConfigJson = Schema.encodeEffect(KeybindingsConfigJson);
...@@ -511,31 +512,34 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -511,31 +512,34 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
511 }).pipe(Effect.provide(makeKeybindingsLayer())),512 }).pipe(Effect.provide(makeKeybindingsLayer())),
512 );513 );
513514
514 it.effect("fails when config directory is not writable", () =>515 // chmod cannot make a directory unwritable on Windows, so the write succeeds.
515 Effect.gen(function* () {516 it.effect.skipIf(HostProcessPlatform.defaultValue() === "win32")(
516 const fs = yield* FileSystem.FileSystem;517 "fails when config directory is not writable",
517 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;518 () =>
518 const { dirname } = yield* Path.Path;519 Effect.gen(function* () {
519 yield* writeKeybindingsConfig(keybindingsConfigPath, [520 const fs = yield* FileSystem.FileSystem;
520 { key: "mod+j", command: "terminal.toggle" },521 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
521 ]);522 const { dirname } = yield* Path.Path;
522 yield* fs.chmod(dirname(keybindingsConfigPath), 0o500);523 yield* writeKeybindingsConfig(keybindingsConfigPath, [
524 { key: "mod+j", command: "terminal.toggle" },
525 ]);
526 yield* fs.chmod(dirname(keybindingsConfigPath), 0o500);
523527
524 const result = yield* Effect.gen(function* () {528 const result = yield* Effect.gen(function* () {
525 const keybindings = yield* Keybindings.Keybindings;529 const keybindings = yield* Keybindings.Keybindings;
526 return yield* keybindings.upsertKeybindingRule({530 return yield* keybindings.upsertKeybindingRule({
527 key: "mod+shift+r",531 key: "mod+shift+r",
528 command: "script.run-tests.run",532 command: "script.run-tests.run",
529 });533 });
530 }).pipe(toDetailResult);534 }).pipe(toDetailResult);
531 assertFailure(result, "failed to write keybindings config");535 assertFailure(result, "failed to write keybindings config");
532536
533 yield* fs.chmod(dirname(keybindingsConfigPath), 0o700);537 yield* fs.chmod(dirname(keybindingsConfigPath), 0o700);
534538
535 const persisted = yield* readKeybindingsConfig(keybindingsConfigPath);539 const persisted = yield* readKeybindingsConfig(keybindingsConfigPath);
536 const persistedView = persisted.map(({ key, command }) => ({ key, command }));540 const persistedView = persisted.map(({ key, command }) => ({ key, command }));
537 assert.deepEqual(persistedView, [{ key: "mod+j", command: "terminal.toggle" }]);541 assert.deepEqual(persistedView, [{ key: "mod+j", command: "terminal.toggle" }]);
538 }).pipe(Effect.provide(makeKeybindingsLayer())),542 }).pipe(Effect.provide(makeKeybindingsLayer())),
539 );543 );
540544
541 it.effect("caches loaded resolved config across repeated reads", () =>545 it.effect("caches loaded resolved config across repeated reads", () =>
packages/shared/src/logging.test.ts+6-1
...@@ -3,6 +3,7 @@ import * as NodeFS from "node:fs";...@@ -3,6 +3,7 @@ import * as NodeFS from "node:fs";
3import * as NodeOS from "node:os";3import * as NodeOS from "node:os";
4import * as NodePath from "node:path";4import * as NodePath from "node:path";
5import { afterEach, describe, expect, it } from "vite-plus/test";5import { afterEach, describe, expect, it } from "vite-plus/test";
6import { HostProcessPlatform } from "./hostProcess.ts";
67
7import {8import {
8 RotatingFileSink,9 RotatingFileSink,
...@@ -10,6 +11,7 @@ import {...@@ -10,6 +11,7 @@ import {
10 RotatingFileSinkError,11 RotatingFileSinkError,
11} from "./logging.ts";12} from "./logging.ts";
1213
14const windowsHost = HostProcessPlatform.defaultValue() === "win32";
13const tempDirectories: string[] = [];15const tempDirectories: string[] = [];
1416
15const makeTempDirectory = (): string => {17const makeTempDirectory = (): string => {
...@@ -69,7 +71,10 @@ describe("RotatingFileSink", () => {...@@ -69,7 +71,10 @@ describe("RotatingFileSink", () => {
69 expect((thrown as RotatingFileSinkError).cause).toBeInstanceOf(Error);71 expect((thrown as RotatingFileSinkError).cause).toBeInstanceOf(Error);
70 });72 });
7173
72 it("only treats a missing log file as an empty current size", () => {74 // An over-long name is the one stat failure that is neither ENOENT nor a
75 // permission problem on posix. Windows reports it as ENOENT, so the sink
76 // correctly treats it as an absent file and there is nothing to assert.
77 it.skipIf(windowsHost)("only treats a missing log file as an empty current size", () => {
73 const directory = makeTempDirectory();78 const directory = makeTempDirectory();
74 const filePath = NodePath.join(directory, "a".repeat(300));79 const filePath = NodePath.join(directory, "a".repeat(300));
7580
packages/shared/src/relayClient.test.ts+136-125
...@@ -18,6 +18,10 @@ import {...@@ -18,6 +18,10 @@ import {
18 makeCloudflaredRelayClient,18 makeCloudflaredRelayClient,
19} from "./relayClient.ts";19} from "./relayClient.ts";
2020
21// The suite runs the linux code path against the real filesystem, checking
22// POSIX exec bits that NTFS never reports; the win32 branch skips that check.
23const windowsHost = HostProcessPlatform.defaultValue() === "win32";
24
21const hostRuntimeLayer = (env: Record<string, string> = {}) =>25const hostRuntimeLayer = (env: Record<string, string> = {}) =>
22 Layer.mergeAll(26 Layer.mergeAll(
23 Layer.succeed(HostProcessPlatform, "linux"),27 Layer.succeed(HostProcessPlatform, "linux"),
...@@ -63,102 +67,106 @@ const makeSpawnerLayer = (commands: Array<string>) =>...@@ -63,102 +67,106 @@ const makeSpawnerLayer = (commands: Array<string>) =>
63 );67 );
6468
65describe("RelayClient", () => {69describe("RelayClient", () => {
66 it.effect("resolves explicit overrides before managed and PATH executables", () =>70 it.effect.skipIf(windowsHost)(
67 Effect.gen(function* () {71 "resolves explicit overrides before managed and PATH executables",
68 const fileSystem = yield* FileSystem.FileSystem;72 () =>
69 const baseDir = yield* fileSystem.makeTempDirectoryScoped({73 Effect.gen(function* () {
70 prefix: "t3-cloudflared-test-",74 const fileSystem = yield* FileSystem.FileSystem;
71 });75 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
72 const overridePath = `${baseDir}/override-cloudflared`;76 prefix: "t3-cloudflared-test-",
73 yield* fileSystem.writeFileString(overridePath, "override");77 });
74 yield* fileSystem.chmod(overridePath, 0o755);78 const overridePath = `${baseDir}/override-cloudflared`;
75 const manager = yield* makeCloudflaredRelayClient({79 yield* fileSystem.writeFileString(overridePath, "override");
76 baseDir,80 yield* fileSystem.chmod(overridePath, 0o755);
77 });81 const manager = yield* makeCloudflaredRelayClient({
82 baseDir,
83 });
7884
79 expect(85 expect(
80 yield* manager.resolve.pipe(86 yield* manager.resolve.pipe(
81 Effect.provideService(87 Effect.provideService(
82 ConfigProvider.ConfigProvider,88 ConfigProvider.ConfigProvider,
83 ConfigProvider.fromEnv({89 ConfigProvider.fromEnv({
84 env: { PATH: "", T3CODE_CLOUDFLARED_PATH: overridePath },90 env: { PATH: "", T3CODE_CLOUDFLARED_PATH: overridePath },
85 }),91 }),
92 ),
93 ),
94 ).toEqual({
95 status: "available",
96 executablePath: overridePath,
97 source: "override",
98 version: CLOUDFLARED_VERSION,
99 });
100 }).pipe(
101 Effect.scoped,
102 Effect.provide(
103 Layer.mergeAll(
104 NodeServices.layer,
105 makeHttpClientLayer(new Uint8Array()),
106 makeSpawnerLayer([]),
107 hostRuntimeLayer(),
86 ),108 ),
87 ),
88 ).toEqual({
89 status: "available",
90 executablePath: overridePath,
91 source: "override",
92 version: CLOUDFLARED_VERSION,
93 });
94 }).pipe(
95 Effect.scoped,
96 Effect.provide(
97 Layer.mergeAll(
98 NodeServices.layer,
99 makeHttpClientLayer(new Uint8Array()),
100 makeSpawnerLayer([]),
101 hostRuntimeLayer(),
102 ),109 ),
103 ),110 ),
104 ),
105 );111 );
106112
107 it.effect("downloads, verifies, validates, and atomically installs the managed executable", () =>113 it.effect.skipIf(windowsHost)(
108 Effect.gen(function* () {114 "downloads, verifies, validates, and atomically installs the managed executable",
109 const fileSystem = yield* FileSystem.FileSystem;115 () =>
110 const baseDir = yield* fileSystem.makeTempDirectoryScoped({116 Effect.gen(function* () {
111 prefix: "t3-cloudflared-test-",117 const fileSystem = yield* FileSystem.FileSystem;
112 });118 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
113 const bytes = new TextEncoder().encode("test-cloudflared-binary");119 prefix: "t3-cloudflared-test-",
114 const manager = yield* makeCloudflaredRelayClient({120 });
115 baseDir,121 const bytes = new TextEncoder().encode("test-cloudflared-binary");
116 releaseAsset: {122 const manager = yield* makeCloudflaredRelayClient({
117 url: "https://example.test/cloudflared",123 baseDir,
118 sha256: Encoding.encodeHex(sha256(bytes)),124 releaseAsset: {
119 archive: "binary",125 url: "https://example.test/cloudflared",
120 },126 sha256: Encoding.encodeHex(sha256(bytes)),
121 });127 archive: "binary",
128 },
129 });
122130
123 const progress: Array<string> = [];131 const progress: Array<string> = [];
124 const installed = yield* manager.installWithProgress((event) =>132 const installed = yield* manager.installWithProgress((event) =>
125 Effect.sync(() => {133 Effect.sync(() => {
126 if (event.type === "progress") {134 if (event.type === "progress") {
127 progress.push(event.stage);135 progress.push(event.stage);
128 }136 }
129 }),137 }),
130 );138 );
131 const managedPath = `${baseDir}/tools/cloudflared/${CLOUDFLARED_VERSION}/linux-x64/cloudflared`;139 const managedPath = `${baseDir}/tools/cloudflared/${CLOUDFLARED_VERSION}/linux-x64/cloudflared`;
132 expect(installed).toEqual({140 expect(installed).toEqual({
133 status: "available",141 status: "available",
134 executablePath: managedPath,142 executablePath: managedPath,
135 source: "managed",143 source: "managed",
136 version: CLOUDFLARED_VERSION,144 version: CLOUDFLARED_VERSION,
137 });145 });
138 expect(new TextDecoder().decode(yield* fileSystem.readFile(managedPath))).toBe(146 expect(new TextDecoder().decode(yield* fileSystem.readFile(managedPath))).toBe(
139 "test-cloudflared-binary",147 "test-cloudflared-binary",
140 );148 );
141 expect(progress).toEqual([149 expect(progress).toEqual([
142 "checking",150 "checking",
143 "waiting_for_lock",151 "waiting_for_lock",
144 "downloading",152 "downloading",
145 "verifying",153 "verifying",
146 "installing",154 "installing",
147 "validating",155 "validating",
148 "activating",156 "activating",
149 ]);157 ]);
150 expect(yield* manager.resolve).toEqual(installed);158 expect(yield* manager.resolve).toEqual(installed);
151 }).pipe(159 }).pipe(
152 Effect.scoped,160 Effect.scoped,
153 Effect.provide(161 Effect.provide(
154 Layer.mergeAll(162 Layer.mergeAll(
155 NodeServices.layer,163 NodeServices.layer,
156 makeHttpClientLayer(new TextEncoder().encode("test-cloudflared-binary")),164 makeHttpClientLayer(new TextEncoder().encode("test-cloudflared-binary")),
157 makeSpawnerLayer([]),165 makeSpawnerLayer([]),
158 hostRuntimeLayer(),166 hostRuntimeLayer(),
167 ),
159 ),168 ),
160 ),169 ),
161 ),
162 );170 );
163171
164 it.effect("rejects downloads whose checksum does not match the pinned manifest", () =>172 it.effect("rejects downloads whose checksum does not match the pinned manifest", () =>
...@@ -192,7 +200,7 @@ describe("RelayClient", () => {...@@ -192,7 +200,7 @@ describe("RelayClient", () => {
192 ),200 ),
193 );201 );
194202
195 it.effect("serializes concurrent installs within one runtime", () => {203 it.effect.skipIf(windowsHost)("serializes concurrent installs within one runtime", () => {
196 const commands: Array<string> = [];204 const commands: Array<string> = [];
197 const bytes = new TextEncoder().encode("test-cloudflared-binary");205 const bytes = new TextEncoder().encode("test-cloudflared-binary");
198 return Effect.gen(function* () {206 return Effect.gen(function* () {
...@@ -227,45 +235,48 @@ describe("RelayClient", () => {...@@ -227,45 +235,48 @@ describe("RelayClient", () => {
227 );235 );
228 });236 });
229237
230 it.effect("observes PATH changes after the manager has been constructed", () => {238 it.effect.skipIf(windowsHost)(
231 const env = { PATH: "" };239 "observes PATH changes after the manager has been constructed",
232 return Effect.gen(function* () {240 () => {
233 const fileSystem = yield* FileSystem.FileSystem;241 const env = { PATH: "" };
234 const baseDir = yield* fileSystem.makeTempDirectoryScoped({242 return Effect.gen(function* () {
235 prefix: "t3-cloudflared-test-",243 const fileSystem = yield* FileSystem.FileSystem;
236 });244 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
237 const binDir = `${baseDir}/bin`;245 prefix: "t3-cloudflared-test-",
238 const executablePath = `${binDir}/cloudflared`;246 });
239 const manager = yield* makeCloudflaredRelayClient({247 const binDir = `${baseDir}/bin`;
240 baseDir,248 const executablePath = `${binDir}/cloudflared`;
241 });249 const manager = yield* makeCloudflaredRelayClient({
250 baseDir,
251 });
242252
243 expect(yield* manager.resolve).toEqual({253 expect(yield* manager.resolve).toEqual({
244 status: "missing",254 status: "missing",
245 version: CLOUDFLARED_VERSION,255 version: CLOUDFLARED_VERSION,
246 });256 });
247257
248 yield* fileSystem.makeDirectory(binDir);258 yield* fileSystem.makeDirectory(binDir);
249 yield* fileSystem.writeFileString(executablePath, "cloudflared");259 yield* fileSystem.writeFileString(executablePath, "cloudflared");
250 yield* fileSystem.chmod(executablePath, 0o755);260 yield* fileSystem.chmod(executablePath, 0o755);
251 env.PATH = binDir;261 env.PATH = binDir;
252262
253 expect(yield* manager.resolve).toEqual({263 expect(yield* manager.resolve).toEqual({
254 status: "available",264 status: "available",
255 executablePath,265 executablePath,
256 source: "path",266 source: "path",
257 version: CLOUDFLARED_VERSION,267 version: CLOUDFLARED_VERSION,
258 });268 });
259 }).pipe(269 }).pipe(
260 Effect.scoped,270 Effect.scoped,
261 Effect.provide(271 Effect.provide(
262 Layer.mergeAll(272 Layer.mergeAll(
263 NodeServices.layer,273 NodeServices.layer,
264 makeHttpClientLayer(new Uint8Array()),274 makeHttpClientLayer(new Uint8Array()),
265 makeSpawnerLayer([]),275 makeSpawnerLayer([]),
266 hostRuntimeLayer(env),276 hostRuntimeLayer(env),
277 ),
267 ),278 ),
268 ),279 );
269 );280 },
270 });281 );
271});282});