test: collapse channel setup test suites

This commit is contained in:
Peter Steinberger
2026-03-25 04:50:35 +00:00
parent e5d0d810e1
commit 6e050808ef
20 changed files with 842 additions and 886 deletions

View File

@@ -1,63 +0,0 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import {
expectStopPendingUntilAbort,
startAccountAndTrackLifecycle,
waitForStartedMocks,
} from "../../../test/helpers/extensions/start-account-lifecycle.js";
import type { ResolvedIrcAccount } from "./accounts.js";
const hoisted = vi.hoisted(() => ({
monitorIrcProvider: vi.fn(),
}));
vi.mock("./monitor.js", async () => {
const actual = await vi.importActual<typeof import("./monitor.js")>("./monitor.js");
return {
...actual,
monitorIrcProvider: hoisted.monitorIrcProvider,
};
});
import { ircPlugin } from "./channel.js";
function buildAccount(): ResolvedIrcAccount {
return {
accountId: "default",
enabled: true,
name: "default",
configured: true,
host: "irc.example.com",
port: 6697,
tls: true,
nick: "openclaw",
username: "openclaw",
realname: "OpenClaw",
password: "",
passwordSource: "none",
config: {} as ResolvedIrcAccount["config"],
};
}
describe("ircPlugin gateway.startAccount", () => {
afterEach(() => {
vi.clearAllMocks();
});
it("keeps startAccount pending until abort, then stops the monitor", async () => {
const stop = vi.fn();
hoisted.monitorIrcProvider.mockResolvedValue({ stop });
const { abort, task, isSettled } = startAccountAndTrackLifecycle({
startAccount: ircPlugin.gateway!.startAccount!,
account: buildAccount(),
});
await expectStopPendingUntilAbort({
waitForStarted: waitForStartedMocks(hoisted.monitorIrcProvider),
isSettled,
abort,
task,
stop,
});
});
});

View File

@@ -1,185 +0,0 @@
import { describe, expect, it } from "vitest";
import {
ircSetupAdapter,
parsePort,
setIrcAllowFrom,
setIrcDmPolicy,
setIrcGroupAccess,
setIrcNickServ,
updateIrcAccountConfig,
} from "./setup-core.js";
import type { CoreConfig } from "./types.js";
describe("irc setup core", () => {
it("parses valid ports and falls back for invalid values", () => {
expect(parsePort("6697", 6667)).toBe(6697);
expect(parsePort(" 7000 ", 6667)).toBe(7000);
expect(parsePort("", 6667)).toBe(6667);
expect(parsePort("70000", 6667)).toBe(6667);
expect(parsePort("abc", 6667)).toBe(6667);
});
it("updates top-level dm policy and allowlist", () => {
const cfg: CoreConfig = { channels: { irc: {} } };
expect(setIrcDmPolicy(cfg, "open")).toMatchObject({
channels: {
irc: {
dmPolicy: "open",
},
},
});
expect(setIrcAllowFrom(cfg, ["alice", "bob"])).toMatchObject({
channels: {
irc: {
allowFrom: ["alice", "bob"],
},
},
});
});
it("stores nickserv and account config patches on the scoped account", () => {
const cfg: CoreConfig = { channels: { irc: {} } };
expect(
setIrcNickServ(cfg, "work", {
enabled: true,
service: "NickServ",
}),
).toMatchObject({
channels: {
irc: {
accounts: {
work: {
nickserv: {
enabled: true,
service: "NickServ",
},
},
},
},
},
});
expect(
updateIrcAccountConfig(cfg, "work", {
host: "irc.libera.chat",
nick: "openclaw-work",
}),
).toMatchObject({
channels: {
irc: {
accounts: {
work: {
host: "irc.libera.chat",
nick: "openclaw-work",
},
},
},
},
});
});
it("normalizes allowlist groups and handles non-allowlist policies", () => {
const cfg: CoreConfig = { channels: { irc: {} } };
expect(
setIrcGroupAccess(
cfg,
"default",
"allowlist",
["openclaw", "#ops", "openclaw", "*"],
(raw) => {
const trimmed = raw.trim();
if (!trimmed) {
return null;
}
if (trimmed === "*") {
return "*";
}
return trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
},
),
).toMatchObject({
channels: {
irc: {
enabled: true,
groupPolicy: "allowlist",
groups: {
"#openclaw": {},
"#ops": {},
"*": {},
},
},
},
});
expect(setIrcGroupAccess(cfg, "default", "disabled", [], () => null)).toMatchObject({
channels: {
irc: {
enabled: true,
groupPolicy: "disabled",
},
},
});
});
it("validates required input and applies normalized account config", () => {
const validateInput = ircSetupAdapter.validateInput;
const applyAccountConfig = ircSetupAdapter.applyAccountConfig;
expect(validateInput).toBeTypeOf("function");
expect(applyAccountConfig).toBeTypeOf("function");
expect(
validateInput!({
input: { host: "", nick: "openclaw" },
} as never),
).toBe("IRC requires host.");
expect(
validateInput!({
input: { host: "irc.libera.chat", nick: "" },
} as never),
).toBe("IRC requires nick.");
expect(
validateInput!({
input: { host: "irc.libera.chat", nick: "openclaw" },
} as never),
).toBeNull();
expect(
applyAccountConfig!({
cfg: { channels: { irc: {} } },
accountId: "default",
input: {
name: "Default",
host: " irc.libera.chat ",
port: "7000",
tls: true,
nick: " openclaw ",
username: " claw ",
realname: " OpenClaw Bot ",
password: " secret ",
channels: ["#openclaw"],
},
} as never),
).toEqual({
channels: {
irc: {
enabled: true,
name: "Default",
host: "irc.libera.chat",
port: 7000,
tls: true,
nick: "openclaw",
username: "claw",
realname: "OpenClaw Bot",
password: "secret",
channels: ["#openclaw"],
},
},
});
});
});

View File

@@ -1,108 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import {
createPluginSetupWizardAdapter,
createTestWizardPrompter,
promptSetupWizardAllowFrom,
runSetupWizardConfigure,
type WizardPrompter,
} from "../../../test/helpers/extensions/setup-wizard.js";
import { ircPlugin } from "./channel.js";
import type { CoreConfig } from "./types.js";
const ircConfigureAdapter = createPluginSetupWizardAdapter(ircPlugin);
describe("irc setup wizard", () => {
it("configures host and nick via setup prompts", async () => {
const prompter = createTestWizardPrompter({
text: vi.fn(async ({ message }: { message: string }) => {
if (message === "IRC server host") {
return "irc.libera.chat";
}
if (message === "IRC server port") {
return "6697";
}
if (message === "IRC nick") {
return "openclaw-bot";
}
if (message === "IRC username") {
return "openclaw";
}
if (message === "IRC real name") {
return "OpenClaw Bot";
}
if (message.startsWith("Auto-join IRC channels")) {
return "#openclaw, #ops";
}
if (message.startsWith("IRC channels allowlist")) {
return "#openclaw, #ops";
}
throw new Error(`Unexpected prompt: ${message}`);
}) as WizardPrompter["text"],
confirm: vi.fn(async ({ message }: { message: string }) => {
if (message === "Use TLS for IRC?") {
return true;
}
if (message === "Configure IRC channels access?") {
return true;
}
return false;
}),
});
const result = await runSetupWizardConfigure({
configure: ircConfigureAdapter.configure,
cfg: {} as CoreConfig,
prompter,
options: {},
});
expect(result.accountId).toBe("default");
expect(result.cfg.channels?.irc?.enabled).toBe(true);
expect(result.cfg.channels?.irc?.host).toBe("irc.libera.chat");
expect(result.cfg.channels?.irc?.nick).toBe("openclaw-bot");
expect(result.cfg.channels?.irc?.tls).toBe(true);
expect(result.cfg.channels?.irc?.channels).toEqual(["#openclaw", "#ops"]);
expect(result.cfg.channels?.irc?.groupPolicy).toBe("allowlist");
expect(Object.keys(result.cfg.channels?.irc?.groups ?? {})).toEqual(["#openclaw", "#ops"]);
});
it("writes DM allowFrom to top-level config for non-default account prompts", async () => {
const prompter = createTestWizardPrompter({
text: vi.fn(async ({ message }: { message: string }) => {
if (message === "IRC allowFrom (nick or nick!user@host)") {
return "Alice, Bob!ident@example.org";
}
throw new Error(`Unexpected prompt: ${message}`);
}) as WizardPrompter["text"],
confirm: vi.fn(async () => false),
});
const promptAllowFrom = ircConfigureAdapter.dmPolicy?.promptAllowFrom;
if (!promptAllowFrom) {
throw new Error("promptAllowFrom unavailable");
}
const cfg: CoreConfig = {
channels: {
irc: {
accounts: {
work: {
host: "irc.libera.chat",
nick: "openclaw-work",
},
},
},
},
};
const updated = (await promptSetupWizardAllowFrom({
promptAllowFrom,
cfg,
prompter,
accountId: "work",
})) as CoreConfig;
expect(updated.channels?.irc?.allowFrom).toEqual(["alice", "bob!ident@example.org"]);
expect(updated.channels?.irc?.accounts?.work?.allowFrom).toBeUndefined();
});
});

View File

@@ -0,0 +1,347 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import {
createPluginSetupWizardAdapter,
createTestWizardPrompter,
promptSetupWizardAllowFrom,
runSetupWizardConfigure,
type WizardPrompter,
} from "../../../test/helpers/extensions/setup-wizard.js";
import {
expectStopPendingUntilAbort,
startAccountAndTrackLifecycle,
waitForStartedMocks,
} from "../../../test/helpers/extensions/start-account-lifecycle.js";
import type { ResolvedIrcAccount } from "./accounts.js";
import { ircPlugin } from "./channel.js";
import {
ircSetupAdapter,
parsePort,
setIrcAllowFrom,
setIrcDmPolicy,
setIrcGroupAccess,
setIrcNickServ,
updateIrcAccountConfig,
} from "./setup-core.js";
import type { CoreConfig } from "./types.js";
const hoisted = vi.hoisted(() => ({
monitorIrcProvider: vi.fn(),
}));
vi.mock("./monitor.js", async () => {
const actual = await vi.importActual<typeof import("./monitor.js")>("./monitor.js");
return {
...actual,
monitorIrcProvider: hoisted.monitorIrcProvider,
};
});
const ircConfigureAdapter = createPluginSetupWizardAdapter(ircPlugin);
function buildAccount(): ResolvedIrcAccount {
return {
accountId: "default",
enabled: true,
name: "default",
configured: true,
host: "irc.example.com",
port: 6697,
tls: true,
nick: "openclaw",
username: "openclaw",
realname: "OpenClaw",
password: "",
passwordSource: "none",
config: {} as ResolvedIrcAccount["config"],
};
}
describe("irc setup", () => {
afterEach(() => {
vi.clearAllMocks();
});
it("parses valid ports and falls back for invalid values", () => {
expect(parsePort("6697", 6667)).toBe(6697);
expect(parsePort(" 7000 ", 6667)).toBe(7000);
expect(parsePort("", 6667)).toBe(6667);
expect(parsePort("70000", 6667)).toBe(6667);
expect(parsePort("abc", 6667)).toBe(6667);
});
it("updates top-level dm policy and allowlist", () => {
const cfg: CoreConfig = { channels: { irc: {} } };
expect(setIrcDmPolicy(cfg, "open")).toMatchObject({
channels: {
irc: {
dmPolicy: "open",
},
},
});
expect(setIrcAllowFrom(cfg, ["alice", "bob"])).toMatchObject({
channels: {
irc: {
allowFrom: ["alice", "bob"],
},
},
});
});
it("stores nickserv and account config patches on the scoped account", () => {
const cfg: CoreConfig = { channels: { irc: {} } };
expect(
setIrcNickServ(cfg, "work", {
enabled: true,
service: "NickServ",
}),
).toMatchObject({
channels: {
irc: {
accounts: {
work: {
nickserv: {
enabled: true,
service: "NickServ",
},
},
},
},
},
});
expect(
updateIrcAccountConfig(cfg, "work", {
host: "irc.libera.chat",
nick: "openclaw-work",
}),
).toMatchObject({
channels: {
irc: {
accounts: {
work: {
host: "irc.libera.chat",
nick: "openclaw-work",
},
},
},
},
});
});
it("normalizes allowlist groups and handles non-allowlist policies", () => {
const cfg: CoreConfig = { channels: { irc: {} } };
expect(
setIrcGroupAccess(
cfg,
"default",
"allowlist",
["openclaw", "#ops", "openclaw", "*"],
(raw) => {
const trimmed = raw.trim();
if (!trimmed) {
return null;
}
if (trimmed === "*") {
return "*";
}
return trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
},
),
).toMatchObject({
channels: {
irc: {
enabled: true,
groupPolicy: "allowlist",
groups: {
"#openclaw": {},
"#ops": {},
"*": {},
},
},
},
});
expect(setIrcGroupAccess(cfg, "default", "disabled", [], () => null)).toMatchObject({
channels: {
irc: {
enabled: true,
groupPolicy: "disabled",
},
},
});
});
it("validates required input and applies normalized account config", () => {
const validateInput = ircSetupAdapter.validateInput;
const applyAccountConfig = ircSetupAdapter.applyAccountConfig;
expect(validateInput).toBeTypeOf("function");
expect(applyAccountConfig).toBeTypeOf("function");
expect(
validateInput!({
input: { host: "", nick: "openclaw" },
} as never),
).toBe("IRC requires host.");
expect(
validateInput!({
input: { host: "irc.libera.chat", nick: "" },
} as never),
).toBe("IRC requires nick.");
expect(
validateInput!({
input: { host: "irc.libera.chat", nick: "openclaw" },
} as never),
).toBeNull();
expect(
applyAccountConfig!({
cfg: { channels: { irc: {} } },
accountId: "default",
input: {
name: "Default",
host: " irc.libera.chat ",
port: "7000",
tls: true,
nick: " openclaw ",
username: " claw ",
realname: " OpenClaw Bot ",
password: " secret ",
channels: ["#openclaw"],
},
} as never),
).toEqual({
channels: {
irc: {
enabled: true,
name: "Default",
host: "irc.libera.chat",
port: 7000,
tls: true,
nick: "openclaw",
username: "claw",
realname: "OpenClaw Bot",
password: "secret",
channels: ["#openclaw"],
},
},
});
});
it("configures host and nick via setup prompts", async () => {
const prompter = createTestWizardPrompter({
text: vi.fn(async ({ message }: { message: string }) => {
if (message === "IRC server host") {
return "irc.libera.chat";
}
if (message === "IRC server port") {
return "6697";
}
if (message === "IRC nick") {
return "openclaw-bot";
}
if (message === "IRC username") {
return "openclaw";
}
if (message === "IRC real name") {
return "OpenClaw Bot";
}
if (message.startsWith("Auto-join IRC channels")) {
return "#openclaw, #ops";
}
if (message.startsWith("IRC channels allowlist")) {
return "#openclaw, #ops";
}
throw new Error(`Unexpected prompt: ${message}`);
}) as WizardPrompter["text"],
confirm: vi.fn(async ({ message }: { message: string }) => {
if (message === "Use TLS for IRC?") {
return true;
}
if (message === "Configure IRC channels access?") {
return true;
}
return false;
}),
});
const result = await runSetupWizardConfigure({
configure: ircConfigureAdapter.configure,
cfg: {} as CoreConfig,
prompter,
options: {},
});
expect(result.accountId).toBe("default");
expect(result.cfg.channels?.irc?.enabled).toBe(true);
expect(result.cfg.channels?.irc?.host).toBe("irc.libera.chat");
expect(result.cfg.channels?.irc?.nick).toBe("openclaw-bot");
expect(result.cfg.channels?.irc?.tls).toBe(true);
expect(result.cfg.channels?.irc?.channels).toEqual(["#openclaw", "#ops"]);
expect(result.cfg.channels?.irc?.groupPolicy).toBe("allowlist");
expect(Object.keys(result.cfg.channels?.irc?.groups ?? {})).toEqual(["#openclaw", "#ops"]);
});
it("writes DM allowFrom to top-level config for non-default account prompts", async () => {
const prompter = createTestWizardPrompter({
text: vi.fn(async ({ message }: { message: string }) => {
if (message === "IRC allowFrom (nick or nick!user@host)") {
return "Alice, Bob!ident@example.org";
}
throw new Error(`Unexpected prompt: ${message}`);
}) as WizardPrompter["text"],
confirm: vi.fn(async () => false),
});
const promptAllowFrom = ircConfigureAdapter.dmPolicy?.promptAllowFrom;
if (!promptAllowFrom) {
throw new Error("promptAllowFrom unavailable");
}
const cfg: CoreConfig = {
channels: {
irc: {
accounts: {
work: {
host: "irc.libera.chat",
nick: "openclaw-work",
},
},
},
},
};
const updated = (await promptSetupWizardAllowFrom({
promptAllowFrom,
cfg,
prompter,
accountId: "work",
})) as CoreConfig;
expect(updated.channels?.irc?.allowFrom).toEqual(["alice", "bob!ident@example.org"]);
expect(updated.channels?.irc?.accounts?.work?.allowFrom).toBeUndefined();
});
it("keeps startAccount pending until abort, then stops the monitor", async () => {
const stop = vi.fn();
hoisted.monitorIrcProvider.mockResolvedValue({ stop });
const { abort, task, isSettled } = startAccountAndTrackLifecycle({
startAccount: ircPlugin.gateway!.startAccount!,
account: buildAccount(),
});
await expectStopPendingUntilAbort({
waitForStarted: waitForStartedMocks(hoisted.monitorIrcProvider),
isSettled,
abort,
task,
stop,
});
});
});

View File

@@ -1,85 +0,0 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { createStartAccountContext } from "../../../test/helpers/extensions/start-account-context.js";
import {
expectStopPendingUntilAbort,
startAccountAndTrackLifecycle,
waitForStartedMocks,
} from "../../../test/helpers/extensions/start-account-lifecycle.js";
import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
const hoisted = vi.hoisted(() => ({
monitorNextcloudTalkProvider: vi.fn(),
}));
vi.mock("./monitor.js", async () => {
const actual = await vi.importActual<typeof import("./monitor.js")>("./monitor.js");
return {
...actual,
monitorNextcloudTalkProvider: hoisted.monitorNextcloudTalkProvider,
};
});
import { nextcloudTalkPlugin } from "./channel.js";
function buildAccount(): ResolvedNextcloudTalkAccount {
return {
accountId: "default",
enabled: true,
baseUrl: "https://nextcloud.example.com",
secret: "secret", // pragma: allowlist secret
secretSource: "config", // pragma: allowlist secret
config: {
baseUrl: "https://nextcloud.example.com",
botSecret: "secret", // pragma: allowlist secret
webhookPath: "/nextcloud-talk-webhook",
webhookPort: 8788,
},
};
}
function mockStartedMonitor() {
const stop = vi.fn();
hoisted.monitorNextcloudTalkProvider.mockResolvedValue({ stop });
return stop;
}
function startNextcloudAccount(abortSignal?: AbortSignal) {
return nextcloudTalkPlugin.gateway!.startAccount!(
createStartAccountContext({
account: buildAccount(),
abortSignal,
}),
);
}
describe("nextcloudTalkPlugin gateway.startAccount", () => {
afterEach(() => {
vi.clearAllMocks();
});
it("keeps startAccount pending until abort, then stops the monitor", async () => {
const stop = mockStartedMonitor();
const { abort, task, isSettled } = startAccountAndTrackLifecycle({
startAccount: nextcloudTalkPlugin.gateway!.startAccount!,
account: buildAccount(),
});
await expectStopPendingUntilAbort({
waitForStarted: waitForStartedMocks(hoisted.monitorNextcloudTalkProvider),
isSettled,
abort,
task,
stop,
});
});
it("stops immediately when startAccount receives an already-aborted signal", async () => {
const stop = mockStartedMonitor();
const abort = new AbortController();
abort.abort();
await startNextcloudAccount(abort.signal);
expect(hoisted.monitorNextcloudTalkProvider).toHaveBeenCalledOnce();
expect(stop).toHaveBeenCalledOnce();
});
});

View File

@@ -1,36 +0,0 @@
import { describe, expect, it } from "vitest";
import { NextcloudTalkConfigSchema } from "./config-schema.js";
describe("NextcloudTalkConfigSchema SecretInput", () => {
it("accepts SecretRef botSecret and apiPassword at top-level", () => {
const result = NextcloudTalkConfigSchema.safeParse({
baseUrl: "https://cloud.example.com",
botSecret: { source: "env", provider: "default", id: "NEXTCLOUD_TALK_BOT_SECRET" },
apiUser: "bot",
apiPassword: { source: "env", provider: "default", id: "NEXTCLOUD_TALK_API_PASSWORD" },
});
expect(result.success).toBe(true);
});
it("accepts SecretRef botSecret and apiPassword on account", () => {
const result = NextcloudTalkConfigSchema.safeParse({
accounts: {
main: {
baseUrl: "https://cloud.example.com",
botSecret: {
source: "env",
provider: "default",
id: "NEXTCLOUD_TALK_MAIN_BOT_SECRET",
},
apiUser: "bot",
apiPassword: {
source: "env",
provider: "default",
id: "NEXTCLOUD_TALK_MAIN_API_PASSWORD",
},
},
},
});
expect(result.success).toBe(true);
});
});

View File

@@ -0,0 +1,99 @@
import { describe, expect, it } from "vitest";
import { NextcloudTalkConfigSchema } from "./config-schema.js";
import {
escapeNextcloudTalkMarkdown,
formatNextcloudTalkCodeBlock,
formatNextcloudTalkInlineCode,
formatNextcloudTalkMention,
markdownToNextcloudTalk,
stripNextcloudTalkFormatting,
truncateNextcloudTalkText,
} from "./format.js";
import { resolveNextcloudTalkOutboundSessionRoute } from "./session-route.js";
describe("nextcloud talk core", () => {
it("accepts SecretRef botSecret and apiPassword at top-level", () => {
const result = NextcloudTalkConfigSchema.safeParse({
baseUrl: "https://cloud.example.com",
botSecret: { source: "env", provider: "default", id: "NEXTCLOUD_TALK_BOT_SECRET" },
apiUser: "bot",
apiPassword: { source: "env", provider: "default", id: "NEXTCLOUD_TALK_API_PASSWORD" },
});
expect(result.success).toBe(true);
});
it("accepts SecretRef botSecret and apiPassword on account", () => {
const result = NextcloudTalkConfigSchema.safeParse({
accounts: {
main: {
baseUrl: "https://cloud.example.com",
botSecret: {
source: "env",
provider: "default",
id: "NEXTCLOUD_TALK_MAIN_BOT_SECRET",
},
apiUser: "bot",
apiPassword: {
source: "env",
provider: "default",
id: "NEXTCLOUD_TALK_MAIN_API_PASSWORD",
},
},
},
});
expect(result.success).toBe(true);
});
it("keeps markdown mostly intact while trimming outer whitespace", () => {
expect(markdownToNextcloudTalk(" **hello** ")).toBe("**hello**");
});
it("escapes markdown-sensitive characters", () => {
expect(escapeNextcloudTalkMarkdown("*hello* [x](y)")).toBe("\\*hello\\* \\[x\\]\\(y\\)");
});
it("formats mentions and code consistently", () => {
expect(formatNextcloudTalkMention("@alice")).toBe("@alice");
expect(formatNextcloudTalkMention("bob")).toBe("@bob");
expect(formatNextcloudTalkCodeBlock("const x = 1;", "ts")).toBe("```ts\nconst x = 1;\n```");
expect(formatNextcloudTalkInlineCode("x")).toBe("`x`");
expect(formatNextcloudTalkInlineCode("x ` y")).toBe("`` x ` y ``");
});
it("strips markdown formatting and truncates on word boundaries", () => {
expect(stripNextcloudTalkFormatting("**bold** [link](https://example.com) `code`")).toBe(
"bold link",
);
expect(truncateNextcloudTalkText("alpha beta gamma delta", 14)).toBe("alpha beta...");
expect(truncateNextcloudTalkText("short", 14)).toBe("short");
});
it("builds an outbound session route for normalized room targets", () => {
const route = resolveNextcloudTalkOutboundSessionRoute({
cfg: {},
agentId: "main",
accountId: "acct-1",
target: "nextcloud-talk:room-123",
});
expect(route).toMatchObject({
peer: {
kind: "group",
id: "room-123",
},
from: "nextcloud-talk:room:room-123",
to: "nextcloud-talk:room-123",
});
});
it("returns null when the target cannot be normalized to a room id", () => {
expect(
resolveNextcloudTalkOutboundSessionRoute({
cfg: {},
agentId: "main",
accountId: "acct-1",
target: "",
}),
).toBeNull();
});
});

View File

@@ -1,36 +0,0 @@
import { describe, expect, it } from "vitest";
import {
escapeNextcloudTalkMarkdown,
formatNextcloudTalkCodeBlock,
formatNextcloudTalkInlineCode,
formatNextcloudTalkMention,
markdownToNextcloudTalk,
stripNextcloudTalkFormatting,
truncateNextcloudTalkText,
} from "./format.js";
describe("nextcloud talk format helpers", () => {
it("keeps markdown mostly intact while trimming outer whitespace", () => {
expect(markdownToNextcloudTalk(" **hello** ")).toBe("**hello**");
});
it("escapes markdown-sensitive characters", () => {
expect(escapeNextcloudTalkMarkdown("*hello* [x](y)")).toBe("\\*hello\\* \\[x\\]\\(y\\)");
});
it("formats mentions and code consistently", () => {
expect(formatNextcloudTalkMention("@alice")).toBe("@alice");
expect(formatNextcloudTalkMention("bob")).toBe("@bob");
expect(formatNextcloudTalkCodeBlock("const x = 1;", "ts")).toBe("```ts\nconst x = 1;\n```");
expect(formatNextcloudTalkInlineCode("x")).toBe("`x`");
expect(formatNextcloudTalkInlineCode("x ` y")).toBe("`` x ` y ``");
});
it("strips markdown formatting and truncates on word boundaries", () => {
expect(stripNextcloudTalkFormatting("**bold** [link](https://example.com) `code`")).toBe(
"bold link",
);
expect(truncateNextcloudTalkText("alpha beta gamma delta", 14)).toBe("alpha beta...");
expect(truncateNextcloudTalkText("short", 14)).toBe("short");
});
});

View File

@@ -1,33 +0,0 @@
import { describe, expect, it } from "vitest";
import { resolveNextcloudTalkOutboundSessionRoute } from "./session-route.js";
describe("nextcloud talk session route", () => {
it("builds an outbound session route for normalized room targets", () => {
const route = resolveNextcloudTalkOutboundSessionRoute({
cfg: {},
agentId: "main",
accountId: "acct-1",
target: "nextcloud-talk:room-123",
});
expect(route).toMatchObject({
peer: {
kind: "group",
id: "room-123",
},
from: "nextcloud-talk:room:room-123",
to: "nextcloud-talk:room-123",
});
});
it("returns null when the target cannot be normalized to a room id", () => {
expect(
resolveNextcloudTalkOutboundSessionRoute({
cfg: {},
agentId: "main",
accountId: "acct-1",
target: "",
}),
).toBeNull();
});
});

View File

@@ -1,53 +0,0 @@
import { describe, expect, it } from "vitest";
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
import { nextcloudTalkSetupAdapter, nextcloudTalkSetupWizard } from "./setup-surface.js";
describe("nextcloudTalk setup surface", () => {
it("clears stored bot secret fields when switching the default account to env", () => {
type ApplyAccountConfigContext = Parameters<
typeof nextcloudTalkSetupAdapter.applyAccountConfig
>[0];
const next = nextcloudTalkSetupAdapter.applyAccountConfig({
cfg: {
channels: {
"nextcloud-talk": {
enabled: true,
baseUrl: "https://cloud.old.example",
botSecret: "stored-secret",
botSecretFile: "/tmp/secret.txt",
},
},
},
accountId: DEFAULT_ACCOUNT_ID,
input: {
baseUrl: "https://cloud.example.com",
useEnv: true,
},
} as unknown as ApplyAccountConfigContext);
expect(next.channels?.["nextcloud-talk"]?.baseUrl).toBe("https://cloud.example.com");
expect(next.channels?.["nextcloud-talk"]).not.toHaveProperty("botSecret");
expect(next.channels?.["nextcloud-talk"]).not.toHaveProperty("botSecretFile");
});
it("clears stored bot secret fields when the wizard switches to env", async () => {
const credential = nextcloudTalkSetupWizard.credentials[0];
const next = await credential.applyUseEnv?.({
cfg: {
channels: {
"nextcloud-talk": {
enabled: true,
baseUrl: "https://cloud.example.com",
botSecret: "stored-secret",
botSecretFile: "/tmp/secret.txt",
},
},
},
accountId: DEFAULT_ACCOUNT_ID,
});
expect(next?.channels?.["nextcloud-talk"]).not.toHaveProperty("botSecret");
expect(next?.channels?.["nextcloud-talk"]).not.toHaveProperty("botSecretFile");
});
});

View File

@@ -1,5 +1,13 @@
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/routing";
import { describe, expect, it } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { createStartAccountContext } from "../../../test/helpers/extensions/start-account-context.js";
import {
expectStopPendingUntilAbort,
startAccountAndTrackLifecycle,
waitForStartedMocks,
} from "../../../test/helpers/extensions/start-account-lifecycle.js";
import type { ResolvedNextcloudTalkAccount } from "./accounts.js";
import { nextcloudTalkPlugin } from "./channel.js";
import {
clearNextcloudTalkAccountFields,
nextcloudTalkDmPolicy,
@@ -8,9 +16,57 @@ import {
setNextcloudTalkAccountConfig,
validateNextcloudTalkBaseUrl,
} from "./setup-core.js";
import { nextcloudTalkSetupWizard } from "./setup-surface.js";
import type { CoreConfig } from "./types.js";
describe("nextcloud talk setup core", () => {
const hoisted = vi.hoisted(() => ({
monitorNextcloudTalkProvider: vi.fn(),
}));
vi.mock("./monitor.js", async () => {
const actual = await vi.importActual<typeof import("./monitor.js")>("./monitor.js");
return {
...actual,
monitorNextcloudTalkProvider: hoisted.monitorNextcloudTalkProvider,
};
});
function buildAccount(): ResolvedNextcloudTalkAccount {
return {
accountId: "default",
enabled: true,
baseUrl: "https://nextcloud.example.com",
secret: "secret", // pragma: allowlist secret
secretSource: "config", // pragma: allowlist secret
config: {
baseUrl: "https://nextcloud.example.com",
botSecret: "secret", // pragma: allowlist secret
webhookPath: "/nextcloud-talk-webhook",
webhookPort: 8788,
},
};
}
function mockStartedMonitor() {
const stop = vi.fn();
hoisted.monitorNextcloudTalkProvider.mockResolvedValue({ stop });
return stop;
}
function startNextcloudAccount(abortSignal?: AbortSignal) {
return nextcloudTalkPlugin.gateway!.startAccount!(
createStartAccountContext({
account: buildAccount(),
abortSignal,
}),
);
}
describe("nextcloud talk setup", () => {
afterEach(() => {
vi.clearAllMocks();
});
it("normalizes and validates base urls", () => {
expect(normalizeNextcloudTalkBaseUrl(" https://cloud.example.com/// ")).toBe(
"https://cloud.example.com",
@@ -188,4 +244,78 @@ describe("nextcloud talk setup core", () => {
},
});
});
it("clears stored bot secret fields when switching the default account to env", () => {
type ApplyAccountConfigContext = Parameters<
typeof nextcloudTalkSetupAdapter.applyAccountConfig
>[0];
const next = nextcloudTalkSetupAdapter.applyAccountConfig({
cfg: {
channels: {
"nextcloud-talk": {
enabled: true,
baseUrl: "https://cloud.old.example",
botSecret: "stored-secret",
botSecretFile: "/tmp/secret.txt",
},
},
},
accountId: DEFAULT_ACCOUNT_ID,
input: {
baseUrl: "https://cloud.example.com",
useEnv: true,
},
} as unknown as ApplyAccountConfigContext);
expect(next.channels?.["nextcloud-talk"]?.baseUrl).toBe("https://cloud.example.com");
expect(next.channels?.["nextcloud-talk"]).not.toHaveProperty("botSecret");
expect(next.channels?.["nextcloud-talk"]).not.toHaveProperty("botSecretFile");
});
it("clears stored bot secret fields when the wizard switches to env", async () => {
const credential = nextcloudTalkSetupWizard.credentials[0];
const next = await credential.applyUseEnv?.({
cfg: {
channels: {
"nextcloud-talk": {
enabled: true,
baseUrl: "https://cloud.example.com",
botSecret: "stored-secret",
botSecretFile: "/tmp/secret.txt",
},
},
},
accountId: DEFAULT_ACCOUNT_ID,
});
expect(next?.channels?.["nextcloud-talk"]).not.toHaveProperty("botSecret");
expect(next?.channels?.["nextcloud-talk"]).not.toHaveProperty("botSecretFile");
});
it("keeps startAccount pending until abort, then stops the monitor", async () => {
const stop = mockStartedMonitor();
const { abort, task, isSettled } = startAccountAndTrackLifecycle({
startAccount: nextcloudTalkPlugin.gateway!.startAccount!,
account: buildAccount(),
});
await expectStopPendingUntilAbort({
waitForStarted: waitForStartedMocks(hoisted.monitorNextcloudTalkProvider),
isSettled,
abort,
task,
stop,
});
});
it("stops immediately when startAccount receives an already-aborted signal", async () => {
const stop = mockStartedMonitor();
const abort = new AbortController();
abort.abort();
await startNextcloudAccount(abort.signal);
expect(hoisted.monitorNextcloudTalkProvider).toHaveBeenCalledOnce();
expect(stop).toHaveBeenCalledOnce();
});
});

View File

@@ -1,17 +0,0 @@
import { describe, expect, it } from "vitest";
import { SynologyChatChannelConfigSchema } from "./config-schema.js";
describe("SynologyChatChannelConfigSchema", () => {
it("exports dangerouslyAllowNameMatching in the JSON schema", () => {
const properties = (SynologyChatChannelConfigSchema.schema.properties ?? {}) as Record<
string,
{ type?: string }
>;
expect(properties.dangerouslyAllowNameMatching?.type).toBe("boolean");
});
it("keeps the schema open for plugin-specific passthrough fields", () => {
expect([true, {}]).toContainEqual(SynologyChatChannelConfigSchema.schema.additionalProperties);
});
});

View File

@@ -7,10 +7,49 @@ import {
type WizardPrompter,
} from "../../../test/helpers/extensions/setup-wizard.js";
import { synologyChatPlugin } from "./channel.js";
import { SynologyChatChannelConfigSchema } from "./config-schema.js";
import { buildSynologyChatInboundSessionKey } from "./session-key.js";
const synologyChatConfigure = createPluginSetupWizardConfigure(synologyChatPlugin);
describe("synology-chat setup wizard", () => {
describe("synology-chat core", () => {
it("exports dangerouslyAllowNameMatching in the JSON schema", () => {
const properties = (SynologyChatChannelConfigSchema.schema.properties ?? {}) as Record<
string,
{ type?: string }
>;
expect(properties.dangerouslyAllowNameMatching?.type).toBe("boolean");
});
it("keeps the schema open for plugin-specific passthrough fields", () => {
expect([true, {}]).toContainEqual(SynologyChatChannelConfigSchema.schema.additionalProperties);
});
it("isolates direct-message sessions by account and user", () => {
const alpha = buildSynologyChatInboundSessionKey({
agentId: "main",
accountId: "alpha",
userId: "123",
});
const beta = buildSynologyChatInboundSessionKey({
agentId: "main",
accountId: "beta",
userId: "123",
});
const otherUser = buildSynologyChatInboundSessionKey({
agentId: "main",
accountId: "alpha",
userId: "456",
});
expect(alpha).toBe("agent:main:synology-chat:alpha:direct:123");
expect(beta).toBe("agent:main:synology-chat:beta:direct:123");
expect(otherUser).toBe("agent:main:synology-chat:alpha:direct:456");
expect(alpha).not.toBe(beta);
expect(alpha).not.toBe(otherUser);
});
it("configures token and incoming webhook for the default account", async () => {
const prompter = createTestWizardPrompter({
text: vi.fn(async ({ message }: { message: string }) => {

View File

@@ -1,28 +0,0 @@
import { describe, expect, it } from "vitest";
import { buildSynologyChatInboundSessionKey } from "./session-key.js";
describe("buildSynologyChatInboundSessionKey", () => {
it("isolates direct-message sessions by account and user", () => {
const alpha = buildSynologyChatInboundSessionKey({
agentId: "main",
accountId: "alpha",
userId: "123",
});
const beta = buildSynologyChatInboundSessionKey({
agentId: "main",
accountId: "beta",
userId: "123",
});
const otherUser = buildSynologyChatInboundSessionKey({
agentId: "main",
accountId: "alpha",
userId: "456",
});
expect(alpha).toBe("agent:main:synology-chat:alpha:direct:123");
expect(beta).toBe("agent:main:synology-chat:beta:direct:123");
expect(otherUser).toBe("agent:main:synology-chat:alpha:direct:456");
expect(alpha).not.toBe(beta);
expect(alpha).not.toBe(otherUser);
});
});

View File

@@ -1,32 +0,0 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../api.js";
import { tlonPlugin } from "./channel.js";
describe("tlonPlugin config", () => {
it("formats dm allowlist entries through the shared hybrid adapter", () => {
expect(
tlonPlugin.config.formatAllowFrom?.({
cfg: {} as OpenClawConfig,
allowFrom: ["zod", " ~nec "],
}),
).toEqual(["~zod", "~nec"]);
});
it("resolves dm allowlist from the default account", () => {
expect(
tlonPlugin.config.resolveAllowFrom?.({
cfg: {
channels: {
tlon: {
ship: "~sampel-palnet",
url: "https://urbit.example.com",
code: "lidlut-tabwed-pillex-ridrup",
dmAllowlist: ["~zod"],
},
},
} as OpenClawConfig,
accountId: "default",
}),
).toEqual(["~zod"]);
});
});

View File

@@ -1,31 +0,0 @@
import { describe, expect, it } from "vitest";
import { TlonAuthorizationSchema, TlonConfigSchema } from "./config-schema.js";
describe("Tlon config schema", () => {
it("accepts channelRules with string keys", () => {
const parsed = TlonAuthorizationSchema.parse({
channelRules: {
"chat/~zod/test": {
mode: "open",
allowedShips: ["~zod"],
},
},
});
expect(parsed.channelRules?.["chat/~zod/test"]?.mode).toBe("open");
});
it("accepts accounts with string keys", () => {
const parsed = TlonConfigSchema.parse({
accounts: {
primary: {
ship: "~zod",
url: "https://example.com",
code: "code-123",
},
},
});
expect(parsed.accounts?.primary?.ship).toBe("~zod");
});
});

View File

@@ -0,0 +1,224 @@
import { describe, expect, it, vi } from "vitest";
import {
createPluginSetupWizardConfigure,
createTestWizardPrompter,
runSetupWizardConfigure,
type WizardPrompter,
} from "../../../test/helpers/extensions/setup-wizard.js";
import type { OpenClawConfig } from "../api.js";
import { tlonPlugin } from "./channel.js";
import { TlonAuthorizationSchema, TlonConfigSchema } from "./config-schema.js";
import { resolveTlonOutboundTarget } from "./targets.js";
import { listTlonAccountIds, resolveTlonAccount } from "./types.js";
const tlonConfigure = createPluginSetupWizardConfigure(tlonPlugin);
describe("tlon core", () => {
it("formats dm allowlist entries through the shared hybrid adapter", () => {
expect(
tlonPlugin.config.formatAllowFrom?.({
cfg: {} as OpenClawConfig,
allowFrom: ["zod", " ~nec "],
}),
).toEqual(["~zod", "~nec"]);
});
it("resolves dm allowlist from the default account", () => {
expect(
tlonPlugin.config.resolveAllowFrom?.({
cfg: {
channels: {
tlon: {
ship: "~sampel-palnet",
url: "https://urbit.example.com",
code: "lidlut-tabwed-pillex-ridrup",
dmAllowlist: ["~zod"],
},
},
} as OpenClawConfig,
accountId: "default",
}),
).toEqual(["~zod"]);
});
it("accepts channelRules with string keys", () => {
const parsed = TlonAuthorizationSchema.parse({
channelRules: {
"chat/~zod/test": {
mode: "open",
allowedShips: ["~zod"],
},
},
});
expect(parsed.channelRules?.["chat/~zod/test"]?.mode).toBe("open");
});
it("accepts accounts with string keys", () => {
const parsed = TlonConfigSchema.parse({
accounts: {
primary: {
ship: "~zod",
url: "https://example.com",
code: "code-123",
},
},
});
expect(parsed.accounts?.primary?.ship).toBe("~zod");
});
it("configures ship, auth, and discovery settings", async () => {
const prompter = createTestWizardPrompter({
text: vi.fn(async ({ message }: { message: string }) => {
if (message === "Ship name") {
return "sampel-palnet";
}
if (message === "Ship URL") {
return "https://urbit.example.com";
}
if (message === "Login code") {
return "lidlut-tabwed-pillex-ridrup";
}
if (message === "Group channels (comma-separated)") {
return "chat/~host-ship/general, chat/~host-ship/support";
}
if (message === "DM allowlist (comma-separated ship names)") {
return "~zod, nec";
}
throw new Error(`Unexpected prompt: ${message}`);
}) as WizardPrompter["text"],
confirm: vi.fn(async ({ message }: { message: string }) => {
if (message === "Add group channels manually? (optional)") {
return true;
}
if (message === "Restrict DMs with an allowlist?") {
return true;
}
if (message === "Enable auto-discovery of group channels?") {
return true;
}
return false;
}),
});
const result = await runSetupWizardConfigure({
configure: tlonConfigure,
cfg: {} as OpenClawConfig,
prompter,
options: {},
});
expect(result.accountId).toBe("default");
expect(result.cfg.channels?.tlon?.enabled).toBe(true);
expect(result.cfg.channels?.tlon?.ship).toBe("~sampel-palnet");
expect(result.cfg.channels?.tlon?.url).toBe("https://urbit.example.com");
expect(result.cfg.channels?.tlon?.code).toBe("lidlut-tabwed-pillex-ridrup");
expect(result.cfg.channels?.tlon?.groupChannels).toEqual([
"chat/~host-ship/general",
"chat/~host-ship/support",
]);
expect(result.cfg.channels?.tlon?.dmAllowlist).toEqual(["~zod", "~nec"]);
expect(result.cfg.channels?.tlon?.autoDiscoverChannels).toBe(true);
expect(result.cfg.channels?.tlon?.allowPrivateNetwork).toBe(false);
});
it("resolves dm targets to normalized ships", () => {
expect(resolveTlonOutboundTarget("dm/sampel-palnet")).toEqual({
ok: true,
to: "~sampel-palnet",
});
});
it("resolves group targets to canonical chat nests", () => {
expect(resolveTlonOutboundTarget("group:host-ship/general")).toEqual({
ok: true,
to: "chat/~host-ship/general",
});
});
it("returns a helpful error for invalid targets", () => {
const resolved = resolveTlonOutboundTarget("group:bad-target");
expect(resolved.ok).toBe(false);
if (resolved.ok) {
throw new Error("expected invalid target");
}
expect(resolved.error.message).toMatch(/invalid tlon target/i);
});
it("lists named accounts and the implicit default account", () => {
const cfg = {
channels: {
tlon: {
ship: "~zod",
accounts: {
Work: { ship: "~bus" },
alerts: { ship: "~nec" },
},
},
},
} as OpenClawConfig;
expect(listTlonAccountIds(cfg)).toEqual(["alerts", "default", "work"]);
});
it("merges named account config over channel defaults", () => {
const resolved = resolveTlonAccount(
{
channels: {
tlon: {
name: "Base",
ship: "~zod",
url: "https://urbit.example.com",
code: "base-code",
dmAllowlist: ["~nec"],
groupInviteAllowlist: ["~bus"],
defaultAuthorizedShips: ["~marzod"],
accounts: {
Work: {
name: "Work",
code: "work-code",
dmAllowlist: ["~rovnys"],
},
},
},
},
} as OpenClawConfig,
"work",
);
expect(resolved.accountId).toBe("work");
expect(resolved.name).toBe("Work");
expect(resolved.ship).toBe("~zod");
expect(resolved.url).toBe("https://urbit.example.com");
expect(resolved.code).toBe("work-code");
expect(resolved.dmAllowlist).toEqual(["~rovnys"]);
expect(resolved.groupInviteAllowlist).toEqual(["~bus"]);
expect(resolved.defaultAuthorizedShips).toEqual(["~marzod"]);
expect(resolved.configured).toBe(true);
});
it("keeps the default account on channel-level config only", () => {
const resolved = resolveTlonAccount(
{
channels: {
tlon: {
ship: "~zod",
url: "https://urbit.example.com",
code: "base-code",
accounts: {
default: {
ship: "~ignored",
code: "ignored-code",
},
},
},
},
} as OpenClawConfig,
"default",
);
expect(resolved.ship).toBe("~zod");
expect(resolved.code).toBe("base-code");
});
});

View File

@@ -1,68 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import {
createPluginSetupWizardConfigure,
createTestWizardPrompter,
runSetupWizardConfigure,
type WizardPrompter,
} from "../../../test/helpers/extensions/setup-wizard.js";
import type { OpenClawConfig } from "../api.js";
import { tlonPlugin } from "./channel.js";
const tlonConfigure = createPluginSetupWizardConfigure(tlonPlugin);
describe("tlon setup wizard", () => {
it("configures ship, auth, and discovery settings", async () => {
const prompter = createTestWizardPrompter({
text: vi.fn(async ({ message }: { message: string }) => {
if (message === "Ship name") {
return "sampel-palnet";
}
if (message === "Ship URL") {
return "https://urbit.example.com";
}
if (message === "Login code") {
return "lidlut-tabwed-pillex-ridrup";
}
if (message === "Group channels (comma-separated)") {
return "chat/~host-ship/general, chat/~host-ship/support";
}
if (message === "DM allowlist (comma-separated ship names)") {
return "~zod, nec";
}
throw new Error(`Unexpected prompt: ${message}`);
}) as WizardPrompter["text"],
confirm: vi.fn(async ({ message }: { message: string }) => {
if (message === "Add group channels manually? (optional)") {
return true;
}
if (message === "Restrict DMs with an allowlist?") {
return true;
}
if (message === "Enable auto-discovery of group channels?") {
return true;
}
return false;
}),
});
const result = await runSetupWizardConfigure({
configure: tlonConfigure,
cfg: {} as OpenClawConfig,
prompter,
options: {},
});
expect(result.accountId).toBe("default");
expect(result.cfg.channels?.tlon?.enabled).toBe(true);
expect(result.cfg.channels?.tlon?.ship).toBe("~sampel-palnet");
expect(result.cfg.channels?.tlon?.url).toBe("https://urbit.example.com");
expect(result.cfg.channels?.tlon?.code).toBe("lidlut-tabwed-pillex-ridrup");
expect(result.cfg.channels?.tlon?.groupChannels).toEqual([
"chat/~host-ship/general",
"chat/~host-ship/support",
]);
expect(result.cfg.channels?.tlon?.dmAllowlist).toEqual(["~zod", "~nec"]);
expect(result.cfg.channels?.tlon?.autoDiscoverChannels).toBe(true);
expect(result.cfg.channels?.tlon?.allowPrivateNetwork).toBe(false);
});
});

View File

@@ -1,27 +0,0 @@
import { describe, expect, it } from "vitest";
import { resolveTlonOutboundTarget } from "./targets.js";
describe("resolveTlonOutboundTarget", () => {
it("resolves dm targets to normalized ships", () => {
expect(resolveTlonOutboundTarget("dm/sampel-palnet")).toEqual({
ok: true,
to: "~sampel-palnet",
});
});
it("resolves group targets to canonical chat nests", () => {
expect(resolveTlonOutboundTarget("group:host-ship/general")).toEqual({
ok: true,
to: "chat/~host-ship/general",
});
});
it("returns a helpful error for invalid targets", () => {
const resolved = resolveTlonOutboundTarget("group:bad-target");
expect(resolved.ok).toBe(false);
if (resolved.ok) {
throw new Error("expected invalid target");
}
expect(resolved.error.message).toMatch(/invalid tlon target/i);
});
});

View File

@@ -1,81 +0,0 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../api.js";
import { listTlonAccountIds, resolveTlonAccount } from "./types.js";
describe("tlon account helpers", () => {
it("lists named accounts and the implicit default account", () => {
const cfg = {
channels: {
tlon: {
ship: "~zod",
accounts: {
Work: { ship: "~bus" },
alerts: { ship: "~nec" },
},
},
},
} as OpenClawConfig;
expect(listTlonAccountIds(cfg)).toEqual(["alerts", "default", "work"]);
});
it("merges named account config over channel defaults", () => {
const resolved = resolveTlonAccount(
{
channels: {
tlon: {
name: "Base",
ship: "~zod",
url: "https://urbit.example.com",
code: "base-code",
dmAllowlist: ["~nec"],
groupInviteAllowlist: ["~bus"],
defaultAuthorizedShips: ["~marzod"],
accounts: {
Work: {
name: "Work",
code: "work-code",
dmAllowlist: ["~rovnys"],
},
},
},
},
} as OpenClawConfig,
"work",
);
expect(resolved.accountId).toBe("work");
expect(resolved.name).toBe("Work");
expect(resolved.ship).toBe("~zod");
expect(resolved.url).toBe("https://urbit.example.com");
expect(resolved.code).toBe("work-code");
expect(resolved.dmAllowlist).toEqual(["~rovnys"]);
expect(resolved.groupInviteAllowlist).toEqual(["~bus"]);
expect(resolved.defaultAuthorizedShips).toEqual(["~marzod"]);
expect(resolved.configured).toBe(true);
});
it("keeps the default account on channel-level config only", () => {
const resolved = resolveTlonAccount(
{
channels: {
tlon: {
ship: "~zod",
url: "https://urbit.example.com",
code: "base-code",
accounts: {
default: {
ship: "~ignored",
code: "ignored-code",
},
},
},
},
} as OpenClawConfig,
"default",
);
expect(resolved.ship).toBe("~zod");
expect(resolved.code).toBe("base-code");
});
});