From 8b529a18f2827e5fafa04e19ebfc6909020cdd16 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Sun, 15 Feb 2026 22:22:40 -0800 Subject: [PATCH] refactor(channels): share reset-command normalization utility --- .../2026-02-16-codebase-audit-report.md | 6 +++++- docs/plans/state.json | 17 ++++++++++++++++- src/channels/discord/adapter.ts | 7 ++++--- src/channels/slack/adapter.ts | 7 ++++--- src/channels/utils.test.ts | 16 +++++++++++++++- src/channels/utils.ts | 11 +++++++++++ src/channels/whatsapp/adapter.ts | 6 ++++-- 7 files changed, 59 insertions(+), 11 deletions(-) diff --git a/docs/plans/analysis/2026-02-16-codebase-audit-report.md b/docs/plans/analysis/2026-02-16-codebase-audit-report.md index b375b0a..eb675de 100644 --- a/docs/plans/analysis/2026-02-16-codebase-audit-report.md +++ b/docs/plans/analysis/2026-02-16-codebase-audit-report.md @@ -19,13 +19,14 @@ Scope: Production-risk-first audit of bugs, code improvements, and feature oppor - ✅ F-003 addressed: tool execution now has an `AbortSignal` contract, executor triggers abort on timeout, high-risk tools (`shell.exec`, sandbox docker exec, `process.start`, browser tools, `web.fetch`, `web.search`) respond to cancellation, and executor regression tests verify cancellable tools do not apply side effects after timeout. - ✅ F-015 addressed: retry defaults no longer classify timeout-style failures as non-retryable, improving resilience for transient timeout conditions. - ✅ F-011 addressed: Slack user-name resolution now uses bounded TTL+LRU caching to prevent unbounded growth. +- ◑ F-013 partially addressed: reset-command normalization is now shared across Discord/Slack/WhatsApp adapters via `src/channels/utils.ts`, reducing duplicated command-parsing logic. ## Executive Summary Current health snapshot: - `pnpm typecheck`: passing - `pnpm build`: passing -- `pnpm test:run`: passing (`140/140` files, `1770/1770` tests) +- `pnpm test:run`: passing (`140/140` files, `1773/1773` tests) - `pnpm lint`: failing (`148 errors`, `530 warnings`) Top conclusions: @@ -260,6 +261,9 @@ Remediation update (2026-02-16): - Recommended fix: - Extract shared middleware utilities for common inbound/outbound behaviors. +Remediation update (2026-02-16): +- Added shared `normalizeResetCommandText()` utility and migrated Discord/Slack/WhatsApp adapters to use it, reducing repeated reset-command parsing logic. + ### F-014 Low: ModelRouter listener API has destructive setter footgun - Severity: Low diff --git a/docs/plans/state.json b/docs/plans/state.json index 931e18d..eb549fd 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -2613,10 +2613,25 @@ "docs/plans/analysis/2026-02-16-codebase-audit-report.md" ], "test_status": "pnpm test:run src/channels/slack/adapter.test.ts + pnpm typecheck passing" + }, + "audit-followup-channel-reset-command-dedup": { + "status": "in_progress", + "date": "2026-02-16", + "updated": "2026-02-16", + "summary": "Started reducing channel adapter duplication by extracting shared reset-command normalization and migrating Discord/Slack/WhatsApp adapters to use it.", + "files_modified": [ + "src/channels/utils.ts", + "src/channels/utils.test.ts", + "src/channels/discord/adapter.ts", + "src/channels/slack/adapter.ts", + "src/channels/whatsapp/adapter.ts", + "docs/plans/analysis/2026-02-16-codebase-audit-report.md" + ], + "test_status": "pnpm test:run src/channels/utils.test.ts src/channels/discord/adapter.test.ts src/channels/slack/adapter.test.ts src/channels/whatsapp/adapter.test.ts + pnpm typecheck passing" } }, "overall_progress": { - "total_test_count": 1770, + "total_test_count": 1773, "all_tests_passing": true, "p0_completion": "3/3 (100%)", "p1_completion": "4/4 (100%)", diff --git a/src/channels/discord/adapter.ts b/src/channels/discord/adapter.ts index 1d8b44a..d922e94 100644 --- a/src/channels/discord/adapter.ts +++ b/src/channels/discord/adapter.ts @@ -17,7 +17,7 @@ import type { ChannelAdapter, ChannelStatus, } from '../types.js'; -import { splitMessage } from '../utils.js'; +import { normalizeResetCommandText, splitMessage } from '../utils.js'; import type { PairingManager } from '../pairing.js'; /** Configuration for the Discord channel adapter. */ @@ -221,7 +221,8 @@ export class DiscordAdapter implements ChannelAdapter { } catch { /* ignore typing errors */ } // Strip bot mention from the message text - const text = message.content.replace(/<@!?\d+>/g, '').trim(); + const rawText = message.content.replace(/<@!?\d+>/g, '').trim(); + const text = normalizeResetCommandText(rawText); // ── Extract media attachments ── const attachments: Attachment[] = []; @@ -240,7 +241,7 @@ export class DiscordAdapter implements ChannelAdapter { } // ── Reset command ── - if (text === '!reset' || text === 'reset') { + if (text === '!reset') { this.messageHandler({ id: message.id, channel: 'discord', diff --git a/src/channels/slack/adapter.ts b/src/channels/slack/adapter.ts index 00711a8..4ea597a 100644 --- a/src/channels/slack/adapter.ts +++ b/src/channels/slack/adapter.ts @@ -15,7 +15,7 @@ import type { ChannelAdapter, ChannelStatus, } from '../types.js'; -import { splitMessage } from '../utils.js'; +import { normalizeResetCommandText, splitMessage } from '../utils.js'; import type { PairingManager } from '../pairing.js'; /** Configuration for the Slack channel adapter. */ @@ -344,7 +344,8 @@ export class SlackAdapter implements ChannelAdapter { const peerId = `${channelId}:${threadTs}`; // Strip bot mentions: <@U\w+> pattern - let text = (message.text ?? '').replace(/<@U\w+>/g, '').trim(); + const rawText = (message.text ?? '').replace(/<@U\w+>/g, '').trim(); + const text = normalizeResetCommandText(rawText); // Resolve display name from Slack user ID const senderName = message.user @@ -355,7 +356,7 @@ export class SlackAdapter implements ChannelAdapter { const attachments = await this.extractMediaAttachments(message.files); // Detect reset command - if (text === '!reset' || text === 'reset') { + if (text === '!reset') { this.messageHandler({ id: message.ts ?? '', channel: 'slack', diff --git a/src/channels/utils.test.ts b/src/channels/utils.test.ts index db8baba..81fdbf3 100644 --- a/src/channels/utils.test.ts +++ b/src/channels/utils.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { splitMessage } from './utils.js'; +import { normalizeResetCommandText, splitMessage } from './utils.js'; describe('splitMessage', () => { it('returns single chunk for empty string', () => { @@ -84,3 +84,17 @@ describe('splitMessage', () => { expect(result[1]).toBe('klmnopqrst'); }); }); + +describe('normalizeResetCommandText', () => { + it('normalizes reset to !reset', () => { + expect(normalizeResetCommandText('reset')).toBe('!reset'); + }); + + it('keeps !reset unchanged', () => { + expect(normalizeResetCommandText('!reset')).toBe('!reset'); + }); + + it('does not change non-command text', () => { + expect(normalizeResetCommandText('hello')).toBe('hello'); + }); +}); diff --git a/src/channels/utils.ts b/src/channels/utils.ts index e26703d..44287bb 100644 --- a/src/channels/utils.ts +++ b/src/channels/utils.ts @@ -31,3 +31,14 @@ export function splitMessage(text: string, maxLength: number): string[] { return chunks; } + +/** + * Normalize reset command variants to a canonical text form. + * Returns '!reset' for recognized variants, otherwise returns the original text. + */ +export function normalizeResetCommandText(text: string): string { + if (text === '!reset' || text === 'reset') { + return '!reset'; + } + return text; +} diff --git a/src/channels/whatsapp/adapter.ts b/src/channels/whatsapp/adapter.ts index 2a8a97a..ee1f6fc 100644 --- a/src/channels/whatsapp/adapter.ts +++ b/src/channels/whatsapp/adapter.ts @@ -17,7 +17,7 @@ import type { ChannelAdapter, ChannelStatus, } from '../types.js'; -import { splitMessage } from '../utils.js'; +import { normalizeResetCommandText, splitMessage } from '../utils.js'; import type { PairingManager } from '../pairing.js'; /** Configuration for the WhatsApp channel adapter. */ @@ -309,8 +309,10 @@ export class WhatsAppAdapter implements ChannelAdapter { } } + text = normalizeResetCommandText(text); + // Detect reset command - if (text === '!reset' || text === 'reset') { + if (text === '!reset') { this.messageHandler({ id: message.id.id, channel: 'whatsapp',