test: tighten regression assertions across extension tests

This commit is contained in:
Peter Steinberger
2026-03-22 07:46:07 +00:00
parent 4becbc8b25
commit 2d492ab534
11 changed files with 195 additions and 78 deletions

View File

@@ -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",

View File

@@ -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", {

View File

@@ -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?.({

View File

@@ -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" });
});
});

View File

@@ -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",

View File

@@ -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 () => {

View File

@@ -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);

View File

@@ -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)]);
});
});

View File

@@ -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 () => {

View File

@@ -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",
}),
]);
});
});
});

View File

@@ -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!");
});