Skip to content

Commit 986c163

Browse files
committed
fix(server): match native Codex home expansion
1 parent 2d52654 commit 986c163

2 files changed

Lines changed: 129 additions & 12 deletions

File tree

‎apps/server/src/terminal/Manager.test.ts‎

Lines changed: 124 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2760,17 +2760,128 @@ it.layer(
27602760
}
27612761
}
27622762

2763-
for (const source of ["inherited", "request", "provider", "empty", "unset"] as const) {
2763+
for (const source of ["inherited", "request", "provider"] as const) {
2764+
for (const home of ["~/legacy", "~//legacy"] as const) {
2765+
for (const selectedUnsafe of [true, false]) {
2766+
it.effect.skipIf(!symlinksSupported)(
2767+
`checks one-pass native HOME expansion for ${source} (${home}, ${selectedUnsafe ? "selected unsafe" : "unused unsafe"})`,
2768+
() =>
2769+
Effect.gen(function* () {
2770+
const fs = yield* FileSystem.FileSystem;
2771+
const path = yield* Path.Path;
2772+
const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-native-tilde-" });
2773+
const selected = path.join(root, "~", "legacy", "legacy", ".codex");
2774+
const unexpanded = path.join(root, "~", "legacy", ".codex");
2775+
for (const directory of [selected, unexpanded]) {
2776+
yield* fs.makeDirectory(directory, { recursive: true });
2777+
}
2778+
const unsafe = selectedUnsafe ? selected : unexpanded;
2779+
const database = path.join(root, "database.sqlite");
2780+
const link = path.join(unsafe, "state_5.sqlite");
2781+
yield* fs.writeFileString(database, "preserved database");
2782+
yield* fs.symlink(database, link);
2783+
for (const sidecar of ["wal", "shm", "journal"]) {
2784+
yield* fs.writeFileString(path.join(unsafe, `state_5.sqlite-${sidecar}`), sidecar);
2785+
}
2786+
const providerInstanceId = ProviderInstanceId.make("codex_native_tilde");
2787+
const environment =
2788+
source === "provider" ? [{ name: "HOME", value: home, sensitive: false }] : [];
2789+
const serverSettings = yield* ServerSettings.ServerSettingsService.pipe(
2790+
Effect.provide(
2791+
ServerSettings.layerTest({
2792+
providerInstances: {
2793+
[providerInstanceId]: {
2794+
driver: ProviderDriverKind.make("codex"),
2795+
config: { setupMode: "managed", homePath: "", shadowHomePath: "" },
2796+
environment,
2797+
},
2798+
},
2799+
}),
2800+
),
2801+
);
2802+
const inherited = source === "inherited" ? { HOME: home } : {};
2803+
const { manager, ptyAdapter } = yield* createManager(5, {
2804+
env: inherited,
2805+
resolveProviderInstanceEnvironment: (
2806+
rawProviderInstanceId,
2807+
env,
2808+
inheritedEnvironment,
2809+
cwd,
2810+
) =>
2811+
TerminalManager.resolveProviderInstanceTerminalEnvironment({
2812+
serverSettings,
2813+
path,
2814+
rawProviderInstanceId,
2815+
stateDir: root,
2816+
cwd,
2817+
env,
2818+
inheritedEnvironment,
2819+
}).pipe(
2820+
Effect.provideService(HostProcess.HomeDirectory, path.join(root, "server")),
2821+
),
2822+
});
2823+
const input = openInput({
2824+
providerInstanceId,
2825+
cwd: root,
2826+
env: source === "request" ? { HOME: home } : undefined,
2827+
});
2828+
const original = JSON.stringify([input, inherited, environment]);
2829+
for (let attempt = 0; attempt < 2; attempt += 1) {
2830+
const result = yield* manager.open(input).pipe(Effect.result);
2831+
expect(result._tag).toBe(selectedUnsafe ? "Failure" : "Success");
2832+
if (result._tag === "Failure") {
2833+
expect(result.failure).toMatchObject({
2834+
_tag: "TerminalProviderEnvironmentError",
2835+
cause: { _tag: "CodexShadowHomeSqliteRecoveryRequiredError", linkPath: link },
2836+
});
2837+
expect(result.failure.message).not.toContain(root);
2838+
expect(ptyAdapter.spawnInputs).toHaveLength(0);
2839+
} else {
2840+
expect(ptyAdapter.spawnInputs).toHaveLength(1);
2841+
expect(ptyAdapter.spawnInputs[0]?.env.HOME).toBe(home);
2842+
expect(ptyAdapter.spawnInputs[0]?.env.CODEX_HOME).toBeUndefined();
2843+
expect(ptyAdapter.spawnInputs[0]?.env.CODEX_SQLITE_HOME).toBeUndefined();
2844+
}
2845+
expect(yield* fs.readLink(link)).toBe(database);
2846+
expect(yield* fs.readFileString(database)).toBe("preserved database");
2847+
for (const sidecar of ["wal", "shm", "journal"]) {
2848+
expect(
2849+
yield* fs.readFileString(path.join(unsafe, `state_5.sqlite-${sidecar}`)),
2850+
).toBe(sidecar);
2851+
}
2852+
expect(JSON.stringify([input, inherited, environment])).toBe(original);
2853+
expect(yield* fs.exists(path.join(root, "providers"))).toBe(false);
2854+
}
2855+
}).pipe(Effect.provideService(HostProcess.Platform, "linux")),
2856+
);
2857+
}
2858+
}
2859+
}
2860+
2861+
for (const source of [
2862+
"inherited",
2863+
"request",
2864+
"provider",
2865+
"empty",
2866+
"unset",
2867+
"tilde-inherited",
2868+
"tilde-request",
2869+
"tilde-provider",
2870+
] as const) {
27642871
it.effect.skipIf(!symlinksSupported)(
27652872
`refuses restart before killing an existing terminal selected through ${source} HOME`,
27662873
() =>
27672874
Effect.gen(function* () {
27682875
const fs = yield* FileSystem.FileSystem;
27692876
const path = yield* Path.Path;
27702877
const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-terminal-native-home-" });
2771-
const home = path.join(root, "terminal-home");
2772-
const codexHome = path.join(home, ".codex");
2773-
const accountFallback = source === "empty" || source === "unset";
2878+
const tilde = source.startsWith("tilde-");
2879+
const homeSource = tilde ? source.slice(6) : source;
2880+
const home = tilde ? "~/legacy" : path.join(root, "terminal-home");
2881+
const codexHome = tilde
2882+
? path.join(root, "~", "legacy", "legacy", ".codex")
2883+
: path.join(home, ".codex");
2884+
const accountFallback = homeSource === "empty" || homeSource === "unset";
27742885
yield* fs.makeDirectory(codexHome, { recursive: true });
27752886
const providerInstanceId = ProviderInstanceId.make("codex_native_home");
27762887
const serverSettings = yield* ServerSettings.ServerSettingsService.pipe(
@@ -2781,7 +2892,7 @@ it.layer(
27812892
driver: ProviderDriverKind.make("codex"),
27822893
config: { setupMode: "managed", homePath: "", shadowHomePath: "" },
27832894
environment:
2784-
source === "provider"
2895+
homeSource === "provider"
27852896
? [{ name: "HOME", value: home, sensitive: false }]
27862897
: [],
27872898
},
@@ -2791,9 +2902,9 @@ it.layer(
27912902
);
27922903
const { manager, ptyAdapter } = yield* createManager(5, {
27932904
env:
2794-
source === "unset"
2905+
homeSource === "unset"
27952906
? {}
2796-
: { HOME: source === "inherited" ? home : path.join(root, "server-home") },
2907+
: { HOME: homeSource === "inherited" ? home : path.join(root, "server-home") },
27972908
resolveProviderInstanceEnvironment: (
27982909
rawProviderInstanceId,
27992910
env,
@@ -2820,12 +2931,16 @@ it.layer(
28202931
providerInstanceId,
28212932
cwd: root,
28222933
env:
2823-
source === "empty" ? { HOME: "" } : source === "request" ? { HOME: home } : undefined,
2934+
homeSource === "empty"
2935+
? { HOME: "" }
2936+
: homeSource === "request"
2937+
? { HOME: home }
2938+
: undefined,
28242939
});
28252940
yield* manager.open(input);
28262941
expect(ptyAdapter.spawnInputs).toHaveLength(1);
28272942
expect(ptyAdapter.spawnInputs[0]?.env.HOME).toBe(
2828-
source === "empty" ? "" : source === "unset" ? undefined : home,
2943+
homeSource === "empty" ? "" : homeSource === "unset" ? undefined : home,
28292944
);
28302945
expect(ptyAdapter.spawnInputs[0]?.env.CODEX_HOME).toBeUndefined();
28312946
expect(ptyAdapter.spawnInputs[0]?.env.CODEX_SQLITE_HOME).toBeUndefined();

‎apps/server/src/terminal/Manager.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1422,11 +1422,13 @@ export const resolveProviderInstanceTerminalEnvironment = Effect.fn(
14221422
!explicitHome && platform !== "win32"
14231423
? terminalEnvironment.HOME || (yield* HostProcess.AccountHomeDirectory)
14241424
: undefined;
1425-
// Native HOME goes through Codex's lexical AbsolutePathBuf normalization;
1426-
// explicit CODEX_HOME is canonicalized first and must keep filesystem semantics.
1425+
// Native HOME's AbsolutePathBuf expands '~/' once against HOME itself,
1426+
// then normalizes lexically. Explicit CODEX_HOME is canonicalized first.
14271427
const terminalHome =
14281428
explicitHome ||
1429-
(nativeHome ? input.path.normalize(`${nativeHome}${input.path.sep}.codex`) : undefined);
1429+
(nativeHome
1430+
? input.path.normalize(`${nativeHome}/.codex`.replace(/^~\/+/u, () => `${nativeHome}/`))
1431+
: undefined);
14301432
// Keep the cwd and explicit CODEX_HOME spelling: POSIX traverses symlinks
14311433
// before '..'. Even fs.realpath's JS implementation normalizes lexically.
14321434
// Windows resolves drive-relative paths using Win32 semantics.

0 commit comments

Comments
 (0)