From 0920c6e981aed3abd86131725ef718102e6145f8 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Sun, 17 May 2026 17:13:12 +0200 Subject: [PATCH] refactor(paths): replace eager constants with lazy getters (N4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Theme N4 — Drop eager paths.ts constants. The bare exports `codexDir`, `accountsDir`, `authPath`, `currentNamePath`, `registryPath`, and `sessionMapPath` were evaluated at module import time, so env-var overrides (`CODEX_AUTH_CODEX_DIR`, `CODEX_AUTH_JSON_PATH`, etc.) set after the first `import` had no effect. That broke tests that set the env after `require()`, and broke systemd `--user` daemons that inherited a different `HOME` than the user-facing CLI. All internal call sites already use the `resolveX()` functions; no conversions were needed in this PR. The bare constants are now marked `@deprecated` and scheduled for removal in v0.2.0 so any external library consumers get a one-release migration window. A new `src/tests/paths.test.ts` proves the resolvers track env-var changes applied after module load, and includes a regression guard asserting the deprecated bare constants do NOT track env-var changes (so we know the defect is gone when v0.2.0 removes them). Exit criteria (docs/future/17-ROADMAP.md Theme N4): - No file imports the bare constants. - Constants marked @deprecated with one-release removal note. - Test under src/tests/paths.test.ts proves env-var changes apply after module load. --- src/lib/config/paths.ts | 31 +++++++ src/tests/paths.test.ts | 199 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 230 insertions(+) create mode 100644 src/tests/paths.test.ts diff --git a/src/lib/config/paths.ts b/src/lib/config/paths.ts index 31eb6a4..4667afd 100644 --- a/src/lib/config/paths.ts +++ b/src/lib/config/paths.ts @@ -59,9 +59,40 @@ export function resolveSnapshotBackupDir(): string { return path.join(resolveAccountsDir(), ".snapshot-backups"); } +/** + * @deprecated Use {@link resolveCodexDir} — this constant is evaluated at + * module import time, so env-var overrides (`CODEX_AUTH_CODEX_DIR`, `HOME`) + * set after the first `import` have no effect. Scheduled for removal in + * v0.2.0 (Theme N4, `docs/future/17-ROADMAP.md`). + */ export const codexDir: string = resolveCodexDir(); +/** + * @deprecated Use {@link resolveAccountsDir} — eager binding ignores + * env-var overrides set after import. Scheduled for removal in v0.2.0 + * (Theme N4, `docs/future/17-ROADMAP.md`). + */ export const accountsDir: string = resolveAccountsDir(); +/** + * @deprecated Use {@link resolveAuthPath} — eager binding ignores env-var + * overrides set after import. Scheduled for removal in v0.2.0 (Theme N4, + * `docs/future/17-ROADMAP.md`). + */ export const authPath: string = resolveAuthPath(); +/** + * @deprecated Use {@link resolveCurrentNamePath} — eager binding ignores + * env-var overrides set after import. Scheduled for removal in v0.2.0 + * (Theme N4, `docs/future/17-ROADMAP.md`). + */ export const currentNamePath: string = resolveCurrentNamePath(); +/** + * @deprecated Use {@link resolveRegistryPath} — eager binding ignores + * env-var overrides set after import. Scheduled for removal in v0.2.0 + * (Theme N4, `docs/future/17-ROADMAP.md`). + */ export const registryPath: string = resolveRegistryPath(); +/** + * @deprecated Use {@link resolveSessionMapPath} — eager binding ignores + * env-var overrides set after import. Scheduled for removal in v0.2.0 + * (Theme N4, `docs/future/17-ROADMAP.md`). + */ export const sessionMapPath: string = resolveSessionMapPath(); diff --git a/src/tests/paths.test.ts b/src/tests/paths.test.ts new file mode 100644 index 0000000..8ecb9ff --- /dev/null +++ b/src/tests/paths.test.ts @@ -0,0 +1,199 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fsp from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +import { + resolveAccountsDir, + resolveAuthPath, + resolveCodexDir, + resolveCurrentNamePath, + resolveRegistryPath, + resolveSessionMapPath, + // Deprecated bare-constant exports — kept for one release per Theme N4. + // Imported here so the regression guard below can prove they do NOT track + // env-var changes after module load (which is exactly why they are + // deprecated). When v0.2.0 removes them, delete this import along with the + // dedicated regression-guard test below. + codexDir as eagerCodexDir, + accountsDir as eagerAccountsDir, + authPath as eagerAuthPath, + currentNamePath as eagerCurrentNamePath, + registryPath as eagerRegistryPath, + sessionMapPath as eagerSessionMapPath, +} from "../lib/config/paths"; + +type EnvKey = + | "CODEX_AUTH_CODEX_DIR" + | "CODEX_AUTH_ACCOUNTS_DIR" + | "CODEX_AUTH_JSON_PATH" + | "CODEX_AUTH_CURRENT_PATH" + | "CODEX_AUTH_SESSION_MAP_PATH"; + +const ENV_KEYS: EnvKey[] = [ + "CODEX_AUTH_CODEX_DIR", + "CODEX_AUTH_ACCOUNTS_DIR", + "CODEX_AUTH_JSON_PATH", + "CODEX_AUTH_CURRENT_PATH", + "CODEX_AUTH_SESSION_MAP_PATH", +]; + +function snapshotEnv(): Record { + const snap = {} as Record; + for (const key of ENV_KEYS) { + snap[key] = process.env[key]; + } + return snap; +} + +function restoreEnv(snap: Record): void { + for (const key of ENV_KEYS) { + const value = snap[key]; + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } +} + +async function withTmpDir(fn: (dir: string) => Promise): Promise { + const dir = await fsp.mkdtemp(path.join(os.tmpdir(), "authmux-paths-")); + try { + return await fn(dir); + } finally { + await fsp.rm(dir, { recursive: true, force: true }); + } +} + +test("resolveCodexDir() reflects CODEX_AUTH_CODEX_DIR set after module load", async () => { + const snap = snapshotEnv(); + try { + await withTmpDir(async (dirA) => { + process.env.CODEX_AUTH_CODEX_DIR = dirA; + assert.equal(resolveCodexDir(), path.resolve(dirA)); + + await withTmpDir(async (dirB) => { + process.env.CODEX_AUTH_CODEX_DIR = dirB; + assert.equal(resolveCodexDir(), path.resolve(dirB)); + + delete process.env.CODEX_AUTH_CODEX_DIR; + assert.equal(resolveCodexDir(), path.join(os.homedir(), ".codex")); + }); + }); + } finally { + restoreEnv(snap); + } +}); + +test("resolveAccountsDir() prefers CODEX_AUTH_ACCOUNTS_DIR, falls back under codex dir", async () => { + const snap = snapshotEnv(); + try { + await withTmpDir(async (codex) => { + process.env.CODEX_AUTH_CODEX_DIR = codex; + delete process.env.CODEX_AUTH_ACCOUNTS_DIR; + assert.equal(resolveAccountsDir(), path.join(path.resolve(codex), "accounts")); + + await withTmpDir(async (override) => { + process.env.CODEX_AUTH_ACCOUNTS_DIR = override; + assert.equal(resolveAccountsDir(), path.resolve(override)); + }); + }); + } finally { + restoreEnv(snap); + } +}); + +test("resolveAuthPath() reflects CODEX_AUTH_JSON_PATH set after module load", async () => { + const snap = snapshotEnv(); + try { + await withTmpDir(async (dir) => { + const target = path.join(dir, "auth.json"); + process.env.CODEX_AUTH_JSON_PATH = target; + assert.equal(resolveAuthPath(), path.resolve(target)); + + delete process.env.CODEX_AUTH_JSON_PATH; + process.env.CODEX_AUTH_CODEX_DIR = dir; + assert.equal(resolveAuthPath(), path.join(path.resolve(dir), "auth.json")); + }); + } finally { + restoreEnv(snap); + } +}); + +test("resolveCurrentNamePath() reflects CODEX_AUTH_CURRENT_PATH set after module load", async () => { + const snap = snapshotEnv(); + try { + await withTmpDir(async (dir) => { + const target = path.join(dir, "current"); + process.env.CODEX_AUTH_CURRENT_PATH = target; + assert.equal(resolveCurrentNamePath(), path.resolve(target)); + }); + } finally { + restoreEnv(snap); + } +}); + +test("resolveRegistryPath() and resolveSessionMapPath() track CODEX_AUTH_ACCOUNTS_DIR", async () => { + const snap = snapshotEnv(); + try { + await withTmpDir(async (accounts) => { + process.env.CODEX_AUTH_ACCOUNTS_DIR = accounts; + delete process.env.CODEX_AUTH_SESSION_MAP_PATH; + assert.equal( + resolveRegistryPath(), + path.join(path.resolve(accounts), "registry.json"), + ); + assert.equal( + resolveSessionMapPath(), + path.join(path.resolve(accounts), "sessions.json"), + ); + + await withTmpDir(async (sessions) => { + const override = path.join(sessions, "sessions.json"); + process.env.CODEX_AUTH_SESSION_MAP_PATH = override; + assert.equal(resolveSessionMapPath(), path.resolve(override)); + }); + }); + } finally { + restoreEnv(snap); + } +}); + +test("deprecated bare constants do NOT track env-var changes (regression guard)", async () => { + // Snapshot the eager values once. They were bound at module import time, + // before this test changed any env vars. Mutating env after import must + // not change them — that is the very defect Theme N4 documents and is + // the reason callers must use the resolveX() functions. + const initialCodexDir = eagerCodexDir; + const initialAccountsDir = eagerAccountsDir; + const initialAuthPath = eagerAuthPath; + const initialCurrentNamePath = eagerCurrentNamePath; + const initialRegistryPath = eagerRegistryPath; + const initialSessionMapPath = eagerSessionMapPath; + + const snap = snapshotEnv(); + try { + await withTmpDir(async (dir) => { + process.env.CODEX_AUTH_CODEX_DIR = dir; + process.env.CODEX_AUTH_ACCOUNTS_DIR = path.join(dir, "alt-accounts"); + process.env.CODEX_AUTH_JSON_PATH = path.join(dir, "alt-auth.json"); + process.env.CODEX_AUTH_CURRENT_PATH = path.join(dir, "alt-current"); + process.env.CODEX_AUTH_SESSION_MAP_PATH = path.join(dir, "alt-sessions.json"); + + assert.equal(eagerCodexDir, initialCodexDir); + assert.equal(eagerAccountsDir, initialAccountsDir); + assert.equal(eagerAuthPath, initialAuthPath); + assert.equal(eagerCurrentNamePath, initialCurrentNamePath); + assert.equal(eagerRegistryPath, initialRegistryPath); + assert.equal(eagerSessionMapPath, initialSessionMapPath); + + // And confirm the resolvers DO pick up the override, so the + // documented migration target genuinely fixes the bug. + assert.notEqual(resolveCodexDir(), eagerCodexDir); + }); + } finally { + restoreEnv(snap); + } +});