mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-29 16:03:34 +00:00
* test(qa): run vitest and playwright scenarios from qa suite * fix(qa): harden scenario suite dispatch * refactor(qa): share scenario path utilities * refactor(qa): share test file scenario runner * refactor(qa): route test file scenarios through suite runtime * refactor(qa): use explicit suite runtime result kind * test(qa): write suite evidence artifact * refactor(qa): clarify suite execution dispatch * fix(qa): keep test-file scenarios out of flow-only runners * refactor(qa): export mixed scenario suite runner
100 lines
3.3 KiB
TypeScript
100 lines
3.3 KiB
TypeScript
// Qa Lab plugin module implements cli paths behavior.
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { assertNoSymlinkParents, pathScope } from "openclaw/plugin-sdk/security-runtime";
|
|
|
|
export function toRepoPath(filePath: string): string {
|
|
return filePath.split(path.sep).join("/");
|
|
}
|
|
|
|
export function toRepoRelativePath(repoRoot: string, filePath: string): string {
|
|
return toRepoPath(path.relative(repoRoot, filePath));
|
|
}
|
|
|
|
export function isRepoRootRelativeRef(value: string) {
|
|
return !path.isAbsolute(value) && value.split(/[\\/]+/u).every((part) => part !== "..");
|
|
}
|
|
|
|
export function resolveRepoRelativeOutputDir(repoRoot: string, outputDir?: string) {
|
|
if (!outputDir) {
|
|
return undefined;
|
|
}
|
|
if (path.isAbsolute(outputDir)) {
|
|
throw new Error("--output-dir must be a relative path inside the repo root.");
|
|
}
|
|
const resolved = pathScope(repoRoot, { label: "repo root" }).resolve(outputDir);
|
|
if (!resolved.ok) {
|
|
throw new Error("--output-dir must stay within the repo root.");
|
|
}
|
|
return resolved.path;
|
|
}
|
|
|
|
async function resolveNearestExistingPath(targetPath: string) {
|
|
let current = path.resolve(targetPath);
|
|
while (true) {
|
|
try {
|
|
await fs.lstat(current);
|
|
return current;
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
throw error;
|
|
}
|
|
}
|
|
const parent = path.dirname(current);
|
|
if (parent === current) {
|
|
throw new Error(`failed to resolve existing path for ${targetPath}`);
|
|
}
|
|
current = parent;
|
|
}
|
|
}
|
|
|
|
function assertRepoRelativePath(repoRoot: string, targetPath: string, label: string) {
|
|
const relative = path.relative(repoRoot, targetPath);
|
|
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
throw new Error(`${label} must stay within the repo root.`);
|
|
}
|
|
return relative;
|
|
}
|
|
|
|
async function assertNoSymlinkSegments(repoRoot: string, targetPath: string, label: string) {
|
|
assertRepoRelativePath(repoRoot, targetPath, label);
|
|
try {
|
|
await assertNoSymlinkParents({
|
|
rootDir: repoRoot,
|
|
targetPath,
|
|
messagePrefix: label,
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof Error && error.message.includes("symlink")) {
|
|
throw new Error(`${label} must not traverse symlinks.`, { cause: error });
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function assertRepoBoundPath(repoRoot: string, targetPath: string, label: string) {
|
|
const repoRootResolved = path.resolve(repoRoot);
|
|
const targetResolved = path.resolve(targetPath);
|
|
assertRepoRelativePath(repoRootResolved, targetResolved, label);
|
|
await assertNoSymlinkSegments(repoRootResolved, targetResolved, label);
|
|
const repoRootReal = await fs.realpath(repoRootResolved);
|
|
const nearestExistingPath = await resolveNearestExistingPath(targetResolved);
|
|
const nearestExistingReal = await fs.realpath(nearestExistingPath);
|
|
assertRepoRelativePath(repoRootReal, nearestExistingReal, label);
|
|
return targetResolved;
|
|
}
|
|
|
|
export async function ensureRepoBoundDirectory(
|
|
repoRoot: string,
|
|
targetDir: string,
|
|
label: string,
|
|
opts?: { mode?: number },
|
|
) {
|
|
await assertNoSymlinkSegments(path.resolve(repoRoot), path.resolve(targetDir), label);
|
|
const result = await pathScope(repoRoot, { label }).ensureDir(targetDir, { mode: opts?.mode });
|
|
if (!result.ok) {
|
|
throw new Error(`${label} must stay within the repo root.`);
|
|
}
|
|
return result.path;
|
|
}
|