mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
250 lines
9.2 KiB
TypeScript
250 lines
9.2 KiB
TypeScript
/**
|
||
* 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);
|