refactor(channels): share reset message construction across adapters
This commit is contained in:
@@ -268,6 +268,7 @@ Remediation update (2026-02-16):
|
|||||||
|
|
||||||
Remediation update (2026-02-16):
|
Remediation update (2026-02-16):
|
||||||
- Added shared `normalizeResetCommandText()` utility and migrated Discord/Slack/WhatsApp adapters to use it, reducing repeated reset-command parsing logic.
|
- Added shared `normalizeResetCommandText()` utility and migrated Discord/Slack/WhatsApp adapters to use it, reducing repeated reset-command parsing logic.
|
||||||
|
- Added shared `buildResetInboundMessage()` utility and migrated Discord/Slack/WhatsApp adapters to use it, reducing repeated reset-metadata construction logic.
|
||||||
|
|
||||||
### F-014 Low: ModelRouter listener API has destructive setter footgun
|
### F-014 Low: ModelRouter listener API has destructive setter footgun
|
||||||
|
|
||||||
|
|||||||
@@ -2618,7 +2618,7 @@
|
|||||||
"status": "in_progress",
|
"status": "in_progress",
|
||||||
"date": "2026-02-16",
|
"date": "2026-02-16",
|
||||||
"updated": "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.",
|
"summary": "Started reducing channel adapter duplication by extracting shared reset-command normalization and reset message construction utilities, and migrating Discord/Slack/WhatsApp adapters to use them.",
|
||||||
"files_modified": [
|
"files_modified": [
|
||||||
"src/channels/utils.ts",
|
"src/channels/utils.ts",
|
||||||
"src/channels/utils.test.ts",
|
"src/channels/utils.test.ts",
|
||||||
@@ -2627,7 +2627,7 @@
|
|||||||
"src/channels/whatsapp/adapter.ts",
|
"src/channels/whatsapp/adapter.ts",
|
||||||
"docs/plans/analysis/2026-02-16-codebase-audit-report.md"
|
"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"
|
"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 + pnpm lint passing"
|
||||||
},
|
},
|
||||||
"audit-followup-lint-error-baseline": {
|
"audit-followup-lint-error-baseline": {
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import type {
|
|||||||
ChannelAdapter,
|
ChannelAdapter,
|
||||||
ChannelStatus,
|
ChannelStatus,
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
import { normalizeResetCommandText, splitMessage } from '../utils.js';
|
import { buildResetInboundMessage, normalizeResetCommandText, splitMessage } from '../utils.js';
|
||||||
import type { PairingManager } from '../pairing.js';
|
import type { PairingManager } from '../pairing.js';
|
||||||
|
|
||||||
/** Configuration for the Discord channel adapter. */
|
/** Configuration for the Discord channel adapter. */
|
||||||
@@ -242,15 +242,12 @@ export class DiscordAdapter implements ChannelAdapter {
|
|||||||
|
|
||||||
// ── Reset command ──
|
// ── Reset command ──
|
||||||
if (text === '!reset') {
|
if (text === '!reset') {
|
||||||
this.messageHandler({
|
this.messageHandler(buildResetInboundMessage({
|
||||||
id: message.id,
|
id: message.id,
|
||||||
channel: 'discord',
|
channel: 'discord',
|
||||||
senderId: message.channelId,
|
senderId: message.channelId,
|
||||||
senderName: message.author.username,
|
senderName: message.author.username,
|
||||||
text: '!reset',
|
}));
|
||||||
timestamp: Date.now(),
|
|
||||||
metadata: { isCommand: true, command: 'reset' },
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import type {
|
|||||||
ChannelAdapter,
|
ChannelAdapter,
|
||||||
ChannelStatus,
|
ChannelStatus,
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
import { normalizeResetCommandText, splitMessage } from '../utils.js';
|
import { buildResetInboundMessage, normalizeResetCommandText, splitMessage } from '../utils.js';
|
||||||
import type { PairingManager } from '../pairing.js';
|
import type { PairingManager } from '../pairing.js';
|
||||||
|
|
||||||
/** Configuration for the Slack channel adapter. */
|
/** Configuration for the Slack channel adapter. */
|
||||||
@@ -357,16 +357,13 @@ export class SlackAdapter implements ChannelAdapter {
|
|||||||
|
|
||||||
// Detect reset command
|
// Detect reset command
|
||||||
if (text === '!reset') {
|
if (text === '!reset') {
|
||||||
this.messageHandler({
|
this.messageHandler(buildResetInboundMessage({
|
||||||
id: message.ts ?? '',
|
id: message.ts ?? '',
|
||||||
channel: 'slack',
|
channel: 'slack',
|
||||||
senderId: peerId,
|
senderId: peerId,
|
||||||
senderName,
|
senderName,
|
||||||
text: '!reset',
|
attachments,
|
||||||
timestamp: Date.now(),
|
}));
|
||||||
metadata: { isCommand: true, command: 'reset' },
|
|
||||||
...(attachments.length > 0 && { attachments }),
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { normalizeResetCommandText, splitMessage } from './utils.js';
|
import { buildResetInboundMessage, normalizeResetCommandText, splitMessage } from './utils.js';
|
||||||
|
|
||||||
describe('splitMessage', () => {
|
describe('splitMessage', () => {
|
||||||
it('returns single chunk for empty string', () => {
|
it('returns single chunk for empty string', () => {
|
||||||
@@ -98,3 +98,38 @@ describe('normalizeResetCommandText', () => {
|
|||||||
expect(normalizeResetCommandText('hello')).toBe('hello');
|
expect(normalizeResetCommandText('hello')).toBe('hello');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('buildResetInboundMessage', () => {
|
||||||
|
it('builds canonical reset command metadata and text', () => {
|
||||||
|
const message = buildResetInboundMessage({
|
||||||
|
id: 'id-1',
|
||||||
|
channel: 'slack',
|
||||||
|
senderId: 'C1:T1',
|
||||||
|
senderName: 'Alice',
|
||||||
|
timestamp: 123,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(message).toEqual({
|
||||||
|
id: 'id-1',
|
||||||
|
channel: 'slack',
|
||||||
|
senderId: 'C1:T1',
|
||||||
|
senderName: 'Alice',
|
||||||
|
text: '!reset',
|
||||||
|
timestamp: 123,
|
||||||
|
metadata: { isCommand: true, command: 'reset' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('includes attachments only when provided', () => {
|
||||||
|
const message = buildResetInboundMessage({
|
||||||
|
id: 'id-2',
|
||||||
|
channel: 'whatsapp',
|
||||||
|
senderId: '123@c.us',
|
||||||
|
attachments: [{ mimeType: 'image/png', url: 'https://example.com/a.png' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(message.attachments).toHaveLength(1);
|
||||||
|
expect(message.text).toBe('!reset');
|
||||||
|
expect(message.metadata).toEqual({ isCommand: true, command: 'reset' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Shared utilities for channel adapters.
|
* Shared utilities for channel adapters.
|
||||||
*/
|
*/
|
||||||
|
import type { Attachment, InboundMessage } from './types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Split a long message into chunks that respect a platform's character limit.
|
* Split a long message into chunks that respect a platform's character limit.
|
||||||
@@ -42,3 +43,26 @@ export function normalizeResetCommandText(text: string): string {
|
|||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ResetMessageParams {
|
||||||
|
id: string;
|
||||||
|
channel: InboundMessage['channel'];
|
||||||
|
senderId: string;
|
||||||
|
senderName?: string;
|
||||||
|
timestamp?: number;
|
||||||
|
attachments?: Attachment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build a normalized inbound reset command message. */
|
||||||
|
export function buildResetInboundMessage(params: ResetMessageParams): InboundMessage {
|
||||||
|
return {
|
||||||
|
id: params.id,
|
||||||
|
channel: params.channel,
|
||||||
|
senderId: params.senderId,
|
||||||
|
senderName: params.senderName,
|
||||||
|
text: '!reset',
|
||||||
|
timestamp: params.timestamp ?? Date.now(),
|
||||||
|
metadata: { isCommand: true, command: 'reset' },
|
||||||
|
...(params.attachments && params.attachments.length > 0 ? { attachments: params.attachments } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import type {
|
|||||||
ChannelAdapter,
|
ChannelAdapter,
|
||||||
ChannelStatus,
|
ChannelStatus,
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
import { normalizeResetCommandText, splitMessage } from '../utils.js';
|
import { buildResetInboundMessage, normalizeResetCommandText, splitMessage } from '../utils.js';
|
||||||
import type { PairingManager } from '../pairing.js';
|
import type { PairingManager } from '../pairing.js';
|
||||||
|
|
||||||
/** Configuration for the WhatsApp channel adapter. */
|
/** Configuration for the WhatsApp channel adapter. */
|
||||||
@@ -313,16 +313,13 @@ export class WhatsAppAdapter implements ChannelAdapter {
|
|||||||
|
|
||||||
// Detect reset command
|
// Detect reset command
|
||||||
if (text === '!reset') {
|
if (text === '!reset') {
|
||||||
this.messageHandler({
|
this.messageHandler(buildResetInboundMessage({
|
||||||
id: message.id.id,
|
id: message.id.id,
|
||||||
channel: 'whatsapp',
|
channel: 'whatsapp',
|
||||||
senderId: from,
|
senderId: from,
|
||||||
senderName,
|
senderName,
|
||||||
text: '!reset',
|
attachments,
|
||||||
timestamp: Date.now(),
|
}));
|
||||||
metadata: { isCommand: true, command: 'reset' },
|
|
||||||
...(attachments.length > 0 ? { attachments } : {}),
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user