From bbf494955d3e371e07cb02bd8f3e158aefebd8b3 Mon Sep 17 00:00:00 2001 From: zhang-guiping Date: Thu, 18 Jun 2026 23:49:56 +0800 Subject: [PATCH] fix(outbound): preserve configured directory entries before reserved-literal rejection in resolveMessagingTarget Move the reserved-literal check from before directory lookup to after directory miss, so configured Telegram groups/channels whose directory key is a reserved word (current, self, this, me) still resolve through the directory before failing closed. The reserved check now runs only after the directory returns no match and before plugin fallback resolution. Update the regression test to verify directory-first ordering: a configured directory entry named current resolves successfully, and a directory miss with a reserved literal fails with the descriptive error. --- src/infra/outbound/target-resolver.test.ts | 40 ++++++++++++++++++++-- src/infra/outbound/target-resolver.ts | 11 +++--- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/infra/outbound/target-resolver.test.ts b/src/infra/outbound/target-resolver.test.ts index 9ecbbdd57328..12139cead5c2 100644 --- a/src/infra/outbound/target-resolver.test.ts +++ b/src/infra/outbound/target-resolver.test.ts @@ -130,7 +130,7 @@ describe("resolveMessagingTarget (directory fallback)", () => { expect(mocks.listGroupsLive).toHaveBeenCalledTimes(1); }); - it("rejects plugin-reserved literal targets before directory lookup", async () => { + it("preserves configured directory entries before rejecting reserved literal targets", async () => { mocks.getChannelPlugin.mockReturnValue({ ...createChannelTestPluginBase({ id: "telegram", label: "Telegram" }), directory: { @@ -162,13 +162,47 @@ describe("resolveMessagingTarget (directory fallback)", () => { input: "current", }); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.target.to).toBe("-1002458651455"); + expect(result.target.source).toBe("directory"); + } + expect(mocks.listGroups).toHaveBeenCalled(); + expect(mocks.resolveTarget).not.toHaveBeenCalled(); + }); + + it("rejects reserved literal targets after directory miss", async () => { + mocks.getChannelPlugin.mockReturnValue({ + ...createChannelTestPluginBase({ id: "telegram", label: "Telegram" }), + directory: { + listPeers: mocks.listPeers, + listPeersLive: mocks.listPeersLive, + listGroups: mocks.listGroups, + listGroupsLive: mocks.listGroupsLive, + }, + messaging: { + targetResolver: { + reservedLiterals: ["current", "self", "this", "me"], + hint: "", + resolveTarget: mocks.resolveTarget, + }, + }, + }); + mocks.listGroups.mockResolvedValue([]); + mocks.listGroupsLive.mockResolvedValue([]); + + const result = await resolveMessagingTarget({ + cfg, + channel: "telegram", + input: "current", + }); + expect(result.ok).toBe(false); if (!result.ok) { expect(result.error.message).toContain('Reserved target "current"'); expect(result.error.message).toContain("Telegram"); } - expect(mocks.listGroups).not.toHaveBeenCalled(); - expect(mocks.listGroupsLive).not.toHaveBeenCalled(); + expect(mocks.listGroups).toHaveBeenCalled(); expect(mocks.resolveTarget).not.toHaveBeenCalled(); }); diff --git a/src/infra/outbound/target-resolver.ts b/src/infra/outbound/target-resolver.ts index 4aea73a3caa2..c84060009f6b 100644 --- a/src/infra/outbound/target-resolver.ts +++ b/src/infra/outbound/target-resolver.ts @@ -400,10 +400,6 @@ export async function resolveMessagingTarget(params: { const plugin = params.plugin ?? getChannelPlugin(params.channel); const providerLabel = plugin?.meta?.label ?? params.channel; const hint = plugin?.messaging?.targetResolver?.hint; - const reservedLiteral = resolveReservedTargetLiteral({ raw, plugin }); - if (reservedLiteral) { - return { ok: false, error: reservedTargetLiteralError(providerLabel, reservedLiteral, hint) }; - } const kind = detectTargetKind(params.channel, raw, params.preferredKind, plugin); const normalizedInput = resolveNormalizedTargetInput(params.channel, raw, plugin); const normalized = normalizedInput?.normalized ?? raw; @@ -483,6 +479,13 @@ export async function resolveMessagingTarget(params: { candidates: match.entries, }; } + // Directory miss: reject reserved literals before falling back to plugin + // resolution, so a bare reserved word without a matching directory entry + // does not accidentally resolve to a public channel or incorrect target. + const reservedLiteral = resolveReservedTargetLiteral({ raw, plugin }); + if (reservedLiteral) { + return { ok: false, error: reservedTargetLiteralError(providerLabel, reservedLiteral, hint) }; + } const resolvedFallbackTarget = asResolvedMessagingTarget( await maybeResolvePluginMessagingTarget({ cfg: params.cfg,