test: skip symlink fixtures where the host cannot create symlinks#9566

merged1
windows-tests/posix-fixtures
windows-tests/symlinks
openedSep 4, 2026byJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 3d ago
2 unresolved threads ยท Jump to file
Selected thread: apps/server/src/provider/Drivers/CodexHomeLayout.test.ts:140 โ€” ### Shadow home tests still need symlinks **Medium Severity** <!-- DESCRIPTION START --> Only the fixture-planting `materializeCodexShadowHome` case is gated on `symlinksSupported`. Sibling tests still call `materializeCodexShadowHome`, which creates file and directory links via `ensureSymlink`. On hosts that cannot create them those cases fail at setup or with `CodexShadowHomeFileSystemError` instead of the expected conflict, so the Windows skip does not cover this file. <!-- DESCRIPTION END --> <!-- BUGBOT_BUG_ID: 0625e808-084c-4aaf-a5c3-61ad508c0709 --> <!-- LOCATIONS START apps/server/src/provider/Drivers/CodexHomeLayout.test.ts#L142-L173 apps/server/src/provider/Drivers/CodexHomeLayout.test.ts#L174-L217 apps/server/src/provider/Drivers/CodexHomeLayout.test.ts#L241-L272 LOCATIONS END --> <details> <summary>Additional Locations (2)</summary> - [`apps/server/src/provider/Drivers/CodexHomeLayout.test.ts#L174-L217`](https://github.com/pingdotgg/t3code/blob/41b026ab4328737b724ffaa40b10e644195c423e/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts#L174-L217) - [`apps/server/src/provider/Drivers/CodexHomeLayout.test.ts#L241-L272`](https://github.com/pingdotgg/t3code/blob/41b026ab4328737b724ffaa40b10e644195c423e/apps/server/src/provider/Drivers/CodexHomeLayout.test.ts#L241-L272) </details> <div><a href="https://cursor.com/open?link=eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9JTl9DVVJTT1IiLCJkYXRhIjp7InJlZGlzS2V5IjoiYnVnYm90OmExZWYzMGQ2LTFlYWQtNGM2Mi04NWU5LTE4ZmY0NWNjM2IzOCIsImVuY3J5cHRpb25LZXkiOiJyUkpPaWZhU2J1ak9vSktKLTVKWmhEbVVvM2NwSlQwSkU3WmtWb054X1Q0IiwiYnJhbmNoIjoid2luZG93cy10ZXN0cy9zeW1saW5rcyIsInJlcG9Pd25lciI6InBpbmdkb3RnZyIsInJlcG9OYW1lIjoidDNjb2RlIiwicHJvdmlkZXIiOiJnaXRodWIifX0" target="_blank" rel="noopener noreferrer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/fix-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/fix-in-cursor-light.png"><img alt="Fix in Cursor" width="115" height="28" src="https://cursor.com/assets/images/fix-in-cursor-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/agents?link=eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiQlVHQk9UX0ZJWF9JTl9XRUIiLCJkYXRhIjp7InJlZGlzS2V5IjoiYnVnYm90OmExZWYzMGQ2LTFlYWQtNGM2Mi04NWU5LTE4ZmY0NWNjM2IzOCIsImVuY3J5cHRpb25LZXkiOiJyUkpPaWZhU2J1ak9vSktKLTVKWmhEbVVvM2NwSlQwSkU3WmtWb054X1Q0IiwiYnJhbmNoIjoid2luZG93cy10ZXN0cy9zeW1saW5rcyIsInJlcG9Pd25lciI6InBpbmdkb3RnZyIsInJlcG9OYW1lIjoidDNjb2RlIiwicHJOdW1iZXIiOjk1NjYsImNvbW1pdFNoYSI6IjQxYjAyNmFiNDMyODczN2I3MjRmZmFhNDBiMTBlNjQ0MTk1YzQyM2UiLCJwcm92aWRlciI6ImdpdGh1YiJ9fQ" target="_blank" rel="noopener noreferrer"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/fix-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/fix-in-web-light.png"><img alt="Fix in Web" width="99" height="28" src="https://cursor.com/assets/images/fix-in-web-dark.png"></picture></a></div> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 41b026ab4328737b724ffaa40b10e644195c423e. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
0 / 20 viewed20 files changedLine totals unavailable

apps/server/src/provider/Drivers/CodexHomeLayout.test.ts ยท Earlier revision 41b026ab ยท right 140

Original diff
@@ -84,54 +85,59 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => {
   });
 
   describe("materializeCodexShadowHome", () => {
-    it.effect("materializes a shadow home with shared state links and private auth", () =>
-      Effect.gen(function* () {
-        const fileSystem = yield* FileSystem.FileSystem;
-        const path = yield* Path.Path;
-        const sharedHome = yield* makeTempDir("t3code-codex-shared-");
-        const shadowRoot = yield* makeTempDir("t3code-codex-shadow-root-");
-        const shadowHome = path.join(shadowRoot, "shadow");
-
-        yield* fileSystem.makeDirectory(path.join(sharedHome, "sessions"));
-        yield* writeTextFile(path.join(sharedHome, "config.toml"), 'model = "gpt-5-codex"\n');
-        yield* writeTextFile(path.join(sharedHome, "models_cache.json"), '{"models":["shared"]}\n');
-        yield* writeTextFile(path.join(sharedHome, "auth.json"), '{"shared":true}\n');
-        yield* fileSystem.makeDirectory(shadowHome, { recursive: true });
-        yield* writeTextFile(path.join(shadowHome, "auth.json"), '{"shadow":true}\n');
-        yield* fileSystem.symlink(
-          path.join(sharedHome, "models_cache.json"),
-          path.join(shadowHome, "models_cache.json"),
-        );
-
-        const layout = yield* resolveCodexHomeLayout(
-          decodeCodexSettings({
-            homePath: sharedHome,
-            shadowHomePath: shadowHome,
-          }),
-        );
-
-        yield* materializeCodexShadowHome(layout);
-
-        const sessionsTarget = yield* fileSystem.readLink(path.join(shadowHome, "sessions"));
-        const configTarget = yield* fileSystem.readLink(path.join(shadowHome, "config.toml"));
-        const mcpOauthLocksTarget = yield* fileSystem.readLink(
-          path.join(shadowHome, "mcp-oauth-locks"),
-        );
-        const modelsCacheExists = yield* fileSystem.exists(
-          path.join(shadowHome, "models_cache.json"),
-        );
-        const authLinkResult = yield* fileSystem
-          .readLink(path.join(shadowHome, "auth.json"))
-          .pipe(Effect.result);
-        const authContents = yield* fileSystem.readFileString(path.join(shadowHome, "auth.json"));
-
-        expect(sessionsTarget).toBe(path.join(sharedHome, "sessions"));
-        expect(configTarget).toBe(path.join(sharedHome, "config.toml"));
-        expect(mcpOauthLocksTarget).toBe(path.join(sharedHome, "mcp-oauth-locks"));
-        expect(modelsCacheExists).toBe(false);
-        expect(authLinkResult._tag).toBe("Failure");
-        expect(authContents).toContain("shadow");
-      }),
+    it.effect.skipIf(!symlinksSupported)(
+      "materializes a shadow home with shared state links and private auth",
+      () =>
+        Effect.gen(function* () {
+          const fileSystem = yield* FileSystem.FileSystem;
+          const path = yield* Path.Path;
+          const sharedHome = yield* makeTempDir("t3code-codex-shared-");
+          const shadowRoot = yield* makeTempDir("t3code-codex-shadow-root-");
+          const shadowHome = path.join(shadowRoot, "shadow");
+
+          yield* fileSystem.makeDirectory(path.join(sharedHome, "sessions"));
+          yield* writeTextFile(path.join(sharedHome, "config.toml"), 'model = "gpt-5-codex"\n');
+          yield* writeTextFile(
+            path.join(sharedHome, "models_cache.json"),
+            '{"models":["shared"]}\n',
+          );
+          yield* writeTextFile(path.join(sharedHome, "auth.json"), '{"shared":true}\n');
+          yield* fileSystem.makeDirectory(shadowHome, { recursive: true });
+          yield* writeTextFile(path.join(shadowHome, "auth.json"), '{"shadow":true}\n');
+          yield* fileSystem.symlink(
+            path.join(sharedHome, "models_cache.json"),
+            path.join(shadowHome, "models_cache.json"),
+          );
+
+          const layout = yield* resolveCodexHomeLayout(
+            decodeCodexSettings({
+              homePath: sharedHome,
+              shadowHomePath: shadowHome,
+            }),
+          );
+
+          yield* materializeCodexShadowHome(layout);
+
+          const sessionsTarget = yield* fileSystem.readLink(path.join(shadowHome, "sessions"));
+          const configTarget = yield* fileSystem.readLink(path.join(shadowHome, "config.toml"));
+          const mcpOauthLocksTarget = yield* fileSystem.readLink(
+            path.join(shadowHome, "mcp-oauth-locks"),
+          );
+          const modelsCacheExists = yield* fileSystem.exists(
+            path.join(shadowHome, "models_cache.json"),
+          );
+          const authLinkResult = yield* fileSystem
+            .readLink(path.join(shadowHome, "auth.json"))
+            .pipe(Effect.result);
+          const authContents = yield* fileSystem.readFileString(path.join(shadowHome, "auth.json"));
+
+          expect(sessionsTarget).toBe(path.join(sharedHome, "sessions"));
+          expect(configTarget).toBe(path.join(sharedHome, "config.toml"));
+          expect(mcpOauthLocksTarget).toBe(path.join(sharedHome, "mcp-oauth-locks"));
+          expect(modelsCacheExists).toBe(false);
+          expect(authLinkResult._tag).toBe("Failure");
+          expect(authContents).toContain("shadow");
+        }),
C
cursorBotSep 4, 2026

Shadow home tests still need symlinks

Medium Severity

Only the fixture-planting materializeCodexShadowHome case is gated on symlinksSupported. Sibling tests still call materializeCodexShadowHome, which creates file and directory links via ensureSymlink. On hosts that cannot create them those cases fail at setup or with CodexShadowHomeFileSystemError instead of the expected conflict, so the Windows skip does not cover this file.

Additional Locations (2)

Reviewed by Cursor Bugbot for commit 41b026ab4328737b724ffaa40b10e644195c423e. Configure here.

packages/shared/src/testing/symlinks.ts ยท Earlier revision 13f4759d ยท right 21โ€“21

Original diff
@@ -0,0 +1,23 @@
+// @effect-diagnostics nodeBuiltinImport:off - one synchronous probe at module load, outside any Effect runtime.
+import * as NodeFS from "node:fs";
+import * as NodeOS from "node:os";
+import * as NodePath from "node:path";
+
+/**
+ * Whether this host lets an unprivileged process create symlinks. Windows
+ * refuses without Developer Mode or elevation, so tests that plant a symlink
+ * to prove containment or escape detection should `skipIf(!symlinksSupported)`
+ * rather than fail at setup. Probed once per process.
+ */
+export const symlinksSupported: boolean = (() => {
+  let directory: string | undefined;
+  try {
+    directory = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-symlink-probe-"));
+    NodeFS.symlinkSync(NodePath.join(directory, "target"), NodePath.join(directory, "link"));
+    return true;
+  } catch {
+    return false;
+  } finally {
+    if (directory !== undefined) NodeFS.rmSync(directory, { recursive: true, force: true });

packages/shared/src/testing/symlinks.ts ยท right 21โ€“21

M
macroscopeappBotSep 4, 2026

๐ŸŸก Medium testing/symlinks.ts:21

A cleanup error from rmSync escapes the probe and makes importing this helper throw instead of returning false or true, so every importing test fails during module initialization when Windows temporarily reports EPERM or EBUSY. Catch cleanup failures so they cannot override the probe result.

No longer relevant as of 8b79a693fbb1e76f4ffa6256b03e21162dabedf1

J
Julius MarmingeSep 4, 2026

Good call โ€” a failed cleanup shouldn't decide the probe. Caught and ignored now.

M
macroscopeappBotSep 4, 2026

I still see rmSync called without a nested try/catch, so a cleanup error can still escape the probe.