mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-23 22:55:24 +00:00
test: tighten regression assertions across extension tests
This commit is contained in:
@@ -1,6 +1,22 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { imessagePlugin } from "./channel.js";
|
||||
|
||||
function requireIMessageSendText() {
|
||||
const sendText = imessagePlugin.outbound?.sendText;
|
||||
if (!sendText) {
|
||||
throw new Error("imessage outbound.sendText unavailable");
|
||||
}
|
||||
return sendText;
|
||||
}
|
||||
|
||||
function requireIMessageSendMedia() {
|
||||
const sendMedia = imessagePlugin.outbound?.sendMedia;
|
||||
if (!sendMedia) {
|
||||
throw new Error("imessage outbound.sendMedia unavailable");
|
||||
}
|
||||
return sendMedia;
|
||||
}
|
||||
|
||||
describe("imessagePlugin outbound", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
@@ -12,10 +28,9 @@ describe("imessagePlugin outbound", () => {
|
||||
|
||||
it("forwards replyToId on direct sendText adapter path", async () => {
|
||||
const sendIMessage = vi.fn().mockResolvedValue({ messageId: "m-text" });
|
||||
const sendText = imessagePlugin.outbound?.sendText;
|
||||
expect(sendText).toBeDefined();
|
||||
const sendText = requireIMessageSendText();
|
||||
|
||||
const result = await sendText!({
|
||||
const result = await sendText({
|
||||
cfg,
|
||||
to: "chat_id:12",
|
||||
text: "hello",
|
||||
@@ -38,10 +53,9 @@ describe("imessagePlugin outbound", () => {
|
||||
|
||||
it("forwards replyToId on direct sendMedia adapter path", async () => {
|
||||
const sendIMessage = vi.fn().mockResolvedValue({ messageId: "m-media" });
|
||||
const sendMedia = imessagePlugin.outbound?.sendMedia;
|
||||
expect(sendMedia).toBeDefined();
|
||||
const sendMedia = requireIMessageSendMedia();
|
||||
|
||||
const result = await sendMedia!({
|
||||
const result = await sendMedia({
|
||||
cfg,
|
||||
to: "chat_id:77",
|
||||
text: "caption",
|
||||
@@ -66,11 +80,10 @@ describe("imessagePlugin outbound", () => {
|
||||
|
||||
it("forwards mediaLocalRoots on direct sendMedia adapter path", async () => {
|
||||
const sendIMessage = vi.fn().mockResolvedValue({ messageId: "m-media-local" });
|
||||
const sendMedia = imessagePlugin.outbound?.sendMedia;
|
||||
expect(sendMedia).toBeDefined();
|
||||
const sendMedia = requireIMessageSendMedia();
|
||||
const mediaLocalRoots = ["/tmp/workspace"];
|
||||
|
||||
const result = await sendMedia!({
|
||||
const result = await sendMedia({
|
||||
cfg,
|
||||
to: "chat_id:88",
|
||||
text: "caption",
|
||||
|
||||
@@ -66,16 +66,12 @@ describe("memory plugin e2e", () => {
|
||||
}) as MemoryPluginTestConfig | undefined;
|
||||
}
|
||||
|
||||
test("memory plugin registers and initializes correctly", async () => {
|
||||
// Dynamic import to avoid loading LanceDB when not testing
|
||||
test("memory plugin exports stable metadata", async () => {
|
||||
const { default: memoryPlugin } = await import("./index.js");
|
||||
|
||||
expect(memoryPlugin.id).toBe("memory-lancedb");
|
||||
expect(memoryPlugin.name).toBe("Memory (LanceDB)");
|
||||
expect(memoryPlugin.kind).toBe("memory");
|
||||
expect(memoryPlugin.configSchema).toBeDefined();
|
||||
// oxlint-disable-next-line typescript/unbound-method
|
||||
expect(memoryPlugin.register).toBeInstanceOf(Function);
|
||||
});
|
||||
|
||||
test("config schema parses valid config", async () => {
|
||||
@@ -84,7 +80,6 @@ describe("memory plugin e2e", () => {
|
||||
autoRecall: true,
|
||||
});
|
||||
|
||||
expect(config).toBeDefined();
|
||||
expect(config?.embedding?.apiKey).toBe(OPENAI_API_KEY);
|
||||
expect(config?.dbPath).toBe(getDbPath());
|
||||
expect(config?.captureMaxChars).toBe(500);
|
||||
@@ -214,7 +209,9 @@ describe("memory plugin e2e", () => {
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
memoryPlugin.register(mockApi as any);
|
||||
const recallTool = registeredTools.find((t) => t.opts?.name === "memory_recall")?.tool;
|
||||
expect(recallTool).toBeDefined();
|
||||
if (!recallTool) {
|
||||
throw new Error("memory_recall tool was not registered");
|
||||
}
|
||||
await recallTool.execute("test-call-dims", { query: "hello dimensions" });
|
||||
|
||||
expect(embeddingsCreate).toHaveBeenCalledWith({
|
||||
@@ -375,8 +372,8 @@ describeLive("memory plugin live tests", () => {
|
||||
});
|
||||
|
||||
expect(storeResult.details?.action).toBe("created");
|
||||
expect(storeResult.details?.id).toBeDefined();
|
||||
const storedId = storeResult.details?.id;
|
||||
expect(storedId).toMatch(/.+/);
|
||||
|
||||
// Test recall
|
||||
const recallResult = await recallTool.execute("test-call-2", {
|
||||
|
||||
@@ -65,8 +65,9 @@ describeLive("openrouter plugin live", () => {
|
||||
const provider =
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
providers.find((entry) => (entry as any).id === "openrouter");
|
||||
|
||||
expect(provider).toBeDefined();
|
||||
if (!provider) {
|
||||
throw new Error("openrouter provider was not registered");
|
||||
}
|
||||
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
const resolved = (provider as any).resolveDynamicModel?.({
|
||||
|
||||
@@ -11,7 +11,7 @@ describe("signalPlugin outbound sendMedia", () => {
|
||||
throw new Error("signal outbound sendMedia is unavailable");
|
||||
}
|
||||
|
||||
await sendMedia({
|
||||
const result = await sendMedia({
|
||||
cfg: {} as never,
|
||||
to: "signal:+15551234567",
|
||||
text: "photo",
|
||||
@@ -30,6 +30,7 @@ describe("signalPlugin outbound sendMedia", () => {
|
||||
accountId: "default",
|
||||
}),
|
||||
);
|
||||
expect(result).toEqual({ channel: "signal", messageId: "m1" });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -54,7 +54,6 @@ describe("Synology channel wiring integration", () => {
|
||||
const registered = firstCall[0];
|
||||
expect(registered.path).toBe("/webhook/synology-alerts");
|
||||
expect(registered.accountId).toBe("alerts");
|
||||
expect(typeof registered.handler).toBe("function");
|
||||
|
||||
const req = makeReq(
|
||||
"POST",
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { makeSecurityAccount, registerPluginHttpRouteMock } from "./channel.test-mocks.js";
|
||||
import { sendMessage } from "./client.js";
|
||||
|
||||
vi.mock("./webhook-handler.js", () => ({
|
||||
createWebhookHandler: vi.fn(() => vi.fn()),
|
||||
}));
|
||||
|
||||
const { createSynologyChatPlugin } = await import("./channel.js");
|
||||
const mockSendMessage = vi.mocked(sendMessage);
|
||||
|
||||
describe("createSynologyChatPlugin", () => {
|
||||
beforeEach(() => {
|
||||
mockSendMessage.mockClear();
|
||||
});
|
||||
|
||||
describe("meta", () => {
|
||||
it("has correct id and label", () => {
|
||||
const plugin = createSynologyChatPlugin();
|
||||
@@ -27,17 +33,52 @@ describe("createSynologyChatPlugin", () => {
|
||||
});
|
||||
|
||||
describe("config", () => {
|
||||
it("listAccountIds delegates to accounts module", () => {
|
||||
it("listAccountIds includes default and named accounts when configured", () => {
|
||||
const plugin = createSynologyChatPlugin();
|
||||
const result = plugin.config.listAccountIds({});
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
const result = plugin.config.listAccountIds({
|
||||
channels: {
|
||||
"synology-chat": {
|
||||
token: "base-token",
|
||||
accounts: {
|
||||
office: { token: "office-token" },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result).toEqual(["default", "office"]);
|
||||
});
|
||||
|
||||
it("resolveAccount returns account config", () => {
|
||||
const cfg = { channels: { "synology-chat": { token: "t1" } } };
|
||||
it("resolveAccount merges account overrides with base config defaults", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
"synology-chat": {
|
||||
token: "base-token",
|
||||
incomingUrl: "https://nas/base",
|
||||
nasHost: "nas-base",
|
||||
allowedUserIds: ["base-user"],
|
||||
rateLimitPerMinute: 45,
|
||||
botName: "Base Bot",
|
||||
accounts: {
|
||||
office: {
|
||||
token: "office-token",
|
||||
allowInsecureSsl: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const plugin = createSynologyChatPlugin();
|
||||
const account = plugin.config.resolveAccount(cfg, "default");
|
||||
expect(account.accountId).toBe("default");
|
||||
const account = plugin.config.resolveAccount(cfg, "office");
|
||||
expect(account).toMatchObject({
|
||||
accountId: "office",
|
||||
token: "office-token",
|
||||
incomingUrl: "https://nas/base",
|
||||
nasHost: "nas-base",
|
||||
allowedUserIds: ["base-user"],
|
||||
rateLimitPerMinute: 45,
|
||||
botName: "Base Bot",
|
||||
allowInsecureSsl: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("defaultAccountId returns 'default'", () => {
|
||||
@@ -73,23 +114,45 @@ describe("createSynologyChatPlugin", () => {
|
||||
allowInsecureSsl: true,
|
||||
};
|
||||
const result = plugin.security.resolveDmPolicy({ cfg: {}, account });
|
||||
if (!result) {
|
||||
throw new Error("resolveDmPolicy returned null");
|
||||
}
|
||||
expect(result.policy).toBe("allowlist");
|
||||
expect(result.allowFrom).toEqual(["user1"]);
|
||||
expect(typeof result.normalizeEntry).toBe("function");
|
||||
expect(result.normalizeEntry?.(" USER1 ")).toBe("user1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("pairing", () => {
|
||||
it("has notifyApproval and normalizeAllowEntry", () => {
|
||||
it("normalizes entries and notifies approved users", async () => {
|
||||
const plugin = createSynologyChatPlugin();
|
||||
expect(plugin.pairing.idLabel).toBe("synologyChatUserId");
|
||||
const normalize = plugin.pairing.normalizeAllowEntry;
|
||||
expect(typeof normalize).toBe("function");
|
||||
if (normalize) {
|
||||
expect(normalize(" USER1 ")).toBe("user1");
|
||||
const notifyApproval = plugin.pairing.notifyApproval;
|
||||
if (!normalize || !notifyApproval) {
|
||||
throw new Error("synology-chat pairing helpers unavailable");
|
||||
}
|
||||
expect(typeof plugin.pairing.notifyApproval).toBe("function");
|
||||
expect(normalize(" USER1 ")).toBe("user1");
|
||||
|
||||
await notifyApproval({
|
||||
cfg: {
|
||||
channels: {
|
||||
"synology-chat": {
|
||||
token: "t",
|
||||
incomingUrl: "https://nas/incoming",
|
||||
allowInsecureSsl: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
id: "USER1",
|
||||
});
|
||||
|
||||
expect(mockSendMessage).toHaveBeenCalledWith(
|
||||
"https://nas/incoming",
|
||||
"OpenClaw: your access has been approved.",
|
||||
"USER1",
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -161,9 +224,9 @@ describe("createSynologyChatPlugin", () => {
|
||||
it("returns formatting hints", () => {
|
||||
const plugin = createSynologyChatPlugin();
|
||||
const hints = plugin.agentPrompt.messageToolHints();
|
||||
expect(Array.isArray(hints)).toBe(true);
|
||||
expect(hints.length).toBeGreaterThan(5);
|
||||
expect(hints.some((h: string) => h.includes("<URL|display text>"))).toBe(true);
|
||||
expect(hints).toContain("### Synology Chat Formatting");
|
||||
expect(hints).toContain("**Links**: Use `<URL|display text>` to create clickable links.");
|
||||
expect(hints).toContain("- No buttons, cards, or interactive elements");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -199,9 +262,11 @@ describe("createSynologyChatPlugin", () => {
|
||||
text: "hello",
|
||||
to: "user1",
|
||||
});
|
||||
expect(result.channel).toBe("synology-chat");
|
||||
expect(result.messageId).toBeDefined();
|
||||
expect(result.chatId).toBe("user1");
|
||||
expect(result).toMatchObject({
|
||||
channel: "synology-chat",
|
||||
chatId: "user1",
|
||||
});
|
||||
expect(result.messageId).toMatch(/^sc-\d+$/);
|
||||
});
|
||||
|
||||
it("sendMedia throws when missing incomingUrl", async () => {
|
||||
|
||||
@@ -2,12 +2,6 @@ import { describe, expect, it } from "vitest";
|
||||
import plugin from "./index.js";
|
||||
|
||||
describe("tavily plugin", () => {
|
||||
it("exports a valid plugin entry with correct id and name", () => {
|
||||
expect(plugin.id).toBe("tavily");
|
||||
expect(plugin.name).toBe("Tavily Plugin");
|
||||
expect(typeof plugin.register).toBe("function");
|
||||
});
|
||||
|
||||
it("registers web search provider and two tools", () => {
|
||||
const registrations: {
|
||||
webSearchProviders: unknown[];
|
||||
@@ -26,6 +20,8 @@ describe("tavily plugin", () => {
|
||||
|
||||
plugin.register(mockApi as never);
|
||||
|
||||
expect(plugin.id).toBe("tavily");
|
||||
expect(plugin.name).toBe("Tavily Plugin");
|
||||
expect(registrations.webSearchProviders).toHaveLength(1);
|
||||
expect(registrations.tools).toHaveLength(2);
|
||||
|
||||
|
||||
@@ -79,9 +79,13 @@ describe("outbound", () => {
|
||||
expect(twitchOutbound.textChunkLimit).toBe(500);
|
||||
});
|
||||
|
||||
it("should have chunker function", () => {
|
||||
expect(twitchOutbound.chunker).toBeDefined();
|
||||
expect(typeof twitchOutbound.chunker).toBe("function");
|
||||
it("should chunk long messages at 500 characters", () => {
|
||||
const chunker = twitchOutbound.chunker;
|
||||
if (!chunker) {
|
||||
throw new Error("twitch outbound.chunker unavailable");
|
||||
}
|
||||
|
||||
expect(chunker("a".repeat(600), 500)).toEqual(["a".repeat(500), "a".repeat(100)]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -113,9 +113,12 @@ describe("setup surface helpers", () => {
|
||||
expect(result).toBe("oauth:test123");
|
||||
|
||||
// Test the validate function
|
||||
expect(capturedValidate).toBeDefined();
|
||||
expect(capturedValidate!("")).toBe("Required");
|
||||
expect(capturedValidate!("notoauth")).toBe("Token should start with 'oauth:'");
|
||||
if (!capturedValidate) {
|
||||
throw new Error("promptToken validate callback was not captured");
|
||||
}
|
||||
expect(capturedValidate("")).toBe("Required");
|
||||
expect(capturedValidate("notoauth")).toBe("Token should start with 'oauth:'");
|
||||
expect(capturedValidate("oauth:goodtoken")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should return early when no existing token and no env token", async () => {
|
||||
|
||||
@@ -50,8 +50,12 @@ describe("status", () => {
|
||||
const issues = collectTwitchStatusIssues(snapshots);
|
||||
|
||||
expect(issues.length).toBeGreaterThan(0);
|
||||
const disabledIssue = issues.find((i) => i.message.includes("disabled"));
|
||||
expect(disabledIssue).toBeDefined();
|
||||
expect(issues).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "config",
|
||||
message: "Twitch account is disabled",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should detect missing clientId when account configured (simplified config)", () => {
|
||||
@@ -64,8 +68,12 @@ describe("status", () => {
|
||||
|
||||
const issues = collectTwitchStatusIssues(snapshots, () => mockCfg as never);
|
||||
|
||||
const clientIdIssue = issues.find((i) => i.message.includes("client ID"));
|
||||
expect(clientIdIssue).toBeDefined();
|
||||
expect(issues).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "config",
|
||||
message: "Twitch client ID is required",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should warn about oauth: prefix in token (simplified config)", () => {
|
||||
@@ -78,9 +86,12 @@ describe("status", () => {
|
||||
|
||||
const issues = collectTwitchStatusIssues(snapshots, () => mockCfg as never);
|
||||
|
||||
const prefixIssue = issues.find((i) => i.message.includes("oauth:"));
|
||||
expect(prefixIssue).toBeDefined();
|
||||
expect(prefixIssue?.kind).toBe("config");
|
||||
expect(issues).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "config",
|
||||
message: "Token contains 'oauth:' prefix (will be stripped)",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should detect clientSecret without refreshToken (simplified config)", () => {
|
||||
@@ -95,8 +106,12 @@ describe("status", () => {
|
||||
|
||||
const issues = collectTwitchStatusIssues(snapshots, () => mockCfg as never);
|
||||
|
||||
const secretIssue = issues.find((i) => i.message.includes("clientSecret"));
|
||||
expect(secretIssue).toBeDefined();
|
||||
expect(issues).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "config",
|
||||
message: "clientSecret provided without refreshToken",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should detect empty allowFrom array (simplified config)", () => {
|
||||
@@ -110,8 +125,12 @@ describe("status", () => {
|
||||
|
||||
const issues = collectTwitchStatusIssues(snapshots, () => mockCfg as never);
|
||||
|
||||
const allowFromIssue = issues.find((i) => i.message.includes("allowFrom"));
|
||||
expect(allowFromIssue).toBeDefined();
|
||||
expect(issues).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "config",
|
||||
message: "allowFrom is configured but empty",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should detect allowedRoles 'all' with allowFrom conflict (simplified config)", () => {
|
||||
@@ -126,9 +145,12 @@ describe("status", () => {
|
||||
|
||||
const issues = collectTwitchStatusIssues(snapshots, () => mockCfg as never);
|
||||
|
||||
const conflictIssue = issues.find((i) => i.kind === "intent");
|
||||
expect(conflictIssue).toBeDefined();
|
||||
expect(conflictIssue?.message).toContain("allowedRoles is set to 'all'");
|
||||
expect(issues).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "intent",
|
||||
message: "allowedRoles is set to 'all' but allowFrom is also configured",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should detect runtime errors", () => {
|
||||
@@ -138,9 +160,12 @@ describe("status", () => {
|
||||
|
||||
const issues = collectTwitchStatusIssues(snapshots);
|
||||
|
||||
const runtimeIssue = issues.find((i) => i.kind === "runtime");
|
||||
expect(runtimeIssue).toBeDefined();
|
||||
expect(runtimeIssue?.message).toContain("Connection timeout");
|
||||
expect(issues).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "runtime",
|
||||
message: "Last error: Connection timeout",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should detect accounts that never connected", () => {
|
||||
@@ -154,10 +179,12 @@ describe("status", () => {
|
||||
|
||||
const issues = collectTwitchStatusIssues(snapshots);
|
||||
|
||||
const neverConnectedIssue = issues.find((i) =>
|
||||
i.message.includes("never connected successfully"),
|
||||
expect(issues).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "runtime",
|
||||
message: "Account has never connected successfully",
|
||||
}),
|
||||
);
|
||||
expect(neverConnectedIssue).toBeDefined();
|
||||
});
|
||||
|
||||
it("should detect long-running connections", () => {
|
||||
@@ -172,8 +199,12 @@ describe("status", () => {
|
||||
|
||||
const issues = collectTwitchStatusIssues(snapshots);
|
||||
|
||||
const uptimeIssue = issues.find((i) => i.message.includes("running for"));
|
||||
expect(uptimeIssue).toBeDefined();
|
||||
expect(issues).toContainEqual(
|
||||
expect.objectContaining({
|
||||
kind: "runtime",
|
||||
message: "Connection has been running for 8 days",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle empty snapshots array", () => {
|
||||
@@ -194,8 +225,13 @@ describe("status", () => {
|
||||
|
||||
const issues = collectTwitchStatusIssues(snapshots);
|
||||
|
||||
// Should not crash, may return empty or minimal issues
|
||||
expect(Array.isArray(issues)).toBe(true);
|
||||
expect(issues).toEqual([
|
||||
expect.objectContaining({
|
||||
accountId: "unknown",
|
||||
kind: "config",
|
||||
message: "Twitch account is not properly configured",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -348,8 +348,10 @@ describe("TwitchClientManager", () => {
|
||||
it("should send message successfully", async () => {
|
||||
const result = await manager.sendMessage(testAccount, "testchannel", "Hello, world!");
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.messageId).toBeDefined();
|
||||
expect(result).toMatchObject({ ok: true });
|
||||
expect(result.messageId).toMatch(
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
||||
);
|
||||
expect(mockSay).toHaveBeenCalledWith("testchannel", "Hello, world!");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user