Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions apps/bot/src/discord/connect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@ vi.mock("./gates.js", async (orig) => ({
...(await orig<typeof import("./gates.js")>()),
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,
Expand Down Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions apps/bot/src/discord/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -720,7 +720,8 @@ function openaiModal(): ModalBuilder {
new ActionRowBuilder<TextInputBuilder>().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),
Expand All @@ -744,7 +745,8 @@ function openrouterModal(): ModalBuilder {
new ActionRowBuilder<TextInputBuilder>().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),
Expand Down
Loading