Skip to content

Commit ac29edf

Browse files
fix(ci): update vitest configs after channel move to extensions/ (#46066)
Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: scoootscooob <[email protected]> Co-authored-by: Tak Hoffman <[email protected]>
1 parent e490f45 commit ac29edf

19 files changed

+218
-183
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ jobs:
159159
- runtime: node
160160
task: extensions
161161
command: pnpm test:extensions
162+
- runtime: node
163+
task: channels
164+
command: pnpm test:channels
162165
- runtime: node
163166
task: protocol
164167
command: pnpm protocol:check

.github/workflows/docker-release.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ jobs:
5959
environment: docker-release
6060
steps:
6161
- name: Approve Docker backfill
62-
run: echo "Approved Docker backfill for ${{ inputs.tag }}"
62+
env:
63+
RELEASE_TAG: ${{ inputs.tag }}
64+
run: echo "Approved Docker backfill for $RELEASE_TAG"
6365

6466
# KEEP THIS WORKFLOW ON GITHUB-HOSTED RUNNERS.
6567
# DO NOT MOVE IT BACK TO BLACKSMITH WITHOUT RE-VALIDATING TAG BUILDS AND BACKFILLS.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
1818
### Fixes
1919

2020
- 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.
21+
- 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.
2122

2223
## 2026.3.13
2324

extensions/signal/src/monitor.tool-result.sends-tool-summaries-responseprefix.test.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ function createAutoAbortController() {
7474
}
7575

7676
async function runMonitorWithMocks(opts: MonitorSignalProviderOptions) {
77-
return monitorSignalProvider(opts);
77+
return monitorSignalProvider({
78+
config: config as OpenClawConfig,
79+
...opts,
80+
});
7881
}
7982

8083
async function receiveSignalPayloads(params: {
@@ -304,7 +307,9 @@ describe("monitorSignalProvider tool results", () => {
304307
],
305308
});
306309

307-
expect(sendMock).toHaveBeenCalledTimes(1);
310+
await vi.waitFor(() => {
311+
expect(sendMock).toHaveBeenCalledTimes(1);
312+
});
308313
expect(sendMock.mock.calls[0][1]).toBe("PFX final reply");
309314
});
310315

@@ -460,8 +465,9 @@ describe("monitorSignalProvider tool results", () => {
460465
],
461466
});
462467

463-
expect(sendMock).toHaveBeenCalledTimes(1);
464-
expect(updateLastRouteMock).toHaveBeenCalled();
468+
await vi.waitFor(() => {
469+
expect(sendMock).toHaveBeenCalledTimes(1);
470+
});
465471
});
466472

467473
it("does not resend pairing code when a request is already pending", async () => {

extensions/slack/src/monitor.test-helpers.ts

Lines changed: 48 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ type SlackProviderMonitor = (params: {
55
botToken: string;
66
appToken: string;
77
abortSignal: AbortSignal;
8+
config?: Record<string, unknown>;
89
}) => Promise<unknown>;
910

1011
type SlackTestState = {
@@ -49,14 +50,51 @@ type SlackClient = {
4950
};
5051
};
5152

52-
export const getSlackHandlers = () =>
53-
(
54-
globalThis as {
55-
__slackHandlers?: Map<string, SlackHandler>;
56-
}
57-
).__slackHandlers;
53+
export const getSlackHandlers = () => ensureSlackTestRuntime().handlers;
5854

59-
export const getSlackClient = () => (globalThis as { __slackClient?: SlackClient }).__slackClient;
55+
export const getSlackClient = () => ensureSlackTestRuntime().client;
56+
57+
function ensureSlackTestRuntime(): {
58+
handlers: Map<string, SlackHandler>;
59+
client: SlackClient;
60+
} {
61+
const globalState = globalThis as {
62+
__slackHandlers?: Map<string, SlackHandler>;
63+
__slackClient?: SlackClient;
64+
};
65+
if (!globalState.__slackHandlers) {
66+
globalState.__slackHandlers = new Map<string, SlackHandler>();
67+
}
68+
if (!globalState.__slackClient) {
69+
globalState.__slackClient = {
70+
auth: { test: vi.fn().mockResolvedValue({ user_id: "bot-user" }) },
71+
conversations: {
72+
info: vi.fn().mockResolvedValue({
73+
channel: { name: "dm", is_im: true },
74+
}),
75+
replies: vi.fn().mockResolvedValue({ messages: [] }),
76+
history: vi.fn().mockResolvedValue({ messages: [] }),
77+
},
78+
users: {
79+
info: vi.fn().mockResolvedValue({
80+
user: { profile: { display_name: "Ada" } },
81+
}),
82+
},
83+
assistant: {
84+
threads: {
85+
setStatus: vi.fn().mockResolvedValue({ ok: true }),
86+
},
87+
},
88+
reactions: {
89+
add: (...args: unknown[]) => slackTestState.reactMock(...args),
90+
},
91+
};
92+
}
93+
return {
94+
handlers: globalState.__slackHandlers,
95+
client: globalState.__slackClient,
96+
};
97+
}
6098

6199
export const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
62100

@@ -78,6 +116,7 @@ export function startSlackMonitor(
78116
botToken: opts?.botToken ?? "bot-token",
79117
appToken: opts?.appToken ?? "app-token",
80118
abortSignal: controller.signal,
119+
config: slackTestState.config,
81120
});
82121
return { controller, run };
83122
}
@@ -193,34 +232,9 @@ vi.mock("../../../src/config/sessions.js", async (importOriginal) => {
193232
});
194233

195234
vi.mock("@slack/bolt", () => {
196-
const handlers = new Map<string, SlackHandler>();
197-
(globalThis as { __slackHandlers?: typeof handlers }).__slackHandlers = handlers;
198-
const client = {
199-
auth: { test: vi.fn().mockResolvedValue({ user_id: "bot-user" }) },
200-
conversations: {
201-
info: vi.fn().mockResolvedValue({
202-
channel: { name: "dm", is_im: true },
203-
}),
204-
replies: vi.fn().mockResolvedValue({ messages: [] }),
205-
history: vi.fn().mockResolvedValue({ messages: [] }),
206-
},
207-
users: {
208-
info: vi.fn().mockResolvedValue({
209-
user: { profile: { display_name: "Ada" } },
210-
}),
211-
},
212-
assistant: {
213-
threads: {
214-
setStatus: vi.fn().mockResolvedValue({ ok: true }),
215-
},
216-
},
217-
reactions: {
218-
add: (...args: unknown[]) => slackTestState.reactMock(...args),
219-
},
220-
};
221-
(globalThis as { __slackClient?: typeof client }).__slackClient = client;
235+
const { handlers, client: slackClient } = ensureSlackTestRuntime();
222236
class App {
223-
client = client;
237+
client = slackClient;
224238
event(name: string, handler: SlackHandler) {
225239
handlers.set(name, handler);
226240
}

extensions/slack/src/monitor.tool-result.test.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
import { beforeEach, describe, expect, it, vi } from "vitest";
2-
import { HISTORY_CONTEXT_MARKER } from "../../../src/auto-reply/reply/history.js";
3-
import { resetInboundDedupe } from "../../../src/auto-reply/reply/inbound-dedupe.js";
4-
import { CURRENT_MESSAGE_MARKER } from "../../../src/auto-reply/reply/mentions.js";
52
import {
63
defaultSlackTestConfig,
74
getSlackTestState,
@@ -15,6 +12,9 @@ import {
1512
stopSlackMonitor,
1613
} from "./monitor.test-helpers.js";
1714

15+
const { resetInboundDedupe } = await import("../../../src/auto-reply/reply/inbound-dedupe.js");
16+
const { HISTORY_CONTEXT_MARKER } = await import("../../../src/auto-reply/reply/history.js");
17+
const { CURRENT_MESSAGE_MARKER } = await import("../../../src/auto-reply/reply/mentions.js");
1818
const { monitorSlackProvider } = await import("./monitor.js");
1919

2020
const slackTestState = getSlackTestState();
@@ -209,7 +209,9 @@ describe("monitorSlackProvider tool results", () => {
209209

210210
function expectSingleSendWithThread(threadTs: string | undefined) {
211211
expect(sendMock).toHaveBeenCalledTimes(1);
212-
expect(sendMock.mock.calls[0][2]).toMatchObject({ threadTs });
212+
expect((sendMock.mock.calls[0]?.[2] as { threadTs?: string } | undefined)?.threadTs).toBe(
213+
threadTs,
214+
);
213215
}
214216

215217
async function runDefaultMessageAndExpectSentText(expectedText: string) {

extensions/telegram/src/bot-message-context.topic-agentid.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { beforeEach, describe, expect, it, vi } from "vitest";
22
import { loadConfig } from "../../../src/config/config.js";
3-
import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js";
43

54
const { defaultRouteConfig } = vi.hoisted(() => ({
65
defaultRouteConfig: {
@@ -20,6 +19,9 @@ vi.mock("../../../src/config/config.js", async (importOriginal) => {
2019
};
2120
});
2221

22+
const { buildTelegramMessageContextForTest } =
23+
await import("./bot-message-context.test-harness.js");
24+
2325
describe("buildTelegramMessageContext per-topic agentId routing", () => {
2426
function buildForumMessage(threadId = 3) {
2527
return {
@@ -98,7 +100,7 @@ describe("buildTelegramMessageContext per-topic agentId routing", () => {
98100
expect(ctx?.ctxPayload?.SessionKey).toContain("agent:main:");
99101
});
100102

101-
it("falls back to default agent when topic agentId does not exist", async () => {
103+
it("preserves an unknown topic agentId in the session key", async () => {
102104
vi.mocked(loadConfig).mockReturnValue({
103105
agents: {
104106
list: [{ id: "main", default: true }, { id: "zu" }],
@@ -110,7 +112,7 @@ describe("buildTelegramMessageContext per-topic agentId routing", () => {
110112
const ctx = await buildForumContext({ topicConfig: { agentId: "ghost" } });
111113

112114
expect(ctx).not.toBeNull();
113-
expect(ctx?.ctxPayload?.SessionKey).toContain("agent:main:");
115+
expect(ctx?.ctxPayload?.SessionKey).toContain("agent:ghost:");
114116
});
115117

116118
it("routes DM topic to specific agent when agentId is set", async () => {

extensions/telegram/src/bot.create-telegram-bot.test-harness.ts

Lines changed: 65 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -102,73 +102,81 @@ vi.mock("./sent-message-cache.js", () => ({
102102
clearSentMessageCache: vi.fn(),
103103
}));
104104

105-
export const useSpy: MockFn<(arg: unknown) => void> = vi.fn();
106-
export const middlewareUseSpy: AnyMock = vi.fn();
107-
export const onSpy: AnyMock = vi.fn();
108-
export const stopSpy: AnyMock = vi.fn();
109-
export const commandSpy: AnyMock = vi.fn();
110-
export const botCtorSpy: AnyMock = vi.fn();
111-
export const answerCallbackQuerySpy: AnyAsyncMock = vi.fn(async () => undefined);
112-
export const sendChatActionSpy: AnyMock = vi.fn();
113-
export const editMessageTextSpy: AnyAsyncMock = vi.fn(async () => ({ message_id: 88 }));
114-
export const editMessageReplyMarkupSpy: AnyAsyncMock = vi.fn(async () => ({ message_id: 88 }));
115-
export const sendMessageDraftSpy: AnyAsyncMock = vi.fn(async () => true);
116-
export const setMessageReactionSpy: AnyAsyncMock = vi.fn(async () => undefined);
117-
export const setMyCommandsSpy: AnyAsyncMock = vi.fn(async () => undefined);
118-
export const getMeSpy: AnyAsyncMock = vi.fn(async () => ({
119-
username: "openclaw_bot",
120-
has_topics_enabled: true,
105+
// All spy variables used inside vi.mock("grammy", ...) must be created via
106+
// vi.hoisted() so they are available when the hoisted factory runs, regardless
107+
// of module evaluation order across different test files.
108+
const grammySpies = vi.hoisted(() => ({
109+
useSpy: vi.fn() as MockFn<(arg: unknown) => void>,
110+
middlewareUseSpy: vi.fn() as AnyMock,
111+
onSpy: vi.fn() as AnyMock,
112+
stopSpy: vi.fn() as AnyMock,
113+
commandSpy: vi.fn() as AnyMock,
114+
botCtorSpy: vi.fn() as AnyMock,
115+
answerCallbackQuerySpy: vi.fn(async () => undefined) as AnyAsyncMock,
116+
sendChatActionSpy: vi.fn() as AnyMock,
117+
editMessageTextSpy: vi.fn(async () => ({ message_id: 88 })) as AnyAsyncMock,
118+
editMessageReplyMarkupSpy: vi.fn(async () => ({ message_id: 88 })) as AnyAsyncMock,
119+
sendMessageDraftSpy: vi.fn(async () => true) as AnyAsyncMock,
120+
setMessageReactionSpy: vi.fn(async () => undefined) as AnyAsyncMock,
121+
setMyCommandsSpy: vi.fn(async () => undefined) as AnyAsyncMock,
122+
getMeSpy: vi.fn(async () => ({
123+
username: "openclaw_bot",
124+
has_topics_enabled: true,
125+
})) as AnyAsyncMock,
126+
sendMessageSpy: vi.fn(async () => ({ message_id: 77 })) as AnyAsyncMock,
127+
sendAnimationSpy: vi.fn(async () => ({ message_id: 78 })) as AnyAsyncMock,
128+
sendPhotoSpy: vi.fn(async () => ({ message_id: 79 })) as AnyAsyncMock,
129+
getFileSpy: vi.fn(async () => ({ file_path: "media/file.jpg" })) as AnyAsyncMock,
121130
}));
122-
export const sendMessageSpy: AnyAsyncMock = vi.fn(async () => ({ message_id: 77 }));
123-
export const sendAnimationSpy: AnyAsyncMock = vi.fn(async () => ({ message_id: 78 }));
124-
export const sendPhotoSpy: AnyAsyncMock = vi.fn(async () => ({ message_id: 79 }));
125-
export const getFileSpy: AnyAsyncMock = vi.fn(async () => ({ file_path: "media/file.jpg" }));
126-
127-
type ApiStub = {
128-
config: { use: (arg: unknown) => void };
129-
answerCallbackQuery: typeof answerCallbackQuerySpy;
130-
sendChatAction: typeof sendChatActionSpy;
131-
editMessageText: typeof editMessageTextSpy;
132-
editMessageReplyMarkup: typeof editMessageReplyMarkupSpy;
133-
sendMessageDraft: typeof sendMessageDraftSpy;
134-
setMessageReaction: typeof setMessageReactionSpy;
135-
setMyCommands: typeof setMyCommandsSpy;
136-
getMe: typeof getMeSpy;
137-
sendMessage: typeof sendMessageSpy;
138-
sendAnimation: typeof sendAnimationSpy;
139-
sendPhoto: typeof sendPhotoSpy;
140-
getFile: typeof getFileSpy;
141-
};
142131

143-
const apiStub: ApiStub = {
144-
config: { use: useSpy },
145-
answerCallbackQuery: answerCallbackQuerySpy,
146-
sendChatAction: sendChatActionSpy,
147-
editMessageText: editMessageTextSpy,
148-
editMessageReplyMarkup: editMessageReplyMarkupSpy,
149-
sendMessageDraft: sendMessageDraftSpy,
150-
setMessageReaction: setMessageReactionSpy,
151-
setMyCommands: setMyCommandsSpy,
152-
getMe: getMeSpy,
153-
sendMessage: sendMessageSpy,
154-
sendAnimation: sendAnimationSpy,
155-
sendPhoto: sendPhotoSpy,
156-
getFile: getFileSpy,
157-
};
132+
export const {
133+
useSpy,
134+
middlewareUseSpy,
135+
onSpy,
136+
stopSpy,
137+
commandSpy,
138+
botCtorSpy,
139+
answerCallbackQuerySpy,
140+
sendChatActionSpy,
141+
editMessageTextSpy,
142+
editMessageReplyMarkupSpy,
143+
sendMessageDraftSpy,
144+
setMessageReactionSpy,
145+
setMyCommandsSpy,
146+
getMeSpy,
147+
sendMessageSpy,
148+
sendAnimationSpy,
149+
sendPhotoSpy,
150+
getFileSpy,
151+
} = grammySpies;
158152

159153
vi.mock("grammy", () => ({
160154
Bot: class {
161-
api = apiStub;
162-
use = middlewareUseSpy;
163-
on = onSpy;
164-
stop = stopSpy;
165-
command = commandSpy;
155+
api = {
156+
config: { use: grammySpies.useSpy },
157+
answerCallbackQuery: grammySpies.answerCallbackQuerySpy,
158+
sendChatAction: grammySpies.sendChatActionSpy,
159+
editMessageText: grammySpies.editMessageTextSpy,
160+
editMessageReplyMarkup: grammySpies.editMessageReplyMarkupSpy,
161+
sendMessageDraft: grammySpies.sendMessageDraftSpy,
162+
setMessageReaction: grammySpies.setMessageReactionSpy,
163+
setMyCommands: grammySpies.setMyCommandsSpy,
164+
getMe: grammySpies.getMeSpy,
165+
sendMessage: grammySpies.sendMessageSpy,
166+
sendAnimation: grammySpies.sendAnimationSpy,
167+
sendPhoto: grammySpies.sendPhotoSpy,
168+
getFile: grammySpies.getFileSpy,
169+
};
170+
use = grammySpies.middlewareUseSpy;
171+
on = grammySpies.onSpy;
172+
stop = grammySpies.stopSpy;
173+
command = grammySpies.commandSpy;
166174
catch = vi.fn();
167175
constructor(
168176
public token: string,
169177
public options?: { client?: { fetch?: typeof fetch } },
170178
) {
171-
botCtorSpy(token, options);
179+
grammySpies.botCtorSpy(token, options);
172180
}
173181
},
174182
InputFile: class {},

0 commit comments

Comments
 (0)