mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 09:21:35 +07:00
test: collapse channel setup test suites
This commit is contained in:
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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"],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
347
extensions/irc/src/setup.test.ts
Normal file
347
extensions/irc/src/setup.test.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
99
extensions/nextcloud-talk/src/core.test.ts
Normal file
99
extensions/nextcloud-talk/src/core.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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 }) => {
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
224
extensions/tlon/src/core.test.ts
Normal file
224
extensions/tlon/src/core.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user