mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 09:21:35 +07:00
test: harden path resolution test helpers
This commit is contained in:
3
.github/workflows/workflow-sanity.yml
vendored
3
.github/workflows/workflow-sanity.yml
vendored
@@ -72,6 +72,9 @@ jobs:
|
||||
- name: Disallow direct inputs interpolation in composite run blocks
|
||||
run: python3 scripts/check-composite-action-input-interpolation.py
|
||||
|
||||
- name: Disallow tracked merge conflict markers
|
||||
run: node scripts/check-no-conflict-markers.mjs
|
||||
|
||||
generated-doc-baselines:
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
|
||||
@@ -571,13 +571,14 @@
|
||||
"build:plugin-sdk:dts": "tsc -p tsconfig.plugin-sdk.dts.json",
|
||||
"build:strict-smoke": "pnpm canvas:a2ui:bundle && node scripts/tsdown-build.mjs && node scripts/runtime-postbuild.mjs && pnpm build:plugin-sdk:dts",
|
||||
"canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh",
|
||||
"check": "pnpm check:host-env-policy:swift && pnpm check:base-config-schema && pnpm check:bundled-plugin-metadata && pnpm check:bundled-provider-auth-env-vars && pnpm format:check && pnpm tsgo && pnpm plugin-sdk:check-exports && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:tmp:no-raw-channel-fetch && pnpm lint:agent:ingress-owner && pnpm lint:plugins:no-register-http-handler && pnpm lint:plugins:no-monolithic-plugin-sdk-entry-imports && pnpm lint:plugins:no-extension-src-imports && pnpm lint:plugins:no-extension-test-core-imports && pnpm lint:plugins:no-extension-imports && pnpm lint:plugins:plugin-sdk-subpaths-exported && pnpm lint:extensions:no-src-outside-plugin-sdk && pnpm lint:extensions:no-plugin-sdk-internal && pnpm lint:extensions:no-relative-outside-package && pnpm lint:web-search-provider-boundaries && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope",
|
||||
"check": "pnpm check:host-env-policy:swift && pnpm check:base-config-schema && pnpm check:bundled-plugin-metadata && pnpm check:bundled-provider-auth-env-vars && pnpm check:no-conflict-markers && pnpm format:check && pnpm tsgo && pnpm plugin-sdk:check-exports && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:tmp:no-raw-channel-fetch && pnpm lint:agent:ingress-owner && pnpm lint:plugins:no-register-http-handler && pnpm lint:plugins:no-monolithic-plugin-sdk-entry-imports && pnpm lint:plugins:no-extension-src-imports && pnpm lint:plugins:no-extension-test-core-imports && pnpm lint:plugins:no-extension-imports && pnpm lint:plugins:plugin-sdk-subpaths-exported && pnpm lint:extensions:no-src-outside-plugin-sdk && pnpm lint:extensions:no-plugin-sdk-internal && pnpm lint:extensions:no-relative-outside-package && pnpm lint:web-search-provider-boundaries && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope",
|
||||
"check:base-config-schema": "node --import tsx scripts/generate-base-config-schema.ts --check",
|
||||
"check:bundled-plugin-metadata": "node scripts/generate-bundled-plugin-metadata.mjs --check",
|
||||
"check:bundled-provider-auth-env-vars": "node scripts/generate-bundled-provider-auth-env-vars.mjs --check",
|
||||
"check:docs": "pnpm format:docs:check && pnpm lint:docs && pnpm docs:check-i18n-glossary && pnpm docs:check-links",
|
||||
"check:host-env-policy:swift": "node scripts/generate-host-env-security-policy-swift.mjs --check",
|
||||
"check:loc": "node --import tsx scripts/check-ts-max-loc.ts --max 500",
|
||||
"check:no-conflict-markers": "node scripts/check-no-conflict-markers.mjs",
|
||||
"config:docs:check": "node --import tsx scripts/generate-config-doc-baseline.ts --check",
|
||||
"config:docs:gen": "node --import tsx scripts/generate-config-doc-baseline.ts --write",
|
||||
"config:schema:check": "node --import tsx scripts/generate-base-config-schema.ts --check",
|
||||
|
||||
80
scripts/check-no-conflict-markers.mjs
Normal file
80
scripts/check-no-conflict-markers.mjs
Normal file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { runAsScript } from "./lib/ts-guard-utils.mjs";
|
||||
|
||||
function isBinaryBuffer(buffer) {
|
||||
return buffer.includes(0);
|
||||
}
|
||||
|
||||
export function findConflictMarkerLines(content) {
|
||||
const lines = content.split(/\r?\n/u);
|
||||
const matches = [];
|
||||
for (const [index, line] of lines.entries()) {
|
||||
if (
|
||||
line.startsWith("<<<<<<< ") ||
|
||||
line.startsWith("||||||| ") ||
|
||||
line === "=======" ||
|
||||
line.startsWith(">>>>>>> ")
|
||||
) {
|
||||
matches.push(index + 1);
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
export function listTrackedFiles(cwd = process.cwd()) {
|
||||
const output = execFileSync("git", ["ls-files", "-z"], {
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
});
|
||||
return output
|
||||
.split("\0")
|
||||
.filter(Boolean)
|
||||
.map((relativePath) => path.join(cwd, relativePath));
|
||||
}
|
||||
|
||||
export function findConflictMarkersInFiles(filePaths, readFile = fs.readFileSync) {
|
||||
const violations = [];
|
||||
for (const filePath of filePaths) {
|
||||
let content;
|
||||
try {
|
||||
content = readFile(filePath);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (!Buffer.isBuffer(content)) {
|
||||
content = Buffer.from(String(content));
|
||||
}
|
||||
if (isBinaryBuffer(content)) {
|
||||
continue;
|
||||
}
|
||||
const lines = findConflictMarkerLines(content.toString("utf8"));
|
||||
if (lines.length > 0) {
|
||||
violations.push({
|
||||
filePath,
|
||||
lines,
|
||||
});
|
||||
}
|
||||
}
|
||||
return violations;
|
||||
}
|
||||
|
||||
export async function main() {
|
||||
const cwd = process.cwd();
|
||||
const violations = findConflictMarkersInFiles(listTrackedFiles(cwd));
|
||||
if (violations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error("Found unresolved merge conflict markers:");
|
||||
for (const violation of violations) {
|
||||
const relativePath = path.relative(cwd, violation.filePath) || violation.filePath;
|
||||
console.error(`- ${relativePath}:${violation.lines.join(",")}`);
|
||||
}
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
runAsScript(import.meta.url, main);
|
||||
@@ -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 };
|
||||
|
||||
|
||||
64
test/scripts/check-no-conflict-markers.test.ts
Normal file
64
test/scripts/check-no-conflict-markers.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
findConflictMarkerLines,
|
||||
findConflictMarkersInFiles,
|
||||
} from "../../scripts/check-no-conflict-markers.mjs";
|
||||
|
||||
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-conflict-markers-"));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
describe("check-no-conflict-markers", () => {
|
||||
it("finds git conflict markers at the start of lines", () => {
|
||||
expect(
|
||||
findConflictMarkerLines(
|
||||
[
|
||||
"const ok = true;",
|
||||
"<<<<<<< HEAD",
|
||||
"value = left;",
|
||||
"=======",
|
||||
"value = right;",
|
||||
">>>>>>> main",
|
||||
].join("\n"),
|
||||
),
|
||||
).toEqual([2, 4, 6]);
|
||||
});
|
||||
|
||||
it("ignores marker-like text when it is indented or inline", () => {
|
||||
expect(
|
||||
findConflictMarkerLines(
|
||||
["Example:", " <<<<<<< HEAD", "const text = '======= not a conflict';"].join("\n"),
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("scans text files and skips binary files", () => {
|
||||
const rootDir = makeTempDir();
|
||||
const textFile = path.join(rootDir, "CHANGELOG.md");
|
||||
const binaryFile = path.join(rootDir, "image.png");
|
||||
fs.writeFileSync(textFile, "<<<<<<< HEAD\nconflict\n>>>>>>> main\n");
|
||||
fs.writeFileSync(binaryFile, Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00]));
|
||||
|
||||
const violations = findConflictMarkersInFiles([textFile, binaryFile]);
|
||||
|
||||
expect(violations).toEqual([
|
||||
{
|
||||
filePath: textFile,
|
||||
lines: [1, 3],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user