@angli/unit-test-tool 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +14 -0
- package/README.md +232 -0
- package/dist/src/cli/commands/analyze.js +20 -0
- package/dist/src/cli/commands/guard.js +26 -0
- package/dist/src/cli/commands/init.js +39 -0
- package/dist/src/cli/commands/run.js +72 -0
- package/dist/src/cli/commands/schedule.js +29 -0
- package/dist/src/cli/commands/start.js +102 -0
- package/dist/src/cli/commands/status.js +27 -0
- package/dist/src/cli/commands/verify.js +15 -0
- package/dist/src/cli/context/create-context.js +101 -0
- package/dist/src/cli/index.js +23 -0
- package/dist/src/cli/utils/scan-dir.js +6 -0
- package/dist/src/core/analyzers/coverage-analyzer.js +8 -0
- package/dist/src/core/analyzers/dependency-complexity-analyzer.js +19 -0
- package/dist/src/core/analyzers/existing-test-analyzer.js +66 -0
- package/dist/src/core/analyzers/failure-history-analyzer.js +9 -0
- package/dist/src/core/analyzers/file-classifier-analyzer.js +24 -0
- package/dist/src/core/analyzers/index.js +37 -0
- package/dist/src/core/analyzers/llm-semantic-analyzer.js +3 -0
- package/dist/src/core/analyzers/path-priority-analyzer.js +34 -0
- package/dist/src/core/coverage/read-coverage-summary.js +183 -0
- package/dist/src/core/executor/claude-cli-executor.js +91 -0
- package/dist/src/core/middleware/loop-detection.js +6 -0
- package/dist/src/core/middleware/pre-completion-checklist.js +27 -0
- package/dist/src/core/middleware/silent-success-post-check.js +13 -0
- package/dist/src/core/planner/rank-candidates.js +3 -0
- package/dist/src/core/planner/rule-planner.js +41 -0
- package/dist/src/core/planner/score-candidate.js +49 -0
- package/dist/src/core/prompts/case-library.js +35 -0
- package/dist/src/core/prompts/edit-boundary-prompt.js +24 -0
- package/dist/src/core/prompts/retry-prompt.js +18 -0
- package/dist/src/core/prompts/system-prompt.js +27 -0
- package/dist/src/core/prompts/task-prompt.js +22 -0
- package/dist/src/core/reporter/index.js +48 -0
- package/dist/src/core/state-machine/index.js +12 -0
- package/dist/src/core/storage/defaults.js +16 -0
- package/dist/src/core/storage/event-store.js +16 -0
- package/dist/src/core/storage/lifecycle-store.js +18 -0
- package/dist/src/core/storage/report-store.js +16 -0
- package/dist/src/core/storage/state-store.js +11 -0
- package/dist/src/core/strategies/classify-failure.js +14 -0
- package/dist/src/core/strategies/switch-mock-strategy.js +9 -0
- package/dist/src/core/tools/analyze-baseline.js +54 -0
- package/dist/src/core/tools/guard.js +68 -0
- package/dist/src/core/tools/run-loop.js +108 -0
- package/dist/src/core/tools/run-with-claude-cli.js +645 -0
- package/dist/src/core/tools/verify-all.js +75 -0
- package/dist/src/core/worktrees/is-git-repo.js +10 -0
- package/dist/src/types/index.js +1 -0
- package/dist/src/types/logger.js +1 -0
- package/dist/src/utils/clock.js +10 -0
- package/dist/src/utils/command-runner.js +18 -0
- package/dist/src/utils/commands.js +28 -0
- package/dist/src/utils/duration.js +22 -0
- package/dist/src/utils/fs.js +53 -0
- package/dist/src/utils/logger.js +10 -0
- package/dist/src/utils/paths.js +21 -0
- package/dist/src/utils/process-lifecycle.js +74 -0
- package/dist/src/utils/prompts.js +20 -0
- package/dist/tests/core/create-context.test.js +41 -0
- package/dist/tests/core/default-state.test.js +10 -0
- package/dist/tests/core/failure-classification.test.js +7 -0
- package/dist/tests/core/loop-detection.test.js +7 -0
- package/dist/tests/core/paths.test.js +11 -0
- package/dist/tests/core/prompt-builders.test.js +33 -0
- package/dist/tests/core/score-candidate.test.js +28 -0
- package/dist/tests/core/state-machine.test.js +12 -0
- package/dist/tests/integration/status-report.test.js +21 -0
- package/docs/architecture.md +20 -0
- package/docs/demo.sh +266 -0
- package/docs/skill-integration.md +15 -0
- package/docs/state-machine.md +15 -0
- package/package.json +31 -0
- package/src/cli/commands/analyze.ts +22 -0
- package/src/cli/commands/guard.ts +28 -0
- package/src/cli/commands/init.ts +41 -0
- package/src/cli/commands/run.ts +79 -0
- package/src/cli/commands/schedule.ts +32 -0
- package/src/cli/commands/start.ts +111 -0
- package/src/cli/commands/status.ts +30 -0
- package/src/cli/commands/verify.ts +17 -0
- package/src/cli/context/create-context.ts +142 -0
- package/src/cli/index.ts +27 -0
- package/src/cli/utils/scan-dir.ts +5 -0
- package/src/core/analyzers/coverage-analyzer.ts +10 -0
- package/src/core/analyzers/dependency-complexity-analyzer.ts +25 -0
- package/src/core/analyzers/existing-test-analyzer.ts +76 -0
- package/src/core/analyzers/failure-history-analyzer.ts +12 -0
- package/src/core/analyzers/file-classifier-analyzer.ts +25 -0
- package/src/core/analyzers/index.ts +51 -0
- package/src/core/analyzers/llm-semantic-analyzer.ts +6 -0
- package/src/core/analyzers/path-priority-analyzer.ts +41 -0
- package/src/core/coverage/read-coverage-summary.ts +224 -0
- package/src/core/executor/claude-cli-executor.ts +94 -0
- package/src/core/middleware/loop-detection.ts +8 -0
- package/src/core/middleware/pre-completion-checklist.ts +32 -0
- package/src/core/middleware/silent-success-post-check.ts +16 -0
- package/src/core/planner/rank-candidates.ts +5 -0
- package/src/core/planner/rule-planner.ts +65 -0
- package/src/core/planner/score-candidate.ts +60 -0
- package/src/core/prompts/case-library.ts +36 -0
- package/src/core/prompts/edit-boundary-prompt.ts +26 -0
- package/src/core/prompts/retry-prompt.ts +22 -0
- package/src/core/prompts/system-prompt.ts +32 -0
- package/src/core/prompts/task-prompt.ts +26 -0
- package/src/core/reporter/index.ts +56 -0
- package/src/core/state-machine/index.ts +14 -0
- package/src/core/storage/defaults.ts +18 -0
- package/src/core/storage/event-store.ts +18 -0
- package/src/core/storage/lifecycle-store.ts +20 -0
- package/src/core/storage/report-store.ts +19 -0
- package/src/core/storage/state-store.ts +18 -0
- package/src/core/strategies/classify-failure.ts +9 -0
- package/src/core/strategies/switch-mock-strategy.ts +12 -0
- package/src/core/tools/analyze-baseline.ts +61 -0
- package/src/core/tools/guard.ts +89 -0
- package/src/core/tools/run-loop.ts +142 -0
- package/src/core/tools/run-with-claude-cli.ts +926 -0
- package/src/core/tools/verify-all.ts +83 -0
- package/src/core/worktrees/is-git-repo.ts +10 -0
- package/src/types/index.ts +291 -0
- package/src/types/logger.ts +6 -0
- package/src/utils/clock.ts +10 -0
- package/src/utils/command-runner.ts +24 -0
- package/src/utils/commands.ts +42 -0
- package/src/utils/duration.ts +20 -0
- package/src/utils/fs.ts +50 -0
- package/src/utils/logger.ts +12 -0
- package/src/utils/paths.ts +24 -0
- package/src/utils/process-lifecycle.ts +92 -0
- package/src/utils/prompts.ts +22 -0
- package/tests/core/create-context.test.ts +45 -0
- package/tests/core/default-state.test.ts +11 -0
- package/tests/core/failure-classification.test.ts +8 -0
- package/tests/core/loop-detection.test.ts +8 -0
- package/tests/core/paths.test.ts +13 -0
- package/tests/core/prompt-builders.test.ts +38 -0
- package/tests/core/score-candidate.test.ts +30 -0
- package/tests/core/state-machine.test.ts +14 -0
- package/tests/fixtures/simple-project/.openclaw-testbot/logs/events.jsonl +10 -0
- package/tests/fixtures/simple-project/.openclaw-testbot/plan.json +75 -0
- package/tests/fixtures/simple-project/.openclaw-testbot/reports/coverage-summary.json +9 -0
- package/tests/fixtures/simple-project/.openclaw-testbot/reports/final-report.json +14 -0
- package/tests/fixtures/simple-project/.openclaw-testbot/state.json +18 -0
- package/tests/fixtures/simple-project/coverage-summary.json +1 -0
- package/tests/fixtures/simple-project/package.json +8 -0
- package/tests/fixtures/simple-project/src/add.js +3 -0
- package/tests/fixtures/simple-project/test-runner.js +18 -0
- package/tests/integration/status-report.test.ts +24 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readCoverageSummary } from '../coverage/read-coverage-summary.js';
|
|
3
|
+
export async function verifyAll(ctx) {
|
|
4
|
+
const testDirs = await detectTestDirNames(ctx);
|
|
5
|
+
ctx.logger.info(`[verify] step=test-dirs value=${testDirs.join(',')}`);
|
|
6
|
+
ctx.logger.info(`[verify] step=run status=start cmd=${ctx.commands.verifyFull}`);
|
|
7
|
+
const verifyResult = await ctx.commandRunner.run(ctx.commands.verifyFull);
|
|
8
|
+
if (verifyResult.exitCode !== 0) {
|
|
9
|
+
if (verifyResult.stdout)
|
|
10
|
+
console.log(`[verify] stdout:\n${verifyResult.stdout}`);
|
|
11
|
+
if (verifyResult.stderr)
|
|
12
|
+
console.log(`[verify] stderr:\n${verifyResult.stderr}`);
|
|
13
|
+
}
|
|
14
|
+
const ok = verifyResult.exitCode === 0;
|
|
15
|
+
const coverageSnapshot = ok ? await readCoverageSummary(ctx) : undefined;
|
|
16
|
+
const coverageValue = coverageSnapshot?.lines;
|
|
17
|
+
const coveragePassed = ok ? coverageValue !== undefined && coverageValue >= ctx.coverageTarget : false;
|
|
18
|
+
ctx.logger.info(`[verify] step=run status=done ok=${ok} exit=${verifyResult.exitCode} coverage=${coverageValue ?? 'unknown'} target=${ctx.coverageTarget}`);
|
|
19
|
+
const state = await ctx.stateStore.load();
|
|
20
|
+
const nextState = {
|
|
21
|
+
...state,
|
|
22
|
+
phase: 'VERIFYING_FULL',
|
|
23
|
+
status: (ok ? 'running' : 'failed'),
|
|
24
|
+
lastAction: 'verify-all',
|
|
25
|
+
nextSuggestedAction: ok ? 'report' : 'manual-intervention',
|
|
26
|
+
updatedAt: ctx.clock.nowIso()
|
|
27
|
+
};
|
|
28
|
+
await ctx.stateStore.save(nextState);
|
|
29
|
+
if (ok) {
|
|
30
|
+
await ctx.eventStore.append({
|
|
31
|
+
eventType: 'verify_full_passed',
|
|
32
|
+
phase: 'VERIFYING_FULL',
|
|
33
|
+
message: `verify all passed, coverage=${coverageValue ?? 'unknown'} target=${ctx.coverageTarget}`,
|
|
34
|
+
data: { coverageSnapshot, coverageTarget: ctx.coverageTarget },
|
|
35
|
+
timestamp: ctx.clock.nowIso()
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
await ctx.eventStore.append({
|
|
40
|
+
eventType: 'verify_full_failed',
|
|
41
|
+
phase: 'VERIFYING_FULL',
|
|
42
|
+
message: 'verify all failed',
|
|
43
|
+
data: { stderr: verifyResult.stderr },
|
|
44
|
+
timestamp: ctx.clock.nowIso()
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
ok,
|
|
49
|
+
phase: 'VERIFYING_FULL',
|
|
50
|
+
summary: ok ? '全量验证通过。' : '全量验证失败。',
|
|
51
|
+
nextAction: ok ? 'report' : 'manual-intervention',
|
|
52
|
+
artifacts: {
|
|
53
|
+
stdout: verifyResult.stdout,
|
|
54
|
+
stderr: verifyResult.stderr
|
|
55
|
+
},
|
|
56
|
+
coveragePassed,
|
|
57
|
+
testPassed: ok,
|
|
58
|
+
coverageSnapshot
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
async function detectTestDirNames(ctx) {
|
|
62
|
+
if (ctx.cache.detectedTestDirNames)
|
|
63
|
+
return ctx.cache.detectedTestDirNames;
|
|
64
|
+
const configured = ctx.config.testDirNames?.filter(Boolean) ?? ['__test__', '__tests__'];
|
|
65
|
+
const jestConfigPath = path.join(ctx.projectPath, 'jest.config.js');
|
|
66
|
+
const jestConfig = await ctx.fileSystem.readText(jestConfigPath);
|
|
67
|
+
if (!jestConfig) {
|
|
68
|
+
ctx.cache.detectedTestDirNames = configured;
|
|
69
|
+
return configured;
|
|
70
|
+
}
|
|
71
|
+
const detected = configured.filter((dir) => jestConfig.includes(dir));
|
|
72
|
+
const result = detected.length > 0 ? detected : configured;
|
|
73
|
+
ctx.cache.detectedTestDirNames = result;
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
export function createCommandRunner(options) {
|
|
3
|
+
return {
|
|
4
|
+
async run(command, timeoutMs = 120000) {
|
|
5
|
+
const result = await execa(command, {
|
|
6
|
+
cwd: options.cwd,
|
|
7
|
+
shell: true,
|
|
8
|
+
reject: false,
|
|
9
|
+
timeout: timeoutMs
|
|
10
|
+
});
|
|
11
|
+
return {
|
|
12
|
+
stdout: result.stdout,
|
|
13
|
+
stderr: result.stderr,
|
|
14
|
+
exitCode: result.exitCode ?? 0
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function buildCommandSet(config, scripts = {}) {
|
|
2
|
+
const verifyFromScript = scripts.test ? 'npm test' : undefined;
|
|
3
|
+
const coverageScript = scripts['test:coverage'] ? 'npm run test:coverage' : undefined;
|
|
4
|
+
const lintScript = scripts.lint ? 'npm run lint' : undefined;
|
|
5
|
+
const typecheckScript = scripts.typecheck ? 'npm run typecheck' : undefined;
|
|
6
|
+
return {
|
|
7
|
+
analyzeBaseline: config.commandOverrides?.analyzeBaseline ??
|
|
8
|
+
coverageScript ??
|
|
9
|
+
'npm test -- --coverage --runInBand',
|
|
10
|
+
verifyFull: config.commandOverrides?.verifyFull ??
|
|
11
|
+
verifyFromScript ??
|
|
12
|
+
'npm test -- --runInBand',
|
|
13
|
+
incrementalTest: (targetFile) => {
|
|
14
|
+
const base = config.commandOverrides?.incrementalTest ??
|
|
15
|
+
verifyFromScript ??
|
|
16
|
+
'npm test -- --runInBand';
|
|
17
|
+
if (!targetFile) {
|
|
18
|
+
return base;
|
|
19
|
+
}
|
|
20
|
+
return `${base} ${JSON.stringify(targetFile)}`;
|
|
21
|
+
},
|
|
22
|
+
lint: config.commandOverrides?.lint ?? lintScript ?? 'npm run lint',
|
|
23
|
+
typecheck: config.commandOverrides?.typecheck ??
|
|
24
|
+
typecheckScript ??
|
|
25
|
+
'npx tsc -p tsconfig.json --noEmit',
|
|
26
|
+
format: config.commandOverrides?.format ?? 'npm run lint'
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function parseDuration(input) {
|
|
2
|
+
const trimmed = input.trim();
|
|
3
|
+
if (!trimmed)
|
|
4
|
+
return undefined;
|
|
5
|
+
const match = trimmed.match(/^(\d+)(ms|s|m|h)?$/);
|
|
6
|
+
if (!match)
|
|
7
|
+
return undefined;
|
|
8
|
+
const value = Number(match[1]);
|
|
9
|
+
const unit = match[2] ?? 'ms';
|
|
10
|
+
switch (unit) {
|
|
11
|
+
case 'ms':
|
|
12
|
+
return value;
|
|
13
|
+
case 's':
|
|
14
|
+
return value * 1000;
|
|
15
|
+
case 'm':
|
|
16
|
+
return value * 60 * 1000;
|
|
17
|
+
case 'h':
|
|
18
|
+
return value * 60 * 60 * 1000;
|
|
19
|
+
default:
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export function createFileSystem() {
|
|
4
|
+
return {
|
|
5
|
+
async ensureDir(dirPath) {
|
|
6
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
7
|
+
},
|
|
8
|
+
async fileExists(filePath) {
|
|
9
|
+
try {
|
|
10
|
+
await fs.access(filePath);
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
async readJson(filePath) {
|
|
18
|
+
const exists = await this.fileExists(filePath);
|
|
19
|
+
if (!exists)
|
|
20
|
+
return undefined;
|
|
21
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
22
|
+
return JSON.parse(content);
|
|
23
|
+
},
|
|
24
|
+
async writeJson(filePath, value) {
|
|
25
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
26
|
+
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
27
|
+
},
|
|
28
|
+
async appendLine(filePath, line) {
|
|
29
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
30
|
+
await fs.appendFile(filePath, `${line}\n`, 'utf8');
|
|
31
|
+
},
|
|
32
|
+
async appendText(filePath, content) {
|
|
33
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
34
|
+
await fs.appendFile(filePath, content, 'utf8');
|
|
35
|
+
},
|
|
36
|
+
async readText(filePath) {
|
|
37
|
+
const exists = await this.fileExists(filePath);
|
|
38
|
+
if (!exists)
|
|
39
|
+
return undefined;
|
|
40
|
+
return fs.readFile(filePath, 'utf8');
|
|
41
|
+
},
|
|
42
|
+
async writeText(filePath, content) {
|
|
43
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
44
|
+
await fs.writeFile(filePath, content, 'utf8');
|
|
45
|
+
},
|
|
46
|
+
async readDir(dirPath) {
|
|
47
|
+
const exists = await this.fileExists(dirPath);
|
|
48
|
+
if (!exists)
|
|
49
|
+
return [];
|
|
50
|
+
return fs.readdir(dirPath);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import pino from 'pino';
|
|
2
|
+
export function createLogger(level) {
|
|
3
|
+
const logger = pino({ level });
|
|
4
|
+
return {
|
|
5
|
+
info: (message, meta) => logger.info(meta ?? {}, message),
|
|
6
|
+
warn: (message, meta) => logger.warn(meta ?? {}, message),
|
|
7
|
+
error: (message, meta) => logger.error(meta ?? {}, message),
|
|
8
|
+
debug: (message, meta) => logger.debug(meta ?? {}, message)
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
export function buildPaths(projectPath) {
|
|
3
|
+
const runtimeDir = path.join(projectPath, '.unit_test_tool_workspace');
|
|
4
|
+
const logsPath = path.join(runtimeDir, 'logs');
|
|
5
|
+
const promptsDir = path.join(runtimeDir, 'prompts');
|
|
6
|
+
const promptRunsDir = path.join(runtimeDir, 'prompt-runs');
|
|
7
|
+
const reportsDir = path.join(runtimeDir, 'reports');
|
|
8
|
+
return {
|
|
9
|
+
runtimeDir,
|
|
10
|
+
configPath: path.join(runtimeDir, 'config.json'),
|
|
11
|
+
statePath: path.join(runtimeDir, 'state.json'),
|
|
12
|
+
logsPath,
|
|
13
|
+
promptsDir,
|
|
14
|
+
promptRunsDir,
|
|
15
|
+
eventsPath: path.join(logsPath, 'events.jsonl'),
|
|
16
|
+
reportsDir,
|
|
17
|
+
coverageSummaryPath: path.join(reportsDir, 'coverage-summary.json'),
|
|
18
|
+
finalReportPath: path.join(reportsDir, 'final-report.json'),
|
|
19
|
+
lifecyclePath: path.join(runtimeDir, 'lifecycle.json')
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
2
|
+
export async function startLifecycle(ctx, options) {
|
|
3
|
+
const startedAt = ctx.clock.nowIso();
|
|
4
|
+
const lifecycle = {
|
|
5
|
+
runId: ctx.runId,
|
|
6
|
+
command: options.command,
|
|
7
|
+
pid: process.pid,
|
|
8
|
+
argv: options.argv,
|
|
9
|
+
startedAt,
|
|
10
|
+
lastHeartbeat: startedAt,
|
|
11
|
+
status: 'running',
|
|
12
|
+
restartCount: 0,
|
|
13
|
+
updatedAt: startedAt
|
|
14
|
+
};
|
|
15
|
+
await ctx.lifecycleStore.save(lifecycle);
|
|
16
|
+
await ctx.eventStore.append({
|
|
17
|
+
eventType: 'lifecycle_started',
|
|
18
|
+
phase: 'INIT',
|
|
19
|
+
message: `${options.command} started`,
|
|
20
|
+
data: { pid: process.pid, argv: options.argv },
|
|
21
|
+
timestamp: startedAt
|
|
22
|
+
});
|
|
23
|
+
const interval = setInterval(async () => {
|
|
24
|
+
const now = ctx.clock.nowIso();
|
|
25
|
+
await ctx.lifecycleStore.patch({
|
|
26
|
+
lastHeartbeat: now,
|
|
27
|
+
updatedAt: now
|
|
28
|
+
});
|
|
29
|
+
}, options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS);
|
|
30
|
+
const stop = async (status, exitCode, lastError) => {
|
|
31
|
+
clearInterval(interval);
|
|
32
|
+
const now = ctx.clock.nowIso();
|
|
33
|
+
await ctx.lifecycleStore.patch({
|
|
34
|
+
status,
|
|
35
|
+
exitCode,
|
|
36
|
+
lastError,
|
|
37
|
+
updatedAt: now
|
|
38
|
+
});
|
|
39
|
+
await ctx.eventStore.append({
|
|
40
|
+
eventType: 'lifecycle_stopped',
|
|
41
|
+
phase: 'DONE',
|
|
42
|
+
message: `${options.command} ${status}`,
|
|
43
|
+
data: { exitCode, lastError },
|
|
44
|
+
timestamp: now
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
const handleExit = async (code) => {
|
|
48
|
+
await stop(code === 0 ? 'exited' : 'crashed', code);
|
|
49
|
+
};
|
|
50
|
+
const handleSignal = async (signal) => {
|
|
51
|
+
await stop('exited', undefined, `signal:${signal}`);
|
|
52
|
+
process.exit(0);
|
|
53
|
+
};
|
|
54
|
+
const handleError = async (error) => {
|
|
55
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
56
|
+
await stop('crashed', 1, message);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
};
|
|
59
|
+
process.on('exit', handleExit);
|
|
60
|
+
process.on('SIGINT', handleSignal);
|
|
61
|
+
process.on('SIGTERM', handleSignal);
|
|
62
|
+
process.on('uncaughtException', handleError);
|
|
63
|
+
process.on('unhandledRejection', handleError);
|
|
64
|
+
return {
|
|
65
|
+
stop: async () => {
|
|
66
|
+
process.off('exit', handleExit);
|
|
67
|
+
process.off('SIGINT', handleSignal);
|
|
68
|
+
process.off('SIGTERM', handleSignal);
|
|
69
|
+
process.off('uncaughtException', handleError);
|
|
70
|
+
process.off('unhandledRejection', handleError);
|
|
71
|
+
await stop('exited', 0);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { buildSystemPrompt } from '../core/prompts/system-prompt.js';
|
|
2
|
+
import { buildTaskPrompt } from '../core/prompts/task-prompt.js';
|
|
3
|
+
import { buildRetryPrompt } from '../core/prompts/retry-prompt.js';
|
|
4
|
+
import { buildEditBoundaryPrompt } from '../core/prompts/edit-boundary-prompt.js';
|
|
5
|
+
export function buildPromptBuilders(overrides) {
|
|
6
|
+
return {
|
|
7
|
+
buildSystemPrompt() {
|
|
8
|
+
return buildSystemPrompt(overrides);
|
|
9
|
+
},
|
|
10
|
+
buildTaskPrompt(task) {
|
|
11
|
+
return buildTaskPrompt(task, overrides);
|
|
12
|
+
},
|
|
13
|
+
buildRetryPrompt(task, failure) {
|
|
14
|
+
return buildRetryPrompt(task, failure, overrides);
|
|
15
|
+
},
|
|
16
|
+
buildEditBoundaryPrompt(task) {
|
|
17
|
+
return buildEditBoundaryPrompt(task);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { createContext } from '../../src/cli/context/create-context.js';
|
|
6
|
+
describe('createContext', () => {
|
|
7
|
+
it('passes promptOverrides into prompt builders', async () => {
|
|
8
|
+
const projectPath = mkdtempSync(path.join(os.tmpdir(), 'unit-test-tool-'));
|
|
9
|
+
try {
|
|
10
|
+
writeFileSync(path.join(projectPath, 'package.json'), JSON.stringify({ scripts: {} }));
|
|
11
|
+
const ctx = await createContext({
|
|
12
|
+
projectPath,
|
|
13
|
+
coverageTarget: 80,
|
|
14
|
+
configOverrides: {
|
|
15
|
+
promptOverrides: {
|
|
16
|
+
systemAppend: 'system override',
|
|
17
|
+
taskAppend: 'task override',
|
|
18
|
+
retryAppend: 'retry override'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
const task = {
|
|
23
|
+
taskId: 't1',
|
|
24
|
+
targetFile: 'src/foo.ts',
|
|
25
|
+
testFiles: [],
|
|
26
|
+
score: 1,
|
|
27
|
+
reasons: [],
|
|
28
|
+
estimatedDifficulty: 'low',
|
|
29
|
+
strategy: '补测试',
|
|
30
|
+
status: 'pending',
|
|
31
|
+
attemptCount: 0
|
|
32
|
+
};
|
|
33
|
+
expect(ctx.prompts.buildSystemPrompt()).toContain('system override');
|
|
34
|
+
expect(ctx.prompts.buildTaskPrompt(task)).toContain('task override');
|
|
35
|
+
expect(ctx.prompts.buildRetryPrompt(task, 'failed')).toContain('retry override');
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
rmSync(projectPath, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { defaultState } from '../../src/core/storage/defaults.js';
|
|
3
|
+
describe('defaultState', () => {
|
|
4
|
+
it('provides baseline state', () => {
|
|
5
|
+
const state = defaultState();
|
|
6
|
+
expect(state.phase).toBe('INIT');
|
|
7
|
+
expect(state.status).toBe('idle');
|
|
8
|
+
expect(state.totals.pending).toBe(0);
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { classifyFailure } from '../../src/core/strategies/classify-failure.js';
|
|
3
|
+
describe('classifyFailure', () => {
|
|
4
|
+
it('classifies timeout messages', () => {
|
|
5
|
+
expect(classifyFailure('request timeout')).toBe('CLI_TIMEOUT');
|
|
6
|
+
});
|
|
7
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { loopDetection } from '../../src/core/middleware/loop-detection.js';
|
|
3
|
+
describe('loopDetection', () => {
|
|
4
|
+
it('detects when threshold exceeded', () => {
|
|
5
|
+
expect(loopDetection({ attemptCount: 3 }, 2).loopDetected).toBe(true);
|
|
6
|
+
});
|
|
7
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { buildPaths } from '../../src/utils/paths.js';
|
|
3
|
+
describe('buildPaths', () => {
|
|
4
|
+
it('builds runtime file paths', () => {
|
|
5
|
+
const paths = buildPaths('/tmp/demo-project');
|
|
6
|
+
expect(paths.runtimeDir).toBe('/tmp/demo-project/.unit_test_tool_workspace');
|
|
7
|
+
expect(paths.statePath).toBe('/tmp/demo-project/.unit_test_tool_workspace/state.json');
|
|
8
|
+
expect(paths.eventsPath).toBe('/tmp/demo-project/.unit_test_tool_workspace/logs/events.jsonl');
|
|
9
|
+
expect(paths.finalReportPath).toBe('/tmp/demo-project/.unit_test_tool_workspace/reports/final-report.json');
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { buildPromptBuilders } from '../../src/utils/prompts.js';
|
|
3
|
+
describe('buildPromptBuilders', () => {
|
|
4
|
+
it('includes default strategy and case library in system prompt', () => {
|
|
5
|
+
const prompts = buildPromptBuilders();
|
|
6
|
+
const systemPrompt = prompts.buildSystemPrompt();
|
|
7
|
+
expect(systemPrompt).toContain('通用单测策略:');
|
|
8
|
+
expect(systemPrompt).toContain('典型案例约束:');
|
|
9
|
+
expect(systemPrompt).toContain('render/HOC 包装组件');
|
|
10
|
+
expect(systemPrompt).toContain('强依赖上下文组件');
|
|
11
|
+
});
|
|
12
|
+
it('appends project prompt overrides', () => {
|
|
13
|
+
const prompts = buildPromptBuilders({
|
|
14
|
+
systemAppend: 'system rule',
|
|
15
|
+
taskAppend: 'task rule',
|
|
16
|
+
retryAppend: 'retry rule'
|
|
17
|
+
});
|
|
18
|
+
const task = {
|
|
19
|
+
taskId: 't1',
|
|
20
|
+
targetFile: 'src/foo.ts',
|
|
21
|
+
testFiles: [],
|
|
22
|
+
score: 1,
|
|
23
|
+
reasons: [],
|
|
24
|
+
estimatedDifficulty: 'low',
|
|
25
|
+
strategy: '补测试',
|
|
26
|
+
status: 'pending',
|
|
27
|
+
attemptCount: 0
|
|
28
|
+
};
|
|
29
|
+
expect(prompts.buildSystemPrompt()).toContain('system rule');
|
|
30
|
+
expect(prompts.buildTaskPrompt(task)).toContain('task rule');
|
|
31
|
+
expect(prompts.buildRetryPrompt(task, 'boom')).toContain('retry rule');
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { scoreCandidate } from '../../src/core/planner/score-candidate.js';
|
|
3
|
+
describe('scoreCandidate', () => {
|
|
4
|
+
it('scores priority and low complexity candidates higher', () => {
|
|
5
|
+
const result = scoreCandidate({
|
|
6
|
+
targetFile: 'src/a.ts',
|
|
7
|
+
coverage: { coverageGap: 20 },
|
|
8
|
+
pathPriority: { label: 'priority', score: 100 },
|
|
9
|
+
fileClassification: {},
|
|
10
|
+
dependencyComplexity: {
|
|
11
|
+
importCount: 1,
|
|
12
|
+
externalDependencyCount: 1,
|
|
13
|
+
complexityLevel: 'low'
|
|
14
|
+
},
|
|
15
|
+
existingTest: {
|
|
16
|
+
hasExistingTest: false,
|
|
17
|
+
testFilePaths: []
|
|
18
|
+
},
|
|
19
|
+
failureHistory: {
|
|
20
|
+
previousAttemptCount: 0,
|
|
21
|
+
loopDetected: false,
|
|
22
|
+
blockedBefore: false,
|
|
23
|
+
failurePenaltyScore: 0
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
expect(result.baseScore).toBeGreaterThan(100);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { deriveStatusFromPhase } from '../../src/core/state-machine/index.js';
|
|
3
|
+
describe('deriveStatusFromPhase', () => {
|
|
4
|
+
it('maps blocked and done phases', () => {
|
|
5
|
+
expect(deriveStatusFromPhase('BLOCKED')).toBe('blocked');
|
|
6
|
+
expect(deriveStatusFromPhase('DONE')).toBe('done');
|
|
7
|
+
});
|
|
8
|
+
it('maps init to idle and active phases to running', () => {
|
|
9
|
+
expect(deriveStatusFromPhase('INIT')).toBe('idle');
|
|
10
|
+
expect(deriveStatusFromPhase('WRITING_TESTS')).toBe('running');
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createContext } from '../../src/cli/context/create-context.js';
|
|
4
|
+
import { analyzeBaseline } from '../../src/core/tools/analyze-baseline.js';
|
|
5
|
+
import { buildFinalReport, reportStatus } from '../../src/core/reporter/index.js';
|
|
6
|
+
const fixture = path.resolve('tests/fixtures/simple-project');
|
|
7
|
+
describe('integration: status/report', () => {
|
|
8
|
+
it('builds status and report', async () => {
|
|
9
|
+
const ctx = await createContext({
|
|
10
|
+
projectPath: fixture,
|
|
11
|
+
coverageTarget: 80
|
|
12
|
+
});
|
|
13
|
+
await analyzeBaseline(ctx);
|
|
14
|
+
const status = await reportStatus(ctx);
|
|
15
|
+
expect(status.ok).toBe(true);
|
|
16
|
+
const report = await buildFinalReport(ctx);
|
|
17
|
+
await ctx.reportStore.saveFinalReport(report);
|
|
18
|
+
const loaded = await ctx.reportStore.loadFinalReport();
|
|
19
|
+
expect(loaded).toBeTruthy();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# 架构概览
|
|
2
|
+
|
|
3
|
+
本工具采用 **Claude CLI-first** 的轻量架构:
|
|
4
|
+
|
|
5
|
+
1. **Core 层**:提供仓库扫描、候选筛选、命令探测、验证与报告等能力。
|
|
6
|
+
2. **CLI 层**:作为薄适配层,解析参数、构造上下文并触发 Claude CLI。
|
|
7
|
+
|
|
8
|
+
核心原则:
|
|
9
|
+
- 以 Claude CLI 直接在工作区补测为主;本工具只提供辅助与校验
|
|
10
|
+
- 规则筛选替代重编排与多轮状态机
|
|
11
|
+
- 命令与结果保持简洁、可读、可复用
|
|
12
|
+
|
|
13
|
+
关键路径:
|
|
14
|
+
- analyze/recommend → run → verify → status/report
|
|
15
|
+
|
|
16
|
+
目录约定:
|
|
17
|
+
- `src/core/`:核心能力
|
|
18
|
+
- `src/cli/`:命令入口
|
|
19
|
+
- `src/types/`:类型与协议
|
|
20
|
+
- `src/utils/`:工具层
|