mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-11 17:21:03 +08:00
Add comprehensive tests for CLI functionality and CodexLens compatibility
- Introduced tests for stale running fallback in CLI watch functionality to ensure proper handling of saved conversations. - Added compatibility tests for CodexLens CLI to verify index initialization despite compatibility conflicts. - Implemented tests for Smart Search MCP usage to validate default settings and path handling. - Created tests for UV Manager to ensure Python preference handling works as expected. - Added a detailed guide for CCW/Codex commands and skills, covering core commands, execution modes, and templates.
This commit is contained in:
162
ccw/tests/cli-output-command-final.test.js
Normal file
162
ccw/tests/cli-output-command-final.test.js
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* ccw cli output --final regression tests
|
||||
*
|
||||
* Verifies strict final-result behavior for cached executions.
|
||||
*/
|
||||
|
||||
import { after, afterEach, before, describe, it, mock } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const TEST_CCW_HOME = mkdtempSync(join(tmpdir(), 'ccw-cli-output-final-home-'));
|
||||
|
||||
const cliCommandPath = new URL('../dist/commands/cli.js', import.meta.url).href;
|
||||
const historyStorePath = new URL('../dist/tools/cli-history-store.js', import.meta.url).href;
|
||||
|
||||
function createTestProjectRoot() {
|
||||
const dirPath = mkdtempSync(join(tmpdir(), 'ccw-cli-output-final-project-'));
|
||||
if (!existsSync(dirPath)) {
|
||||
mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
return dirPath;
|
||||
}
|
||||
|
||||
function createConversation({ id, stdoutFull = '', parsedOutput, finalOutput }) {
|
||||
return {
|
||||
id,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
tool: 'codex',
|
||||
model: 'default',
|
||||
mode: 'analysis',
|
||||
category: 'user',
|
||||
total_duration_ms: 100,
|
||||
turn_count: 1,
|
||||
latest_status: 'success',
|
||||
turns: [
|
||||
{
|
||||
turn: 1,
|
||||
timestamp: new Date().toISOString(),
|
||||
prompt: 'test prompt',
|
||||
duration_ms: 100,
|
||||
status: 'success',
|
||||
exit_code: 0,
|
||||
output: {
|
||||
stdout: stdoutFull,
|
||||
stderr: '',
|
||||
truncated: false,
|
||||
cached: true,
|
||||
stdout_full: stdoutFull,
|
||||
stderr_full: '',
|
||||
parsed_output: parsedOutput,
|
||||
final_output: finalOutput,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
describe('ccw cli output --final', async () => {
|
||||
let cliModule;
|
||||
let historyStoreModule;
|
||||
|
||||
before(async () => {
|
||||
process.env.CCW_DATA_DIR = TEST_CCW_HOME;
|
||||
cliModule = await import(cliCommandPath);
|
||||
historyStoreModule = await import(historyStorePath);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restoreAll();
|
||||
try {
|
||||
historyStoreModule?.closeAllStores?.();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
|
||||
after(() => {
|
||||
try {
|
||||
historyStoreModule?.closeAllStores?.();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
rmSync(TEST_CCW_HOME, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('reconstructs the final agent message from raw JSONL when final_output is missing', async () => {
|
||||
const projectRoot = createTestProjectRoot();
|
||||
const store = new historyStoreModule.CliHistoryStore(projectRoot);
|
||||
const stdoutFull = [
|
||||
JSON.stringify({ type: 'thread.started', thread_id: 'THREAD-1' }),
|
||||
JSON.stringify({ type: 'turn.started' }),
|
||||
JSON.stringify({ type: 'item.completed', item: { id: 'item_0', type: 'agent_message', text: 'Running `pwd` now.' } }),
|
||||
JSON.stringify({ type: 'item.completed', item: { id: 'item_1', type: 'command_execution', command: 'pwd', aggregated_output: 'D:\\Claude_dms3\\ccw', exit_code: 0, status: 'completed' } }),
|
||||
JSON.stringify({ type: 'item.completed', item: { id: 'item_2', type: 'agent_message', text: 'Waiting for the command output, then I’ll return it verbatim.' } }),
|
||||
JSON.stringify({ type: 'item.completed', item: { id: 'item_3', type: 'agent_message', text: 'D:\\Claude_dms3\\ccw' } }),
|
||||
JSON.stringify({ type: 'turn.completed', usage: { input_tokens: 1, output_tokens: 1 } }),
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
try {
|
||||
store.saveConversation(createConversation({
|
||||
id: 'EXEC-RECONSTRUCT-FINAL',
|
||||
stdoutFull,
|
||||
parsedOutput: 'Running `pwd` now.\nWaiting for the command output, then I’ll return it verbatim.\nD:\\Claude_dms3\\ccw',
|
||||
finalOutput: undefined,
|
||||
}));
|
||||
|
||||
const logs = [];
|
||||
mock.method(console, 'log', (...args) => {
|
||||
logs.push(args.map(String).join(' '));
|
||||
});
|
||||
mock.method(console, 'error', () => {});
|
||||
|
||||
await cliModule.cliCommand('output', ['EXEC-RECONSTRUCT-FINAL'], { final: true, project: projectRoot });
|
||||
|
||||
assert.deepEqual(logs, ['D:\\Claude_dms3\\ccw']);
|
||||
} finally {
|
||||
store.close();
|
||||
rmSync(projectRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('fails fast for explicit --final when no final agent result can be recovered', async () => {
|
||||
const projectRoot = createTestProjectRoot();
|
||||
const store = new historyStoreModule.CliHistoryStore(projectRoot);
|
||||
|
||||
try {
|
||||
store.saveConversation(createConversation({
|
||||
id: 'EXEC-NO-FINAL',
|
||||
stdoutFull: 'plain stdout without JSONL final message',
|
||||
parsedOutput: 'INTERMEDIATE_STATUS_LINE',
|
||||
finalOutput: undefined,
|
||||
}));
|
||||
|
||||
const logs = [];
|
||||
const errors = [];
|
||||
const exitCodes = [];
|
||||
|
||||
mock.method(console, 'log', (...args) => {
|
||||
logs.push(args.map(String).join(' '));
|
||||
});
|
||||
mock.method(console, 'error', (...args) => {
|
||||
errors.push(args.map(String).join(' '));
|
||||
});
|
||||
mock.method(process, 'exit', (code) => {
|
||||
exitCodes.push(code);
|
||||
});
|
||||
|
||||
await cliModule.cliCommand('output', ['EXEC-NO-FINAL'], { final: true, project: projectRoot });
|
||||
|
||||
assert.deepEqual(logs, []);
|
||||
assert.deepEqual(exitCodes, [1]);
|
||||
assert.ok(errors.some((line) => line.includes('No final agent result found in cached output.')));
|
||||
} finally {
|
||||
store.close();
|
||||
rmSync(projectRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
263
ccw/tests/cli-show-running-time.test.js
Normal file
263
ccw/tests/cli-show-running-time.test.js
Normal file
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* ccw cli show - running execution time formatting tests
|
||||
*/
|
||||
|
||||
import { after, afterEach, before, describe, it, mock } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import http from 'node:http';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const TEST_CCW_HOME = mkdtempSync(join(tmpdir(), 'ccw-cli-show-time-home-'));
|
||||
process.env.CCW_DATA_DIR = TEST_CCW_HOME;
|
||||
|
||||
const cliCommandPath = new URL('../dist/commands/cli.js', import.meta.url).href;
|
||||
const historyStorePath = new URL('../dist/tools/cli-history-store.js', import.meta.url).href;
|
||||
|
||||
function createConversationRecord({ id, prompt, updatedAt, durationMs = 2000 }) {
|
||||
return {
|
||||
id,
|
||||
created_at: updatedAt,
|
||||
updated_at: updatedAt,
|
||||
tool: 'codex',
|
||||
model: 'default',
|
||||
mode: 'analysis',
|
||||
category: 'user',
|
||||
total_duration_ms: durationMs,
|
||||
turn_count: 1,
|
||||
latest_status: 'success',
|
||||
turns: [
|
||||
{
|
||||
turn: 1,
|
||||
timestamp: updatedAt,
|
||||
prompt,
|
||||
duration_ms: durationMs,
|
||||
status: 'success',
|
||||
exit_code: 0,
|
||||
output: {
|
||||
stdout: 'saved output',
|
||||
stderr: '',
|
||||
truncated: false,
|
||||
cached: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function stubActiveExecutionsResponse(executions) {
|
||||
mock.method(http, 'request', (_options, callback) => {
|
||||
const payload = JSON.stringify({ executions });
|
||||
const res = {
|
||||
on(event, handler) {
|
||||
if (event === 'data') {
|
||||
handler(Buffer.from(payload, 'utf8'));
|
||||
}
|
||||
if (event === 'end') {
|
||||
handler();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
if (callback) {
|
||||
callback(res);
|
||||
}
|
||||
|
||||
const req = {
|
||||
on() { return req; },
|
||||
write() {},
|
||||
end() {},
|
||||
destroy() {},
|
||||
};
|
||||
return req;
|
||||
});
|
||||
}
|
||||
|
||||
describe('ccw cli show running time formatting', async () => {
|
||||
let cliModule;
|
||||
let historyStoreModule;
|
||||
|
||||
before(async () => {
|
||||
cliModule = await import(cliCommandPath);
|
||||
historyStoreModule = await import(historyStorePath);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restoreAll();
|
||||
try {
|
||||
historyStoreModule?.closeAllStores?.();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
|
||||
after(() => {
|
||||
try {
|
||||
historyStoreModule?.closeAllStores?.();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
rmSync(TEST_CCW_HOME, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('formats running time with the same humanized style as history rows', async () => {
|
||||
const now = 1_741_392_000_000;
|
||||
stubActiveExecutionsResponse([
|
||||
{
|
||||
id: 'EXEC-RUN-125S',
|
||||
tool: 'codex',
|
||||
mode: 'analysis',
|
||||
status: 'running',
|
||||
prompt: 'long task',
|
||||
startTime: now - 125_000,
|
||||
output: ''
|
||||
}
|
||||
]);
|
||||
|
||||
mock.method(Date, 'now', () => now);
|
||||
|
||||
const logs = [];
|
||||
mock.method(console, 'log', (...args) => {
|
||||
logs.push(args.map(String).join(' '));
|
||||
});
|
||||
mock.method(console, 'error', () => {});
|
||||
|
||||
await cliModule.cliCommand('show', [], {});
|
||||
|
||||
const rendered = logs.join('\n');
|
||||
assert.match(rendered, /2m ago/);
|
||||
assert.match(rendered, /2m 5s\.\.\./);
|
||||
assert.doesNotMatch(rendered, /125s ago/);
|
||||
});
|
||||
|
||||
it('normalizes second-based string timestamps for running executions', async () => {
|
||||
const now = 1_741_392_000_000;
|
||||
const startTimeSeconds = String(Math.floor((now - 3_600_000) / 1000));
|
||||
|
||||
stubActiveExecutionsResponse([
|
||||
{
|
||||
id: 'EXEC-RUN-1H',
|
||||
tool: 'gemini',
|
||||
mode: 'write',
|
||||
status: 'running',
|
||||
prompt: 'hour task',
|
||||
startTime: startTimeSeconds,
|
||||
output: ''
|
||||
}
|
||||
]);
|
||||
|
||||
mock.method(Date, 'now', () => now);
|
||||
|
||||
const logs = [];
|
||||
mock.method(console, 'log', (...args) => {
|
||||
logs.push(args.map(String).join(' '));
|
||||
});
|
||||
mock.method(console, 'error', () => {});
|
||||
|
||||
await cliModule.cliCommand('show', [], {});
|
||||
|
||||
const rendered = logs.join('\n');
|
||||
assert.match(rendered, /1h ago/);
|
||||
assert.match(rendered, /1h\.\.\./);
|
||||
});
|
||||
|
||||
it('suppresses stale running rows when saved history is newer than the active start time', async () => {
|
||||
const projectRoot = mkdtempSync(join(tmpdir(), 'ccw-cli-show-stale-project-'));
|
||||
const previousCwd = process.cwd();
|
||||
const now = 1_741_392_000_000;
|
||||
const savedUpdatedAt = new Date(now - 5_000).toISOString();
|
||||
|
||||
try {
|
||||
process.chdir(projectRoot);
|
||||
const store = new historyStoreModule.CliHistoryStore(projectRoot);
|
||||
store.saveConversation(createConversationRecord({
|
||||
id: 'EXEC-STALE-RUNNING',
|
||||
prompt: 'HISTORY PROMPT SHOULD WIN',
|
||||
updatedAt: savedUpdatedAt,
|
||||
durationMs: 2300,
|
||||
}));
|
||||
store.close();
|
||||
|
||||
stubActiveExecutionsResponse([
|
||||
{
|
||||
id: 'EXEC-STALE-RUNNING',
|
||||
tool: 'codex',
|
||||
mode: 'analysis',
|
||||
status: 'running',
|
||||
prompt: 'ACTIVE PROMPT SHOULD BE HIDDEN',
|
||||
startTime: now - 60_000,
|
||||
output: ''
|
||||
}
|
||||
]);
|
||||
|
||||
mock.method(Date, 'now', () => now);
|
||||
|
||||
const logs = [];
|
||||
mock.method(console, 'log', (...args) => {
|
||||
logs.push(args.map(String).join(' '));
|
||||
});
|
||||
mock.method(console, 'error', () => {});
|
||||
|
||||
await cliModule.cliCommand('show', [], {});
|
||||
|
||||
const rendered = logs.join('\n');
|
||||
assert.match(rendered, /HISTORY PROMPT SHOULD WIN/);
|
||||
assert.doesNotMatch(rendered, /ACTIVE PROMPT SHOULD BE HIDDEN/);
|
||||
assert.match(rendered, /2\.3s/);
|
||||
} finally {
|
||||
process.chdir(previousCwd);
|
||||
rmSync(projectRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps active running rows when saved history is older than the active start time \(resume-safe\)', async () => {
|
||||
const projectRoot = mkdtempSync(join(tmpdir(), 'ccw-cli-show-resume-project-'));
|
||||
const previousCwd = process.cwd();
|
||||
const now = 1_741_392_000_000;
|
||||
const savedUpdatedAt = new Date(now - 120_000).toISOString();
|
||||
|
||||
try {
|
||||
process.chdir(projectRoot);
|
||||
const store = new historyStoreModule.CliHistoryStore(projectRoot);
|
||||
store.saveConversation(createConversationRecord({
|
||||
id: 'EXEC-RESUME-RUNNING',
|
||||
prompt: 'OLD HISTORY PROMPT',
|
||||
updatedAt: savedUpdatedAt,
|
||||
durationMs: 1800,
|
||||
}));
|
||||
store.close();
|
||||
|
||||
stubActiveExecutionsResponse([
|
||||
{
|
||||
id: 'EXEC-RESUME-RUNNING',
|
||||
tool: 'codex',
|
||||
mode: 'analysis',
|
||||
status: 'running',
|
||||
prompt: 'ACTIVE RESUME PROMPT',
|
||||
startTime: now - 30_000,
|
||||
output: ''
|
||||
}
|
||||
]);
|
||||
|
||||
mock.method(Date, 'now', () => now);
|
||||
|
||||
const logs = [];
|
||||
mock.method(console, 'log', (...args) => {
|
||||
logs.push(args.map(String).join(' '));
|
||||
});
|
||||
mock.method(console, 'error', () => {});
|
||||
|
||||
await cliModule.cliCommand('show', [], {});
|
||||
|
||||
const rendered = logs.join('\n');
|
||||
assert.match(rendered, /ACTIVE RESUME PROMPT/);
|
||||
assert.doesNotMatch(rendered, /OLD HISTORY PROMPT/);
|
||||
assert.match(rendered, /just now/);
|
||||
} finally {
|
||||
process.chdir(previousCwd);
|
||||
rmSync(projectRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
131
ccw/tests/cli-watch-stale-running.test.js
Normal file
131
ccw/tests/cli-watch-stale-running.test.js
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* ccw cli watch - stale running fallback tests
|
||||
*/
|
||||
|
||||
import { after, afterEach, before, describe, it, mock } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import http from 'node:http';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const TEST_CCW_HOME = mkdtempSync(join(tmpdir(), 'ccw-cli-watch-home-'));
|
||||
process.env.CCW_DATA_DIR = TEST_CCW_HOME;
|
||||
|
||||
const cliCommandPath = new URL('../dist/commands/cli.js', import.meta.url).href;
|
||||
const historyStorePath = new URL('../dist/tools/cli-history-store.js', import.meta.url).href;
|
||||
|
||||
describe('ccw cli watch stale running fallback', async () => {
|
||||
let cliModule;
|
||||
let historyStoreModule;
|
||||
|
||||
before(async () => {
|
||||
cliModule = await import(cliCommandPath);
|
||||
historyStoreModule = await import(historyStorePath);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restoreAll();
|
||||
try {
|
||||
historyStoreModule?.closeAllStores?.();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
});
|
||||
|
||||
after(() => {
|
||||
try {
|
||||
historyStoreModule?.closeAllStores?.();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
rmSync(TEST_CCW_HOME, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('treats stale active running state as completed when saved conversation is newer', async () => {
|
||||
const projectRoot = mkdtempSync(join(tmpdir(), 'ccw-cli-watch-project-'));
|
||||
const previousCwd = process.cwd();
|
||||
const now = Date.now();
|
||||
const executionId = `EXEC-WATCH-STALE-${now}`;
|
||||
|
||||
try {
|
||||
process.chdir(projectRoot);
|
||||
const store = new historyStoreModule.CliHistoryStore(projectRoot);
|
||||
store.saveConversation({
|
||||
id: executionId,
|
||||
created_at: new Date(now - 10_000).toISOString(),
|
||||
updated_at: new Date(now - 5_000).toISOString(),
|
||||
tool: 'codex',
|
||||
model: 'default',
|
||||
mode: 'analysis',
|
||||
category: 'user',
|
||||
total_duration_ms: 2100,
|
||||
turn_count: 1,
|
||||
latest_status: 'success',
|
||||
turns: [{
|
||||
turn: 1,
|
||||
timestamp: new Date(now - 5_000).toISOString(),
|
||||
prompt: 'saved prompt',
|
||||
duration_ms: 2100,
|
||||
status: 'success',
|
||||
exit_code: 0,
|
||||
output: {
|
||||
stdout: 'saved output',
|
||||
stderr: '',
|
||||
truncated: false,
|
||||
cached: false,
|
||||
}
|
||||
}]
|
||||
});
|
||||
store.close();
|
||||
|
||||
mock.method(http, 'request', (_options, callback) => {
|
||||
const payload = JSON.stringify({
|
||||
executions: [{
|
||||
id: executionId,
|
||||
tool: 'codex',
|
||||
mode: 'analysis',
|
||||
status: 'running',
|
||||
prompt: 'stale active prompt',
|
||||
startTime: now - 60_000,
|
||||
output: ''
|
||||
}]
|
||||
});
|
||||
const res = {
|
||||
on(event, handler) {
|
||||
if (event === 'data') handler(Buffer.from(payload, 'utf8'));
|
||||
if (event === 'end') handler();
|
||||
return res;
|
||||
}
|
||||
};
|
||||
if (callback) callback(res);
|
||||
const req = {
|
||||
on() { return req; },
|
||||
end() {},
|
||||
destroy() {},
|
||||
};
|
||||
return req;
|
||||
});
|
||||
|
||||
const stderrWrites = [];
|
||||
const exitCodes = [];
|
||||
mock.method(process.stderr, 'write', (chunk) => {
|
||||
stderrWrites.push(String(chunk));
|
||||
return true;
|
||||
});
|
||||
mock.method(process, 'exit', (code) => {
|
||||
exitCodes.push(code);
|
||||
});
|
||||
|
||||
await cliModule.cliCommand('watch', [executionId], { timeout: '1' });
|
||||
|
||||
const rendered = stderrWrites.join('');
|
||||
assert.match(rendered, /Execution already completed/);
|
||||
assert.match(rendered, new RegExp(`Use: ccw cli output ${executionId}`));
|
||||
assert.deepEqual(exitCodes, [0]);
|
||||
} finally {
|
||||
process.chdir(previousCwd);
|
||||
rmSync(projectRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
35
ccw/tests/codex-lens-cli-compat.test.js
Normal file
35
ccw/tests/codex-lens-cli-compat.test.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { after, describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
|
||||
const tempDirs = [];
|
||||
|
||||
after(() => {
|
||||
for (const dir of tempDirs) {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('CodexLens CLI compatibility retries', () => {
|
||||
it('initializes a tiny index even when CLI emits compatibility conflicts first', async () => {
|
||||
const moduleUrl = new URL(`../dist/tools/codex-lens.js?compat=${Date.now()}`, import.meta.url).href;
|
||||
const { checkVenvStatus, executeCodexLens } = await import(moduleUrl);
|
||||
|
||||
const ready = await checkVenvStatus(true);
|
||||
if (!ready.ready) {
|
||||
console.log('Skipping: CodexLens not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
const projectDir = mkdtempSync(join(tmpdir(), 'codexlens-init-'));
|
||||
tempDirs.push(projectDir);
|
||||
writeFileSync(join(projectDir, 'sample.ts'), 'export const sample = 1;\n');
|
||||
|
||||
const result = await executeCodexLens(['index', 'init', projectDir, '--force'], { timeout: 600000 });
|
||||
|
||||
assert.equal(result.success, true, result.error ?? 'Expected init to succeed');
|
||||
assert.ok((result.output ?? '').length > 0 || (result.warning ?? '').length > 0, 'Expected init output or compatibility warning');
|
||||
});
|
||||
});
|
||||
@@ -199,6 +199,202 @@ describe('cli routes integration', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('GET /api/cli/execution prefers newer saved conversation over stale active running state', async () => {
|
||||
const broadcasts: any[] = [];
|
||||
const { server, baseUrl } = await createServer(PROJECT_ROOT, broadcasts);
|
||||
const historyStoreUrl = new URL('../../dist/tools/cli-history-store.js', import.meta.url);
|
||||
const historyStoreMod: any = await import(historyStoreUrl.href);
|
||||
const executionId = `EXEC-STALE-DETAIL-${Date.now()}`;
|
||||
const now = 1_741_392_000_000;
|
||||
|
||||
try {
|
||||
const store = new historyStoreMod.CliHistoryStore(PROJECT_ROOT);
|
||||
store.saveConversation({
|
||||
id: executionId,
|
||||
created_at: new Date(now - 10_000).toISOString(),
|
||||
updated_at: new Date(now - 5_000).toISOString(),
|
||||
tool: 'codex',
|
||||
model: 'default',
|
||||
mode: 'analysis',
|
||||
category: 'user',
|
||||
total_duration_ms: 2300,
|
||||
turn_count: 1,
|
||||
latest_status: 'success',
|
||||
turns: [{
|
||||
turn: 1,
|
||||
timestamp: new Date(now - 5_000).toISOString(),
|
||||
prompt: 'SAVED DETAIL SHOULD WIN',
|
||||
duration_ms: 2300,
|
||||
status: 'success',
|
||||
exit_code: 0,
|
||||
output: {
|
||||
stdout: 'saved output',
|
||||
stderr: '',
|
||||
truncated: false,
|
||||
cached: false,
|
||||
}
|
||||
}]
|
||||
});
|
||||
store.close();
|
||||
|
||||
mock.method(Date, 'now', () => now - 60_000);
|
||||
mod.updateActiveExecution({
|
||||
type: 'started',
|
||||
executionId,
|
||||
tool: 'codex',
|
||||
mode: 'analysis',
|
||||
prompt: 'STALE ACTIVE DETAIL'
|
||||
});
|
||||
mock.restoreAll();
|
||||
mock.method(console, 'log', () => {});
|
||||
mock.method(console, 'error', () => {});
|
||||
|
||||
const res = await requestJson(
|
||||
baseUrl,
|
||||
'GET',
|
||||
`/api/cli/execution?path=${encodeURIComponent(PROJECT_ROOT)}&id=${encodeURIComponent(executionId)}`,
|
||||
);
|
||||
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(res.json?._active, undefined);
|
||||
assert.equal(res.json?.turns?.[0]?.prompt, 'SAVED DETAIL SHOULD WIN');
|
||||
assert.equal(res.json?.latest_status, 'success');
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
});
|
||||
|
||||
it('GET /api/cli/active filters stale running state when saved conversation is newer', async () => {
|
||||
const broadcasts: any[] = [];
|
||||
const { server, baseUrl } = await createServer(PROJECT_ROOT, broadcasts);
|
||||
const historyStoreUrl = new URL('../../dist/tools/cli-history-store.js', import.meta.url);
|
||||
const historyStoreMod: any = await import(historyStoreUrl.href);
|
||||
const executionId = `EXEC-STALE-ACTIVE-${Date.now()}`;
|
||||
const now = 1_741_392_500_000;
|
||||
|
||||
try {
|
||||
const store = new historyStoreMod.CliHistoryStore(PROJECT_ROOT);
|
||||
store.saveConversation({
|
||||
id: executionId,
|
||||
created_at: new Date(now - 12_000).toISOString(),
|
||||
updated_at: new Date(now - 4_000).toISOString(),
|
||||
tool: 'codex',
|
||||
model: 'default',
|
||||
mode: 'analysis',
|
||||
category: 'user',
|
||||
total_duration_ms: 3200,
|
||||
turn_count: 1,
|
||||
latest_status: 'success',
|
||||
turns: [{
|
||||
turn: 1,
|
||||
timestamp: new Date(now - 4_000).toISOString(),
|
||||
prompt: 'SAVED ACTIVE SHOULD WIN',
|
||||
duration_ms: 3200,
|
||||
status: 'success',
|
||||
exit_code: 0,
|
||||
output: {
|
||||
stdout: 'saved output',
|
||||
stderr: '',
|
||||
truncated: false,
|
||||
cached: false,
|
||||
}
|
||||
}]
|
||||
});
|
||||
store.close();
|
||||
|
||||
mock.method(Date, 'now', () => now - 60_000);
|
||||
mod.updateActiveExecution({
|
||||
type: 'started',
|
||||
executionId,
|
||||
tool: 'codex',
|
||||
mode: 'analysis',
|
||||
prompt: 'STALE ACTIVE SHOULD DISAPPEAR'
|
||||
});
|
||||
mock.restoreAll();
|
||||
mock.method(console, 'log', () => {});
|
||||
mock.method(console, 'error', () => {});
|
||||
|
||||
const res = await requestJson(
|
||||
baseUrl,
|
||||
'GET',
|
||||
`/api/cli/active?path=${encodeURIComponent(PROJECT_ROOT)}`,
|
||||
);
|
||||
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(Array.isArray(res.json?.executions), true);
|
||||
assert.equal(res.json.executions.some((exec: any) => exec.id === executionId), false);
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
});
|
||||
|
||||
it('GET /api/cli/active keeps running state when saved conversation is older', async () => {
|
||||
const broadcasts: any[] = [];
|
||||
const { server, baseUrl } = await createServer(PROJECT_ROOT, broadcasts);
|
||||
const historyStoreUrl = new URL('../../dist/tools/cli-history-store.js', import.meta.url);
|
||||
const historyStoreMod: any = await import(historyStoreUrl.href);
|
||||
const executionId = `EXEC-ACTIVE-RESUME-${Date.now()}`;
|
||||
const now = 1_741_393_000_000;
|
||||
|
||||
try {
|
||||
const store = new historyStoreMod.CliHistoryStore(PROJECT_ROOT);
|
||||
store.saveConversation({
|
||||
id: executionId,
|
||||
created_at: new Date(now - 120_000).toISOString(),
|
||||
updated_at: new Date(now - 110_000).toISOString(),
|
||||
tool: 'codex',
|
||||
model: 'default',
|
||||
mode: 'analysis',
|
||||
category: 'user',
|
||||
total_duration_ms: 1200,
|
||||
turn_count: 1,
|
||||
latest_status: 'success',
|
||||
turns: [{
|
||||
turn: 1,
|
||||
timestamp: new Date(now - 110_000).toISOString(),
|
||||
prompt: 'OLDER SAVED TURN',
|
||||
duration_ms: 1200,
|
||||
status: 'success',
|
||||
exit_code: 0,
|
||||
output: {
|
||||
stdout: 'older output',
|
||||
stderr: '',
|
||||
truncated: false,
|
||||
cached: false,
|
||||
}
|
||||
}]
|
||||
});
|
||||
store.close();
|
||||
|
||||
mock.method(Date, 'now', () => now - 20_000);
|
||||
mod.updateActiveExecution({
|
||||
type: 'started',
|
||||
executionId,
|
||||
tool: 'codex',
|
||||
mode: 'analysis',
|
||||
prompt: 'NEWER ACTIVE SHOULD STAY'
|
||||
});
|
||||
mock.restoreAll();
|
||||
mock.method(console, 'log', () => {});
|
||||
mock.method(console, 'error', () => {});
|
||||
|
||||
const res = await requestJson(
|
||||
baseUrl,
|
||||
'GET',
|
||||
`/api/cli/active?path=${encodeURIComponent(PROJECT_ROOT)}`,
|
||||
);
|
||||
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(Array.isArray(res.json?.executions), true);
|
||||
const activeExecution = res.json.executions.find((exec: any) => exec.id === executionId);
|
||||
assert.ok(activeExecution);
|
||||
assert.equal(activeExecution.status, 'running');
|
||||
assert.equal(activeExecution.prompt, 'NEWER ACTIVE SHOULD STAY');
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
});
|
||||
|
||||
it('PUT /api/cli/config/gemini updates config and broadcasts event', async () => {
|
||||
const broadcasts: any[] = [];
|
||||
const { server, baseUrl } = await createServer(PROJECT_ROOT, broadcasts);
|
||||
|
||||
135
ccw/tests/smart-search-mcp-usage.test.js
Normal file
135
ccw/tests/smart-search-mcp-usage.test.js
Normal file
@@ -0,0 +1,135 @@
|
||||
import { afterEach, before, describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const smartSearchPath = new URL('../dist/tools/smart-search.js', import.meta.url).href;
|
||||
|
||||
describe('Smart Search MCP usage defaults and path handling', async () => {
|
||||
let smartSearchModule;
|
||||
const tempDirs = [];
|
||||
|
||||
before(async () => {
|
||||
try {
|
||||
smartSearchModule = await import(smartSearchPath);
|
||||
} catch (err) {
|
||||
console.log('Note: smart-search module import skipped:', err?.message ?? String(err));
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
while (tempDirs.length > 0) {
|
||||
rmSync(tempDirs.pop(), { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function createWorkspace() {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'ccw-smart-search-'));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
it('keeps schema defaults aligned with runtime docs', () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
const { schema } = smartSearchModule;
|
||||
const props = schema.inputSchema.properties;
|
||||
|
||||
assert.equal(props.maxResults.default, 5);
|
||||
assert.equal(props.limit.default, 5);
|
||||
assert.match(schema.description, /static FTS index/i);
|
||||
assert.match(props.path.description, /single file path/i);
|
||||
});
|
||||
|
||||
it('honors explicit small limit values', async () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
const dir = createWorkspace();
|
||||
const file = join(dir, 'many.ts');
|
||||
writeFileSync(file, ['const hit = 1;', 'const hit = 2;', 'const hit = 3;'].join('\n'));
|
||||
|
||||
const toolResult = await smartSearchModule.handler({
|
||||
action: 'search',
|
||||
query: 'hit',
|
||||
path: dir,
|
||||
limit: 1,
|
||||
regex: false,
|
||||
tokenize: false,
|
||||
});
|
||||
|
||||
assert.equal(toolResult.success, true, toolResult.error);
|
||||
assert.equal(toolResult.result.success, true);
|
||||
assert.equal(toolResult.result.results.length, 1);
|
||||
assert.equal(toolResult.result.metadata.pagination.limit, 1);
|
||||
});
|
||||
|
||||
it('scopes search results to a single file path', async () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
const dir = createWorkspace();
|
||||
const target = join(dir, 'target.ts');
|
||||
const other = join(dir, 'other.ts');
|
||||
writeFileSync(target, 'const TARGET_TOKEN = 1;\n');
|
||||
writeFileSync(other, 'const TARGET_TOKEN = 2;\n');
|
||||
|
||||
const toolResult = await smartSearchModule.handler({
|
||||
action: 'search',
|
||||
query: 'TARGET_TOKEN',
|
||||
path: target,
|
||||
regex: false,
|
||||
tokenize: false,
|
||||
});
|
||||
|
||||
assert.equal(toolResult.success, true, toolResult.error);
|
||||
assert.equal(toolResult.result.success, true);
|
||||
assert.ok(Array.isArray(toolResult.result.results));
|
||||
assert.ok(toolResult.result.results.length >= 1);
|
||||
|
||||
const normalizedFiles = toolResult.result.results.map((item) => String(item.file).replace(/\\/g, '/'));
|
||||
assert.ok(normalizedFiles.every((file) => file.endsWith('/target.ts') || file === 'target.ts'));
|
||||
assert.ok(normalizedFiles.every((file) => !file.endsWith('/other.ts')));
|
||||
});
|
||||
|
||||
it('normalizes wrapped multiline query and file path inputs', async () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
const dir = createWorkspace();
|
||||
const nestedDir = join(dir, 'hydro_generator_module', 'builders');
|
||||
mkdirSync(nestedDir, { recursive: true });
|
||||
const target = join(nestedDir, 'full_machine_builders.py');
|
||||
writeFileSync(target, 'def _resolve_rotor_inner():\n return rotor_main_seg\n');
|
||||
|
||||
const wrappedPath = target.replace(/([\\/])builders([\\/])/, '$1\n builders$2');
|
||||
const wrappedQuery = '_resolve_rotor_inner OR\n rotor_main_seg';
|
||||
|
||||
const toolResult = await smartSearchModule.handler({
|
||||
action: 'search',
|
||||
query: wrappedQuery,
|
||||
path: wrappedPath,
|
||||
regex: false,
|
||||
caseSensitive: false,
|
||||
});
|
||||
|
||||
assert.equal(toolResult.success, true, toolResult.error);
|
||||
assert.equal(toolResult.result.success, true);
|
||||
assert.ok(toolResult.result.results.length >= 1);
|
||||
});
|
||||
|
||||
it('surfaces backend failure details when fuzzy search fully fails', async () => {
|
||||
if (!smartSearchModule) return;
|
||||
|
||||
const missingPath = join(createWorkspace(), 'missing-folder', 'missing.ts');
|
||||
const toolResult = await smartSearchModule.handler({
|
||||
action: 'search',
|
||||
query: 'TARGET_TOKEN',
|
||||
path: missingPath,
|
||||
regex: false,
|
||||
tokenize: false,
|
||||
});
|
||||
|
||||
assert.equal(toolResult.success, false);
|
||||
assert.match(toolResult.error, /Both search backends failed:/);
|
||||
assert.match(toolResult.error, /(FTS|Ripgrep)/);
|
||||
});
|
||||
});
|
||||
51
ccw/tests/uv-manager-codexlens-python.test.js
Normal file
51
ccw/tests/uv-manager-codexlens-python.test.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { afterEach, before, describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
const uvManagerPath = new URL('../dist/utils/uv-manager.js', import.meta.url).href;
|
||||
|
||||
describe('CodexLens UV python preference', async () => {
|
||||
let mod;
|
||||
const originalPython = process.env.CCW_PYTHON;
|
||||
|
||||
before(async () => {
|
||||
mod = await import(uvManagerPath);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalPython === undefined) {
|
||||
delete process.env.CCW_PYTHON;
|
||||
return;
|
||||
}
|
||||
process.env.CCW_PYTHON = originalPython;
|
||||
});
|
||||
|
||||
it('honors CCW_PYTHON override', () => {
|
||||
process.env.CCW_PYTHON = 'C:/Custom/Python/python.exe';
|
||||
assert.equal(mod.getPreferredCodexLensPythonSpec(), 'C:/Custom/Python/python.exe');
|
||||
});
|
||||
|
||||
it('prefers Python 3.11 or 3.10 on Windows when available', () => {
|
||||
if (process.platform !== 'win32') return;
|
||||
delete process.env.CCW_PYTHON;
|
||||
|
||||
let installed = '';
|
||||
try {
|
||||
installed = execSync('py -0p', { encoding: 'utf-8' });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const has311 = installed.includes('-V:3.11');
|
||||
const has310 = installed.includes('-V:3.10');
|
||||
if (!has311 && !has310) {
|
||||
return;
|
||||
}
|
||||
|
||||
const preferred = mod.getPreferredCodexLensPythonSpec();
|
||||
assert.ok(
|
||||
preferred === '3.11' || preferred === '3.10',
|
||||
`expected Windows preference to avoid 3.12 when 3.11/3.10 exists, got ${preferred}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user