Skip to content

Commit 9ffd764

Browse files
committed
fix(feishu): use resolved runtime config for gateway sends
1 parent e24582d commit 9ffd764

3 files changed

Lines changed: 111 additions & 9 deletions

File tree

extensions/feishu/src/media.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,48 @@ describe("sendMediaFeishu msg_type routing", () => {
266266
expect(callData<{ msg_type?: string }>(messageCreateMock).msg_type).toBe("media");
267267
});
268268

269+
it("uses resolved runtime account credentials when creating the media upload client", async () => {
270+
resolveFeishuAccountMock.mockReturnValue({
271+
configured: true,
272+
accountId: "main",
273+
config: {},
274+
appId: "cli_resolved",
275+
appSecret: "resolved-feishu-app-secret", // pragma: allowlist secret
276+
domain: "feishu",
277+
});
278+
loadWebMediaMock.mockResolvedValueOnce({
279+
buffer: Buffer.from("remote-image"),
280+
fileName: "photo.png",
281+
kind: "image",
282+
contentType: "image/png",
283+
});
284+
285+
await sendMediaFeishu({
286+
cfg: {
287+
channels: {
288+
feishu: {
289+
appId: "cli_resolved",
290+
appSecret: {
291+
source: "env",
292+
provider: "default",
293+
id: "FEISHU_SECRET_REF_NOT_FOR_CLIENT",
294+
},
295+
},
296+
},
297+
} as never,
298+
to: "user:ou_target",
299+
mediaUrl: "https://example.com/photo.png",
300+
accountId: "main",
301+
});
302+
303+
const clientConfig = mockCallArg<{ appSecret?: string }>(createFeishuClientMock, 0, 0);
304+
expect(clientConfig.appSecret).toBe("resolved-feishu-app-secret");
305+
expect(JSON.stringify(createFeishuClientMock.mock.calls)).not.toContain(
306+
"FEISHU_SECRET_REF_NOT_FOR_CLIENT",
307+
);
308+
expect(JSON.stringify(loadWebMediaMock.mock.calls)).toContain("https://example.com/photo.png");
309+
});
310+
269311
it("falls back to generic file for unsupported audio formats", async () => {
270312
loadWebMediaMock.mockResolvedValueOnce({
271313
buffer: Buffer.from("remote-mp3"),

src/gateway/server-methods/send.test.ts

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,64 @@ describe("gateway send mirroring", () => {
379379
expect(response?.[0]).toBe(true);
380380
});
381381

382+
it("uses the resolved runtime config for gateway sends when Feishu appSecret is SecretRef-backed", async () => {
383+
const sourceConfig = {
384+
channels: {
385+
feishu: {
386+
appId: "cli_source",
387+
appSecret: {
388+
source: "env",
389+
provider: "default",
390+
id: "FEISHU_APP_SECRET",
391+
},
392+
},
393+
},
394+
};
395+
const runtimeConfig = {
396+
channels: {
397+
feishu: {
398+
appId: "cli_source",
399+
appSecret: "resolved-feishu-secret",
400+
},
401+
},
402+
};
403+
mocks.applyPluginAutoEnable.mockImplementation(({ config }) => ({
404+
config,
405+
changes: [],
406+
autoEnabledReasons: {},
407+
}));
408+
mocks.getRuntimeConfigSnapshot.mockReturnValue(runtimeConfig);
409+
mocks.getRuntimeConfigSourceSnapshot.mockReturnValue(sourceConfig);
410+
mocks.deliverOutboundPayloads.mockResolvedValue([
411+
{ messageId: "feishu-media", channel: "feishu" },
412+
]);
413+
414+
const context = {
415+
...makeContext(),
416+
getRuntimeConfig: () => sourceConfig,
417+
} as unknown as GatewayRequestContext;
418+
const respond = vi.fn();
419+
await sendHandlers.send({
420+
params: {
421+
channel: "feishu",
422+
to: "chat:oc_target",
423+
mediaUrl: "https://example.com/image.png",
424+
idempotencyKey: "idem-feishu-runtime-config",
425+
} as never,
426+
respond,
427+
context,
428+
req: { type: "req", id: "1", method: "send" },
429+
client: null as never,
430+
isWebchatConnect: () => false,
431+
});
432+
433+
expect(deliveryCall()?.cfg).toBe(runtimeConfig);
434+
expect(JSON.stringify(deliveryCall()?.cfg)).toContain("resolved-feishu-secret");
435+
expect(JSON.stringify(deliveryCall()?.cfg)).not.toContain("FEISHU_APP_SECRET");
436+
const response = firstRespondCall(respond);
437+
expect(response?.[0]).toBe(true);
438+
});
439+
382440
it("matches message.action runtime config against the canonical pre-auto-enable source config", async () => {
383441
const sourceConfig = {
384442
channels: {
@@ -549,18 +607,19 @@ describe("gateway send mirroring", () => {
549607
expect(lastDispatchChannelMessageActionCall()?.cfg).toBe(autoEnabledRequestConfig);
550608
});
551609

552-
it("does not read the runtime config snapshot for send requests", async () => {
553-
mockDeliverySuccess("m-no-runtime-config-read");
610+
it("checks the runtime config snapshot for send requests", async () => {
611+
mockDeliverySuccess("m-runtime-config-checked");
554612

555613
await runSend({
556614
to: "channel:C1",
557615
message: "hi",
558616
channel: "slack",
559-
idempotencyKey: "idem-send-no-runtime-config-read",
617+
idempotencyKey: "idem-send-runtime-config-checked",
560618
});
561619

562-
expect(mocks.getRuntimeConfigSnapshot).not.toHaveBeenCalled();
563-
expect(mocks.getRuntimeConfigSourceSnapshot).not.toHaveBeenCalled();
620+
expect(mocks.getRuntimeConfigSnapshot).toHaveBeenCalledTimes(1);
621+
expect(mocks.getRuntimeConfigSourceSnapshot).toHaveBeenCalledTimes(1);
622+
expect(deliveryCall()?.cfg).toEqual({});
564623
});
565624

566625
it("dedupes concurrent message.action requests while inflight", async () => {

src/gateway/server-methods/send.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ function resolveGatewayOutboundTarget(params: {
261261
return { ok: true, to: resolved.to };
262262
}
263263

264-
function resolveMessageActionRuntimeConfig(params: {
264+
function resolveCurrentMessageRuntimeConfig(params: {
265265
cfg: OpenClawConfig;
266266
sourceCfg: OpenClawConfig;
267267
}): OpenClawConfig {
@@ -275,7 +275,7 @@ function resolveMessageActionRuntimeConfig(params: {
275275
runtimeConfig,
276276
runtimeSourceConfig,
277277
});
278-
// Message actions must use the hot runtime snapshot when it matches the caller's source config.
278+
// Message sends/actions must use the hot runtime snapshot when it matches the caller's source config.
279279
if (selected === runtimeConfig && selected !== params.cfg) {
280280
return resolveGatewayPluginConfig({ config: selected });
281281
}
@@ -492,7 +492,7 @@ export const sendHandlers: GatewayRequestHandlers = {
492492
return { ok: false, error: resolvedChannel.error };
493493
}
494494
const { cfg: selectedCfg, sourceCfg, channel } = resolvedChannel;
495-
const cfg = resolveMessageActionRuntimeConfig({ cfg: selectedCfg, sourceCfg });
495+
const cfg = resolveCurrentMessageRuntimeConfig({ cfg: selectedCfg, sourceCfg });
496496
const plugin = resolveOutboundChannelPlugin({ channel, cfg });
497497
if (!plugin?.actions?.handleAction) {
498498
return {
@@ -630,7 +630,8 @@ export const sendHandlers: GatewayRequestHandlers = {
630630
if (resolvedChannel.kind !== "ready") {
631631
return resolvedChannel.result;
632632
}
633-
const { cfg, channel } = resolvedChannel;
633+
const { cfg: selectedCfg, sourceCfg, channel } = resolvedChannel;
634+
const cfg = resolveCurrentMessageRuntimeConfig({ cfg: selectedCfg, sourceCfg });
634635
const outboundChannel = channel;
635636
const plugin = resolveOutboundChannelPlugin({ channel, cfg });
636637
if (!plugin) {

0 commit comments

Comments
 (0)