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";
77
88import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient";
99import { runSqliteState } from "./t3-sqlite-state.ts";
10import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
1011import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
1112
1213const createFixtureDatabase = Effect.fn("createSqliteStateFixtureDatabase")(function* (
......@@ -89,7 +90,8 @@ it.layer(NodeServices.layer)("t3-sqlite-state", (it) => {
8990 sql: "INSERT INTO fixtures (id, label) VALUES (2, 'seeded')",
9091 });
9192 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.
9395 assert.equal((yield* fs.stat(mutation.backup)).mode & 0o777, 0o600);
9496 }
9597
apps/server/src/cli/theme.test.ts+9-4
......@@ -14,6 +14,11 @@ import { Command } from "effect/unstable/cli";
1414
1515import { cli } from "../bin.ts";
1616import { 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
1823const runCli = (args: ReadonlyArray<string>) =>
1924 Command.runWith(cli, { version: "0.0.0" })(args).pipe(
......@@ -161,7 +166,7 @@ describe("t3 theme", () => {
161166 // rolled back rather than left mutating the environment's theme set. The
162167 // userdata directory is made read-only while themes stays writable, so the
163168 // 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", () =>
165170 Effect.gen(function* () {
166171 const baseDir = makeBaseDir();
167172 writeSettings(baseDir, {});
......@@ -229,7 +234,7 @@ describe("t3 theme", () => {
229234 // Rollback moves the previous directory entry aside and back, so even an
230235 // entry the watcher would never publish -- here a symlink -- comes back
231236 // exactly as it was when the set fails.
232 it.effect.skipIf(!symlinksSupported)(
237 it.effect.skipIf(!symlinksSupported || windowsHost)(
233238 "restores a non-theme destination entry when the set fails",
234239 () =>
235240 Effect.gen(function* () {
......@@ -255,7 +260,7 @@ describe("t3 theme", () => {
255260 }),
256261 );
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", () =>
259264 Effect.gen(function* () {
260265 const baseDir = makeBaseDir();
261266 writeSettings(baseDir, {});
......@@ -352,7 +357,7 @@ describe("t3 theme", () => {
352357
353358 // An unreadable settings file must never read as "no settings": writing a
354359 // 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", () =>
356361 Effect.gen(function* () {
357362 const baseDir = makeBaseDir();
358363 writeSettings(baseDir, { enableProviderUpdateChecks: false });
apps/server/src/keybindings.test.ts+26-22
......@@ -12,6 +12,7 @@ import * as Schema from "effect/Schema";
1212import * as ServerConfig from "./config.ts";
1313import * as Keybindings from "./keybindings.ts";
1414import { KeybindingsConfigError } from "@t3tools/contracts";
15import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
1516
1617const KeybindingsConfigJson = Schema.fromJsonString(KeybindingsConfig);
1718const encodeKeybindingsConfigJson = Schema.encodeEffect(KeybindingsConfigJson);
......@@ -511,31 +512,34 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
511512 }).pipe(Effect.provide(makeKeybindingsLayer())),
512513 );
513514
514 it.effect("fails when config directory is not writable", () =>
515 Effect.gen(function* () {
516 const fs = yield* FileSystem.FileSystem;
517 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
518 const { dirname } = yield* Path.Path;
519 yield* writeKeybindingsConfig(keybindingsConfigPath, [
520 { key: "mod+j", command: "terminal.toggle" },
521 ]);
522 yield* fs.chmod(dirname(keybindingsConfigPath), 0o500);
515 // chmod cannot make a directory unwritable on Windows, so the write succeeds.
516 it.effect.skipIf(HostProcessPlatform.defaultValue() === "win32")(
517 "fails when config directory is not writable",
518 () =>
519 Effect.gen(function* () {
520 const fs = yield* FileSystem.FileSystem;
521 const { keybindingsConfigPath } = yield* ServerConfig.ServerConfig;
522 const { dirname } = yield* Path.Path;
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* () {
525 const keybindings = yield* Keybindings.Keybindings;
526 return yield* keybindings.upsertKeybindingRule({
527 key: "mod+shift+r",
528 command: "script.run-tests.run",
529 });
530 }).pipe(toDetailResult);
531 assertFailure(result, "failed to write keybindings config");
528 const result = yield* Effect.gen(function* () {
529 const keybindings = yield* Keybindings.Keybindings;
530 return yield* keybindings.upsertKeybindingRule({
531 key: "mod+shift+r",
532 command: "script.run-tests.run",
533 });
534 }).pipe(toDetailResult);
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);
536 const persistedView = persisted.map(({ key, command }) => ({ key, command }));
537 assert.deepEqual(persistedView, [{ key: "mod+j", command: "terminal.toggle" }]);
538 }).pipe(Effect.provide(makeKeybindingsLayer())),
539 const persisted = yield* readKeybindingsConfig(keybindingsConfigPath);
540 const persistedView = persisted.map(({ key, command }) => ({ key, command }));
541 assert.deepEqual(persistedView, [{ key: "mod+j", command: "terminal.toggle" }]);
542 }).pipe(Effect.provide(makeKeybindingsLayer())),
539543 );
540544
541545 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";
33import * as NodeOS from "node:os";
44import * as NodePath from "node:path";
55import { afterEach, describe, expect, it } from "vite-plus/test";
6import { HostProcessPlatform } from "./hostProcess.ts";
67
78import {
89 RotatingFileSink,
......@@ -10,6 +11,7 @@ import {
1011 RotatingFileSinkError,
1112} from "./logging.ts";
1213
14const windowsHost = HostProcessPlatform.defaultValue() === "win32";
1315const tempDirectories: string[] = [];
1416
1517const makeTempDirectory = (): string => {
......@@ -69,7 +71,10 @@ describe("RotatingFileSink", () => {
6971 expect((thrown as RotatingFileSinkError).cause).toBeInstanceOf(Error);
7072 });
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", () => {
7378 const directory = makeTempDirectory();
7479 const filePath = NodePath.join(directory, "a".repeat(300));
7580
packages/shared/src/relayClient.test.ts+136-125
......@@ -18,6 +18,10 @@ import {
1818 makeCloudflaredRelayClient,
1919} 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
2125const hostRuntimeLayer = (env: Record<string, string> = {}) =>
2226 Layer.mergeAll(
2327 Layer.succeed(HostProcessPlatform, "linux"),
......@@ -63,102 +67,106 @@ const makeSpawnerLayer = (commands: Array<string>) =>
6367 );
6468
6569describe("RelayClient", () => {
66 it.effect("resolves explicit overrides before managed and PATH executables", () =>
67 Effect.gen(function* () {
68 const fileSystem = yield* FileSystem.FileSystem;
69 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
70 prefix: "t3-cloudflared-test-",
71 });
72 const overridePath = `${baseDir}/override-cloudflared`;
73 yield* fileSystem.writeFileString(overridePath, "override");
74 yield* fileSystem.chmod(overridePath, 0o755);
75 const manager = yield* makeCloudflaredRelayClient({
76 baseDir,
77 });
70 it.effect.skipIf(windowsHost)(
71 "resolves explicit overrides before managed and PATH executables",
72 () =>
73 Effect.gen(function* () {
74 const fileSystem = yield* FileSystem.FileSystem;
75 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
76 prefix: "t3-cloudflared-test-",
77 });
78 const overridePath = `${baseDir}/override-cloudflared`;
79 yield* fileSystem.writeFileString(overridePath, "override");
80 yield* fileSystem.chmod(overridePath, 0o755);
81 const manager = yield* makeCloudflaredRelayClient({
82 baseDir,
83 });
7884
79 expect(
80 yield* manager.resolve.pipe(
81 Effect.provideService(
82 ConfigProvider.ConfigProvider,
83 ConfigProvider.fromEnv({
84 env: { PATH: "", T3CODE_CLOUDFLARED_PATH: overridePath },
85 }),
85 expect(
86 yield* manager.resolve.pipe(
87 Effect.provideService(
88 ConfigProvider.ConfigProvider,
89 ConfigProvider.fromEnv({
90 env: { PATH: "", T3CODE_CLOUDFLARED_PATH: overridePath },
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(),
86108 ),
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(),
102109 ),
103110 ),
104 ),
105111 );
106112
107 it.effect("downloads, verifies, validates, and atomically installs the managed executable", () =>
108 Effect.gen(function* () {
109 const fileSystem = yield* FileSystem.FileSystem;
110 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
111 prefix: "t3-cloudflared-test-",
112 });
113 const bytes = new TextEncoder().encode("test-cloudflared-binary");
114 const manager = yield* makeCloudflaredRelayClient({
115 baseDir,
116 releaseAsset: {
117 url: "https://example.test/cloudflared",
118 sha256: Encoding.encodeHex(sha256(bytes)),
119 archive: "binary",
120 },
121 });
113 it.effect.skipIf(windowsHost)(
114 "downloads, verifies, validates, and atomically installs the managed executable",
115 () =>
116 Effect.gen(function* () {
117 const fileSystem = yield* FileSystem.FileSystem;
118 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
119 prefix: "t3-cloudflared-test-",
120 });
121 const bytes = new TextEncoder().encode("test-cloudflared-binary");
122 const manager = yield* makeCloudflaredRelayClient({
123 baseDir,
124 releaseAsset: {
125 url: "https://example.test/cloudflared",
126 sha256: Encoding.encodeHex(sha256(bytes)),
127 archive: "binary",
128 },
129 });
122130
123 const progress: Array<string> = [];
124 const installed = yield* manager.installWithProgress((event) =>
125 Effect.sync(() => {
126 if (event.type === "progress") {
127 progress.push(event.stage);
128 }
129 }),
130 );
131 const managedPath = `${baseDir}/tools/cloudflared/${CLOUDFLARED_VERSION}/linux-x64/cloudflared`;
132 expect(installed).toEqual({
133 status: "available",
134 executablePath: managedPath,
135 source: "managed",
136 version: CLOUDFLARED_VERSION,
137 });
138 expect(new TextDecoder().decode(yield* fileSystem.readFile(managedPath))).toBe(
139 "test-cloudflared-binary",
140 );
141 expect(progress).toEqual([
142 "checking",
143 "waiting_for_lock",
144 "downloading",
145 "verifying",
146 "installing",
147 "validating",
148 "activating",
149 ]);
150 expect(yield* manager.resolve).toEqual(installed);
151 }).pipe(
152 Effect.scoped,
153 Effect.provide(
154 Layer.mergeAll(
155 NodeServices.layer,
156 makeHttpClientLayer(new TextEncoder().encode("test-cloudflared-binary")),
157 makeSpawnerLayer([]),
158 hostRuntimeLayer(),
131 const progress: Array<string> = [];
132 const installed = yield* manager.installWithProgress((event) =>
133 Effect.sync(() => {
134 if (event.type === "progress") {
135 progress.push(event.stage);
136 }
137 }),
138 );
139 const managedPath = `${baseDir}/tools/cloudflared/${CLOUDFLARED_VERSION}/linux-x64/cloudflared`;
140 expect(installed).toEqual({
141 status: "available",
142 executablePath: managedPath,
143 source: "managed",
144 version: CLOUDFLARED_VERSION,
145 });
146 expect(new TextDecoder().decode(yield* fileSystem.readFile(managedPath))).toBe(
147 "test-cloudflared-binary",
148 );
149 expect(progress).toEqual([
150 "checking",
151 "waiting_for_lock",
152 "downloading",
153 "verifying",
154 "installing",
155 "validating",
156 "activating",
157 ]);
158 expect(yield* manager.resolve).toEqual(installed);
159 }).pipe(
160 Effect.scoped,
161 Effect.provide(
162 Layer.mergeAll(
163 NodeServices.layer,
164 makeHttpClientLayer(new TextEncoder().encode("test-cloudflared-binary")),
165 makeSpawnerLayer([]),
166 hostRuntimeLayer(),
167 ),
159168 ),
160169 ),
161 ),
162170 );
163171
164172 it.effect("rejects downloads whose checksum does not match the pinned manifest", () =>
......@@ -192,7 +200,7 @@ describe("RelayClient", () => {
192200 ),
193201 );
194202
195 it.effect("serializes concurrent installs within one runtime", () => {
203 it.effect.skipIf(windowsHost)("serializes concurrent installs within one runtime", () => {
196204 const commands: Array<string> = [];
197205 const bytes = new TextEncoder().encode("test-cloudflared-binary");
198206 return Effect.gen(function* () {
......@@ -227,45 +235,48 @@ describe("RelayClient", () => {
227235 );
228236 });
229237
230 it.effect("observes PATH changes after the manager has been constructed", () => {
231 const env = { PATH: "" };
232 return Effect.gen(function* () {
233 const fileSystem = yield* FileSystem.FileSystem;
234 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
235 prefix: "t3-cloudflared-test-",
236 });
237 const binDir = `${baseDir}/bin`;
238 const executablePath = `${binDir}/cloudflared`;
239 const manager = yield* makeCloudflaredRelayClient({
240 baseDir,
241 });
238 it.effect.skipIf(windowsHost)(
239 "observes PATH changes after the manager has been constructed",
240 () => {
241 const env = { PATH: "" };
242 return Effect.gen(function* () {
243 const fileSystem = yield* FileSystem.FileSystem;
244 const baseDir = yield* fileSystem.makeTempDirectoryScoped({
245 prefix: "t3-cloudflared-test-",
246 });
247 const binDir = `${baseDir}/bin`;
248 const executablePath = `${binDir}/cloudflared`;
249 const manager = yield* makeCloudflaredRelayClient({
250 baseDir,
251 });
242252
243 expect(yield* manager.resolve).toEqual({
244 status: "missing",
245 version: CLOUDFLARED_VERSION,
246 });
253 expect(yield* manager.resolve).toEqual({
254 status: "missing",
255 version: CLOUDFLARED_VERSION,
256 });
247257
248 yield* fileSystem.makeDirectory(binDir);
249 yield* fileSystem.writeFileString(executablePath, "cloudflared");
250 yield* fileSystem.chmod(executablePath, 0o755);
251 env.PATH = binDir;
258 yield* fileSystem.makeDirectory(binDir);
259 yield* fileSystem.writeFileString(executablePath, "cloudflared");
260 yield* fileSystem.chmod(executablePath, 0o755);
261 env.PATH = binDir;
252262
253 expect(yield* manager.resolve).toEqual({
254 status: "available",
255 executablePath,
256 source: "path",
257 version: CLOUDFLARED_VERSION,
258 });
259 }).pipe(
260 Effect.scoped,
261 Effect.provide(
262 Layer.mergeAll(
263 NodeServices.layer,
264 makeHttpClientLayer(new Uint8Array()),
265 makeSpawnerLayer([]),
266 hostRuntimeLayer(env),
263 expect(yield* manager.resolve).toEqual({
264 status: "available",
265 executablePath,
266 source: "path",
267 version: CLOUDFLARED_VERSION,
268 });
269 }).pipe(
270 Effect.scoped,
271 Effect.provide(
272 Layer.mergeAll(
273 NodeServices.layer,
274 makeHttpClientLayer(new Uint8Array()),
275 makeSpawnerLayer([]),
276 hostRuntimeLayer(env),
277 ),
267278 ),
268 ),
269 );
270 });
279 );
280 },
281 );
271282});