mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-04 15:20:21 +00:00
test: harden path resolution test helpers
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readCommandSource } from "./command-source.test-helpers.js";
|
||||
|
||||
const SECRET_TARGET_CALLSITES = [
|
||||
"src/cli/memory-cli.ts",
|
||||
@@ -14,36 +13,6 @@ const SECRET_TARGET_CALLSITES = [
|
||||
"src/commands/status.scan.ts",
|
||||
] as const;
|
||||
|
||||
async function readCommandSource(relativePath: string): Promise<string> {
|
||||
const absolutePath = path.join(process.cwd(), relativePath);
|
||||
const source = await fs.readFile(absolutePath, "utf8");
|
||||
const reexportMatch = source.match(/^export \* from "(?<target>[^"]+)";$/m)?.groups?.target;
|
||||
const runtimeImportMatch = source.match(/import\("(?<target>\.[^"]+\.runtime\.js)"\)/m)?.groups
|
||||
?.target;
|
||||
if (runtimeImportMatch) {
|
||||
const resolvedTarget = path.join(path.dirname(absolutePath), runtimeImportMatch);
|
||||
const tsResolvedTarget = resolvedTarget.replace(/\.js$/u, ".ts");
|
||||
const runtimeSource = await fs.readFile(tsResolvedTarget, "utf8");
|
||||
return `${source}\n${runtimeSource}`;
|
||||
}
|
||||
if (!reexportMatch) {
|
||||
if (source.includes("resolveCommandSecretRefsViaGateway")) {
|
||||
return source;
|
||||
}
|
||||
const runtimeImportMatch = source.match(/import\("(?<target>\.[^"]+\.runtime\.js)"\)/m)?.groups
|
||||
?.target;
|
||||
if (!runtimeImportMatch) {
|
||||
return source;
|
||||
}
|
||||
const resolvedTarget = path.join(path.dirname(absolutePath), runtimeImportMatch);
|
||||
const tsResolvedTarget = resolvedTarget.replace(/\.js$/u, ".ts");
|
||||
return await fs.readFile(tsResolvedTarget, "utf8");
|
||||
}
|
||||
const resolvedTarget = path.join(path.dirname(absolutePath), reexportMatch);
|
||||
const tsResolvedTarget = resolvedTarget.replace(/\.js$/u, ".ts");
|
||||
return await fs.readFile(tsResolvedTarget, "utf8");
|
||||
}
|
||||
|
||||
function hasSupportedTargetIdsWiring(source: string): boolean {
|
||||
return (
|
||||
/targetIds:\s*get[A-Za-z0-9_]+\(\)/m.test(source) ||
|
||||
|
||||
63
src/cli/command-source.test-helpers.test.ts
Normal file
63
src/cli/command-source.test-helpers.test.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { readCommandSource } from "./command-source.test-helpers.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function makeTempDir(): string {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-command-source-"));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
describe("readCommandSource", () => {
|
||||
it("follows re-export shims and runtime boundaries", async () => {
|
||||
const rootDir = makeTempDir();
|
||||
const cliDir = path.join(rootDir, "src", "cli");
|
||||
fs.mkdirSync(cliDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(cliDir, "index.ts"), 'export * from "./command.js";\n');
|
||||
fs.writeFileSync(
|
||||
path.join(cliDir, "command.ts"),
|
||||
[
|
||||
"async function loadRuntime() {",
|
||||
' return await import("./command.runtime.js");',
|
||||
"}",
|
||||
"export { loadRuntime };",
|
||||
].join("\n"),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(cliDir, "command.runtime.ts"),
|
||||
'export const marker = "resolveCommandSecretRefsViaGateway";\n',
|
||||
);
|
||||
|
||||
const source = await readCommandSource("src/cli/index.ts", rootDir);
|
||||
|
||||
expect(source).toContain('export * from "./command.js";');
|
||||
expect(source).toContain('import("./command.runtime.js")');
|
||||
expect(source).toContain("resolveCommandSecretRefsViaGateway");
|
||||
});
|
||||
|
||||
it("dedupes repeated runtime imports", async () => {
|
||||
const rootDir = makeTempDir();
|
||||
const cliDir = path.join(rootDir, "src", "cli");
|
||||
fs.mkdirSync(cliDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(cliDir, "command.ts"),
|
||||
['await import("./shared.runtime.js");', 'await import("./shared.runtime.js");'].join("\n"),
|
||||
);
|
||||
fs.writeFileSync(path.join(cliDir, "shared.runtime.ts"), "export const shared = true;\n");
|
||||
|
||||
const source = await readCommandSource("src/cli/command.ts", rootDir);
|
||||
const occurrences = source.match(/export const shared = true;/gu) ?? [];
|
||||
|
||||
expect(occurrences).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
50
src/cli/command-source.test-helpers.ts
Normal file
50
src/cli/command-source.test-helpers.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
function resolveImportedTypeScriptPath(importerPath: string, target: string): string {
|
||||
const resolvedTarget = path.join(path.dirname(importerPath), target);
|
||||
return resolvedTarget.replace(/\.js$/u, ".ts");
|
||||
}
|
||||
|
||||
async function readModuleSource(modulePath: string, seen: Set<string>): Promise<string> {
|
||||
const resolvedPath = path.resolve(modulePath);
|
||||
if (seen.has(resolvedPath)) {
|
||||
return "";
|
||||
}
|
||||
seen.add(resolvedPath);
|
||||
|
||||
const source = await fs.readFile(resolvedPath, "utf8");
|
||||
if (source.includes("resolveCommandSecretRefsViaGateway")) {
|
||||
return source;
|
||||
}
|
||||
const nestedTargets = new Set<string>();
|
||||
|
||||
for (const match of source.matchAll(/^export \* from "(?<target>[^"]+)";$/gmu)) {
|
||||
const target = match.groups?.target;
|
||||
if (target) {
|
||||
nestedTargets.add(resolveImportedTypeScriptPath(resolvedPath, target));
|
||||
}
|
||||
}
|
||||
|
||||
for (const match of source.matchAll(/import\("(?<target>\.[^"]+\.runtime\.js)"\)/gmu)) {
|
||||
const target = match.groups?.target;
|
||||
if (target) {
|
||||
nestedTargets.add(resolveImportedTypeScriptPath(resolvedPath, target));
|
||||
}
|
||||
}
|
||||
|
||||
const nestedSources = (
|
||||
await Promise.all(
|
||||
[...nestedTargets].map(async (targetPath) => await readModuleSource(targetPath, seen)),
|
||||
)
|
||||
).filter(Boolean);
|
||||
|
||||
return nestedSources.length > 0 ? [source, ...nestedSources].join("\n") : source;
|
||||
}
|
||||
|
||||
export async function readCommandSource(
|
||||
relativePath: string,
|
||||
cwd = process.cwd(),
|
||||
): Promise<string> {
|
||||
return await readModuleSource(path.join(cwd, relativePath), new Set<string>());
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { withTempHome } from "../../../test/helpers/temp-home.js";
|
||||
import { withPathResolutionEnv } from "../../test-utils/env.js";
|
||||
import type { OpenClawConfig } from "../config.js";
|
||||
import { resolveStorePath } from "./paths.js";
|
||||
import {
|
||||
@@ -87,16 +88,25 @@ describe("resolveSessionStoreTargets", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const targets = resolveSessionStoreTargets(cfg, { allAgents: true });
|
||||
const homeDir = path.resolve(path.sep, "tmp", "openclaw-home");
|
||||
const targets = withPathResolutionEnv(homeDir, {}, (env) =>
|
||||
resolveSessionStoreTargets(cfg, { allAgents: true }, { env }),
|
||||
);
|
||||
|
||||
expect(targets).toEqual([
|
||||
{
|
||||
agentId: "main",
|
||||
storePath: resolveStorePath(cfg.session?.store, { agentId: "main", env: process.env }),
|
||||
storePath: resolveStorePath(cfg.session?.store, {
|
||||
agentId: "main",
|
||||
env: { HOME: homeDir },
|
||||
}),
|
||||
},
|
||||
{
|
||||
agentId: "work",
|
||||
storePath: resolveStorePath(cfg.session?.store, { agentId: "work", env: process.env }),
|
||||
storePath: resolveStorePath(cfg.session?.store, {
|
||||
agentId: "work",
|
||||
env: { HOME: homeDir },
|
||||
}),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { withEnv } from "../test-utils/env.js";
|
||||
import { withPathResolutionEnv } from "../test-utils/env.js";
|
||||
import { formatPluginSourceForTable, resolvePluginSourceRoots } from "./source-display.js";
|
||||
|
||||
function createPluginSourceRoots() {
|
||||
@@ -63,24 +63,16 @@ describe("formatPluginSourceForTable", () => {
|
||||
});
|
||||
|
||||
it("resolves source roots from an explicit env override", () => {
|
||||
const ignoredHome = path.resolve(path.sep, "tmp", "ignored-home");
|
||||
const homeDir = path.resolve(path.sep, "tmp", "openclaw-home");
|
||||
const roots = withEnv(
|
||||
const roots = withPathResolutionEnv(
|
||||
homeDir,
|
||||
{
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(ignoredHome, "ignored-bundled"),
|
||||
OPENCLAW_STATE_DIR: path.join(ignoredHome, "ignored-state"),
|
||||
OPENCLAW_HOME: undefined,
|
||||
HOME: ignoredHome,
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: "~/bundled",
|
||||
OPENCLAW_STATE_DIR: "~/state",
|
||||
},
|
||||
() =>
|
||||
(env) =>
|
||||
resolvePluginSourceRoots({
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: homeDir,
|
||||
OPENCLAW_HOME: undefined,
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: "~/bundled",
|
||||
OPENCLAW_STATE_DIR: "~/state",
|
||||
},
|
||||
env,
|
||||
workspaceDir: "~/ws",
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { captureEnv, captureFullEnv, withEnv, withEnvAsync } from "./env.js";
|
||||
import {
|
||||
captureEnv,
|
||||
captureFullEnv,
|
||||
createPathResolutionEnv,
|
||||
withEnv,
|
||||
withEnvAsync,
|
||||
withPathResolutionEnv,
|
||||
} from "./env.js";
|
||||
|
||||
function restoreEnvKey(key: string, previous: string | undefined): void {
|
||||
if (previous === undefined) {
|
||||
@@ -109,4 +117,58 @@ describe("env test utils", () => {
|
||||
expect(process.env[key]).toBe("outer");
|
||||
restoreEnvKey(key, prev);
|
||||
});
|
||||
|
||||
it("createPathResolutionEnv clears leaked path overrides before applying explicit ones", () => {
|
||||
const homeDir = path.join(path.sep, "tmp", "openclaw-home");
|
||||
const previousOpenClawHome = process.env.OPENCLAW_HOME;
|
||||
const previousStateDir = process.env.OPENCLAW_STATE_DIR;
|
||||
const previousBundledDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
process.env.OPENCLAW_HOME = "/srv/openclaw-home";
|
||||
process.env.OPENCLAW_STATE_DIR = "/srv/openclaw-state";
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = "/srv/openclaw-bundled";
|
||||
|
||||
try {
|
||||
const env = createPathResolutionEnv(homeDir, {
|
||||
OPENCLAW_STATE_DIR: "~/state",
|
||||
});
|
||||
|
||||
expect(env.HOME).toBe(homeDir);
|
||||
expect(env.OPENCLAW_HOME).toBeUndefined();
|
||||
expect(env.OPENCLAW_BUNDLED_PLUGINS_DIR).toBeUndefined();
|
||||
expect(env.OPENCLAW_STATE_DIR).toBe("~/state");
|
||||
} finally {
|
||||
restoreEnvKey("OPENCLAW_HOME", previousOpenClawHome);
|
||||
restoreEnvKey("OPENCLAW_STATE_DIR", previousStateDir);
|
||||
restoreEnvKey("OPENCLAW_BUNDLED_PLUGINS_DIR", previousBundledDir);
|
||||
}
|
||||
});
|
||||
|
||||
it("withPathResolutionEnv only applies the explicit path env inside the callback", () => {
|
||||
const homeDir = path.join(path.sep, "tmp", "openclaw-home");
|
||||
const previousOpenClawHome = process.env.OPENCLAW_HOME;
|
||||
process.env.OPENCLAW_HOME = "/srv/openclaw-home";
|
||||
|
||||
try {
|
||||
const seen = withPathResolutionEnv(
|
||||
homeDir,
|
||||
{ OPENCLAW_BUNDLED_PLUGINS_DIR: "~/bundled" },
|
||||
(env) => ({
|
||||
processHome: process.env.HOME,
|
||||
processOpenClawHome: process.env.OPENCLAW_HOME,
|
||||
processBundledDir: process.env.OPENCLAW_BUNDLED_PLUGINS_DIR,
|
||||
envBundledDir: env.OPENCLAW_BUNDLED_PLUGINS_DIR,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(seen).toEqual({
|
||||
processHome: homeDir,
|
||||
processOpenClawHome: undefined,
|
||||
processBundledDir: "~/bundled",
|
||||
envBundledDir: "~/bundled",
|
||||
});
|
||||
expect(process.env.OPENCLAW_HOME).toBe("/srv/openclaw-home");
|
||||
} finally {
|
||||
restoreEnvKey("OPENCLAW_HOME", previousOpenClawHome);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import path from "node:path";
|
||||
|
||||
export function captureEnv(keys: string[]) {
|
||||
const snapshot = new Map<string, string | undefined>();
|
||||
for (const key of keys) {
|
||||
@@ -27,6 +29,70 @@ function applyEnvValues(env: Record<string, string | undefined>): void {
|
||||
}
|
||||
}
|
||||
|
||||
const PATH_RESOLUTION_ENV_KEYS = [
|
||||
"HOME",
|
||||
"USERPROFILE",
|
||||
"HOMEDRIVE",
|
||||
"HOMEPATH",
|
||||
"OPENCLAW_HOME",
|
||||
"OPENCLAW_STATE_DIR",
|
||||
"CLAWDBOT_STATE_DIR",
|
||||
"OPENCLAW_BUNDLED_PLUGINS_DIR",
|
||||
] as const;
|
||||
|
||||
function resolveWindowsHomeParts(homeDir: string): { homeDrive?: string; homePath?: string } {
|
||||
if (process.platform !== "win32") {
|
||||
return {};
|
||||
}
|
||||
const match = homeDir.match(/^([A-Za-z]:)(.*)$/);
|
||||
if (!match) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
homeDrive: match[1],
|
||||
homePath: match[2] || "\\",
|
||||
};
|
||||
}
|
||||
|
||||
export function createPathResolutionEnv(
|
||||
homeDir: string,
|
||||
env: Record<string, string | undefined> = {},
|
||||
): NodeJS.ProcessEnv {
|
||||
const resolvedHome = path.resolve(homeDir);
|
||||
const nextEnv: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
HOME: resolvedHome,
|
||||
USERPROFILE: resolvedHome,
|
||||
OPENCLAW_HOME: undefined,
|
||||
OPENCLAW_STATE_DIR: undefined,
|
||||
CLAWDBOT_STATE_DIR: undefined,
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: undefined,
|
||||
};
|
||||
|
||||
const windowsHome = resolveWindowsHomeParts(resolvedHome);
|
||||
nextEnv.HOMEDRIVE = windowsHome.homeDrive;
|
||||
nextEnv.HOMEPATH = windowsHome.homePath;
|
||||
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
nextEnv[key] = value;
|
||||
}
|
||||
|
||||
return nextEnv;
|
||||
}
|
||||
|
||||
export function withPathResolutionEnv<T>(
|
||||
homeDir: string,
|
||||
env: Record<string, string | undefined>,
|
||||
fn: (resolvedEnv: NodeJS.ProcessEnv) => T,
|
||||
): T {
|
||||
const resolvedEnv = createPathResolutionEnv(homeDir, env);
|
||||
const scopedEnv: Record<string, string | undefined> = {};
|
||||
for (const key of new Set([...PATH_RESOLUTION_ENV_KEYS, ...Object.keys(env)])) {
|
||||
scopedEnv[key] = resolvedEnv[key];
|
||||
}
|
||||
return withEnv(scopedEnv, () => fn(resolvedEnv));
|
||||
}
|
||||
|
||||
export function captureFullEnv() {
|
||||
const snapshot: Record<string, string | undefined> = { ...process.env };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user