import * as NodeServices from "@effect/platform-node/NodeServices"; import { describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as PlatformError from "effect/PlatformError"; import * as Schema from "effect/Schema"; import { CodexSettings } from "@t3tools/contracts"; import { CodexShadowHomeEntryConflictError, CodexShadowHomePathConflictError, materializeCodexShadowHome, resolveCodexHomeLayout, } from "./CodexHomeLayout.ts"; import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; const decodeCodexSettingsValue = Schema.decodeSync(CodexSettings); const decodeCodexSettings = (input: { readonly enabled?: boolean; readonly homePath?: string; readonly shadowHomePath?: string; readonly customModels?: readonly string[]; readonly binaryPath?: string; }): CodexSettings => decodeCodexSettingsValue(input); const makeTempDir = Effect.fn("CodexHomeLayout.test.makeTempDir")(function* (prefix: string) { const fileSystem = yield* FileSystem.FileSystem; return yield* fileSystem.makeTempDirectoryScoped({ prefix }); }); const writeTextFile = Effect.fn("CodexHomeLayout.test.writeTextFile")(function* ( filePath: string, contents: string, ) { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; yield* fileSystem.makeDirectory(path.dirname(filePath), { recursive: true }); yield* fileSystem.writeFileString(filePath, contents); }); it.layer(NodeServices.layer)("CodexHomeLayout", (it) => { describe("resolveCodexHomeLayout", () => { it.effect("uses direct CODEX_HOME when no shadow home is configured", () => Effect.gen(function* () { const homePath = yield* makeTempDir("t3code-codex-home-"); const layout = yield* resolveCodexHomeLayout( decodeCodexSettings({ homePath, }), ); expect(layout).toMatchObject({ mode: "direct", sharedHomePath: homePath, effectiveHomePath: homePath, continuationKey: `codex:home:${homePath}`, }); }), ); it.effect("uses the shared home for continuation and the shadow home for runtime", () => Effect.gen(function* () { 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"); const layout = yield* resolveCodexHomeLayout( decodeCodexSettings({ homePath: sharedHome, shadowHomePath: shadowHome, }), ); expect(layout).toMatchObject({ mode: "authOverlay", sharedHomePath: sharedHome, effectiveHomePath: shadowHome, continuationKey: `codex:home:${sharedHome}`, }); }), ); }); describe("materializeCodexShadowHome", () => { 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"); }), ); it.effect.skipIf(!symlinksSupported)( "replaces Codex-created local MCP OAuth locks with the shared lock directory", () => 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"); const sharedLocks = path.join(sharedHome, "mcp-oauth-locks"); const shadowLocks = path.join(shadowHome, "mcp-oauth-locks"); yield* writeTextFile(path.join(sharedLocks, "file-store.lock"), ""); yield* writeTextFile(path.join(shadowLocks, "file-store.lock"), ""); const layout = yield* resolveCodexHomeLayout( decodeCodexSettings({ homePath: sharedHome, shadowHomePath: shadowHome, }), ); yield* materializeCodexShadowHome(layout); const locksTarget = yield* fileSystem.readLink(shadowLocks); const sharedLockExists = yield* fileSystem.exists( path.join(sharedLocks, "file-store.lock"), ); expect(locksTarget).toBe(sharedLocks); expect(sharedLockExists).toBe(true); }), ); it.effect.skipIf(!symlinksSupported)( "accepts Codex-created shadow-local runtime directories", () => 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, "log")); yield* fileSystem.makeDirectory(path.join(sharedHome, "memories")); yield* fileSystem.makeDirectory(path.join(sharedHome, "tmp")); yield* writeTextFile(path.join(sharedHome, "config.toml"), 'model = "gpt-5-codex"\n'); yield* writeTextFile(path.join(shadowHome, "auth.json"), '{"shadow":true}\n'); yield* fileSystem.makeDirectory(path.join(shadowHome, "log"), { recursive: true }); yield* fileSystem.makeDirectory(path.join(shadowHome, "memories"), { recursive: true }); yield* fileSystem.makeDirectory(path.join(shadowHome, "tmp"), { recursive: true }); const layout = yield* resolveCodexHomeLayout( decodeCodexSettings({ homePath: sharedHome, shadowHomePath: shadowHome, }), ); yield* materializeCodexShadowHome(layout); const configTarget = yield* fileSystem.readLink(path.join(shadowHome, "config.toml")); const logLinkResult = yield* fileSystem .readLink(path.join(shadowHome, "log")) .pipe(Effect.result); const memoriesLinkResult = yield* fileSystem .readLink(path.join(shadowHome, "memories")) .pipe(Effect.result); const tmpLinkResult = yield* fileSystem .readLink(path.join(shadowHome, "tmp")) .pipe(Effect.result); expect(configTarget).toBe(path.join(sharedHome, "config.toml")); expect(logLinkResult._tag).toBe("Failure"); expect(memoriesLinkResult._tag).toBe("Failure"); expect(tmpLinkResult._tag).toBe("Failure"); }), ); it.effect("rejects shadow homes that point at the shared home", () => Effect.gen(function* () { const sharedHome = yield* makeTempDir("t3code-codex-shared-"); const layout = yield* resolveCodexHomeLayout( decodeCodexSettings({ homePath: sharedHome, shadowHomePath: sharedHome, }), ); const error = yield* materializeCodexShadowHome(layout).pipe(Effect.flip); expect(error).toBeInstanceOf(CodexShadowHomePathConflictError); expect(error).toMatchObject({ sharedHomePath: sharedHome, effectiveHomePath: sharedHome, }); expect(error.message).toBe( `Codex shadow home path '${sharedHome}' must be different from the shared home path '${sharedHome}'.`, ); }), ); it.effect.skipIf(!symlinksSupported)( "rejects shared entries that already exist in the shadow home as real files", () => Effect.gen(function* () { 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* writeTextFile(path.join(sharedHome, "config.toml"), 'model = "gpt-5-codex"\n'); yield* writeTextFile(path.join(shadowHome, "config.toml"), 'model = "local"\n'); const layout = yield* resolveCodexHomeLayout( decodeCodexSettings({ homePath: sharedHome, shadowHomePath: shadowHome, }), ); const error = yield* materializeCodexShadowHome(layout).pipe(Effect.flip); expect(error).toBeInstanceOf(CodexShadowHomeEntryConflictError); expect(error).toMatchObject({ sharedHomePath: sharedHome, effectiveHomePath: shadowHome, entryName: "config.toml", linkPath: path.join(shadowHome, "config.toml"), targetPath: path.join(sharedHome, "config.toml"), }); expect(error.message).toBe( `Cannot create Codex shadow home entry 'config.toml' because '${path.join(shadowHome, "config.toml")}' already exists and is not a symlink.`, ); }), ); it.effect("preserves filesystem operation, paths, and cause", () => Effect.gen(function* () { const path = yield* Path.Path; const sharedRoot = yield* makeTempDir("t3code-codex-shared-root-"); const sharedHome = path.join(sharedRoot, "shared-home"); const shadowRoot = yield* makeTempDir("t3code-codex-shadow-root-"); const shadowHome = path.join(shadowRoot, "shadow"); yield* writeTextFile(sharedHome, "not a directory\n"); const layout = yield* resolveCodexHomeLayout( decodeCodexSettings({ homePath: sharedHome, shadowHomePath: shadowHome, }), ); const error = yield* materializeCodexShadowHome(layout).pipe(Effect.flip); expect(error._tag).toBe("CodexShadowHomeFileSystemError"); if (error._tag !== "CodexShadowHomeFileSystemError") { return expect.fail("Expected CodexShadowHomeFileSystemError"); } expect(error).toMatchObject({ operation: "makeDirectory", sharedHomePath: sharedHome, effectiveHomePath: shadowHome, }); expect(error.path.startsWith(sharedHome)).toBe(true); expect(error.cause).toBeInstanceOf(PlatformError.PlatformError); expect(error.message).toBe( `Codex shadow home filesystem operation 'makeDirectory' failed for '${error.path}'.`, ); }), ); }); });