mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
feat(cli): add CLI prompt simulation and testing scripts
- Introduced `simulate-cli-prompt.js` to simulate various prompt formats and display the final content passed to the CLI. - Added `test-shell-prompt.js` to test actual shell execution of different prompt formats, demonstrating correct vs incorrect multi-line prompt handling. - Created comprehensive tests in `cli-prompt-parsing.test.ts` to validate prompt parsing, including single-line, multi-line, special characters, and template concatenation. - Implemented edge case handling for empty lines, long prompts, and Unicode characters.
This commit is contained in:
32
ccw/bin/ccw-native.js
Normal file
32
ccw/bin/ccw-native.js
Normal file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* CCW Native Wrapper
|
||||
*
|
||||
* 替代 npm 生成的 shell wrapper,直接用 Node.js 处理参数传递,
|
||||
* 避免 Git Bash + Windows 的多行参数问题。
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// 目标脚本路径
|
||||
const targetScript = join(__dirname, 'ccw.js');
|
||||
|
||||
// 直接传递所有参数,不经过 shell
|
||||
const child = spawn(process.execPath, [targetScript, ...process.argv.slice(2)], {
|
||||
stdio: 'inherit',
|
||||
shell: false, // 关键:不使用 shell,避免参数被 shell 解析
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
process.exit(code || 0);
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
console.error('Failed to start ccw:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
283
ccw/bin/ccw-prompt-test.js
Normal file
283
ccw/bin/ccw-prompt-test.js
Normal file
@@ -0,0 +1,283 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* CCW Prompt Test Endpoint
|
||||
*
|
||||
* 独立的提示词解析测试工具,用于调试不同格式的提示词传递。
|
||||
*
|
||||
* Usage:
|
||||
* node ccw/bin/ccw-prompt-test.js -p "prompt" # 测试 -p 参数
|
||||
* node ccw/bin/ccw-prompt-test.js -f file.txt # 测试文件读取
|
||||
* echo "prompt" | node ccw/bin/ccw-prompt-test.js # 测试 stdin
|
||||
* node ccw/bin/ccw-prompt-test.js --raw # 原始 argv 输出
|
||||
*/
|
||||
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
|
||||
// ANSI colors
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
bold: '\x1b[1m',
|
||||
dim: '\x1b[2m',
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
magenta: '\x1b[35m',
|
||||
cyan: '\x1b[36m',
|
||||
white: '\x1b[37m',
|
||||
gray: '\x1b[90m',
|
||||
};
|
||||
|
||||
const c = (color, text) => `${colors[color]}${text}${colors.reset}`;
|
||||
|
||||
/**
|
||||
* Parse command line arguments manually (no dependencies)
|
||||
*/
|
||||
function parseArgs(argv) {
|
||||
const args = argv.slice(2);
|
||||
const result = {
|
||||
prompt: undefined,
|
||||
file: undefined,
|
||||
tool: 'gemini',
|
||||
mode: 'analysis',
|
||||
raw: false,
|
||||
help: false,
|
||||
positional: [],
|
||||
};
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
|
||||
if (arg === '--help' || arg === '-h') {
|
||||
result.help = true;
|
||||
} else if (arg === '--raw') {
|
||||
result.raw = true;
|
||||
} else if (arg === '-p' || arg === '--prompt') {
|
||||
result.prompt = args[++i];
|
||||
} else if (arg === '-f' || arg === '--file') {
|
||||
result.file = args[++i];
|
||||
} else if (arg === '--tool') {
|
||||
result.tool = args[++i];
|
||||
} else if (arg === '--mode') {
|
||||
result.mode = args[++i];
|
||||
} else if (!arg.startsWith('-')) {
|
||||
result.positional.push(arg);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read from stdin if available (non-TTY)
|
||||
*/
|
||||
function readStdin() {
|
||||
if (process.stdin.isTTY) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return readFileSync(0, 'utf8').trim();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze prompt content
|
||||
*/
|
||||
function analyzePrompt(prompt) {
|
||||
if (!prompt) return null;
|
||||
|
||||
const analysis = {
|
||||
length: prompt.length,
|
||||
lines: prompt.split('\n').length,
|
||||
hasNewlines: prompt.includes('\n'),
|
||||
hasAtPatterns: /@[^\s]+/.test(prompt),
|
||||
atPatterns: [],
|
||||
hasBullets: /[•●○■□▪▫]/.test(prompt),
|
||||
hasMemory: /Memory:/i.test(prompt),
|
||||
sections: [],
|
||||
};
|
||||
|
||||
// Extract @ patterns
|
||||
const atMatches = prompt.matchAll(/@[^\s|]+/g);
|
||||
analysis.atPatterns = Array.from(atMatches).map(m => m[0]);
|
||||
|
||||
// Detect sections
|
||||
const sectionPatterns = ['PURPOSE', 'TASK', 'MODE', 'CONTEXT', 'EXPECTED', 'CONSTRAINTS'];
|
||||
for (const section of sectionPatterns) {
|
||||
if (new RegExp(`${section}:`, 'i').test(prompt)) {
|
||||
analysis.sections.push(section);
|
||||
}
|
||||
}
|
||||
|
||||
return analysis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print box
|
||||
*/
|
||||
function printBox(title, content, color = 'cyan') {
|
||||
const width = 70;
|
||||
const line = '─'.repeat(width);
|
||||
console.log(c(color, `┌${line}┐`));
|
||||
console.log(c(color, '│') + c('bold', ` ${title}`.padEnd(width)) + c(color, '│'));
|
||||
console.log(c(color, `├${line}┤`));
|
||||
|
||||
const lines = content.split('\n');
|
||||
for (const l of lines) {
|
||||
const truncated = l.length > width - 2 ? l.substring(0, width - 5) + '...' : l;
|
||||
console.log(c(color, '│') + ` ${truncated}`.padEnd(width) + c(color, '│'));
|
||||
}
|
||||
console.log(c(color, `└${line}┘`));
|
||||
}
|
||||
|
||||
/**
|
||||
* Main
|
||||
*/
|
||||
function main() {
|
||||
const parsed = parseArgs(process.argv);
|
||||
|
||||
// Help
|
||||
if (parsed.help) {
|
||||
console.log(`
|
||||
${c('bold', 'CCW Prompt Test Endpoint')}
|
||||
|
||||
${c('cyan', 'Usage:')}
|
||||
node ccw/bin/ccw-prompt-test.js -p "prompt" Test -p argument
|
||||
node ccw/bin/ccw-prompt-test.js -f file.txt Test file input
|
||||
echo "prompt" | node ccw/bin/ccw-prompt-test.js Test stdin pipe
|
||||
node ccw/bin/ccw-prompt-test.js --raw Show raw argv only
|
||||
|
||||
${c('cyan', 'Options:')}
|
||||
-p, --prompt <text> Prompt text
|
||||
-f, --file <path> Read prompt from file
|
||||
--tool <tool> Tool name (default: gemini)
|
||||
--mode <mode> Mode (default: analysis)
|
||||
--raw Show only raw process.argv
|
||||
-h, --help Show this help
|
||||
|
||||
${c('cyan', 'Multi-line prompt methods:')}
|
||||
${c('green', '1. Stdin pipe (recommended):')}
|
||||
echo "PURPOSE: Test
|
||||
TASK: Step 1" | node ccw/bin/ccw-prompt-test.js
|
||||
|
||||
${c('green', '2. File input:')}
|
||||
node ccw/bin/ccw-prompt-test.js -f prompt.txt
|
||||
|
||||
${c('green', '3. Heredoc:')}
|
||||
node ccw/bin/ccw-prompt-test.js << 'EOF'
|
||||
PURPOSE: Test
|
||||
TASK: Step 1
|
||||
EOF
|
||||
`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Raw mode
|
||||
if (parsed.raw) {
|
||||
console.log(c('bold', '\nRaw process.argv:'));
|
||||
process.argv.forEach((arg, i) => {
|
||||
console.log(` [${i}]: ${JSON.stringify(arg)}`);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(c('bold', '\n═══════════════════════════════════════════════════════════════════════'));
|
||||
console.log(c('bold', ' CCW PROMPT TEST ENDPOINT'));
|
||||
console.log(c('bold', '═══════════════════════════════════════════════════════════════════════\n'));
|
||||
|
||||
// 1. Raw argv
|
||||
console.log(c('yellow', '📦 1. RAW PROCESS.ARGV:'));
|
||||
console.log(c('gray', ` Total: ${process.argv.length} arguments`));
|
||||
process.argv.forEach((arg, i) => {
|
||||
const display = arg.length > 60 ? arg.substring(0, 57) + '...' : arg;
|
||||
const hasNewline = arg.includes('\n');
|
||||
console.log(c('gray', ` [${i}]: `) + c(hasNewline ? 'green' : 'white', JSON.stringify(display)));
|
||||
if (hasNewline) {
|
||||
console.log(c('green', ` ↳ Contains ${arg.split('\n').length} lines (newlines preserved!)`));
|
||||
}
|
||||
});
|
||||
console.log();
|
||||
|
||||
// 2. Parsed options
|
||||
console.log(c('yellow', '📋 2. PARSED OPTIONS:'));
|
||||
console.log(c('gray', ' --prompt: ') + (parsed.prompt ? c('green', JSON.stringify(parsed.prompt.substring(0, 50) + (parsed.prompt.length > 50 ? '...' : ''))) : c('dim', '(not set)')));
|
||||
console.log(c('gray', ' --file: ') + (parsed.file ? c('cyan', parsed.file) : c('dim', '(not set)')));
|
||||
console.log(c('gray', ' --tool: ') + c('white', parsed.tool));
|
||||
console.log(c('gray', ' --mode: ') + c('white', parsed.mode));
|
||||
console.log(c('gray', ' stdin.isTTY: ') + c(process.stdin.isTTY ? 'yellow' : 'green', String(process.stdin.isTTY)));
|
||||
console.log();
|
||||
|
||||
// 3. Resolve final prompt
|
||||
console.log(c('yellow', '🎯 3. PROMPT RESOLUTION:'));
|
||||
let finalPrompt = null;
|
||||
let source = null;
|
||||
|
||||
// Priority: file > stdin > -p > positional
|
||||
if (parsed.file) {
|
||||
source = 'file';
|
||||
const filePath = resolve(parsed.file);
|
||||
if (existsSync(filePath)) {
|
||||
finalPrompt = readFileSync(filePath, 'utf8').trim();
|
||||
console.log(c('gray', ' Source: ') + c('magenta', `--file (${filePath})`));
|
||||
} else {
|
||||
console.log(c('red', ` Error: File not found: ${filePath}`));
|
||||
}
|
||||
} else {
|
||||
const stdinContent = readStdin();
|
||||
if (stdinContent) {
|
||||
source = 'stdin';
|
||||
finalPrompt = stdinContent;
|
||||
console.log(c('gray', ' Source: ') + c('green', 'stdin (piped input)'));
|
||||
} else if (parsed.prompt) {
|
||||
source = '-p option';
|
||||
finalPrompt = parsed.prompt;
|
||||
console.log(c('gray', ' Source: ') + c('cyan', '--prompt/-p option'));
|
||||
} else if (parsed.positional.length > 0) {
|
||||
source = 'positional';
|
||||
finalPrompt = parsed.positional.join(' ');
|
||||
console.log(c('gray', ' Source: ') + c('yellow', 'positional argument'));
|
||||
} else {
|
||||
console.log(c('red', ' No prompt found!'));
|
||||
}
|
||||
}
|
||||
console.log();
|
||||
|
||||
// 4. Prompt analysis
|
||||
if (finalPrompt) {
|
||||
const analysis = analyzePrompt(finalPrompt);
|
||||
|
||||
console.log(c('yellow', '📊 4. PROMPT ANALYSIS:'));
|
||||
console.log(c('gray', ' Length: ') + c('white', `${analysis.length} chars`));
|
||||
console.log(c('gray', ' Lines: ') + c('white', String(analysis.lines)));
|
||||
console.log(c('gray', ' Has newlines: ') + c(analysis.hasNewlines ? 'green' : 'yellow', analysis.hasNewlines ? '✓ Yes' : '✗ No'));
|
||||
console.log(c('gray', ' Has @ patterns: ') + c(analysis.hasAtPatterns ? 'green' : 'dim', analysis.hasAtPatterns ? '✓ Yes' : '✗ No'));
|
||||
if (analysis.atPatterns.length > 0) {
|
||||
console.log(c('gray', ' @ patterns:'));
|
||||
analysis.atPatterns.forEach(p => console.log(c('blue', ` • ${p}`)));
|
||||
}
|
||||
console.log(c('gray', ' Has bullets: ') + c(analysis.hasBullets ? 'green' : 'dim', analysis.hasBullets ? '✓ Yes' : '✗ No'));
|
||||
console.log(c('gray', ' Has Memory: ') + c(analysis.hasMemory ? 'green' : 'dim', analysis.hasMemory ? '✓ Yes' : '✗ No'));
|
||||
if (analysis.sections.length > 0) {
|
||||
console.log(c('gray', ' Sections: ') + c('cyan', analysis.sections.join(', ')));
|
||||
}
|
||||
console.log();
|
||||
|
||||
// 5. Final prompt content
|
||||
console.log(c('yellow', '📄 5. FINAL PROMPT CONTENT:'));
|
||||
printBox(`${source} → ${analysis.length} chars, ${analysis.lines} lines`, finalPrompt, 'green');
|
||||
console.log();
|
||||
|
||||
// 6. Simulated CLI command
|
||||
console.log(c('yellow', '🚀 6. SIMULATED CLI EXECUTION:'));
|
||||
console.log(c('gray', ' Would execute:'));
|
||||
console.log(c('cyan', ` ${parsed.tool} cli --mode ${parsed.mode}`));
|
||||
console.log(c('gray', ' With prompt of ') + c('green', `${analysis.length} chars, ${analysis.lines} lines`));
|
||||
}
|
||||
|
||||
console.log(c('bold', '\n═══════════════════════════════════════════════════════════════════════\n'));
|
||||
}
|
||||
|
||||
main();
|
||||
214
ccw/scripts/simulate-cli-prompt.js
Normal file
214
ccw/scripts/simulate-cli-prompt.js
Normal file
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* CLI Prompt Simulation Script
|
||||
*
|
||||
* Simulates different prompt formats and outputs the final content passed to CLI.
|
||||
* Usage: node ccw/scripts/simulate-cli-prompt.js
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
|
||||
// Test cases for different prompt formats
|
||||
const testCases = [
|
||||
{
|
||||
name: 'Single-line prompt',
|
||||
input: {
|
||||
prompt: 'Analyze the authentication module for security issues',
|
||||
tool: 'gemini',
|
||||
mode: 'analysis',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Single-line with quotes',
|
||||
input: {
|
||||
prompt: 'Fix the error: "Cannot read property \'id\' of undefined"',
|
||||
tool: 'gemini',
|
||||
mode: 'analysis',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Multi-line structured prompt',
|
||||
input: {
|
||||
prompt: `PURPOSE: Identify security vulnerabilities
|
||||
TASK: • Scan injection flaws • Check auth bypass
|
||||
MODE: analysis
|
||||
CONTEXT: @src/auth/**/*
|
||||
EXPECTED: Security report`,
|
||||
tool: 'gemini',
|
||||
mode: 'analysis',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '@ patterns with glob wildcards',
|
||||
input: {
|
||||
prompt: `CONTEXT: @src/**/*.{ts,tsx} @!node_modules/** @!dist/**
|
||||
TASK: Analyze TypeScript files`,
|
||||
tool: 'gemini',
|
||||
mode: 'analysis',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '@ patterns with Memory section',
|
||||
input: {
|
||||
prompt: `PURPOSE: Security audit
|
||||
CONTEXT: @src/auth/**/* @src/middleware/auth.ts | Memory: Using bcrypt for passwords, JWT for sessions
|
||||
EXPECTED: Vulnerability report`,
|
||||
tool: 'gemini',
|
||||
mode: 'analysis',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Full template format (from cli-tools-usage.md)',
|
||||
input: {
|
||||
prompt: `PURPOSE: Identify OWASP Top 10 vulnerabilities in authentication module to pass security audit; success = all critical/high issues documented with remediation
|
||||
TASK: • Scan for injection flaws (SQL, command, LDAP) • Check authentication bypass vectors • Evaluate session management • Assess sensitive data exposure
|
||||
MODE: analysis
|
||||
CONTEXT: @src/auth/**/* @src/middleware/auth.ts | Memory: Using bcrypt for passwords, JWT for sessions
|
||||
EXPECTED: Security report with: severity matrix, file:line references, CVE mappings where applicable, remediation code snippets prioritized by risk
|
||||
CONSTRAINTS: Focus on authentication | Ignore test files`,
|
||||
tool: 'gemini',
|
||||
mode: 'analysis',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Special characters and Unicode',
|
||||
input: {
|
||||
prompt: `TASK: • 分析代码 • Check → errors • Fix ✗ issues
|
||||
EXPECTED: Report with ✓ checkmarks`,
|
||||
tool: 'gemini',
|
||||
mode: 'analysis',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Code-like content',
|
||||
input: {
|
||||
prompt: `Fix: const result = arr.filter(x => x > 0).map(x => x * 2);
|
||||
Error at line 42: TypeError: Cannot read property 'length' of null`,
|
||||
tool: 'gemini',
|
||||
mode: 'write',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Shell-like patterns',
|
||||
input: {
|
||||
prompt: `Run: npm run build && npm test | grep "passed"
|
||||
Expected output: All tests passed`,
|
||||
tool: 'gemini',
|
||||
mode: 'analysis',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Simulate prompt processing (mirrors cli.ts logic)
|
||||
*/
|
||||
function simulatePromptProcessing(input) {
|
||||
const { prompt, tool, mode, rule = 'universal-rigorous-style' } = input;
|
||||
|
||||
// Step 1: Get base prompt
|
||||
let finalPrompt = prompt;
|
||||
|
||||
// Step 2: Extract @ patterns from CONTEXT (for cache simulation)
|
||||
const contextMatch = prompt.match(/CONTEXT:\s*([^\n]+)/i);
|
||||
let extractedPatterns = [];
|
||||
if (contextMatch) {
|
||||
const contextLine = contextMatch[1];
|
||||
const patternMatches = contextLine.matchAll(/@[^\s|]+/g);
|
||||
extractedPatterns = Array.from(patternMatches).map(m => m[0]);
|
||||
}
|
||||
|
||||
// Step 3: Simulate template concatenation
|
||||
const mockSystemRules = `[SYSTEM RULES - ${mode} mode protocol loaded]`;
|
||||
const mockRoles = `[ROLES - ${rule} template loaded]`;
|
||||
|
||||
const parts = [finalPrompt];
|
||||
parts.push(`\n=== SYSTEM RULES ===\n${mockSystemRules}`);
|
||||
parts.push(`\n=== ROLES ===\n${mockRoles}`);
|
||||
|
||||
return {
|
||||
originalPrompt: prompt,
|
||||
finalPrompt: parts.join('\n'),
|
||||
extractedPatterns,
|
||||
metadata: {
|
||||
tool,
|
||||
mode,
|
||||
rule,
|
||||
originalLength: prompt.length,
|
||||
finalLength: parts.join('\n').length,
|
||||
lineCount: parts.join('\n').split('\n').length,
|
||||
hasMultiline: prompt.includes('\n'),
|
||||
hasAtPatterns: extractedPatterns.length > 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Display test result
|
||||
*/
|
||||
function displayResult(testCase, result) {
|
||||
console.log(chalk.bold.cyan('\n' + '═'.repeat(70)));
|
||||
console.log(chalk.bold.white(`📋 Test: ${testCase.name}`));
|
||||
console.log(chalk.cyan('═'.repeat(70)));
|
||||
|
||||
// Input section
|
||||
console.log(chalk.bold.yellow('\n📥 INPUT:'));
|
||||
console.log(chalk.gray(' Tool: ') + chalk.green(testCase.input.tool));
|
||||
console.log(chalk.gray(' Mode: ') + chalk.green(testCase.input.mode));
|
||||
console.log(chalk.gray(' Prompt:'));
|
||||
console.log(chalk.white(' ┌' + '─'.repeat(66) + '┐'));
|
||||
testCase.input.prompt.split('\n').forEach(line => {
|
||||
const truncated = line.length > 64 ? line.substring(0, 61) + '...' : line;
|
||||
console.log(chalk.white(' │ ') + chalk.cyan(truncated.padEnd(64)) + chalk.white(' │'));
|
||||
});
|
||||
console.log(chalk.white(' └' + '─'.repeat(66) + '┘'));
|
||||
|
||||
// Metadata section
|
||||
console.log(chalk.bold.yellow('\n📊 METADATA:'));
|
||||
console.log(chalk.gray(' Original length: ') + chalk.magenta(result.metadata.originalLength + ' chars'));
|
||||
console.log(chalk.gray(' Final length: ') + chalk.magenta(result.metadata.finalLength + ' chars'));
|
||||
console.log(chalk.gray(' Line count: ') + chalk.magenta(result.metadata.lineCount));
|
||||
console.log(chalk.gray(' Has multiline: ') + (result.metadata.hasMultiline ? chalk.green('✓') : chalk.red('✗')));
|
||||
console.log(chalk.gray(' Has @ patterns: ') + (result.metadata.hasAtPatterns ? chalk.green('✓') : chalk.red('✗')));
|
||||
|
||||
if (result.extractedPatterns.length > 0) {
|
||||
console.log(chalk.gray(' Extracted patterns:'));
|
||||
result.extractedPatterns.forEach(p => {
|
||||
console.log(chalk.gray(' • ') + chalk.blue(p));
|
||||
});
|
||||
}
|
||||
|
||||
// Final prompt section
|
||||
console.log(chalk.bold.yellow('\n📤 FINAL PROMPT (passed to CLI):'));
|
||||
console.log(chalk.white(' ┌' + '─'.repeat(66) + '┐'));
|
||||
result.finalPrompt.split('\n').slice(0, 15).forEach(line => {
|
||||
const truncated = line.length > 64 ? line.substring(0, 61) + '...' : line;
|
||||
console.log(chalk.white(' │ ') + chalk.green(truncated.padEnd(64)) + chalk.white(' │'));
|
||||
});
|
||||
if (result.finalPrompt.split('\n').length > 15) {
|
||||
console.log(chalk.white(' │ ') + chalk.dim(`... (${result.finalPrompt.split('\n').length - 15} more lines)`.padEnd(64)) + chalk.white(' │'));
|
||||
}
|
||||
console.log(chalk.white(' └' + '─'.repeat(66) + '┘'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Main execution
|
||||
*/
|
||||
function main() {
|
||||
console.log(chalk.bold.magenta('\n' + '█'.repeat(70)));
|
||||
console.log(chalk.bold.white(' CLI PROMPT SIMULATION - Testing Different Prompt Formats'));
|
||||
console.log(chalk.bold.magenta('█'.repeat(70)));
|
||||
|
||||
console.log(chalk.gray('\nThis script simulates how different prompt formats are processed'));
|
||||
console.log(chalk.gray('and shows the final content passed to the CLI executor.\n'));
|
||||
|
||||
for (const testCase of testCases) {
|
||||
const result = simulatePromptProcessing(testCase.input);
|
||||
displayResult(testCase, result);
|
||||
}
|
||||
|
||||
console.log(chalk.bold.magenta('\n' + '█'.repeat(70)));
|
||||
console.log(chalk.bold.white(` Completed ${testCases.length} simulations`));
|
||||
console.log(chalk.bold.magenta('█'.repeat(70) + '\n'));
|
||||
}
|
||||
|
||||
main();
|
||||
159
ccw/scripts/test-shell-prompt.js
Normal file
159
ccw/scripts/test-shell-prompt.js
Normal file
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* CLI Shell Prompt Test Script
|
||||
*
|
||||
* Tests actual shell execution of different prompt formats.
|
||||
* Demonstrates correct vs incorrect multi-line prompt handling.
|
||||
*/
|
||||
|
||||
import { execSync, exec } from 'child_process';
|
||||
import { writeFileSync, unlinkSync, mkdtempSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import chalk from 'chalk';
|
||||
|
||||
const tmpDir = mkdtempSync(join(tmpdir(), 'ccw-shell-test-'));
|
||||
|
||||
/**
|
||||
* Execute ccw cli test-parse and capture output
|
||||
*/
|
||||
function testParse(command, description) {
|
||||
console.log(chalk.bold.cyan(`\n${'─'.repeat(70)}`));
|
||||
console.log(chalk.bold.white(`📋 ${description}`));
|
||||
console.log(chalk.gray(`Command: ${command.substring(0, 100)}${command.length > 100 ? '...' : ''}`));
|
||||
console.log(chalk.cyan('─'.repeat(70)));
|
||||
|
||||
try {
|
||||
const result = execSync(command, {
|
||||
encoding: 'utf8',
|
||||
cwd: process.cwd(),
|
||||
shell: true,
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
// Extract key info from output
|
||||
const promptMatch = result.match(/Value: "([^"]+)"/);
|
||||
const sourceMatch = result.match(/Source: ([^\n]+)/);
|
||||
|
||||
if (promptMatch) {
|
||||
console.log(chalk.green('✓ Parsed prompt: ') + chalk.yellow(promptMatch[1].substring(0, 60)));
|
||||
}
|
||||
if (sourceMatch) {
|
||||
console.log(chalk.gray(' Source: ') + sourceMatch[1]);
|
||||
}
|
||||
|
||||
// Check if prompt was truncated or split
|
||||
if (result.includes('Positional Arguments') && result.includes('[0]:')) {
|
||||
const posMatch = result.match(/\[0\]: "([^"]+)"/);
|
||||
if (posMatch) {
|
||||
console.log(chalk.red('⚠ WARNING: Part of prompt leaked to positional args: ') + chalk.yellow(posMatch[1]));
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, output: result };
|
||||
} catch (error) {
|
||||
console.log(chalk.red('✗ Error: ') + error.message);
|
||||
return { success: false, error };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main test suite
|
||||
*/
|
||||
function main() {
|
||||
console.log(chalk.bold.magenta('\n' + '█'.repeat(70)));
|
||||
console.log(chalk.bold.white(' SHELL PROMPT FORMAT TESTS'));
|
||||
console.log(chalk.bold.magenta('█'.repeat(70)));
|
||||
|
||||
// ============================================
|
||||
// INCORRECT METHODS (will fail or parse wrong)
|
||||
// ============================================
|
||||
console.log(chalk.bold.red('\n\n⛔ INCORRECT METHODS (will fail):'));
|
||||
|
||||
// Method 1: Direct multi-line in double quotes (WRONG)
|
||||
testParse(
|
||||
`ccw cli test-parse -p "PURPOSE: Test
|
||||
TASK: Step 1" --tool gemini`,
|
||||
'❌ Direct multi-line in double quotes (WRONG)'
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// CORRECT METHODS
|
||||
// ============================================
|
||||
console.log(chalk.bold.green('\n\n✅ CORRECT METHODS:'));
|
||||
|
||||
// Method 1: Single line (works)
|
||||
testParse(
|
||||
`ccw cli test-parse -p "PURPOSE: Test | TASK: Step 1" --tool gemini`,
|
||||
'✓ Single line with pipe separator'
|
||||
);
|
||||
|
||||
// Method 2: File-based (-f option) - RECOMMENDED
|
||||
const promptFile = join(tmpDir, 'prompt.txt');
|
||||
writeFileSync(promptFile, `PURPOSE: Identify vulnerabilities
|
||||
TASK: • Scan injection flaws • Check auth bypass
|
||||
MODE: analysis
|
||||
CONTEXT: @src/auth/**/*
|
||||
EXPECTED: Security report`);
|
||||
|
||||
testParse(
|
||||
`ccw cli test-parse -f "${promptFile}" --tool gemini`,
|
||||
'✓ File-based prompt (-f option) - RECOMMENDED'
|
||||
);
|
||||
|
||||
// Method 3: $'...' syntax with literal \n (bash only)
|
||||
testParse(
|
||||
`ccw cli test-parse -p $'PURPOSE: Test\\nTASK: Step 1\\nMODE: analysis' --tool gemini`,
|
||||
"✓ $'...' syntax with \\n (bash only)"
|
||||
);
|
||||
|
||||
// Method 4: Heredoc via stdin (if supported)
|
||||
// Note: ccw cli currently doesn't support stdin, but showing the pattern
|
||||
console.log(chalk.bold.cyan(`\n${'─'.repeat(70)}`));
|
||||
console.log(chalk.bold.white(`📋 ✓ Heredoc pattern (for reference)`));
|
||||
console.log(chalk.gray(`Command: cat <<'EOF' > prompt.txt && ccw cli -f prompt.txt ...`));
|
||||
console.log(chalk.cyan('─'.repeat(70)));
|
||||
console.log(chalk.green('✓ Create file with heredoc, then use -f option'));
|
||||
|
||||
// Method 5: Single line with escaped newlines in content
|
||||
testParse(
|
||||
`ccw cli test-parse -p "PURPOSE: Test | TASK: Step 1 | MODE: analysis | CONTEXT: @src/**/*" --tool gemini`,
|
||||
'✓ Single line with | as logical separator'
|
||||
);
|
||||
|
||||
// Cleanup
|
||||
try {
|
||||
unlinkSync(promptFile);
|
||||
} catch {}
|
||||
|
||||
// Summary
|
||||
console.log(chalk.bold.magenta('\n\n' + '█'.repeat(70)));
|
||||
console.log(chalk.bold.white(' SUMMARY: Recommended Approaches'));
|
||||
console.log(chalk.bold.magenta('█'.repeat(70)));
|
||||
|
||||
console.log(`
|
||||
${chalk.bold.green('For multi-line prompts, use ONE of these methods:')}
|
||||
|
||||
${chalk.bold('1. File-based (RECOMMENDED):')}
|
||||
${chalk.cyan('ccw cli -f prompt.txt --tool gemini --mode analysis')}
|
||||
|
||||
${chalk.bold("2. $'...' syntax (bash only):")}
|
||||
${chalk.cyan("ccw cli -p $'PURPOSE: ...\\nTASK: ...\\nMODE: ...' --tool gemini")}
|
||||
|
||||
${chalk.bold('3. Heredoc + file:')}
|
||||
${chalk.cyan(`cat <<'EOF' > /tmp/prompt.txt
|
||||
PURPOSE: ...
|
||||
TASK: ...
|
||||
EOF
|
||||
ccw cli -f /tmp/prompt.txt --tool gemini`)}
|
||||
|
||||
${chalk.bold('4. Single line with separators:')}
|
||||
${chalk.cyan('ccw cli -p "PURPOSE: ... | TASK: ... | MODE: ..." --tool gemini')}
|
||||
|
||||
${chalk.bold.red('AVOID:')}
|
||||
${chalk.red('ccw cli -p "PURPOSE: ...')}
|
||||
${chalk.red('TASK: ..." # Multi-line in quotes - WILL BREAK')}
|
||||
`);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -441,6 +441,11 @@ function testParseAction(args: string[], options: CliExecOptions): void {
|
||||
console.log(chalk.bold.cyan(' │ CLI PARSE TEST ENDPOINT │'));
|
||||
console.log(chalk.bold.cyan(' ═══════════════════════════════════════════════\n'));
|
||||
|
||||
// Debug: show raw options.prompt with JSON.stringify to reveal hidden characters
|
||||
console.log(chalk.bold.yellow('🔬 RAW OPTIONS.PROMPT (JSON):'));
|
||||
console.log(chalk.cyan(' ' + JSON.stringify(options.prompt)));
|
||||
console.log();
|
||||
|
||||
// Show args array parsing
|
||||
console.log(chalk.bold.yellow('📦 Positional Arguments (args[]):'));
|
||||
console.log(chalk.gray(' Length: ') + chalk.white(args.length));
|
||||
@@ -550,7 +555,7 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
||||
console.log(chalk.yellow(' Debug mode enabled\n'));
|
||||
}
|
||||
|
||||
// Priority: 1. --file, 2. --prompt/-p option, 3. positional argument
|
||||
// Priority: 1. --file, 2. stdin (piped), 3. --prompt/-p option, 4. positional argument
|
||||
// Note: On Windows, quoted arguments like -p "say hello" may be split into
|
||||
// -p "say" and positional "hello". We merge them back together.
|
||||
let finalPrompt: string | undefined;
|
||||
@@ -569,13 +574,30 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
||||
console.error(chalk.red('Error: File is empty'));
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (optionPrompt) {
|
||||
// Use --prompt/-p option (preferred for multi-line)
|
||||
// Merge with positional argument if Windows split the quoted string
|
||||
finalPrompt = positionalPrompt ? `${optionPrompt} ${positionalPrompt}` : optionPrompt;
|
||||
} else {
|
||||
// Fall back to positional argument
|
||||
finalPrompt = positionalPrompt;
|
||||
} else if (!process.stdin.isTTY) {
|
||||
// Read from stdin (piped input) - enables: echo "prompt" | ccw cli --tool gemini
|
||||
// This bypasses Windows shell multi-line argument limitations
|
||||
const { readFileSync } = await import('fs');
|
||||
try {
|
||||
finalPrompt = readFileSync(0, 'utf8').trim(); // fd 0 = stdin
|
||||
if (debug) {
|
||||
console.log(chalk.gray(` Read ${finalPrompt.length} chars from stdin`));
|
||||
}
|
||||
} catch {
|
||||
// stdin not available or empty, fall through to other methods
|
||||
}
|
||||
}
|
||||
|
||||
// If no stdin input, try --prompt/-p option or positional argument
|
||||
if (!finalPrompt) {
|
||||
if (optionPrompt) {
|
||||
// Use --prompt/-p option (preferred for multi-line)
|
||||
// Merge with positional argument if Windows split the quoted string
|
||||
finalPrompt = positionalPrompt ? `${optionPrompt} ${positionalPrompt}` : optionPrompt;
|
||||
} else {
|
||||
// Fall back to positional argument
|
||||
finalPrompt = positionalPrompt;
|
||||
}
|
||||
}
|
||||
|
||||
// Prompt is required unless resuming OR using review mode with target flags
|
||||
@@ -1257,15 +1279,16 @@ export async function cliCommand(
|
||||
default: {
|
||||
const execOptions = options as CliExecOptions;
|
||||
// Auto-exec if: has -p/--prompt, has -f/--file, has --resume, subcommand looks like a prompt,
|
||||
// or review mode with target flags (--uncommitted, --base, --commit)
|
||||
// review mode with target flags (--uncommitted, --base, --commit), or stdin has piped input
|
||||
const hasPromptOption = !!execOptions.prompt;
|
||||
const hasFileOption = !!execOptions.file;
|
||||
const hasResume = execOptions.resume !== undefined;
|
||||
const subcommandIsPrompt = subcommand && !subcommand.startsWith('-');
|
||||
const hasReviewTarget = execOptions.mode === 'review' &&
|
||||
(execOptions.uncommitted || execOptions.base || execOptions.commit);
|
||||
const hasStdinInput = !process.stdin.isTTY; // piped input detected
|
||||
|
||||
if (hasPromptOption || hasFileOption || hasResume || subcommandIsPrompt || hasReviewTarget) {
|
||||
if (hasPromptOption || hasFileOption || hasResume || subcommandIsPrompt || hasReviewTarget || hasStdinInput) {
|
||||
// Treat as exec: use subcommand as positional prompt if no -p/-f option
|
||||
let positionalPrompt = subcommandIsPrompt ? subcommand : undefined;
|
||||
|
||||
@@ -1284,6 +1307,7 @@ export async function cliCommand(
|
||||
console.log(' Usage:');
|
||||
console.log(chalk.gray(' ccw cli -f prompt.txt --tool <tool> Execute from file (recommended for multi-line)'));
|
||||
console.log(chalk.gray(' ccw cli -p "<prompt>" --tool <tool> Execute with prompt (single-line)'));
|
||||
console.log(chalk.gray(' echo "prompt" | ccw cli --tool <tool> Execute from stdin (pipe)'));
|
||||
console.log();
|
||||
console.log(' Subcommands:');
|
||||
console.log(chalk.gray(' status Check CLI tools availability'));
|
||||
|
||||
539
ccw/tests/cli-prompt-parsing.test.ts
Normal file
539
ccw/tests/cli-prompt-parsing.test.ts
Normal file
@@ -0,0 +1,539 @@
|
||||
/**
|
||||
* CLI Prompt Parsing Tests
|
||||
*
|
||||
* Tests different prompt formats and verifies the final prompt passed to CLI executor.
|
||||
* Covers: single-line, multi-line, @ symbols, special characters, template concatenation.
|
||||
*/
|
||||
|
||||
import { after, afterEach, before, describe, it, mock } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import http from 'node:http';
|
||||
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const TEST_CCW_HOME = mkdtempSync(join(tmpdir(), 'ccw-prompt-parse-'));
|
||||
process.env.CCW_DATA_DIR = TEST_CCW_HOME;
|
||||
|
||||
const cliCommandPath = new URL('../dist/commands/cli.js', import.meta.url).href;
|
||||
const cliExecutorPath = new URL('../dist/tools/cli-executor.js', import.meta.url).href;
|
||||
const historyStorePath = new URL('../dist/tools/cli-history-store.js', import.meta.url).href;
|
||||
|
||||
function stubHttpRequest(): void {
|
||||
mock.method(http, 'request', () => {
|
||||
const req = {
|
||||
on(event: string, handler: (arg?: any) => void) {
|
||||
if (event === 'socket') handler({ unref() {} });
|
||||
return req;
|
||||
},
|
||||
write() {},
|
||||
end() {},
|
||||
destroy() {},
|
||||
};
|
||||
return req as any;
|
||||
});
|
||||
}
|
||||
|
||||
function createMockExecutor(calls: any[]) {
|
||||
return async (params: any) => {
|
||||
calls.push({
|
||||
prompt: params.prompt,
|
||||
tool: params.tool,
|
||||
mode: params.mode,
|
||||
model: params.model,
|
||||
promptLength: params.prompt?.length,
|
||||
hasNewlines: params.prompt?.includes('\n'),
|
||||
lineCount: params.prompt?.split('\n').length,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
stdout: 'ok',
|
||||
stderr: '',
|
||||
execution: { id: 'EXEC-TEST', duration_ms: 1, status: 'success' },
|
||||
conversation: { turn_count: 1, total_duration_ms: 1 },
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
describe('CLI Prompt Parsing', async () => {
|
||||
let cliModule: any;
|
||||
let cliExecutorModule: any;
|
||||
let historyStoreModule: any;
|
||||
|
||||
before(async () => {
|
||||
cliModule = await import(cliCommandPath);
|
||||
cliExecutorModule = await import(cliExecutorPath);
|
||||
historyStoreModule = await import(historyStorePath);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restoreAll();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
try {
|
||||
historyStoreModule?.closeAllStores?.();
|
||||
} catch { /* ignore */ }
|
||||
rmSync(TEST_CCW_HOME, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('Single-line prompts', () => {
|
||||
it('passes simple single-line prompt via -p option', async () => {
|
||||
stubHttpRequest();
|
||||
mock.method(console, 'log', () => {});
|
||||
mock.method(console, 'error', () => {});
|
||||
mock.method(process as any, 'exit', () => {});
|
||||
|
||||
const calls: any[] = [];
|
||||
mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls));
|
||||
|
||||
await cliModule.cliCommand('exec', [], {
|
||||
prompt: 'Simple single line prompt',
|
||||
tool: 'gemini',
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
|
||||
assert.equal(calls.length, 1);
|
||||
// Prompt contains original + template rules
|
||||
assert.ok(calls[0].prompt.includes('Simple single line prompt'));
|
||||
assert.equal(calls[0].hasNewlines, true); // Templates add newlines
|
||||
console.log('\n📝 Single-line prompt final length:', calls[0].promptLength);
|
||||
});
|
||||
|
||||
it('passes prompt with quotes', async () => {
|
||||
stubHttpRequest();
|
||||
mock.method(console, 'log', () => {});
|
||||
mock.method(console, 'error', () => {});
|
||||
mock.method(process as any, 'exit', () => {});
|
||||
|
||||
const calls: any[] = [];
|
||||
mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls));
|
||||
|
||||
await cliModule.cliCommand('exec', [], {
|
||||
prompt: 'Say "Hello World" to me',
|
||||
tool: 'gemini',
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
|
||||
assert.equal(calls.length, 1);
|
||||
assert.ok(calls[0].prompt.includes('Say "Hello World" to me'));
|
||||
console.log('\n📝 Quoted prompt preserved:', calls[0].prompt.substring(0, 100));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multi-line prompts', () => {
|
||||
it('passes multi-line prompt via -p option', async () => {
|
||||
stubHttpRequest();
|
||||
mock.method(console, 'log', () => {});
|
||||
mock.method(console, 'error', () => {});
|
||||
mock.method(process as any, 'exit', () => {});
|
||||
|
||||
const calls: any[] = [];
|
||||
mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls));
|
||||
|
||||
const multiLinePrompt = `PURPOSE: Test multi-line prompt
|
||||
TASK: • Step 1 • Step 2 • Step 3
|
||||
MODE: analysis
|
||||
CONTEXT: @src/**/*
|
||||
EXPECTED: Test output`;
|
||||
|
||||
await cliModule.cliCommand('exec', [], {
|
||||
prompt: multiLinePrompt,
|
||||
tool: 'gemini',
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
|
||||
assert.equal(calls.length, 1);
|
||||
assert.ok(calls[0].prompt.includes('PURPOSE: Test multi-line prompt'));
|
||||
assert.ok(calls[0].prompt.includes('TASK: • Step 1 • Step 2 • Step 3'));
|
||||
assert.ok(calls[0].prompt.includes('CONTEXT: @src/**/*'));
|
||||
console.log('\n📝 Multi-line prompt lines:', calls[0].lineCount);
|
||||
});
|
||||
|
||||
it('reads multi-line prompt from file via -f option', async () => {
|
||||
stubHttpRequest();
|
||||
mock.method(console, 'log', () => {});
|
||||
mock.method(console, 'error', () => {});
|
||||
mock.method(process as any, 'exit', () => {});
|
||||
|
||||
const calls: any[] = [];
|
||||
mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls));
|
||||
|
||||
// Create temp prompt file
|
||||
const promptFile = join(TEST_CCW_HOME, 'test-prompt.txt');
|
||||
const fileContent = `PURPOSE: File-based prompt test
|
||||
TASK: • Read from file
|
||||
MODE: analysis
|
||||
CONTEXT: @**/*.ts | Memory: test context
|
||||
EXPECTED: Success`;
|
||||
writeFileSync(promptFile, fileContent);
|
||||
|
||||
await cliModule.cliCommand('exec', [], {
|
||||
file: promptFile,
|
||||
tool: 'gemini',
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
|
||||
assert.equal(calls.length, 1);
|
||||
assert.ok(calls[0].prompt.includes('PURPOSE: File-based prompt test'));
|
||||
assert.ok(calls[0].prompt.includes('Memory: test context'));
|
||||
console.log('\n📝 File prompt loaded, lines:', calls[0].lineCount);
|
||||
});
|
||||
});
|
||||
|
||||
describe('@ symbol patterns', () => {
|
||||
it('preserves @ patterns in CONTEXT field', async () => {
|
||||
stubHttpRequest();
|
||||
mock.method(console, 'log', () => {});
|
||||
mock.method(console, 'error', () => {});
|
||||
mock.method(process as any, 'exit', () => {});
|
||||
|
||||
const calls: any[] = [];
|
||||
mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls));
|
||||
|
||||
const promptWithPatterns = `PURPOSE: Test @ patterns
|
||||
CONTEXT: @src/auth/**/*.ts @src/middleware/*.ts @shared/utils/security.ts
|
||||
EXPECTED: Pattern preservation`;
|
||||
|
||||
await cliModule.cliCommand('exec', [], {
|
||||
prompt: promptWithPatterns,
|
||||
tool: 'gemini',
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
|
||||
assert.equal(calls.length, 1);
|
||||
assert.ok(calls[0].prompt.includes('@src/auth/**/*.ts'));
|
||||
assert.ok(calls[0].prompt.includes('@src/middleware/*.ts'));
|
||||
assert.ok(calls[0].prompt.includes('@shared/utils/security.ts'));
|
||||
console.log('\n📝 @ patterns preserved in final prompt');
|
||||
});
|
||||
|
||||
it('preserves @ patterns with glob wildcards', async () => {
|
||||
stubHttpRequest();
|
||||
mock.method(console, 'log', () => {});
|
||||
mock.method(console, 'error', () => {});
|
||||
mock.method(process as any, 'exit', () => {});
|
||||
|
||||
const calls: any[] = [];
|
||||
mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls));
|
||||
|
||||
await cliModule.cliCommand('exec', [], {
|
||||
prompt: 'CONTEXT: @**/*.{ts,tsx} @!node_modules/** @!dist/**',
|
||||
tool: 'gemini',
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
|
||||
assert.equal(calls.length, 1);
|
||||
assert.ok(calls[0].prompt.includes('@**/*.{ts,tsx}'));
|
||||
assert.ok(calls[0].prompt.includes('@!node_modules/**'));
|
||||
console.log('\n📝 Complex glob patterns preserved');
|
||||
});
|
||||
|
||||
it('preserves @ patterns with Memory section', async () => {
|
||||
stubHttpRequest();
|
||||
mock.method(console, 'log', () => {});
|
||||
mock.method(console, 'error', () => {});
|
||||
mock.method(process as any, 'exit', () => {});
|
||||
|
||||
const calls: any[] = [];
|
||||
mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls));
|
||||
|
||||
await cliModule.cliCommand('exec', [], {
|
||||
prompt: 'CONTEXT: @src/**/* | Memory: Using bcrypt for passwords, JWT for sessions',
|
||||
tool: 'gemini',
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
|
||||
assert.equal(calls.length, 1);
|
||||
assert.ok(calls[0].prompt.includes('@src/**/*'));
|
||||
assert.ok(calls[0].prompt.includes('Memory: Using bcrypt for passwords'));
|
||||
console.log('\n📝 @ pattern + Memory preserved');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Special characters', () => {
|
||||
it('preserves bullet points and special characters', async () => {
|
||||
stubHttpRequest();
|
||||
mock.method(console, 'log', () => {});
|
||||
mock.method(console, 'error', () => {});
|
||||
mock.method(process as any, 'exit', () => {});
|
||||
|
||||
const calls: any[] = [];
|
||||
mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls));
|
||||
|
||||
await cliModule.cliCommand('exec', [], {
|
||||
prompt: 'TASK: • First item • Second item • Third item ✓ ✗',
|
||||
tool: 'gemini',
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
|
||||
assert.equal(calls.length, 1);
|
||||
assert.ok(calls[0].prompt.includes('• First item'));
|
||||
assert.ok(calls[0].prompt.includes('✓'));
|
||||
console.log('\n📝 Special characters preserved');
|
||||
});
|
||||
|
||||
it('preserves code-like content', async () => {
|
||||
stubHttpRequest();
|
||||
mock.method(console, 'log', () => {});
|
||||
mock.method(console, 'error', () => {});
|
||||
mock.method(process as any, 'exit', () => {});
|
||||
|
||||
const calls: any[] = [];
|
||||
mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls));
|
||||
|
||||
await cliModule.cliCommand('exec', [], {
|
||||
prompt: 'Fix: const x = arr.filter(i => i > 0).map(i => i * 2);',
|
||||
tool: 'gemini',
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
|
||||
assert.equal(calls.length, 1);
|
||||
assert.ok(calls[0].prompt.includes('const x = arr.filter'));
|
||||
assert.ok(calls[0].prompt.includes('=>'));
|
||||
console.log('\n📝 Code-like content preserved');
|
||||
});
|
||||
|
||||
it('preserves shell-like patterns', async () => {
|
||||
stubHttpRequest();
|
||||
mock.method(console, 'log', () => {});
|
||||
mock.method(console, 'error', () => {});
|
||||
mock.method(process as any, 'exit', () => {});
|
||||
|
||||
const calls: any[] = [];
|
||||
mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls));
|
||||
|
||||
await cliModule.cliCommand('exec', [], {
|
||||
prompt: 'Run: npm run build && npm test | grep "passed"',
|
||||
tool: 'gemini',
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
|
||||
assert.equal(calls.length, 1);
|
||||
assert.ok(calls[0].prompt.includes('npm run build && npm test'));
|
||||
assert.ok(calls[0].prompt.includes('| grep'));
|
||||
console.log('\n📝 Shell-like patterns preserved');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Template concatenation', () => {
|
||||
it('concatenates system rules and roles to prompt', async () => {
|
||||
stubHttpRequest();
|
||||
mock.method(console, 'log', () => {});
|
||||
mock.method(console, 'error', () => {});
|
||||
mock.method(process as any, 'exit', () => {});
|
||||
|
||||
const calls: any[] = [];
|
||||
mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls));
|
||||
|
||||
await cliModule.cliCommand('exec', [], {
|
||||
prompt: 'Test prompt',
|
||||
tool: 'gemini',
|
||||
rule: 'universal-rigorous-style',
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
|
||||
assert.equal(calls.length, 1);
|
||||
// Should have SYSTEM RULES and ROLES sections
|
||||
assert.ok(calls[0].prompt.includes('Test prompt'));
|
||||
assert.ok(calls[0].prompt.includes('=== SYSTEM RULES ===') || calls[0].promptLength > 100);
|
||||
console.log('\n📝 Template concatenated, total length:', calls[0].promptLength);
|
||||
});
|
||||
|
||||
it('preserves user prompt structure with templates', async () => {
|
||||
stubHttpRequest();
|
||||
mock.method(console, 'log', () => {});
|
||||
mock.method(console, 'error', () => {});
|
||||
mock.method(process as any, 'exit', () => {});
|
||||
|
||||
const calls: any[] = [];
|
||||
mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls));
|
||||
|
||||
const structuredPrompt = `PURPOSE: Security audit
|
||||
TASK: • Scan injection flaws • Check auth bypass
|
||||
MODE: analysis
|
||||
CONTEXT: @src/auth/**/*
|
||||
EXPECTED: Security report
|
||||
CONSTRAINTS: Focus on auth`;
|
||||
|
||||
await cliModule.cliCommand('exec', [], {
|
||||
prompt: structuredPrompt,
|
||||
tool: 'gemini',
|
||||
mode: 'analysis',
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
|
||||
assert.equal(calls.length, 1);
|
||||
// User prompt should come first
|
||||
const promptStart = calls[0].prompt.indexOf('PURPOSE: Security audit');
|
||||
const rulesStart = calls[0].prompt.indexOf('=== SYSTEM RULES ===');
|
||||
if (rulesStart > 0) {
|
||||
assert.ok(promptStart < rulesStart, 'User prompt should precede system rules');
|
||||
}
|
||||
console.log('\n📝 User prompt preserved at start');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('handles empty lines in prompt', async () => {
|
||||
stubHttpRequest();
|
||||
mock.method(console, 'log', () => {});
|
||||
mock.method(console, 'error', () => {});
|
||||
mock.method(process as any, 'exit', () => {});
|
||||
|
||||
const calls: any[] = [];
|
||||
mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls));
|
||||
|
||||
await cliModule.cliCommand('exec', [], {
|
||||
prompt: 'Line 1\n\nLine 3\n\n\nLine 6',
|
||||
tool: 'gemini',
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
|
||||
assert.equal(calls.length, 1);
|
||||
assert.ok(calls[0].prompt.includes('Line 1'));
|
||||
assert.ok(calls[0].prompt.includes('Line 3'));
|
||||
assert.ok(calls[0].prompt.includes('Line 6'));
|
||||
console.log('\n📝 Empty lines handled');
|
||||
});
|
||||
|
||||
it('handles very long single-line prompt', async () => {
|
||||
stubHttpRequest();
|
||||
mock.method(console, 'log', () => {});
|
||||
mock.method(console, 'error', () => {});
|
||||
mock.method(process as any, 'exit', () => {});
|
||||
|
||||
const calls: any[] = [];
|
||||
mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls));
|
||||
|
||||
const longPrompt = 'A'.repeat(10000);
|
||||
await cliModule.cliCommand('exec', [], {
|
||||
prompt: longPrompt,
|
||||
tool: 'gemini',
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
|
||||
assert.equal(calls.length, 1);
|
||||
assert.ok(calls[0].prompt.includes('A'.repeat(100)));
|
||||
console.log('\n📝 Long prompt handled, length:', calls[0].promptLength);
|
||||
});
|
||||
|
||||
it('handles Unicode characters', async () => {
|
||||
stubHttpRequest();
|
||||
mock.method(console, 'log', () => {});
|
||||
mock.method(console, 'error', () => {});
|
||||
mock.method(process as any, 'exit', () => {});
|
||||
|
||||
const calls: any[] = [];
|
||||
mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls));
|
||||
|
||||
await cliModule.cliCommand('exec', [], {
|
||||
prompt: '中文测试 日本語テスト 한국어테스트 🚀🔥💡',
|
||||
tool: 'gemini',
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
|
||||
assert.equal(calls.length, 1);
|
||||
assert.ok(calls[0].prompt.includes('中文测试'));
|
||||
assert.ok(calls[0].prompt.includes('日本語テスト'));
|
||||
assert.ok(calls[0].prompt.includes('🚀'));
|
||||
console.log('\n📝 Unicode preserved');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Full template prompt', () => {
|
||||
it('preserves complete CLI tools usage template format', async () => {
|
||||
stubHttpRequest();
|
||||
mock.method(console, 'log', () => {});
|
||||
mock.method(console, 'error', () => {});
|
||||
mock.method(process as any, 'exit', () => {});
|
||||
|
||||
const calls: any[] = [];
|
||||
mock.method(cliExecutorModule.cliExecutorTool, 'execute', createMockExecutor(calls));
|
||||
|
||||
const fullTemplate = `PURPOSE: Identify OWASP Top 10 vulnerabilities in authentication module to pass security audit; success = all critical/high issues documented with remediation
|
||||
TASK: • Scan for injection flaws (SQL, command, LDAP) • Check authentication bypass vectors • Evaluate session management • Assess sensitive data exposure
|
||||
MODE: analysis
|
||||
CONTEXT: @src/auth/**/* @src/middleware/auth.ts | Memory: Using bcrypt for passwords, JWT for sessions
|
||||
EXPECTED: Security report with: severity matrix, file:line references, CVE mappings where applicable, remediation code snippets prioritized by risk
|
||||
CONSTRAINTS: Focus on authentication | Ignore test files`;
|
||||
|
||||
await cliModule.cliCommand('exec', [], {
|
||||
prompt: fullTemplate,
|
||||
tool: 'gemini',
|
||||
mode: 'analysis',
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
|
||||
assert.equal(calls.length, 1);
|
||||
// Verify all sections preserved
|
||||
assert.ok(calls[0].prompt.includes('PURPOSE:'));
|
||||
assert.ok(calls[0].prompt.includes('TASK:'));
|
||||
assert.ok(calls[0].prompt.includes('MODE:'));
|
||||
assert.ok(calls[0].prompt.includes('CONTEXT:'));
|
||||
assert.ok(calls[0].prompt.includes('EXPECTED:'));
|
||||
assert.ok(calls[0].prompt.includes('CONSTRAINTS:'));
|
||||
// Verify specific content
|
||||
assert.ok(calls[0].prompt.includes('OWASP Top 10'));
|
||||
assert.ok(calls[0].prompt.includes('@src/auth/**/*'));
|
||||
assert.ok(calls[0].prompt.includes('Memory: Using bcrypt'));
|
||||
assert.ok(calls[0].prompt.includes('severity matrix'));
|
||||
|
||||
console.log('\n📝 Full template format preserved');
|
||||
console.log(' Final prompt length:', calls[0].promptLength);
|
||||
console.log(' Line count:', calls[0].lineCount);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Prompt Output Visualization', () => {
|
||||
let cliModule: any;
|
||||
let cliExecutorModule: any;
|
||||
|
||||
before(async () => {
|
||||
cliModule = await import(cliCommandPath);
|
||||
cliExecutorModule = await import(cliExecutorPath);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restoreAll();
|
||||
});
|
||||
|
||||
it('demonstrates final prompt structure (visual output)', async () => {
|
||||
stubHttpRequest();
|
||||
mock.method(console, 'log', () => {});
|
||||
mock.method(console, 'error', () => {});
|
||||
mock.method(process as any, 'exit', () => {});
|
||||
|
||||
const capturedPrompts: string[] = [];
|
||||
mock.method(cliExecutorModule.cliExecutorTool, 'execute', async (params: any) => {
|
||||
capturedPrompts.push(params.prompt);
|
||||
return {
|
||||
success: true,
|
||||
stdout: 'ok',
|
||||
stderr: '',
|
||||
execution: { id: 'EXEC-VIS', duration_ms: 1, status: 'success' },
|
||||
conversation: { turn_count: 1, total_duration_ms: 1 },
|
||||
};
|
||||
});
|
||||
|
||||
await cliModule.cliCommand('exec', [], {
|
||||
prompt: 'Test visualization prompt',
|
||||
tool: 'gemini',
|
||||
});
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
|
||||
// Output visualization
|
||||
console.log('\n' + '='.repeat(60));
|
||||
console.log('📋 FINAL PROMPT PASSED TO CLI:');
|
||||
console.log('='.repeat(60));
|
||||
if (capturedPrompts[0]) {
|
||||
const preview = capturedPrompts[0].substring(0, 500);
|
||||
console.log(preview);
|
||||
if (capturedPrompts[0].length > 500) {
|
||||
console.log(`\n... [${capturedPrompts[0].length - 500} more characters]`);
|
||||
}
|
||||
}
|
||||
console.log('='.repeat(60));
|
||||
});
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
{"root":["./src/cli.ts","./src/index.ts","./src/commands/cli.ts","./src/commands/core-memory.ts","./src/commands/hook.ts","./src/commands/install.ts","./src/commands/issue.ts","./src/commands/list.ts","./src/commands/memory.ts","./src/commands/serve.ts","./src/commands/session-path-resolver.ts","./src/commands/session.ts","./src/commands/stop.ts","./src/commands/tool.ts","./src/commands/uninstall.ts","./src/commands/upgrade.ts","./src/commands/view.ts","./src/config/litellm-api-config-manager.ts","./src/config/provider-models.ts","./src/config/storage-paths.ts","./src/core/cache-manager.ts","./src/core/claude-freshness.ts","./src/core/core-memory-store.ts","./src/core/dashboard-generator-patch.ts","./src/core/dashboard-generator.ts","./src/core/data-aggregator.ts","./src/core/history-importer.ts","./src/core/lite-scanner-complete.ts","./src/core/lite-scanner.ts","./src/core/manifest.ts","./src/core/memory-embedder-bridge.ts","./src/core/memory-store.ts","./src/core/server.ts","./src/core/session-clustering-service.ts","./src/core/session-scanner.ts","./src/core/websocket.ts","./src/core/routes/ccw-routes.ts","./src/core/routes/claude-routes.ts","./src/core/routes/cli-routes.ts","./src/core/routes/codexlens-routes.ts","./src/core/routes/core-memory-routes.ts","./src/core/routes/discovery-routes.ts","./src/core/routes/files-routes.ts","./src/core/routes/graph-routes.ts","./src/core/routes/help-routes.ts","./src/core/routes/hooks-routes.ts","./src/core/routes/issue-routes.ts","./src/core/routes/litellm-api-routes.ts","./src/core/routes/litellm-routes.ts","./src/core/routes/mcp-routes.ts","./src/core/routes/mcp-templates-db.ts","./src/core/routes/memory-routes.ts","./src/core/routes/nav-status-routes.ts","./src/core/routes/rules-routes.ts","./src/core/routes/session-routes.ts","./src/core/routes/skills-routes.ts","./src/core/routes/status-routes.ts","./src/core/routes/system-routes.ts","./src/mcp-server/index.ts","./src/tools/classify-folders.ts","./src/tools/claude-cli-tools.ts","./src/tools/cli-config-manager.ts","./src/tools/cli-executor.ts","./src/tools/cli-history-store.ts","./src/tools/codex-lens.ts","./src/tools/context-cache-store.ts","./src/tools/context-cache.ts","./src/tools/convert-tokens-to-css.ts","./src/tools/core-memory.ts","./src/tools/detect-changed-modules.ts","./src/tools/discover-design-files.ts","./src/tools/edit-file.ts","./src/tools/generate-module-docs.ts","./src/tools/get-modules-by-depth.ts","./src/tools/index.ts","./src/tools/litellm-client.ts","./src/tools/litellm-executor.ts","./src/tools/native-session-discovery.ts","./src/tools/notifier.ts","./src/tools/pattern-parser.ts","./src/tools/read-file.ts","./src/tools/resume-strategy.ts","./src/tools/session-content-parser.ts","./src/tools/session-manager.ts","./src/tools/smart-context.ts","./src/tools/smart-search.ts","./src/tools/storage-manager.ts","./src/tools/ui-generate-preview.js","./src/tools/ui-instantiate-prototypes.js","./src/tools/update-module-claude.js","./src/tools/write-file.ts","./src/types/config.ts","./src/types/index.ts","./src/types/litellm-api-config.ts","./src/types/session.ts","./src/types/tool.ts","./src/utils/browser-launcher.ts","./src/utils/file-utils.ts","./src/utils/path-resolver.ts","./src/utils/path-validator.ts","./src/utils/python-utils.ts","./src/utils/ui.ts"],"version":"5.9.3"}
|
||||
Reference in New Issue
Block a user