Files
openclaw/test/test-env.test.ts
Peter Steinberger ccd3fbb918 fix(gateway): install despite stale Linux session bus addresses (#146569)
## What Problem This Solves

Fresh Linux Gateway installs can report `SERVICE_DEFINITION_UNKNOWN` when the shell carries a stale session-bus address, even though status reaches the running user manager. Replacing every explicit address with the runtime bus also breaks updates for a working custom bus when an unrelated runtime socket exists.

## User Impact

Gateway installation works with healthy, stale, or missing shell bus settings when the user manager is reachable. Working custom buses remain usable for installation and update admission. Install and status share the service inspection verdict; unavailable managers receive actionable session-repair guidance, while unreadable or foreign definitions identify the affected file.

## Why This Change Was Made

One transport owner probes `Manager.Version` without activation, preserving a working explicit address before trying the runtime bus and private socket. Existing machine/sudo routing remains at that owner. Child commands, effective-command inspection, and update admission consume the selected route; deep status exposes it. A socket's existence does not establish that another address is stale.

The owner records whether an earlier probe timed out. Later admission rechecks that unresolved preference within its own remaining deadline, including when it joined another caller's short discovery. Ordinary commands reuse the positive route. Native locality checks apply to the selected transport, and admitted peer connections retain their unique-owner, UID/PID/start-time, deadline, and authority checks.

The filesystem override, per-caller private-first decision, and duplicate command-time machine fallback were removed. Payload bus/account variables cannot redirect supervisor inspection. Private-socket reopen failures and discovery deadlines retain manager diagnostics. Existing publication backups and rollback remain unchanged; no configuration options, dependencies, protocol changes, or storage migrations were added.

Refs #146022, #145023. Reported in Discord support; no contributor code adopted.

## Evidence

The Debian 12 rig uses systemd `252.39-1~deb12u2`, a synthetic non-root account, and the built `dist/entry.js` from `/tmp`. The custom variant runs the manager on `/tmp/dbus-custom` with an unrelated broker at the runtime bus path.

```text
Before round three, built head 25e675a6d7:
  healthy:     install succeeds; runtime running; RPC true
  stale:       install succeeds; runtime running; RPC true
  private-only: install succeeds; runtime running; RPC true
  unavailable: actionable manager diagnosis
  custom bus:  Manager.Version succeeds; install/status succeed
               update admission fails
  Move only the unrelated runtime socket aside: admission succeeds
  Restore it: admission fails again

After, built head baca81d53f:
  healthy:      install 0; running; RPC true; runtime-bus
  stale:        install 0; running; RPC true; runtime-bus
  private-only: install 0; running; RPC true; private socket
  custom:       install 0; running; RPC true; custom session-bus
  unavailable:  install 1; actionable diagnosis; no definition error
  custom update admission: loaded; command present; running;
                           selected unix:path=/tmp/dbus-custom
```

Regression tests failed before their fixes for custom-bus admission, private-socket reopen, payload route drift, timeout-dependent fallback, and nonlocal-to-local fallback. The tests preserve successful discovery sharing, per-caller deadlines, custody retirement, and both positive-fallback and wholly failed short-discovery cases. No stash was used.

The 17 initial daemon failures were outdated fixture boundaries: service absence (4), definition mutation (7), private command-query cleanup (2), availability routing (3), and child environment routing (1). Fixtures now answer the exact nonactivating Version probe while retaining unit ownership, mount, absence, cleanup, and environment-isolation assertions. No baselines, suppressions, or assertions were weakened.

Focused commands:

```sh
node scripts/run-vitest.mjs src/daemon/systemd.test.ts src/daemon/systemd-command-query.test.ts src/daemon/systemd-peer.test.ts src/daemon/systemd-peer-native.test.ts src/daemon/systemd-user-transport.test.ts src/daemon/service-process-env.integration.test.ts src/daemon/systemd-unavailable.test.ts src/daemon/service-absence.test.ts src/daemon/systemd-definition-mutation.test.ts src/daemon/systemd-user-command-deadline.test.ts src/daemon/systemd-loaded-runtime.test.ts src/daemon/service.test.ts src/daemon/service-operation-lock.test.ts src/daemon/service-env.test.ts
node scripts/run-vitest.mjs src/commands/doctor-maintenance.service-inspection.test.ts src/cli/daemon-cli/install.test.ts src/cli/daemon-cli/install.integration.test.ts src/cli/daemon-cli/install.reinstall.test.ts
node scripts/check-changed.mjs
node scripts/check-changed.mjs -- src/daemon/systemd-user-transport.ts src/daemon/systemd-user-transport.test.ts
pnpm build
```

The custom update proof invokes built `readGatewayServiceState` with effective, loaded-only admission; it does not mutate packages. A full published-driver update and native macOS/Windows execution were not rerun. Bound update and launchd diagnostic suites passed.


Validation passed: 759 focused tests across the suites and final regression runs; the full changed-file check and final delta check; `pnpm build` on the committed head; all five Debian rig variants. Autoreview was scoped clean with no accepted P0–P2 findings. The final cache regression was run with its fix hunk removed: it failed because admission retained `private` instead of selecting the working `session-bus`; after restoring the exact reviewed bytes, all 12 transport tests passed.

Final round-three numstat: production +370/-210, tests/support +955/-222, docs +7/-3. The initial consolidation removed duplicated routing decisions: production changed from +278/-136 (net +142) to +321/-200 (net +121); subsequent deadline, custody, diagnostic, and route-consistency fixes account for the final +160. No baseline or lint exception was added.

## Final validation

Fresh [CI run 34759398349](https://github.com/openclaw/openclaw/actions/runs/34759398349) completed successfully on approved head `012ea719b454c46812d8c0ffdaa1a81367eafe9f`. The native CI watcher returned `GREEN` with zero pending checks. Its preflight checkout log confirms merge `fe948e7724bdb2be507d48035ab023ecaa08105c`, with main `0a7b7fecfd7f7f525d53d3bc0656f0c12806ea03` as first parent and the approved head as second. This merge includes the main-side reclamation-memory harness correction `d4d3d6cda5d`; core-2 typechecking, compact-small-1, build, Docker proof, and Windows passed.

The first reopening event captured the previous merge commit despite the refreshed pull-request metadata. Its checkout log proved it had not tested the main correction. After confirming the recomputed merge through both GitHub's API and Git refs, a second close/reopen produced the successful run above. No source changes, empty commits, assertion retries, or gate bypasses were used.

## ClawSweeper review

Skipped the P1 proposal to label typed manager failures as `SERVICE_DEFINITION_UNKNOWN`. The v2026.9.4 updater does choose ordinary restart for these diagnostics, but the candidate still rejects unknown manager state and independently requires effective-command inspection, writable definition capability, and a matching Gateway target before repair. Publication repeats those checks. Actual unreadable/sealed definition failures retain the shipped denial marker. Source review did not establish an unsafe rewrite from a manager-only failure; a published-driver fault-injection run for failure followed by recovery remains a proof gap. The Linux transport expectation finding was already addressed in this head. No code changed during landing review.
2026-09-13 06:45:35 -07:00

677 lines
26 KiB
TypeScript

// Test environment tests validate shared env setup helpers.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
inspectPersistedAuthProfileStateRaw,
inspectPersistedAuthProfileStoreRaw,
resolveAuthProfileDatabasePath,
runAuthProfileWriteTransaction,
writePersistedAuthProfileStateRaw,
writePersistedAuthProfileStoreRaw,
} from "../src/agents/auth-profiles/sqlite.js";
import { isCurrentProcessLaunchdServiceLabel } from "../src/daemon/launchd-current-service.js";
import { detectGatewayRespawnSupervisor } from "../src/infra/supervisor-markers.js";
import { closeOpenClawAgentDatabaseByPath } from "../src/state/openclaw-agent-db.js";
import { closeOpenClawStateDatabaseByPath } from "../src/state/openclaw-state-db-cache.js";
import { resolveOpenClawStateSqlitePath } from "../src/state/openclaw-state-db.paths.js";
import {
captureFullEnv,
deleteTestEnvValue,
setTestEnvValue,
withEnv,
} from "../src/test-utils/env.js";
import { cleanupTempDirs, makeTempDir } from "./helpers/temp-dir.js";
import { installTestEnv } from "./test-env.js";
const originalEnv = captureFullEnv();
const tempDirs = new Set<string>();
const cleanupFns: Array<() => void> = [];
// Compare every key without printing ambient credentials in assertion failures.
function changedEnvKeys(expected: NodeJS.ProcessEnv): string[] {
return [...new Set([...Object.keys(expected), ...Object.keys(process.env)])].filter(
(key) => expected[key] !== process.env[key],
);
}
function writeFile(targetPath: string, content: string): void {
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
fs.writeFileSync(targetPath, content, "utf8");
}
function createTempHome(): string {
return makeTempDir(tempDirs, "openclaw-test-env-real-home-");
}
function requireRecord(
value: Record<string, unknown> | undefined,
label: string,
): Record<string, unknown> {
if (!value) {
throw new Error(`expected copied ${label} config`);
}
return value;
}
function requireTelegramStreaming(
value:
| {
mode?: string;
chunkMode?: string;
block?: { enabled?: boolean };
preview?: { chunk?: { minChars?: number } };
}
| undefined,
) {
if (!value) {
throw new Error("expected copied telegram streaming config");
}
return value;
}
afterEach(() => {
while (cleanupFns.length > 0) {
cleanupFns.pop()?.();
}
originalEnv.restore();
vi.restoreAllMocks();
vi.doUnmock("node:child_process");
cleanupTempDirs(tempDirs);
});
describe("installTestEnv", () => {
it("isolates native manager sockets and restores the caller runtime directory", () => {
const callerRuntime = path.join(createTempHome(), "runtime");
withEnv({ XDG_RUNTIME_DIR: callerRuntime }, () => {
const testEnv = installTestEnv({ mode: "hermetic" });
cleanupFns.push(testEnv.cleanup);
expect(process.env.XDG_RUNTIME_DIR).toBe(path.join(testEnv.tempHome, ".runtime"));
testEnv.cleanup();
expect(process.env.XDG_RUNTIME_DIR).toBe(callerRuntime);
});
});
it.each([".openclaw", ".claude"])(
"rolls back live staging failure at %s before another installation",
(failedDirectory) => {
const sandbox = makeTempDir(tempDirs, "openclaw-env-acquisition-");
const realHome = createTempHome();
writeFile(
path.join(realHome, ".profile"),
[
"export ACQUISITION_PROFILE_ADDED=from-profile",
"export ACQUISITION_PROFILE_EMPTY=from-profile",
"export OPENCLAW_TEST_FAST=from-profile",
].join("\n"),
);
const configPath = path.join(realHome, ".openclaw", "openclaw.json");
writeFile(configPath, "{}\n");
writeFile(path.join(realHome, ".claude", "settings.json"), "{}\n");
vi.spyOn(os, "tmpdir").mockReturnValue(sandbox);
const snapshot = captureFullEnv();
cleanupFns.push(() => snapshot.restore());
withEnv(
{
HOME: realHome,
USERPROFILE: realHome,
OPENCLAW_HOME: realHome,
OPENCLAW_STATE_DIR: path.join(realHome, ".openclaw"),
OPENCLAW_CONFIG_PATH: configPath,
OPENCLAW_AGENT_DIR: path.join(realHome, "caller-agent"),
PI_CODING_AGENT_DIR: path.join(realHome, "caller-legacy-agent"),
OPENCLAW_LIVE_TEST: "1",
OPENCLAW_LIVE_USE_REAL_HOME: undefined,
OPENCLAW_LIVE_TEST_QUIET: "1",
OPENCLAW_TEST_FAST: "",
COREPACK_HOME: undefined,
ACQUISITION_PROFILE_ADDED: undefined,
ACQUISITION_PROFILE_EMPTY: "",
},
() => {
const callerEnv = { ...process.env };
const failure = new Error(`staging failed at ${failedDirectory}`);
const mkdirSync = fs.mkdirSync;
let failedHome = "";
const fault = vi.spyOn(fs, "mkdirSync").mockImplementation((target, options) => {
const home = process.env.HOME;
if (home && home !== realHome && target === path.join(home, failedDirectory)) {
failedHome = home;
throw failure;
}
return mkdirSync(target, options);
});
try {
let caught: unknown;
try {
const unexpected = installTestEnv();
cleanupFns.push(unexpected.cleanup);
} catch (error) {
caught = error;
}
expect(caught).toBe(failure);
} finally {
fault.mockRestore();
}
expect(failedHome).not.toBe("");
expect.soft(changedEnvKeys(callerEnv)).toEqual([]);
expect.soft(fs.existsSync(failedHome)).toBe(false);
expect(fs.readdirSync(sandbox)).toEqual([]);
expect(fs.readFileSync(configPath, "utf8")).toBe("{}\n");
const next = installTestEnv();
cleanupFns.push(next.cleanup);
expect(next.tempHome).not.toBe(failedHome);
expect(process.env.OPENCLAW_AGENT_DIR).toBeUndefined();
expect(process.env.PI_CODING_AGENT_DIR).toBeUndefined();
expect(process.env.ACQUISITION_PROFILE_ADDED).toBe("from-profile");
expect(process.env.ACQUISITION_PROFILE_EMPTY).toBe("from-profile");
expect(
fs.readFileSync(path.join(next.tempHome, ".claude", "settings.json"), "utf8"),
).toBe("{}\n");
next.cleanup();
expect(process.env.HOME).toBe(realHome);
expect(process.env.OPENCLAW_AGENT_DIR).toBe(callerEnv.OPENCLAW_AGENT_DIR);
expect(process.env.PI_CODING_AGENT_DIR).toBe(callerEnv.PI_CODING_AGENT_DIR);
expect(process.env.OPENCLAW_TEST_FAST).toBe("from-profile");
expect(process.env.ACQUISITION_PROFILE_ADDED).toBe("from-profile");
expect(fs.readdirSync(sandbox)).toEqual([]);
},
);
},
);
it("keeps live tests on a temp HOME while copying config and auth state", () => {
const realHome = createTempHome();
const openClawHome = createTempHome();
const priorIsolatedHome = createTempHome();
writeFile(path.join(realHome, ".profile"), "export TEST_PROFILE_ONLY=from-profile\n");
writeFile(
path.join(openClawHome, "custom-openclaw.json5"),
`{
// Preserve provider config, strip host-bound paths.
agents: {
defaults: {
workspace: "/Users/peter/Projects",
agentDir: "/Users/peter/.openclaw/agents/main/agent",
},
list: [
{
id: "dev",
workspace: "/Users/peter/dev-workspace",
agentDir: "/Users/peter/.openclaw/agents/dev/agent",
},
],
},
models: {
providers: {
custom: { baseUrl: "https://example.test/v1" },
},
},
channels: {
telegram: {
streaming: {
mode: "block",
chunkMode: "newline",
block: {
enabled: true,
},
preview: {
chunk: {
minChars: 120,
},
},
},
},
},
}`,
);
writeFile(path.join(openClawHome, ".openclaw", "credentials", "token.txt"), "secret\n");
writeFile(
path.join(openClawHome, ".openclaw", "external-plugins", "glueclaw", "openclaw.plugin.json"),
'{"id":"glueclaw"}\n',
);
const realStateDir = path.join(openClawHome, ".openclaw");
const realAgentDir = path.join(realStateDir, "agents", "main", "agent");
const liveAuthStore = {
version: 1,
profiles: {
"openai:api-key": {
type: "api_key",
provider: "openai",
keyRef: {
source: "env",
provider: "default",
id: "OPENCLAW_LIVE_OPENAI_KEY",
},
},
},
};
const liveAuthState = {
version: 1,
order: { openai: ["openai:api-key"] },
};
runAuthProfileWriteTransaction(
realAgentDir,
(database) => {
writePersistedAuthProfileStoreRaw(liveAuthStore, realAgentDir, database);
writePersistedAuthProfileStateRaw(liveAuthState, realAgentDir, database);
},
{ stateDir: realStateDir },
);
cleanupFns.push(() => {
closeOpenClawAgentDatabaseByPath(resolveAuthProfileDatabasePath(realAgentDir));
closeOpenClawStateDatabaseByPath(
resolveOpenClawStateSqlitePath({
...process.env,
OPENCLAW_STATE_DIR: realStateDir,
}),
);
});
writeFile(path.join(realHome, ".claude", ".credentials.json"), '{"accessToken":"token"}\n');
writeFile(path.join(realHome, ".claude", "projects", "old-session.jsonl"), "session\n");
fs.mkdirSync(path.join(realHome, ".claude", "settings.local.json"), { recursive: true });
writeFile(path.join(realHome, ".codex", "auth.json"), '{"OPENAI_API_KEY":"token"}\n');
writeFile(path.join(realHome, ".codex", "config.toml"), 'model = "gpt-5.4"\n');
writeFile(
path.join(realHome, ".codex", "sessions", "2026", "02", "26", "rollout.jsonl"),
"session\n",
);
writeFile(path.join(realHome, ".gemini", "oauth_creds.json"), '{"token":"gemini"}\n');
writeFile(path.join(realHome, ".gemini", "settings.json"), '{"theme":"dark"}\n');
writeFile(path.join(realHome, ".gemini", "commands", "Cache", "review.toml"), "prompt\n");
writeFile(path.join(realHome, ".minimax", "Cache", "credentials.json"), "minimax\n");
writeFile(
path.join(
realHome,
".gemini",
"antigravity-browser-profile",
"Default",
"Cache",
"Cache_Data",
"blob",
),
"cached-browser-bytes\n",
);
writeFile(
path.join(realHome, ".gemini", "antigravity", "browser_recordings", "session.webm"),
"recording\n",
);
writeFile(
path.join(realHome, ".gemini", "cli-browser-profile", "Default", "History"),
"browser-history\n",
);
writeFile(path.join(realHome, ".gemini", "GPUCache", "data.bin"), "gpu-cache\n");
writeFile(
path.join(realHome, ".gemini", "Service Worker", "CacheStorage", "cache.bin"),
"worker-cache\n",
);
setTestEnvValue("HOME", realHome);
setTestEnvValue("USERPROFILE", realHome);
setTestEnvValue("OPENCLAW_HOME", openClawHome);
setTestEnvValue("OPENCLAW_LIVE_TEST", "1");
setTestEnvValue("OPENCLAW_LIVE_TEST_QUIET", "1");
setTestEnvValue("OPENCLAW_CONFIG_PATH", "~/custom-openclaw.json5");
setTestEnvValue("OPENCLAW_TEST_HOME", priorIsolatedHome);
setTestEnvValue("OPENCLAW_STATE_DIR", path.join(priorIsolatedHome, ".openclaw"));
const testEnv = installTestEnv();
cleanupFns.push(testEnv.cleanup);
expect(testEnv.tempHome).not.toBe(realHome);
expect(process.env.HOME).toBe(testEnv.tempHome);
expect(process.env.OPENCLAW_HOME).toBeUndefined();
expect(process.env.OPENCLAW_TEST_HOME).toBe(testEnv.tempHome);
expect(process.env.TEST_PROFILE_ONLY).toBe("from-profile");
const copiedConfigPath = path.join(testEnv.tempHome, ".openclaw", "openclaw.json");
const copiedConfig = JSON.parse(fs.readFileSync(copiedConfigPath, "utf8")) as {
agents?: {
defaults?: Record<string, unknown>;
list?: Array<Record<string, unknown>>;
};
models?: { providers?: Record<string, unknown> };
channels?: {
telegram?: {
streaming?: {
mode?: string;
chunkMode?: string;
block?: { enabled?: boolean };
preview?: { chunk?: { minChars?: number } };
};
};
};
};
const providers = requireRecord(copiedConfig.models?.providers, "model providers");
expect(providers.custom).toEqual({ baseUrl: "https://example.test/v1" });
const agentDefaults = requireRecord(copiedConfig.agents?.defaults, "agent defaults");
const agentConfig = requireRecord(copiedConfig.agents?.list?.[0], "agent");
expect(agentDefaults.workspace).toBeUndefined();
expect(agentDefaults.agentDir).toBeUndefined();
expect(agentConfig.workspace).toBeUndefined();
expect(agentConfig.agentDir).toBeUndefined();
const telegramStreaming = requireTelegramStreaming(copiedConfig.channels?.telegram?.streaming);
expect(telegramStreaming).toEqual({
mode: "block",
chunkMode: "newline",
block: { enabled: true },
preview: { chunk: { minChars: 120 } },
});
expect(
fs.existsSync(path.join(testEnv.tempHome, ".openclaw", "credentials", "token.txt")),
).toBe(true);
expect(
fs.existsSync(
path.join(
testEnv.tempHome,
".openclaw",
"external-plugins",
"glueclaw",
"openclaw.plugin.json",
),
),
).toBe(true);
const stagedAgentDir = path.join(testEnv.tempHome, ".openclaw", "agents", "main", "agent");
expect(inspectPersistedAuthProfileStoreRaw(stagedAgentDir)).toEqual({
status: "readable",
raw: liveAuthStore,
});
expect(inspectPersistedAuthProfileStateRaw(stagedAgentDir)).toEqual({
status: "readable",
raw: liveAuthState,
});
expect(fs.existsSync(path.join(stagedAgentDir, "auth-profiles.json"))).toBe(false);
expect(fs.existsSync(path.join(testEnv.tempHome, ".claude", ".credentials.json"))).toBe(true);
expect(fs.existsSync(path.join(testEnv.tempHome, ".claude", "projects"))).toBe(false);
expect(fs.existsSync(path.join(testEnv.tempHome, ".claude", "settings.local.json"))).toBe(
false,
);
expect(fs.existsSync(path.join(testEnv.tempHome, ".codex", "auth.json"))).toBe(true);
expect(fs.existsSync(path.join(testEnv.tempHome, ".codex", "config.toml"))).toBe(true);
expect(fs.existsSync(path.join(testEnv.tempHome, ".codex", "sessions"))).toBe(false);
expect(fs.existsSync(path.join(testEnv.tempHome, ".gemini", "oauth_creds.json"))).toBe(true);
expect(fs.existsSync(path.join(testEnv.tempHome, ".gemini", "settings.json"))).toBe(true);
expect(
fs.existsSync(path.join(testEnv.tempHome, ".gemini", "commands", "Cache", "review.toml")),
).toBe(true);
expect(
fs.existsSync(path.join(testEnv.tempHome, ".minimax", "Cache", "credentials.json")),
).toBe(true);
expect(
fs.existsSync(path.join(testEnv.tempHome, ".gemini", "antigravity-browser-profile")),
).toBe(false);
expect(
fs.existsSync(path.join(testEnv.tempHome, ".gemini", "antigravity", "browser_recordings")),
).toBe(false);
expect(fs.existsSync(path.join(testEnv.tempHome, ".gemini", "cli-browser-profile"))).toBe(
false,
);
expect(fs.existsSync(path.join(testEnv.tempHome, ".gemini", "GPUCache"))).toBe(false);
expect(
fs.existsSync(path.join(testEnv.tempHome, ".gemini", "Service Worker", "CacheStorage")),
).toBe(false);
});
it("allows explicit live runs against the real HOME", () => {
const realHome = createTempHome();
writeFile(path.join(realHome, ".profile"), "export TEST_PROFILE_ONLY=from-profile\n");
setTestEnvValue("HOME", realHome);
setTestEnvValue("USERPROFILE", realHome);
setTestEnvValue("OPENCLAW_LIVE_TEST", "1");
setTestEnvValue("OPENCLAW_LIVE_USE_REAL_HOME", "1");
setTestEnvValue("OPENCLAW_LIVE_TEST_QUIET", "1");
const agentDir = path.join(realHome, "caller-agent");
const legacyAgentDir = path.join(realHome, "caller-legacy-agent");
setTestEnvValue("OPENCLAW_AGENT_DIR", agentDir);
setTestEnvValue("PI_CODING_AGENT_DIR", legacyAgentDir);
const testEnv = installTestEnv();
expect(testEnv.tempHome).toBe(realHome);
expect(process.env.HOME).toBe(realHome);
expect(process.env.TEST_PROFILE_ONLY).toBe("from-profile");
expect(process.env.OPENCLAW_AGENT_DIR).toBe(agentDir);
expect(process.env.PI_CODING_AGENT_DIR).toBe(legacyAgentDir);
testEnv.cleanup();
expect(process.env.OPENCLAW_AGENT_DIR).toBe(agentDir);
expect(process.env.PI_CODING_AGENT_DIR).toBe(legacyAgentDir);
});
it("keeps hermetic mode isolated when live flags request the real HOME", () => {
const realHome = createTempHome();
writeFile(path.join(realHome, ".profile"), "export TEST_PROFILE_ONLY=from-profile\n");
writeFile(path.join(realHome, ".openclaw", "openclaw.json"), '{"live":true}\n');
writeFile(path.join(realHome, ".openclaw", "credentials", "token.txt"), "secret\n");
setTestEnvValue("HOME", realHome);
setTestEnvValue("USERPROFILE", realHome);
setTestEnvValue("LIVE", "1");
setTestEnvValue("OPENCLAW_LIVE_TEST", "1");
setTestEnvValue("OPENCLAW_LIVE_GATEWAY", "1");
setTestEnvValue("OPENCLAW_LIVE_USE_REAL_HOME", "1");
const callerPluginDir = path.join(realHome, "caller-plugins");
setTestEnvValue("OPENCLAW_BUNDLED_PLUGINS_DIR", callerPluginDir);
setTestEnvValue("OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR", "1");
setTestEnvValue("OPENCLAW_DISABLE_BUNDLED_PLUGINS", "1");
setTestEnvValue("OPENCLAW_HOME", realHome);
setTestEnvValue("OPENCLAW_AGENT_DIR", path.join(realHome, "caller-agent"));
setTestEnvValue("PI_CODING_AGENT_DIR", path.join(realHome, "caller-legacy-agent"));
const testEnv = installTestEnv({ mode: "hermetic" });
cleanupFns.push(testEnv.cleanup);
expect(testEnv.tempHome).not.toBe(realHome);
expect(process.env.HOME).toBe(testEnv.tempHome);
expect(process.env.TEST_PROFILE_ONLY).toBeUndefined();
expect(process.env.LIVE).toBeUndefined();
expect(process.env.OPENCLAW_LIVE_TEST).toBeUndefined();
expect(process.env.OPENCLAW_LIVE_GATEWAY).toBeUndefined();
expect(process.env.OPENCLAW_LIVE_USE_REAL_HOME).toBeUndefined();
expect(process.env.OPENCLAW_BUNDLED_PLUGINS_DIR).not.toBe(callerPluginDir);
expect(path.basename(process.env.OPENCLAW_BUNDLED_PLUGINS_DIR ?? "")).toBe("extensions");
expect(process.env.OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR).toBe("1");
expect(process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS).toBeUndefined();
expect(process.env.OPENCLAW_HOME).toBeUndefined();
expect(process.env.OPENCLAW_AGENT_DIR).toBeUndefined();
expect(process.env.PI_CODING_AGENT_DIR).toBeUndefined();
expect(fs.existsSync(path.join(testEnv.tempHome, ".openclaw", "openclaw.json"))).toBe(false);
expect(
fs.existsSync(path.join(testEnv.tempHome, ".openclaw", "credentials", "token.txt")),
).toBe(false);
});
it.each(["OPENCLAW_HOME", "OPENCLAW_AGENT_DIR", "PI_CODING_AGENT_DIR"])(
"clears and restores %s for normal isolated test runs",
(key) => {
const realHome = createTempHome();
const callerPath = path.join(realHome, "caller-override");
setTestEnvValue("HOME", realHome);
setTestEnvValue("USERPROFILE", realHome);
setTestEnvValue(key, callerPath);
const testEnv = installTestEnv();
cleanupFns.push(testEnv.cleanup);
expect(testEnv.tempHome).not.toBe(realHome);
expect(process.env[key]).toBeUndefined();
setTestEnvValue(key, path.join(testEnv.tempHome, "explicit-override"));
testEnv.cleanup();
expect(process.env[key]).toBe(callerPath);
},
);
it.each([
{
name: "explicit",
corepack: "tool-cache",
xdg: "xdg",
local: "local",
expected: "tool-cache",
},
{ name: "explicit empty", corepack: "", xdg: "xdg", local: "local", expected: "" },
{ name: "explicit whitespace", corepack: " tool-cache ", expected: " tool-cache " },
{ name: "XDG before LOCALAPPDATA", xdg: "xdg", local: "local", expected: "xdg/node/corepack" },
{ name: "empty XDG", xdg: "", local: "local", expected: "node/corepack" },
{ name: "LOCALAPPDATA", local: "local", expected: "local/node/corepack" },
{ name: "empty LOCALAPPDATA", local: "", expected: "node/corepack" },
{ name: "OS home" },
])("preserves the $name Corepack cache across HOME isolation and cleanup", (testCase) => {
const realHome = createTempHome();
withEnv(
{
HOME: realHome,
USERPROFILE: realHome,
COREPACK_HOME: testCase.corepack,
XDG_CACHE_HOME: testCase.xdg,
LOCALAPPDATA: testCase.local,
},
() => {
const callerEnv = { ...process.env };
const expected =
testCase.expected === undefined
? path.join(
os.homedir(),
process.platform === "win32" ? "AppData/Local" : ".cache",
"node/corepack",
)
: testCase.corepack === undefined
? path.normalize(testCase.expected)
: testCase.expected;
const testEnv = installTestEnv({ mode: "hermetic" });
cleanupFns.push(testEnv.cleanup);
expect(process.env.HOME).toBe(testEnv.tempHome);
expect(process.env.XDG_CACHE_HOME).toBe(path.join(testEnv.tempHome, ".cache"));
expect(process.env.COREPACK_HOME).toBe(expected);
setTestEnvValue("COREPACK_HOME", path.join(testEnv.tempHome, "changed-tool-cache"));
testEnv.cleanup();
expect(changedEnvKeys(callerEnv)).toEqual([]);
expect(fs.existsSync(testEnv.tempHome)).toBe(false);
},
);
});
it.each([
"TWILIO_ACCOUNT_SID",
"TWILIO_AUTH_TOKEN",
"TWILIO_PHONE_NUMBER",
"TWILIO_SMS_FROM",
"TWILIO_MESSAGING_SERVICE_SID",
])("isolates and restores the SMS activation variable %s", (key) => {
setTestEnvValue(key, "test-channel-value");
const testEnv = installTestEnv({ mode: "hermetic" });
cleanupFns.push(testEnv.cleanup);
expect(process.env[key]).toBeUndefined();
testEnv.cleanup();
expect(process.env[key]).toBe("test-channel-value");
});
it.each(["live-aware", "hermetic"] as const)(
"isolates and restores inherited supervisor identity in %s mode",
(mode) => {
const supervisorEnv = {
LAUNCH_JOB_LABEL: "ai.openclaw.gateway",
LAUNCH_JOB_NAME: "ai.openclaw.gateway",
XPC_SERVICE_NAME: "ai.openclaw.gateway",
OPENCLAW_LAUNCHD_LABEL: "ai.openclaw.gateway",
OPENCLAW_SERVICE_MARKER: "openclaw",
OPENCLAW_SERVICE_KIND: "gateway",
OPENCLAW_SYSTEMD_UNIT: "openclaw-gateway.service",
INVOCATION_ID: "test-invocation",
SYSTEMD_EXEC_PID: "1234",
JOURNAL_STREAM: "8:1234",
OPENCLAW_WINDOWS_TASK_NAME: "OpenClaw Gateway",
OPENCLAW_SUPERVISOR_MODE: "external",
OPENCLAW_WRAPPER: "/fixture/operator-wrapper",
OPENCLAW_GATEWAY_SERVICE_PID: "4321",
OPENCLAW_SERVICE_MANAGED_ENV_KEYS: "FIXTURE_AUTH_REF",
OPENCLAW_WINDOWS_TASK_HIDDEN_LAUNCHER: "1",
};
for (const [key, value] of Object.entries(supervisorEnv)) {
setTestEnvValue(key, value);
}
setTestEnvValue("TEST_UNRELATED_SERVICE_HINT", "preserved");
const testEnv = installTestEnv({ mode });
cleanupFns.push(testEnv.cleanup);
expect(isCurrentProcessLaunchdServiceLabel("ai.openclaw.gateway")).toBe(false);
for (const platform of ["darwin", "linux", "win32"] as const) {
expect(detectGatewayRespawnSupervisor(process.env, platform)).toBeNull();
}
expect(Object.keys(supervisorEnv).filter((key) => process.env[key] !== undefined)).toEqual(
[],
);
expect(process.env.TEST_UNRELATED_SERVICE_HINT).toBe("preserved");
withEnv({ XPC_SERVICE_NAME: "ai.openclaw.gateway" }, () => {
expect(isCurrentProcessLaunchdServiceLabel("ai.openclaw.gateway")).toBe(true);
});
withEnv({ XPC_SERVICE_NAME: "0" }, () => {
expect(isCurrentProcessLaunchdServiceLabel("ai.openclaw.gateway")).toBe(false);
});
testEnv.cleanup();
for (const [key, value] of Object.entries(supervisorEnv)) {
expect(process.env[key]).toBe(value);
}
},
);
it("does not load ~/.profile for normal isolated test runs", () => {
const realHome = createTempHome();
writeFile(path.join(realHome, ".profile"), "export TEST_PROFILE_ONLY=from-profile\n");
setTestEnvValue("HOME", realHome);
setTestEnvValue("USERPROFILE", realHome);
deleteTestEnvValue("LIVE");
deleteTestEnvValue("OPENCLAW_LIVE_TEST");
deleteTestEnvValue("OPENCLAW_LIVE_GATEWAY");
deleteTestEnvValue("OPENCLAW_LIVE_USE_REAL_HOME");
deleteTestEnvValue("OPENCLAW_LIVE_TEST_QUIET");
const testEnv = installTestEnv();
cleanupFns.push(testEnv.cleanup);
expect(testEnv.tempHome).not.toBe(realHome);
expect(process.env.TEST_PROFILE_ONLY).toBeUndefined();
});
it("falls back to parsing ~/.profile when bash is unavailable", async () => {
const realHome = createTempHome();
writeFile(path.join(realHome, ".profile"), "export TEST_PROFILE_ONLY=from-profile\n");
setTestEnvValue("HOME", realHome);
setTestEnvValue("USERPROFILE", realHome);
setTestEnvValue("OPENCLAW_LIVE_TEST", "1");
setTestEnvValue("OPENCLAW_LIVE_USE_REAL_HOME", "1");
setTestEnvValue("OPENCLAW_LIVE_TEST_QUIET", "1");
vi.doMock("node:child_process", () => ({
execFileSync: () => {
throw Object.assign(new Error("bash missing"), { code: "ENOENT" });
},
}));
const { installTestEnv: installFreshTestEnv } = await importFreshModule<
typeof import("./test-env.js")
>(import.meta.url, "./test-env.js?scope=profile-fallback");
const testEnv = installFreshTestEnv();
expect(testEnv.tempHome).toBe(realHome);
expect(process.env.TEST_PROFILE_ONLY).toBe("from-profile");
});
});