Compare commits

...

2 Commits

Author SHA1 Message Date
Peter Steinberger
3c163c71b5 fix: export SECTION_META for config form (#1418) (thanks @MaudeBot) 2026-01-22 04:33:37 +00:00
Maude Bot
277881b52f fix(ui): export SECTION_META from config-form module
Export the SECTION_META constant from config-form.render.ts and
re-export it through config-form.ts so it can be imported by config.ts.

This fixes a runtime error where SECTION_META was being referenced
but not properly exported from its source module.
2026-01-22 04:16:51 +00:00
9 changed files with 179 additions and 138 deletions

View File

@ -24,6 +24,7 @@ Docs: https://docs.clawd.bot
- Cron: cap reminder context history to 10 messages and honor `contextMessages`. (#1103) Thanks @mkbehr. - Cron: cap reminder context history to 10 messages and honor `contextMessages`. (#1103) Thanks @mkbehr.
- Exec approvals: treat main as the default agent + migrate legacy default allowlists. (#1417) Thanks @czekaj. - Exec approvals: treat main as the default agent + migrate legacy default allowlists. (#1417) Thanks @czekaj.
- UI: refresh debug panel on route-driven tab changes. (#1373) Thanks @yazinsai. - UI: refresh debug panel on route-driven tab changes. (#1373) Thanks @yazinsai.
- UI: export config form section metadata for shared usage. (#1418) Thanks @MaudeBot.
## 2026.1.21 ## 2026.1.21

View File

@ -141,83 +141,101 @@ describe("createTelegramBot", () => {
// groupPolicy tests // groupPolicy tests
it("accepts group messages when mentionPatterns match (without @botUsername)", async () => { it("accepts group messages when mentionPatterns match (without @botUsername)", async () => {
onSpy.mockReset(); const originalTz = process.env.TZ;
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>; process.env.TZ = "UTC";
replySpy.mockReset();
loadConfig.mockReturnValue({ try {
identity: { name: "Bert" }, onSpy.mockReset();
messages: { groupChat: { mentionPatterns: ["\\bbert\\b"] } }, const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
channels: { replySpy.mockReset();
telegram: {
groupPolicy: "open", loadConfig.mockReturnValue({
groups: { "*": { requireMention: true } }, identity: { name: "Bert" },
messages: { groupChat: { mentionPatterns: ["\\bbert\\b"] } },
channels: {
telegram: {
groupPolicy: "open",
groups: { "*": { requireMention: true } },
},
}, },
}, });
});
createTelegramBot({ token: "tok" }); createTelegramBot({ token: "tok" });
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>; const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
await handler({ await handler({
message: { message: {
chat: { id: 7, type: "group", title: "Test Group" }, chat: { id: 7, type: "group", title: "Test Group" },
text: "bert: introduce yourself", text: "bert: introduce yourself",
date: 1736380800, date: 1736380800,
message_id: 1, message_id: 1,
from: { id: 9, first_name: "Ada" }, from: { id: 9, first_name: "Ada" },
}, },
me: { username: "clawdbot_bot" }, me: { username: "clawdbot_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }), getFile: async () => ({ download: async () => new Uint8Array() }),
}); });
expect(replySpy).toHaveBeenCalledTimes(1); expect(replySpy).toHaveBeenCalledTimes(1);
const payload = replySpy.mock.calls[0][0]; const payload = replySpy.mock.calls[0][0];
expect(payload.WasMentioned).toBe(true); expect(payload.WasMentioned).toBe(true);
expect(payload.SenderName).toBe("Ada"); expect(payload.SenderName).toBe("Ada");
expect(payload.SenderId).toBe("9"); expect(payload.SenderId).toBe("9");
expect(payload.Body).toMatch(/^\[Telegram Test Group id:7 (\+\d+[smhd] )?2025-01-09T00:00Z\]/); expect(payload.Body).toMatch(
/^\[Telegram Test Group id:7 (\+\d+[smhd] )?2025-01-09 00:00 [^\]]+\]/,
);
} finally {
process.env.TZ = originalTz;
}
}); });
it("keeps group envelope headers stable (sender identity is separate)", async () => { it("keeps group envelope headers stable (sender identity is separate)", async () => {
onSpy.mockReset(); const originalTz = process.env.TZ;
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>; process.env.TZ = "UTC";
replySpy.mockReset();
loadConfig.mockReturnValue({ try {
channels: { onSpy.mockReset();
telegram: { const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
groupPolicy: "open", replySpy.mockReset();
groups: { "*": { requireMention: false } },
loadConfig.mockReturnValue({
channels: {
telegram: {
groupPolicy: "open",
groups: { "*": { requireMention: false } },
},
}, },
}, });
});
createTelegramBot({ token: "tok" }); createTelegramBot({ token: "tok" });
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>; const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
await handler({ await handler({
message: { message: {
chat: { id: 42, type: "group", title: "Ops" }, chat: { id: 42, type: "group", title: "Ops" },
text: "hello", text: "hello",
date: 1736380800, date: 1736380800,
message_id: 2, message_id: 2,
from: { from: {
id: 99, id: 99,
first_name: "Ada", first_name: "Ada",
last_name: "Lovelace", last_name: "Lovelace",
username: "ada", username: "ada",
},
}, },
}, me: { username: "clawdbot_bot" },
me: { username: "clawdbot_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }),
getFile: async () => ({ download: async () => new Uint8Array() }), });
});
expect(replySpy).toHaveBeenCalledTimes(1); expect(replySpy).toHaveBeenCalledTimes(1);
const payload = replySpy.mock.calls[0][0]; const payload = replySpy.mock.calls[0][0];
expect(payload.SenderName).toBe("Ada Lovelace"); expect(payload.SenderName).toBe("Ada Lovelace");
expect(payload.SenderId).toBe("99"); expect(payload.SenderId).toBe("99");
expect(payload.SenderUsername).toBe("ada"); expect(payload.SenderUsername).toBe("ada");
expect(payload.Body).toMatch(/^\[Telegram Ops id:42 (\+\d+[smhd] )?2025-01-09T00:00Z\]/); expect(payload.Body).toMatch(
/^\[Telegram Ops id:42 (\+\d+[smhd] )?2025-01-09 00:00 [^\]]+\]/,
);
} finally {
process.env.TZ = originalTz;
}
}); });
it("reacts to mention-gated group messages when ackReaction is enabled", async () => { it("reacts to mention-gated group messages when ackReaction is enabled", async () => {
onSpy.mockReset(); onSpy.mockReset();

View File

@ -329,7 +329,7 @@ describe("createTelegramBot", () => {
expect(replySpy).toHaveBeenCalledTimes(1); expect(replySpy).toHaveBeenCalledTimes(1);
const payload = replySpy.mock.calls[0][0]; const payload = replySpy.mock.calls[0][0];
expect(payload.Body).toMatch( expect(payload.Body).toMatch(
/^\[Telegram Ada Lovelace \(@ada_bot\) id:1234 (\+\d+[smhd] )?2025-01-09T00:00Z\]/, /^\[Telegram Ada Lovelace \(@ada_bot\) id:1234 (\+\d+[smhd] )?2025-01-09 01:00 [^\]]+\]/,
); );
expect(payload.Body).toContain("hello world"); expect(payload.Body).toContain("hello world");
} finally { } finally {

View File

@ -451,7 +451,7 @@ describe("createTelegramBot", () => {
expect(replySpy).toHaveBeenCalledTimes(1); expect(replySpy).toHaveBeenCalledTimes(1);
const payload = replySpy.mock.calls[0][0]; const payload = replySpy.mock.calls[0][0];
expect(payload.Body).toMatch( expect(payload.Body).toMatch(
/^\[Telegram Ada Lovelace \(@ada_bot\) id:1234 (\+\d+[smhd] )?2025-01-09T00:00Z\]/, /^\[Telegram Ada Lovelace \(@ada_bot\) id:1234 (\+\d+[smhd] )?2025-01-09 01:00 [^\]]+\]/,
); );
expect(payload.Body).toContain("hello world"); expect(payload.Body).toContain("hello world");
} finally { } finally {
@ -551,86 +551,104 @@ describe("createTelegramBot", () => {
}); });
it("accepts group messages when mentionPatterns match (without @botUsername)", async () => { it("accepts group messages when mentionPatterns match (without @botUsername)", async () => {
onSpy.mockReset(); const originalTz = process.env.TZ;
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>; process.env.TZ = "UTC";
replySpy.mockReset();
loadConfig.mockReturnValue({ try {
identity: { name: "Bert" }, onSpy.mockReset();
messages: { groupChat: { mentionPatterns: ["\\bbert\\b"] } }, const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
channels: { replySpy.mockReset();
telegram: {
groupPolicy: "open", loadConfig.mockReturnValue({
groups: { "*": { requireMention: true } }, identity: { name: "Bert" },
messages: { groupChat: { mentionPatterns: ["\\bbert\\b"] } },
channels: {
telegram: {
groupPolicy: "open",
groups: { "*": { requireMention: true } },
},
}, },
}, });
});
createTelegramBot({ token: "tok" }); createTelegramBot({ token: "tok" });
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>; const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
await handler({ await handler({
message: { message: {
chat: { id: 7, type: "group", title: "Test Group" }, chat: { id: 7, type: "group", title: "Test Group" },
text: "bert: introduce yourself", text: "bert: introduce yourself",
date: 1736380800, date: 1736380800,
message_id: 1, message_id: 1,
from: { id: 9, first_name: "Ada" }, from: { id: 9, first_name: "Ada" },
}, },
me: { username: "clawdbot_bot" }, me: { username: "clawdbot_bot" },
getFile: async () => ({ download: async () => new Uint8Array() }), getFile: async () => ({ download: async () => new Uint8Array() }),
}); });
expect(replySpy).toHaveBeenCalledTimes(1); expect(replySpy).toHaveBeenCalledTimes(1);
const payload = replySpy.mock.calls[0][0]; const payload = replySpy.mock.calls[0][0];
expectInboundContextContract(payload); expectInboundContextContract(payload);
expect(payload.WasMentioned).toBe(true); expect(payload.WasMentioned).toBe(true);
expect(payload.Body).toMatch(/^\[Telegram Test Group id:7 (\+\d+[smhd] )?2025-01-09T00:00Z\]/); expect(payload.Body).toMatch(
expect(payload.SenderName).toBe("Ada"); /^\[Telegram Test Group id:7 (\+\d+[smhd] )?2025-01-09 00:00 [^\]]+\]/,
expect(payload.SenderId).toBe("9"); );
expect(payload.SenderName).toBe("Ada");
expect(payload.SenderId).toBe("9");
} finally {
process.env.TZ = originalTz;
}
}); });
it("includes sender identity in group envelope headers", async () => { it("includes sender identity in group envelope headers", async () => {
onSpy.mockReset(); const originalTz = process.env.TZ;
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>; process.env.TZ = "UTC";
replySpy.mockReset();
loadConfig.mockReturnValue({ try {
channels: { onSpy.mockReset();
telegram: { const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
groupPolicy: "open", replySpy.mockReset();
groups: { "*": { requireMention: false } },
loadConfig.mockReturnValue({
channels: {
telegram: {
groupPolicy: "open",
groups: { "*": { requireMention: false } },
},
}, },
}, });
});
createTelegramBot({ token: "tok" }); createTelegramBot({ token: "tok" });
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>; const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
await handler({ await handler({
message: { message: {
chat: { id: 42, type: "group", title: "Ops" }, chat: { id: 42, type: "group", title: "Ops" },
text: "hello", text: "hello",
date: 1736380800, date: 1736380800,
message_id: 2, message_id: 2,
from: { from: {
id: 99, id: 99,
first_name: "Ada", first_name: "Ada",
last_name: "Lovelace", last_name: "Lovelace",
username: "ada", username: "ada",
},
}, },
}, me: { username: "clawdbot_bot" },
me: { username: "clawdbot_bot" }, getFile: async () => ({ download: async () => new Uint8Array() }),
getFile: async () => ({ download: async () => new Uint8Array() }), });
});
expect(replySpy).toHaveBeenCalledTimes(1); expect(replySpy).toHaveBeenCalledTimes(1);
const payload = replySpy.mock.calls[0][0]; const payload = replySpy.mock.calls[0][0];
expectInboundContextContract(payload); expectInboundContextContract(payload);
expect(payload.Body).toMatch(/^\[Telegram Ops id:42 (\+\d+[smhd] )?2025-01-09T00:00Z\]/); expect(payload.Body).toMatch(
expect(payload.SenderName).toBe("Ada Lovelace"); /^\[Telegram Ops id:42 (\+\d+[smhd] )?2025-01-09 00:00 [^\]]+\]/,
expect(payload.SenderId).toBe("99"); );
expect(payload.SenderUsername).toBe("ada"); expect(payload.SenderName).toBe("Ada Lovelace");
expect(payload.SenderId).toBe("99");
expect(payload.SenderUsername).toBe("ada");
} finally {
process.env.TZ = originalTz;
}
}); });
it("reacts to mention-gated group messages when ackReaction is enabled", async () => { it("reacts to mention-gated group messages when ackReaction is enabled", async () => {

View File

@ -329,11 +329,11 @@ describe("web auto-reply", () => {
const firstArgs = resolver.mock.calls[0][0]; const firstArgs = resolver.mock.calls[0][0];
const secondArgs = resolver.mock.calls[1][0]; const secondArgs = resolver.mock.calls[1][0];
expect(firstArgs.Body).toMatch( expect(firstArgs.Body).toMatch(
/\[WhatsApp \+1 (\+\d+[smhd] )?2025-01-01T00:00Z\] \[clawdbot\] first/, /\[WhatsApp \+1 (\+\d+[smhd] )?2025-01-01 01:00 [^\]]+\] \[clawdbot\] first/,
); );
expect(firstArgs.Body).not.toContain("second"); expect(firstArgs.Body).not.toContain("second");
expect(secondArgs.Body).toMatch( expect(secondArgs.Body).toMatch(
/\[WhatsApp \+1 (\+\d+[smhd] )?2025-01-01T01:00Z\] \[clawdbot\] second/, /\[WhatsApp \+1 (\+\d+[smhd] )?2025-01-01 02:00 [^\]]+\] \[clawdbot\] second/,
); );
expect(secondArgs.Body).not.toContain("first"); expect(secondArgs.Body).not.toContain("first");

View File

@ -1,7 +1,7 @@
import { render } from "lit"; import { render } from "lit";
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import { analyzeConfigSchema, renderConfigForm } from "./views/config-form"; import { analyzeConfigSchema, renderConfigForm, SECTION_META } from "./views/config-form";
const rootSchema = { const rootSchema = {
type: "object", type: "object",
@ -40,6 +40,10 @@ const rootSchema = {
}; };
describe("config form renderer", () => { describe("config form renderer", () => {
it("exposes section metadata", () => {
expect(SECTION_META.env.label).toBe("Environment Variables");
});
it("renders inputs and patches values", () => { it("renders inputs and patches values", () => {
const onPatch = vi.fn(); const onPatch = vi.fn();
const container = document.createElement("div"); const container = document.createElement("div");

View File

@ -54,7 +54,7 @@ const sectionIcons = {
}; };
// Section metadata // Section metadata
const SECTION_META: Record<string, { label: string; description: string }> = { export const SECTION_META: Record<string, { label: string; description: string }> = {
env: { label: "Environment Variables", description: "Environment variables passed to the gateway process" }, env: { label: "Environment Variables", description: "Environment variables passed to the gateway process" },
update: { label: "Updates", description: "Auto-update settings and release channel" }, update: { label: "Updates", description: "Auto-update settings and release channel" },
agents: { label: "Agents", description: "Agent configurations, models, and identities" }, agents: { label: "Agents", description: "Agent configurations, models, and identities" },

View File

@ -1,4 +1,4 @@
export { renderConfigForm, type ConfigFormProps } from "./config-form.render"; export { renderConfigForm, type ConfigFormProps, SECTION_META } from "./config-form.render";
export { export {
analyzeConfigSchema, analyzeConfigSchema,
type ConfigSchemaAnalysis, type ConfigSchemaAnalysis,

View File

@ -1,6 +1,6 @@
import { html, nothing } from "lit"; import { html, nothing } from "lit";
import type { ConfigUiHints } from "../types"; import type { ConfigUiHints } from "../types";
import { analyzeConfigSchema, renderConfigForm } from "./config-form"; import { analyzeConfigSchema, renderConfigForm, SECTION_META } from "./config-form";
import { import {
hintForPath, hintForPath,
humanize, humanize,