Skip to content

Commit aa8e066

Browse files
molefrogclaude
andcommitted
Fix moi env CLI tests on macOS with a MOI_DATA_DIR seam
The e2e tests isolated the data dir via XDG_DATA_HOME, which env-paths only honors on Linux — on macOS every spawned CLI read the real ~/Library/Application Support/moi registry and exited 1 (18 failures). Add a MOI_DATA_DIR override as the single source of the data dir (server/data-dir.ts) and use it in the tests. Also realpath the temp dirs: macOS tmpdir() is a symlink (/var -> /private/var) and the spawned CLI's resolved cwd must prefix-match the registered path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 07a2ddf commit aa8e066

5 files changed

Lines changed: 29 additions & 18 deletions

File tree

server/data-dir.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Single source of truth for the OS data dir holding moi's global state
2+
// (workspaces.json, thread-config.json, workspace-env.json, …).
3+
//
4+
// MOI_DATA_DIR overrides it wholesale. This is the isolation seam for CLI
5+
// e2e tests: env-paths only honors XDG_DATA_HOME on Linux, so on macOS a
6+
// spawned CLI would otherwise read the developer's real data dir.
7+
import envPaths from 'env-paths'
8+
9+
export const DATA_DIR = process.env.MOI_DATA_DIR || envPaths('moi', { suffix: false }).data

server/registry.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import envPaths from 'env-paths'
21
import { mkdir } from 'node:fs/promises'
32
import { homedir } from 'node:os'
43
import { join, resolve, sep } from 'path'
54

65
import type { DiscoveredWorkspace, WorkspaceEntry, WorkspaceType } from '@/lib/types'
76

7+
import { DATA_DIR } from './data-dir'
88
import { type OpenClawAgent, discoverOpenClawAgents } from './openclaw'
99

1010
// Replace the home-dir prefix with `~` for display. Keeps the original
@@ -22,7 +22,6 @@ function withDisplayPath<T extends { path: string }>(entry: T): T & { displayPat
2222
return { ...entry, displayPath: tildify(entry.path) }
2323
}
2424

25-
const DATA_DIR = envPaths('moi', { suffix: false }).data
2625
export const DEFAULT_REGISTRY_PATH = join(DATA_DIR, 'workspaces.json')
2726

2827
// Overridable for tests

server/test/cli-env.test.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
// End-to-end tests for the `moi env` CLI: each test spawns the real CLI
22
// (`bun server/cli.ts env …`) against a temp workspace, with the registry and
3-
// env stores isolated under a temp XDG_DATA_HOME. MOI_SECRET_BACKEND=file pins
4-
// the file secret store so a test run can never touch the real OS keychain.
3+
// env stores isolated under a temp MOI_DATA_DIR (env-paths only honors
4+
// XDG_DATA_HOME on Linux, so an explicit override is the only portable seam).
5+
// MOI_SECRET_BACKEND=file pins the file secret store so a test run can never
6+
// touch the real OS keychain.
57
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
6-
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
8+
import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'
79
import { tmpdir } from 'node:os'
810
import { join } from 'path'
911

@@ -13,9 +15,6 @@ let dataHome: string
1315
let wsDir: string
1416
let outsideDir: string
1517

16-
// The stores keyed by env-paths land in $XDG_DATA_HOME/moi on Linux and
17-
// $XDG_DATA_HOME/moi (via env-paths override) elsewhere too, because env-paths
18-
// honors XDG_DATA_HOME on every platform when set.
1918
const moiDataDir = () => join(dataHome, 'moi')
2019
const secretsPath = () => join(moiDataDir(), 'workspace-secrets.json')
2120

@@ -40,7 +39,7 @@ async function runCli(
4039
stderr: 'pipe',
4140
env: {
4241
...process.env,
43-
XDG_DATA_HOME: dataHome,
42+
MOI_DATA_DIR: moiDataDir(),
4443
MOI_SECRET_BACKEND: 'file',
4544
// No NO_COLOR override: stdout is a pipe here, exactly like an agent
4645
// capturing the command, so plain output must be the default.
@@ -64,10 +63,15 @@ async function storedSecrets(): Promise<Record<string, Record<string, string>>>
6463
}
6564
}
6665

66+
// realpath matters: on macOS tmpdir() is a symlink (/var → /private/var), and
67+
// the spawned CLI sees the resolved cwd, which must prefix-match the
68+
// registered workspace path.
69+
const tempDir = async (prefix: string) => realpath(await mkdtemp(join(tmpdir(), prefix)))
70+
6771
beforeEach(async () => {
68-
dataHome = await mkdtemp(join(tmpdir(), 'moi-cli-data-'))
69-
wsDir = await mkdtemp(join(tmpdir(), 'moi-cli-ws-'))
70-
outsideDir = await mkdtemp(join(tmpdir(), 'moi-cli-outside-'))
72+
dataHome = await tempDir('moi-cli-data-')
73+
wsDir = await tempDir('moi-cli-ws-')
74+
outsideDir = await tempDir('moi-cli-outside-')
7175
await registerWorkspace(wsDir)
7276
})
7377

@@ -252,7 +256,7 @@ describe('moi env exec', () => {
252256
stderr: 'pipe',
253257
env: {
254258
...process.env,
255-
XDG_DATA_HOME: dataHome,
259+
MOI_DATA_DIR: moiDataDir(),
256260
MOI_SECRET_BACKEND: 'file',
257261
NO_COLOR: '1',
258262
HOME_MADE: 'stale'

server/thread-config.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { mkdir, rename } from 'node:fs/promises'
22
import { join } from 'path'
33

4-
import envPaths from 'env-paths'
5-
64
import type { ThreadConfig } from '@/lib/types'
75

6+
import { DATA_DIR } from './data-dir'
7+
88
// Per-thread model/effort overrides for chat threads. Stored OUTSIDE the user's
99
// workspace (no repo churn) in ONE global JSON file in moi's data dir, alongside
1010
// the workspace registry. Keyed by workspace path then sessionId:
@@ -20,7 +20,6 @@ export type ThreadConfigPatch = {
2020

2121
type Store = Record<string, Record<string, ThreadConfig>>
2222

23-
const DATA_DIR = envPaths('moi', { suffix: false }).data
2423
let _path = join(DATA_DIR, 'thread-config.json')
2524

2625
// Test seam: point the store at a scratch file (mirrors registry.setRegistryPath).

server/workspace-env.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@
1414
// Non-secret metadata (the `inheritDotenv` mode flag) lives in a small JSON
1515
// file in the OS data dir. Secret values live in the SecretStore. The two are
1616
// keyed by the absolute workspace path.
17-
import envPaths from 'env-paths'
1817
import { chmod, mkdir, rename } from 'node:fs/promises'
1918
import { dirname, join, resolve } from 'node:path'
2019
import { parseEnv } from 'node:util'
2120

2221
import type { WorkspaceEnvView } from '@/lib/types'
2322

23+
import { DATA_DIR } from './data-dir'
24+
2425
// Dotenv files scanned in the workspace root, low → high precedence. A later
2526
// file's keys override an earlier file's (base `.env`, machine-local `.env.local`).
2627
const DOTENV_FILES = ['.env', '.env.local'] as const
@@ -32,7 +33,6 @@ export function isValidEnvKey(key: string): boolean {
3233
return ENV_KEY_RE.test(key)
3334
}
3435

35-
const DATA_DIR = envPaths('moi', { suffix: false }).data
3636
const DEFAULT_META_PATH = join(DATA_DIR, 'workspace-env.json')
3737
const DEFAULT_SECRET_PATH = join(DATA_DIR, 'workspace-secrets.json')
3838
// Keychain namespace for Bun.secrets entries.

0 commit comments

Comments
 (0)