From fb3deeff385e104c68190b876d25a0487a51aa40 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sun, 16 Nov 2025 06:40:31 -0500 Subject: [PATCH] fix(delegation): handle undefined totalCost in timeout error formatting Add defensive checks in result-formatter, headless-executor, and session-manager to prevent TypeError when delegated sessions timeout without emitting a result. Includes 4 comprehensive unit tests for undefined/null totalCost scenarios. Fixes formatting error that prevented timeout messages from displaying. --- bin/delegation/headless-executor.js | 3 +- bin/delegation/result-formatter.js | 4 +- bin/delegation/session-manager.js | 3 +- .../unit/delegation/result-formatter.test.js | 80 +++++++++++++++++++ 4 files changed, 87 insertions(+), 3 deletions(-) diff --git a/bin/delegation/headless-executor.js b/bin/delegation/headless-executor.js index e4104e03..4c05d3a3 100644 --- a/bin/delegation/headless-executor.js +++ b/bin/delegation/headless-executor.js @@ -88,7 +88,8 @@ class HeadlessExecutor { if (lastSession) { args.push('--resume', lastSession.sessionId); if (process.env.CCS_DEBUG) { - console.error(`[i] Resuming session: ${lastSession.sessionId} (${lastSession.turns} turns, $${lastSession.totalCost.toFixed(4)})`); + const cost = lastSession.totalCost !== undefined && lastSession.totalCost !== null ? lastSession.totalCost.toFixed(4) : '0.0000'; + console.error(`[i] Resuming session: ${lastSession.sessionId} (${lastSession.turns} turns, $${cost})`); } } else if (sessionId) { args.push('--resume', sessionId); diff --git a/bin/delegation/result-formatter.js b/bin/delegation/result-formatter.js index 798f674c..85257e8a 100644 --- a/bin/delegation/result-formatter.js +++ b/bin/delegation/result-formatter.js @@ -437,7 +437,9 @@ class ResultFormatter { // Abbreviate session ID (Git-style first 8 chars) const shortId = sessionId && sessionId.length > 8 ? sessionId.substring(0, 8) : sessionId; output += `[i] Session persisted with ID: ${shortId}\n`; - output += `[i] Cost: $${totalCost.toFixed(4)}\n`; + if (totalCost !== undefined && totalCost !== null) { + output += `[i] Cost: $${totalCost.toFixed(4)}\n`; + } return output; } diff --git a/bin/delegation/session-manager.js b/bin/delegation/session-manager.js index 5bc8a62a..6d2a3af6 100644 --- a/bin/delegation/session-manager.js +++ b/bin/delegation/session-manager.js @@ -60,7 +60,8 @@ class SessionManager { this._saveSessions(sessions); if (process.env.CCS_DEBUG) { - console.error(`[i] Updated session: ${sessionId}, total: $${sessions[key].totalCost.toFixed(4)}, turns: ${sessions[key].turns}`); + const cost = sessions[key].totalCost !== undefined && sessions[key].totalCost !== null ? sessions[key].totalCost.toFixed(4) : '0.0000'; + console.error(`[i] Updated session: ${sessionId}, total: $${cost}, turns: ${sessions[key].turns}`); } } } diff --git a/tests/unit/delegation/result-formatter.test.js b/tests/unit/delegation/result-formatter.test.js index 7a6f1bba..b442a6b8 100644 --- a/tests/unit/delegation/result-formatter.test.js +++ b/tests/unit/delegation/result-formatter.test.js @@ -284,5 +284,85 @@ runner.test('Should show file counts in info box', () => { assertIncludes(formatted, 'Files Modified: 1', 'Should show modified count'); }); +// Test 15: Handle undefined totalCost in timeout error +runner.test('Should handle undefined totalCost in timeout error', () => { + const result = { + profile: 'glm', + cwd: '/test', + duration: 120000, + sessionId: 'test-session-123', + totalCost: undefined, + numTurns: 5, + timedOut: true + }; + + // Should not throw TypeError + const formatted = ResultFormatter.format(result); + + assertIncludes(formatted, 'Execution timed out', 'Should show timeout message'); + assertIncludes(formatted, 'test-ses', 'Should show abbreviated session ID'); + // Cost line should be omitted when undefined + assert(!formatted.includes('Cost: $'), 'Should not show cost when undefined'); +}); + +// Test 16: Handle null totalCost in timeout error +runner.test('Should handle null totalCost in timeout error', () => { + const result = { + profile: 'kimi', + cwd: '/test', + duration: 60000, + sessionId: 'test-session-456', + totalCost: null, + numTurns: 3, + timedOut: true + }; + + // Should not throw TypeError + const formatted = ResultFormatter.format(result); + + assertIncludes(formatted, 'Execution timed out', 'Should show timeout message'); + assert(!formatted.includes('Cost: $'), 'Should not show cost when null'); +}); + +// Test 17: Show totalCost when defined in timeout error +runner.test('Should show totalCost when defined in timeout error', () => { + const result = { + profile: 'glm', + cwd: '/test', + duration: 90000, + sessionId: 'test-session-789', + totalCost: 0.1234, + numTurns: 4, + timedOut: true + }; + + const formatted = ResultFormatter.format(result); + + assertIncludes(formatted, 'Cost: $0.1234', 'Should show formatted cost'); +}); + +// Test 18: Handle undefined totalCost in normal result +runner.test('Should handle undefined totalCost in normal result', () => { + const result = { + profile: 'kimi', + cwd: '/test', + exitCode: 0, + stdout: 'Task completed', + stderr: '', + duration: 5000, + success: true, + sessionId: 'session-abc', + totalCost: undefined, + numTurns: 2 + }; + + // Should not throw TypeError + const formatted = ResultFormatter.format(result); + + assertIncludes(formatted, '[OK]', 'Should show success'); + // Cost line should be omitted in info box when undefined + assert(!formatted.includes('Cost: $'), 'Should not show cost when undefined'); +}); + // Run all tests runner.run();