Files
Claude-Code-Workflow/ccw/tests/integration/cli-executor/tool-stub.js
catlog22 99eeeff6f7 test(cli-executor): add qwen/codex and multi-tool workflow tests
Solution-ID: SOL-1735410003

Issue-ID: ISS-1766921318981-23

Task-ID: T3
2025-12-29 17:36:50 +08:00

175 lines
5.1 KiB
JavaScript

#!/usr/bin/env node
import { mkdirSync, writeFileSync } from 'node:fs';
import { dirname, resolve as resolvePath } from 'node:path';
import { globSync } from 'glob';
function parseEnhancedPrompt(prompt) {
const fields = ['PURPOSE', 'TASK', 'MODE', 'CONTEXT', 'EXPECTED', 'RULES'];
const out = {};
for (const field of fields) {
const line = prompt
.split(/\r?\n/)
.find((l) => l.startsWith(`${field}:`));
out[field.toLowerCase()] = line ? line.slice(field.length + 1).trim() : null;
}
return out;
}
function parseDirectives(prompt) {
const lines = prompt.split(/\r?\n/);
const directiveLine = lines.find((l) => l.startsWith('CCW_TEST_DIRECTIVES:'));
if (!directiveLine) return null;
const json = directiveLine.slice('CCW_TEST_DIRECTIVES:'.length).trim();
if (!json) return null;
try {
return JSON.parse(json);
} catch {
return null;
}
}
function parseIncludeDirs(tool, args) {
if (tool === 'gemini' || tool === 'qwen') {
const idx = args.indexOf('--include-directories');
if (idx >= 0 && typeof args[idx + 1] === 'string') {
return String(args[idx + 1])
.split(',')
.map((d) => d.trim())
.filter(Boolean);
}
return [];
}
if (tool === 'codex') {
const dirs = [];
for (let i = 0; i < args.length; i++) {
if (args[i] === '--add-dir' && typeof args[i + 1] === 'string') {
dirs.push(String(args[i + 1]));
i++;
}
}
return dirs;
}
return [];
}
function extractAtPatterns(prompt) {
const patterns = [];
const re = /(^|\s)@([^\s]+)/g;
let match;
while ((match = re.exec(prompt)) !== null) {
const raw = match[2] || '';
const cleaned = raw.replace(/[),;"']+$/g, '');
if (cleaned) patterns.push(cleaned);
}
return patterns;
}
function normalizeSlash(value) {
return String(value).replace(/\\/g, '/');
}
function isOutsideCwdPattern(pattern) {
const p = normalizeSlash(pattern);
return p.startsWith('../') || p.startsWith('..\\');
}
function isAllowedOutsidePattern(pattern, includeDirs) {
const p = normalizeSlash(pattern);
const include = includeDirs.map((d) => normalizeSlash(d));
return include.some((dir) => p === dir || p.startsWith(`${dir}/`));
}
function resolvePatterns(prompt, tool, args) {
const includeDirs = parseIncludeDirs(tool, args);
const patterns = extractAtPatterns(prompt);
const files = new Set();
for (const pattern of patterns) {
if (isOutsideCwdPattern(pattern) && !isAllowedOutsidePattern(pattern, includeDirs)) {
continue;
}
const matches = globSync(pattern, {
cwd: process.cwd(),
nodir: true,
dot: true,
windowsPathsNoEscape: true,
});
for (const m of matches) files.add(normalizeSlash(m));
}
return Array.from(files).sort();
}
function safeWriteFiles(writeFiles) {
const wrote = [];
for (const [rel, content] of Object.entries(writeFiles || {})) {
const abs = resolvePath(process.cwd(), String(rel));
if (!abs.startsWith(resolvePath(process.cwd()))) continue;
mkdirSync(dirname(abs), { recursive: true });
writeFileSync(abs, String(content ?? ''), 'utf8');
wrote.push(String(rel));
}
return wrote.sort();
}
async function readStdin() {
if (process.stdin.isTTY) return '';
return new Promise((resolve) => {
let buf = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
buf += chunk;
});
process.stdin.on('end', () => resolve(buf));
process.stdin.on('error', () => resolve(buf));
});
}
async function main() {
const tool = String(process.argv[2] || 'unknown');
const args = process.argv.slice(3).map(String);
const prompt = await readStdin();
const baseDirectives = parseDirectives(prompt) || {};
const overrides =
baseDirectives.tool_overrides &&
typeof baseDirectives.tool_overrides === 'object' &&
baseDirectives.tool_overrides[tool] &&
typeof baseDirectives.tool_overrides[tool] === 'object'
? baseDirectives.tool_overrides[tool]
: {};
const directives = { ...baseDirectives, ...overrides };
const resolvedFiles = directives.resolve_patterns ? resolvePatterns(prompt, tool, args) : [];
const wroteFiles = directives.write_files ? safeWriteFiles(directives.write_files) : [];
const payload = {
tool,
cwd: normalizeSlash(process.cwd()),
args,
prompt,
parsed: parseEnhancedPrompt(prompt),
resolved_files: resolvedFiles,
wrote_files: wroteFiles,
};
const stdoutText =
typeof directives.stdout === 'string' ? directives.stdout : `${JSON.stringify(payload)}\n`;
const stderrText = typeof directives.stderr === 'string' ? directives.stderr : '';
const exitCode = Number.isFinite(Number(directives.exit_code)) ? Number(directives.exit_code) : 0;
const sleepMs = Number.isFinite(Number(directives.sleep_ms)) ? Number(directives.sleep_ms) : 0;
if (sleepMs > 0) {
await new Promise((r) => setTimeout(r, sleepMs));
}
process.stdout.write(stdoutText);
if (stderrText) process.stderr.write(stderrText);
process.exit(exitCode);
}
main().catch((err) => {
process.stderr.write(String(err?.stack || err?.message || err));
process.exit(1);
});