From 5b467bbb22e81262811e9b0de5bc644d08891e53 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 07:47:58 +0000 Subject: [PATCH] fix(bot): stop stderr output from swallowing PR/MR URLs in bot responses Stderr output (git warnings, npm notices, etc.) was incorrectly setting hasError=true, causing runSessionToCompletion to take the error branch which discarded the completionResult containing the PR/MR URL. Changes: - Track stderr via separate hasStderr flag instead of hasError - Include completionResult in error responses when available - Add hasStderr to RunSessionResult for diagnostics - Add tests for extractTextFromMessage and resolveStreamUrl --- src/lib/cloud-agent-next/run-session.test.ts | 92 ++++++++++++++++++++ src/lib/cloud-agent-next/run-session.ts | 24 ++++- 2 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 src/lib/cloud-agent-next/run-session.test.ts diff --git a/src/lib/cloud-agent-next/run-session.test.ts b/src/lib/cloud-agent-next/run-session.test.ts new file mode 100644 index 000000000..3b3db4f5a --- /dev/null +++ b/src/lib/cloud-agent-next/run-session.test.ts @@ -0,0 +1,92 @@ +/** + * Tests for run-session.ts + * + * Tests the pure helper functions (resolveStreamUrl, extractTextFromMessage) + * exported from run-session.ts. + */ + +import { resolveStreamUrl, extractTextFromMessage } from './run-session'; +import type { ProcessedMessage } from './processor'; + +// ─── resolveStreamUrl ─────────────────────────────────────────────── + +describe('resolveStreamUrl', () => { + it('throws when streamUrl is empty', () => { + expect(() => resolveStreamUrl('')).toThrow('Cloud Agent stream URL is missing'); + }); + + it('returns an absolute wss:// URL when given an absolute https:// URL', () => { + const result = resolveStreamUrl('https://agent.example.com/stream?id=123'); + expect(result).toBe('wss://agent.example.com/stream?id=123'); + }); + + it('returns an absolute ws:// URL when given an absolute http:// URL', () => { + const result = resolveStreamUrl('http://agent.example.com/stream'); + expect(result).toBe('ws://agent.example.com/stream'); + }); + + it('passes through an already-wss URL unchanged', () => { + const result = resolveStreamUrl('wss://agent.example.com/stream'); + expect(result).toBe('wss://agent.example.com/stream'); + }); +}); + +// ─── extractTextFromMessage ───────────────────────────────────────── + +function makeMessage(parts: ProcessedMessage['parts']): ProcessedMessage { + return { + info: { + id: 'msg-1', + sessionID: 'session-1', + role: 'assistant', + time: { created: Date.now() }, + }, + parts, + }; +} + +describe('extractTextFromMessage', () => { + it('returns empty string when message has no parts', () => { + expect(extractTextFromMessage(makeMessage([]))).toBe(''); + }); + + it('extracts text from a single text part', () => { + const message = makeMessage([ + { id: 'p1', sessionID: 's1', messageID: 'm1', type: 'text', text: 'PR opened at https://github.com/org/repo/pull/42' }, + ]); + expect(extractTextFromMessage(message)).toBe( + 'PR opened at https://github.com/org/repo/pull/42' + ); + }); + + it('concatenates multiple text parts', () => { + const message = makeMessage([ + { id: 'p1', sessionID: 's1', messageID: 'm1', type: 'text', text: 'Changes applied. ' }, + { id: 'p2', sessionID: 's1', messageID: 'm1', type: 'text', text: 'PR: https://github.com/org/repo/pull/42' }, + ]); + expect(extractTextFromMessage(message)).toBe( + 'Changes applied. PR: https://github.com/org/repo/pull/42' + ); + }); + + it('ignores non-text parts like tool parts', () => { + const message = makeMessage([ + { + id: 'p1', + sessionID: 's1', + messageID: 'm1', + type: 'tool', + call: { id: 'call-1', name: 'bash', input: '{}' }, + } as ProcessedMessage['parts'][number], + { id: 'p2', sessionID: 's1', messageID: 'm1', type: 'text', text: 'Done!' }, + ]); + expect(extractTextFromMessage(message)).toBe('Done!'); + }); + + it('trims whitespace from the final result', () => { + const message = makeMessage([ + { id: 'p1', sessionID: 's1', messageID: 'm1', type: 'text', text: ' Hello ' }, + ]); + expect(extractTextFromMessage(message)).toBe('Hello'); + }); +}); diff --git a/src/lib/cloud-agent-next/run-session.ts b/src/lib/cloud-agent-next/run-session.ts index 4425be6a9..e183b5138 100644 --- a/src/lib/cloud-agent-next/run-session.ts +++ b/src/lib/cloud-agent-next/run-session.ts @@ -120,8 +120,10 @@ export type RunSessionResult = { response: string; /** The cloud-agent session ID (available even on failure). */ sessionId?: string; - /** Whether the session encountered an error. */ + /** Whether the session encountered a fatal error (stream/WS/session-level). */ hasError: boolean; + /** Whether stderr output was observed (informational; does NOT imply failure). */ + hasStderr: boolean; /** Collected status/error messages for diagnostics. */ statusMessages: string[]; }; @@ -157,6 +159,7 @@ export async function runSessionToCompletion(input: RunSessionInput): Promise