Config: stabilize bundled channel metadata loading

This commit is contained in:
Onur Solmaz
2026-04-13 00:26:44 +02:00
parent b2f94d9bb8
commit 4503a43b90
3 changed files with 401 additions and 284 deletions

View File

@@ -281,17 +281,23 @@ export async function loadChannelConfigSurfaceModule(
candidatePath: string,
): { schema: Record<string, unknown>; uiHints?: Record<string, unknown> } | null => {
try {
const bunLoaded = loadViaBun(candidatePath);
if (bunLoaded && isBuiltChannelConfigSchema(bunLoaded)) {
return bunLoaded;
// Prefer the source-aware Jiti path so generated config metadata stays
// stable before and after build output exists in the repo.
const imported = loadViaJiti(candidatePath);
const resolved = resolveConfigSchemaExport(imported);
if (resolved) {
return resolved;
}
} catch {
// Bun is the fastest happy path, but some plugin config modules only load
// correctly through the source-aware Jiti alias setup.
// Fall back to Bun below when the source-aware loader cannot resolve the
// module graph in the current environment.
}
const imported = loadViaJiti(candidatePath);
return resolveConfigSchemaExport(imported);
const bunLoaded = loadViaBun(candidatePath);
if (bunLoaded && isBuiltChannelConfigSchema(bunLoaded)) {
return bunLoaded;
}
return null;
};
try {

View File

@@ -731,6 +731,10 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
type: "string",
enum: ["open", "disabled", "allowlist"],
},
contextVisibility: {
type: "string",
enum: ["all", "allowlist", "allowlist_quote"],
},
historyLimit: {
type: "integer",
minimum: 0,
@@ -763,77 +767,84 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
exclusiveMinimum: 0,
maximum: 9007199254740991,
},
chunkMode: {
type: "string",
enum: ["length", "newline"],
},
blockStreaming: {
type: "boolean",
},
blockStreamingCoalesce: {
streaming: {
type: "object",
properties: {
minChars: {
type: "integer",
exclusiveMinimum: 0,
maximum: 9007199254740991,
},
maxChars: {
type: "integer",
exclusiveMinimum: 0,
maximum: 9007199254740991,
},
idleMs: {
type: "integer",
minimum: 0,
maximum: 9007199254740991,
},
},
additionalProperties: false,
},
streaming: {
anyOf: [
{
type: "boolean",
},
{
mode: {
type: "string",
enum: ["off", "partial", "block", "progress"],
},
],
},
streamMode: {
type: "string",
enum: ["partial", "block", "off"],
},
draftChunk: {
type: "object",
properties: {
minChars: {
type: "integer",
exclusiveMinimum: 0,
maximum: 9007199254740991,
chunkMode: {
type: "string",
enum: ["length", "newline"],
},
maxChars: {
type: "integer",
exclusiveMinimum: 0,
maximum: 9007199254740991,
preview: {
type: "object",
properties: {
chunk: {
type: "object",
properties: {
minChars: {
type: "integer",
exclusiveMinimum: 0,
maximum: 9007199254740991,
},
maxChars: {
type: "integer",
exclusiveMinimum: 0,
maximum: 9007199254740991,
},
breakPreference: {
anyOf: [
{
type: "string",
const: "paragraph",
},
{
type: "string",
const: "newline",
},
{
type: "string",
const: "sentence",
},
],
},
},
additionalProperties: false,
},
},
additionalProperties: false,
},
breakPreference: {
anyOf: [
{
type: "string",
const: "paragraph",
block: {
type: "object",
properties: {
enabled: {
type: "boolean",
},
{
type: "string",
const: "newline",
coalesce: {
type: "object",
properties: {
minChars: {
type: "integer",
exclusiveMinimum: 0,
maximum: 9007199254740991,
},
maxChars: {
type: "integer",
exclusiveMinimum: 0,
maximum: 9007199254740991,
},
idleMs: {
type: "integer",
minimum: 0,
maximum: 9007199254740991,
},
},
additionalProperties: false,
},
{
type: "string",
const: "sentence",
},
],
},
additionalProperties: false,
},
},
additionalProperties: false,
@@ -950,6 +961,10 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
type: "string",
const: "all",
},
{
type: "string",
const: "batched",
},
],
},
dmPolicy: {
@@ -959,14 +974,7 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
allowFrom: {
type: "array",
items: {
anyOf: [
{
type: "string",
},
{
type: "number",
},
],
type: "string",
},
},
defaultTo: {
@@ -985,14 +993,7 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
allowFrom: {
type: "array",
items: {
anyOf: [
{
type: "string",
},
{
type: "number",
},
],
type: "string",
},
},
groupEnabled: {
@@ -1001,14 +1002,7 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
groupChannels: {
type: "array",
items: {
anyOf: [
{
type: "string",
},
{
type: "number",
},
],
type: "string",
},
},
},
@@ -1092,27 +1086,13 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
users: {
type: "array",
items: {
anyOf: [
{
type: "string",
},
{
type: "number",
},
],
type: "string",
},
},
roles: {
type: "array",
items: {
anyOf: [
{
type: "string",
},
{
type: "number",
},
],
type: "string",
},
},
channels: {
@@ -1123,9 +1103,6 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
additionalProperties: {
type: "object",
properties: {
allow: {
type: "boolean",
},
requireMention: {
type: "boolean",
},
@@ -1198,27 +1175,13 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
users: {
type: "array",
items: {
anyOf: [
{
type: "string",
},
{
type: "number",
},
],
type: "string",
},
},
roles: {
type: "array",
items: {
anyOf: [
{
type: "string",
},
{
type: "number",
},
],
type: "string",
},
},
systemPrompt: {
@@ -1299,14 +1262,7 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
approvers: {
type: "array",
items: {
anyOf: [
{
type: "string",
},
{
type: "number",
},
],
type: "string",
},
},
agentFilter: {
@@ -1939,6 +1895,10 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
type: "string",
enum: ["open", "disabled", "allowlist"],
},
contextVisibility: {
type: "string",
enum: ["all", "allowlist", "allowlist_quote"],
},
historyLimit: {
type: "integer",
minimum: 0,
@@ -1971,77 +1931,84 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
exclusiveMinimum: 0,
maximum: 9007199254740991,
},
chunkMode: {
type: "string",
enum: ["length", "newline"],
},
blockStreaming: {
type: "boolean",
},
blockStreamingCoalesce: {
streaming: {
type: "object",
properties: {
minChars: {
type: "integer",
exclusiveMinimum: 0,
maximum: 9007199254740991,
},
maxChars: {
type: "integer",
exclusiveMinimum: 0,
maximum: 9007199254740991,
},
idleMs: {
type: "integer",
minimum: 0,
maximum: 9007199254740991,
},
},
additionalProperties: false,
},
streaming: {
anyOf: [
{
type: "boolean",
},
{
mode: {
type: "string",
enum: ["off", "partial", "block", "progress"],
},
],
},
streamMode: {
type: "string",
enum: ["partial", "block", "off"],
},
draftChunk: {
type: "object",
properties: {
minChars: {
type: "integer",
exclusiveMinimum: 0,
maximum: 9007199254740991,
chunkMode: {
type: "string",
enum: ["length", "newline"],
},
maxChars: {
type: "integer",
exclusiveMinimum: 0,
maximum: 9007199254740991,
preview: {
type: "object",
properties: {
chunk: {
type: "object",
properties: {
minChars: {
type: "integer",
exclusiveMinimum: 0,
maximum: 9007199254740991,
},
maxChars: {
type: "integer",
exclusiveMinimum: 0,
maximum: 9007199254740991,
},
breakPreference: {
anyOf: [
{
type: "string",
const: "paragraph",
},
{
type: "string",
const: "newline",
},
{
type: "string",
const: "sentence",
},
],
},
},
additionalProperties: false,
},
},
additionalProperties: false,
},
breakPreference: {
anyOf: [
{
type: "string",
const: "paragraph",
block: {
type: "object",
properties: {
enabled: {
type: "boolean",
},
{
type: "string",
const: "newline",
coalesce: {
type: "object",
properties: {
minChars: {
type: "integer",
exclusiveMinimum: 0,
maximum: 9007199254740991,
},
maxChars: {
type: "integer",
exclusiveMinimum: 0,
maximum: 9007199254740991,
},
idleMs: {
type: "integer",
minimum: 0,
maximum: 9007199254740991,
},
},
additionalProperties: false,
},
{
type: "string",
const: "sentence",
},
],
},
additionalProperties: false,
},
},
additionalProperties: false,
@@ -2158,6 +2125,10 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
type: "string",
const: "all",
},
{
type: "string",
const: "batched",
},
],
},
dmPolicy: {
@@ -2167,14 +2138,7 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
allowFrom: {
type: "array",
items: {
anyOf: [
{
type: "string",
},
{
type: "number",
},
],
type: "string",
},
},
defaultTo: {
@@ -2193,14 +2157,7 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
allowFrom: {
type: "array",
items: {
anyOf: [
{
type: "string",
},
{
type: "number",
},
],
type: "string",
},
},
groupEnabled: {
@@ -2209,14 +2166,7 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
groupChannels: {
type: "array",
items: {
anyOf: [
{
type: "string",
},
{
type: "number",
},
],
type: "string",
},
},
},
@@ -2300,27 +2250,13 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
users: {
type: "array",
items: {
anyOf: [
{
type: "string",
},
{
type: "number",
},
],
type: "string",
},
},
roles: {
type: "array",
items: {
anyOf: [
{
type: "string",
},
{
type: "number",
},
],
type: "string",
},
},
channels: {
@@ -2331,9 +2267,6 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
additionalProperties: {
type: "object",
properties: {
allow: {
type: "boolean",
},
requireMention: {
type: "boolean",
},
@@ -2406,27 +2339,13 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
users: {
type: "array",
items: {
anyOf: [
{
type: "string",
},
{
type: "number",
},
],
type: "string",
},
},
roles: {
type: "array",
items: {
anyOf: [
{
type: "string",
},
{
type: "number",
},
],
type: "string",
},
},
systemPrompt: {
@@ -2507,14 +2426,7 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
approvers: {
type: "array",
items: {
anyOf: [
{
type: "string",
},
{
type: "number",
},
],
type: "string",
},
},
agentFilter: {
@@ -4393,9 +4305,6 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
enabled: {
type: "boolean",
},
allow: {
type: "boolean",
},
requireMention: {
type: "boolean",
},
@@ -4636,11 +4545,6 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
},
additionalProperties: false,
},
streamMode: {
default: "replace",
type: "string",
enum: ["replace", "status_final", "append"],
},
mediaMaxMb: {
type: "number",
exclusiveMinimum: 0,
@@ -4659,6 +4563,10 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
type: "string",
const: "all",
},
{
type: "string",
const: "batched",
},
],
},
actions: {
@@ -4775,9 +4683,6 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
enabled: {
type: "boolean",
},
allow: {
type: "boolean",
},
requireMention: {
type: "boolean",
},
@@ -5018,11 +4923,6 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
},
additionalProperties: false,
},
streamMode: {
default: "replace",
type: "string",
enum: ["replace", "status_final", "append"],
},
mediaMaxMb: {
type: "number",
exclusiveMinimum: 0,
@@ -5041,6 +4941,10 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
type: "string",
const: "all",
},
{
type: "string",
const: "batched",
},
],
},
actions: {
@@ -5097,7 +5001,7 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
type: "string",
},
},
required: ["groupPolicy", "streamMode"],
required: ["groupPolicy"],
additionalProperties: false,
},
},
@@ -5105,7 +5009,7 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
type: "string",
},
},
required: ["groupPolicy", "streamMode"],
required: ["groupPolicy"],
additionalProperties: false,
},
},
@@ -7929,6 +7833,22 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
tenantId: {
type: "string",
},
authType: {
type: "string",
enum: ["secret", "federated"],
},
certificatePath: {
type: "string",
},
certificateThumbprint: {
type: "string",
},
useManagedIdentity: {
type: "boolean",
},
managedIdentityClientId: {
type: "string",
},
webhook: {
type: "object",
properties: {
@@ -7968,6 +7888,10 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
type: "string",
enum: ["open", "disabled", "allowlist"],
},
contextVisibility: {
type: "string",
enum: ["all", "allowlist", "allowlist_quote"],
},
textChunkLimit: {
type: "integer",
exclusiveMinimum: 0,
@@ -7977,6 +7901,9 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
type: "string",
enum: ["length", "newline"],
},
typingIndicator: {
type: "boolean",
},
blockStreaming: {
type: "boolean",
},
@@ -8250,6 +8177,33 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
minimum: 0,
maximum: 9007199254740991,
},
delegatedAuth: {
type: "object",
properties: {
enabled: {
type: "boolean",
},
scopes: {
type: "array",
items: {
type: "string",
},
},
},
additionalProperties: false,
},
sso: {
type: "object",
properties: {
enabled: {
type: "boolean",
},
connectionName: {
type: "string",
},
},
additionalProperties: false,
},
},
required: ["dmPolicy", "groupPolicy"],
additionalProperties: false,
@@ -8546,6 +8500,10 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
minimum: 0,
maximum: 9007199254740991,
},
contextVisibility: {
type: "string",
enum: ["all", "allowlist", "allowlist_quote"],
},
dms: {
type: "object",
propertyNames: {
@@ -8883,6 +8841,10 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
minimum: 0,
maximum: 9007199254740991,
},
contextVisibility: {
type: "string",
enum: ["all", "allowlist", "allowlist_quote"],
},
dms: {
type: "object",
propertyNames: {
@@ -14965,6 +14927,10 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
type: "string",
enum: ["open", "disabled", "allowlist"],
},
contextVisibility: {
type: "string",
enum: ["all", "allowlist", "allowlist_quote"],
},
historyLimit: {
type: "integer",
minimum: 0,
@@ -15214,6 +15180,10 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
type: "string",
enum: ["open", "disabled", "allowlist"],
},
contextVisibility: {
type: "string",
enum: ["all", "allowlist", "allowlist_quote"],
},
historyLimit: {
type: "integer",
minimum: 0,

View File

@@ -24,8 +24,105 @@ async function importLoaderWithMissingBun() {
}
}
async function importLoaderWithFailingJitiAndWorkingBun() {
const spawnSync = vi.fn(() => ({
error: undefined,
status: 0,
stdout: JSON.stringify({
schema: {
type: "object",
properties: {
ok: { type: "number" },
},
},
}),
stderr: "",
}));
const createJiti = vi.fn(() => () => {
throw new Error("jiti failed");
});
vi.doMock("node:child_process", () => ({ spawnSync }));
vi.doMock("jiti", () => ({ createJiti }));
try {
const imported = await importFreshModule<
typeof import("../../scripts/load-channel-config-surface.ts")
>(import.meta.url, "../../scripts/load-channel-config-surface.ts?scope=failing-jiti");
return {
loadChannelConfigSurfaceModule: imported.loadChannelConfigSurfaceModule,
spawnSync,
createJiti,
};
} finally {
vi.doUnmock("node:child_process");
vi.doUnmock("jiti");
}
}
describe("loadChannelConfigSurfaceModule", () => {
it("falls back to Jiti when bun is unavailable", async () => {
it("prefers the source-aware loader over bun when both succeed", async () => {
await withTempDir({ prefix: "openclaw-config-surface-" }, async (repoRoot) => {
const packageRoot = path.join(repoRoot, "extensions", "demo");
const modulePath = path.join(packageRoot, "src", "config-schema.js");
fs.mkdirSync(path.join(packageRoot, "src"), { recursive: true });
fs.writeFileSync(
path.join(packageRoot, "package.json"),
JSON.stringify({ name: "@openclaw/demo", type: "module" }, null, 2),
"utf8",
);
fs.writeFileSync(
modulePath,
[
"export const DemoChannelConfigSchema = {",
" schema: {",
" type: 'object',",
" properties: { ok: { type: 'string' } },",
" },",
"};",
"",
].join("\n"),
"utf8",
);
const spawnSync = vi.fn(() => ({
error: undefined,
status: 0,
stdout: JSON.stringify({
schema: {
type: "object",
properties: {
ok: { type: "number" },
},
},
}),
stderr: "",
}));
vi.doMock("node:child_process", () => ({ spawnSync }));
try {
const imported = await importFreshModule<
typeof import("../../scripts/load-channel-config-surface.ts")
>(import.meta.url, "../../scripts/load-channel-config-surface.ts?scope=prefer-jiti");
await expect(
imported.loadChannelConfigSurfaceModule(modulePath, { repoRoot }),
).resolves.toMatchObject({
schema: {
type: "object",
properties: {
ok: { type: "string" },
},
},
});
expect(spawnSync).not.toHaveBeenCalled();
} finally {
vi.doUnmock("node:child_process");
}
});
});
it("does not require bun when the source-aware loader succeeds", async () => {
await withTempDir({ prefix: "openclaw-config-surface-" }, async (repoRoot) => {
const packageRoot = path.join(repoRoot, "extensions", "demo");
const modulePath = path.join(packageRoot, "src", "config-schema.js");
@@ -61,6 +158,50 @@ describe("loadChannelConfigSurfaceModule", () => {
},
},
});
expect(spawnSync).not.toHaveBeenCalled();
});
});
it("falls back to bun when the source-aware loader fails", async () => {
await withTempDir({ prefix: "openclaw-config-surface-" }, async (repoRoot) => {
const packageRoot = path.join(repoRoot, "extensions", "demo");
const modulePath = path.join(packageRoot, "src", "config-schema.js");
fs.mkdirSync(path.join(packageRoot, "src"), { recursive: true });
fs.writeFileSync(
path.join(packageRoot, "package.json"),
JSON.stringify({ name: "@openclaw/demo", type: "module" }, null, 2),
"utf8",
);
fs.writeFileSync(
modulePath,
[
"export const DemoChannelConfigSchema = {",
" schema: {",
" type: 'object',",
" properties: { ok: { type: 'string' } },",
" },",
"};",
"",
].join("\n"),
"utf8",
);
const {
loadChannelConfigSurfaceModule: loadWithFailingJiti,
spawnSync,
createJiti,
} = await importLoaderWithFailingJitiAndWorkingBun();
await expect(loadWithFailingJiti(modulePath, { repoRoot })).resolves.toMatchObject({
schema: {
type: "object",
properties: {
ok: { type: "number" },
},
},
});
expect(createJiti).toHaveBeenCalled();
expect(spawnSync).toHaveBeenCalledWith("bun", expect.any(Array), expect.any(Object));
});
});