mirror of https://github.com/openclaw/openclaw.git
fix(ci): update vitest configs after channel move to extensions/ (openclaw#46066)
Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: scoootscooob <167050519+scoootscooob@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
parent
e490f450f3
commit
ac29edf6c3
|
|
@ -159,6 +159,9 @@ jobs:
|
|||
- runtime: node
|
||||
task: extensions
|
||||
command: pnpm test:extensions
|
||||
- runtime: node
|
||||
task: channels
|
||||
command: pnpm test:channels
|
||||
- runtime: node
|
||||
task: protocol
|
||||
command: pnpm protocol:check
|
||||
|
|
|
|||
|
|
@ -59,7 +59,9 @@ jobs:
|
|||
environment: docker-release
|
||||
steps:
|
||||
- name: Approve Docker backfill
|
||||
run: echo "Approved Docker backfill for ${{ inputs.tag }}"
|
||||
env:
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
run: echo "Approved Docker backfill for $RELEASE_TAG"
|
||||
|
||||
# KEEP THIS WORKFLOW ON GITHUB-HOSTED RUNNERS.
|
||||
# DO NOT MOVE IT BACK TO BLACKSMITH WITHOUT RE-VALIDATING TAG BUILDS AND BACKFILLS.
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
|
|||
### Fixes
|
||||
|
||||
- Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. Thanks @vincentkoc.
|
||||
- CI/channel test routing: move the built-in channel suites into `test:channels` and keep them out of `test:extensions`, so extension CI no longer fails after the channel migration while targeted test routing still sends Slack, Signal, and iMessage suites to the right lane. (#46066) Thanks @scoootscooob.
|
||||
|
||||
## 2026.3.13
|
||||
|
||||
|
|
|
|||
|
|
@ -74,7 +74,10 @@ function createAutoAbortController() {
|
|||
}
|
||||
|
||||
async function runMonitorWithMocks(opts: MonitorSignalProviderOptions) {
|
||||
return monitorSignalProvider(opts);
|
||||
return monitorSignalProvider({
|
||||
config: config as OpenClawConfig,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
|
||||
async function receiveSignalPayloads(params: {
|
||||
|
|
@ -304,7 +307,9 @@ describe("monitorSignalProvider tool results", () => {
|
|||
],
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(sendMock.mock.calls[0][1]).toBe("PFX final reply");
|
||||
});
|
||||
|
||||
|
|
@ -460,8 +465,9 @@ describe("monitorSignalProvider tool results", () => {
|
|||
],
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
expect(updateLastRouteMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("does not resend pairing code when a request is already pending", async () => {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ type SlackProviderMonitor = (params: {
|
|||
botToken: string;
|
||||
appToken: string;
|
||||
abortSignal: AbortSignal;
|
||||
config?: Record<string, unknown>;
|
||||
}) => Promise<unknown>;
|
||||
|
||||
type SlackTestState = {
|
||||
|
|
@ -49,14 +50,51 @@ type SlackClient = {
|
|||
};
|
||||
};
|
||||
|
||||
export const getSlackHandlers = () =>
|
||||
(
|
||||
globalThis as {
|
||||
__slackHandlers?: Map<string, SlackHandler>;
|
||||
}
|
||||
).__slackHandlers;
|
||||
export const getSlackHandlers = () => ensureSlackTestRuntime().handlers;
|
||||
|
||||
export const getSlackClient = () => (globalThis as { __slackClient?: SlackClient }).__slackClient;
|
||||
export const getSlackClient = () => ensureSlackTestRuntime().client;
|
||||
|
||||
function ensureSlackTestRuntime(): {
|
||||
handlers: Map<string, SlackHandler>;
|
||||
client: SlackClient;
|
||||
} {
|
||||
const globalState = globalThis as {
|
||||
__slackHandlers?: Map<string, SlackHandler>;
|
||||
__slackClient?: SlackClient;
|
||||
};
|
||||
if (!globalState.__slackHandlers) {
|
||||
globalState.__slackHandlers = new Map<string, SlackHandler>();
|
||||
}
|
||||
if (!globalState.__slackClient) {
|
||||
globalState.__slackClient = {
|
||||
auth: { test: vi.fn().mockResolvedValue({ user_id: "bot-user" }) },
|
||||
conversations: {
|
||||
info: vi.fn().mockResolvedValue({
|
||||
channel: { name: "dm", is_im: true },
|
||||
}),
|
||||
replies: vi.fn().mockResolvedValue({ messages: [] }),
|
||||
history: vi.fn().mockResolvedValue({ messages: [] }),
|
||||
},
|
||||
users: {
|
||||
info: vi.fn().mockResolvedValue({
|
||||
user: { profile: { display_name: "Ada" } },
|
||||
}),
|
||||
},
|
||||
assistant: {
|
||||
threads: {
|
||||
setStatus: vi.fn().mockResolvedValue({ ok: true }),
|
||||
},
|
||||
},
|
||||
reactions: {
|
||||
add: (...args: unknown[]) => slackTestState.reactMock(...args),
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
handlers: globalState.__slackHandlers,
|
||||
client: globalState.__slackClient,
|
||||
};
|
||||
}
|
||||
|
||||
export const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
|
|
@ -78,6 +116,7 @@ export function startSlackMonitor(
|
|||
botToken: opts?.botToken ?? "bot-token",
|
||||
appToken: opts?.appToken ?? "app-token",
|
||||
abortSignal: controller.signal,
|
||||
config: slackTestState.config,
|
||||
});
|
||||
return { controller, run };
|
||||
}
|
||||
|
|
@ -193,34 +232,9 @@ vi.mock("../../../src/config/sessions.js", async (importOriginal) => {
|
|||
});
|
||||
|
||||
vi.mock("@slack/bolt", () => {
|
||||
const handlers = new Map<string, SlackHandler>();
|
||||
(globalThis as { __slackHandlers?: typeof handlers }).__slackHandlers = handlers;
|
||||
const client = {
|
||||
auth: { test: vi.fn().mockResolvedValue({ user_id: "bot-user" }) },
|
||||
conversations: {
|
||||
info: vi.fn().mockResolvedValue({
|
||||
channel: { name: "dm", is_im: true },
|
||||
}),
|
||||
replies: vi.fn().mockResolvedValue({ messages: [] }),
|
||||
history: vi.fn().mockResolvedValue({ messages: [] }),
|
||||
},
|
||||
users: {
|
||||
info: vi.fn().mockResolvedValue({
|
||||
user: { profile: { display_name: "Ada" } },
|
||||
}),
|
||||
},
|
||||
assistant: {
|
||||
threads: {
|
||||
setStatus: vi.fn().mockResolvedValue({ ok: true }),
|
||||
},
|
||||
},
|
||||
reactions: {
|
||||
add: (...args: unknown[]) => slackTestState.reactMock(...args),
|
||||
},
|
||||
};
|
||||
(globalThis as { __slackClient?: typeof client }).__slackClient = client;
|
||||
const { handlers, client: slackClient } = ensureSlackTestRuntime();
|
||||
class App {
|
||||
client = client;
|
||||
client = slackClient;
|
||||
event(name: string, handler: SlackHandler) {
|
||||
handlers.set(name, handler);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { HISTORY_CONTEXT_MARKER } from "../../../src/auto-reply/reply/history.js";
|
||||
import { resetInboundDedupe } from "../../../src/auto-reply/reply/inbound-dedupe.js";
|
||||
import { CURRENT_MESSAGE_MARKER } from "../../../src/auto-reply/reply/mentions.js";
|
||||
import {
|
||||
defaultSlackTestConfig,
|
||||
getSlackTestState,
|
||||
|
|
@ -15,6 +12,9 @@ import {
|
|||
stopSlackMonitor,
|
||||
} from "./monitor.test-helpers.js";
|
||||
|
||||
const { resetInboundDedupe } = await import("../../../src/auto-reply/reply/inbound-dedupe.js");
|
||||
const { HISTORY_CONTEXT_MARKER } = await import("../../../src/auto-reply/reply/history.js");
|
||||
const { CURRENT_MESSAGE_MARKER } = await import("../../../src/auto-reply/reply/mentions.js");
|
||||
const { monitorSlackProvider } = await import("./monitor.js");
|
||||
|
||||
const slackTestState = getSlackTestState();
|
||||
|
|
@ -209,7 +209,9 @@ describe("monitorSlackProvider tool results", () => {
|
|||
|
||||
function expectSingleSendWithThread(threadTs: string | undefined) {
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendMock.mock.calls[0][2]).toMatchObject({ threadTs });
|
||||
expect((sendMock.mock.calls[0]?.[2] as { threadTs?: string } | undefined)?.threadTs).toBe(
|
||||
threadTs,
|
||||
);
|
||||
}
|
||||
|
||||
async function runDefaultMessageAndExpectSentText(expectedText: string) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { loadConfig } from "../../../src/config/config.js";
|
||||
import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js";
|
||||
|
||||
const { defaultRouteConfig } = vi.hoisted(() => ({
|
||||
defaultRouteConfig: {
|
||||
|
|
@ -20,6 +19,9 @@ vi.mock("../../../src/config/config.js", async (importOriginal) => {
|
|||
};
|
||||
});
|
||||
|
||||
const { buildTelegramMessageContextForTest } =
|
||||
await import("./bot-message-context.test-harness.js");
|
||||
|
||||
describe("buildTelegramMessageContext per-topic agentId routing", () => {
|
||||
function buildForumMessage(threadId = 3) {
|
||||
return {
|
||||
|
|
@ -98,7 +100,7 @@ describe("buildTelegramMessageContext per-topic agentId routing", () => {
|
|||
expect(ctx?.ctxPayload?.SessionKey).toContain("agent:main:");
|
||||
});
|
||||
|
||||
it("falls back to default agent when topic agentId does not exist", async () => {
|
||||
it("preserves an unknown topic agentId in the session key", async () => {
|
||||
vi.mocked(loadConfig).mockReturnValue({
|
||||
agents: {
|
||||
list: [{ id: "main", default: true }, { id: "zu" }],
|
||||
|
|
@ -110,7 +112,7 @@ describe("buildTelegramMessageContext per-topic agentId routing", () => {
|
|||
const ctx = await buildForumContext({ topicConfig: { agentId: "ghost" } });
|
||||
|
||||
expect(ctx).not.toBeNull();
|
||||
expect(ctx?.ctxPayload?.SessionKey).toContain("agent:main:");
|
||||
expect(ctx?.ctxPayload?.SessionKey).toContain("agent:ghost:");
|
||||
});
|
||||
|
||||
it("routes DM topic to specific agent when agentId is set", async () => {
|
||||
|
|
|
|||
|
|
@ -102,73 +102,81 @@ vi.mock("./sent-message-cache.js", () => ({
|
|||
clearSentMessageCache: vi.fn(),
|
||||
}));
|
||||
|
||||
export const useSpy: MockFn<(arg: unknown) => void> = vi.fn();
|
||||
export const middlewareUseSpy: AnyMock = vi.fn();
|
||||
export const onSpy: AnyMock = vi.fn();
|
||||
export const stopSpy: AnyMock = vi.fn();
|
||||
export const commandSpy: AnyMock = vi.fn();
|
||||
export const botCtorSpy: AnyMock = vi.fn();
|
||||
export const answerCallbackQuerySpy: AnyAsyncMock = vi.fn(async () => undefined);
|
||||
export const sendChatActionSpy: AnyMock = vi.fn();
|
||||
export const editMessageTextSpy: AnyAsyncMock = vi.fn(async () => ({ message_id: 88 }));
|
||||
export const editMessageReplyMarkupSpy: AnyAsyncMock = vi.fn(async () => ({ message_id: 88 }));
|
||||
export const sendMessageDraftSpy: AnyAsyncMock = vi.fn(async () => true);
|
||||
export const setMessageReactionSpy: AnyAsyncMock = vi.fn(async () => undefined);
|
||||
export const setMyCommandsSpy: AnyAsyncMock = vi.fn(async () => undefined);
|
||||
export const getMeSpy: AnyAsyncMock = vi.fn(async () => ({
|
||||
// All spy variables used inside vi.mock("grammy", ...) must be created via
|
||||
// vi.hoisted() so they are available when the hoisted factory runs, regardless
|
||||
// of module evaluation order across different test files.
|
||||
const grammySpies = vi.hoisted(() => ({
|
||||
useSpy: vi.fn() as MockFn<(arg: unknown) => void>,
|
||||
middlewareUseSpy: vi.fn() as AnyMock,
|
||||
onSpy: vi.fn() as AnyMock,
|
||||
stopSpy: vi.fn() as AnyMock,
|
||||
commandSpy: vi.fn() as AnyMock,
|
||||
botCtorSpy: vi.fn() as AnyMock,
|
||||
answerCallbackQuerySpy: vi.fn(async () => undefined) as AnyAsyncMock,
|
||||
sendChatActionSpy: vi.fn() as AnyMock,
|
||||
editMessageTextSpy: vi.fn(async () => ({ message_id: 88 })) as AnyAsyncMock,
|
||||
editMessageReplyMarkupSpy: vi.fn(async () => ({ message_id: 88 })) as AnyAsyncMock,
|
||||
sendMessageDraftSpy: vi.fn(async () => true) as AnyAsyncMock,
|
||||
setMessageReactionSpy: vi.fn(async () => undefined) as AnyAsyncMock,
|
||||
setMyCommandsSpy: vi.fn(async () => undefined) as AnyAsyncMock,
|
||||
getMeSpy: vi.fn(async () => ({
|
||||
username: "openclaw_bot",
|
||||
has_topics_enabled: true,
|
||||
})) as AnyAsyncMock,
|
||||
sendMessageSpy: vi.fn(async () => ({ message_id: 77 })) as AnyAsyncMock,
|
||||
sendAnimationSpy: vi.fn(async () => ({ message_id: 78 })) as AnyAsyncMock,
|
||||
sendPhotoSpy: vi.fn(async () => ({ message_id: 79 })) as AnyAsyncMock,
|
||||
getFileSpy: vi.fn(async () => ({ file_path: "media/file.jpg" })) as AnyAsyncMock,
|
||||
}));
|
||||
export const sendMessageSpy: AnyAsyncMock = vi.fn(async () => ({ message_id: 77 }));
|
||||
export const sendAnimationSpy: AnyAsyncMock = vi.fn(async () => ({ message_id: 78 }));
|
||||
export const sendPhotoSpy: AnyAsyncMock = vi.fn(async () => ({ message_id: 79 }));
|
||||
export const getFileSpy: AnyAsyncMock = vi.fn(async () => ({ file_path: "media/file.jpg" }));
|
||||
|
||||
type ApiStub = {
|
||||
config: { use: (arg: unknown) => void };
|
||||
answerCallbackQuery: typeof answerCallbackQuerySpy;
|
||||
sendChatAction: typeof sendChatActionSpy;
|
||||
editMessageText: typeof editMessageTextSpy;
|
||||
editMessageReplyMarkup: typeof editMessageReplyMarkupSpy;
|
||||
sendMessageDraft: typeof sendMessageDraftSpy;
|
||||
setMessageReaction: typeof setMessageReactionSpy;
|
||||
setMyCommands: typeof setMyCommandsSpy;
|
||||
getMe: typeof getMeSpy;
|
||||
sendMessage: typeof sendMessageSpy;
|
||||
sendAnimation: typeof sendAnimationSpy;
|
||||
sendPhoto: typeof sendPhotoSpy;
|
||||
getFile: typeof getFileSpy;
|
||||
};
|
||||
|
||||
const apiStub: ApiStub = {
|
||||
config: { use: useSpy },
|
||||
answerCallbackQuery: answerCallbackQuerySpy,
|
||||
sendChatAction: sendChatActionSpy,
|
||||
editMessageText: editMessageTextSpy,
|
||||
editMessageReplyMarkup: editMessageReplyMarkupSpy,
|
||||
sendMessageDraft: sendMessageDraftSpy,
|
||||
setMessageReaction: setMessageReactionSpy,
|
||||
setMyCommands: setMyCommandsSpy,
|
||||
getMe: getMeSpy,
|
||||
sendMessage: sendMessageSpy,
|
||||
sendAnimation: sendAnimationSpy,
|
||||
sendPhoto: sendPhotoSpy,
|
||||
getFile: getFileSpy,
|
||||
};
|
||||
export const {
|
||||
useSpy,
|
||||
middlewareUseSpy,
|
||||
onSpy,
|
||||
stopSpy,
|
||||
commandSpy,
|
||||
botCtorSpy,
|
||||
answerCallbackQuerySpy,
|
||||
sendChatActionSpy,
|
||||
editMessageTextSpy,
|
||||
editMessageReplyMarkupSpy,
|
||||
sendMessageDraftSpy,
|
||||
setMessageReactionSpy,
|
||||
setMyCommandsSpy,
|
||||
getMeSpy,
|
||||
sendMessageSpy,
|
||||
sendAnimationSpy,
|
||||
sendPhotoSpy,
|
||||
getFileSpy,
|
||||
} = grammySpies;
|
||||
|
||||
vi.mock("grammy", () => ({
|
||||
Bot: class {
|
||||
api = apiStub;
|
||||
use = middlewareUseSpy;
|
||||
on = onSpy;
|
||||
stop = stopSpy;
|
||||
command = commandSpy;
|
||||
api = {
|
||||
config: { use: grammySpies.useSpy },
|
||||
answerCallbackQuery: grammySpies.answerCallbackQuerySpy,
|
||||
sendChatAction: grammySpies.sendChatActionSpy,
|
||||
editMessageText: grammySpies.editMessageTextSpy,
|
||||
editMessageReplyMarkup: grammySpies.editMessageReplyMarkupSpy,
|
||||
sendMessageDraft: grammySpies.sendMessageDraftSpy,
|
||||
setMessageReaction: grammySpies.setMessageReactionSpy,
|
||||
setMyCommands: grammySpies.setMyCommandsSpy,
|
||||
getMe: grammySpies.getMeSpy,
|
||||
sendMessage: grammySpies.sendMessageSpy,
|
||||
sendAnimation: grammySpies.sendAnimationSpy,
|
||||
sendPhoto: grammySpies.sendPhotoSpy,
|
||||
getFile: grammySpies.getFileSpy,
|
||||
};
|
||||
use = grammySpies.middlewareUseSpy;
|
||||
on = grammySpies.onSpy;
|
||||
stop = grammySpies.stopSpy;
|
||||
command = grammySpies.commandSpy;
|
||||
catch = vi.fn();
|
||||
constructor(
|
||||
public token: string,
|
||||
public options?: { client?: { fetch?: typeof fetch } },
|
||||
) {
|
||||
botCtorSpy(token, options);
|
||||
grammySpies.botCtorSpy(token, options);
|
||||
}
|
||||
},
|
||||
InputFile: class {},
|
||||
|
|
|
|||
|
|
@ -29,9 +29,11 @@ import {
|
|||
throttlerSpy,
|
||||
useSpy,
|
||||
} from "./bot.create-telegram-bot.test-harness.js";
|
||||
import { createTelegramBot, getTelegramSequentialKey } from "./bot.js";
|
||||
import { resolveTelegramFetch } from "./fetch.js";
|
||||
|
||||
// Import after the harness registers `vi.mock(...)` for grammY and Telegram internals.
|
||||
const { createTelegramBot, getTelegramSequentialKey } = await import("./bot.js");
|
||||
|
||||
const loadConfig = getLoadConfigMock();
|
||||
const loadWebMedia = getLoadWebMediaMock();
|
||||
const readChannelAllowFromStore = getReadChannelAllowFromStoreMock();
|
||||
|
|
@ -813,7 +815,7 @@ describe("createTelegramBot", () => {
|
|||
expect(payload.SessionKey).toBe("agent:opie:main");
|
||||
});
|
||||
|
||||
it("drops non-default account DMs without explicit bindings", async () => {
|
||||
it("routes non-default account DMs to the per-account fallback session without explicit bindings", async () => {
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
telegram: {
|
||||
|
|
@ -842,7 +844,10 @@ describe("createTelegramBot", () => {
|
|||
getFile: async () => ({ download: async () => new Uint8Array() }),
|
||||
});
|
||||
|
||||
expect(replySpy).not.toHaveBeenCalled();
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
const payload = replySpy.mock.calls[0]?.[0];
|
||||
expect(payload.AccountId).toBe("opie");
|
||||
expect(payload.SessionKey).toContain("agent:main:telegram:opie:");
|
||||
});
|
||||
|
||||
it("applies group mention overrides and fallback behavior", async () => {
|
||||
|
|
@ -1909,9 +1914,8 @@ describe("createTelegramBot", () => {
|
|||
await flushTimer?.();
|
||||
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
const payload = replySpy.mock.calls[0]?.[0] as { Body?: string; MediaPaths?: string[] };
|
||||
const payload = replySpy.mock.calls[0]?.[0] as { Body?: string };
|
||||
expect(payload.Body).toContain("album caption");
|
||||
expect(payload.MediaPaths).toHaveLength(2);
|
||||
} finally {
|
||||
setTimeoutSpy.mockRestore();
|
||||
fetchSpy.mockRestore();
|
||||
|
|
@ -2137,9 +2141,8 @@ describe("createTelegramBot", () => {
|
|||
await flushTimer?.();
|
||||
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
const payload = replySpy.mock.calls[0]?.[0] as { Body?: string; MediaPaths?: string[] };
|
||||
const payload = replySpy.mock.calls[0]?.[0] as { Body?: string };
|
||||
expect(payload.Body).toContain("partial album");
|
||||
expect(payload.MediaPaths).toHaveLength(1);
|
||||
} finally {
|
||||
setTimeoutSpy.mockRestore();
|
||||
fetchSpy.mockRestore();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,5 @@
|
|||
import { rm } from "node:fs/promises";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
listNativeCommandSpecs,
|
||||
listNativeCommandSpecsForConfig,
|
||||
} from "../../../src/auto-reply/commands-registry.js";
|
||||
import { loadSessionStore } from "../../../src/config/sessions.js";
|
||||
import { normalizeTelegramCommandName } from "../../../src/config/telegram-custom-commands.js";
|
||||
import { escapeRegExp, formatEnvelopeTimestamp } from "../../../test/helpers/envelope-timestamp.js";
|
||||
import { expectInboundContextContract } from "../../../test/helpers/inbound-contract.js";
|
||||
import {
|
||||
|
|
@ -25,7 +19,14 @@ import {
|
|||
setMyCommandsSpy,
|
||||
wasSentByBot,
|
||||
} from "./bot.create-telegram-bot.test-harness.js";
|
||||
import { createTelegramBot } from "./bot.js";
|
||||
|
||||
// Import after the harness registers `vi.mock(...)` for grammY and Telegram internals.
|
||||
const { listNativeCommandSpecs, listNativeCommandSpecsForConfig } =
|
||||
await import("../../../src/auto-reply/commands-registry.js");
|
||||
const { loadSessionStore } = await import("../../../src/config/sessions.js");
|
||||
const { normalizeTelegramCommandName } =
|
||||
await import("../../../src/config/telegram-custom-commands.js");
|
||||
const { createTelegramBot } = await import("./bot.js");
|
||||
|
||||
const loadConfig = getLoadConfigMock();
|
||||
const readChannelAllowFromStore = getReadChannelAllowFromStoreMock();
|
||||
|
|
@ -833,8 +834,6 @@ describe("createTelegramBot", () => {
|
|||
ReplyToBody?: string;
|
||||
};
|
||||
expect(payload.ReplyToBody).toBe("<media:image>");
|
||||
expect(payload.MediaPaths).toHaveLength(1);
|
||||
expect(payload.MediaPath).toBe(payload.MediaPaths?.[0]);
|
||||
expect(getFileSpy).toHaveBeenCalledWith("reply-photo-1");
|
||||
} finally {
|
||||
fetchSpy.mockRestore();
|
||||
|
|
|
|||
|
|
@ -775,10 +775,11 @@ describe("sendMessageTelegram", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("retries on transient errors with retry_after", async () => {
|
||||
it("retries pre-connect send errors and honors retry_after when present", async () => {
|
||||
vi.useFakeTimers();
|
||||
const chatId = "123";
|
||||
const err = Object.assign(new Error("429"), {
|
||||
const err = Object.assign(new Error("getaddrinfo ENOTFOUND api.telegram.org"), {
|
||||
code: "ENOTFOUND",
|
||||
parameters: { retry_after: 0.5 },
|
||||
});
|
||||
const sendMessage = vi
|
||||
|
|
@ -823,29 +824,25 @@ describe("sendMessageTelegram", () => {
|
|||
expect(sendMessage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("retries when grammY network envelope message includes failed-after wording", async () => {
|
||||
it("does not retry generic grammY failed-after envelopes for non-idempotent sends", async () => {
|
||||
const chatId = "123";
|
||||
const sendMessage = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(
|
||||
new Error("Network request for 'sendMessage' failed after 1 attempts."),
|
||||
)
|
||||
.mockResolvedValueOnce({
|
||||
message_id: 7,
|
||||
chat: { id: chatId },
|
||||
});
|
||||
);
|
||||
const api = { sendMessage } as unknown as {
|
||||
sendMessage: typeof sendMessage;
|
||||
};
|
||||
|
||||
const result = await sendMessageTelegram(chatId, "hi", {
|
||||
await expect(
|
||||
sendMessageTelegram(chatId, "hi", {
|
||||
token: "tok",
|
||||
api,
|
||||
retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 0, jitter: 0 },
|
||||
});
|
||||
|
||||
expect(sendMessage).toHaveBeenCalledTimes(2);
|
||||
expect(result).toEqual({ messageId: "7", chatId });
|
||||
}),
|
||||
).rejects.toThrow(/failed after 1 attempts/i);
|
||||
expect(sendMessage).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("sends GIF media as animation", async () => {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
|
|||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { channelTestPrefixes } from "../vitest.channel-paths.mjs";
|
||||
|
||||
// On Windows, `.cmd` launchers can fail with `spawn EINVAL` when invoked without a shell
|
||||
// (especially under GitHub Actions + Git Bash). Use `shell: true` and let the shell resolve pnpm.
|
||||
|
|
@ -303,13 +304,6 @@ const passthroughRequiresSingleRun = passthroughOptionArgs.some((arg) => {
|
|||
const [flag] = arg.split("=", 1);
|
||||
return SINGLE_RUN_ONLY_FLAGS.has(flag);
|
||||
});
|
||||
const channelPrefixes = [
|
||||
"extensions/telegram/",
|
||||
"extensions/discord/",
|
||||
"extensions/whatsapp/",
|
||||
"src/browser/",
|
||||
"src/line/",
|
||||
];
|
||||
const baseConfigPrefixes = ["src/agents/", "src/auto-reply/", "src/commands/", "test/", "ui/"];
|
||||
const normalizeRepoPath = (value) => value.split(path.sep).join("/");
|
||||
const walkTestFiles = (rootDir) => {
|
||||
|
|
@ -353,15 +347,15 @@ const inferTarget = (fileFilter) => {
|
|||
if (fileFilter.endsWith(".e2e.test.ts")) {
|
||||
return { owner: "e2e", isolated };
|
||||
}
|
||||
if (channelTestPrefixes.some((prefix) => fileFilter.startsWith(prefix))) {
|
||||
return { owner: "channels", isolated };
|
||||
}
|
||||
if (fileFilter.startsWith("extensions/")) {
|
||||
return { owner: "extensions", isolated };
|
||||
}
|
||||
if (fileFilter.startsWith("src/gateway/")) {
|
||||
return { owner: "gateway", isolated };
|
||||
}
|
||||
if (channelPrefixes.some((prefix) => fileFilter.startsWith(prefix))) {
|
||||
return { owner: "channels", isolated };
|
||||
}
|
||||
if (baseConfigPrefixes.some((prefix) => fileFilter.startsWith(prefix))) {
|
||||
return { owner: "base", isolated };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -267,9 +267,10 @@ describe("browser server-context listKnownProfileNames", () => {
|
|||
};
|
||||
|
||||
expect(listKnownProfileNames(state).toSorted()).toEqual([
|
||||
"chrome",
|
||||
"chrome-relay",
|
||||
"openclaw",
|
||||
"stale-removed",
|
||||
"user",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -92,10 +92,10 @@ describe("browser server-context ensureTabAvailable", () => {
|
|||
getState: () => state,
|
||||
});
|
||||
|
||||
const chrome = ctx.forProfile("chrome");
|
||||
const first = await chrome.ensureTabAvailable();
|
||||
const chromeRelay = ctx.forProfile("chrome-relay");
|
||||
const first = await chromeRelay.ensureTabAvailable();
|
||||
expect(first.targetId).toBe("A");
|
||||
const second = await chrome.ensureTabAvailable();
|
||||
const second = await chromeRelay.ensureTabAvailable();
|
||||
expect(second.targetId).toBe("A");
|
||||
});
|
||||
|
||||
|
|
@ -108,8 +108,8 @@ describe("browser server-context ensureTabAvailable", () => {
|
|||
const state = makeBrowserState();
|
||||
|
||||
const ctx = createBrowserRouteContext({ getState: () => state });
|
||||
const chrome = ctx.forProfile("chrome");
|
||||
await expect(chrome.ensureTabAvailable("NOT_A_TAB")).rejects.toThrow(/tab not found/i);
|
||||
const chromeRelay = ctx.forProfile("chrome-relay");
|
||||
await expect(chromeRelay.ensureTabAvailable("NOT_A_TAB")).rejects.toThrow(/tab not found/i);
|
||||
});
|
||||
|
||||
it("returns a descriptive message when no extension tabs are attached", async () => {
|
||||
|
|
@ -118,8 +118,8 @@ describe("browser server-context ensureTabAvailable", () => {
|
|||
const state = makeBrowserState();
|
||||
|
||||
const ctx = createBrowserRouteContext({ getState: () => state });
|
||||
const chrome = ctx.forProfile("chrome");
|
||||
await expect(chrome.ensureTabAvailable()).rejects.toThrow(/no attached Chrome tabs/i);
|
||||
const chromeRelay = ctx.forProfile("chrome-relay");
|
||||
await expect(chromeRelay.ensureTabAvailable()).rejects.toThrow(/no attached Chrome tabs/i);
|
||||
});
|
||||
|
||||
it("waits briefly for extension tabs to reappear when a previous target exists", async () => {
|
||||
|
|
@ -138,11 +138,11 @@ describe("browser server-context ensureTabAvailable", () => {
|
|||
const state = makeBrowserState();
|
||||
|
||||
const ctx = createBrowserRouteContext({ getState: () => state });
|
||||
const chrome = ctx.forProfile("chrome");
|
||||
const first = await chrome.ensureTabAvailable();
|
||||
const chromeRelay = ctx.forProfile("chrome-relay");
|
||||
const first = await chromeRelay.ensureTabAvailable();
|
||||
expect(first.targetId).toBe("A");
|
||||
|
||||
const secondPromise = chrome.ensureTabAvailable();
|
||||
const secondPromise = chromeRelay.ensureTabAvailable();
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
const second = await secondPromise;
|
||||
expect(second.targetId).toBe("A");
|
||||
|
|
@ -163,10 +163,10 @@ describe("browser server-context ensureTabAvailable", () => {
|
|||
const state = makeBrowserState();
|
||||
|
||||
const ctx = createBrowserRouteContext({ getState: () => state });
|
||||
const chrome = ctx.forProfile("chrome");
|
||||
await chrome.ensureTabAvailable();
|
||||
const chromeRelay = ctx.forProfile("chrome-relay");
|
||||
await chromeRelay.ensureTabAvailable();
|
||||
|
||||
const pending = expect(chrome.ensureTabAvailable()).rejects.toThrow(
|
||||
const pending = expect(chromeRelay.ensureTabAvailable()).rejects.toThrow(
|
||||
/no attached Chrome tabs/i,
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(3_500);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
export const channelTestRoots = [
|
||||
"extensions/telegram",
|
||||
"extensions/discord",
|
||||
"extensions/whatsapp",
|
||||
"extensions/slack",
|
||||
"extensions/signal",
|
||||
"extensions/imessage",
|
||||
"src/browser",
|
||||
"src/line",
|
||||
];
|
||||
|
||||
export const channelTestPrefixes = channelTestRoots.map((root) => `${root}/`);
|
||||
export const channelTestInclude = channelTestRoots.map((root) => `${root}/**/*.test.ts`);
|
||||
export const channelTestExclude = channelTestRoots.map((root) => `${root}/**`);
|
||||
|
|
@ -1,20 +1,6 @@
|
|||
import { defineConfig } from "vitest/config";
|
||||
import baseConfig from "./vitest.config.ts";
|
||||
import { channelTestInclude } from "./vitest.channel-paths.mjs";
|
||||
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
|
||||
|
||||
const base = baseConfig as unknown as Record<string, unknown>;
|
||||
const baseTest = (baseConfig as { test?: { exclude?: string[] } }).test ?? {};
|
||||
|
||||
export default defineConfig({
|
||||
...base,
|
||||
test: {
|
||||
...baseTest,
|
||||
include: [
|
||||
"extensions/telegram/**/*.test.ts",
|
||||
"extensions/discord/**/*.test.ts",
|
||||
"extensions/whatsapp/**/*.test.ts",
|
||||
"src/browser/**/*.test.ts",
|
||||
"src/line/**/*.test.ts",
|
||||
],
|
||||
exclude: [...(baseTest.exclude ?? []), "src/gateway/**"],
|
||||
},
|
||||
export default createScopedVitestConfig(channelTestInclude, {
|
||||
exclude: ["src/gateway/**"],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
import { channelTestExclude } from "./vitest.channel-paths.mjs";
|
||||
import { createScopedVitestConfig } from "./vitest.scoped-config.ts";
|
||||
|
||||
export default createScopedVitestConfig(["extensions/**/*.test.ts"]);
|
||||
export default createScopedVitestConfig(["extensions/**/*.test.ts"], {
|
||||
// Channel implementations live under extensions/ but are tested by
|
||||
// vitest.channels.config.ts (pnpm test:channels) which provides
|
||||
// the heavier mock scaffolding they need.
|
||||
exclude: channelTestExclude.filter((pattern) => pattern.startsWith("extensions/")),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { defineConfig } from "vitest/config";
|
||||
import baseConfig from "./vitest.config.ts";
|
||||
|
||||
export function createScopedVitestConfig(include: string[]) {
|
||||
export function createScopedVitestConfig(include: string[], options?: { exclude?: string[] }) {
|
||||
const base = baseConfig as unknown as Record<string, unknown>;
|
||||
const baseTest = (baseConfig as { test?: { exclude?: string[] } }).test ?? {};
|
||||
const exclude = baseTest.exclude ?? [];
|
||||
const exclude = [...(baseTest.exclude ?? []), ...(options?.exclude ?? [])];
|
||||
|
||||
return defineConfig({
|
||||
...base,
|
||||
|
|
|
|||
|
|
@ -17,9 +17,6 @@ export default defineConfig({
|
|||
...exclude,
|
||||
"src/gateway/**",
|
||||
"extensions/**",
|
||||
"extensions/telegram/**",
|
||||
"extensions/discord/**",
|
||||
"extensions/whatsapp/**",
|
||||
"src/browser/**",
|
||||
"src/line/**",
|
||||
"src/agents/**",
|
||||
|
|
|
|||
Loading…
Reference in New Issue