mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
refactor(tests): share harnesses for cli and monitor fixtures
This commit is contained in:
@@ -27,6 +27,25 @@ vi.mock("qrcode-terminal", () => ({
|
||||
|
||||
const { registerQrCli } = await import("./qr-cli.js");
|
||||
|
||||
function createRemoteQrConfig(params?: { withTailscale?: boolean }) {
|
||||
return {
|
||||
gateway: {
|
||||
...(params?.withTailscale ? { tailscale: { mode: "serve" } } : {}),
|
||||
remote: { url: "wss://remote.example.com:444", token: "remote-tok" },
|
||||
auth: { mode: "token", token: "local-tok" },
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
"device-pair": {
|
||||
config: {
|
||||
publicUrl: "wss://wrong.example.com:443",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("registerQrCli", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -124,21 +143,7 @@ describe("registerQrCli", () => {
|
||||
});
|
||||
|
||||
it("uses gateway.remote.url when --remote is set (ignores device-pair publicUrl)", async () => {
|
||||
loadConfig.mockReturnValue({
|
||||
gateway: {
|
||||
remote: { url: "wss://remote.example.com:444", token: "remote-tok" },
|
||||
auth: { mode: "token", token: "local-tok" },
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
"device-pair": {
|
||||
config: {
|
||||
publicUrl: "wss://wrong.example.com:443",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
loadConfig.mockReturnValue(createRemoteQrConfig());
|
||||
|
||||
const program = new Command();
|
||||
registerQrCli(program);
|
||||
@@ -152,21 +157,7 @@ describe("registerQrCli", () => {
|
||||
});
|
||||
|
||||
it("reports gateway.remote.url as source in --remote json output", async () => {
|
||||
loadConfig.mockReturnValue({
|
||||
gateway: {
|
||||
remote: { url: "wss://remote.example.com:444", token: "remote-tok" },
|
||||
auth: { mode: "token", token: "local-tok" },
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
"device-pair": {
|
||||
config: {
|
||||
publicUrl: "wss://wrong.example.com:443",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
loadConfig.mockReturnValue(createRemoteQrConfig());
|
||||
|
||||
const program = new Command();
|
||||
registerQrCli(program);
|
||||
@@ -202,22 +193,7 @@ describe("registerQrCli", () => {
|
||||
});
|
||||
|
||||
it("prefers gateway.remote.url over tailscale when --remote is set", async () => {
|
||||
loadConfig.mockReturnValue({
|
||||
gateway: {
|
||||
tailscale: { mode: "serve" },
|
||||
remote: { url: "wss://remote.example.com:444", token: "remote-tok" },
|
||||
auth: { mode: "token", token: "local-tok" },
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
"device-pair": {
|
||||
config: {
|
||||
publicUrl: "wss://wrong.example.com:443",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
loadConfig.mockReturnValue(createRemoteQrConfig({ withTailscale: true }));
|
||||
runCommandWithTimeout.mockResolvedValue({
|
||||
code: 0,
|
||||
stdout: '{"Self":{"DNSName":"ts-host.tailnet.ts.net."}}',
|
||||
|
||||
@@ -38,6 +38,22 @@ const makeEntries = (
|
||||
return out;
|
||||
};
|
||||
|
||||
function createAutoThreadMentionContext() {
|
||||
const guildInfo: DiscordGuildEntryResolved = {
|
||||
requireMention: true,
|
||||
channels: {
|
||||
general: { allow: true, autoThread: true },
|
||||
},
|
||||
};
|
||||
const channelConfig = resolveDiscordChannelConfig({
|
||||
guildInfo,
|
||||
channelId: "1",
|
||||
channelName: "General",
|
||||
channelSlug: "general",
|
||||
});
|
||||
return { guildInfo, channelConfig };
|
||||
}
|
||||
|
||||
describe("registerDiscordListener", () => {
|
||||
class FakeListener {}
|
||||
|
||||
@@ -402,18 +418,7 @@ describe("discord mention gating", () => {
|
||||
});
|
||||
|
||||
it("does not require mention inside autoThread threads", () => {
|
||||
const guildInfo: DiscordGuildEntryResolved = {
|
||||
requireMention: true,
|
||||
channels: {
|
||||
general: { allow: true, autoThread: true },
|
||||
},
|
||||
};
|
||||
const channelConfig = resolveDiscordChannelConfig({
|
||||
guildInfo,
|
||||
channelId: "1",
|
||||
channelName: "General",
|
||||
channelSlug: "general",
|
||||
});
|
||||
const { guildInfo, channelConfig } = createAutoThreadMentionContext();
|
||||
expect(
|
||||
resolveDiscordShouldRequireMention({
|
||||
isGuildMessage: true,
|
||||
@@ -427,18 +432,7 @@ describe("discord mention gating", () => {
|
||||
});
|
||||
|
||||
it("requires mention inside user-created threads with autoThread enabled", () => {
|
||||
const guildInfo: DiscordGuildEntryResolved = {
|
||||
requireMention: true,
|
||||
channels: {
|
||||
general: { allow: true, autoThread: true },
|
||||
},
|
||||
};
|
||||
const channelConfig = resolveDiscordChannelConfig({
|
||||
guildInfo,
|
||||
channelId: "1",
|
||||
channelName: "General",
|
||||
channelSlug: "general",
|
||||
});
|
||||
const { guildInfo, channelConfig } = createAutoThreadMentionContext();
|
||||
expect(
|
||||
resolveDiscordShouldRequireMention({
|
||||
isGuildMessage: true,
|
||||
@@ -452,18 +446,7 @@ describe("discord mention gating", () => {
|
||||
});
|
||||
|
||||
it("requires mention when thread owner is unknown", () => {
|
||||
const guildInfo: DiscordGuildEntryResolved = {
|
||||
requireMention: true,
|
||||
channels: {
|
||||
general: { allow: true, autoThread: true },
|
||||
},
|
||||
};
|
||||
const channelConfig = resolveDiscordChannelConfig({
|
||||
guildInfo,
|
||||
channelId: "1",
|
||||
channelName: "General",
|
||||
channelSlug: "general",
|
||||
});
|
||||
const { guildInfo, channelConfig } = createAutoThreadMentionContext();
|
||||
expect(
|
||||
resolveDiscordShouldRequireMention({
|
||||
isGuildMessage: true,
|
||||
|
||||
@@ -1,6 +1,31 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { resolveFetch, wrapFetchWithAbortSignal } from "./fetch.js";
|
||||
|
||||
function createForeignSignalHarness() {
|
||||
let abortHandler: (() => void) | null = null;
|
||||
const removeEventListener = vi.fn((event: string, handler: () => void) => {
|
||||
if (event === "abort" && abortHandler === handler) {
|
||||
abortHandler = null;
|
||||
}
|
||||
});
|
||||
|
||||
const fakeSignal = {
|
||||
aborted: false,
|
||||
addEventListener: (event: string, handler: () => void) => {
|
||||
if (event === "abort") {
|
||||
abortHandler = handler;
|
||||
}
|
||||
},
|
||||
removeEventListener,
|
||||
} as AbortSignal;
|
||||
|
||||
return {
|
||||
fakeSignal,
|
||||
removeEventListener,
|
||||
triggerAbort: () => abortHandler?.(),
|
||||
};
|
||||
}
|
||||
|
||||
describe("wrapFetchWithAbortSignal", () => {
|
||||
it("adds duplex for requests with a body", async () => {
|
||||
let seenInit: RequestInit | undefined;
|
||||
@@ -25,27 +50,14 @@ describe("wrapFetchWithAbortSignal", () => {
|
||||
|
||||
const wrapped = wrapFetchWithAbortSignal(fetchImpl);
|
||||
|
||||
let abortHandler: (() => void) | null = null;
|
||||
const fakeSignal = {
|
||||
aborted: false,
|
||||
addEventListener: (event: string, handler: () => void) => {
|
||||
if (event === "abort") {
|
||||
abortHandler = handler;
|
||||
}
|
||||
},
|
||||
removeEventListener: (event: string, handler: () => void) => {
|
||||
if (event === "abort" && abortHandler === handler) {
|
||||
abortHandler = null;
|
||||
}
|
||||
},
|
||||
} as AbortSignal;
|
||||
const { fakeSignal, triggerAbort } = createForeignSignalHarness();
|
||||
|
||||
const promise = wrapped("https://example.com", { signal: fakeSignal });
|
||||
expect(fetchImpl).toHaveBeenCalledOnce();
|
||||
expect(seenSignal).toBeInstanceOf(AbortSignal);
|
||||
expect(seenSignal).not.toBe(fakeSignal);
|
||||
|
||||
abortHandler?.();
|
||||
triggerAbort();
|
||||
expect(seenSignal?.aborted).toBe(true);
|
||||
|
||||
await promise;
|
||||
@@ -64,22 +76,7 @@ describe("wrapFetchWithAbortSignal", () => {
|
||||
);
|
||||
const wrapped = wrapFetchWithAbortSignal(fetchImpl);
|
||||
|
||||
let abortHandler: (() => void) | null = null;
|
||||
const removeEventListener = vi.fn((event: string, handler: () => void) => {
|
||||
if (event === "abort" && abortHandler === handler) {
|
||||
abortHandler = null;
|
||||
}
|
||||
});
|
||||
|
||||
const fakeSignal = {
|
||||
aborted: false,
|
||||
addEventListener: (event: string, handler: () => void) => {
|
||||
if (event === "abort") {
|
||||
abortHandler = handler;
|
||||
}
|
||||
},
|
||||
removeEventListener,
|
||||
} as AbortSignal;
|
||||
const { fakeSignal, removeEventListener } = createForeignSignalHarness();
|
||||
|
||||
try {
|
||||
await expect(wrapped("https://example.com", { signal: fakeSignal })).rejects.toBe(fetchError);
|
||||
@@ -100,22 +97,7 @@ describe("wrapFetchWithAbortSignal", () => {
|
||||
});
|
||||
const wrapped = wrapFetchWithAbortSignal(fetchImpl);
|
||||
|
||||
let abortHandler: (() => void) | null = null;
|
||||
const removeEventListener = vi.fn((event: string, handler: () => void) => {
|
||||
if (event === "abort" && abortHandler === handler) {
|
||||
abortHandler = null;
|
||||
}
|
||||
});
|
||||
|
||||
const fakeSignal = {
|
||||
aborted: false,
|
||||
addEventListener: (event: string, handler: () => void) => {
|
||||
if (event === "abort") {
|
||||
abortHandler = handler;
|
||||
}
|
||||
},
|
||||
removeEventListener,
|
||||
} as AbortSignal;
|
||||
const { fakeSignal, removeEventListener } = createForeignSignalHarness();
|
||||
|
||||
expect(() => wrapped("https://example.com", { signal: fakeSignal })).toThrow(syncError);
|
||||
expect(removeEventListener).toHaveBeenCalledOnce();
|
||||
|
||||
@@ -5,22 +5,26 @@ import {
|
||||
shouldEnableWindowsGitBashPasteFallback,
|
||||
} from "./tui.js";
|
||||
|
||||
function createSubmitHarness() {
|
||||
const editor = {
|
||||
setText: vi.fn(),
|
||||
addToHistory: vi.fn(),
|
||||
};
|
||||
const handleCommand = vi.fn();
|
||||
const sendMessage = vi.fn();
|
||||
const handleBangLine = vi.fn();
|
||||
const onSubmit = createEditorSubmitHandler({
|
||||
editor,
|
||||
handleCommand,
|
||||
sendMessage,
|
||||
handleBangLine,
|
||||
});
|
||||
return { editor, handleCommand, sendMessage, handleBangLine, onSubmit };
|
||||
}
|
||||
|
||||
describe("createEditorSubmitHandler", () => {
|
||||
it("routes lines starting with ! to handleBangLine", () => {
|
||||
const editor = {
|
||||
setText: vi.fn(),
|
||||
addToHistory: vi.fn(),
|
||||
};
|
||||
const handleCommand = vi.fn();
|
||||
const sendMessage = vi.fn();
|
||||
const handleBangLine = vi.fn();
|
||||
|
||||
const onSubmit = createEditorSubmitHandler({
|
||||
editor,
|
||||
handleCommand,
|
||||
sendMessage,
|
||||
handleBangLine,
|
||||
});
|
||||
const { handleCommand, sendMessage, handleBangLine, onSubmit } = createSubmitHarness();
|
||||
|
||||
onSubmit("!ls");
|
||||
|
||||
@@ -31,20 +35,7 @@ describe("createEditorSubmitHandler", () => {
|
||||
});
|
||||
|
||||
it("treats a lone ! as a normal message", () => {
|
||||
const editor = {
|
||||
setText: vi.fn(),
|
||||
addToHistory: vi.fn(),
|
||||
};
|
||||
const handleCommand = vi.fn();
|
||||
const sendMessage = vi.fn();
|
||||
const handleBangLine = vi.fn();
|
||||
|
||||
const onSubmit = createEditorSubmitHandler({
|
||||
editor,
|
||||
handleCommand,
|
||||
sendMessage,
|
||||
handleBangLine,
|
||||
});
|
||||
const { sendMessage, handleBangLine, onSubmit } = createSubmitHarness();
|
||||
|
||||
onSubmit("!");
|
||||
|
||||
@@ -54,20 +45,7 @@ describe("createEditorSubmitHandler", () => {
|
||||
});
|
||||
|
||||
it("does not treat leading whitespace before ! as a bang command", () => {
|
||||
const editor = {
|
||||
setText: vi.fn(),
|
||||
addToHistory: vi.fn(),
|
||||
};
|
||||
const handleCommand = vi.fn();
|
||||
const sendMessage = vi.fn();
|
||||
const handleBangLine = vi.fn();
|
||||
|
||||
const onSubmit = createEditorSubmitHandler({
|
||||
editor,
|
||||
handleCommand,
|
||||
sendMessage,
|
||||
handleBangLine,
|
||||
});
|
||||
const { editor, sendMessage, handleBangLine, onSubmit } = createSubmitHarness();
|
||||
|
||||
onSubmit(" !ls");
|
||||
|
||||
@@ -77,20 +55,7 @@ describe("createEditorSubmitHandler", () => {
|
||||
});
|
||||
|
||||
it("trims normal messages before sending and adding to history", () => {
|
||||
const editor = {
|
||||
setText: vi.fn(),
|
||||
addToHistory: vi.fn(),
|
||||
};
|
||||
const handleCommand = vi.fn();
|
||||
const sendMessage = vi.fn();
|
||||
const handleBangLine = vi.fn();
|
||||
|
||||
const onSubmit = createEditorSubmitHandler({
|
||||
editor,
|
||||
handleCommand,
|
||||
sendMessage,
|
||||
handleBangLine,
|
||||
});
|
||||
const { editor, sendMessage, onSubmit } = createSubmitHarness();
|
||||
|
||||
onSubmit(" hello ");
|
||||
|
||||
@@ -99,20 +64,7 @@ describe("createEditorSubmitHandler", () => {
|
||||
});
|
||||
|
||||
it("preserves internal newlines for multiline messages", () => {
|
||||
const editor = {
|
||||
setText: vi.fn(),
|
||||
addToHistory: vi.fn(),
|
||||
};
|
||||
const handleCommand = vi.fn();
|
||||
const sendMessage = vi.fn();
|
||||
const handleBangLine = vi.fn();
|
||||
|
||||
const onSubmit = createEditorSubmitHandler({
|
||||
editor,
|
||||
handleCommand,
|
||||
sendMessage,
|
||||
handleBangLine,
|
||||
});
|
||||
const { editor, handleCommand, sendMessage, handleBangLine, onSubmit } = createSubmitHarness();
|
||||
|
||||
onSubmit("Line 1\nLine 2\nLine 3");
|
||||
|
||||
|
||||
@@ -16,6 +16,27 @@ const DEFAULT_MESSAGES_CFG = {
|
||||
responsePrefix: undefined,
|
||||
} as const;
|
||||
|
||||
function createAllowListConfig(allowFrom: string[]) {
|
||||
return {
|
||||
channels: {
|
||||
whatsapp: {
|
||||
allowFrom,
|
||||
},
|
||||
},
|
||||
messages: DEFAULT_MESSAGES_CFG,
|
||||
};
|
||||
}
|
||||
|
||||
async function openInboxMonitor(onMessage = vi.fn()) {
|
||||
const listener = await monitorWebInbox({
|
||||
verbose: false,
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
authDir: getAuthDir(),
|
||||
onMessage,
|
||||
});
|
||||
return { onMessage, listener, sock: getSock() };
|
||||
}
|
||||
|
||||
async function expectOutboundDmSkipsPairing(params: {
|
||||
selfChatMode: boolean;
|
||||
messageId: string;
|
||||
@@ -73,27 +94,9 @@ describe("web monitor inbox", () => {
|
||||
installWebMonitorInboxUnitTestHooks();
|
||||
|
||||
it("allows messages from senders in allowFrom list", async () => {
|
||||
mockLoadConfig.mockReturnValue({
|
||||
channels: {
|
||||
whatsapp: {
|
||||
// Allow +999
|
||||
allowFrom: ["+111", "+999"],
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
messagePrefix: undefined,
|
||||
responsePrefix: undefined,
|
||||
},
|
||||
});
|
||||
mockLoadConfig.mockReturnValue(createAllowListConfig(["+111", "+999"]));
|
||||
|
||||
const onMessage = vi.fn();
|
||||
const listener = await monitorWebInbox({
|
||||
verbose: false,
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
authDir: getAuthDir(),
|
||||
onMessage,
|
||||
});
|
||||
const sock = getSock();
|
||||
const { onMessage, listener, sock } = await openInboxMonitor();
|
||||
|
||||
const upsert = {
|
||||
type: "notify",
|
||||
@@ -124,27 +127,9 @@ describe("web monitor inbox", () => {
|
||||
it("allows same-phone messages even if not in allowFrom", async () => {
|
||||
// Same-phone mode: when from === selfJid, should always be allowed
|
||||
// This allows users to message themselves even with restrictive allowFrom
|
||||
mockLoadConfig.mockReturnValue({
|
||||
channels: {
|
||||
whatsapp: {
|
||||
// Only allow +111, but self is +123
|
||||
allowFrom: ["+111"],
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
messagePrefix: undefined,
|
||||
responsePrefix: undefined,
|
||||
},
|
||||
});
|
||||
mockLoadConfig.mockReturnValue(createAllowListConfig(["+111"]));
|
||||
|
||||
const onMessage = vi.fn();
|
||||
const listener = await monitorWebInbox({
|
||||
verbose: false,
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
authDir: getAuthDir(),
|
||||
onMessage,
|
||||
});
|
||||
const sock = getSock();
|
||||
const { onMessage, listener, sock } = await openInboxMonitor();
|
||||
|
||||
// Message from self (sock.user.id is "123@s.whatsapp.net" in mock)
|
||||
const upsert = {
|
||||
@@ -176,14 +161,7 @@ describe("web monitor inbox", () => {
|
||||
.mockResolvedValueOnce({ code: "PAIRCODE", created: true })
|
||||
.mockResolvedValueOnce({ code: "PAIRCODE", created: false });
|
||||
|
||||
const onMessage = vi.fn();
|
||||
const listener = await monitorWebInbox({
|
||||
verbose: false,
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
authDir: getAuthDir(),
|
||||
onMessage,
|
||||
});
|
||||
const sock = getSock();
|
||||
const { onMessage, listener, sock } = await openInboxMonitor();
|
||||
|
||||
// Message from someone else should be blocked
|
||||
const upsertBlocked = {
|
||||
@@ -280,14 +258,7 @@ describe("web monitor inbox", () => {
|
||||
});
|
||||
|
||||
it("handles append messages by marking them read but skipping auto-reply", async () => {
|
||||
const onMessage = vi.fn();
|
||||
const listener = await monitorWebInbox({
|
||||
verbose: false,
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
authDir: getAuthDir(),
|
||||
onMessage,
|
||||
});
|
||||
const sock = getSock();
|
||||
const { onMessage, listener, sock } = await openInboxMonitor();
|
||||
|
||||
const upsert = {
|
||||
type: "append",
|
||||
|
||||
Reference in New Issue
Block a user