mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
refactor(irc,workboard): trim internal exports (#107749)
* refactor(irc): privatize internal plugin surfaces * refactor(workboard): privatize internal plugin surfaces * chore(deadcode): refresh unused-export baseline
This commit is contained in:
@@ -1,13 +1,8 @@
|
||||
// Irc tests cover channel plugin behavior.
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ircOutboundBaseAdapter } from "./outbound-base.js";
|
||||
import { clearIrcRuntime } from "./runtime.js";
|
||||
|
||||
describe("irc outbound chunking", () => {
|
||||
afterEach(() => {
|
||||
clearIrcRuntime();
|
||||
});
|
||||
|
||||
it("chunks outbound text without requiring IRC runtime initialization", () => {
|
||||
expect(ircOutboundBaseAdapter.chunker("alpha beta", 5)).toEqual(["alpha", "beta"]);
|
||||
expect(ircOutboundBaseAdapter.deliveryMode).toBe("direct");
|
||||
@@ -17,10 +12,6 @@ describe("irc outbound chunking", () => {
|
||||
});
|
||||
|
||||
describe("irc outbound sanitizeText", () => {
|
||||
afterEach(() => {
|
||||
clearIrcRuntime();
|
||||
});
|
||||
|
||||
it("strips internal tool-trace banners before outbound delivery", () => {
|
||||
const text = "Done.\n⚠️ 🛠️ `search repos (agent)` failed";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Irc tests cover client plugin behavior.
|
||||
import net from "node:net";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildFallbackNick, buildIrcNickServCommands, connectIrcClient } from "./client.js";
|
||||
import { connectIrcClient } from "./client.js";
|
||||
|
||||
type LoopbackIrcServer = {
|
||||
port: number;
|
||||
@@ -17,13 +17,16 @@ type HangingIrcServer = {
|
||||
close(): Promise<void>;
|
||||
};
|
||||
|
||||
async function startLoopbackIrcServer(): Promise<LoopbackIrcServer> {
|
||||
async function startLoopbackIrcServer(options?: {
|
||||
rejectInitialNick?: boolean;
|
||||
}): Promise<LoopbackIrcServer> {
|
||||
const lines: string[] = [];
|
||||
const sockets = new Set<net.Socket>();
|
||||
const server = net.createServer((socket) => {
|
||||
sockets.add(socket);
|
||||
socket.setEncoding("utf8");
|
||||
let buffer = "";
|
||||
let awaitingFallbackNick = false;
|
||||
socket.on("data", (chunk: string) => {
|
||||
buffer += chunk;
|
||||
let idx = buffer.indexOf("\n");
|
||||
@@ -33,7 +36,15 @@ async function startLoopbackIrcServer(): Promise<LoopbackIrcServer> {
|
||||
idx = buffer.indexOf("\n");
|
||||
lines.push(line);
|
||||
if (line.startsWith("USER ")) {
|
||||
socket.write(":server 001 bot :welcome\r\n");
|
||||
if (options?.rejectInitialNick) {
|
||||
awaitingFallbackNick = true;
|
||||
socket.write(":server 433 * bot :Nickname in use\r\n");
|
||||
} else {
|
||||
socket.write(":server 001 bot :welcome\r\n");
|
||||
}
|
||||
} else if (awaitingFallbackNick && line.startsWith("NICK ")) {
|
||||
awaitingFallbackNick = false;
|
||||
socket.write(`:server 001 ${line.slice("NICK ".length)} :welcome\r\n`);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -68,6 +79,54 @@ async function startLoopbackIrcServer(): Promise<LoopbackIrcServer> {
|
||||
};
|
||||
}
|
||||
|
||||
async function connectAndCollectRegistration(params: {
|
||||
nickserv: NonNullable<Parameters<typeof connectIrcClient>[0]["nickserv"]>;
|
||||
done: (lines: string[], errors: Error[]) => boolean;
|
||||
}): Promise<{ lines: string[]; errors: Error[] }> {
|
||||
const server = await startLoopbackIrcServer();
|
||||
const errors: Error[] = [];
|
||||
const client = await connectIrcClient({
|
||||
host: "127.0.0.1",
|
||||
port: server.port,
|
||||
tls: false,
|
||||
nick: "bot",
|
||||
username: "bot",
|
||||
realname: "OpenClaw Bot",
|
||||
nickserv: params.nickserv,
|
||||
onError: (error) => errors.push(error),
|
||||
});
|
||||
try {
|
||||
await waitForIrcCondition(
|
||||
() => params.done(server.lines, errors),
|
||||
"expected IRC registration outcome",
|
||||
);
|
||||
return { lines: [...server.lines], errors };
|
||||
} finally {
|
||||
client.close();
|
||||
await server.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function connectAfterNickCollision(nick: string): Promise<string> {
|
||||
const server = await startLoopbackIrcServer({ rejectInitialNick: true });
|
||||
const client = await connectIrcClient({
|
||||
host: "127.0.0.1",
|
||||
port: server.port,
|
||||
tls: false,
|
||||
nick,
|
||||
username: "bot",
|
||||
realname: "OpenClaw Bot",
|
||||
});
|
||||
try {
|
||||
const nickLines = server.lines.filter((line) => line.startsWith("NICK "));
|
||||
expect(nickLines).toHaveLength(2);
|
||||
return nickLines[1]!.slice("NICK ".length);
|
||||
} finally {
|
||||
client.close();
|
||||
await server.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForIrcCondition(
|
||||
predicate: () => boolean,
|
||||
message: string,
|
||||
@@ -132,43 +191,53 @@ async function startHangingIrcServer(): Promise<HangingIrcServer> {
|
||||
}
|
||||
|
||||
describe("irc client nickserv", () => {
|
||||
it("builds IDENTIFY command when password is set", () => {
|
||||
expect(
|
||||
buildIrcNickServCommands({
|
||||
password: "secret",
|
||||
}),
|
||||
).toEqual(["PRIVMSG NickServ :IDENTIFY secret"]);
|
||||
it("sends IDENTIFY when a password is configured", async () => {
|
||||
const result = await connectAndCollectRegistration({
|
||||
nickserv: { password: "secret" },
|
||||
done: (lines) => lines.includes("PRIVMSG NickServ :IDENTIFY secret"),
|
||||
});
|
||||
|
||||
expect(result.lines).toContain("PRIVMSG NickServ :IDENTIFY secret");
|
||||
});
|
||||
|
||||
it("builds REGISTER command when enabled with email", () => {
|
||||
expect(
|
||||
buildIrcNickServCommands({
|
||||
it("sends REGISTER after IDENTIFY when enabled with email", async () => {
|
||||
const result = await connectAndCollectRegistration({
|
||||
nickserv: {
|
||||
password: "secret",
|
||||
register: true,
|
||||
registerEmail: "bot@example.com",
|
||||
}),
|
||||
).toEqual([
|
||||
},
|
||||
done: (lines) => lines.some((line) => line.startsWith("PRIVMSG NickServ :REGISTER ")),
|
||||
});
|
||||
|
||||
expect(result.lines.filter((line) => line.startsWith("PRIVMSG NickServ :"))).toEqual([
|
||||
"PRIVMSG NickServ :IDENTIFY secret",
|
||||
"PRIVMSG NickServ :REGISTER secret bot@example.com",
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects register without registerEmail", () => {
|
||||
expect(() =>
|
||||
buildIrcNickServCommands({
|
||||
it("reports register without registerEmail", async () => {
|
||||
const result = await connectAndCollectRegistration({
|
||||
nickserv: {
|
||||
password: "secret",
|
||||
register: true,
|
||||
}),
|
||||
).toThrow(/registerEmail/);
|
||||
},
|
||||
done: (_lines, errors) => errors.length > 0,
|
||||
});
|
||||
|
||||
expect(result.errors[0]?.message).toMatch(/registerEmail/);
|
||||
});
|
||||
|
||||
it("sanitizes outbound NickServ payloads", () => {
|
||||
expect(
|
||||
buildIrcNickServCommands({
|
||||
it("sanitizes outbound NickServ payloads", async () => {
|
||||
const result = await connectAndCollectRegistration({
|
||||
nickserv: {
|
||||
service: "NickServ\n",
|
||||
password: "secret\r\nJOIN #bad",
|
||||
}),
|
||||
).toEqual(["PRIVMSG NickServ :IDENTIFY secret JOIN #bad"]);
|
||||
},
|
||||
done: (lines) => lines.some((line) => line.startsWith("PRIVMSG NickServ :IDENTIFY")),
|
||||
});
|
||||
|
||||
expect(result.lines).toContain("PRIVMSG NickServ :IDENTIFY secret JOIN #bad");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -200,35 +269,29 @@ describe("irc client readiness timeout", () => {
|
||||
});
|
||||
|
||||
describe("irc client fallback nick", () => {
|
||||
it("produces unique fallback nicks across sequential calls", () => {
|
||||
const first = buildFallbackNick("bot");
|
||||
const second = buildFallbackNick("bot");
|
||||
const third = buildFallbackNick("bot");
|
||||
// First call gets suffix _ (seq=1), subsequent calls get _2, _3, ...
|
||||
expect(first).toBe("bot_");
|
||||
it("produces unique fallback nicks across sequential collisions", async () => {
|
||||
const first = await connectAfterNickCollision("bot");
|
||||
const second = await connectAfterNickCollision("bot");
|
||||
const third = await connectAfterNickCollision("bot");
|
||||
expect(first).toMatch(/^bot_\d*$/);
|
||||
expect(second).toMatch(/^bot_\d+$/);
|
||||
expect(third).toMatch(/^bot_\d+$/);
|
||||
expect(new Set([first, second, third]).size).toBe(3);
|
||||
});
|
||||
|
||||
it("sanitizes whitespace and special characters in nick", () => {
|
||||
const nick = buildFallbackNick("my bot!");
|
||||
it("sanitizes whitespace and special characters after a collision", async () => {
|
||||
const nick = await connectAfterNickCollision("my bot!");
|
||||
expect(nick).toMatch(/^mybot_\d*$/);
|
||||
});
|
||||
|
||||
it("falls back to openclaw when nick consists entirely of special characters", () => {
|
||||
const nick = buildFallbackNick("!!!");
|
||||
it("falls back to openclaw when a colliding nick is entirely special characters", async () => {
|
||||
const nick = await connectAfterNickCollision("!!!");
|
||||
expect(nick).toMatch(/^openclaw_\d*$/);
|
||||
});
|
||||
|
||||
it("falls back to openclaw when nick is empty after sanitization", () => {
|
||||
const nick = buildFallbackNick("");
|
||||
expect(nick).toMatch(/^openclaw_\d*$/);
|
||||
});
|
||||
|
||||
it("truncates long nicks to max 30 chars", () => {
|
||||
it("truncates a long fallback nick to 30 characters", async () => {
|
||||
const longNick = "a".repeat(50);
|
||||
const nick = buildFallbackNick(longNick);
|
||||
const nick = await connectAfterNickCollision(longNick);
|
||||
expect(nick.length).toBeLessThanOrEqual(30);
|
||||
expect(nick).toMatch(/^a+_\d*$/);
|
||||
});
|
||||
|
||||
@@ -100,7 +100,7 @@ function toError(err: unknown): Error {
|
||||
|
||||
let nickCollisionFallbackSeq = 0;
|
||||
|
||||
export function buildFallbackNick(nick: string): string {
|
||||
function buildFallbackNick(nick: string): string {
|
||||
const normalized = nick.replace(/\s+/g, "");
|
||||
const safe = normalized.replace(/[^A-Za-z0-9_\-[\]\\`^{}|]/g, "");
|
||||
const base = safe || "openclaw";
|
||||
@@ -117,7 +117,7 @@ function normalizeIrcNick(value: string): string {
|
||||
return normalizeLowercaseStringOrEmpty(value);
|
||||
}
|
||||
|
||||
export function buildIrcNickServCommands(options?: IrcNickServOptions): string[] {
|
||||
function buildIrcNickServCommands(options?: IrcNickServOptions): string[] {
|
||||
if (!options || options.enabled === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -1,27 +1,41 @@
|
||||
// Irc tests cover config schema plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { IrcConfigSchema } from "./config-schema.js";
|
||||
import { IrcChannelConfigSchema } from "./config-schema.js";
|
||||
import type { IrcAccountConfig } from "./types.js";
|
||||
|
||||
function expectValidConfig(result: ReturnType<typeof IrcConfigSchema.safeParse>) {
|
||||
type ParsedIrcConfig = IrcAccountConfig & {
|
||||
accounts?: Record<string, IrcAccountConfig>;
|
||||
defaultAccount?: string;
|
||||
};
|
||||
|
||||
function parseIrcConfig(value: unknown) {
|
||||
const runtime = IrcChannelConfigSchema.runtime;
|
||||
if (!runtime) {
|
||||
throw new Error("expected IRC channel config runtime");
|
||||
}
|
||||
return runtime.safeParse(value);
|
||||
}
|
||||
|
||||
function expectValidConfig(result: ReturnType<typeof parseIrcConfig>) {
|
||||
expect(result.success).toBe(true);
|
||||
if (!result.success) {
|
||||
throw new Error("expected config to be valid");
|
||||
}
|
||||
return result.data;
|
||||
return result.data as ParsedIrcConfig;
|
||||
}
|
||||
|
||||
function expectInvalidConfig(result: ReturnType<typeof IrcConfigSchema.safeParse>) {
|
||||
function expectInvalidConfig(result: ReturnType<typeof parseIrcConfig>) {
|
||||
expect(result.success).toBe(false);
|
||||
if (result.success) {
|
||||
throw new Error("expected config to be invalid");
|
||||
}
|
||||
return result.error.issues;
|
||||
return result.issues;
|
||||
}
|
||||
|
||||
describe("irc config schema", () => {
|
||||
it("accepts basic config", () => {
|
||||
const config = expectValidConfig(
|
||||
IrcConfigSchema.safeParse({
|
||||
parseIrcConfig({
|
||||
host: "irc.libera.chat",
|
||||
nick: "openclaw-bot",
|
||||
channels: ["#openclaw"],
|
||||
@@ -34,18 +48,18 @@ describe("irc config schema", () => {
|
||||
|
||||
it('rejects dmPolicy="open" without allowFrom "*"', () => {
|
||||
const issues = expectInvalidConfig(
|
||||
IrcConfigSchema.safeParse({
|
||||
parseIrcConfig({
|
||||
dmPolicy: "open",
|
||||
allowFrom: ["alice"],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(issues[0]?.path.join(".")).toBe("allowFrom");
|
||||
expect(issues[0]?.path?.join(".")).toBe("allowFrom");
|
||||
});
|
||||
|
||||
it('accepts dmPolicy="open" with allowFrom "*"', () => {
|
||||
const config = expectValidConfig(
|
||||
IrcConfigSchema.safeParse({
|
||||
parseIrcConfig({
|
||||
dmPolicy: "open",
|
||||
allowFrom: ["*"],
|
||||
}),
|
||||
@@ -55,31 +69,35 @@ describe("irc config schema", () => {
|
||||
});
|
||||
|
||||
it("accepts numeric allowFrom and groupAllowFrom entries", () => {
|
||||
const parsed = IrcConfigSchema.parse({
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: [12345, "alice"],
|
||||
groupAllowFrom: [67890, "alice!ident@example.org"],
|
||||
});
|
||||
const parsed = expectValidConfig(
|
||||
parseIrcConfig({
|
||||
dmPolicy: "allowlist",
|
||||
allowFrom: [12345, "alice"],
|
||||
groupAllowFrom: [67890, "alice!ident@example.org"],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(parsed.allowFrom).toEqual([12345, "alice"]);
|
||||
expect(parsed.groupAllowFrom).toEqual([67890, "alice!ident@example.org"]);
|
||||
});
|
||||
|
||||
it("accepts numeric per-channel allowFrom entries", () => {
|
||||
const parsed = IrcConfigSchema.parse({
|
||||
groups: {
|
||||
"#ops": {
|
||||
allowFrom: [42, "alice"],
|
||||
const parsed = expectValidConfig(
|
||||
parseIrcConfig({
|
||||
groups: {
|
||||
"#ops": {
|
||||
allowFrom: [42, "alice"],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
expect(parsed.groups?.["#ops"]?.allowFrom).toEqual([42, "alice"]);
|
||||
});
|
||||
|
||||
it("rejects nickserv register without registerEmail", () => {
|
||||
const issues = expectInvalidConfig(
|
||||
IrcConfigSchema.safeParse({
|
||||
parseIrcConfig({
|
||||
nickserv: {
|
||||
register: true,
|
||||
password: "secret",
|
||||
@@ -87,12 +105,12 @@ describe("irc config schema", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
expect(issues[0]?.path.join(".")).toBe("nickserv.registerEmail");
|
||||
expect(issues[0]?.path?.join(".")).toBe("nickserv.registerEmail");
|
||||
});
|
||||
|
||||
it("accepts nickserv register with password and registerEmail", () => {
|
||||
const config = expectValidConfig(
|
||||
IrcConfigSchema.safeParse({
|
||||
parseIrcConfig({
|
||||
nickserv: {
|
||||
register: true,
|
||||
password: "secret",
|
||||
@@ -106,7 +124,7 @@ describe("irc config schema", () => {
|
||||
|
||||
it("accepts nickserv register with registerEmail only", () => {
|
||||
expectValidConfig(
|
||||
IrcConfigSchema.safeParse({
|
||||
parseIrcConfig({
|
||||
nickserv: {
|
||||
register: true,
|
||||
registerEmail: "bot@example.com",
|
||||
|
||||
@@ -79,7 +79,7 @@ const IrcAccountSchema = IrcAccountSchemaBase.superRefine((value, ctx) => {
|
||||
});
|
||||
});
|
||||
|
||||
export const IrcConfigSchema = IrcAccountSchemaBase.extend({
|
||||
const IrcConfigSchema = IrcAccountSchemaBase.extend({
|
||||
accounts: z.record(z.string(), IrcAccountSchema.optional()).optional(),
|
||||
defaultAccount: z.string().optional(),
|
||||
}).superRefine((value, ctx) => {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
// Irc tests cover control chars plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { hasIrcControlChars, isIrcControlChar, stripIrcControlChars } from "./control-chars.js";
|
||||
import { hasIrcControlChars, stripIrcControlChars } from "./control-chars.js";
|
||||
|
||||
describe("irc control char helpers", () => {
|
||||
it("detects IRC control characters by codepoint", () => {
|
||||
expect(isIrcControlChar(0x00)).toBe(true);
|
||||
expect(isIrcControlChar(0x1f)).toBe(true);
|
||||
expect(isIrcControlChar(0x7f)).toBe(true);
|
||||
expect(isIrcControlChar(0x20)).toBe(false);
|
||||
it("detects IRC control characters without classifying spaces as control text", () => {
|
||||
expect(hasIrcControlChars("\u0000hello\u001f")).toBe(true);
|
||||
expect(hasIrcControlChars("hello\u007f")).toBe(true);
|
||||
expect(hasIrcControlChars("hello world")).toBe(false);
|
||||
});
|
||||
|
||||
it("detects and strips IRC control characters from strings", () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Irc plugin module implements control chars behavior.
|
||||
export function isIrcControlChar(charCode: number): boolean {
|
||||
function isIrcControlChar(charCode: number): boolean {
|
||||
return charCode <= 0x1f || charCode === 0x7f;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// Irc tests cover inbound.behavior plugin behavior.
|
||||
import { createPluginRuntimeMock } from "openclaw/plugin-sdk/channel-test-helpers";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ResolvedIrcAccount } from "./accounts.js";
|
||||
import { handleIrcInbound } from "./inbound.js";
|
||||
import type { RuntimeEnv } from "./runtime-api.js";
|
||||
import { clearIrcRuntime, setIrcRuntime } from "./runtime.js";
|
||||
import { setIrcRuntime } from "./runtime.js";
|
||||
import type { CoreConfig, IrcInboundMessage } from "./types.js";
|
||||
|
||||
const {
|
||||
@@ -98,10 +98,6 @@ describe("irc inbound behavior", () => {
|
||||
installIrcRuntime();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearIrcRuntime();
|
||||
});
|
||||
|
||||
it("issues a DM pairing challenge and sends the reply to the sender nick", async () => {
|
||||
const sendReply = vi.fn<(target: string, text: string, replyToId?: string) => Promise<void>>(
|
||||
async () => {},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Irc tests cover monitor plugin behavior.
|
||||
import net from "node:net";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { monitorIrcProvider, resolveIrcInboundTarget } from "./monitor.js";
|
||||
import { clearIrcRuntime, setIrcRuntime } from "./runtime.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { monitorIrcProvider } from "./monitor.js";
|
||||
import { setIrcRuntime } from "./runtime.js";
|
||||
import type { CoreConfig, IrcInboundMessage } from "./types.js";
|
||||
|
||||
type DisconnectingIrcServer = {
|
||||
port: number;
|
||||
@@ -12,6 +12,11 @@ type DisconnectingIrcServer = {
|
||||
close(): Promise<void>;
|
||||
};
|
||||
|
||||
type InboundIrcServer = {
|
||||
port: number;
|
||||
close(): Promise<void>;
|
||||
};
|
||||
|
||||
async function waitForIrcCondition(
|
||||
predicate: () => boolean,
|
||||
message: string,
|
||||
@@ -90,6 +95,56 @@ async function startDisconnectingIrcServer(): Promise<DisconnectingIrcServer> {
|
||||
};
|
||||
}
|
||||
|
||||
async function startInboundIrcServer(target: string): Promise<InboundIrcServer> {
|
||||
const sockets = new Set<net.Socket>();
|
||||
const server = net.createServer((socket) => {
|
||||
sockets.add(socket);
|
||||
socket.setEncoding("utf8");
|
||||
let buffer = "";
|
||||
socket.on("data", (chunk: string) => {
|
||||
buffer += chunk;
|
||||
let idx = buffer.indexOf("\n");
|
||||
while (idx !== -1) {
|
||||
const line = buffer.slice(0, idx).replace(/\r$/, "");
|
||||
buffer = buffer.slice(idx + 1);
|
||||
idx = buffer.indexOf("\n");
|
||||
if (line.startsWith("USER ")) {
|
||||
socket.write(":server 001 bot :welcome\r\n");
|
||||
setTimeout(() => {
|
||||
socket.write(`:alice!ident@example.org PRIVMSG ${target} :hello\r\n`);
|
||||
}, 20);
|
||||
}
|
||||
}
|
||||
});
|
||||
socket.on("close", () => sockets.delete(socket));
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, "127.0.0.1", resolve);
|
||||
});
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
throw new Error("expected loopback IRC server to bind a TCP port");
|
||||
}
|
||||
return {
|
||||
port: address.port,
|
||||
close: async () => {
|
||||
for (const socket of sockets) {
|
||||
socket.destroy();
|
||||
}
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.close((error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function installMonitorRuntime() {
|
||||
setIrcRuntime({
|
||||
logging: {
|
||||
@@ -109,10 +164,6 @@ function installMonitorRuntime() {
|
||||
} as never);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
clearIrcRuntime();
|
||||
});
|
||||
|
||||
describe("irc monitor reconnect", () => {
|
||||
it("reconnects when an established IRC socket closes", async () => {
|
||||
installMonitorRuntime();
|
||||
@@ -150,42 +201,49 @@ describe("irc monitor reconnect", () => {
|
||||
});
|
||||
|
||||
describe("irc monitor inbound target", () => {
|
||||
it("keeps channel target for group messages", () => {
|
||||
expect(
|
||||
resolveIrcInboundTarget({
|
||||
target: "#openclaw",
|
||||
it.each([
|
||||
{
|
||||
label: "channel",
|
||||
serverTarget: "#openclaw",
|
||||
expected: { isGroup: true, target: "#openclaw", rawTarget: "#openclaw" },
|
||||
},
|
||||
{
|
||||
label: "DM",
|
||||
serverTarget: "openclaw-bot",
|
||||
expected: { isGroup: false, target: "alice", rawTarget: "openclaw-bot" },
|
||||
},
|
||||
])("maps $label targets through the monitor boundary", async ({ serverTarget, expected }) => {
|
||||
installMonitorRuntime();
|
||||
const server = await startInboundIrcServer(serverTarget);
|
||||
const messages: IrcInboundMessage[] = [];
|
||||
let monitor: { stop: () => void } | undefined;
|
||||
try {
|
||||
monitor = await monitorIrcProvider({
|
||||
config: {
|
||||
channels: {
|
||||
irc: {
|
||||
host: "127.0.0.1",
|
||||
port: server.port,
|
||||
tls: false,
|
||||
nick: "bot",
|
||||
username: "bot",
|
||||
realname: "OpenClaw",
|
||||
},
|
||||
},
|
||||
} as CoreConfig,
|
||||
onMessage: (message) => {
|
||||
messages.push(message);
|
||||
},
|
||||
});
|
||||
await waitForIrcCondition(() => messages.length === 1, "expected one inbound IRC message");
|
||||
expect(messages[0]).toMatchObject({
|
||||
...expected,
|
||||
senderNick: "alice",
|
||||
}),
|
||||
).toEqual({
|
||||
isGroup: true,
|
||||
target: "#openclaw",
|
||||
rawTarget: "#openclaw",
|
||||
});
|
||||
});
|
||||
|
||||
it("maps DM target to sender nick and preserves raw target", () => {
|
||||
expect(
|
||||
resolveIrcInboundTarget({
|
||||
target: "openclaw-bot",
|
||||
senderNick: "alice",
|
||||
}),
|
||||
).toEqual({
|
||||
isGroup: false,
|
||||
target: "alice",
|
||||
rawTarget: "openclaw-bot",
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to raw target when sender nick is empty", () => {
|
||||
expect(
|
||||
resolveIrcInboundTarget({
|
||||
target: "openclaw-bot",
|
||||
senderNick: " ",
|
||||
}),
|
||||
).toEqual({
|
||||
isGroup: false,
|
||||
target: "openclaw-bot",
|
||||
rawTarget: "openclaw-bot",
|
||||
});
|
||||
text: "hello",
|
||||
});
|
||||
} finally {
|
||||
monitor?.stop();
|
||||
await server.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ type IrcMonitorOptions = {
|
||||
|
||||
const IRC_MONITOR_RECONNECT_DELAY_MS = 1000;
|
||||
|
||||
export function resolveIrcInboundTarget(params: { target: string; senderNick: string }): {
|
||||
function resolveIrcInboundTarget(params: { target: string; senderNick: string }): {
|
||||
isGroup: boolean;
|
||||
target: string;
|
||||
rawTarget: string;
|
||||
|
||||
@@ -2,15 +2,9 @@
|
||||
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
||||
import type { PluginRuntime } from "./runtime-api.js";
|
||||
|
||||
const {
|
||||
setRuntime: setIrcRuntime,
|
||||
clearRuntime: clearStoredIrcRuntime,
|
||||
getRuntime: getIrcRuntime,
|
||||
} = createPluginRuntimeStore<PluginRuntime>({
|
||||
pluginId: "irc",
|
||||
errorMessage: "IRC runtime not initialized",
|
||||
});
|
||||
const { setRuntime: setIrcRuntime, getRuntime: getIrcRuntime } =
|
||||
createPluginRuntimeStore<PluginRuntime>({
|
||||
pluginId: "irc",
|
||||
errorMessage: "IRC runtime not initialized",
|
||||
});
|
||||
export { getIrcRuntime, setIrcRuntime };
|
||||
export function clearIrcRuntime() {
|
||||
clearStoredIrcRuntime();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Irc tests cover send plugin behavior.
|
||||
import { verifyChannelMessageAdapterCapabilityProofs } from "openclaw/plugin-sdk/channel-outbound";
|
||||
import { createSendCfgThreadingRuntime } from "openclaw/plugin-sdk/channel-test-helpers";
|
||||
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { IrcClient } from "./client.js";
|
||||
import { clearIrcRuntime, setIrcRuntime } from "./runtime.js";
|
||||
import { setIrcRuntime } from "./runtime.js";
|
||||
import type { CoreConfig } from "./types.js";
|
||||
|
||||
const hoisted = vi.hoisted(() => {
|
||||
@@ -95,10 +95,6 @@ describe("sendMessageIrc cfg threading", () => {
|
||||
setIrcRuntime(createSendCfgThreadingRuntime(hoisted) as never);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearIrcRuntime();
|
||||
});
|
||||
|
||||
it("uses explicitly provided cfg without loading runtime config", async () => {
|
||||
const providedCfg = {
|
||||
channels: {
|
||||
@@ -178,7 +174,7 @@ describe("sendMessageIrc cfg threading", () => {
|
||||
expect(hoisted.record).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sends with provided cfg even when the runtime store is not initialized", async () => {
|
||||
it("sends with provided cfg when runtime activity recording is unavailable", async () => {
|
||||
const providedCfg = {
|
||||
channels: {
|
||||
irc: {
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
type ResolvedIrcAccount,
|
||||
} from "./accounts.js";
|
||||
import { startIrcGatewayAccount } from "./gateway.js";
|
||||
import { clearIrcRuntime, setIrcRuntime } from "./runtime.js";
|
||||
import { setIrcRuntime } from "./runtime.js";
|
||||
import {
|
||||
ircSetupAdapter,
|
||||
parsePort,
|
||||
@@ -105,7 +105,6 @@ function installIrcRuntime() {
|
||||
describe("irc setup", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
clearIrcRuntime();
|
||||
});
|
||||
|
||||
it("parses valid ports and falls back for invalid values", () => {
|
||||
|
||||
@@ -10,8 +10,9 @@ import type {
|
||||
} from "openclaw/plugin-sdk/runtime-doctor";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { stateMigrations } from "./doctor-contract-api.js";
|
||||
import type { PersistedWorkboardCard } from "./src/persistence-types.js";
|
||||
import { createWorkboardSqliteStores } from "./src/sqlite-store.js";
|
||||
import { WorkboardStore, type PersistedWorkboardCard } from "./src/store.js";
|
||||
import { WorkboardStore } from "./src/store.js";
|
||||
|
||||
function createDoctorContext(env: NodeJS.ProcessEnv): PluginDoctorStateMigrationContext {
|
||||
return {
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import { Command } from "commander";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { registerWorkboardCli } from "./cli.js";
|
||||
import { WorkboardStore, type PersistedWorkboardCard, type WorkboardKeyedStore } from "./store.js";
|
||||
import type { PersistedWorkboardCard, WorkboardKeyedStore } from "./persistence-types.js";
|
||||
import { WorkboardStore } from "./store.js";
|
||||
|
||||
const gatewayRuntime = vi.hoisted(() => ({
|
||||
callGatewayFromCli: vi.fn(),
|
||||
|
||||
@@ -3,9 +3,9 @@ import { expectDefined } from "@openclaw/normalization-core";
|
||||
import type { OpenClawPluginCommandDefinition } from "openclaw/plugin-sdk/core";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawPluginApi } from "../api.js";
|
||||
import { handleWorkboardCommand, registerWorkboardCommand } from "./command.js";
|
||||
import type { WorkboardSubagentRuntime, WorkboardWorktreeRuntime } from "./dispatcher.js";
|
||||
import { WorkboardStore, type PersistedWorkboardCard, type WorkboardKeyedStore } from "./store.js";
|
||||
import { registerWorkboardCommand } from "./command.js";
|
||||
import type { PersistedWorkboardCard, WorkboardKeyedStore } from "./persistence-types.js";
|
||||
import { WorkboardStore } from "./store.js";
|
||||
import {
|
||||
resolveAgentWorkboardWorkspaceRuntime,
|
||||
resolveCommandWorkboardWorkspaceAccess,
|
||||
@@ -29,10 +29,9 @@ function createMemoryStore<T = PersistedWorkboardCard>(): WorkboardKeyedStore<T>
|
||||
};
|
||||
}
|
||||
|
||||
function createApi(run = vi.fn().mockResolvedValue({ runId: "run-1" })): {
|
||||
runtime: { subagent: WorkboardSubagentRuntime; worktrees: WorkboardWorktreeRuntime };
|
||||
} {
|
||||
function createApi(run = vi.fn().mockResolvedValue({ runId: "run-1" })): OpenClawPluginApi {
|
||||
return {
|
||||
registerCommand: vi.fn(),
|
||||
runtime: {
|
||||
subagent: { run },
|
||||
worktrees: {
|
||||
@@ -41,8 +40,46 @@ function createApi(run = vi.fn().mockResolvedValue({ runId: "run-1" })): {
|
||||
release: vi.fn(),
|
||||
removeIfLossless: vi.fn(),
|
||||
},
|
||||
sandbox: {
|
||||
resolveWorkspaceAuthority: vi.fn(() => ({
|
||||
sandboxed: false,
|
||||
workspaceAccess: "rw",
|
||||
})),
|
||||
prepareWorkspaceAuthority: vi.fn(async () => ({
|
||||
sandboxed: false,
|
||||
workspaceAccess: "rw",
|
||||
})),
|
||||
},
|
||||
},
|
||||
} as unknown as OpenClawPluginApi;
|
||||
}
|
||||
|
||||
async function runWorkboardCommand(params: {
|
||||
api: OpenClawPluginApi;
|
||||
store: WorkboardStore;
|
||||
args?: string;
|
||||
context?: {
|
||||
senderIsOwner?: boolean;
|
||||
gatewayClientScopes?: string[];
|
||||
config?: Record<string, unknown>;
|
||||
agentId?: string;
|
||||
sessionKey?: string;
|
||||
};
|
||||
}) {
|
||||
let command: OpenClawPluginCommandDefinition | undefined;
|
||||
vi.mocked(params.api.registerCommand).mockImplementationOnce((definition) => {
|
||||
command = definition;
|
||||
});
|
||||
registerWorkboardCommand({ api: params.api, store: params.store });
|
||||
return await expectDefined(command, "registered Workboard command").handler({
|
||||
channel: "test",
|
||||
isAuthorizedSender: true,
|
||||
commandBody: "/workboard",
|
||||
config: {},
|
||||
sessionKey: "agent:main:main",
|
||||
args: params.args,
|
||||
...params.context,
|
||||
} as never);
|
||||
}
|
||||
|
||||
async function createAmbiguousPrefix(store: WorkboardStore): Promise<string> {
|
||||
@@ -194,11 +231,11 @@ describe("handleWorkboardCommand", () => {
|
||||
const api = createApi();
|
||||
|
||||
await expect(
|
||||
handleWorkboardCommand({
|
||||
runWorkboardCommand({
|
||||
api,
|
||||
store,
|
||||
args: "create Ship CLI",
|
||||
senderIsOwner: true,
|
||||
context: { senderIsOwner: true },
|
||||
}),
|
||||
).resolves.toEqual(expect.objectContaining({ text: expect.stringContaining("Ship CLI") }));
|
||||
const card = expectDefined((await store.list())[0], "created workboard card");
|
||||
@@ -207,16 +244,16 @@ describe("handleWorkboardCommand", () => {
|
||||
metadata: { automation: { workspaceAccess: { unrestricted: true } } },
|
||||
});
|
||||
|
||||
await expect(handleWorkboardCommand({ api, store, args: "list" })).resolves.toEqual(
|
||||
await expect(runWorkboardCommand({ api, store, args: "list" })).resolves.toEqual(
|
||||
expect.objectContaining({ text: expect.stringContaining("Ship CLI") }),
|
||||
);
|
||||
await store.update(card.id, { status: "ready" });
|
||||
await expect(
|
||||
handleWorkboardCommand({
|
||||
runWorkboardCommand({
|
||||
api,
|
||||
store,
|
||||
args: "dispatch",
|
||||
gatewayClientScopes: ["operator.write"],
|
||||
context: { senderIsOwner: true },
|
||||
}),
|
||||
).resolves.toEqual(expect.objectContaining({ text: expect.stringContaining("started=1") }));
|
||||
expect(api.runtime.subagent.run).toHaveBeenCalledOnce();
|
||||
@@ -227,23 +264,23 @@ describe("handleWorkboardCommand", () => {
|
||||
const api = createApi();
|
||||
const card = await store.create({ title: "Ready worker", status: "ready" });
|
||||
|
||||
await expect(handleWorkboardCommand({ api, store, args: "list" })).resolves.toEqual(
|
||||
await expect(runWorkboardCommand({ api, store, args: "list" })).resolves.toEqual(
|
||||
expect.objectContaining({ text: expect.stringContaining("Ready worker") }),
|
||||
);
|
||||
await expect(handleWorkboardCommand({ api, store, args: "create Blocked" })).resolves.toEqual(
|
||||
await expect(runWorkboardCommand({ api, store, args: "create Blocked" })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
isError: true,
|
||||
text: expect.stringContaining("operator.write"),
|
||||
}),
|
||||
);
|
||||
await expect(handleWorkboardCommand({ api, store, args: "dispatch" })).resolves.toEqual(
|
||||
await expect(runWorkboardCommand({ api, store, args: "dispatch" })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
isError: true,
|
||||
text: expect.stringContaining("operator.write"),
|
||||
}),
|
||||
);
|
||||
await expect(
|
||||
handleWorkboardCommand({ api, store, args: `move ${card.id} --status running` }),
|
||||
runWorkboardCommand({ api, store, args: `move ${card.id} --status running` }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
isError: true,
|
||||
@@ -261,11 +298,11 @@ describe("handleWorkboardCommand", () => {
|
||||
await store.claim(card.id, { ownerId: "worker", token: "secret-token" });
|
||||
|
||||
await expect(
|
||||
handleWorkboardCommand({
|
||||
runWorkboardCommand({
|
||||
api,
|
||||
store,
|
||||
args: `move ${card.id.slice(0, 8)} --status review`,
|
||||
gatewayClientScopes: ["operator.write"],
|
||||
context: { gatewayClientScopes: ["operator.write"] },
|
||||
}),
|
||||
).resolves.toEqual(expect.objectContaining({ text: expect.stringContaining("review") }));
|
||||
await expect(store.get(card.id)).resolves.toMatchObject({
|
||||
@@ -280,11 +317,11 @@ describe("handleWorkboardCommand", () => {
|
||||
const card = await store.create({ title: "Invalid slash move" });
|
||||
|
||||
await expect(
|
||||
handleWorkboardCommand({
|
||||
runWorkboardCommand({
|
||||
api,
|
||||
store,
|
||||
args: `move ${card.id} --status later`,
|
||||
senderIsOwner: true,
|
||||
context: { senderIsOwner: true },
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({ isError: true, text: expect.stringContaining("status must be") }),
|
||||
@@ -306,13 +343,33 @@ describe("handleWorkboardCommand", () => {
|
||||
workspace: { kind: "worktree", path: "/repo-denied" },
|
||||
});
|
||||
|
||||
const restrictedConfig = {
|
||||
tools: { fs: { workspaceOnly: true } },
|
||||
agents: {
|
||||
list: [
|
||||
{ id: "main", default: true, workspace: "/workspace" },
|
||||
{ id: "restricted", workspace: "/workspace" },
|
||||
],
|
||||
},
|
||||
};
|
||||
vi.mocked(api.runtime.sandbox.resolveWorkspaceAuthority).mockReturnValue({
|
||||
sandboxed: true,
|
||||
workspaceAccess: "rw",
|
||||
});
|
||||
vi.mocked(api.runtime.sandbox.prepareWorkspaceAuthority).mockResolvedValue({
|
||||
sandboxed: true,
|
||||
workspaceAccess: "rw",
|
||||
});
|
||||
await expect(
|
||||
handleWorkboardCommand({
|
||||
runWorkboardCommand({
|
||||
api,
|
||||
store,
|
||||
args: "dispatch",
|
||||
gatewayClientScopes: ["operator.write"],
|
||||
workspaceAccess: { unrestricted: false, roots: ["/workspace"], writable: true },
|
||||
context: {
|
||||
gatewayClientScopes: ["operator.write"],
|
||||
config: restrictedConfig,
|
||||
agentId: "main",
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({ text: expect.stringContaining("outside the caller") }),
|
||||
@@ -328,17 +385,15 @@ describe("handleWorkboardCommand", () => {
|
||||
agentId: "restricted",
|
||||
workspace: { kind: "worktree", path: "/workspace" },
|
||||
});
|
||||
await handleWorkboardCommand({
|
||||
await runWorkboardCommand({
|
||||
api,
|
||||
store,
|
||||
args: "dispatch",
|
||||
senderIsOwner: true,
|
||||
resolveAgentWorkspace: () => "/workspace",
|
||||
resolveAgentWorkspaceRuntime: () => ({
|
||||
sandboxed: true,
|
||||
workspaceAccess: { unrestricted: false, roots: ["/workspace"], writable: true },
|
||||
}),
|
||||
workspaceAccess: { unrestricted: false, roots: ["/workspace"], writable: true },
|
||||
context: {
|
||||
senderIsOwner: true,
|
||||
config: restrictedConfig,
|
||||
agentId: "main",
|
||||
},
|
||||
});
|
||||
expect(createWorktree).not.toHaveBeenCalled();
|
||||
expect(api.runtime.subagent.run).toHaveBeenCalledWith(
|
||||
@@ -355,12 +410,23 @@ describe("handleWorkboardCommand", () => {
|
||||
workspace: { kind: "worktree", path: "/repo-allowed" },
|
||||
workspaceAccess: { unrestricted: true },
|
||||
});
|
||||
await handleWorkboardCommand({
|
||||
vi.mocked(api.runtime.sandbox.resolveWorkspaceAuthority).mockReturnValue({
|
||||
sandboxed: false,
|
||||
workspaceAccess: "rw",
|
||||
});
|
||||
vi.mocked(api.runtime.sandbox.prepareWorkspaceAuthority).mockResolvedValue({
|
||||
sandboxed: false,
|
||||
workspaceAccess: "rw",
|
||||
});
|
||||
await runWorkboardCommand({
|
||||
api,
|
||||
store,
|
||||
args: "dispatch",
|
||||
gatewayClientScopes: ["operator.admin"],
|
||||
workspaceAccess: { unrestricted: true },
|
||||
context: {
|
||||
gatewayClientScopes: ["operator.admin"],
|
||||
config: { agents: { list: [{ id: "admin", default: true, workspace: "/repo-allowed" }] } },
|
||||
agentId: "admin",
|
||||
},
|
||||
});
|
||||
|
||||
expect(createWorktree).toHaveBeenCalledWith(
|
||||
@@ -376,7 +442,7 @@ describe("handleWorkboardCommand", () => {
|
||||
const api = createApi();
|
||||
const prefix = await createAmbiguousPrefix(store);
|
||||
|
||||
await expect(handleWorkboardCommand({ api, store, args: `show ${prefix}` })).resolves.toEqual(
|
||||
await expect(runWorkboardCommand({ api, store, args: `show ${prefix}` })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
isError: true,
|
||||
text: expect.stringContaining("Ambiguous card id prefix"),
|
||||
|
||||
@@ -96,7 +96,7 @@ function requireWriteAccess(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export async function handleWorkboardCommand(params: {
|
||||
async function handleWorkboardCommand(params: {
|
||||
api: WorkboardCommandApi;
|
||||
store: WorkboardStore;
|
||||
args?: string;
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { cleanupWorkboardRunWorktree } from "./dispatcher-workspace.js";
|
||||
import { dispatchAndStartWorkboardCards } from "./dispatcher.js";
|
||||
import { WorkboardStore, type PersistedWorkboardCard, type WorkboardKeyedStore } from "./store.js";
|
||||
import type { PersistedWorkboardCard, WorkboardKeyedStore } from "./persistence-types.js";
|
||||
import { WorkboardStore } from "./store.js";
|
||||
|
||||
function createMemoryStore<T = PersistedWorkboardCard>(): WorkboardKeyedStore<T> {
|
||||
const entries = new Map<string, T>();
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawPluginApi } from "../api.js";
|
||||
import { registerWorkboardGatewayMethods } from "./gateway.js";
|
||||
import { WorkboardStore, type PersistedWorkboardCard, type WorkboardKeyedStore } from "./store.js";
|
||||
import type { PersistedWorkboardCard, WorkboardKeyedStore } from "./persistence-types.js";
|
||||
import { WorkboardStore } from "./store.js";
|
||||
|
||||
function createMemoryStore<T = PersistedWorkboardCard>(): WorkboardKeyedStore<T> {
|
||||
const entries = new Map<string, T>();
|
||||
|
||||
@@ -5,15 +5,15 @@ import path from "node:path";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { MAX_DATE_TIMESTAMP_MS } from "openclaw/plugin-sdk/number-runtime";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type {
|
||||
PersistedWorkboardAttachment,
|
||||
PersistedWorkboardBoard,
|
||||
PersistedWorkboardCard,
|
||||
PersistedWorkboardNotificationSubscription,
|
||||
WorkboardKeyedStore,
|
||||
} from "./persistence-types.js";
|
||||
import { createWorkboardSqliteStores } from "./sqlite-store.js";
|
||||
import {
|
||||
WorkboardStore,
|
||||
type PersistedWorkboardAttachment,
|
||||
type PersistedWorkboardBoard,
|
||||
type PersistedWorkboardCard,
|
||||
type PersistedWorkboardNotificationSubscription,
|
||||
type WorkboardKeyedStore,
|
||||
} from "./store.js";
|
||||
import { WorkboardStore } from "./store.js";
|
||||
|
||||
function createMemoryStore<T = PersistedWorkboardCard>(options?: {
|
||||
beforeRegister?: (key: string, value: T) => Promise<void> | void;
|
||||
|
||||
@@ -41,13 +41,6 @@ import {
|
||||
} from "./store-normalizers.js";
|
||||
import { WorkboardNotificationStore } from "./store-notifications.js";
|
||||
|
||||
export type {
|
||||
PersistedWorkboardAttachment,
|
||||
PersistedWorkboardBoard,
|
||||
PersistedWorkboardCard,
|
||||
PersistedWorkboardNotificationSubscription,
|
||||
WorkboardKeyedStore,
|
||||
} from "./persistence-types.js";
|
||||
export type { WorkboardDispatchResult } from "./store-inputs.js";
|
||||
|
||||
// Capability layers split review boundaries only; the core still owns persistence and mutation order.
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawPluginApi } from "../api.js";
|
||||
import { WorkboardStore, type PersistedWorkboardCard, type WorkboardKeyedStore } from "./store.js";
|
||||
import type { PersistedWorkboardCard, WorkboardKeyedStore } from "./persistence-types.js";
|
||||
import { WorkboardStore } from "./store.js";
|
||||
import { createWorkboardTools } from "./tools.js";
|
||||
import { guardWorkboardToolsForWorkspaceAccess } from "./workspace-access.js";
|
||||
|
||||
|
||||
@@ -42,12 +42,6 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [
|
||||
"extensions/googlechat/src/monitor.ts: testing",
|
||||
"extensions/googlechat/src/targets.ts: resolveGoogleChatSpaceChatType",
|
||||
"extensions/imessage/src/monitor-reply-cache.ts: resetIMessageShortIdState",
|
||||
"extensions/irc/src/client.ts: buildFallbackNick",
|
||||
"extensions/irc/src/client.ts: buildIrcNickServCommands",
|
||||
"extensions/irc/src/config-schema.ts: IrcConfigSchema",
|
||||
"extensions/irc/src/control-chars.ts: isIrcControlChar",
|
||||
"extensions/irc/src/monitor.ts: resolveIrcInboundTarget",
|
||||
"extensions/irc/src/runtime.ts: clearIrcRuntime",
|
||||
"extensions/linux-node/src/commands.ts: LinuxNodeCommandDeps",
|
||||
"extensions/linux-node/src/commands.ts: listLinuxVideoDevices",
|
||||
"extensions/linux-node/src/commands.ts: MAX_MEDIA_RAW_BYTES",
|
||||
@@ -137,12 +131,6 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [
|
||||
"extensions/voice-call/src/cli.ts: testing",
|
||||
"extensions/voice-call/src/runtime-state.ts: clearVoiceCallStateRuntime",
|
||||
"extensions/whatsapp/src/auto-reply/monitor/group-gating.ts: resetGroupDropWarningsForTests",
|
||||
"extensions/workboard/src/command.ts: handleWorkboardCommand",
|
||||
"extensions/workboard/src/store.ts: PersistedWorkboardAttachment",
|
||||
"extensions/workboard/src/store.ts: PersistedWorkboardBoard",
|
||||
"extensions/workboard/src/store.ts: PersistedWorkboardCard",
|
||||
"extensions/workboard/src/store.ts: PersistedWorkboardNotificationSubscription",
|
||||
"extensions/workboard/src/store.ts: WorkboardKeyedStore",
|
||||
"extensions/workspaces/src/broadcast.ts: resetWorkspaceBroadcastForTest",
|
||||
"extensions/workspaces/src/cli.ts: parseWorkspaceBindingShorthand",
|
||||
"extensions/workspaces/src/manifest.ts: loadWidgetManifest",
|
||||
|
||||
Reference in New Issue
Block a user