From 340e421b022b28b563e6a00e69188b382f280675 Mon Sep 17 00:00:00 2001 From: Simon Grieve Date: Tue, 29 Sep 2026 11:04:13 +0100 Subject: [PATCH] Release ctrlaltbot 1.1.3 Fix /ask and /docs search failing with "didn't respond in time" when a docs site's home page is among the results. Its empty slug produced an empty select-menu option value, which makes Discord reject the reply. /docs page now searches for text that isn't a page slug instead of replying "Page not found". --- CHANGELOG.md | 10 ++++++++++ package-lock.json | 4 ++-- package.json | 2 +- src/discord/responses.ts | 3 ++- src/index.ts | 7 ++++++- test/responses.test.ts | 8 ++++++++ 6 files changed, 29 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f02c77..2c613a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project are documented here. +## [1.1.3] - 2026-09-29 + +### Fixed + +- `/ask` and `/docs search` failed with "didn't respond in time" whenever a docs site's home page was among the results: its empty slug made an empty menu option, and Discord rejects the whole reply for that. The home page is now left out of the results menu. + +### Changed + +- `/docs page` with text that isn't a page slug, such as `tiktok`, now searches for that text instead of replying "Page not found". + ## [1.1.2] - 2026-09-29 ### Fixed diff --git a/package-lock.json b/package-lock.json index 7379511..6a86548 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ctrlaltbot", - "version": "1.1.2", + "version": "1.1.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ctrlaltbot", - "version": "1.1.2", + "version": "1.1.3", "license": "ISC", "dependencies": { "discord-interactions": "^4.4.0" diff --git a/package.json b/package.json index 1d86a4c..6b38670 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ctrlaltbot", - "version": "1.1.2", + "version": "1.1.3", "description": "Discord bot for ctrl alt doc documentation sites, running as a Cloudflare Worker.", "repository": { "type": "git", diff --git a/src/discord/responses.ts b/src/discord/responses.ts index 38fe2cb..ec60858 100644 --- a/src/discord/responses.ts +++ b/src/discord/responses.ts @@ -135,7 +135,8 @@ function searchResultSelect(results: SearchResult[], selectedValue: string): Sel for (const result of results) { const value = resultValue(result.slug, result.heading?.id); - if (value.length > MAX_CHOICE_LENGTH || seen.has(value)) continue; + // The home page has an empty slug, and Discord rejects the whole reply if any option value is empty. + if (!result.slug || value.length > MAX_CHOICE_LENGTH || seen.has(value)) continue; seen.add(value); const option: SelectOption = { label: truncate(resultTitle(result), MAX_CHOICE_LENGTH), value, default: value === selectedValue }; diff --git a/src/index.ts b/src/index.ts index 7f07c24..cd7eaf8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -133,9 +133,14 @@ async function pageMessage(slug: string, header: string | undefined, env: Env, b return formatPageResult(page, env.CAD_BASE_URL, brand, header); } catch (error) { + // Text typed instead of a suggestion, such as "tiktok", is more useful as a search than as an error. + if (error instanceof Error && error.message === 'CAD_NOT_FOUND') { + return searchMessage(slug, env, brand); + } + console.error('CAD page failed:', describeCadError(error)); - return cadErrorMessage(error, brand, `There’s no page at \`${slug}\`. Pick one of the suggestions while typing, or try \`/ask\`.`); + return cadErrorMessage(error, brand); } } diff --git a/test/responses.test.ts b/test/responses.test.ts index c61b01e..2306702 100644 --- a/test/responses.test.ts +++ b/test/responses.test.ts @@ -59,6 +59,14 @@ test('omits the menu for a single result and skips values Discord would reject', assert.equal(select(formatSearchResults([results[1]!, longSlug], baseUrl, 'pasta', brand) as Message), undefined); }); +test('leaves the home page out of the menu, since Discord rejects empty option values', () => { + const home = { title: 'Home', description: 'Welcome.', slug: '', excerpt: '' }; + const options = select(formatSearchResults([...results, home], baseUrl, 'cooking', brand) as Message)?.options ?? []; + + assert.deepEqual(options.map((option) => option.value), ['cooking/eggs#boiling-an-egg', 'cooking/pasta']); + assert.ok(options.every((option) => option.value.length > 0 && option.value.length <= 100)); +}); + test('moves the selection when another result is chosen', () => { const original = formatSearchResults(results, baseUrl, 'cooking', brand) as Message; const updated = formatSelectedResult(eggsPage, baseUrl, brand, 'cooking/eggs#frying', original.components) as Message;