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.
This commit is contained in:
zhang-guiping
2026-06-18 23:49:56 +08:00
committed by Ayaan Zaidi
parent f12ade0082
commit bbf494955d
2 changed files with 44 additions and 7 deletions

View File

@@ -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: "<chatId>",
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();
});

View File

@@ -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,