Files
openclaw/test/test-env.ts
Peter Steinberger f91de52f0d refactor: move runtime state to SQLite
* refactor: remove stale file-backed shims

* fix: harden sqlite state ci boundaries

* refactor: store matrix idb snapshots in sqlite

* fix: satisfy rebased CI guardrails

* refactor: store current conversation bindings in sqlite table

* refactor: store tui last sessions in sqlite table

* refactor: reset sqlite schema history

* refactor: drop unshipped sqlite table migration

* refactor: remove plugin index file rollback

* refactor: drop unshipped sqlite sidecar migrations

* refactor: remove runtime commitments kv migration

* refactor: preserve kysely sync result types

* refactor: drop unshipped sqlite schema migration table

* test: keep session usage coverage sqlite-backed

* refactor: keep sqlite migration doctor-only

* refactor: isolate device legacy imports

* refactor: isolate push voicewake legacy imports

* refactor: isolate remaining runtime legacy imports

* refactor: tighten sqlite migration guardrails

* test: cover sqlite persisted enum parsing

* refactor: isolate legacy update and tui imports

* refactor: tighten sqlite state ownership

* refactor: move legacy imports behind doctor

* refactor: remove legacy session row lookup

* refactor: canonicalize memory transcript locators

* refactor: drop transcript path scope fallbacks

* refactor: drop runtime legacy session delivery pruning

* refactor: store tts prefs only in sqlite

* refactor: remove cron store path runtime

* refactor: use cron sqlite store keys

* refactor: rename telegram message cache scope

* refactor: read memory dreaming status from sqlite

* refactor: rename cron status store key

* refactor: stop remembering transcript file paths

* test: use sqlite locators in agent fixtures

* refactor: remove file-shaped commitments and cron store surfaces

* refactor: keep compaction transcript handles out of session rows

* refactor: derive transcript handles from session identity

* refactor: derive runtime transcript handles

* refactor: remove gateway session locator reads

* refactor: remove transcript locator from session rows

* refactor: store raw stream diagnostics in sqlite

* refactor: remove file-shaped transcript rotation

* refactor: hide legacy trajectory paths from runtime

* refactor: remove runtime transcript file bridges

* refactor: repair database-first rebase fallout

* refactor: align tests with database-first state

* refactor: remove transcript file handoffs

* refactor: sync post-compaction memory by transcript scope

* refactor: run codex app-server sessions by id

* refactor: bind codex runtime state by session id

* refactor: pass memory transcripts by sqlite scope

* refactor: remove transcript locator cleanup leftovers

* test: remove stale transcript file fixtures

* refactor: remove transcript locator test helper

* test: make cron sqlite keys explicit

* test: remove cron runtime store paths

* test: remove stale session file fixtures

* test: use sqlite cron keys in diagnostics

* refactor: remove runtime delivery queue backfill

* test: drop fake export session file mocks

* refactor: rename acp session read failure flag

* refactor: rename acp row session key

* refactor: remove session store test seams

* refactor: move legacy session parser tests to doctor

* refactor: reindex managed memory in place

* refactor: drop stale session store wording

* refactor: rename session row helpers

* refactor: rename sqlite session entry modules

* refactor: remove transcript locator leftovers

* refactor: trim file-era audit wording

* refactor: clean managed media through sqlite

* fix: prefer explicit agent for exports

* fix: use prepared agent for session resets

* fix: canonicalize legacy codex binding import

* test: rename state cleanup helper

* docs: align backup docs with sqlite state

* refactor: drop legacy Pi usage auth fallback

* refactor: move legacy auth profile imports to doctor

* refactor: keep Pi model discovery auth in memory

* refactor: remove MSTeams legacy learning key fallback

* refactor: store model catalog config in sqlite

* refactor: use sqlite model catalog at runtime

* refactor: remove model json compatibility aliases

* refactor: store auth profiles in sqlite

* refactor: seed copied auth profiles in sqlite

* refactor: make auth profile runtime sqlite-addressed

* refactor: migrate hermes secrets into sqlite auth store

* refactor: move plugin install config migration to doctor

* refactor: rename plugin index audit checks

* test: drop auth file assumptions

* test: remove legacy transcript file assertions

* refactor: drop legacy cli session aliases

* refactor: store skill uploads in sqlite

* refactor: keep subagent attachments in sqlite vfs

* refactor: drop subagent attachment cleanup state

* refactor: move legacy session aliases to doctor

* refactor: require node 24 for sqlite state runtime

* refactor: move provider caches into sqlite state

* fix: harden virtual agent filesystem

* refactor: enforce database-first runtime state

* refactor: rename compaction transcript rotation setting

* test: clean sqlite refactor test types

* refactor: consolidate sqlite runtime state

* refactor: model session conversations in sqlite

* refactor: stop deriving cron delivery from session keys

* refactor: stop classifying sessions from key shape

* refactor: hydrate announce targets from typed delivery

* refactor: route heartbeat delivery from typed sqlite context

* refactor: tighten typed sqlite session routing

* refactor: remove session origin routing shadow

* refactor: drop session origin shadow fixtures

* perf: query sqlite vfs paths by prefix

* refactor: use typed conversation metadata for sessions

* refactor: prefer typed session routing metadata

* refactor: require typed session routing metadata

* refactor: resolve group tool policy from typed sessions

* refactor: delete dead session thread info bridge

* Show Codex subscription reset times in channel errors (#80456)

* feat(plugin-sdk): consolidate session workflow APIs

* fix(agents): allow read-only agent mount reads

* [codex] refresh plugin regression fixtures

* fix(agents): restore compaction gateway logs

* test: tighten gateway startup assertions

* Redact persisted secret-shaped payloads [AI] (#79006)

* test: tighten device pair notify assertions

* test: tighten hermes secret assertions

* test: assert matrix client error shapes

* test: assert config compat warnings

* fix(heartbeat): remap cron-run exec events to session keys (#80214)

* fix(codex): route btw through native side threads

* fix(auth): accept friendly OpenAI order for Codex profiles

* fix(codex): rotate auth profiles inside harness

* fix: keep browser status page probe within timeout

* test: assert agents add outputs

* test: pin cron read status

* fix(agents): avoid Pi resource discovery stalls

Co-authored-by: dataCenter430 <titan032000@gmail.com>

* fix: retire timed-out codex app-server clients

* test: tighten qa lab runtime assertions

* test: check security fix outputs

* test: verify extension runtime messages

* feat(wake): expose typed sessionKey on wake protocol + system event CLI

* fix(gateway): await session_end during shutdown drain and track channel + compaction lifecycle paths (#57790)

* test: guard talk consult call helper

* fix(codex): scale context engine projection (#80761)

* fix(codex): scale context engine projection

* fix: document Codex context projection scaling

* fix: document Codex context projection scaling

* fix: document Codex context projection scaling

* fix: document Codex context projection scaling

* chore: align Codex projection changelog

* chore: realign Codex projection changelog

* fix: isolate Codex projection patch

---------

Co-authored-by: Eva (agent) <eva+agent-78055@100yen.org>
Co-authored-by: Josh Lehman <josh@martian.engineering>

* refactor: move agent runtime state toward piless

* refactor: remove cron session reaper

* refactor: move session management to sqlite

* refactor: finish database-first state migration

* chore: refresh generated sqlite db types

* refactor: remove stale file-backed shims

* test: harden kysely type coverage

# Conflicts:
#	.agents/skills/kysely-database-access/SKILL.md
#	src/infra/kysely-sync.types.test.ts
#	src/proxy-capture/store.sqlite.test.ts
#	src/state/openclaw-agent-db.test.ts
#	src/state/openclaw-state-db.test.ts

* refactor: remove cron store path runtime

* refactor: keep compaction transcript handles out of session rows

* refactor: derive embedded transcripts from sqlite identity

* refactor: remove embedded transcript locator handoff

* refactor: remove runtime transcript file bridges

* refactor: remove transcript file handoffs

* refactor: remove MSTeams legacy learning key fallback

* refactor: store model catalog config in sqlite

* refactor: use sqlite model catalog at runtime

# Conflicts:
#	docs/cli/secrets.md
#	docs/gateway/authentication.md
#	docs/gateway/secrets.md

* fix: keep oauth sibling sync sqlite-local

# Conflicts:
#	src/commands/onboard-auth.test.ts

* refactor: remove task session store maintenance

# Conflicts:
#	src/commands/tasks.ts

* refactor: keep diagnostics in state sqlite

* refactor: enforce database-first runtime state

* refactor: consolidate sqlite runtime state

* Show Codex subscription reset times in channel errors (#80456)

* fix(codex): refresh subscription limit resets

* fix(codex): format reset times for channels

* Update CHANGELOG with latest changes and fixes

Updated CHANGELOG with recent fixes and improvements.

* fix(codex): keep command load failures on codex surface

* fix(codex): format account rate limits as rows

* fix(codex): summarize account limits as usage status

* fix(codex): simplify account limit status

* test: tighten subagent announce queue assertion

* test: tighten session delete lifecycle assertions

* test: tighten cron ops assertions

* fix: track cron execution milestones

* test: tighten hermes secret assertions

* test: assert matrix sync store payloads

* test: assert config compat warnings

* fix(codex): align btw side thread semantics

* fix(codex): honor codex fallback blocking

* fix(agents): avoid Pi resource discovery stalls

* test: tighten codex event assertions

* test: tighten cron assertions

* Fix Codex app-server OAuth harness auth

* refactor: move agent runtime state toward piless

* refactor: move device and push state to sqlite

* refactor: move runtime json state imports to doctor

* refactor: finish database-first state migration

* chore: refresh generated sqlite db types

* refactor: clarify cron sqlite store keys

* refactor: remove stale file-backed shims

* refactor: bind codex runtime state by session id

* test: expect sqlite trajectory branch export

* refactor: rename session row helpers

* fix: keep legacy device identity import in doctor

* refactor: enforce database-first runtime state

* refactor: consolidate sqlite runtime state

* build: align pi contract wrappers

* chore: repair database-first rebase

* refactor: remove session file test contracts

* test: update gateway session expectations

* refactor: stop routing from session compatibility shadows

* refactor: stop persisting session route shadows

* refactor: use typed delivery context in clients

* refactor: stop echoing session route shadows

* refactor: repair embedded runner rebase imports

# Conflicts:
#	src/agents/pi-embedded-runner/run/attempt.tool-call-argument-repair.ts

* refactor: align pi contract imports

* refactor: satisfy kysely sync helper guard

* refactor: remove file transcript bridge remnants

* refactor: remove session locator compatibility

* refactor: remove session file test contracts

* refactor: keep rebase database-first clean

* refactor: remove session file assumptions from e2e

* docs: clarify database-first goal state

* test: remove legacy store markers from sqlite runtime tests

* refactor: remove legacy store assumptions from runtime seams

* refactor: align sqlite runtime helper seams

* test: update memory recall sqlite audit mock

* refactor: align database-first runtime type seams

* test: clarify doctor cron legacy store names

* fix: preserve sqlite session route projections

* test: fix copilot token cache test syntax

* docs: update database-first proof status

* test: align database-first test fixtures

* docs: update database-first proof status

* refactor: clean extension database-first drift

* test: align agent session route proof

* test: clarify doctor legacy path fixtures

* chore: clean database-first changed checks

* chore: repair database-first rebase markers

* build: allow baileys git subdependency

* chore: repair exp-vfs rebase drift

* chore: finish exp-vfs rebase cleanup

* chore: satisfy rebase lint drift

* chore: fix qqbot rebase type seam

* chore: fix rebase drift leftovers

* fix: keep auth profile oauth secrets out of sqlite

* fix: repair rebase drift tests

* test: stabilize pairing request ordering

* test: use source manifests in plugin contract checks

* fix: restore gateway session metadata after rebase

* fix: repair database-first rebase drift

* fix: clean up database-first rebase fallout

* test: stabilize line quick reply receipt time

* fix: repair extension rebase drift

* test: keep transcript redaction tests sqlite-backed

* fix: carry injected transcript redaction through sqlite

* chore: clean database branch rebase residue

* fix: repair database branch CI drift

* fix: repair database branch CI guard drift

* fix: stabilize oauth tls preflight test

* test: align database branch fast guards

* test: repair build artifact boundary guards

* chore: clean changelog rebase markers

---------

Co-authored-by: pashpashpash <nik@vault77.ai>
Co-authored-by: Eva <eva@100yen.org>
Co-authored-by: stainlu <stainlu@newtype-ai.org>
Co-authored-by: Jason Zhou <jason.zhou.design@gmail.com>
Co-authored-by: Ruben Cuevas <hi@rubencu.com>
Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>
Co-authored-by: Shakker <shakkerdroid@gmail.com>
Co-authored-by: Kaspre <36520309+Kaspre@users.noreply.github.com>
Co-authored-by: dataCenter430 <titan032000@gmail.com>
Co-authored-by: Kaspre <kaspre@gmail.com>
Co-authored-by: pandadev66 <nova.full.stack@outlook.com>
Co-authored-by: Eva <admin@100yen.org>
Co-authored-by: Eva (agent) <eva+agent-78055@100yen.org>
Co-authored-by: Josh Lehman <josh@martian.engineering>
Co-authored-by: jeffjhunter <support@aipersonamethod.com>
2026-05-13 13:15:12 +01:00

447 lines
14 KiB
TypeScript

import { execFileSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import JSON5 from "json5";
import {
loadPersistedAuthProfileStore,
savePersistedAuthProfileSecretsStore,
} from "../src/agents/auth-profiles/persisted.js";
type RestoreEntry = { key: string; value: string | undefined };
const LIVE_EXTERNAL_AUTH_DIRS = [".claude/backups", ".gemini", ".minimax"] as const;
const LIVE_EXTERNAL_AUTH_FILES = [
".claude.json",
".claude/.credentials.json",
".claude/settings.json",
".claude/settings.local.json",
".codex/auth.json",
".codex/config.toml",
] as const;
function isTruthyEnvValue(value: string | undefined): boolean {
if (!value) {
return false;
}
switch (value.trim().toLowerCase()) {
case "":
case "0":
case "false":
case "no":
case "off":
return false;
default:
return true;
}
}
function restoreEnv(entries: RestoreEntry[]): void {
for (const { key, value } of entries) {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
}
function resolveHomeRelativePath(input: string, homeDir: string): string {
const trimmed = input.trim();
if (trimmed === "~") {
return homeDir;
}
if (trimmed.startsWith("~/") || trimmed.startsWith("~\\")) {
return path.join(homeDir, trimmed.slice(2));
}
return path.resolve(trimmed);
}
function loadProfileEnv(homeDir = os.homedir()): void {
const profilePath = path.join(homeDir, ".profile");
if (!fs.existsSync(profilePath)) {
return;
}
const applyEntry = (entry: string) => {
const idx = entry.indexOf("=");
if (idx <= 0) {
return false;
}
const key = entry.slice(0, idx).trim();
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key) || (process.env[key] ?? "") !== "") {
return false;
}
process.env[key] = entry.slice(idx + 1);
return true;
};
const countAppliedEntries = (entries: Iterable<string>) => {
let applied = 0;
for (const entry of entries) {
if (applyEntry(entry)) {
applied += 1;
}
}
return applied;
};
try {
const output = execFileSync(
"/bin/bash",
["-lc", `set -a; source "${profilePath}" >/dev/null 2>&1; env -0`],
{ encoding: "utf8" },
);
const applied = countAppliedEntries(output.split("\0").filter(Boolean));
if (applied > 0 && !isTruthyEnvValue(process.env.OPENCLAW_LIVE_TEST_QUIET)) {
console.log(`[live] loaded ${applied} env vars from ~/.profile`);
}
} catch {
try {
const fallbackEntries = fs
.readFileSync(profilePath, "utf8")
.split(/\r?\n/u)
.map((line) => line.trim())
.filter((line) => line && !line.startsWith("#"))
.map((line) => line.replace(/^export\s+/u, ""))
.map((line) => {
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/u);
if (!match) {
return "";
}
let value = match[2].trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
return `${match[1]}=${value}`;
})
.filter(Boolean);
const applied = countAppliedEntries(fallbackEntries);
if (applied > 0 && !isTruthyEnvValue(process.env.OPENCLAW_LIVE_TEST_QUIET)) {
console.log(`[live] loaded ${applied} env vars from ~/.profile`);
}
} catch {
// ignore profile load failures
}
}
}
function resolveRestoreEntries(): RestoreEntry[] {
return [
{ key: "OPENCLAW_TEST_FAST", value: process.env.OPENCLAW_TEST_FAST },
{
key: "OPENCLAW_STRICT_FAST_REPLY_CONFIG",
value: process.env.OPENCLAW_STRICT_FAST_REPLY_CONFIG,
},
{
key: "OPENCLAW_ALLOW_SLOW_REPLY_TESTS",
value: process.env.OPENCLAW_ALLOW_SLOW_REPLY_TESTS,
},
{ key: "HOME", value: process.env.HOME },
{ key: "USERPROFILE", value: process.env.USERPROFILE },
{ key: "XDG_CONFIG_HOME", value: process.env.XDG_CONFIG_HOME },
{ key: "XDG_DATA_HOME", value: process.env.XDG_DATA_HOME },
{ key: "XDG_STATE_HOME", value: process.env.XDG_STATE_HOME },
{ key: "XDG_CACHE_HOME", value: process.env.XDG_CACHE_HOME },
{ key: "OPENCLAW_STATE_DIR", value: process.env.OPENCLAW_STATE_DIR },
{ key: "OPENCLAW_CONFIG_PATH", value: process.env.OPENCLAW_CONFIG_PATH },
{ key: "OPENCLAW_GATEWAY_PORT", value: process.env.OPENCLAW_GATEWAY_PORT },
{ key: "OPENCLAW_BRIDGE_ENABLED", value: process.env.OPENCLAW_BRIDGE_ENABLED },
{ key: "OPENCLAW_BRIDGE_HOST", value: process.env.OPENCLAW_BRIDGE_HOST },
{ key: "OPENCLAW_BRIDGE_PORT", value: process.env.OPENCLAW_BRIDGE_PORT },
{ key: "OPENCLAW_CANVAS_HOST_PORT", value: process.env.OPENCLAW_CANVAS_HOST_PORT },
{ key: "OPENCLAW_TEST_HOME", value: process.env.OPENCLAW_TEST_HOME },
{ key: "OPENCLAW_AGENT_DIR", value: process.env.OPENCLAW_AGENT_DIR },
{ key: "PI_CODING_AGENT_DIR", value: process.env.PI_CODING_AGENT_DIR },
{ key: "TELEGRAM_BOT_TOKEN", value: process.env.TELEGRAM_BOT_TOKEN },
{ key: "DISCORD_BOT_TOKEN", value: process.env.DISCORD_BOT_TOKEN },
{ key: "SLACK_BOT_TOKEN", value: process.env.SLACK_BOT_TOKEN },
{ key: "SLACK_APP_TOKEN", value: process.env.SLACK_APP_TOKEN },
{ key: "SLACK_USER_TOKEN", value: process.env.SLACK_USER_TOKEN },
{ key: "COPILOT_GITHUB_TOKEN", value: process.env.COPILOT_GITHUB_TOKEN },
{ key: "GH_TOKEN", value: process.env.GH_TOKEN },
{ key: "GITHUB_TOKEN", value: process.env.GITHUB_TOKEN },
{ key: "NODE_OPTIONS", value: process.env.NODE_OPTIONS },
];
}
function createIsolatedTestHome(restore: RestoreEntry[]): {
cleanup: () => void;
tempHome: string;
} {
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-test-home-"));
process.env.HOME = tempHome;
process.env.USERPROFILE = tempHome;
process.env.OPENCLAW_TEST_HOME = tempHome;
process.env.OPENCLAW_TEST_FAST = "1";
process.env.OPENCLAW_STRICT_FAST_REPLY_CONFIG = "1";
delete process.env.OPENCLAW_ALLOW_SLOW_REPLY_TESTS;
// Ensure test runs never touch the developer's real config/state, even if they have overrides set.
delete process.env.OPENCLAW_CONFIG_PATH;
// Prefer deriving state dir from HOME so nested tests that change HOME also isolate correctly.
delete process.env.OPENCLAW_STATE_DIR;
delete process.env.OPENCLAW_AGENT_DIR;
delete process.env.PI_CODING_AGENT_DIR;
// Prefer test-controlled ports over developer overrides (avoid port collisions across tests/workers).
delete process.env.OPENCLAW_GATEWAY_PORT;
delete process.env.OPENCLAW_BRIDGE_ENABLED;
delete process.env.OPENCLAW_BRIDGE_HOST;
delete process.env.OPENCLAW_BRIDGE_PORT;
delete process.env.OPENCLAW_CANVAS_HOST_PORT;
// Avoid leaking real GitHub/Copilot tokens into non-live test runs.
delete process.env.TELEGRAM_BOT_TOKEN;
delete process.env.DISCORD_BOT_TOKEN;
delete process.env.SLACK_BOT_TOKEN;
delete process.env.SLACK_APP_TOKEN;
delete process.env.SLACK_USER_TOKEN;
delete process.env.COPILOT_GITHUB_TOKEN;
delete process.env.GH_TOKEN;
delete process.env.GITHUB_TOKEN;
// Avoid leaking local dev tooling flags into tests (e.g. --inspect).
delete process.env.NODE_OPTIONS;
// Windows: prefer the default state dir so auth/profile tests match real paths.
if (process.platform === "win32") {
process.env.OPENCLAW_STATE_DIR = path.join(tempHome, ".openclaw");
}
process.env.XDG_CONFIG_HOME = path.join(tempHome, ".config");
process.env.XDG_DATA_HOME = path.join(tempHome, ".local", "share");
process.env.XDG_STATE_HOME = path.join(tempHome, ".local", "state");
process.env.XDG_CACHE_HOME = path.join(tempHome, ".cache");
const cleanup = () => {
restoreEnv(restore);
try {
fs.rmSync(tempHome, { recursive: true, force: true });
} catch {
// ignore cleanup errors
}
};
return { cleanup, tempHome };
}
function ensureParentDir(targetPath: string): void {
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
}
function copyDirIfExists(sourcePath: string, targetPath: string): void {
if (!fs.existsSync(sourcePath)) {
return;
}
fs.mkdirSync(targetPath, { recursive: true });
fs.cpSync(sourcePath, targetPath, {
recursive: true,
force: true,
});
}
function copyFileIfExists(sourcePath: string, targetPath: string): void {
if (!fs.existsSync(sourcePath)) {
return;
}
let stat: fs.Stats;
try {
stat = fs.statSync(sourcePath);
} catch {
return;
}
if (!stat.isFile()) {
return;
}
ensureParentDir(targetPath);
fs.copyFileSync(sourcePath, targetPath);
}
function restoreClaudeConfigFromBackupIfNeeded(tempHome: string): void {
const targetPath = path.join(tempHome, ".claude.json");
if (fs.existsSync(targetPath)) {
return;
}
const backupsDir = path.join(tempHome, ".claude", "backups");
if (!fs.existsSync(backupsDir)) {
return;
}
const latestBackup = fs
.readdirSync(backupsDir)
.filter((entry) => entry.startsWith(".claude.json.backup."))
.toSorted()
.at(-1);
if (!latestBackup) {
return;
}
copyFileIfExists(path.join(backupsDir, latestBackup), targetPath);
}
function sanitizeLiveConfig(raw: string): string {
try {
const parsed: {
agents?: {
defaults?: Record<string, unknown>;
list?: Array<Record<string, unknown>>;
};
} = JSON5.parse(raw);
if (!parsed || typeof parsed !== "object") {
return raw;
}
if (parsed.agents?.defaults && typeof parsed.agents.defaults === "object") {
delete parsed.agents.defaults.workspace;
delete parsed.agents.defaults.agentDir;
}
if (Array.isArray(parsed.agents?.list)) {
parsed.agents.list = parsed.agents.list.map((entry) => {
if (!entry || typeof entry !== "object") {
return entry;
}
const nextEntry = { ...entry };
delete nextEntry.workspace;
delete nextEntry.agentDir;
return nextEntry;
});
}
return `${JSON.stringify(parsed, null, 2)}\n`;
} catch {
return raw;
}
}
function stageLiveAuthProfiles(params: {
env: NodeJS.ProcessEnv;
realHome: string;
realStateDir: string;
tempHome: string;
tempStateDir: string;
}): void {
const agentsDir = path.join(params.realStateDir, "agents");
if (!fs.existsSync(agentsDir)) {
return;
}
const sourceEnv: NodeJS.ProcessEnv = {
...params.env,
HOME: params.realHome,
USERPROFILE: params.realHome,
OPENCLAW_STATE_DIR: params.realStateDir,
};
const targetEnv: NodeJS.ProcessEnv = {
...params.env,
HOME: params.tempHome,
USERPROFILE: params.tempHome,
OPENCLAW_STATE_DIR: params.tempStateDir,
};
for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
if (!entry.isDirectory()) {
continue;
}
const sourceAgentDir = path.join(agentsDir, entry.name, "agent");
const store = loadPersistedAuthProfileStore(sourceAgentDir, { env: sourceEnv });
if (!store) {
continue;
}
const targetAgentDir = path.join(params.tempStateDir, "agents", entry.name, "agent");
fs.mkdirSync(targetAgentDir, { recursive: true });
savePersistedAuthProfileSecretsStore(store, targetAgentDir, { env: targetEnv });
}
}
function stageLiveTestState(params: {
env: NodeJS.ProcessEnv;
realHome: string;
tempHome: string;
}): void {
const rawStateDir = params.env.OPENCLAW_STATE_DIR?.trim();
let realStateDir = rawStateDir
? resolveHomeRelativePath(rawStateDir, params.realHome)
: path.join(params.realHome, ".openclaw");
const priorIsolatedHome = params.env.OPENCLAW_TEST_HOME?.trim();
const snapshotHome = params.env.HOME?.trim();
if (
priorIsolatedHome &&
snapshotHome &&
snapshotHome !== priorIsolatedHome &&
realStateDir === path.join(priorIsolatedHome, ".openclaw")
) {
realStateDir = path.join(params.realHome, ".openclaw");
}
const tempStateDir = path.join(params.tempHome, ".openclaw");
fs.mkdirSync(tempStateDir, { recursive: true });
fs.mkdirSync(path.join(params.tempHome, ".gemini"), { recursive: true });
const realConfigPath = params.env.OPENCLAW_CONFIG_PATH?.trim()
? resolveHomeRelativePath(params.env.OPENCLAW_CONFIG_PATH, params.realHome)
: path.join(realStateDir, "openclaw.json");
if (fs.existsSync(realConfigPath)) {
const rawConfig = fs.readFileSync(realConfigPath, "utf8");
fs.writeFileSync(
path.join(tempStateDir, "openclaw.json"),
sanitizeLiveConfig(rawConfig),
"utf8",
);
}
copyDirIfExists(path.join(realStateDir, "credentials"), path.join(tempStateDir, "credentials"));
copyDirIfExists(
path.join(realStateDir, "external-plugins"),
path.join(tempStateDir, "external-plugins"),
);
stageLiveAuthProfiles({
env: params.env,
realHome: params.realHome,
realStateDir,
tempHome: params.tempHome,
tempStateDir,
});
for (const authDir of LIVE_EXTERNAL_AUTH_DIRS) {
copyDirIfExists(path.join(params.realHome, authDir), path.join(params.tempHome, authDir));
}
for (const authFile of LIVE_EXTERNAL_AUTH_FILES) {
copyFileIfExists(path.join(params.realHome, authFile), path.join(params.tempHome, authFile));
}
restoreClaudeConfigFromBackupIfNeeded(params.tempHome);
}
export function installTestEnv(options?: { loadProfileEnv?: boolean }): {
cleanup: () => void;
tempHome: string;
} {
const live =
process.env.LIVE === "1" ||
process.env.OPENCLAW_LIVE_TEST === "1" ||
process.env.OPENCLAW_LIVE_GATEWAY === "1";
const allowRealHome = isTruthyEnvValue(process.env.OPENCLAW_LIVE_USE_REAL_HOME);
const realHome = process.env.HOME ?? os.homedir();
const liveEnvSnapshot = { ...process.env };
const shouldLoadProfileEnv = options?.loadProfileEnv ?? (live || allowRealHome);
if (shouldLoadProfileEnv) {
loadProfileEnv(realHome);
}
if (live && allowRealHome) {
return { cleanup: () => {}, tempHome: realHome };
}
const restore = resolveRestoreEntries();
const testEnv = createIsolatedTestHome(restore);
if (live) {
stageLiveTestState({ env: liveEnvSnapshot, realHome, tempHome: testEnv.tempHome });
}
return testEnv;
}
export function withIsolatedTestHome(options?: { loadProfileEnv?: boolean }): {
cleanup: () => void;
tempHome: string;
} {
return installTestEnv(options);
}