Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/codex/home.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
import { homedir } from "node:os";
import { join, posix, resolve, win32 } from "node:path";
import { expandUserPath } from "../config";
import { expandUserPath } from "../config/paths";
import { redactUserPath } from "../lib/redact";

export type CodexHomeDeps = {
Expand Down
1 change: 1 addition & 0 deletions structure/codex-home.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ user off a Windows home they were running against, and a stat failure other than
local home rather than switching to a different one. Codex runtime discovery (src/codex/runtime.ts) also reads this home on Linux: after an explicit runtime, PATH, and the ordinary install locations, it enumerates the direct children of <effective CODEX_HOME>/bin/wsl/<version-hash>/codex newest first, probes each through the isolated --version seam, and re-enumerates on every resolve so a Desktop update that replaces the hash directory is picked up (issue 5635). An explicitly
set path that is unreadable or not a directory is an error, not a fallback: silently using a
different home than the operator named would write provider state where nobody is looking for it. A fresh install can have that directory but no `config.toml` yet; applying the integration then creates an empty `config.toml` there (never overwriting an existing file) and continues, while a missing home directory is refused with instructions to start Codex once or set `CODEX_HOME` (issue 5422).
`src/codex/home.ts` imports path expansion directly from `src/config/paths.ts`, so a fresh WSL process can resolve its Codex home without re-entering the config facade before the resolver initializes.
The managed files are:

```text
Expand Down
14 changes: 11 additions & 3 deletions tests/cli/cli-help.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { Database } from "bun:sqlite";
import { EXPORT_CLIENT_IDS } from "../../src/clients/config-export";
import { isSystemd, unitPath } from "../../src/service/systemd";
import { SPAWN_BUDGET_MS } from "../helpers/test-budget";
import { removeTreeWithRetry } from "../helpers/remove-tree";

const repoRoot = dirname(fileURLToPath(new URL("../../package.json", import.meta.url)));
const cliPath = join(repoRoot, "src", "cli", "index.ts");
const binPath = join(repoRoot, "bin", "ocx.mjs");
const dockerHost = process.platform === "linux" && existsSync("/.dockerenv");

// Every case below spawns the real CLI. A hung child without a spawnSync timeout can
// pin the whole shard for the full 15-minute CI budget (observed on Linux test 3/4
Expand Down Expand Up @@ -197,7 +199,9 @@ describe("CLI subcommand help", () => {
expect(result.stdout).toContain("Default provider: openai");
expect(result.stdout).toContain("Codex autostart: disabled");
expect(result.stdout).toContain("Service:");
expect(result.stdout).toContain(join(opencodexHome, "service.log"));
if (dockerHost) expect(result.stdout).toContain("Service: unsupported in Docker");
else if (process.platform === "linux" && !isSystemd()) expect(result.stdout).toContain("Service: unsupported: systemd not found");
else expect(result.stdout).toContain(join(opencodexHome, "service.log"));
expect(result.stdout).toContain("Codex autostart shim");
// #2411: status must name the routing kind it already computes. The
// proxy is down in this fixture, so the unused-proxy warning must stay
Expand Down Expand Up @@ -411,9 +415,13 @@ describe("CLI subcommand help", () => {
expect(result.stdout).toContain("Usage: ocx start [--port <port>] [--socks5 [host:port] | --socks5-off]");
});

test("invalid service and codex-shim usage include remove alias", () => {
test("invalid service and codex-shim usage fail with platform guidance", () => {
const cases = [
{ args: ["service", "nope"], expected: "Usage: ocx service [install|repair|restart|start|stop|status|uninstall|remove|claim]" },
{ args: ["service", "nope"], expected: dockerHost
? "Docker detected. Run 'ocx start' directly instead of using the service manager."
: process.platform === "linux" && !isSystemd() && !existsSync(unitPath())
? "systemd not found. Run 'ocx start' under your process supervisor."
: "Usage: ocx service [install|repair|restart|start|stop|status|uninstall|remove|claim]" },
{ args: ["codex-shim", "nope"], expected: "Usage: ocx codex-shim <install|status|uninstall|remove>" },
];

Expand Down
31 changes: 31 additions & 0 deletions tests/codex-integration/codex-home-wsl.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,41 @@
import { describe, expect, test } from "bun:test";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { defaultCodexHome, wslAutomountRoot, listWslWindowsCodexHomes } from "../../src/codex/home";
import { isWindowsInteropDir } from "../../src/codex/shim";
import { currentServiceHomes, serviceCodexHomeMatchesInstall } from "../../src/service";
import { removeTreeWithRetry } from "../helpers/remove-tree";
import { repoPath } from "../helpers/repo-root";

describe("wsl.conf automount root", () => {
test("loads and expands the home resolver first in a fresh WSL-like process", async () => {
const home = mkdtempSync(join(tmpdir(), "ocx-wsl-import-"));
try {
const child = Bun.spawn([process.execPath, "--eval", `
const { wslAutomountRoot, resolveCodexHomeDir } = await import("./src/codex/home.ts");
console.log(wslAutomountRoot({ wslConf: null }));
console.log(resolveCodexHomeDir());
`], {
cwd: repoPath(),
env: { ...process.env, HOME: home, USERPROFILE: home, CODEX_HOME: "~/.codex", WSL_DISTRO_NAME: "Ubuntu" },
stdout: "pipe",
stderr: "pipe",
timeout: 10_000,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(child.stdout).text(),
new Response(child.stderr).text(),
child.exited,
]);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
expect(stdout.trim().split(/\r?\n/)).toEqual(["/mnt", join(home, ".codex")]);
} finally {
removeTreeWithRetry(home);
}
}, 15_000);

test("defaults to /mnt when wsl.conf is absent or silent", () => {
expect(wslAutomountRoot({ wslConf: null })).toBe("/mnt");
expect(wslAutomountRoot({ wslConf: "[boot]\nsystemd=true\n" })).toBe("/mnt");
Expand Down
11 changes: 10 additions & 1 deletion tests/service/service-probe-docker.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from "bun:test";
import { mkdtempSync} from "node:fs";
import { existsSync, mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

Expand All @@ -8,6 +8,8 @@ import {
type ProbeRunner,
} from "../../src/service-manager-probe";
import { removeTreeWithRetry } from "../helpers/remove-tree";
import { serviceLogPath, serviceStatusSummary } from "../../src/service";
import { isSystemd } from "../../src/service/systemd";

test("Linux reports systemd absent when systemctl cannot be spawned", () => {
const home = mkdtempSync(join(tmpdir(), "ocx-probe-docker-"));
Expand All @@ -27,3 +29,10 @@ test("Linux reports systemd absent when systemctl cannot be spawned", () => {
removeTreeWithRetry(home);
}
});

test("status summary reports service availability or the service log path", () => {
const summary = serviceStatusSummary();
if (process.platform === "linux" && existsSync("/.dockerenv")) expect(summary).toBe("unsupported in Docker");
else if (process.platform === "linux" && !isSystemd()) expect(summary).toBe("unsupported: systemd not found");
else expect(summary).toContain(serviceLogPath());
});
8 changes: 1 addition & 7 deletions tests/service/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { pathToFileURL } from "node:url";
import * as serviceModule from "../../src/service";
import { saveConfig } from "../../src/config";
import { windowsEnvIndirectBatchValue } from "../../src/lib/win-paths";
import { assertServiceAuthEnvironment, assertServiceEnvironmentMatchesInstall, bakedServicePathsDiagnostic, confirmServiceServing, launchdListenPort, systemdListenPort, buildPlist, buildUnit, buildWindowsLauncherVbs, buildWindowsSchtasksCreateArgs, buildWindowsSchtasksCreateArgsForXml, buildWindowsServiceScript, buildWindowsTaskXml as buildWindowsTaskXmlProduction, buildWindowsTaskXmlDocument, deriveWindowsServiceDiagnostic, deriveWindowsServiceDiagnosticForCurrentUser, expectedLaunchdCommand, installFreshWindowsSchedulerSafely, installServiceSafely, launchctlLoadFailed, launchdJobMatchesPlist, normalizeServiceSubcommand, parseServiceArgs, parseServiceInstallState, planServiceCommand, prepareServiceInstall, probeServiceInstallation, readWindowsSchedulerXmlState, registerFreshWindowsSchedulerTask, removeNativeWindowsServiceForScheduler, repairService, reportServiceServing, resolveServiceListenPort, runLaunchctl, selectServiceSubcommand, SERVICE_INSTALL_HEALTH_MS, SERVICE_INSTALL_HEALTH_WINDOWS_MS, serviceInstallHealthMs, serviceLogPath, serviceStartableFromTray, serviceStatusReport, serviceRetryCommand, serviceStatusSummary, stableLauncherEntry, systemdNeedsDaemonReload, systemdServiceInstallCleanupOps, uninstallSystemd, windowsListenPort, winswListenPort, startLaunchd, windowsTaskRegistrationHealthy as windowsTaskRegistrationHealthyProduction } from "../../src/service";
import { assertServiceAuthEnvironment, assertServiceEnvironmentMatchesInstall, bakedServicePathsDiagnostic, confirmServiceServing, launchdListenPort, systemdListenPort, buildPlist, buildUnit, buildWindowsLauncherVbs, buildWindowsSchtasksCreateArgs, buildWindowsSchtasksCreateArgsForXml, buildWindowsServiceScript, buildWindowsTaskXml as buildWindowsTaskXmlProduction, buildWindowsTaskXmlDocument, deriveWindowsServiceDiagnostic, deriveWindowsServiceDiagnosticForCurrentUser, expectedLaunchdCommand, installFreshWindowsSchedulerSafely, installServiceSafely, launchctlLoadFailed, launchdJobMatchesPlist, normalizeServiceSubcommand, parseServiceArgs, parseServiceInstallState, planServiceCommand, prepareServiceInstall, probeServiceInstallation, readWindowsSchedulerXmlState, registerFreshWindowsSchedulerTask, removeNativeWindowsServiceForScheduler, repairService, reportServiceServing, resolveServiceListenPort, runLaunchctl, selectServiceSubcommand, SERVICE_INSTALL_HEALTH_MS, SERVICE_INSTALL_HEALTH_WINDOWS_MS, serviceInstallHealthMs, serviceLogPath, serviceStartableFromTray, serviceStatusReport, serviceRetryCommand, stableLauncherEntry, systemdNeedsDaemonReload, systemdServiceInstallCleanupOps, uninstallSystemd, windowsListenPort, winswListenPort, startLaunchd, windowsTaskRegistrationHealthy as windowsTaskRegistrationHealthyProduction } from "../../src/service";
import type { ServiceDiagnostic } from "../../src/service";
import { definitionCarriesCredential, resolvedProxyEnv, writeServiceApiTokenFile, writeServiceDefinitionFile } from "../../src/service";
import { buildWinswXml } from "../../src/lib/winsw";
Expand Down Expand Up @@ -2719,12 +2719,6 @@ describe("service diagnostics", () => {
expect(parseServiceInstallState({ ...valid, version: 1, backend: undefined })?.version).toBe(1);
});

test("status summary exposes the service log path", () => {
const summary = serviceStatusSummary();

expectTextToContainPath(summary, serviceLogPath());
});

test("flags stale baked service paths recorded at install time", () => {
const oldOpenCodexHome = process.env.OPENCODEX_HOME;
const stateDir = join(TEST_DIR, "baked-paths-home");
Expand Down
Loading