mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 19:00:45 +00:00
fix: harden private qa runtime loading
This commit is contained in:
@@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/local models: clarify low-context preflight hints for self-hosted models, point config-backed caps at the relevant OpenClaw setting, and stop suggesting larger models when `agents.defaults.contextTokens` is the real limit. (#66236) Thanks @ImLukeF.
|
||||
- Dreaming/memory-core: change the default `dreaming.storage.mode` from `inline` to `separate` so Dreaming phase blocks (`## Light Sleep`, `## REM Sleep`) land in `memory/dreaming/{phase}/YYYY-MM-DD.md` instead of being injected into `memory/YYYY-MM-DD.md`. Daily memory files no longer get dominated by structured candidate output, and the daily-ingestion scanner that already strips dream marker blocks no longer has to compete with hundreds of phase-block lines on every run. Operators who want the previous behavior can opt in by setting `plugins.entries.memory-core.config.dreaming.storage.mode: "inline"`. (#66412) Thanks @mjamiv.
|
||||
- Control UI/Overview: fix false-positive "missing" alerts on the Model Auth status card for aliased providers, env-backed OAuth with auth.profiles, and unresolvable env SecretRefs. (#67253) Thanks @omarshahine.
|
||||
- QA/private runtime: keep the private QA CLI on the local plugin-sdk seam and preserve staged `dist-runtime` root chunks when isolated QA staging mixes built plugin trees. (#67428) thanks @gumadeiras.
|
||||
|
||||
## 2026.4.15-beta.1
|
||||
|
||||
|
||||
@@ -165,6 +165,22 @@ function resolveQaStagedBundledTreeName(repoRoot: string) {
|
||||
return "dist";
|
||||
}
|
||||
|
||||
function resolveQaBuiltBundledPluginTreeRoot(params: { repoRoot: string; sourceDir: string }) {
|
||||
const sourceDir = path.resolve(params.sourceDir);
|
||||
for (const treeName of ["dist", "dist-runtime"] as const) {
|
||||
const extensionsRoot = path.join(params.repoRoot, treeName, "extensions");
|
||||
const relativeSourceDir = path.relative(extensionsRoot, sourceDir);
|
||||
if (
|
||||
relativeSourceDir.length > 0 &&
|
||||
!relativeSourceDir.startsWith("..") &&
|
||||
!path.isAbsolute(relativeSourceDir)
|
||||
) {
|
||||
return path.join(params.repoRoot, treeName);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function symlinkQaStagedDirEntry(params: {
|
||||
sourcePath: string;
|
||||
targetPath: string;
|
||||
@@ -196,6 +212,57 @@ async function seedQaStagedNodeModules(params: { repoRoot: string; stagedRoot: s
|
||||
}
|
||||
}
|
||||
|
||||
function collectQaBuiltTreeRoots(params: {
|
||||
repoRoot: string;
|
||||
stagedPluginIds: readonly string[];
|
||||
stagedTreeName: string;
|
||||
}) {
|
||||
const treeRoots = new Set<string>();
|
||||
treeRoots.add(path.join(params.repoRoot, params.stagedTreeName));
|
||||
for (const pluginId of params.stagedPluginIds) {
|
||||
const sourceDir = resolveQaBundledPluginSourceDir({
|
||||
repoRoot: params.repoRoot,
|
||||
pluginId,
|
||||
});
|
||||
if (!sourceDir) {
|
||||
continue;
|
||||
}
|
||||
const builtTreeRoot = resolveQaBuiltBundledPluginTreeRoot({
|
||||
repoRoot: params.repoRoot,
|
||||
sourceDir,
|
||||
});
|
||||
if (builtTreeRoot) {
|
||||
treeRoots.add(builtTreeRoot);
|
||||
}
|
||||
}
|
||||
return [...treeRoots];
|
||||
}
|
||||
|
||||
async function seedQaStagedBuiltTreeRoots(params: {
|
||||
stagedTreeRoot: string;
|
||||
sourceTreeRoots: readonly string[];
|
||||
}) {
|
||||
for (const sourceTreeRoot of params.sourceTreeRoots) {
|
||||
if (!existsSync(sourceTreeRoot)) {
|
||||
continue;
|
||||
}
|
||||
for (const entry of await fs.readdir(sourceTreeRoot, { withFileTypes: true })) {
|
||||
if (entry.name === "extensions") {
|
||||
continue;
|
||||
}
|
||||
const targetPath = path.join(params.stagedTreeRoot, entry.name);
|
||||
if (existsSync(targetPath)) {
|
||||
continue;
|
||||
}
|
||||
await symlinkQaStagedDirEntry({
|
||||
sourcePath: path.join(sourceTreeRoot, entry.name),
|
||||
targetPath,
|
||||
directory: entry.isDirectory(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveQaRuntimeHostVersion(params: {
|
||||
repoRoot: string;
|
||||
allowedPluginIds: readonly string[];
|
||||
@@ -254,7 +321,10 @@ export async function createQaBundledPluginsDir(params: {
|
||||
);
|
||||
await fs.rm(stagedRoot, { recursive: true, force: true });
|
||||
await fs.mkdir(stagedRoot, { recursive: true });
|
||||
await fs.copyFile(path.join(params.repoRoot, "package.json"), path.join(stagedRoot, "package.json"));
|
||||
await fs.copyFile(
|
||||
path.join(params.repoRoot, "package.json"),
|
||||
path.join(stagedRoot, "package.json"),
|
||||
);
|
||||
await seedQaStagedNodeModules({
|
||||
repoRoot: params.repoRoot,
|
||||
stagedRoot,
|
||||
@@ -266,21 +336,16 @@ export async function createQaBundledPluginsDir(params: {
|
||||
path.join(stagedOpenClawPackageDir, "package.json"),
|
||||
);
|
||||
const stagedTreeName = resolveQaStagedBundledTreeName(params.repoRoot);
|
||||
const sourceTreeRoot = path.join(params.repoRoot, stagedTreeName);
|
||||
const stagedTreeRoot = path.join(stagedRoot, stagedTreeName);
|
||||
await fs.mkdir(stagedTreeRoot, { recursive: true });
|
||||
if (existsSync(sourceTreeRoot)) {
|
||||
for (const entry of await fs.readdir(sourceTreeRoot, { withFileTypes: true })) {
|
||||
if (entry.name === "extensions") {
|
||||
continue;
|
||||
}
|
||||
await symlinkQaStagedDirEntry({
|
||||
sourcePath: path.join(sourceTreeRoot, entry.name),
|
||||
targetPath: path.join(stagedTreeRoot, entry.name),
|
||||
directory: entry.isDirectory(),
|
||||
});
|
||||
}
|
||||
}
|
||||
await seedQaStagedBuiltTreeRoots({
|
||||
stagedTreeRoot,
|
||||
sourceTreeRoots: collectQaBuiltTreeRoots({
|
||||
repoRoot: params.repoRoot,
|
||||
stagedPluginIds,
|
||||
stagedTreeName,
|
||||
}),
|
||||
});
|
||||
if (stagedTreeName === "dist-runtime" && !existsSync(path.join(stagedRoot, "dist"))) {
|
||||
const repoDistDir = path.join(params.repoRoot, "dist");
|
||||
const stagedDistTarget = existsSync(repoDistDir) ? repoDistDir : stagedTreeRoot;
|
||||
|
||||
@@ -654,9 +654,7 @@ describe("qa bundled plugin dir", () => {
|
||||
repoRoot,
|
||||
pluginId: "qa-channel",
|
||||
}),
|
||||
).toBe(
|
||||
path.join(repoRoot, "dist", "extensions", "qa-channel"),
|
||||
);
|
||||
).toBe(path.join(repoRoot, "dist", "extensions", "qa-channel"));
|
||||
});
|
||||
|
||||
it("falls back to the source bundled plugin when no built copy exists", async () => {
|
||||
@@ -672,9 +670,7 @@ describe("qa bundled plugin dir", () => {
|
||||
repoRoot,
|
||||
pluginId: "qa-channel",
|
||||
}),
|
||||
).toBe(
|
||||
path.join(repoRoot, "extensions", "qa-channel"),
|
||||
);
|
||||
).toBe(path.join(repoRoot, "extensions", "qa-channel"));
|
||||
});
|
||||
|
||||
it("creates a scoped bundled plugin tree for allowed plugins plus always-allowed runtime facades", async () => {
|
||||
@@ -784,6 +780,82 @@ describe("qa bundled plugin dir", () => {
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
|
||||
it("preserves dist-runtime-only root chunks when dist also exists", async () => {
|
||||
const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-mixed-runtime-"));
|
||||
cleanups.push(async () => {
|
||||
await rm(repoRoot, { recursive: true, force: true });
|
||||
});
|
||||
await writeFile(
|
||||
path.join(repoRoot, "package.json"),
|
||||
JSON.stringify({ name: "openclaw", type: "module" }, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
await mkdir(path.join(repoRoot, "dist"), { recursive: true });
|
||||
await writeFile(
|
||||
path.join(repoRoot, "dist", "shared-dist.js"),
|
||||
'export const dist = "dist";\n',
|
||||
"utf8",
|
||||
);
|
||||
await mkdir(path.join(repoRoot, "dist-runtime", "extensions", "runtime-only"), {
|
||||
recursive: true,
|
||||
});
|
||||
await writeFile(
|
||||
path.join(repoRoot, "dist-runtime", "runtime-chunk.js"),
|
||||
'export const marker = "runtime";\n',
|
||||
"utf8",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(repoRoot, "dist-runtime", "extensions", "runtime-only", "package.json"),
|
||||
JSON.stringify({ name: "@openclaw/runtime-only", type: "module" }, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(repoRoot, "dist-runtime", "extensions", "runtime-only", "index.js"),
|
||||
['import { marker } from "../../runtime-chunk.js";', "export { marker };", ""].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-mixed-target-"));
|
||||
cleanups.push(async () => {
|
||||
await rm(tempRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const { bundledPluginsDir } = await __testing.createQaBundledPluginsDir({
|
||||
repoRoot,
|
||||
tempRoot,
|
||||
allowedPluginIds: ["runtime-only"],
|
||||
});
|
||||
|
||||
expect(bundledPluginsDir).toBe(
|
||||
path.join(
|
||||
repoRoot,
|
||||
".artifacts",
|
||||
"qa-runtime",
|
||||
path.basename(tempRoot),
|
||||
"dist",
|
||||
"extensions",
|
||||
),
|
||||
);
|
||||
await expect(
|
||||
import(
|
||||
`${pathToFileURL(path.join(bundledPluginsDir, "runtime-only", "index.js")).href}?t=${Date.now()}`
|
||||
),
|
||||
).resolves.toMatchObject({
|
||||
marker: "runtime",
|
||||
});
|
||||
await expect(
|
||||
lstat(
|
||||
path.join(
|
||||
repoRoot,
|
||||
".artifacts",
|
||||
"qa-runtime",
|
||||
path.basename(tempRoot),
|
||||
"dist",
|
||||
"runtime-chunk.js",
|
||||
),
|
||||
),
|
||||
).resolves.toBeTruthy();
|
||||
});
|
||||
|
||||
it("stages source-only bundled plugins into a repo-like runtime root with node_modules", async () => {
|
||||
const repoRoot = await mkdtemp(path.join(os.tmpdir(), "qa-bundled-source-stage-"));
|
||||
cleanups.push(async () => {
|
||||
|
||||
13
src/cli/program/private-qa-cli.test.ts
Normal file
13
src/cli/program/private-qa-cli.test.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { loadPrivateQaCliModule } from "./private-qa-cli.js";
|
||||
|
||||
describe("private-qa-cli", () => {
|
||||
it("loads the private QA CLI facade through the local plugin-sdk entrypoint", async () => {
|
||||
const module = await loadPrivateQaCliModule();
|
||||
|
||||
expect(module).toMatchObject({
|
||||
isQaLabCliAvailable: expect.any(Function),
|
||||
registerQaLabCli: expect.any(Function),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,6 @@ export function isPrivateQaCliEnabled(env: NodeJS.ProcessEnv = process.env): boo
|
||||
}
|
||||
|
||||
export function loadPrivateQaCliModule(): Promise<Record<string, unknown>> {
|
||||
const specifier = "openclaw/plugin-sdk/qa-lab";
|
||||
const specifier = "../../plugin-sdk/qa-lab.js";
|
||||
return import(specifier) as Promise<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ const { inferAction, registerCapabilityCli } = vi.hoisted(() => {
|
||||
vi.mock("../acp-cli.js", () => ({ registerAcpCli }));
|
||||
vi.mock("../nodes-cli.js", () => ({ registerNodesCli }));
|
||||
vi.mock("../capability-cli.js", () => ({ registerCapabilityCli }));
|
||||
vi.mock("openclaw/plugin-sdk/qa-lab", () => ({ registerQaLabCli }));
|
||||
vi.mock("../../plugin-sdk/qa-lab.js", () => ({ registerQaLabCli }));
|
||||
|
||||
describe("registerSubCliCommands", () => {
|
||||
const originalArgv = process.argv;
|
||||
|
||||
Reference in New Issue
Block a user