mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-20 22:40:58 +00:00
fix(config): split config doc baseline coverage
This commit is contained in:
138
src/config/doc-baseline.integration.test.ts
Normal file
138
src/config/doc-baseline.integration.test.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildConfigDocBaseline,
|
||||
renderConfigDocBaselineStatefile,
|
||||
writeConfigDocBaselineStatefile,
|
||||
} from "./doc-baseline.js";
|
||||
|
||||
describe("config doc baseline integration", () => {
|
||||
const tempRoots: string[] = [];
|
||||
let sharedBaselinePromise: Promise<Awaited<ReturnType<typeof buildConfigDocBaseline>>> | null =
|
||||
null;
|
||||
let sharedRenderedPromise: Promise<
|
||||
Awaited<ReturnType<typeof renderConfigDocBaselineStatefile>>
|
||||
> | null = null;
|
||||
|
||||
function getSharedBaseline() {
|
||||
sharedBaselinePromise ??= buildConfigDocBaseline();
|
||||
return sharedBaselinePromise;
|
||||
}
|
||||
|
||||
function getSharedRendered() {
|
||||
sharedRenderedPromise ??= renderConfigDocBaselineStatefile(getSharedBaseline());
|
||||
return sharedRenderedPromise;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
tempRoots.splice(0).map(async (tempRoot) => {
|
||||
await fs.rm(tempRoot, { recursive: true, force: true });
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("is deterministic across repeated runs", async () => {
|
||||
const first = await renderConfigDocBaselineStatefile();
|
||||
const second = await renderConfigDocBaselineStatefile();
|
||||
|
||||
expect(second.json).toBe(first.json);
|
||||
expect(second.jsonl).toBe(first.jsonl);
|
||||
});
|
||||
|
||||
it("includes core, channel, and plugin config metadata", async () => {
|
||||
const baseline = await getSharedBaseline();
|
||||
const byPath = new Map(baseline.entries.map((entry) => [entry.path, entry]));
|
||||
|
||||
expect(byPath.get("gateway.auth.token")).toMatchObject({
|
||||
kind: "core",
|
||||
sensitive: true,
|
||||
});
|
||||
expect(byPath.get("channels.telegram.botToken")).toMatchObject({
|
||||
kind: "channel",
|
||||
sensitive: true,
|
||||
});
|
||||
expect(byPath.get("plugins.entries.voice-call.config.twilio.authToken")).toMatchObject({
|
||||
kind: "plugin",
|
||||
sensitive: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves help text and tags from merged schema hints", async () => {
|
||||
const baseline = await getSharedBaseline();
|
||||
const byPath = new Map(baseline.entries.map((entry) => [entry.path, entry]));
|
||||
const tokenEntry = byPath.get("gateway.auth.token");
|
||||
|
||||
expect(tokenEntry?.help).toContain("gateway access");
|
||||
expect(tokenEntry?.tags).toContain("auth");
|
||||
expect(tokenEntry?.tags).toContain("security");
|
||||
});
|
||||
|
||||
it("matches array help hints that still use [] notation", async () => {
|
||||
const baseline = await getSharedBaseline();
|
||||
const byPath = new Map(baseline.entries.map((entry) => [entry.path, entry]));
|
||||
|
||||
expect(byPath.get("session.sendPolicy.rules.*.match.keyPrefix")).toMatchObject({
|
||||
help: expect.stringContaining("prefer rawKeyPrefix when exact full-key matching is required"),
|
||||
sensitive: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("walks union branches for nested config keys", async () => {
|
||||
const baseline = await getSharedBaseline();
|
||||
const byPath = new Map(baseline.entries.map((entry) => [entry.path, entry]));
|
||||
|
||||
expect(byPath.get("bindings.*")).toMatchObject({
|
||||
hasChildren: true,
|
||||
});
|
||||
expect(byPath.get("bindings.*.type")).toBeDefined();
|
||||
expect(byPath.get("bindings.*.match.channel")).toBeDefined();
|
||||
expect(byPath.get("bindings.*.match.peer.id")).toBeDefined();
|
||||
});
|
||||
|
||||
it("supports check mode for stale generated artifacts", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-config-doc-baseline-"));
|
||||
tempRoots.push(tempRoot);
|
||||
const rendered = getSharedRendered();
|
||||
|
||||
const initial = await writeConfigDocBaselineStatefile({
|
||||
repoRoot: tempRoot,
|
||||
jsonPath: "docs/.generated/config-baseline.json",
|
||||
statefilePath: "docs/.generated/config-baseline.jsonl",
|
||||
rendered,
|
||||
});
|
||||
expect(initial.wrote).toBe(true);
|
||||
|
||||
const current = await writeConfigDocBaselineStatefile({
|
||||
repoRoot: tempRoot,
|
||||
jsonPath: "docs/.generated/config-baseline.json",
|
||||
statefilePath: "docs/.generated/config-baseline.jsonl",
|
||||
check: true,
|
||||
rendered,
|
||||
});
|
||||
expect(current.changed).toBe(false);
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(tempRoot, "docs/.generated/config-baseline.json"),
|
||||
'{"generatedBy":"broken","entries":[]}\n',
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(tempRoot, "docs/.generated/config-baseline.jsonl"),
|
||||
'{"recordType":"meta","generatedBy":"broken","totalPaths":0}\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const stale = await writeConfigDocBaselineStatefile({
|
||||
repoRoot: tempRoot,
|
||||
jsonPath: "docs/.generated/config-baseline.json",
|
||||
statefilePath: "docs/.generated/config-baseline.jsonl",
|
||||
check: true,
|
||||
rendered,
|
||||
});
|
||||
expect(stale.changed).toBe(true);
|
||||
expect(stale.wrote).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,107 +1,17 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildConfigDocBaseline,
|
||||
collectConfigDocBaselineEntries,
|
||||
dedupeConfigDocBaselineEntries,
|
||||
normalizeConfigDocBaselineHelpPath,
|
||||
renderConfigDocBaselineStatefile,
|
||||
writeConfigDocBaselineStatefile,
|
||||
} from "./doc-baseline.js";
|
||||
|
||||
describe("config doc baseline", () => {
|
||||
const tempRoots: string[] = [];
|
||||
let sharedBaselinePromise: Promise<Awaited<ReturnType<typeof buildConfigDocBaseline>>> | null =
|
||||
null;
|
||||
let sharedRenderedPromise: Promise<
|
||||
Awaited<ReturnType<typeof renderConfigDocBaselineStatefile>>
|
||||
> | null = null;
|
||||
|
||||
function getSharedBaseline() {
|
||||
sharedBaselinePromise ??= buildConfigDocBaseline();
|
||||
return sharedBaselinePromise;
|
||||
}
|
||||
|
||||
function getSharedRendered() {
|
||||
sharedRenderedPromise ??= renderConfigDocBaselineStatefile(getSharedBaseline());
|
||||
return sharedRenderedPromise;
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
tempRoots.splice(0).map(async (tempRoot) => {
|
||||
await fs.rm(tempRoot, { recursive: true, force: true });
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("is deterministic across repeated runs", async () => {
|
||||
const first = await renderConfigDocBaselineStatefile();
|
||||
const second = await renderConfigDocBaselineStatefile();
|
||||
|
||||
expect(second.json).toBe(first.json);
|
||||
expect(second.jsonl).toBe(first.jsonl);
|
||||
});
|
||||
|
||||
it("normalizes array and record paths to wildcard form", async () => {
|
||||
const baseline = await getSharedBaseline();
|
||||
const paths = new Set(baseline.entries.map((entry) => entry.path));
|
||||
|
||||
expect(paths.has("session.sendPolicy.rules.*.match.keyPrefix")).toBe(true);
|
||||
expect(paths.has("env.*")).toBe(true);
|
||||
expect(normalizeConfigDocBaselineHelpPath("agents.list[].skills")).toBe("agents.list.*.skills");
|
||||
});
|
||||
|
||||
it("includes core, channel, and plugin config metadata", async () => {
|
||||
const baseline = await getSharedBaseline();
|
||||
const byPath = new Map(baseline.entries.map((entry) => [entry.path, entry]));
|
||||
|
||||
expect(byPath.get("gateway.auth.token")).toMatchObject({
|
||||
kind: "core",
|
||||
sensitive: true,
|
||||
});
|
||||
expect(byPath.get("channels.telegram.botToken")).toMatchObject({
|
||||
kind: "channel",
|
||||
sensitive: true,
|
||||
});
|
||||
expect(byPath.get("plugins.entries.voice-call.config.twilio.authToken")).toMatchObject({
|
||||
kind: "plugin",
|
||||
sensitive: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves help text and tags from merged schema hints", async () => {
|
||||
const baseline = await getSharedBaseline();
|
||||
const byPath = new Map(baseline.entries.map((entry) => [entry.path, entry]));
|
||||
const tokenEntry = byPath.get("gateway.auth.token");
|
||||
|
||||
expect(tokenEntry?.help).toContain("gateway access");
|
||||
expect(tokenEntry?.tags).toContain("auth");
|
||||
expect(tokenEntry?.tags).toContain("security");
|
||||
});
|
||||
|
||||
it("matches array help hints that still use [] notation", async () => {
|
||||
const baseline = await getSharedBaseline();
|
||||
const byPath = new Map(baseline.entries.map((entry) => [entry.path, entry]));
|
||||
|
||||
expect(byPath.get("session.sendPolicy.rules.*.match.keyPrefix")).toMatchObject({
|
||||
help: expect.stringContaining("prefer rawKeyPrefix when exact full-key matching is required"),
|
||||
sensitive: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("walks union branches for nested config keys", async () => {
|
||||
const baseline = await getSharedBaseline();
|
||||
const byPath = new Map(baseline.entries.map((entry) => [entry.path, entry]));
|
||||
|
||||
expect(byPath.get("bindings.*")).toMatchObject({
|
||||
hasChildren: true,
|
||||
});
|
||||
expect(byPath.get("bindings.*.type")).toBeDefined();
|
||||
expect(byPath.get("bindings.*.match.channel")).toBeDefined();
|
||||
expect(byPath.get("bindings.*.match.peer.id")).toBeDefined();
|
||||
expect(normalizeConfigDocBaselineHelpPath("session.sendPolicy.rules[0].match.keyPrefix")).toBe(
|
||||
"session.sendPolicy.rules.*.match.keyPrefix",
|
||||
);
|
||||
expect(normalizeConfigDocBaselineHelpPath(".env.*.")).toBe("env.*");
|
||||
});
|
||||
|
||||
it("merges tuple item metadata instead of dropping earlier entries", () => {
|
||||
@@ -132,48 +42,4 @@ describe("config doc baseline", () => {
|
||||
expect(tupleEntry?.enumValues).toEqual(expect.arrayContaining([42, "alpha"]));
|
||||
expect(tupleEntry?.enumValues).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("supports check mode for stale generated artifacts", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-config-doc-baseline-"));
|
||||
tempRoots.push(tempRoot);
|
||||
const rendered = getSharedRendered();
|
||||
|
||||
const initial = await writeConfigDocBaselineStatefile({
|
||||
repoRoot: tempRoot,
|
||||
jsonPath: "docs/.generated/config-baseline.json",
|
||||
statefilePath: "docs/.generated/config-baseline.jsonl",
|
||||
rendered,
|
||||
});
|
||||
expect(initial.wrote).toBe(true);
|
||||
|
||||
const current = await writeConfigDocBaselineStatefile({
|
||||
repoRoot: tempRoot,
|
||||
jsonPath: "docs/.generated/config-baseline.json",
|
||||
statefilePath: "docs/.generated/config-baseline.jsonl",
|
||||
check: true,
|
||||
rendered,
|
||||
});
|
||||
expect(current.changed).toBe(false);
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(tempRoot, "docs/.generated/config-baseline.json"),
|
||||
'{"generatedBy":"broken","entries":[]}\n',
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(tempRoot, "docs/.generated/config-baseline.jsonl"),
|
||||
'{"recordType":"meta","generatedBy":"broken","totalPaths":0}\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const stale = await writeConfigDocBaselineStatefile({
|
||||
repoRoot: tempRoot,
|
||||
jsonPath: "docs/.generated/config-baseline.json",
|
||||
statefilePath: "docs/.generated/config-baseline.jsonl",
|
||||
check: true,
|
||||
rendered,
|
||||
});
|
||||
expect(stale.changed).toBe(true);
|
||||
expect(stale.wrote).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fsSync from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { ChannelPlugin } from "../channels/plugins/index.js";
|
||||
import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js";
|
||||
import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js";
|
||||
import { FIELD_HELP } from "./schema.help.js";
|
||||
import { buildConfigSchema, type ConfigSchemaResponse } from "./schema.js";
|
||||
import type { ConfigSchemaResponse } from "./schema.js";
|
||||
import { findWildcardHintMatch, schemaHasChildren } from "./schema.shared.js";
|
||||
|
||||
type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
|
||||
@@ -28,12 +25,6 @@ type JsonSchemaObject = JsonSchemaNode & {
|
||||
oneOf?: JsonSchemaObject[];
|
||||
};
|
||||
|
||||
type PackageChannelMetadata = {
|
||||
id: string;
|
||||
label: string;
|
||||
blurb?: string;
|
||||
};
|
||||
|
||||
type ChannelSurfaceMetadata = {
|
||||
id: string;
|
||||
label: string;
|
||||
@@ -277,191 +268,11 @@ function resolveFirstExistingPath(candidates: string[]): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function loadPackageChannelMetadata(rootDir: string): PackageChannelMetadata | null {
|
||||
try {
|
||||
const packageJson = JSON.parse(
|
||||
fsSync.readFileSync(path.join(rootDir, "package.json"), "utf8"),
|
||||
) as {
|
||||
openclaw?: {
|
||||
channel?: {
|
||||
id?: unknown;
|
||||
label?: unknown;
|
||||
blurb?: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
const channel = packageJson.openclaw?.channel;
|
||||
if (!channel) {
|
||||
return null;
|
||||
}
|
||||
const id = typeof channel.id === "string" ? channel.id.trim() : "";
|
||||
const label = typeof channel.label === "string" ? channel.label.trim() : "";
|
||||
const blurb = typeof channel.blurb === "string" ? channel.blurb.trim() : "";
|
||||
if (!id || !label) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
...(blurb ? { blurb } : {}),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isChannelPlugin(value: unknown): value is ChannelPlugin {
|
||||
if (!value || typeof value !== "object") {
|
||||
return false;
|
||||
}
|
||||
const candidate = value as { id?: unknown; meta?: unknown; capabilities?: unknown };
|
||||
return typeof candidate.id === "string" && typeof candidate.meta === "object";
|
||||
}
|
||||
|
||||
function resolveSetupChannelPlugin(value: unknown): ChannelPlugin | null {
|
||||
if (!value || typeof value !== "object") {
|
||||
return null;
|
||||
}
|
||||
const candidate = value as { plugin?: unknown };
|
||||
return isChannelPlugin(candidate.plugin) ? candidate.plugin : null;
|
||||
}
|
||||
|
||||
async function importChannelPluginModule(rootDir: string): Promise<ChannelPlugin> {
|
||||
logConfigDocBaselineDebug(`resolve channel module ${rootDir}`);
|
||||
const modulePath = resolveFirstExistingPath([
|
||||
path.join(rootDir, "setup-entry.ts"),
|
||||
path.join(rootDir, "setup-entry.js"),
|
||||
path.join(rootDir, "setup-entry.mts"),
|
||||
path.join(rootDir, "setup-entry.mjs"),
|
||||
path.join(rootDir, "src", "channel.ts"),
|
||||
path.join(rootDir, "src", "channel.js"),
|
||||
path.join(rootDir, "src", "plugin.ts"),
|
||||
path.join(rootDir, "src", "plugin.js"),
|
||||
path.join(rootDir, "src", "index.ts"),
|
||||
path.join(rootDir, "src", "index.js"),
|
||||
path.join(rootDir, "src", "channel.mts"),
|
||||
path.join(rootDir, "src", "channel.mjs"),
|
||||
path.join(rootDir, "src", "plugin.mts"),
|
||||
path.join(rootDir, "src", "plugin.mjs"),
|
||||
]);
|
||||
if (!modulePath) {
|
||||
throw new Error(`channel source not found under ${rootDir}`);
|
||||
}
|
||||
|
||||
logConfigDocBaselineDebug(`import channel module ${modulePath}`);
|
||||
const imported = (await import(modulePath)) as Record<string, unknown>;
|
||||
logConfigDocBaselineDebug(`imported channel module ${modulePath}`);
|
||||
for (const value of Object.values(imported)) {
|
||||
if (isChannelPlugin(value)) {
|
||||
logConfigDocBaselineDebug(`resolved channel export ${modulePath}`);
|
||||
return value;
|
||||
}
|
||||
const setupPlugin = resolveSetupChannelPlugin(value);
|
||||
if (setupPlugin) {
|
||||
logConfigDocBaselineDebug(`resolved setup channel export ${modulePath}`);
|
||||
return setupPlugin;
|
||||
}
|
||||
if (typeof value === "function" && value.length === 0) {
|
||||
const resolved = value();
|
||||
if (isChannelPlugin(resolved)) {
|
||||
logConfigDocBaselineDebug(`resolved channel factory ${modulePath}`);
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`channel plugin export not found in ${modulePath}`);
|
||||
}
|
||||
|
||||
async function importChannelSurfaceMetadata(
|
||||
rootDir: string,
|
||||
repoRoot: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): Promise<ChannelSurfaceMetadata | null> {
|
||||
logConfigDocBaselineDebug(`resolve channel config surface ${rootDir}`);
|
||||
const packageMetadata = loadPackageChannelMetadata(rootDir);
|
||||
if (!packageMetadata) {
|
||||
logConfigDocBaselineDebug(`missing package channel metadata ${rootDir}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const modulePath = resolveFirstExistingPath([
|
||||
path.join(rootDir, "src", "config-schema.ts"),
|
||||
path.join(rootDir, "src", "config-schema.js"),
|
||||
path.join(rootDir, "src", "config-schema.mts"),
|
||||
path.join(rootDir, "src", "config-schema.mjs"),
|
||||
]);
|
||||
if (!modulePath) {
|
||||
logConfigDocBaselineDebug(`missing channel config schema module ${rootDir}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
logConfigDocBaselineDebug(`import channel config schema ${modulePath}`);
|
||||
try {
|
||||
logConfigDocBaselineDebug(`spawn channel config schema subprocess ${modulePath}`);
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
"tsx",
|
||||
path.join(repoRoot, "scripts", "load-channel-config-surface.ts"),
|
||||
modulePath,
|
||||
],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
env,
|
||||
timeout: 15_000,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
if (result.status !== 0 || result.error) {
|
||||
throw result.error ?? new Error(result.stderr || `child exited with status ${result.status}`);
|
||||
}
|
||||
logConfigDocBaselineDebug(`completed channel config schema subprocess ${modulePath}`);
|
||||
const configSchema = JSON.parse(result.stdout) as {
|
||||
schema: Record<string, unknown>;
|
||||
uiHints?: ConfigSchemaResponse["uiHints"];
|
||||
};
|
||||
return {
|
||||
id: packageMetadata.id,
|
||||
label: packageMetadata.label,
|
||||
description: packageMetadata.blurb,
|
||||
configSchema: configSchema.schema,
|
||||
configUiHints: configSchema.uiHints,
|
||||
};
|
||||
} catch (error) {
|
||||
logConfigDocBaselineDebug(
|
||||
`channel config schema subprocess failed for ${modulePath}: ${String(error)}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadChannelSurfaceMetadata(
|
||||
rootDir: string,
|
||||
repoRoot: string,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): Promise<ChannelSurfaceMetadata> {
|
||||
logConfigDocBaselineDebug(`load channel surface ${rootDir}`);
|
||||
const configSurface = await importChannelSurfaceMetadata(rootDir, repoRoot, env);
|
||||
if (configSurface) {
|
||||
logConfigDocBaselineDebug(`resolved channel config surface ${rootDir}`);
|
||||
return configSurface;
|
||||
}
|
||||
|
||||
logConfigDocBaselineDebug(`fallback to channel plugin import ${rootDir}`);
|
||||
const plugin = await importChannelPluginModule(rootDir);
|
||||
return {
|
||||
id: plugin.id,
|
||||
label: plugin.meta.label,
|
||||
description: plugin.meta.blurb,
|
||||
configSchema: plugin.configSchema?.schema,
|
||||
configUiHints: plugin.configSchema?.uiHints,
|
||||
};
|
||||
}
|
||||
|
||||
async function loadBundledConfigSchemaResponse(): Promise<ConfigSchemaResponse> {
|
||||
const [{ loadPluginManifestRegistry }, { buildConfigSchema }] = await Promise.all([
|
||||
import("../plugins/manifest-registry.js"),
|
||||
import("./schema.js"),
|
||||
]);
|
||||
const repoRoot = resolveRepoRoot();
|
||||
const env = {
|
||||
...process.env,
|
||||
@@ -479,22 +290,49 @@ async function loadBundledConfigSchemaResponse(): Promise<ConfigSchemaResponse>
|
||||
const bundledChannelPlugins = manifestRegistry.plugins.filter(
|
||||
(plugin) => plugin.origin === "bundled" && plugin.channels.length > 0,
|
||||
);
|
||||
const loadChannelsSequentiallyForDebug = process.env.OPENCLAW_CONFIG_DOC_BASELINE_DEBUG === "1";
|
||||
const channelPlugins = loadChannelsSequentiallyForDebug
|
||||
? await bundledChannelPlugins.reduce<Promise<ChannelSurfaceMetadata[]>>(
|
||||
async (promise, plugin) => {
|
||||
const loaded = await promise;
|
||||
loaded.push(await loadChannelSurfaceMetadata(plugin.rootDir, repoRoot, env));
|
||||
return loaded;
|
||||
},
|
||||
Promise.resolve([]),
|
||||
)
|
||||
: await Promise.all(
|
||||
bundledChannelPlugins.map(
|
||||
async (plugin) => await loadChannelSurfaceMetadata(plugin.rootDir, repoRoot, env),
|
||||
),
|
||||
);
|
||||
logConfigDocBaselineDebug(`imported ${channelPlugins.length} bundled channel plugins`);
|
||||
const channelPlugins =
|
||||
process.env.OPENCLAW_CONFIG_DOC_BASELINE_DEBUG === "1"
|
||||
? await bundledChannelPlugins.reduce<Promise<ChannelSurfaceMetadata[]>>(
|
||||
async (promise, plugin) => {
|
||||
const loaded = await promise;
|
||||
loaded.push(
|
||||
(await loadChannelSurfaceMetadata(
|
||||
plugin.rootDir,
|
||||
plugin.id,
|
||||
plugin.name ?? plugin.id,
|
||||
repoRoot,
|
||||
)) ?? {
|
||||
id: plugin.id,
|
||||
label: plugin.name ?? plugin.id,
|
||||
description: plugin.description,
|
||||
configSchema: plugin.configSchema,
|
||||
configUiHints: plugin.configUiHints,
|
||||
},
|
||||
);
|
||||
return loaded;
|
||||
},
|
||||
Promise.resolve([]),
|
||||
)
|
||||
: await Promise.all(
|
||||
bundledChannelPlugins.map(
|
||||
async (plugin) =>
|
||||
(await loadChannelSurfaceMetadata(
|
||||
plugin.rootDir,
|
||||
plugin.id,
|
||||
plugin.name ?? plugin.id,
|
||||
repoRoot,
|
||||
)) ?? {
|
||||
id: plugin.id,
|
||||
label: plugin.name ?? plugin.id,
|
||||
description: plugin.description,
|
||||
configSchema: plugin.configSchema,
|
||||
configUiHints: plugin.configUiHints,
|
||||
},
|
||||
),
|
||||
);
|
||||
logConfigDocBaselineDebug(
|
||||
`loaded ${channelPlugins.length} bundled channel entries from channel surfaces`,
|
||||
);
|
||||
|
||||
return buildConfigSchema({
|
||||
cache: false,
|
||||
@@ -517,6 +355,48 @@ async function loadBundledConfigSchemaResponse(): Promise<ConfigSchemaResponse>
|
||||
});
|
||||
}
|
||||
|
||||
async function loadChannelSurfaceMetadata(
|
||||
rootDir: string,
|
||||
id: string,
|
||||
label: string,
|
||||
repoRoot: string,
|
||||
): Promise<ChannelSurfaceMetadata | null> {
|
||||
logConfigDocBaselineDebug(`resolve channel config surface ${rootDir}`);
|
||||
const modulePath = resolveFirstExistingPath([
|
||||
path.join(rootDir, "src", "config-schema.ts"),
|
||||
path.join(rootDir, "src", "config-schema.js"),
|
||||
path.join(rootDir, "src", "config-schema.mts"),
|
||||
path.join(rootDir, "src", "config-schema.mjs"),
|
||||
]);
|
||||
if (!modulePath) {
|
||||
logConfigDocBaselineDebug(`missing channel config schema module ${rootDir}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
logConfigDocBaselineDebug(`import channel config schema ${modulePath}`);
|
||||
try {
|
||||
const { loadChannelConfigSurfaceModule } =
|
||||
await import("../../scripts/load-channel-config-surface.ts");
|
||||
const configSurface = await loadChannelConfigSurfaceModule(modulePath, { repoRoot });
|
||||
if (!configSurface) {
|
||||
logConfigDocBaselineDebug(`channel config schema export missing ${modulePath}`);
|
||||
return null;
|
||||
}
|
||||
logConfigDocBaselineDebug(`completed channel config schema import ${modulePath}`);
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
configSchema: configSurface.schema,
|
||||
configUiHints: configSurface.uiHints as ConfigSchemaResponse["uiHints"] | undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
logConfigDocBaselineDebug(
|
||||
`channel config schema import failed for ${modulePath}: ${String(error)}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function collectConfigDocBaselineEntries(
|
||||
schema: JsonSchemaObject,
|
||||
uiHints: ConfigSchemaResponse["uiHints"],
|
||||
|
||||
4
test/fixtures/test-parallel.behavior.json
vendored
4
test/fixtures/test-parallel.behavior.json
vendored
@@ -14,7 +14,7 @@
|
||||
"reason": "Mutates process.cwd() and core loader seams."
|
||||
},
|
||||
{
|
||||
"file": "src/config/doc-baseline.test.ts",
|
||||
"file": "src/config/doc-baseline.integration.test.ts",
|
||||
"reason": "Rebuilds bundled config baselines through many channel schema subprocesses; keep out of the shared lane."
|
||||
},
|
||||
{
|
||||
@@ -28,7 +28,7 @@
|
||||
"reason": "Clean in isolation, but can hang after sharing the broad lane."
|
||||
},
|
||||
{
|
||||
"file": "src/config/doc-baseline.test.ts",
|
||||
"file": "src/config/doc-baseline.integration.test.ts",
|
||||
"reason": "Builds the full bundled config schema graph and is safer outside the shared unit-fast heap."
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user