fix(telegram): accept /pair as a pairing-code alias (#7363)

Users habitually type /pair from the earlier pairing flow. Keep every
suggested wording on /start (the vendor deep-link convention) and accept
/pair <CODE> as a declared inbound-code-prefix alias so those users pair
instead of looping through the connect nudge.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Benjamin Kurrek
2026-08-07 14:37:05 -04:00
committed by GitHub
parent e6a650f1fe
commit df90072c4e
3 changed files with 32 additions and 8 deletions

View File

@@ -401,11 +401,13 @@ fn fixture_with(
deep_link_template: Option<&str>,
template_values: BTreeMap<String, String>,
) -> Fixture {
// Mirrors the bundled Telegram manifest: `/start` (vendor deep-link
// convention, the only suggested wording) plus the `/pair` alias.
fixture_with_prefixes(
installation,
deep_link_template,
template_values,
&["/start"],
&["/start", "/pair"],
)
}
@@ -1161,7 +1163,7 @@ fn direct_message(text: &str, actor_id: &str) -> NormalizedInboundMessage {
}
#[tokio::test]
async fn interceptor_services_manifest_declared_start_messages_only() {
async fn interceptor_services_manifest_declared_code_prefixes_only() {
let fixture = fixture();
let issue = fixture
.service
@@ -1184,10 +1186,10 @@ async fn interceptor_services_manifest_declared_start_messages_only() {
fixture.service.intercept(&install(), &group).await,
ChannelPairingInterception::NotHandled
);
// `/pair` remains ordinary text because Telegram declares only `/start`.
let pair = direct_message(&format!("/pair {}", issue.code.as_str()), "u-1");
// An undeclared command remains ordinary text.
let link = direct_message(&format!("/link {}", issue.code.as_str()), "u-1");
assert_eq!(
fixture.service.intercept(&install(), &pair).await,
fixture.service.intercept(&install(), &link).await,
ChannelPairingInterception::NotHandled
);
@@ -1208,6 +1210,21 @@ async fn interceptor_services_manifest_declared_start_messages_only() {
.expect("lookup"),
Some(user("alice"))
);
// The `/pair` alias is equally declared: a bound sender re-sending a
// fresh code through it is serviced as the idempotent repair path.
let repair = fixture
.service
.issue_or_rotate(&user("alice"))
.await
.expect("re-mint");
let pair = direct_message(&format!("/pair {}", repair.code.as_str()), "u-1");
assert_eq!(
fixture.service.intercept(&install(), &pair).await,
ChannelPairingInterception::Consumed(ChannelPairingConsumeOutcome::AlreadyPairedSameUser {
user_id: user("alice"),
})
);
}
#[tokio::test]

View File

@@ -43,7 +43,10 @@ submit_label = "Open pairing"
error_message = "Telegram pairing failed. Get a fresh code and try again."
connection_success_message = "Telegram is installed as an inbound entrypoint. If WebChat shows a Telegram pairing panel, tell the user to pair via the link, the QR code, or by sending the shown code to the bot in Telegram — nothing is pasted into normal chat. Once paired the user can DM the bot directly. Telegram exposes no tools and cannot read messages or send on the user's behalf."
deep_link_template = "https://t.me/{bot_username}?start={code}"
inbound_code_prefixes = ["/start"]
# `/start` is the vendor deep-link convention and stays the only prefix any
# instruction wording suggests; `/pair` is an accepted alias for users who
# type it from habit and must never appear in suggested wording.
inbound_code_prefixes = ["/start", "/pair"]
[channel.connection.notices]
connect_required = "👋 Pair your Telegram account in the IronClaw web app, then message me here again."

View File

@@ -2645,7 +2645,11 @@ async fn unbound_telegram_actor_pairs_via_web_minted_code_then_turns_attribute_t
// 6. Mint through the web-side pairing service and consume through the
// real verified webhook again. No direct store/service mutation repairs
// the actor binding in this journey.
// the actor binding in this journey. This leg pairs via the plain
// `/pair CODE` alias (the manifest's second declared prefix — kept for
// muscle-memory compatibility while all suggested wording stays
// `/start`), so both declared prefixes and the untargeted command shape
// stay pinned through the real bundled manifest.
let repaired_code = services
.pairing_mint_for_test("telegram", &paired_user)
.await
@@ -2653,7 +2657,7 @@ async fn unbound_telegram_actor_pairs_via_web_minted_code_then_turns_attribute_t
let status = ingress
.post(
TELEGRAM_ROUTE,
&targeted_start_body(606, 515151, &repaired_code),
&dm_body(606, 515151, &format!("/pair {repaired_code}")),
vec![(
"X-Telegram-Bot-Api-Secret-Token",
TELEGRAM_WEBHOOK_SECRET.to_string(),