mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 10:19:37 +00:00
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.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user