From 0e59a7ffdf46cf5ee7cf2eb52bcb2700daf052aa Mon Sep 17 00:00:00 2001 From: Zbigniew Sobiecki Date: Sun, 22 Feb 2026 17:11:49 +0000 Subject: [PATCH] feat(router): generate contextual ack messages via lightweight LLM call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace hardcoded INITIAL_MESSAGES lookups in webhook ack handlers with a single-shot LLM call that produces a short, context-aware message reflecting the actual request (e.g., card name, PR title, comment text). Uses the existing progressModel config (defaults to gemini-2.5-flash-lite via OpenRouter) with a 5-second timeout. On any failure — no model configured, missing API key, LLM error, timeout, or empty output — gracefully falls back to the static INITIAL_MESSAGES. Co-Authored-By: Claude Opus 4.6 --- src/router/ackMessageGenerator.ts | 234 +++++++++++ src/router/github.ts | 6 +- src/router/jira.ts | 6 +- src/router/trello.ts | 6 +- tests/unit/router/ackMessageGenerator.test.ts | 387 ++++++++++++++++++ tests/unit/router/github.test.ts | 5 +- tests/unit/router/jira.test.ts | 5 +- tests/unit/router/trello.test.ts | 5 +- 8 files changed, 639 insertions(+), 15 deletions(-) create mode 100644 src/router/ackMessageGenerator.ts create mode 100644 tests/unit/router/ackMessageGenerator.test.ts diff --git a/src/router/ackMessageGenerator.ts b/src/router/ackMessageGenerator.ts new file mode 100644 index 00000000..c47559af --- /dev/null +++ b/src/router/ackMessageGenerator.ts @@ -0,0 +1,234 @@ +/** + * LLM-generated acknowledgment messages for webhook events. + * + * Makes a single-shot LLM call to a lightweight model (same as progress tracking) + * to produce a short, contextual ack message that reflects the actual request. + * Gracefully falls back to static INITIAL_MESSAGES on any failure. + */ + +import { AgentBuilder, LLMist, type ModelSpec } from 'llmist'; + +import { INITIAL_MESSAGES } from '../config/agentMessages.js'; +import { CUSTOM_MODELS } from '../config/customModels.js'; +import { getOrgCredential, loadConfig } from '../config/provider.js'; + +// --------------------------------------------------------------------------- +// System prompt for ack message generation +// --------------------------------------------------------------------------- + +const ACK_SYSTEM_PROMPT = `You write brief acknowledgment messages for CASCADE, an AI coding automation platform. +Given the agent type and request context, write a SHORT 1-sentence message confirming understanding of the request. Keep it under 25 words. Use markdown bold for the header. Start with an appropriate emoji. Do not mention implementation details — just confirm what you'll be working on.`; + +// --------------------------------------------------------------------------- +// Context extractors — pull relevant snippets from webhook payloads +// --------------------------------------------------------------------------- + +const MAX_CONTEXT_LENGTH = 500; + +function truncate(text: string, maxLength: number = MAX_CONTEXT_LENGTH): string { + if (text.length <= maxLength) return text; + return `${text.slice(0, maxLength)}…`; +} + +/** + * Extract context from a Trello webhook payload. + * Pulls card name and optional comment text. + */ +export function extractTrelloContext(payload: unknown): string { + if (!payload || typeof payload !== 'object') return ''; + + const p = payload as Record; + const action = p.action as Record | undefined; + if (!action) return ''; + + const data = action.data as Record | undefined; + if (!data) return ''; + + const parts: string[] = []; + + const card = data.card as Record | undefined; + if (card?.name) { + parts.push(`Card: ${card.name as string}`); + } + + // Comment text (for commentCard actions) + const text = data.text as string | undefined; + if (text) { + parts.push(`Comment: ${text}`); + } + + return truncate(parts.join('\n')); +} + +/** + * Extract context from a GitHub webhook payload. + * Pulls PR title and optional comment/review body. + */ +export function extractGitHubContext(payload: unknown, eventType: string): string { + if (!payload || typeof payload !== 'object') return ''; + + const p = payload as Record; + const parts: string[] = []; + + const pr = p.pull_request as Record | undefined; + if (pr?.title) { + parts.push(`PR: ${pr.title as string}`); + } + + // Comment body (issue_comment or pull_request_review_comment) + if (eventType === 'issue_comment' || eventType === 'pull_request_review_comment') { + const comment = p.comment as Record | undefined; + if (comment?.body) { + parts.push(`Comment: ${comment.body as string}`); + } + } + + // Review body (pull_request_review) + if (eventType === 'pull_request_review') { + const review = p.review as Record | undefined; + if (review?.body) { + parts.push(`Review: ${review.body as string}`); + } + } + + return truncate(parts.join('\n')); +} + +/** + * Extract context from a JIRA webhook payload. + * Pulls issue summary and optional comment body. + */ +export function extractJiraContext(payload: unknown): string { + if (!payload || typeof payload !== 'object') return ''; + + const p = payload as Record; + const parts: string[] = []; + + const issue = p.issue as Record | undefined; + if (issue) { + const fields = issue.fields as Record | undefined; + if (fields?.summary) { + parts.push(`Issue: ${fields.summary as string}`); + } + } + + const comment = p.comment as Record | undefined; + if (comment?.body) { + parts.push(`Comment: ${comment.body as string}`); + } + + return truncate(parts.join('\n')); +} + +// --------------------------------------------------------------------------- +// Core generator +// --------------------------------------------------------------------------- + +const ACK_TIMEOUT_MS = 5_000; + +const GENERIC_FALLBACK = '**⚙️ Working on it** — Processing your request...'; + +function getStaticFallback(agentType: string): string { + return INITIAL_MESSAGES[agentType] ?? GENERIC_FALLBACK; +} + +/** + * Generate a contextual acknowledgment message using a lightweight LLM call. + * + * Falls back to static INITIAL_MESSAGES on any failure: + * - No progressModel configured + * - No OPENROUTER_API_KEY credential + * - Empty context snippet + * - LLM call failure (network, auth, etc.) + * - LLM call exceeds 5s timeout + * - LLM returns empty output + */ +export async function generateAckMessage( + agentType: string, + contextSnippet: string, + projectId: string, +): Promise { + const fallback = getStaticFallback(agentType); + + // No context to work with — use static message + if (!contextSnippet.trim()) { + return fallback; + } + + let restoreEnv: (() => void) | undefined; + + try { + // Load config to get progressModel + const config = await loadConfig(); + const progressModel = config.defaults.progressModel; + if (!progressModel) { + return fallback; + } + + // Resolve API key + const apiKey = await getOrgCredential(projectId, 'OPENROUTER_API_KEY'); + if (!apiKey) { + return fallback; + } + + // Temporarily inject API key into process.env (same pattern as llmEnv.ts) + const previousKey = process.env.OPENROUTER_API_KEY; + process.env.OPENROUTER_API_KEY = apiKey; + restoreEnv = () => { + if (previousKey === undefined) { + process.env.OPENROUTER_API_KEY = undefined; + } else { + process.env.OPENROUTER_API_KEY = previousKey; + } + }; + + // Single-shot LLM call with timeout + const llmPromise = callAckModel(progressModel, agentType, contextSnippet); + const timeoutPromise = new Promise((_resolve, reject) => { + setTimeout(() => reject(new Error('Ack message generation timed out')), ACK_TIMEOUT_MS); + }); + + const result = await Promise.race([llmPromise, timeoutPromise]); + + if (!result || !result.trim()) { + return fallback; + } + + return result.trim(); + } catch (err) { + console.warn('[Router] Ack message generation failed (using static fallback):', String(err)); + return fallback; + } finally { + restoreEnv?.(); + } +} + +/** + * Make the actual single-shot LLM call to generate an ack message. + */ +async function callAckModel( + model: string, + agentType: string, + contextSnippet: string, +): Promise { + const client = new LLMist({ customModels: CUSTOM_MODELS as ModelSpec[] }); + + const builder = new AgentBuilder(client) + .withModel(model) + .withTemperature(0) + .withSystem(ACK_SYSTEM_PROMPT) + .withMaxIterations(1) + .withGadgets(); + + const userPrompt = `Agent type: ${agentType}\n\nRequest context:\n${contextSnippet}`; + const agent = builder.ask(userPrompt); + + const outputLines: string[] = []; + for await (const event of agent.run()) { + if (event.type === 'text' && event.content) { + outputLines.push(event.content); + } + } + + return outputLines.join('\n').trim(); +} diff --git a/src/router/github.ts b/src/router/github.ts index d5b3ba32..71d4170e 100644 --- a/src/router/github.ts +++ b/src/router/github.ts @@ -5,7 +5,6 @@ * and job queuing for GitHub webhook events. */ -import { INITIAL_MESSAGES } from '../config/agentMessages.js'; import { findProjectByRepo } from '../config/provider.js'; import { type PersonaIdentities, @@ -14,6 +13,7 @@ import { } from '../github/personas.js'; import type { TriggerRegistry } from '../triggers/registry.js'; import type { TriggerContext } from '../types/index.js'; +import { extractGitHubContext, generateAckMessage } from './ackMessageGenerator.js'; import { postGitHubAck, resolveGitHubTokenForAck } from './acknowledgments.js'; import { loadProjectConfig } from './config.js'; import { extractPRNumber } from './notifications.js'; @@ -55,8 +55,8 @@ export async function tryPostGitHubAck( const match = triggerRegistry.matchTrigger(ctx); if (!match) return undefined; - const message = INITIAL_MESSAGES[match.agentType]; - if (!message) return undefined; + const context = extractGitHubContext(payload, eventType); + const message = await generateAckMessage(match.agentType, context, fullProject.id); const resolved = await resolveGitHubTokenForAck(repoFullName); if (!resolved) return undefined; diff --git a/src/router/jira.ts b/src/router/jira.ts index 576110f1..db596499 100644 --- a/src/router/jira.ts +++ b/src/router/jira.ts @@ -5,9 +5,9 @@ * for JIRA webhook events. */ -import { INITIAL_MESSAGES } from '../config/agentMessages.js'; import type { TriggerRegistry } from '../triggers/registry.js'; import type { ProjectConfig, TriggerContext } from '../types/index.js'; +import { extractJiraContext, generateAckMessage } from './ackMessageGenerator.js'; import { postJiraAck, resolveJiraBotAccountId } from './acknowledgments.js'; import { type RouterProjectConfig, loadProjectConfig } from './config.js'; import { type CascadeJob, addJob } from './queue.js'; @@ -35,8 +35,8 @@ export async function tryPostJiraAck( const match = triggerRegistry.matchTrigger(ctx); if (!match) return undefined; - const message = INITIAL_MESSAGES[match.agentType]; - if (!message) return undefined; + const context = extractJiraContext(payload); + const message = await generateAckMessage(match.agentType, context, projectId); const commentId = await postJiraAck(projectId, issueKey, message); return commentId ?? undefined; diff --git a/src/router/trello.ts b/src/router/trello.ts index d512b393..7e8f0aa9 100644 --- a/src/router/trello.ts +++ b/src/router/trello.ts @@ -5,9 +5,9 @@ * for Trello webhook events. */ -import { INITIAL_MESSAGES } from '../config/agentMessages.js'; import type { TriggerRegistry } from '../triggers/registry.js'; import type { TriggerContext } from '../types/index.js'; +import { extractTrelloContext, generateAckMessage } from './ackMessageGenerator.js'; import { postTrelloAck, resolveTrelloBotMemberId } from './acknowledgments.js'; import { type RouterProjectConfig, loadProjectConfig } from './config.js'; import { type CascadeJob, addJob } from './queue.js'; @@ -158,8 +158,8 @@ export async function tryPostTrelloAck( const match = triggerRegistry.matchTrigger(ctx); if (!match) return undefined; - const message = INITIAL_MESSAGES[match.agentType]; - if (!message) return undefined; + const context = extractTrelloContext(payload); + const message = await generateAckMessage(match.agentType, context, projectId); const commentId = await postTrelloAck(projectId, cardId, message); return commentId ?? undefined; diff --git a/tests/unit/router/ackMessageGenerator.test.ts b/tests/unit/router/ackMessageGenerator.test.ts new file mode 100644 index 00000000..4cec7f74 --- /dev/null +++ b/tests/unit/router/ackMessageGenerator.test.ts @@ -0,0 +1,387 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +// Mock heavy imports before importing the module under test +vi.mock('llmist', () => { + const mockRun = vi.fn(); + const mockBuilder = { + withModel: vi.fn().mockReturnThis(), + withTemperature: vi.fn().mockReturnThis(), + withSystem: vi.fn().mockReturnThis(), + withMaxIterations: vi.fn().mockReturnThis(), + withGadgets: vi.fn().mockReturnThis(), + ask: vi.fn().mockReturnValue({ run: mockRun }), + }; + return { + LLMist: vi.fn().mockImplementation(() => ({})), + AgentBuilder: vi.fn().mockImplementation(() => mockBuilder), + __mockBuilder: mockBuilder, + __mockRun: mockRun, + }; +}); + +vi.mock('../../../src/config/provider.js', () => ({ + loadConfig: vi.fn(), + getOrgCredential: vi.fn(), +})); + +vi.mock('../../../src/config/customModels.js', () => ({ + CUSTOM_MODELS: [], +})); + +vi.mock('../../../src/config/agentMessages.js', () => ({ + INITIAL_MESSAGES: { + implementation: + '**🚀 Implementing changes** — Writing code, running tests, and preparing a PR...', + briefing: + '**📋 Analyzing brief** — Reading the card and gathering context to create a clear brief...', + review: '**🔍 Reviewing code** — Examining the PR changes for quality and correctness...', + }, +})); + +import { getOrgCredential, loadConfig } from '../../../src/config/provider.js'; +import { + extractGitHubContext, + extractJiraContext, + extractTrelloContext, + generateAckMessage, +} from '../../../src/router/ackMessageGenerator.js'; + +// Access llmist mocks — biome-ignore lint/suspicious/noExplicitAny: accessing test-only mock internals +const llmistModule = (await import('llmist')) as Record; +const mockRun = llmistModule.__mockRun; + +beforeEach(() => { + vi.clearAllMocks(); +}); + +// --------------------------------------------------------------------------- +// Context extractors +// --------------------------------------------------------------------------- + +describe('extractTrelloContext', () => { + it('extracts card name from payload', () => { + const payload = { + action: { + type: 'updateCard', + data: { + card: { id: 'card1', name: 'Add dark mode support' }, + }, + }, + }; + const result = extractTrelloContext(payload); + expect(result).toBe('Card: Add dark mode support'); + }); + + it('extracts card name and comment text for commentCard actions', () => { + const payload = { + action: { + type: 'commentCard', + data: { + card: { id: 'card1', name: 'Fix auth bug' }, + text: 'Please also check the session handling', + }, + }, + }; + const result = extractTrelloContext(payload); + expect(result).toContain('Card: Fix auth bug'); + expect(result).toContain('Comment: Please also check the session handling'); + }); + + it('returns empty string for null payload', () => { + expect(extractTrelloContext(null)).toBe(''); + }); + + it('returns empty string for payload without action', () => { + expect(extractTrelloContext({})).toBe(''); + }); + + it('returns empty string for payload without data', () => { + expect(extractTrelloContext({ action: { type: 'updateCard' } })).toBe(''); + }); + + it('truncates long context to max length', () => { + const longName = 'A'.repeat(600); + const payload = { + action: { + type: 'updateCard', + data: { card: { id: 'card1', name: longName } }, + }, + }; + const result = extractTrelloContext(payload); + // "Card: " is 6 chars, so total should be truncated to 500 + "…" + expect(result.length).toBeLessThanOrEqual(501); + expect(result.endsWith('…')).toBe(true); + }); +}); + +describe('extractGitHubContext', () => { + it('extracts PR title from pull_request event', () => { + const payload = { + pull_request: { title: 'feat: add dark mode', number: 42 }, + }; + const result = extractGitHubContext(payload, 'pull_request'); + expect(result).toBe('PR: feat: add dark mode'); + }); + + it('extracts PR title and comment body from issue_comment event', () => { + const payload = { + pull_request: { title: 'feat: add dark mode' }, + comment: { body: '@cascade please fix the linting errors' }, + }; + const result = extractGitHubContext(payload, 'issue_comment'); + expect(result).toContain('PR: feat: add dark mode'); + expect(result).toContain('Comment: @cascade please fix the linting errors'); + }); + + it('extracts PR title and review body from pull_request_review event', () => { + const payload = { + pull_request: { title: 'fix: auth bug' }, + review: { body: 'Please handle the edge case for expired tokens' }, + }; + const result = extractGitHubContext(payload, 'pull_request_review'); + expect(result).toContain('PR: fix: auth bug'); + expect(result).toContain('Review: Please handle the edge case for expired tokens'); + }); + + it('extracts comment from pull_request_review_comment event', () => { + const payload = { + pull_request: { title: 'refactor: cleanup' }, + comment: { body: 'This function should be extracted' }, + }; + const result = extractGitHubContext(payload, 'pull_request_review_comment'); + expect(result).toContain('Comment: This function should be extracted'); + }); + + it('returns empty string for null payload', () => { + expect(extractGitHubContext(null, 'pull_request')).toBe(''); + }); + + it('returns empty string for payload without PR', () => { + expect(extractGitHubContext({}, 'check_suite')).toBe(''); + }); + + it('truncates long context', () => { + const longTitle = 'B'.repeat(600); + const payload = { pull_request: { title: longTitle } }; + const result = extractGitHubContext(payload, 'pull_request'); + expect(result.length).toBeLessThanOrEqual(501); + expect(result.endsWith('…')).toBe(true); + }); +}); + +describe('extractJiraContext', () => { + it('extracts issue summary from payload', () => { + const payload = { + issue: { + key: 'PROJ-123', + fields: { summary: 'Implement user authentication' }, + }, + }; + const result = extractJiraContext(payload); + expect(result).toBe('Issue: Implement user authentication'); + }); + + it('extracts issue summary and comment body', () => { + const payload = { + issue: { + key: 'PROJ-123', + fields: { summary: 'Fix login bug' }, + }, + comment: { body: 'This also affects the password reset flow' }, + }; + const result = extractJiraContext(payload); + expect(result).toContain('Issue: Fix login bug'); + expect(result).toContain('Comment: This also affects the password reset flow'); + }); + + it('returns empty string for null payload', () => { + expect(extractJiraContext(null)).toBe(''); + }); + + it('returns empty string for payload without issue', () => { + expect(extractJiraContext({})).toBe(''); + }); + + it('extracts comment even without issue', () => { + const payload = { + comment: { body: 'Some standalone comment' }, + }; + const result = extractJiraContext(payload); + expect(result).toBe('Comment: Some standalone comment'); + }); + + it('truncates long context', () => { + const longSummary = 'C'.repeat(600); + const payload = { + issue: { key: 'PROJ-1', fields: { summary: longSummary } }, + }; + const result = extractJiraContext(payload); + expect(result.length).toBeLessThanOrEqual(501); + expect(result.endsWith('…')).toBe(true); + }); +}); + +// --------------------------------------------------------------------------- +// generateAckMessage +// --------------------------------------------------------------------------- + +describe('generateAckMessage', () => { + function setupHappyPath(llmResponse: string) { + vi.mocked(loadConfig).mockResolvedValue({ + defaults: { progressModel: 'openrouter:google/gemini-2.5-flash-lite' }, + } as never); + vi.mocked(getOrgCredential).mockResolvedValue('sk-test-key'); + + // Mock the async iterator returned by agent.run() + async function* fakeRun() { + yield { type: 'text' as const, content: llmResponse }; + } + mockRun.mockReturnValue(fakeRun()); + } + + it('returns LLM-generated message on happy path', async () => { + setupHappyPath('**🚀 Implementing dark mode** — Adding dark mode support to the application.'); + + const result = await generateAckMessage('implementation', 'Card: Add dark mode support', 'p1'); + + expect(result).toBe( + '**🚀 Implementing dark mode** — Adding dark mode support to the application.', + ); + expect(loadConfig).toHaveBeenCalled(); + expect(getOrgCredential).toHaveBeenCalledWith('p1', 'OPENROUTER_API_KEY'); + }); + + it('falls back to static message when context snippet is empty', async () => { + const result = await generateAckMessage('implementation', '', 'p1'); + + expect(result).toBe( + '**🚀 Implementing changes** — Writing code, running tests, and preparing a PR...', + ); + expect(loadConfig).not.toHaveBeenCalled(); + }); + + it('falls back to static message when context is only whitespace', async () => { + const result = await generateAckMessage('implementation', ' ', 'p1'); + + expect(result).toBe( + '**🚀 Implementing changes** — Writing code, running tests, and preparing a PR...', + ); + expect(loadConfig).not.toHaveBeenCalled(); + }); + + it('falls back to static message when progressModel is not configured', async () => { + vi.mocked(loadConfig).mockResolvedValue({ + defaults: { progressModel: '' }, + } as never); + + const result = await generateAckMessage('implementation', 'Card: Test', 'p1'); + + expect(result).toBe( + '**🚀 Implementing changes** — Writing code, running tests, and preparing a PR...', + ); + }); + + it('falls back to static message when no API key', async () => { + vi.mocked(loadConfig).mockResolvedValue({ + defaults: { progressModel: 'openrouter:google/gemini-2.5-flash-lite' }, + } as never); + vi.mocked(getOrgCredential).mockResolvedValue(null); + + const result = await generateAckMessage('briefing', 'Card: Test', 'p1'); + + expect(result).toBe( + '**📋 Analyzing brief** — Reading the card and gathering context to create a clear brief...', + ); + }); + + it('falls back to static message when LLM call throws', async () => { + vi.mocked(loadConfig).mockResolvedValue({ + defaults: { progressModel: 'openrouter:google/gemini-2.5-flash-lite' }, + } as never); + vi.mocked(getOrgCredential).mockResolvedValue('sk-test-key'); + mockRun.mockImplementation(() => { + throw new Error('Network error'); + }); + + const result = await generateAckMessage('implementation', 'Card: Test', 'p1'); + + expect(result).toBe( + '**🚀 Implementing changes** — Writing code, running tests, and preparing a PR...', + ); + }); + + it('falls back to static message when LLM returns empty output', async () => { + vi.mocked(loadConfig).mockResolvedValue({ + defaults: { progressModel: 'openrouter:google/gemini-2.5-flash-lite' }, + } as never); + vi.mocked(getOrgCredential).mockResolvedValue('sk-test-key'); + + async function* emptyRun() { + // Yields nothing + } + mockRun.mockReturnValue(emptyRun()); + + const result = await generateAckMessage('implementation', 'Card: Test', 'p1'); + + expect(result).toBe( + '**🚀 Implementing changes** — Writing code, running tests, and preparing a PR...', + ); + }); + + it('falls back to generic message for unknown agent types', async () => { + const result = await generateAckMessage('unknown-agent', '', 'p1'); + + expect(result).toBe('**⚙️ Working on it** — Processing your request...'); + }); + + it('restores process.env after successful call', async () => { + const originalKey = process.env.OPENROUTER_API_KEY; + setupHappyPath('**🚀 Test message**'); + + await generateAckMessage('implementation', 'Card: Test', 'p1'); + + expect(process.env.OPENROUTER_API_KEY).toBe(originalKey); + }); + + it('restores process.env after failed call', async () => { + const originalKey = process.env.OPENROUTER_API_KEY; + vi.mocked(loadConfig).mockResolvedValue({ + defaults: { progressModel: 'openrouter:google/gemini-2.5-flash-lite' }, + } as never); + vi.mocked(getOrgCredential).mockResolvedValue('sk-test-key'); + mockRun.mockImplementation(() => { + throw new Error('LLM error'); + }); + + await generateAckMessage('implementation', 'Card: Test', 'p1'); + + expect(process.env.OPENROUTER_API_KEY).toBe(originalKey); + }); + + it('falls back to static message on timeout', async () => { + vi.mocked(loadConfig).mockResolvedValue({ + defaults: { progressModel: 'openrouter:google/gemini-2.5-flash-lite' }, + } as never); + vi.mocked(getOrgCredential).mockResolvedValue('sk-test-key'); + + // Simulate a call that never resolves (will be beaten by the 5s timeout) + let resolveHang: () => void; + const hangForever = new Promise((r) => { + resolveHang = r; + }); + async function* slowRun() { + await hangForever; + yield { type: 'text' as const, content: 'too late' }; + } + mockRun.mockReturnValue(slowRun()); + + const result = await generateAckMessage('implementation', 'Card: Test', 'p1'); + + // Clean up the hanging promise so it doesn't leak + resolveHang?.(); + + expect(result).toBe( + '**🚀 Implementing changes** — Writing code, running tests, and preparing a PR...', + ); + }, 10_000); +}); diff --git a/tests/unit/router/github.test.ts b/tests/unit/router/github.test.ts index 4e90ffee..1298cc02 100644 --- a/tests/unit/router/github.test.ts +++ b/tests/unit/router/github.test.ts @@ -20,8 +20,9 @@ vi.mock('../../../src/router/notifications.js', () => ({ vi.mock('../../../src/router/pre-actions.js', () => ({ addEyesReactionToPR: vi.fn(), })); -vi.mock('../../../src/config/agentMessages.js', () => ({ - INITIAL_MESSAGES: { implementation: 'Starting implementation...' }, +vi.mock('../../../src/router/ackMessageGenerator.js', () => ({ + extractGitHubContext: vi.fn().mockReturnValue('PR: Test PR'), + generateAckMessage: vi.fn().mockResolvedValue('Starting implementation...'), })); vi.mock('../../../src/config/provider.js', () => ({ findProjectByRepo: vi.fn(), diff --git a/tests/unit/router/jira.test.ts b/tests/unit/router/jira.test.ts index 39a76352..6e0c6248 100644 --- a/tests/unit/router/jira.test.ts +++ b/tests/unit/router/jira.test.ts @@ -14,8 +14,9 @@ vi.mock('../../../src/router/acknowledgments.js', () => ({ postJiraAck: vi.fn(), resolveJiraBotAccountId: vi.fn(), })); -vi.mock('../../../src/config/agentMessages.js', () => ({ - INITIAL_MESSAGES: { implementation: 'Starting implementation...' }, +vi.mock('../../../src/router/ackMessageGenerator.js', () => ({ + extractJiraContext: vi.fn().mockReturnValue('Issue: Test issue'), + generateAckMessage: vi.fn().mockResolvedValue('Starting implementation...'), })); import { resolveJiraBotAccountId } from '../../../src/router/acknowledgments.js'; diff --git a/tests/unit/router/trello.test.ts b/tests/unit/router/trello.test.ts index 5abd6378..bde90102 100644 --- a/tests/unit/router/trello.test.ts +++ b/tests/unit/router/trello.test.ts @@ -14,8 +14,9 @@ vi.mock('../../../src/router/acknowledgments.js', () => ({ postTrelloAck: vi.fn(), resolveTrelloBotMemberId: vi.fn(), })); -vi.mock('../../../src/config/agentMessages.js', () => ({ - INITIAL_MESSAGES: { implementation: 'Starting implementation...' }, +vi.mock('../../../src/router/ackMessageGenerator.js', () => ({ + extractTrelloContext: vi.fn().mockReturnValue('Card: Test card'), + generateAckMessage: vi.fn().mockResolvedValue('Starting implementation...'), })); import { postTrelloAck, resolveTrelloBotMemberId } from '../../../src/router/acknowledgments.js';