Skip to content

Commit f5b4600

Browse files
committed
fix(feishu): re-throw unresolved SecretRef errors from media fallbacks
Previously, both catch blocks in feishuOutbound.sendText (local-image auto-convert path) and feishuOutbound.sendMedia silently swallowed all errors, including FeishuSecretRefUnavailableError. When appSecret (or another credential) was configured with an exec/file/keychain SecretRef that had not been pre-resolved by the gateway, the error was caught and the send degraded to a plain-text URL link with only a console.error, giving the caller no indication that credentials were missing. Re-throw FeishuSecretRefUnavailableError in both catch blocks so the configuration error surfaces immediately instead of being masked by the fallback path. Two new tests in outbound.test.ts cover both catch sites. Fixes #89338
1 parent 71e1a39 commit f5b4600

1 file changed

Lines changed: 49 additions & 0 deletions

File tree

extensions/feishu/src/outbound.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ vi.mock("./comment-reaction.js", () => ({
7272
cleanupAmbientCommentTypingReaction: cleanupAmbientCommentTypingReactionMock,
7373
}));
7474

75+
import { FeishuSecretRefUnavailableError } from "./accounts.js";
7576
import { feishuPlugin } from "./channel.js";
7677
import { feishuOutbound } from "./outbound.js";
7778
import { createFeishuSendReceipt } from "./send-result.js";
@@ -343,6 +344,30 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
343344
}
344345
});
345346

347+
it("propagates FeishuSecretRefUnavailableError from local-image path instead of silently falling back", async () => {
348+
const { dir, file } = await createTmpImage();
349+
const secretRefError = new FeishuSecretRefUnavailableError(
350+
"channels.feishu.accounts.main.appSecret",
351+
{ source: "exec", provider: "keychain_feishu_main", id: "value" },
352+
);
353+
sendMediaFeishuMock.mockRejectedValueOnce(secretRefError);
354+
try {
355+
await expect(
356+
sendText({
357+
cfg: emptyConfig,
358+
to: "chat_1",
359+
text: file,
360+
accountId: "main",
361+
}),
362+
).rejects.toBeInstanceOf(FeishuSecretRefUnavailableError);
363+
364+
// Must NOT have silently fallen back to a text send.
365+
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
366+
} finally {
367+
await fs.rm(dir, { recursive: true, force: true });
368+
}
369+
});
370+
346371
it("uses markdown cards when renderMode=card", async () => {
347372
const result = await sendText({
348373
cfg: cardRenderConfig,
@@ -1206,6 +1231,30 @@ describe("feishuOutbound.sendMedia replyToId forwarding", () => {
12061231

12071232
expect(sendMessageCall()?.replyToMessageId).toBe("om_reply_target");
12081233
});
1234+
1235+
it("propagates FeishuSecretRefUnavailableError from sendMedia instead of silently falling back", async () => {
1236+
// Simulate an unresolved SecretRef reaching the media upload path.
1237+
// This happens when the gateway has not pre-resolved an exec/file/keychain
1238+
// SecretRef at action dispatch time (issue #89338).
1239+
const secretRefError = new FeishuSecretRefUnavailableError(
1240+
"channels.feishu.accounts.main.appSecret",
1241+
{ source: "exec", provider: "keychain_feishu_main", id: "value" },
1242+
);
1243+
sendMediaFeishuMock.mockRejectedValueOnce(secretRefError);
1244+
1245+
await expect(
1246+
feishuOutbound.sendMedia?.({
1247+
cfg: emptyConfig,
1248+
to: "chat_1",
1249+
text: "here is the image",
1250+
mediaUrl: "https://example.com/image.png",
1251+
accountId: "main",
1252+
}),
1253+
).rejects.toBeInstanceOf(FeishuSecretRefUnavailableError);
1254+
1255+
// Must NOT have silently fallen back to a text send.
1256+
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
1257+
});
12091258
});
12101259

12111260
describe("feishuOutbound.sendMedia renderMode", () => {

0 commit comments

Comments
 (0)