Files
Claude-Code-Workflow/ccw/tests/api-response-test.ts

250 lines
9.2 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Test script to verify CLI API response format
* Tests that the API returns properly parsed JSON without double-serialization
*/
import { join } from 'path';
async function testApiResponse() {
console.log('=== API Response Format Test ===\n');
// Use parent directory as project root (D:\Claude_dms3 instead of D:\Claude_dms3\ccw)
const projectPath = join(process.cwd(), '..');
// Test 1: Get a sample execution (you'll need to replace with an actual ID)
console.log('Test 1: Get conversation detail');
console.log('Project path:', projectPath);
try {
// Get the most recent execution for testing
const { getHistoryStore } = await import('../src/tools/cli-history-store.js');
const store = getHistoryStore(projectPath);
const history = store.getHistory({ limit: 1 });
if (history.total === 0 || history.executions.length === 0) {
console.log('❌ No execution history found. Please run a CLI command first.');
console.log('Example: ccw cli -p "test" --tool gemini --mode analysis\n');
return;
}
const executionId = history.executions[0].id;
console.log('Testing with execution ID:', executionId, '\n');
// Get conversation detail - use getConversationWithNativeInfo from store directly
const conversation = store.getConversationWithNativeInfo(executionId);
if (!conversation) {
console.log('❌ Conversation not found');
return;
}
console.log('✅ Conversation retrieved');
console.log(' - ID:', conversation.id);
console.log(' - Tool:', conversation.tool);
console.log(' - Mode:', conversation.mode);
console.log(' - Turns:', conversation.turns.length);
console.log();
// Test 2: Check turn output structure
console.log('Test 2: Verify turn output structure');
if (conversation.turns.length > 0) {
const firstTurn = conversation.turns[0];
console.log('First turn output keys:', Object.keys(firstTurn.output));
console.log();
// Test 3: Check for double-serialization
console.log('Test 3: Check for JSON double-serialization');
const outputFields = [
'stdout',
'stderr',
'parsed_output',
'final_output'
];
let hasDoubleSerializtion = false;
for (const field of outputFields) {
const value = firstTurn.output[field as keyof typeof firstTurn.output];
if (value && typeof value === 'string') {
// Check if the string starts with a JSON structure indicator
const trimmed = value.trim();
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
try {
const parsed = JSON.parse(trimmed);
console.log(`⚠️ ${field}: Contains JSON string (length: ${trimmed.length})`);
console.log(` First 100 chars: ${trimmed.substring(0, 100)}...`);
console.log(` Parsed type: ${typeof parsed}, keys: ${Object.keys(parsed).slice(0, 5).join(', ')}`);
hasDoubleSerializtion = true;
} catch {
// Not JSON, this is fine
console.log(`${field}: Plain text (length: ${trimmed.length})`);
}
} else {
console.log(`${field}: Plain text (length: ${trimmed.length})`);
}
} else if (value) {
console.log(` ${field}: Type ${typeof value}`);
}
}
console.log();
if (hasDoubleSerializtion) {
console.log('❌ ISSUE FOUND: Some fields contain JSON strings instead of plain text');
console.log(' This suggests double-serialization or incorrect parsing.');
} else {
console.log('✅ No double-serialization detected');
}
}
// Test 4: Simulate API JSON.stringify
console.log('\nTest 4: Simulate API response serialization');
const apiResponse = JSON.stringify(conversation);
console.log('API response length:', apiResponse.length);
// Parse it back (like frontend would)
const parsed = JSON.parse(apiResponse);
console.log('✅ Can be parsed back');
console.log('Parsed turn count:', parsed.turns.length);
if (parsed.turns.length > 0) {
const parsedTurn = parsed.turns[0];
console.log('Parsed turn output keys:', Object.keys(parsedTurn.output));
// Check if parsed_output is accessible
if (parsedTurn.output.parsed_output) {
console.log('✅ parsed_output field is accessible');
console.log(' Length:', parsedTurn.output.parsed_output.length);
} else {
console.log('❌ parsed_output field is missing or undefined');
}
}
// Test 5: Check stdout content - is it JSON lines or plain text?
console.log('\nTest 5: Check stdout content format');
if (conversation.turns.length > 0) {
const stdout = conversation.turns[0].output.stdout;
const firstLines = stdout.split('\n').slice(0, 5);
console.log('First 5 lines of stdout:');
for (const line of firstLines) {
const trimmed = line.trim();
if (!trimmed) continue;
let isJson = false;
try {
JSON.parse(trimmed);
isJson = true;
} catch {}
console.log(` ${isJson ? '⚠️ JSON' : '✅ TEXT'}: ${trimmed.substring(0, 120)}${trimmed.length > 120 ? '...' : ''}`);
}
// Compare stdout vs parsed_output
const parsedOutput = conversation.turns[0].output.parsed_output;
console.log('\nTest 6: Compare stdout vs parsed_output');
console.log(` stdout length: ${stdout.length}`);
console.log(` parsed_output length: ${parsedOutput?.length || 0}`);
if (parsedOutput) {
const parsedFirstLines = parsedOutput.split('\n').slice(0, 3);
console.log(' First 3 lines of parsed_output:');
for (const line of parsedFirstLines) {
console.log(` ${line.substring(0, 120)}${line.length > 120 ? '...' : ''}`);
}
}
}
} catch (error) {
console.error('❌ Test failed:', error);
}
}
/**
* Test that buildCommand returns correct outputFormat for all tools
*/
async function testOutputFormatDetection() {
console.log('\n=== Output Format Detection Test ===\n');
const { buildCommand } = await import('../src/tools/cli-executor-utils.js');
const tools = ['gemini', 'qwen', 'codex', 'claude', 'opencode'];
for (const tool of tools) {
try {
const result = buildCommand({
tool,
prompt: 'test prompt',
mode: 'analysis',
});
const expected = 'json-lines';
const status = result.outputFormat === expected ? '✅' : '❌';
console.log(` ${status} ${tool}: outputFormat = "${result.outputFormat}" (expected: "${expected}")`);
} catch (err) {
console.log(` ⚠️ ${tool}: buildCommand error (${(err as Error).message})`);
}
}
}
/**
* Test that JsonLinesParser correctly extracts text from Gemini JSON lines
*/
async function testJsonLinesParsing() {
console.log('\n=== JSON Lines Parser Test ===\n');
const { createOutputParser, flattenOutputUnits } = await import('../src/tools/cli-output-converter.js');
const parser = createOutputParser('json-lines');
// Simulate Gemini stream-json output
const geminiLines = [
'{"type":"init","timestamp":"2026-01-01T00:00:00.000Z","session_id":"test-session","model":"gemini-2.5-pro"}',
'{"type":"message","timestamp":"2026-01-01T00:00:01.000Z","role":"user","content":"test prompt"}',
'{"type":"message","timestamp":"2026-01-01T00:00:02.000Z","role":"assistant","content":"Hello, this is the response text.","delta":true}',
'{"type":"message","timestamp":"2026-01-01T00:00:03.000Z","role":"assistant","content":" More response text here.","delta":true}',
'{"type":"result","timestamp":"2026-01-01T00:00:04.000Z","status":"success","stats":{"input_tokens":100,"output_tokens":50}}',
];
const input = Buffer.from(geminiLines.join('\n') + '\n');
const units = parser.parse(input, 'stdout');
const remaining = parser.flush();
const allUnits = [...units, ...remaining];
console.log(` Total IR units created: ${allUnits.length}`);
for (const unit of allUnits) {
const contentPreview = typeof unit.content === 'string'
? unit.content.substring(0, 80)
: JSON.stringify(unit.content).substring(0, 80);
console.log(` Type: ${unit.type.padEnd(20)} Content: ${contentPreview}`);
}
// Test flattenOutputUnits with same filters as cli-executor-core.ts
const parsedOutput = flattenOutputUnits(allUnits, {
excludeTypes: ['stderr', 'progress', 'metadata', 'system', 'tool_call', 'thought', 'code', 'file_diff', 'streaming_content'],
stripCommandJsonBlocks: true
});
console.log();
console.log(` parsed_output result:`);
console.log(` "${parsedOutput}"`);
// Verify it's NOT JSON lines
const firstLine = parsedOutput.split('\n')[0]?.trim();
let isJson = false;
try {
JSON.parse(firstLine);
isJson = true;
} catch {}
if (isJson) {
console.log(` ❌ parsed_output still contains JSON lines!`);
} else {
console.log(` ✅ parsed_output contains plain text (not JSON lines)`);
}
}
// Run all tests
(async () => {
await testApiResponse();
await testOutputFormatDetection();
await testJsonLinesParsing();
})().catch(console.error);