From 2bd5b1281d5a67f457b07bacb64e12ffc9889df3 Mon Sep 17 00:00:00 2001 From: MoKnowOrg Date: Sun, 21 Jun 2026 20:20:09 +0530 Subject: [PATCH] fix(connect): OpenRouter modal label exceeded Discord's 45-char limit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OpenRouter "Model (blank for default, e.g. openrouter/auto)" label was 47 chars; showModal() then threw and Discord showed "Something went wrong". Shorten both OpenAI/OpenRouter model labels to "Model (optional)" and move the example into the placeholder (limit 100). Add a regression test driving every provider modal and asserting label ≤45 / placeholder ≤100. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/bot/src/discord/connect.test.ts | 37 ++++++++++++++++++++++++++++ apps/bot/src/discord/connect.ts | 6 +++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/apps/bot/src/discord/connect.test.ts b/apps/bot/src/discord/connect.test.ts index 77deb81..a222fbd 100644 --- a/apps/bot/src/discord/connect.test.ts +++ b/apps/bot/src/discord/connect.test.ts @@ -21,9 +21,12 @@ vi.mock("./gates.js", async (orig) => ({ ...(await orig()), ensureGuild: vi.fn(async () => ({})), })); +vi.mock("../flags.js", () => ({ isClaudeOauthEnabled: vi.fn(async () => true) })); +import type { ButtonInteraction } from "discord.js"; import { validateLlmAuth } from "../llm/credentials.js"; import { + handleLlmButton, handleLlmModal, clearLlmCredentialWithRetry, type LlmCredStore, @@ -174,6 +177,40 @@ describe("Bounded-retry credential removal (Property 20; Req 8.4,8.5,8.6)", () = }); }); +describe("provider modals respect Discord field limits (showModal regression)", () => { + // A label >45 chars makes showModal throw → Discord's "Something went wrong". + // Drive each provider button and assert every TextInput label ≤45, placeholder ≤100. + it.each(["anthropic_api_key", "claude_oauth", "custom", "openai", "openrouter"])( + "%s modal labels are within limits", + async (action) => { + let modal: { toJSON(): unknown } | undefined; + const interaction = { + memberPermissions: { has: (_p: unknown) => true }, + guildId: "g1", + showModal: vi.fn(async (m: { toJSON(): unknown }) => { + modal = m; + }), + reply: vi.fn(async () => {}), + } as unknown as ButtonInteraction; + const ctx = { db: {}, config } as unknown as BotContext; + await handleLlmButton(ctx, interaction, action); + expect(modal).toBeDefined(); + const json = modal!.toJSON() as { + components: { components: { label: string; placeholder?: string }[] }[]; + }; + for (const row of json.components) { + for (const input of row.components) { + expect(input.label.length).toBeLessThanOrEqual(45); + expect(input.label.length).toBeGreaterThanOrEqual(1); + if (input.placeholder != null) { + expect(input.placeholder.length).toBeLessThanOrEqual(100); + } + } + } + }, + ); +}); + describe("Connect_Flow chooser, modal limits, gating (task 7.7)", () => { it("non-admin modal submit is gated before persistence is irrelevant — admin gate lives on the button", async () => { // handleLlmButton gates on ManageGuild before showing any modal; a modal diff --git a/apps/bot/src/discord/connect.ts b/apps/bot/src/discord/connect.ts index 6f583ba..3d71e0a 100644 --- a/apps/bot/src/discord/connect.ts +++ b/apps/bot/src/discord/connect.ts @@ -720,7 +720,8 @@ function openaiModal(): ModalBuilder { new ActionRowBuilder().addComponents( new TextInputBuilder() .setCustomId("model") - .setLabel("Model (blank for default, e.g. gpt-4o-mini)") + .setLabel("Model (optional)") + .setPlaceholder("blank for default, e.g. gpt-4o-mini") .setStyle(TextInputStyle.Short) .setMaxLength(256) .setRequired(false), @@ -744,7 +745,8 @@ function openrouterModal(): ModalBuilder { new ActionRowBuilder().addComponents( new TextInputBuilder() .setCustomId("model") - .setLabel("Model (blank for default, e.g. openrouter/auto)") + .setLabel("Model (optional)") + .setPlaceholder("blank for default, e.g. openrouter/auto") .setStyle(TextInputStyle.Short) .setMaxLength(200) .setRequired(false),