@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,101 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { buildPaths } from '../../utils/paths.js';
|
|
3
|
+
import { createFileSystem } from '../../utils/fs.js';
|
|
4
|
+
import { createClock } from '../../utils/clock.js';
|
|
5
|
+
import { createCommandRunner } from '../../utils/command-runner.js';
|
|
6
|
+
import { createStateStore } from '../../core/storage/state-store.js';
|
|
7
|
+
import { createReportStore } from '../../core/storage/report-store.js';
|
|
8
|
+
import { createEventStore } from '../../core/storage/event-store.js';
|
|
9
|
+
import { createLifecycleStore } from '../../core/storage/lifecycle-store.js';
|
|
10
|
+
import { createLogger } from '../../utils/logger.js';
|
|
11
|
+
import { buildCommandSet } from '../../utils/commands.js';
|
|
12
|
+
import { buildPromptBuilders } from '../../utils/prompts.js';
|
|
13
|
+
export async function createContext(input) {
|
|
14
|
+
const projectPath = input.projectPath;
|
|
15
|
+
const paths = buildPaths(projectPath);
|
|
16
|
+
const fileSystem = createFileSystem();
|
|
17
|
+
const persistedConfig = await fileSystem.readJson(paths.configPath);
|
|
18
|
+
const config = await loadConfig(projectPath, input.coverageTarget, persistedConfig, input.configOverrides);
|
|
19
|
+
const projectPackageJson = await fileSystem.readJson(path.join(projectPath, 'package.json'));
|
|
20
|
+
const logger = createLogger(config.logLevel ?? 'info');
|
|
21
|
+
const clock = createClock();
|
|
22
|
+
const commandRunner = createCommandRunner({ cwd: projectPath });
|
|
23
|
+
const commands = buildCommandSet(config, projectPackageJson?.scripts ?? {});
|
|
24
|
+
const prompts = buildPromptBuilders(config.promptOverrides);
|
|
25
|
+
const limits = {
|
|
26
|
+
maxIterationsPerRun: config.maxIterationsPerRun ?? 5,
|
|
27
|
+
maxRetryPerTask: config.maxRetryPerTask ?? 2,
|
|
28
|
+
loopThreshold: config.loopThreshold ?? 2,
|
|
29
|
+
concurrency: config.concurrency ?? 8,
|
|
30
|
+
llmTopN: config.llmTopN ?? 20,
|
|
31
|
+
llmAdjustRange: config.llmAdjustRange ?? 10
|
|
32
|
+
};
|
|
33
|
+
const initialState = {
|
|
34
|
+
phase: 'INIT',
|
|
35
|
+
status: 'idle',
|
|
36
|
+
totals: {
|
|
37
|
+
pending: 0,
|
|
38
|
+
running: 0,
|
|
39
|
+
passed: 0,
|
|
40
|
+
failed: 0,
|
|
41
|
+
blocked: 0
|
|
42
|
+
},
|
|
43
|
+
lastAction: 'init',
|
|
44
|
+
nextSuggestedAction: 'analyze',
|
|
45
|
+
updatedAt: clock.nowIso()
|
|
46
|
+
};
|
|
47
|
+
const cache = {};
|
|
48
|
+
const runId = buildRunId(clock.nowIso());
|
|
49
|
+
return {
|
|
50
|
+
projectPath,
|
|
51
|
+
coverageTarget: config.coverageTarget,
|
|
52
|
+
mode: config.mode,
|
|
53
|
+
logger,
|
|
54
|
+
clock,
|
|
55
|
+
fileSystem,
|
|
56
|
+
commandRunner,
|
|
57
|
+
config,
|
|
58
|
+
paths,
|
|
59
|
+
commands,
|
|
60
|
+
prompts,
|
|
61
|
+
limits,
|
|
62
|
+
cache,
|
|
63
|
+
runId,
|
|
64
|
+
stateStore: createStateStore(paths, fileSystem, initialState),
|
|
65
|
+
reportStore: createReportStore(paths, fileSystem),
|
|
66
|
+
eventStore: createEventStore(paths, fileSystem),
|
|
67
|
+
lifecycleStore: createLifecycleStore(paths, fileSystem)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function buildRunId(timestamp) {
|
|
71
|
+
const compact = timestamp.replace(/[-:]/g, '').replace(/\..+$/, '');
|
|
72
|
+
return `r${compact}Z`;
|
|
73
|
+
}
|
|
74
|
+
async function loadConfig(projectPath, coverageTarget, persisted, overrides) {
|
|
75
|
+
const defaultConfig = {
|
|
76
|
+
projectPath,
|
|
77
|
+
coverageTarget,
|
|
78
|
+
include: ['src/**/*'],
|
|
79
|
+
exclude: ['node_modules/**', 'dist/**', 'coverage/**'],
|
|
80
|
+
mode: 'serial',
|
|
81
|
+
concurrency: 1,
|
|
82
|
+
maxIterationsPerRun: 5,
|
|
83
|
+
maxRetryPerTask: 2,
|
|
84
|
+
loopThreshold: 2,
|
|
85
|
+
allowSourceEdit: false,
|
|
86
|
+
testDirNames: ['__test__', '__tests__'],
|
|
87
|
+
commandOverrides: {},
|
|
88
|
+
priorityPaths: [],
|
|
89
|
+
deferredPaths: [],
|
|
90
|
+
excludePaths: [],
|
|
91
|
+
llmTopN: 20,
|
|
92
|
+
llmAdjustRange: 10
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
...defaultConfig,
|
|
96
|
+
...persisted,
|
|
97
|
+
...overrides,
|
|
98
|
+
projectPath,
|
|
99
|
+
coverageTarget: overrides?.coverageTarget ?? persisted?.coverageTarget ?? coverageTarget
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { registerInitCommand } from './commands/init.js';
|
|
3
|
+
import { registerAnalyzeCommand } from './commands/analyze.js';
|
|
4
|
+
import { registerVerifyCommand } from './commands/verify.js';
|
|
5
|
+
import { registerStatusCommand } from './commands/status.js';
|
|
6
|
+
import { registerStartCommand } from './commands/start.js';
|
|
7
|
+
import { registerRunCommand } from './commands/run.js';
|
|
8
|
+
import { registerGuardCommand } from './commands/guard.js';
|
|
9
|
+
import { registerScheduleCommand } from './commands/schedule.js';
|
|
10
|
+
const program = new Command();
|
|
11
|
+
program
|
|
12
|
+
.name('testbot')
|
|
13
|
+
.description('Unit test auto-completion tool')
|
|
14
|
+
.version('0.1.0');
|
|
15
|
+
registerInitCommand(program);
|
|
16
|
+
registerAnalyzeCommand(program);
|
|
17
|
+
registerRunCommand(program);
|
|
18
|
+
registerVerifyCommand(program);
|
|
19
|
+
registerStatusCommand(program);
|
|
20
|
+
registerStartCommand(program);
|
|
21
|
+
registerGuardCommand(program);
|
|
22
|
+
registerScheduleCommand(program);
|
|
23
|
+
program.parseAsync(process.argv);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export async function dependencyComplexityAnalyzer(ctx, candidate) {
|
|
2
|
+
const content = await ctx.fileSystem.readText(`${ctx.projectPath}/${candidate.targetFile}`);
|
|
3
|
+
if (!content) {
|
|
4
|
+
return {
|
|
5
|
+
importCount: 0,
|
|
6
|
+
externalDependencyCount: 0,
|
|
7
|
+
complexityLevel: 'low'
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
const importLines = content.split('\n').filter((line) => line.startsWith('import') || line.includes('require('));
|
|
11
|
+
const importCount = importLines.length;
|
|
12
|
+
const externalDependencyCount = importLines.filter((line) => line.includes("from '") || line.includes('require(')).length;
|
|
13
|
+
const complexityLevel = importCount > 15 ? 'high' : importCount > 5 ? 'medium' : 'low';
|
|
14
|
+
return {
|
|
15
|
+
importCount,
|
|
16
|
+
externalDependencyCount,
|
|
17
|
+
complexityLevel: complexityLevel
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fg from 'fast-glob';
|
|
3
|
+
export async function existingTestAnalyzer(ctx, candidate) {
|
|
4
|
+
const base = path.basename(candidate.targetFile, path.extname(candidate.targetFile));
|
|
5
|
+
const testFilesByBaseName = await getTestFilesByBaseName(ctx);
|
|
6
|
+
const matches = testFilesByBaseName[base] ?? [];
|
|
7
|
+
const relatedMatches = matches.filter((file) => isRelatedTestPath(candidate.targetFile, file));
|
|
8
|
+
return {
|
|
9
|
+
hasExistingTest: relatedMatches.length > 0,
|
|
10
|
+
testFilePaths: relatedMatches,
|
|
11
|
+
partialCoverageLikely: relatedMatches.length > 0
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function isRelatedTestPath(targetFile, testFile) {
|
|
15
|
+
const normalizedTarget = normalizePath(targetFile);
|
|
16
|
+
const normalizedTest = normalizePath(testFile);
|
|
17
|
+
const targetDir = path.dirname(normalizedTarget);
|
|
18
|
+
const targetDirName = path.basename(targetDir);
|
|
19
|
+
const targetBase = path.basename(normalizedTarget, path.extname(normalizedTarget));
|
|
20
|
+
const testBase = path.basename(normalizedTest, path.extname(normalizedTest));
|
|
21
|
+
if (testBase === targetBase || testBase === `${targetBase}.test` || testBase === `${targetBase}.spec`)
|
|
22
|
+
return true;
|
|
23
|
+
if (normalizedTest.startsWith(`${targetDir}/`))
|
|
24
|
+
return true;
|
|
25
|
+
if (normalizedTest.includes(`/${targetDirName}/`))
|
|
26
|
+
return true;
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
function normalizePath(filePath) {
|
|
30
|
+
return filePath.replace(/\\/g, '/');
|
|
31
|
+
}
|
|
32
|
+
async function getTestFilesByBaseName(ctx) {
|
|
33
|
+
if (ctx.cache.testFilesByBaseName)
|
|
34
|
+
return ctx.cache.testFilesByBaseName;
|
|
35
|
+
const testDirNames = await detectTestDirNames(ctx);
|
|
36
|
+
const patterns = [
|
|
37
|
+
'**/*.test.*',
|
|
38
|
+
'**/*.spec.*',
|
|
39
|
+
...testDirNames.map((dir) => `**/${dir}/**/*.*`)
|
|
40
|
+
];
|
|
41
|
+
const files = await fg(patterns, { cwd: ctx.projectPath, ignore: ctx.config.exclude, onlyFiles: true });
|
|
42
|
+
const index = {};
|
|
43
|
+
for (const file of files) {
|
|
44
|
+
const base = path.basename(file, path.extname(file));
|
|
45
|
+
if (!index[base])
|
|
46
|
+
index[base] = [];
|
|
47
|
+
index[base].push(file);
|
|
48
|
+
}
|
|
49
|
+
ctx.cache.testFilesByBaseName = index;
|
|
50
|
+
return index;
|
|
51
|
+
}
|
|
52
|
+
async function detectTestDirNames(ctx) {
|
|
53
|
+
if (ctx.cache.detectedTestDirNames)
|
|
54
|
+
return ctx.cache.detectedTestDirNames;
|
|
55
|
+
const configured = ctx.config.testDirNames?.filter(Boolean) ?? ['__test__', '__tests__'];
|
|
56
|
+
const jestConfigPath = path.join(ctx.projectPath, 'jest.config.js');
|
|
57
|
+
const jestConfig = await ctx.fileSystem.readText(jestConfigPath);
|
|
58
|
+
if (!jestConfig) {
|
|
59
|
+
ctx.cache.detectedTestDirNames = configured;
|
|
60
|
+
return configured;
|
|
61
|
+
}
|
|
62
|
+
const detected = configured.filter((dir) => jestConfig.includes(dir));
|
|
63
|
+
const result = detected.length > 0 ? detected : configured;
|
|
64
|
+
ctx.cache.detectedTestDirNames = result;
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
export async function fileClassifierAnalyzer(_ctx, candidate) {
|
|
3
|
+
const ext = path.extname(candidate.targetFile);
|
|
4
|
+
const lower = candidate.targetFile.toLowerCase();
|
|
5
|
+
const uiSignal = /component|view|widget/.test(lower);
|
|
6
|
+
const domSignal = /dom|browser/.test(lower);
|
|
7
|
+
const frameworkSignal = /tsx?$/.test(ext);
|
|
8
|
+
let ruleCategory = 'unknown';
|
|
9
|
+
if (/util|helper|math|logic|transform/.test(lower))
|
|
10
|
+
ruleCategory = 'pure-logic';
|
|
11
|
+
else if (/service|api/.test(lower))
|
|
12
|
+
ruleCategory = 'business-logic';
|
|
13
|
+
else if (uiSignal)
|
|
14
|
+
ruleCategory = 'ui-heavy';
|
|
15
|
+
else if (/model|mapper|serializer/.test(lower))
|
|
16
|
+
ruleCategory = 'data-transform';
|
|
17
|
+
return {
|
|
18
|
+
ruleCategory,
|
|
19
|
+
semanticCategory: ruleCategory,
|
|
20
|
+
uiSignal,
|
|
21
|
+
domSignal,
|
|
22
|
+
frameworkSignal
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { coverageAnalyzer } from './coverage-analyzer.js';
|
|
2
|
+
import { pathPriorityAnalyzer } from './path-priority-analyzer.js';
|
|
3
|
+
import { fileClassifierAnalyzer } from './file-classifier-analyzer.js';
|
|
4
|
+
import { dependencyComplexityAnalyzer } from './dependency-complexity-analyzer.js';
|
|
5
|
+
import { existingTestAnalyzer } from './existing-test-analyzer.js';
|
|
6
|
+
import { failureHistoryAnalyzer } from './failure-history-analyzer.js';
|
|
7
|
+
import { llmSemanticAnalyzer } from './llm-semantic-analyzer.js';
|
|
8
|
+
export async function runAnalyzers(ctx, candidate, coverageSnapshot) {
|
|
9
|
+
const warnings = [];
|
|
10
|
+
const coverage = coverageAnalyzer(candidate, coverageSnapshot);
|
|
11
|
+
const pathPriority = pathPriorityAnalyzer(candidate, ctx.config);
|
|
12
|
+
const existingTest = await existingTestAnalyzer(ctx, candidate);
|
|
13
|
+
const fileClassification = await fileClassifierAnalyzer(ctx, candidate);
|
|
14
|
+
const dependencyComplexity = await dependencyComplexityAnalyzer(ctx, candidate);
|
|
15
|
+
const failureHistory = await failureHistoryAnalyzer(ctx, candidate);
|
|
16
|
+
let llmSemantic;
|
|
17
|
+
try {
|
|
18
|
+
llmSemantic = await llmSemanticAnalyzer(ctx, candidate);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
warnings.push(`llmSemanticAnalyzer failed: ${String(error)}`);
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
facts: {
|
|
25
|
+
targetFile: candidate.targetFile,
|
|
26
|
+
coverage,
|
|
27
|
+
pathPriority,
|
|
28
|
+
existingTest,
|
|
29
|
+
fileClassification,
|
|
30
|
+
dependencyComplexity,
|
|
31
|
+
failureHistory,
|
|
32
|
+
llmSemantic,
|
|
33
|
+
analyzerWarnings: warnings
|
|
34
|
+
},
|
|
35
|
+
warnings
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function pathPriorityAnalyzer(candidate, config) {
|
|
2
|
+
const priorityPaths = config.priorityPaths ?? [];
|
|
3
|
+
const deferredPaths = config.deferredPaths ?? [];
|
|
4
|
+
const excludePaths = config.excludePaths ?? [];
|
|
5
|
+
const target = candidate.targetFile;
|
|
6
|
+
const matchedExclude = excludePaths.find((p) => target.startsWith(p));
|
|
7
|
+
if (matchedExclude) {
|
|
8
|
+
return {
|
|
9
|
+
label: 'excluded',
|
|
10
|
+
score: -999,
|
|
11
|
+
matchedPath: matchedExclude
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const matchedPriority = priorityPaths.find((p) => target.startsWith(p));
|
|
15
|
+
if (matchedPriority) {
|
|
16
|
+
return {
|
|
17
|
+
label: 'priority',
|
|
18
|
+
score: 100,
|
|
19
|
+
matchedPath: matchedPriority
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const matchedDeferred = deferredPaths.find((p) => target.startsWith(p));
|
|
23
|
+
if (matchedDeferred) {
|
|
24
|
+
return {
|
|
25
|
+
label: 'deferred',
|
|
26
|
+
score: -100,
|
|
27
|
+
matchedPath: matchedDeferred
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
label: 'normal',
|
|
32
|
+
score: 0
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
const DEFAULT_COVERAGE_FILES = [
|
|
3
|
+
path.join('coverage', 'coverage-summary.json'),
|
|
4
|
+
path.join('coverage', 'coverage-final.json'),
|
|
5
|
+
'coverage-summary.json',
|
|
6
|
+
'coverage-final.json'
|
|
7
|
+
];
|
|
8
|
+
const EXCLUDED_DIRS = new Set(['node_modules', '.git', '.unit_test_tool_workspace']);
|
|
9
|
+
export async function runAndReadCoverage(ctx) {
|
|
10
|
+
ctx.logger.info(`[coverage] step=run status=start cmd=${ctx.commands.analyzeBaseline}`);
|
|
11
|
+
const result = await ctx.commandRunner.run(ctx.commands.analyzeBaseline);
|
|
12
|
+
ctx.logger.info(`[coverage] step=run status=done exit=${result.exitCode}`);
|
|
13
|
+
if (result.exitCode !== 0) {
|
|
14
|
+
const stderr = result.stderr?.trim();
|
|
15
|
+
const stdout = result.stdout?.trim();
|
|
16
|
+
const message = stderr || stdout || 'coverage command failed';
|
|
17
|
+
throw new Error(message);
|
|
18
|
+
}
|
|
19
|
+
const summary = await readCoverageSummary(ctx);
|
|
20
|
+
if (!summary) {
|
|
21
|
+
const searched = await listCoverageSearchPaths(ctx);
|
|
22
|
+
throw new Error(`coverage summary not found after command: ${ctx.commands.analyzeBaseline}. searched=${searched.join(', ') || 'none'}`);
|
|
23
|
+
}
|
|
24
|
+
return summary;
|
|
25
|
+
}
|
|
26
|
+
export async function readCoverageSummary(ctx) {
|
|
27
|
+
const searchPaths = await listCoverageSearchPaths(ctx);
|
|
28
|
+
for (const relativePath of searchPaths) {
|
|
29
|
+
const fullPath = path.join(ctx.projectPath, relativePath);
|
|
30
|
+
const data = await ctx.fileSystem.readJson(fullPath);
|
|
31
|
+
if (!data)
|
|
32
|
+
continue;
|
|
33
|
+
const snapshot = parseCoverageSummary(data);
|
|
34
|
+
if (snapshot)
|
|
35
|
+
return snapshot;
|
|
36
|
+
const finalSnapshot = parseCoverageFinal(data);
|
|
37
|
+
if (finalSnapshot)
|
|
38
|
+
return finalSnapshot;
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
async function listCoverageSearchPaths(ctx) {
|
|
43
|
+
const discovered = await findCoverageFiles(ctx.projectPath, '.');
|
|
44
|
+
return Array.from(new Set([...DEFAULT_COVERAGE_FILES, ...discovered]));
|
|
45
|
+
}
|
|
46
|
+
async function findCoverageFiles(projectPath, relativeDir) {
|
|
47
|
+
const fs = await import('node:fs/promises');
|
|
48
|
+
const dirPath = path.join(projectPath, relativeDir);
|
|
49
|
+
let entries;
|
|
50
|
+
try {
|
|
51
|
+
entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
const matches = [];
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
if (entry.isDirectory()) {
|
|
59
|
+
if (EXCLUDED_DIRS.has(entry.name))
|
|
60
|
+
continue;
|
|
61
|
+
matches.push(...await findCoverageFiles(projectPath, path.join(relativeDir, entry.name)));
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!entry.isFile())
|
|
65
|
+
continue;
|
|
66
|
+
if (entry.name !== 'coverage-summary.json' && entry.name !== 'coverage-final.json')
|
|
67
|
+
continue;
|
|
68
|
+
matches.push(path.join(relativeDir, entry.name).replace(/^\.\//, ''));
|
|
69
|
+
}
|
|
70
|
+
return matches;
|
|
71
|
+
}
|
|
72
|
+
function parseCoverageSummary(summary) {
|
|
73
|
+
const totalEntry = summary.total;
|
|
74
|
+
if (!totalEntry)
|
|
75
|
+
return undefined;
|
|
76
|
+
const totals = totalEntry.total ?? totalEntry;
|
|
77
|
+
const snapshot = {
|
|
78
|
+
lines: totals.lines?.pct,
|
|
79
|
+
statements: totals.statements?.pct,
|
|
80
|
+
functions: totals.functions?.pct,
|
|
81
|
+
branches: totals.branches?.pct,
|
|
82
|
+
rawSummary: JSON.stringify(totals)
|
|
83
|
+
};
|
|
84
|
+
const fileEntries = {};
|
|
85
|
+
for (const [key, entry] of Object.entries(summary)) {
|
|
86
|
+
if (key === 'total')
|
|
87
|
+
continue;
|
|
88
|
+
const fileEntry = entry;
|
|
89
|
+
fileEntries[key] = {
|
|
90
|
+
lines: fileEntry.lines?.pct,
|
|
91
|
+
statements: fileEntry.statements?.pct,
|
|
92
|
+
functions: fileEntry.functions?.pct,
|
|
93
|
+
branches: fileEntry.branches?.pct
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
snapshot.fileEntries = fileEntries;
|
|
97
|
+
return snapshot;
|
|
98
|
+
}
|
|
99
|
+
function parseCoverageFinal(finalReport) {
|
|
100
|
+
const summary = summarizeCoverageFinal(finalReport);
|
|
101
|
+
if (!summary)
|
|
102
|
+
return undefined;
|
|
103
|
+
return {
|
|
104
|
+
lines: summary.lines,
|
|
105
|
+
statements: summary.statements,
|
|
106
|
+
functions: summary.functions,
|
|
107
|
+
branches: summary.branches,
|
|
108
|
+
rawSummary: JSON.stringify(summary),
|
|
109
|
+
fileEntries: summary.fileEntries
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function summarizeCoverageFinal(finalReport) {
|
|
113
|
+
const files = Object.entries(finalReport).filter(([key]) => key !== 'total');
|
|
114
|
+
if (files.length === 0)
|
|
115
|
+
return undefined;
|
|
116
|
+
let totalStatements = 0;
|
|
117
|
+
let coveredStatements = 0;
|
|
118
|
+
let totalFunctions = 0;
|
|
119
|
+
let coveredFunctions = 0;
|
|
120
|
+
let totalBranches = 0;
|
|
121
|
+
let coveredBranches = 0;
|
|
122
|
+
let totalLines = 0;
|
|
123
|
+
let coveredLines = 0;
|
|
124
|
+
const fileEntries = {};
|
|
125
|
+
for (const [filePath, entry] of files) {
|
|
126
|
+
const statement = summarizeRecord(entry.s);
|
|
127
|
+
const fn = summarizeRecord(entry.f);
|
|
128
|
+
const branch = summarizeBranches(entry.b);
|
|
129
|
+
const line = summarizeRecord(entry.l);
|
|
130
|
+
totalStatements += statement.total;
|
|
131
|
+
coveredStatements += statement.covered;
|
|
132
|
+
totalFunctions += fn.total;
|
|
133
|
+
coveredFunctions += fn.covered;
|
|
134
|
+
totalBranches += branch.total;
|
|
135
|
+
coveredBranches += branch.covered;
|
|
136
|
+
totalLines += line.total;
|
|
137
|
+
coveredLines += line.covered;
|
|
138
|
+
fileEntries[filePath] = {
|
|
139
|
+
statements: toPct(statement.covered, statement.total),
|
|
140
|
+
functions: toPct(fn.covered, fn.total),
|
|
141
|
+
branches: toPct(branch.covered, branch.total),
|
|
142
|
+
lines: toPct(line.covered, line.total)
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
statements: toPct(coveredStatements, totalStatements),
|
|
147
|
+
functions: toPct(coveredFunctions, totalFunctions),
|
|
148
|
+
branches: toPct(coveredBranches, totalBranches),
|
|
149
|
+
lines: toPct(coveredLines, totalLines),
|
|
150
|
+
fileEntries
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function summarizeRecord(record) {
|
|
154
|
+
if (!record)
|
|
155
|
+
return { total: 0, covered: 0 };
|
|
156
|
+
let total = 0;
|
|
157
|
+
let covered = 0;
|
|
158
|
+
for (const value of Object.values(record)) {
|
|
159
|
+
total += 1;
|
|
160
|
+
if (value > 0)
|
|
161
|
+
covered += 1;
|
|
162
|
+
}
|
|
163
|
+
return { total, covered };
|
|
164
|
+
}
|
|
165
|
+
function summarizeBranches(record) {
|
|
166
|
+
if (!record)
|
|
167
|
+
return { total: 0, covered: 0 };
|
|
168
|
+
let total = 0;
|
|
169
|
+
let covered = 0;
|
|
170
|
+
for (const values of Object.values(record)) {
|
|
171
|
+
total += values.length;
|
|
172
|
+
for (const value of values) {
|
|
173
|
+
if (value > 0)
|
|
174
|
+
covered += 1;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return { total, covered };
|
|
178
|
+
}
|
|
179
|
+
function toPct(covered, total) {
|
|
180
|
+
if (total === 0)
|
|
181
|
+
return undefined;
|
|
182
|
+
return Math.round((covered / total) * 10000) / 100;
|
|
183
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
export async function executeClaudeCli(input) {
|
|
5
|
+
const startedAt = Date.now();
|
|
6
|
+
const heartbeatMs = 10000;
|
|
7
|
+
let heartbeat;
|
|
8
|
+
try {
|
|
9
|
+
if (input.onProgress) {
|
|
10
|
+
heartbeat = setInterval(() => {
|
|
11
|
+
input.onProgress?.(Date.now() - startedAt);
|
|
12
|
+
}, heartbeatMs);
|
|
13
|
+
}
|
|
14
|
+
const args = ['-p', input.prompt];
|
|
15
|
+
if (input.outputFormat)
|
|
16
|
+
args.push('--output-format', input.outputFormat);
|
|
17
|
+
if (input.systemPrompt)
|
|
18
|
+
args.push('--system-prompt', input.systemPrompt);
|
|
19
|
+
if (input.appendSystemPrompt)
|
|
20
|
+
args.push('--append-system-prompt', input.appendSystemPrompt);
|
|
21
|
+
if (input.allowedTools?.length)
|
|
22
|
+
args.push('--allowedTools', input.allowedTools.join(','));
|
|
23
|
+
if (input.permissionMode)
|
|
24
|
+
args.push('--permission-mode', input.permissionMode);
|
|
25
|
+
if (input.timeoutMs)
|
|
26
|
+
args.push('--timeout', String(input.timeoutMs));
|
|
27
|
+
const result = await execa('claude', args, {
|
|
28
|
+
cwd: input.cwd,
|
|
29
|
+
reject: false,
|
|
30
|
+
timeout: input.timeoutMs ?? 120000,
|
|
31
|
+
stdin: 'ignore',
|
|
32
|
+
env: await buildClaudeEnv(input.cwd)
|
|
33
|
+
});
|
|
34
|
+
if (heartbeat)
|
|
35
|
+
clearInterval(heartbeat);
|
|
36
|
+
return {
|
|
37
|
+
ok: result.exitCode === 0,
|
|
38
|
+
exitCode: result.exitCode ?? null,
|
|
39
|
+
stdout: result.stdout,
|
|
40
|
+
stderr: result.stderr,
|
|
41
|
+
durationMs: Date.now() - startedAt,
|
|
42
|
+
taskMeta: input.taskMeta,
|
|
43
|
+
failureCategory: result.exitCode === 0 ? undefined : 'CLI_EXIT_NON_ZERO'
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (heartbeat)
|
|
48
|
+
clearInterval(heartbeat);
|
|
49
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
50
|
+
const category = message.includes('ENOENT')
|
|
51
|
+
? 'CLI_NOT_FOUND'
|
|
52
|
+
: message.toLowerCase().includes('timed out')
|
|
53
|
+
? 'CLI_TIMEOUT'
|
|
54
|
+
: 'RUNTIME_ENV_ERROR';
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
exitCode: null,
|
|
58
|
+
stdout: '',
|
|
59
|
+
stderr: message,
|
|
60
|
+
durationMs: Date.now() - startedAt,
|
|
61
|
+
failureCategory: category,
|
|
62
|
+
taskMeta: input.taskMeta
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function buildClaudeEnv(cwd) {
|
|
67
|
+
const projectEnv = await loadProjectClaudeEnv(cwd);
|
|
68
|
+
return {
|
|
69
|
+
...process.env,
|
|
70
|
+
...(process.env.ANTHROPIC_BASE_URL ? { ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL } : {}),
|
|
71
|
+
...(process.env.ANTHROPIC_AUTH_TOKEN ? { ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_TOKEN } : {}),
|
|
72
|
+
...projectEnv
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async function loadProjectClaudeEnv(cwd) {
|
|
76
|
+
try {
|
|
77
|
+
const configPath = path.join(cwd, '.claude', 'settings.local.json');
|
|
78
|
+
const raw = await fs.readFile(configPath, 'utf-8');
|
|
79
|
+
const parsed = JSON.parse(raw);
|
|
80
|
+
const env = parsed?.env ?? {};
|
|
81
|
+
const result = {};
|
|
82
|
+
if (env.ANTHROPIC_BASE_URL)
|
|
83
|
+
result.ANTHROPIC_BASE_URL = env.ANTHROPIC_BASE_URL;
|
|
84
|
+
if (env.ANTHROPIC_AUTH_TOKEN)
|
|
85
|
+
result.ANTHROPIC_AUTH_TOKEN = env.ANTHROPIC_AUTH_TOKEN;
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export async function preCompletionChecklist(ctx) {
|
|
2
|
+
const lint = await ctx.commandRunner.run(ctx.commands.lint);
|
|
3
|
+
if (lint.exitCode !== 0) {
|
|
4
|
+
return {
|
|
5
|
+
ok: false,
|
|
6
|
+
phase: 'VERIFYING_FULL',
|
|
7
|
+
summary: 'lint 失败',
|
|
8
|
+
nextAction: 'manual-intervention',
|
|
9
|
+
artifacts: { stderr: lint.stderr }
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
const typecheck = await ctx.commandRunner.run(ctx.commands.typecheck);
|
|
13
|
+
if (typecheck.exitCode !== 0) {
|
|
14
|
+
return {
|
|
15
|
+
ok: false,
|
|
16
|
+
phase: 'VERIFYING_FULL',
|
|
17
|
+
summary: 'typecheck 失败',
|
|
18
|
+
nextAction: 'manual-intervention',
|
|
19
|
+
artifacts: { stderr: typecheck.stderr }
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
ok: true,
|
|
24
|
+
phase: 'VERIFYING_FULL',
|
|
25
|
+
summary: 'checklist passed'
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export async function silentSuccessPostCheck(ctx, targetFile) {
|
|
2
|
+
const result = await ctx.commandRunner.run(ctx.commands.incrementalTest(targetFile));
|
|
3
|
+
return {
|
|
4
|
+
ok: result.exitCode === 0,
|
|
5
|
+
phase: 'VERIFYING_FULL',
|
|
6
|
+
summary: result.exitCode === 0 ? 'incremental check passed' : 'incremental check failed',
|
|
7
|
+
nextAction: result.exitCode === 0 ? 'verify' : 'manual-intervention',
|
|
8
|
+
artifacts: {
|
|
9
|
+
stdout: result.stdout,
|
|
10
|
+
stderr: result.stderr
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
}
|