Files
workutils/docs/superpowers/plans/2026-08-04-ai-unit-test-plugin-v1.md
T
2026-08-04 16:30:53 +08:00

25 KiB

AI Unit Test Plugin V1 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build a WorkUtils plugin that generates JUnit 5 tests for JDK 8 Maven projects through codefree-cli, verifies 85% incremental line coverage in an isolated worktree, and safely integrates a test-only commit into a selected local branch.

Architecture: Add a biz.aitest vertical slice with small ports for processes, Git, Maven, coverage, specifications, and AI. AiTestWorkflow coordinates immutable request/result models while AiTestWindow only binds JavaFX controls to workflow operations. Every external command runs through an injectable executor so automated tests remain offline and deterministic.

Tech Stack: Java 17 WorkUtils runtime, JavaFX 17, Spring Boot 2.7, JUnit 5, Maven, Git CLI, JaCoCo XML, codefree-cli JSON protocol.

Global Constraints

  • Execute this plan inline in the primary session; do not use subagents.
  • Preserve the user's untracked files on master; work only in codex/ai-test-plugin-v1.
  • Target projects are JDK 8, Maven, and JUnit 5; WorkUtils itself remains Java 17.
  • AI V1 uses only codefree-cli without --model; retain a provider interface for future providers.
  • Default incremental line coverage gate is exactly 85%; branch coverage is display-only.
  • Auto-select a module only when exactly one Maven artifactId ends with -start; otherwise require user selection.
  • Generate and modify files only below the selected module's src/test/java.
  • Never modify production sources, POM files, Maven settings, or remote Git state.
  • Run generation and validation in a temporary Git worktree; integrate with a test-only commit and local cherry-pick semantics.
  • Use TDD for every production behavior: RED, GREEN, refactor, focused commit.

File Structure

Create the following focused packages under src/main/java/com/example/workutils/biz/aitest/:

  • config: settings persistence and editable test specifications.
  • process: cross-platform, cancellable external command execution.
  • project: project catalog and Maven module/dependency model.
  • git: incremental line analysis, temporary worktrees, branch integration.
  • coverage: JaCoCo XML parsing and incremental coverage calculation.
  • ai: provider request/result contract, CodeFree CLI adapter, generated-file validation.
  • maven: Maven commands, Surefire parsing, and error classification.
  • application: workflow request, state, progress, and orchestration.
  • package root: AiTestPlugin and AiTestWindow.

Tests mirror these packages under src/test/java. XML, JSON, POM, and Git fixtures are created inside JUnit @TempDir; no permanent fixture repository is needed.

Task 1: Process execution and global AI-test settings

Files:

  • Create: src/main/java/com/example/workutils/biz/aitest/process/CommandSpec.java
  • Create: src/main/java/com/example/workutils/biz/aitest/process/CommandResult.java
  • Create: src/main/java/com/example/workutils/biz/aitest/process/ProcessExecutor.java
  • Create: src/main/java/com/example/workutils/biz/aitest/process/DefaultProcessExecutor.java
  • Create: src/main/java/com/example/workutils/biz/aitest/config/AiTestSettings.java
  • Create: src/main/java/com/example/workutils/biz/aitest/config/AiTestSettingsService.java
  • Create: src/main/java/com/example/workutils/biz/aitest/config/TestSpecificationService.java
  • Test: src/test/java/com/example/workutils/biz/aitest/process/DefaultProcessExecutorTest.java
  • Test: src/test/java/com/example/workutils/biz/aitest/config/AiTestSettingsServiceTest.java
  • Test: src/test/java/com/example/workutils/biz/aitest/config/TestSpecificationServiceTest.java

Interfaces:

  • ProcessExecutor.execute(CommandSpec, Consumer<String>) -> CommandResult

  • AiTestSettingsService.load()/save(AiTestSettings) persists data/ai-test/settings.properties.

  • TestSpecificationService.loadDefault()/saveDefault()/resetDefault()/loadProject()/saveProject() persists UTF-8 Markdown outside business repositories.

  • Step 1: Write failing process and settings tests

@Test
void injectsEnvironmentAndCapturesUtf8Output(@TempDir Path dir) {
    CommandSpec spec = new CommandSpec(dir, List.of(javaCommand(), "-version"),
            Map.of("AI_TEST_MARKER", "ok"), Duration.ofSeconds(10), null);
    CommandResult result = executor.execute(spec, ignored -> {});
    assertEquals(0, result.exitCode());
    assertFalse(result.timedOut());
}

@Test
void roundTripsGlobalSettings(@TempDir Path home) throws Exception {
    AiTestSettings expected = new AiTestSettings(home.resolve("jdk8"), home.resolve("maven"),
            "codefree", 85, 3, Duration.ofMinutes(5), Duration.ofMinutes(15));
    service(home).save(expected);
    assertEquals(expected, service(home).load());
}
  • Step 2: Run focused tests and verify RED

Run: mvnw.cmd -Dtest=DefaultProcessExecutorTest,AiTestSettingsServiceTest,TestSpecificationServiceTest test

Expected: test compilation fails because the new contracts do not exist.

  • Step 3: Implement immutable command/settings contracts and real execution
public interface ProcessExecutor {
    CommandResult execute(CommandSpec spec, Consumer<String> output);
    void cancelActive();
}

public record CommandResult(int exitCode, boolean timedOut, boolean cancelled,
                            String stdout, String stderr, Duration duration) {
    public boolean successful() { return exitCode == 0 && !timedOut && !cancelled; }
}

DefaultProcessExecutor uses ProcessBuilder, applies only the supplied environment overrides, reads stdout/stderr concurrently as UTF-8, enforces the timeout, and destroys descendants before the parent on cancellation. AiTestSettingsService uses Properties with atomic temp-file replacement. TestSpecificationService creates a Chinese JUnit 5 default template on first read and uses a stable SHA-256 project-path identifier.

  • Step 4: Run focused tests and full regression tests

Run the focused command above, then mvnw.cmd test with JDK 17.

Expected: all tests pass.

  • Step 5: Commit
git commit -m "feat: add AI test process and settings foundation"

Task 2: Project catalog and Maven module selection

Files:

  • Create: src/main/java/com/example/workutils/biz/aitest/project/AiTestProject.java
  • Create: src/main/java/com/example/workutils/biz/aitest/project/MavenModule.java
  • Create: src/main/java/com/example/workutils/biz/aitest/project/MavenProjectModel.java
  • Create: src/main/java/com/example/workutils/biz/aitest/project/AiTestProjectCatalog.java
  • Create: src/main/java/com/example/workutils/biz/aitest/project/MavenProjectAnalyzer.java
  • Create: src/main/java/com/example/workutils/biz/aitest/project/StartModuleSelector.java
  • Test: src/test/java/com/example/workutils/biz/aitest/project/MavenProjectAnalyzerTest.java
  • Test: src/test/java/com/example/workutils/biz/aitest/project/StartModuleSelectorTest.java

Interfaces:

  • AiTestProjectCatalog.load() -> List<AiTestProject> reuses FileOperationHelper project status data.

  • MavenProjectAnalyzer.analyze(Path) -> MavenProjectModel recursively resolves reactor modules and direct reactor dependencies.

  • StartModuleSelector.autoSelect(model) -> Optional<MavenModule> returns a module only for exactly one artifactId.endsWith("-start").

  • Step 1: Write failing module parsing and selection tests

@Test
void selectsOnlyUniqueStartArtifact(@TempDir Path root) {
    MavenProjectModel model = fixture(root, "order-core", "order-start");
    assertEquals("order-start", selector.autoSelect(model).orElseThrow().artifactId());
}

@Test
void requiresChoiceForNoOrMultipleStartArtifacts(@TempDir Path root) {
    assertTrue(selector.autoSelect(fixture(root, "order-core")).isEmpty());
    assertTrue(selector.autoSelect(fixture(root, "a-start", "b-start")).isEmpty());
}
  • Step 2: Verify RED with focused Maven tests

Run: mvnw.cmd -Dtest=MavenProjectAnalyzerTest,StartModuleSelectorTest test

Expected: compilation fails for missing project model types.

  • Step 3: Implement secure POM parsing and module graph

Use namespace-aware JAXP with external entities and DTDs disabled. Inherit missing group/version from the reactor parent, normalize module paths under the project root, and reject module paths that escape it. Represent dependencies by reactor groupId:artifactId; expose canReach(selected, changed) through graph traversal.

  • Step 4: Verify unique selection, malformed POM, path escape, and dependency reachability

Run focused tests, then full mvnw.cmd test.

  • Step 5: Commit
git commit -m "feat: analyze Maven projects for AI tests"

Task 3: Git incremental lines and JaCoCo coverage

Files:

  • Create: src/main/java/com/example/workutils/biz/aitest/git/ChangedLines.java
  • Create: src/main/java/com/example/workutils/biz/aitest/git/IncrementAnalysis.java
  • Create: src/main/java/com/example/workutils/biz/aitest/git/GitIncrementAnalyzer.java
  • Create: src/main/java/com/example/workutils/biz/aitest/coverage/JacocoLine.java
  • Create: src/main/java/com/example/workutils/biz/aitest/coverage/JacocoXmlParser.java
  • Create: src/main/java/com/example/workutils/biz/aitest/coverage/IncrementalCoverage.java
  • Create: src/main/java/com/example/workutils/biz/aitest/coverage/IncrementalCoverageCalculator.java
  • Test: src/test/java/com/example/workutils/biz/aitest/git/GitIncrementAnalyzerTest.java
  • Test: src/test/java/com/example/workutils/biz/aitest/coverage/JacocoXmlParserTest.java
  • Test: src/test/java/com/example/workutils/biz/aitest/coverage/IncrementalCoverageCalculatorTest.java

Interfaces:

  • GitIncrementAnalyzer.analyze(project, baseline, includeWorkingTree) -> IncrementAnalysis

  • JacocoXmlParser.parse(module, xml) -> List<JacocoLine>

  • IncrementalCoverageCalculator.calculate(analysis, lines, targetPercent) -> IncrementalCoverage

  • Step 1: Write failing tests using a real temporary Git repository and literal JaCoCo XML

@Test
void countsOnlyChangedExecutableCoveredLines() {
    IncrementAnalysis diff = analysis("src/main/java/a/A.java", Set.of(10, 11, 12));
    List<JacocoLine> report = List.of(line(10, 0, 1), line(11, 1, 0));
    IncrementalCoverage result = calculator.calculate(diff, report, 85);
    assertEquals(2, result.executableLines());
    assertEquals(1, result.coveredLines());
    assertEquals(new BigDecimal("50.00"), result.linePercent());
    assertFalse(result.passed());
}
  • Step 2: Verify RED

Run: mvnw.cmd -Dtest=GitIncrementAnalyzerTest,JacocoXmlParserTest,IncrementalCoverageCalculatorTest test

  • Step 3: Implement merge-base diff parsing and coverage mapping

Run Git only through ProcessExecutor: resolve baseline^{commit}, calculate merge-base, parse git diff --unified=0 --no-color, and optionally merge cached/unstaged diffs. Parse JaCoCo package/sourcefile/line entries; executable means mi + ci > 0, covered means ci > 0. Use module plus normalized source path as the key and return N/A when no executable changed lines exist.

  • Step 4: Run focused and full tests

  • Step 5: Commit

git commit -m "feat: calculate incremental JaCoCo coverage"

Task 4: CodeFree provider and generated-test safety

Files:

  • Create: src/main/java/com/example/workutils/biz/aitest/ai/TestGenerationProvider.java
  • Create: src/main/java/com/example/workutils/biz/aitest/ai/TestGenerationRequest.java
  • Create: src/main/java/com/example/workutils/biz/aitest/ai/TestGenerationResult.java
  • Create: src/main/java/com/example/workutils/biz/aitest/ai/GeneratedTestFile.java
  • Create: src/main/java/com/example/workutils/biz/aitest/ai/CodeFreeResultParser.java
  • Create: src/main/java/com/example/workutils/biz/aitest/ai/CodeFreeCliTestGenerationProvider.java
  • Create: src/main/java/com/example/workutils/biz/aitest/ai/GeneratedFileValidator.java
  • Test: src/test/java/com/example/workutils/biz/aitest/ai/CodeFreeResultParserTest.java
  • Test: src/test/java/com/example/workutils/biz/aitest/ai/CodeFreeCliTestGenerationProviderTest.java
  • Test: src/test/java/com/example/workutils/biz/aitest/ai/GeneratedFileValidatorTest.java

Interfaces:

  • TestGenerationProvider.generate(TestGenerationRequest, Consumer<String>) -> TestGenerationResult

  • CodeFreeResultParser.parse(String cliJson) -> TestGenerationResult

  • GeneratedFileValidator.validateAndResolve(moduleRoot, files) -> List<ResolvedGeneratedFile>

  • Step 1: Write failing protocol and path-security tests

@Test
void extractsLastSuccessfulResultRecord() {
    TestGenerationResult result = parser.parse(cliArrayWithResult("{\"tests\":[]}"));
    assertTrue(result.successful());
}

@Test
void rejectsProductionAndTraversalPaths(@TempDir Path module) {
    assertThrows(IllegalArgumentException.class,
            () -> validator.validateAndResolve(module,
                    List.of(file("../src/main/java/A.java"))));
}
  • Step 2: Verify RED

Run: mvnw.cmd -Dtest=CodeFreeResultParserTest,CodeFreeCliTestGenerationProviderTest,GeneratedFileValidatorTest test

  • Step 3: Implement CodeFree invocation and strict JSON parsing

Build the exact argument vector codefree <prompt> --channel CI --approval-mode plan --max-session-turns 1 --output-format json; never pass --model. Parse the outer JSON array and inner result JSON with Fastjson2, reject Markdown fences and missing fields, normalize every output below <module>/src/test/java, require .java, and reject symlink/path escape after resolving the nearest existing ancestor.

  • Step 4: Run focused and full tests

  • Step 5: Commit

git commit -m "feat: integrate CodeFree test generation"

Task 5: Maven execution, Surefire parsing, and AI repair classification

Files:

  • Create: src/main/java/com/example/workutils/biz/aitest/maven/MavenPhase.java
  • Create: src/main/java/com/example/workutils/biz/aitest/maven/MavenFailureType.java
  • Create: src/main/java/com/example/workutils/biz/aitest/maven/MavenExecutionResult.java
  • Create: src/main/java/com/example/workutils/biz/aitest/maven/SurefireFailure.java
  • Create: src/main/java/com/example/workutils/biz/aitest/maven/SurefireReportParser.java
  • Create: src/main/java/com/example/workutils/biz/aitest/maven/MavenErrorClassifier.java
  • Create: src/main/java/com/example/workutils/biz/aitest/maven/MavenRunner.java
  • Test: src/test/java/com/example/workutils/biz/aitest/maven/SurefireReportParserTest.java
  • Test: src/test/java/com/example/workutils/biz/aitest/maven/MavenErrorClassifierTest.java
  • Test: src/test/java/com/example/workutils/biz/aitest/maven/MavenRunnerTest.java

Interfaces:

  • MavenRunner.run(project, module, phase, settings, listener) -> MavenExecutionResult

  • MavenErrorClassifier.classify(result) -> MavenFailureType

  • Step 1: Write failing error-category tests

@ParameterizedTest
@CsvSource({
    "'JAVA_HOME is not defined correctly',TOOLCHAIN",
    "'Non-resolvable parent POM',DEPENDENCY_OR_POM",
    "'OrderServiceTest.java:[42,28] cannot find symbol',TEST_COMPILATION",
    "'There are test failures',TEST_FAILURE"
})
void classifiesMavenFailures(String log, MavenFailureType expected) {
    assertEquals(expected, classifier.classify(failed(log)));
}
  • Step 2: Verify RED

Run: mvnw.cmd -Dtest=SurefireReportParserTest,MavenErrorClassifierTest,MavenRunnerTest test

  • Step 3: Implement phased Maven commands

Resolve mvn.cmd on Windows and mvn elsewhere from Maven home, inject JDK 8, and run from the project root with -pl <module-relative-path> -am. Implement test-compile, test, and JaCoCo report phases. Parse target/surefire-reports/TEST-*.xml; classify toolchain, dependency/POM, production compilation, test compilation, test failure, timeout, coverage, and unknown. Only test compilation/failure/context/coverage categories are AI-repairable.

  • Step 4: Run focused and full tests

  • Step 5: Commit

git commit -m "feat: classify and run Maven test verification"

Task 6: Temporary worktrees and local target-branch integration

Files:

  • Create: src/main/java/com/example/workutils/biz/aitest/git/TemporaryWorktree.java
  • Create: src/main/java/com/example/workutils/biz/aitest/git/TemporaryWorktreeManager.java
  • Create: src/main/java/com/example/workutils/biz/aitest/git/BranchIntegrationRequest.java
  • Create: src/main/java/com/example/workutils/biz/aitest/git/BranchIntegrationResult.java
  • Create: src/main/java/com/example/workutils/biz/aitest/git/TargetBranchIntegrator.java
  • Test: src/test/java/com/example/workutils/biz/aitest/git/TemporaryWorktreeManagerTest.java
  • Test: src/test/java/com/example/workutils/biz/aitest/git/TargetBranchIntegratorTest.java

Interfaces:

  • TemporaryWorktreeManager.create(project, includeWorkingTree) -> TemporaryWorktree

  • TemporaryWorktree.close() removes the worktree or reports its retained path.

  • TargetBranchIntegrator.integrate(request, verifier) -> BranchIntegrationResult

  • Step 1: Write failing real-Git integration tests

@Test
void integratesOnlyVerifiedTestCommitIntoLocalBranch(@TempDir Path temp) {
    RepositoryFixture repo = RepositoryFixture.create(temp);
    BranchIntegrationResult result = integrator.integrate(request(repo), alwaysPass());
    assertTrue(result.successful());
    assertTrue(repo.fileOnBranch("target", "app-start/src/test/java/AServiceTest.java").isPresent());
    assertTrue(repo.fileOnBranch("target", "app-core/src/main/java/AService.java").isPresent());
}
  • Step 2: Verify RED

Run: mvnw.cmd -Dtest=TemporaryWorktreeManagerTest,TargetBranchIntegratorTest test

  • Step 3: Implement detached generation and guarded local integration

Use git worktree add --detach, copy staged/unstaged binary patches only when selected, commit only validated test paths, and integrate through a temporary branch/worktree based on the recorded target HEAD. Re-run the supplied verifier before advancing the local target ref. Refuse dirty checked-out targets, target movement, conflicts, remote operations, and commits containing paths outside the allowlist.

  • Step 4: Run focused and full tests

  • Step 5: Commit

git commit -m "feat: isolate AI tests and integrate local branches"

Task 7: End-to-end workflow and history

Files:

  • Create: src/main/java/com/example/workutils/biz/aitest/application/AiTestRequest.java
  • Create: src/main/java/com/example/workutils/biz/aitest/application/AiTestRun.java
  • Create: src/main/java/com/example/workutils/biz/aitest/application/AiTestRunState.java
  • Create: src/main/java/com/example/workutils/biz/aitest/application/AiTestProgress.java
  • Create: src/main/java/com/example/workutils/biz/aitest/application/AiTestWorkflow.java
  • Create: src/main/java/com/example/workutils/biz/aitest/application/AiTestHistoryService.java
  • Test: src/test/java/com/example/workutils/biz/aitest/application/AiTestWorkflowTest.java
  • Test: src/test/java/com/example/workutils/biz/aitest/application/AiTestHistoryServiceTest.java

Interfaces:

  • AiTestWorkflow.analyze(request, listener) -> AiTestRun

  • AiTestWorkflow.generateAndVerify(run, listener) -> AiTestRun

  • AiTestWorkflow.integrate(run, targetBranch, message, listener) -> AiTestRun

  • AiTestWorkflow.cancel() cancels current CodeFree/Maven work and preserves original repositories.

  • Step 1: Write failing workflow tests with real domain objects and fake external ports

@Test
void repairsTestsUntilCoveragePassesWithoutExceedingThreeRounds() {
    FakeProvider provider = providerReturning(compilationFailure(), passingAtCoverage(88));
    AiTestRun result = workflow(provider).generateAndVerify(analyzedRun(), ignored -> {});
    assertEquals(AiTestRunState.READY_FOR_REVIEW, result.state());
    assertEquals(2, result.generationRounds());
    assertEquals(new BigDecimal("88.00"), result.coverage().linePercent());
}
  • Step 2: Verify RED

Run: mvnw.cmd -Dtest=AiTestWorkflowTest,AiTestHistoryServiceTest test

  • Step 3: Implement the explicit state machine

Permit only documented transitions: environment check, analysis, confirmation, generation, Maven verification, coverage, repair, review, integration, target verification, success/failure/cancel. Generate one changed production class at a time, stop on non-repairable failures, stop after three rounds, persist only summaries/hashes, and never retain full prompts or production source.

  • Step 4: Run focused and full tests

  • Step 5: Commit

git commit -m "feat: orchestrate AI unit test generation workflow"

Task 8: JavaFX plugin UI and application registration

Files:

  • Create: src/main/java/com/example/workutils/biz/aitest/AiTestPlugin.java
  • Create: src/main/java/com/example/workutils/biz/aitest/AiTestWindow.java
  • Modify: src/main/java/com/example/workutils/system/ui/icon/AppIcon.java
  • Modify: src/main/java/com/example/workutils/system/ui/MainWindow.java
  • Test: src/test/java/com/example/workutils/biz/aitest/AiTestPluginTest.java
  • Test: src/test/java/com/example/workutils/biz/aitest/AiTestWindowTest.java
  • Modify: src/test/java/com/example/workutils/system/ui/NativeUiVisualAcceptanceFixture.java

Interfaces:

  • Plugin id ai-unit-test, name AI 单测生成, icon AI_TEST; add the id-to-icon mapping in MainWindow.NavItem.iconForPlugin so existing descriptor registration remains unchanged.

  • Embedded page exposes environment settings, project/module/baseline configuration, editable specifications, progress, four result tabs, review, and local-branch integration.

  • Step 1: Write failing plugin and JavaFX behavior tests

@Test
void pluginIsEmbeddedAndChecksProjectCatalogConfiguration() {
    assertEquals("ai-unit-test", plugin.getId());
    assertNotNull(plugin.getVisualFactory().getEmbeddedPane());
}

@Test
void uniqueStartModuleIsSelectedAndGenerateIsDisabledUntilAnalysisCompletes() {
    AiTestWindow window = fixtureWithModules("order-core", "order-start");
    assertEquals("order-start", window.selectedModuleArtifactId());
    assertTrue(window.generateButtonDisabled());
}
  • Step 2: Verify RED

Run: mvnw.cmd -Dtest=AiTestPluginTest,AiTestWindowTest test

  • Step 3: Implement the UI with existing WorkUtils design tokens and async controls

Use BorderPane, cards, semantic labels, accessible text, LoadingButtonBinder, OperationStatusPane, and background execution. Keep settings in the plugin page instead of enlarging the system settings window. Present tabs for incremental code, generation plan, tests/coverage, and Diff; require explicit review before enabling integration. Add no top-level duplicate navigation controls.

  • Step 4: Run focused UI tests and capture a manual visual acceptance screenshot

Run focused tests and mvnw.cmd test. Inspect the page at 1440 px and 1100 px minimum width; verify no clipping, actionable loading states, and readable logs.

  • Step 5: Commit
git commit -m "feat: add AI unit test generation plugin UI"

Task 9: Release verification and documentation

Files:

  • Modify: README.md

  • Modify: docs/项目说明.md

  • Create: docs/ai-unit-test-plugin.md

  • Update tests only when verification reveals an uncovered behavior.

  • Step 1: Document prerequisites and safe operating flow

Document global JDK 8/Maven/CodeFree selection, *-start behavior, baseline semantics, 85% coverage, three repair rounds, local-only integration, and failure categories. Include the verified CodeFree one-shot command without a model argument.

  • Step 2: Run focused architecture checks

Run all com.example.workutils.biz.aitest tests with JDK 17.

  • Step 3: Run complete verification
mvnw.cmd test
mvnw.cmd -DskipTests package
git diff --check
git status --short

Expected: all tests and packaging pass; diff check emits no errors; status contains only intentional V1 files.

  • Step 4: Perform a real CodeFree smoke test in an empty temporary directory

Invoke the configured CLI in CI/plan mode and assert a successful type=result record. Do not send business source during this smoke test.

  • Step 5: Commit
git commit -m "docs: document AI unit test plugin"
  • Step 6: Use verification-before-completion and finishing-a-development-branch

Review every acceptance criterion in the design spec, run fresh verification evidence, then present the verified branch for integration into master without touching the user's untracked master files.