import { afterEach, describe, expect, it, vi } from "vitest" ;
import {
createPluginSetupWizardAdapter,
createPluginSetupWizardStatus,
createTestWizardPrompter,
promptSetupWizardAllowFrom,
runSetupWizardConfigure,
type WizardPrompter,
} from "../../../test/helpers/plugins/setup-wizard.js" ;
import {
expectStopPendingUntilAbort,
startAccountAndTrackLifecycle,
waitForStartedMocks,
} from "../../../test/helpers/plugins/start-account-lifecycle.js" ;
import {
listIrcAccountIds,
resolveDefaultIrcAccountId,
type ResolvedIrcAccount,
} from "./accounts.js" ;
import { startIrcGatewayAccount } from "./gateway.js" ;
import { clearIrcRuntime, setIrcRuntime } from "./runtime.js" ;
import {
ircSetupAdapter,
parsePort,
setIrcAllowFrom,
setIrcDmPolicy,
setIrcGroupAccess,
setIrcNickServ,
updateIrcAccountConfig,
} from "./setup-core.js" ;
import { ircSetupWizard } from "./setup-surface.js" ;
import type { CoreConfig } from "./types.js" ;
const hoisted = vi.hoisted(() => ({
monitorIrcProvider: vi.fn(),
sendMessageIrc: vi.fn(),
}));
vi.mock("./channel-runtime.js" , () => {
return {
monitorIrcProvider: hoisted.monitorIrcProvider,
sendMessageIrc: hoisted.sendMessageIrc,
};
});
const ircSetupPlugin = {
id: "irc" ,
meta: {
label: "IRC" ,
},
config: {
defaultAccountId: resolveDefaultIrcAccountId,
listAccountIds: listIrcAccountIds,
},
setupWizard: ircSetupWizard,
} as never;
const ircConfigureAdapter = createPluginSetupWizardAdapter(ircSetupPlugin);
const ircStatus = createPluginSetupWizardStatus(ircSetupPlugin);
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" ],
};
}
function installIrcRuntime() {
setIrcRuntime({
logging: {
shouldLogVerbose: vi.fn(() => false ),
getChildLogger: vi.fn(() => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
})),
},
channel: {
activity: {
record: vi.fn(),
get: vi.fn(),
},
},
} as never);
}
describe("irc setup" , () => {
afterEach(() => {
vi.clearAllMocks();
clearIrcRuntime();
});
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("setup status honors the selected named account" , async () => {
const status = await ircStatus({
cfg: {
channels: {
irc: {
accounts: {
ops: {
host: "irc.example.com" ,
nick: "ops-bot" ,
},
work: {
host: "irc.example.com" ,
},
},
},
},
} as CoreConfig,
accountOverrides: {
irc: "work" ,
},
});
expect(status.configured).toBe(false );
expect(status.statusLines).toEqual(["IRC: needs host + nick" ]);
});
it("setup status honors the configured default account" , async () => {
const status = await ircStatus({
cfg: {
channels: {
irc: {
defaultAccount: "work" ,
accounts: {
ops: {
host: "irc.example.com" ,
nick: "ops-bot" ,
},
work: {
host: "irc.example.com" ,
nick: "" ,
},
},
},
},
} as CoreConfig,
accountOverrides: {},
});
expect(status.configured).toBe(false );
expect(status.statusLines).toEqual(["IRC: needs host + nick" ]);
});
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 });
installIrcRuntime();
const { abort, task, isSettled } = startAccountAndTrackLifecycle({
startAccount: async (ctx) =>
await startIrcGatewayAccount({
...ctx,
cfg: ctx.cfg as CoreConfig,
}),
account: buildAccount(),
});
await expectStopPendingUntilAbort({
waitForStarted: waitForStartedMocks(hoisted.monitorIrcProvider),
isSettled,
abort,
task,
stop,
});
});
});
Messung V0.5 in Prozent C=100 H=100 G=100
¤ Dauer der Verarbeitung: 0.7 Sekunden
¤
*© Formatika GbR, Deutschland