From 5f9bd609c398552f30bbf2c84e60649c08512117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=AE=B6=E8=AE=AD?= Date: Tue, 4 Aug 2026 18:10:12 +0800 Subject: [PATCH] feat: analyze AI tests against explicit target ref --- ...08-04-ai-test-target-ref-and-async-race.md | 84 +++++++++++++++++++ .../workutils/biz/aitest/AiTestPane.java | 63 ++++++++++---- .../biz/aitest/LatestRequestGate.java | 15 ++++ .../biz/aitest/application/AiTestRequest.java | 10 ++- .../aitest/application/AiTestWorkflow.java | 7 +- .../biz/aitest/git/GitIncrementAnalyzer.java | 15 +++- .../aitest/git/TemporaryWorktreeManager.java | 12 ++- .../aitest/AiTestPaneArchitectureTest.java | 4 + .../biz/aitest/LatestRequestGateTest.java | 20 +++++ .../application/AiTestHistoryServiceTest.java | 2 +- .../application/AiTestWorkflowTest.java | 7 +- .../aitest/git/GitIncrementAnalyzerTest.java | 28 ++++++- .../git/TargetBranchIntegratorTest.java | 4 +- .../git/TemporaryWorktreeManagerTest.java | 32 ++++++- .../FtLoanCapitalCoreRegressionTest.java | 35 ++++++++ 15 files changed, 301 insertions(+), 37 deletions(-) create mode 100644 docs/superpowers/plans/2026-08-04-ai-test-target-ref-and-async-race.md create mode 100644 src/main/java/com/example/workutils/biz/aitest/LatestRequestGate.java create mode 100644 src/test/java/com/example/workutils/biz/aitest/LatestRequestGateTest.java create mode 100644 src/test/java/com/example/workutils/biz/aitest/project/FtLoanCapitalCoreRegressionTest.java diff --git a/docs/superpowers/plans/2026-08-04-ai-test-target-ref-and-async-race.md b/docs/superpowers/plans/2026-08-04-ai-test-target-ref-and-async-race.md new file mode 100644 index 0000000..3d8d1b6 --- /dev/null +++ b/docs/superpowers/plans/2026-08-04-ai-test-target-ref-and-async-race.md @@ -0,0 +1,84 @@ +# AI 单测目标引用与异步竞态修复实施计划 + +> **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:** 让 AI 单测插件稳定按 `master...DEV_1156339215001460262` 分析真实 `ft-loancapital-core` 增量,并阻止较早的项目加载结果覆盖用户后来选择的项目。 + +**Architecture:** 将 Git 比较终点从隐式 `HEAD` 提升为 `AiTestRequest.targetRef`,增量分析和临时 worktree 均解析同一目标引用;工作区改动仅允许叠加在目标引用等于当前 `HEAD` 的场景。项目元数据加载使用单调递增的请求令牌,只有最新令牌可以更新 JavaFX 控件。 + +**Tech Stack:** Java 17(WorkUtils 运行时)、JavaFX、Spring、Maven、JUnit 5、Mockito、Git CLI + +## Global Constraints + +- 真实业务项目使用 `D:/bestpay-srdcloud/ft-loancapital-core`;该目录不存在时集成回归测试使用 JUnit assumption 跳过。 +- 默认基线为 `master`;目标引用可填写分支或 Commit,并对当前真实场景使用 `DEV_1156339215001460262`。 +- 不修改真实业务仓库、不切换其分支、不提交其文件。 +- 不触碰 WorkUtils 中现有未跟踪的审计文档、截图和 SQL 文件。 +- 全程单线程实施,不使用子代理。 + +--- + +### Task 1: 显式 baseline 与 targetRef 的增量分析 + +**Files:** +- Modify: `src/main/java/com/example/workutils/biz/aitest/git/GitIncrementAnalyzer.java` +- Modify: `src/main/java/com/example/workutils/biz/aitest/application/AiTestRequest.java` +- Modify: `src/main/java/com/example/workutils/biz/aitest/application/AiTestWorkflow.java` +- Modify: `src/main/java/com/example/workutils/biz/aitest/git/TemporaryWorktreeManager.java` +- Test: `src/test/java/com/example/workutils/biz/aitest/git/GitIncrementAnalyzerTest.java` +- Test: `src/test/java/com/example/workutils/biz/aitest/git/TemporaryWorktreeManagerTest.java` +- Test: `src/test/java/com/example/workutils/biz/aitest/application/AiTestWorkflowTest.java` + +**Interfaces:** +- Produces: `GitIncrementAnalyzer.analyze(Path, String baseline, String targetRef, boolean includeWorkingTree)` +- Produces: `TemporaryWorktreeManager.create(Path, String targetRef, boolean includeWorkingTree)` +- Produces: `AiTestRequest.targetRef()` + +- [ ] **Step 1: 写失败测试**:构造 `master` 与未检出的 `DEV_*` 分支,断言分析结果来自 DEV 而不是当前 HEAD;断言临时 worktree 检出目标引用。 +- [ ] **Step 2: 运行聚焦测试确认 RED**:`mvn -Dtest=GitIncrementAnalyzerTest,TemporaryWorktreeManagerTest,AiTestWorkflowTest test`,预期因新签名或仍使用 HEAD 而失败。 +- [ ] **Step 3: 最小实现**:解析 baseline/target commit,以其 merge-base 到 target commit 生成 patch;workflow 将 request 的 targetRef 贯穿到分析和 worktree。 +- [ ] **Step 4: 运行聚焦测试确认 GREEN**:同一 Maven 命令全部通过。 + +### Task 2: 真实 ft-loancapital-core 回归 + +**Files:** +- Create: `src/test/java/com/example/workutils/biz/aitest/project/FtLoanCapitalCoreRegressionTest.java` + +**Interfaces:** +- Consumes: `MavenProjectAnalyzer.analyze(Path)` +- Consumes: `GitIncrementAnalyzer.analyze(Path, "master", "DEV_1156339215001460262", false)` + +- [ ] **Step 1: 写真实只读回归测试**:断言解析到 `ft-loancapital-core-start`,并断言 master 到 DEV 的增量包含已知 service Java 文件及新增行。 +- [ ] **Step 2: 运行测试确认结果**:`mvn -Dtest=FtLoanCapitalCoreRegressionTest test`;真实仓库存在时必须通过,不存在时仅跳过。 + +### Task 3: 项目加载的最新请求获胜 + +**Files:** +- Create: `src/main/java/com/example/workutils/biz/aitest/LatestRequestGate.java` +- Modify: `src/main/java/com/example/workutils/biz/aitest/AiTestPane.java` +- Create: `src/test/java/com/example/workutils/biz/aitest/LatestRequestGateTest.java` +- Modify: `src/test/java/com/example/workutils/biz/aitest/AiTestPaneArchitectureTest.java` + +**Interfaces:** +- Produces: `LatestRequestGate.next()` 返回请求令牌。 +- Produces: `LatestRequestGate.isLatest(long)` 仅对最后发出的令牌返回 true。 + +- [ ] **Step 1: 写失败测试**:先申请旧令牌再申请新令牌,断言旧令牌失效、新令牌有效;架构测试断言项目加载成功与失败回调都经过 gate。 +- [ ] **Step 2: 运行聚焦测试确认 RED**:`mvn -Dtest=LatestRequestGateTest,AiTestPaneArchitectureTest test`,预期类不存在或 Pane 未使用 gate。 +- [ ] **Step 3: 最小实现**:`loadProject` 发出令牌,清空旧模块;后台成功或失败回到 FX 线程时仅允许最新请求更新 UI。 +- [ ] **Step 4: 运行聚焦测试确认 GREEN**:同一 Maven 命令全部通过。 + +### Task 4: UI 参数与完整验证 + +**Files:** +- Modify: `src/main/java/com/example/workutils/biz/aitest/AiTestPane.java` +- Modify: `src/test/java/com/example/workutils/biz/aitest/AiTestPaneArchitectureTest.java` + +**Interfaces:** +- UI 提供 `baselineField`(默认 `master`)与 `targetRefField`(默认 `HEAD`,加载项目后显示当前分支)。 + +- [ ] **Step 1: 写失败测试**:断言 Pane 存在独立 targetRef 字段,且 baseline 默认 master。 +- [ ] **Step 2: 最小实现**:把 targetRef 写入 `AiTestRequest`,校验 baseline、targetRef、项目和模块均已选择。 +- [ ] **Step 3: 运行 AI 单测插件测试**:`mvn -Dtest='com.example.workutils.biz.aitest.**' test`。 +- [ ] **Step 4: 运行全量验证**:`mvn test`、`mvn -DskipTests package`、`git diff --check`。 +- [ ] **Step 5: 检查 `git status` 仅包含本次文件和用户原有未跟踪文件,然后提交到 WorkUtils `master`。** diff --git a/src/main/java/com/example/workutils/biz/aitest/AiTestPane.java b/src/main/java/com/example/workutils/biz/aitest/AiTestPane.java index 5e7adcf..cd82944 100644 --- a/src/main/java/com/example/workutils/biz/aitest/AiTestPane.java +++ b/src/main/java/com/example/workutils/biz/aitest/AiTestPane.java @@ -43,7 +43,8 @@ public class AiTestPane extends BorderPane { private final ComboBox projectCombo = new ComboBox<>(); private final ComboBox moduleCombo = new ComboBox<>(); - private final TextField baselineField = new TextField("HEAD~1"); + private final TextField baselineField = new TextField("master"); + private final TextField targetRefField = new TextField("HEAD"); private final CheckBox includeWorkingTree = new CheckBox("包含未提交改动"); private final TextField jdk8Field = new TextField(); private final TextField mavenField = new TextField(); @@ -67,6 +68,7 @@ public class AiTestPane extends BorderPane { private final CheckBox reviewApproved = new CheckBox("我已审核生成的测试代码"); private final TextField targetBranch = new TextField(); private final TextField commitMessage = new TextField("test: add AI generated incremental tests"); + private final LatestRequestGate projectLoadGate = new LatestRequestGate(); private volatile AiTestRun currentRun; private volatile boolean busy; @@ -116,7 +118,8 @@ public class AiTestPane extends BorderPane { private Node buildContent() { GridPane selection = grid(); addRow(selection, 0, "项目", projectCombo, "生成模块", moduleCombo); - addRow(selection, 1, "基线分支/Commit", baselineField, "工作区", includeWorkingTree); + addRow(selection, 1, "基线分支/Commit", baselineField, "目标分支/Commit", targetRefField); + selection.add(includeWorkingTree, 1, 2); GridPane toolchain = grid(); Button validateToolchain = new Button("检查并保存工具链"); @@ -193,26 +196,50 @@ public class AiTestPane extends BorderPane { } private void loadProject(AiTestProject project) { + long projectRequest = projectLoadGate.next(); + moduleCombo.getItems().clear(); + moduleCombo.setValue(null); if (project == null || !project.available()) { - moduleCombo.getItems().clear(); return; } - runAsync(() -> new ProjectData(projectAnalyzer.analyze(project.path()), + statusLabel.setText("正在加载项目模块..."); + AppExecutors.io().submit(() -> { + try { + ProjectData data = new ProjectData(projectAnalyzer.analyze(project.path()), specificationService.loadProject(project.path()), - modulePreferences.load(project.path()).orElse("")), data -> { - moduleCombo.setItems(FXCollections.observableArrayList(data.model().modules())); - projectSpecification.setText(data.specification()); - MavenModule selected = data.model().moduleByArtifactId(data.preferredModule()).orElse(null); - if (selected == null) { - List starts = data.model().modules().stream() - .filter(module -> module.artifactId().endsWith("-start")).toList(); - selected = starts.size() == 1 ? starts.get(0) : null; + modulePreferences.load(project.path()).orElse("")); + Platform.runLater(() -> { + if (!projectLoadGate.isLatest(projectRequest)) { + return; + } + applyProjectData(data); + }); + } catch (Exception failure) { + Platform.runLater(() -> { + if (!projectLoadGate.isLatest(projectRequest)) { + return; + } + statusLabel.setText(failure.getMessage() == null + ? failure.getClass().getSimpleName() : failure.getMessage()); + appendLog("项目加载失败: " + statusLabel.getText()); + }); } - moduleCombo.setValue(selected); - statusLabel.setText(selected == null ? "请选择生成模块" : "已选择模块 " + selected.artifactId()); }); } + private void applyProjectData(ProjectData data) { + moduleCombo.setItems(FXCollections.observableArrayList(data.model().modules())); + projectSpecification.setText(data.specification()); + MavenModule selected = data.model().moduleByArtifactId(data.preferredModule()).orElse(null); + if (selected == null) { + List starts = data.model().modules().stream() + .filter(module -> module.artifactId().endsWith("-start")).toList(); + selected = starts.size() == 1 ? starts.get(0) : null; + } + moduleCombo.setValue(selected); + statusLabel.setText(selected == null ? "请选择生成模块" : "已选择模块 " + selected.artifactId()); + } + private void validateAndSaveToolchain() { AiTestSettings settings = settingsFromForm(); runAsync(() -> { @@ -240,12 +267,14 @@ public class AiTestPane extends BorderPane { private void analyze() { AiTestProject project = projectCombo.getValue(); MavenModule module = moduleCombo.getValue(); - if (project == null || module == null || baselineField.getText().isBlank()) { - statusLabel.setText("请选择项目、模块并填写基线"); + if (project == null || module == null || baselineField.getText().isBlank() + || targetRefField.getText().isBlank()) { + statusLabel.setText("请选择项目、模块并填写基线和目标引用"); return; } AiTestRequest request = new AiTestRequest(project.path(), module.artifactId(), - baselineField.getText().trim(), includeWorkingTree.isSelected(), settingsFromForm()); + baselineField.getText().trim(), targetRefField.getText().trim(), + includeWorkingTree.isSelected(), settingsFromForm()); runAsync(() -> workflow.analyze(request, this::onProgress), run -> { currentRun = run; renderRun(run); diff --git a/src/main/java/com/example/workutils/biz/aitest/LatestRequestGate.java b/src/main/java/com/example/workutils/biz/aitest/LatestRequestGate.java new file mode 100644 index 0000000..870fac0 --- /dev/null +++ b/src/main/java/com/example/workutils/biz/aitest/LatestRequestGate.java @@ -0,0 +1,15 @@ +package com.example.workutils.biz.aitest; + +import java.util.concurrent.atomic.AtomicLong; + +final class LatestRequestGate { + private final AtomicLong sequence = new AtomicLong(); + + long next() { + return sequence.incrementAndGet(); + } + + boolean isLatest(long request) { + return sequence.get() == request; + } +} diff --git a/src/main/java/com/example/workutils/biz/aitest/application/AiTestRequest.java b/src/main/java/com/example/workutils/biz/aitest/application/AiTestRequest.java index f7648f7..0b4bb6f 100644 --- a/src/main/java/com/example/workutils/biz/aitest/application/AiTestRequest.java +++ b/src/main/java/com/example/workutils/biz/aitest/application/AiTestRequest.java @@ -4,9 +4,17 @@ import com.example.workutils.biz.aitest.config.AiTestSettings; import java.nio.file.Path; -public record AiTestRequest(Path project, String moduleArtifactId, String baseline, +public record AiTestRequest(Path project, String moduleArtifactId, String baseline, String targetRef, boolean includeWorkingTree, AiTestSettings settings) { public AiTestRequest { project = project.toAbsolutePath().normalize(); + if (baseline == null || baseline.isBlank()) { + throw new IllegalArgumentException("Baseline ref is required"); + } + if (targetRef == null || targetRef.isBlank()) { + throw new IllegalArgumentException("Target ref is required"); + } + baseline = baseline.trim(); + targetRef = targetRef.trim(); } } diff --git a/src/main/java/com/example/workutils/biz/aitest/application/AiTestWorkflow.java b/src/main/java/com/example/workutils/biz/aitest/application/AiTestWorkflow.java index e9ee655..576b29f 100644 --- a/src/main/java/com/example/workutils/biz/aitest/application/AiTestWorkflow.java +++ b/src/main/java/com/example/workutils/biz/aitest/application/AiTestWorkflow.java @@ -63,7 +63,7 @@ public class AiTestWorkflow { emit(progress, AiTestRunState.CREATED, 0, "正在分析 Maven 项目和 Git 增量"); MavenProjectModel model = projectAnalyzer.analyze(request.project()); MavenModule selected = requiredModule(model, request.moduleArtifactId()); - var increment = incrementAnalyzer.analyze(request.project(), request.baseline(), + var increment = incrementAnalyzer.analyze(request.project(), request.baseline(), request.targetRef(), request.includeWorkingTree()); for (var file : increment.files()) { model.modules().stream() @@ -91,7 +91,7 @@ public class AiTestWorkflow { AiTestRun run = analyzed; try { TemporaryWorktree worktree = worktreeManager.create(analyzed.request().project(), - analyzed.request().includeWorkingTree()); + analyzed.request().targetRef(), analyzed.request().includeWorkingTree()); run = run.workspace(worktree); MavenProjectModel workspaceModel = projectAnalyzer.analyze(worktree.path()); MavenModule module = requiredModule(workspaceModel, analyzed.request().moduleArtifactId()); @@ -241,7 +241,7 @@ public class AiTestWorkflow { %s 所选模块:%s - 基线:%s + 增量范围:%s...%s 目标增量行覆盖率:%d%% 当前轮次:%d 上轮反馈:%s @@ -254,6 +254,7 @@ public class AiTestWorkflow { 只返回纯 JSON,不要 Markdown: {"action":"WRITE_TESTS","tests":[{"path":"src/test/java/...Test.java","content":"...","targetClass":"...","coveredLines":[1],"scenarios":["..."]}],"assumptions":[],"warnings":[],"diagnosis":""} """.formatted(specifications, module.artifactId(), run.request().baseline(), + run.request().targetRef(), run.request().settings().coverageTarget(), round, feedback, sources, existingTests); } diff --git a/src/main/java/com/example/workutils/biz/aitest/git/GitIncrementAnalyzer.java b/src/main/java/com/example/workutils/biz/aitest/git/GitIncrementAnalyzer.java index 90844d0..3cd81b6 100644 --- a/src/main/java/com/example/workutils/biz/aitest/git/GitIncrementAnalyzer.java +++ b/src/main/java/com/example/workutils/biz/aitest/git/GitIncrementAnalyzer.java @@ -26,16 +26,23 @@ public class GitIncrementAnalyzer { private static final Pattern HUNK = Pattern.compile("^@@ -\\d+(?:,\\d+)? \\+(\\d+)(?:,(\\d+))? @@.*$"); private final ProcessExecutor processExecutor; - public IncrementAnalysis analyze(Path project, String baseline, boolean includeWorkingTree) { + public IncrementAnalysis analyze(Path project, String baseline, String targetRef, + boolean includeWorkingTree) { Path root = project.toAbsolutePath().normalize(); String baselineCommit = git(root, "rev-parse", "--verify", baseline + "^{commit}").trim(); - String mergeBase = git(root, "merge-base", baselineCommit, "HEAD").trim(); + String targetCommit = git(root, "rev-parse", "--verify", targetRef + "^{commit}").trim(); + String mergeBase = git(root, "merge-base", baselineCommit, targetCommit).trim(); Map> changed = new LinkedHashMap<>(); merge(changed, parsePatch(git(root, "-c", "core.quotepath=false", "diff", - "--unified=0", "--no-color", mergeBase + "...HEAD"))); + "--unified=0", "--no-color", mergeBase, targetCommit))); if (includeWorkingTree) { + String headCommit = git(root, "rev-parse", "HEAD").trim(); + if (!headCommit.equals(targetCommit)) { + throw new IllegalArgumentException( + "Working tree changes require target ref to resolve to current HEAD"); + } merge(changed, parsePatch(git(root, "-c", "core.quotepath=false", "diff", - "--unified=0", "--no-color", "HEAD"))); + "--unified=0", "--no-color", targetCommit))); addUntrackedJavaFiles(root, changed); } List files = changed.entrySet().stream() diff --git a/src/main/java/com/example/workutils/biz/aitest/git/TemporaryWorktreeManager.java b/src/main/java/com/example/workutils/biz/aitest/git/TemporaryWorktreeManager.java index 973c96d..6ee3d97 100644 --- a/src/main/java/com/example/workutils/biz/aitest/git/TemporaryWorktreeManager.java +++ b/src/main/java/com/example/workutils/biz/aitest/git/TemporaryWorktreeManager.java @@ -17,21 +17,27 @@ public class TemporaryWorktreeManager { this.git = new GitCommandRunner(executor); } - public TemporaryWorktree create(Path repository, boolean includeWorkingTree) { + public TemporaryWorktree create(Path repository, String targetRef, boolean includeWorkingTree) { Path repo = repository.toAbsolutePath().normalize(); if (Files.notExists(repo.resolve(".git"))) { throw new IllegalArgumentException("Not a Git repository: " + repo); } try { String head = git.required(repo, "rev-parse", "HEAD").trim(); + String targetCommit = git.required(repo, "rev-parse", "--verify", + targetRef + "^{commit}").trim(); + if (includeWorkingTree && !head.equals(targetCommit)) { + throw new IllegalArgumentException( + "Working tree changes require target ref to resolve to current HEAD"); + } Path parent = Files.createTempDirectory("workutils-ai-test-"); Path worktree = parent.resolve("generation"); - git.required(repo, "worktree", "add", "--detach", worktree.toString(), head); + git.required(repo, "worktree", "add", "--detach", worktree.toString(), targetCommit); try { if (includeWorkingTree) { copyWorkingTreeChanges(repo, worktree); } - return new TemporaryWorktree(repo, worktree, parent, head, this); + return new TemporaryWorktree(repo, worktree, parent, targetCommit, this); } catch (Exception failure) { try { git.required(repo, "worktree", "remove", "--force", worktree.toString()); } catch (RuntimeException ignored) { } diff --git a/src/test/java/com/example/workutils/biz/aitest/AiTestPaneArchitectureTest.java b/src/test/java/com/example/workutils/biz/aitest/AiTestPaneArchitectureTest.java index e80a1ed..7ecbbc1 100644 --- a/src/test/java/com/example/workutils/biz/aitest/AiTestPaneArchitectureTest.java +++ b/src/test/java/com/example/workutils/biz/aitest/AiTestPaneArchitectureTest.java @@ -17,6 +17,10 @@ class AiTestPaneArchitectureTest { assertTrue(source.contains("projectCombo")); assertTrue(source.contains("moduleCombo")); assertTrue(source.contains("baselineField")); + assertTrue(source.contains("targetRefField")); + assertTrue(source.contains("new TextField(\"master\")")); + assertTrue(source.contains("LatestRequestGate")); + assertTrue(source.contains("isLatest(projectRequest)")); assertTrue(source.contains("includeWorkingTree")); assertTrue(source.contains("defaultSpecification")); assertTrue(source.contains("projectSpecification")); diff --git a/src/test/java/com/example/workutils/biz/aitest/LatestRequestGateTest.java b/src/test/java/com/example/workutils/biz/aitest/LatestRequestGateTest.java new file mode 100644 index 0000000..bac0a4f --- /dev/null +++ b/src/test/java/com/example/workutils/biz/aitest/LatestRequestGateTest.java @@ -0,0 +1,20 @@ +package com.example.workutils.biz.aitest; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class LatestRequestGateTest { + + @Test + void onlyLatestIssuedRequestMayApplyItsResult() { + LatestRequestGate gate = new LatestRequestGate(); + + long older = gate.next(); + long latest = gate.next(); + + assertFalse(gate.isLatest(older)); + assertTrue(gate.isLatest(latest)); + } +} diff --git a/src/test/java/com/example/workutils/biz/aitest/application/AiTestHistoryServiceTest.java b/src/test/java/com/example/workutils/biz/aitest/application/AiTestHistoryServiceTest.java index c056fca..fc4835c 100644 --- a/src/test/java/com/example/workutils/biz/aitest/application/AiTestHistoryServiceTest.java +++ b/src/test/java/com/example/workutils/biz/aitest/application/AiTestHistoryServiceTest.java @@ -17,7 +17,7 @@ class AiTestHistoryServiceTest { @Test void persistsRunSummaryWithoutPromptOrProductionSource(@TempDir Path home) throws Exception { AiTestHistoryService service = new AiTestHistoryService(ApplicationPaths.of(home)); - AiTestRequest request = new AiTestRequest(home.resolve("project"), "order-start", "develop", + AiTestRequest request = new AiTestRequest(home.resolve("project"), "order-start", "develop", "feature/test", false, new AiTestSettings(Path.of("jdk8"), Path.of("maven"), "codefree", 85, 3, Duration.ofMinutes(5), Duration.ofMinutes(15))); AiTestRun run = AiTestRun.initial("run-1", request).fail("Maven仓库认证失败"); diff --git a/src/test/java/com/example/workutils/biz/aitest/application/AiTestWorkflowTest.java b/src/test/java/com/example/workutils/biz/aitest/application/AiTestWorkflowTest.java index 9e1e07a..5f153d6 100644 --- a/src/test/java/com/example/workutils/biz/aitest/application/AiTestWorkflowTest.java +++ b/src/test/java/com/example/workutils/biz/aitest/application/AiTestWorkflowTest.java @@ -45,11 +45,11 @@ class AiTestWorkflowTest { when(projectAnalyzer.analyze(project)).thenReturn(originalModel); when(projectAnalyzer.analyze(workspace)).thenReturn(workspaceModel); GitIncrementAnalyzer incrementAnalyzer = mock(GitIncrementAnalyzer.class); - when(incrementAnalyzer.analyze(project, "develop", false)).thenReturn(analysis); + when(incrementAnalyzer.analyze(project, "develop", "feature/test", false)).thenReturn(analysis); TemporaryWorktreeManager worktrees = mock(TemporaryWorktreeManager.class); TemporaryWorktree temporary = mock(TemporaryWorktree.class); when(temporary.path()).thenReturn(workspace); - when(worktrees.create(project, false)).thenReturn(temporary); + when(worktrees.create(project, "feature/test", false)).thenReturn(temporary); TestGenerationProvider provider = mock(TestGenerationProvider.class); GeneratedTestFile generated = new GeneratedTestFile("src/test/java/a/ATest.java", @@ -74,7 +74,7 @@ class AiTestWorkflowTest { provider, fileValidator, maven, classifier, jacoco, coverage, mock(TestSpecificationService.class), mock(AiTestHistoryService.class), mock(TargetBranchIntegrator.class)); - AiTestRequest request = new AiTestRequest(project, "order-start", "develop", false, + AiTestRequest request = new AiTestRequest(project, "order-start", "develop", "feature/test", false, settings(root)); AiTestRun analyzed = workflow.analyze(request, ignored -> { }); @@ -86,6 +86,7 @@ class AiTestWorkflowTest { ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(TestGenerationRequest.class); verify(provider, times(2)).generate(requestCaptor.capture(), any()); assertTrue(requestCaptor.getAllValues().get(0).prompt().contains("projectStyle")); + assertTrue(requestCaptor.getAllValues().get(0).prompt().contains("develop...feature/test")); } private static MavenModule module(Path root) { diff --git a/src/test/java/com/example/workutils/biz/aitest/git/GitIncrementAnalyzerTest.java b/src/test/java/com/example/workutils/biz/aitest/git/GitIncrementAnalyzerTest.java index ff2453b..eb7bf3a 100644 --- a/src/test/java/com/example/workutils/biz/aitest/git/GitIncrementAnalyzerTest.java +++ b/src/test/java/com/example/workutils/biz/aitest/git/GitIncrementAnalyzerTest.java @@ -32,14 +32,38 @@ class GitIncrementAnalyzerTest { write(source, "class A {\n int value() { return 2; }\n int next() { return 4; }\n}\n"); GitIncrementAnalyzer analyzer = new GitIncrementAnalyzer(new DefaultProcessExecutor()); - IncrementAnalysis committed = analyzer.analyze(repository, baseline, false); - IncrementAnalysis withWorkingTree = analyzer.analyze(repository, baseline, true); + IncrementAnalysis committed = analyzer.analyze(repository, baseline, "HEAD", false); + IncrementAnalysis withWorkingTree = analyzer.analyze(repository, baseline, "HEAD", true); assertEquals(Set.of(2, 3), committed.files().get(0).lines()); assertTrue(withWorkingTree.files().get(0).lines().containsAll(Set.of(2, 3))); assertTrue(withWorkingTree.includesWorkingTree()); } + @Test + void analyzesExplicitTargetBranchWithoutCheckingItOut(@TempDir Path repository) throws Exception { + git(repository, "init"); + git(repository, "config", "user.email", "workutils@example.com"); + git(repository, "config", "user.name", "WorkUtils Test"); + Path source = repository.resolve("core/src/main/java/a/A.java"); + write(source, "class A {\n int value() { return 1; }\n}\n"); + git(repository, "add", "."); + git(repository, "commit", "-m", "base"); + git(repository, "branch", "-M", "master"); + + git(repository, "checkout", "-b", "DEV_1156339215001460262"); + write(source, "class A {\n int value() { return 2; }\n int added() { return 3; }\n}\n"); + git(repository, "add", "."); + git(repository, "commit", "-m", "dev change"); + git(repository, "checkout", "master"); + + IncrementAnalysis result = new GitIncrementAnalyzer(new DefaultProcessExecutor()) + .analyze(repository, "master", "DEV_1156339215001460262", false); + + assertEquals(Set.of(2, 3), result.files().get(0).lines()); + assertEquals("master", git(repository, "branch", "--show-current").trim()); + } + private static void write(Path file, String content) throws Exception { Files.createDirectories(file.getParent()); Files.writeString(file, content, StandardCharsets.UTF_8); diff --git a/src/test/java/com/example/workutils/biz/aitest/git/TargetBranchIntegratorTest.java b/src/test/java/com/example/workutils/biz/aitest/git/TargetBranchIntegratorTest.java index 649efc5..7953d51 100644 --- a/src/test/java/com/example/workutils/biz/aitest/git/TargetBranchIntegratorTest.java +++ b/src/test/java/com/example/workutils/biz/aitest/git/TargetBranchIntegratorTest.java @@ -19,7 +19,7 @@ class TargetBranchIntegratorTest { TargetBranchIntegrator integrator = new TargetBranchIntegrator(new DefaultProcessExecutor()); BranchIntegrationResult result; - try (TemporaryWorktree generation = manager.create(repository, false)) { + try (TemporaryWorktree generation = manager.create(repository, "HEAD", false)) { Path test = generation.path().resolve("app-start/src/test/java/a/AServiceTest.java"); GitFixture.write(test, "package a; class AServiceTest {}"); result = integrator.integrate(new BranchIntegrationRequest(repository, generation.path(), @@ -42,7 +42,7 @@ class TargetBranchIntegratorTest { TargetBranchIntegrator integrator = new TargetBranchIntegrator(new DefaultProcessExecutor()); BranchIntegrationResult result; - try (TemporaryWorktree generation = manager.create(repository, false)) { + try (TemporaryWorktree generation = manager.create(repository, "HEAD", false)) { GitFixture.write(generation.path().resolve("app-core/src/main/java/a/A.java"), "class A { int value = 99; }"); result = integrator.integrate(new BranchIntegrationRequest(repository, generation.path(), diff --git a/src/test/java/com/example/workutils/biz/aitest/git/TemporaryWorktreeManagerTest.java b/src/test/java/com/example/workutils/biz/aitest/git/TemporaryWorktreeManagerTest.java index 8b58573..d55e4f7 100644 --- a/src/test/java/com/example/workutils/biz/aitest/git/TemporaryWorktreeManagerTest.java +++ b/src/test/java/com/example/workutils/biz/aitest/git/TemporaryWorktreeManagerTest.java @@ -20,7 +20,7 @@ class TemporaryWorktreeManagerTest { TemporaryWorktreeManager manager = new TemporaryWorktreeManager(new DefaultProcessExecutor()); Path worktreePath; - try (TemporaryWorktree worktree = manager.create(repository, true)) { + try (TemporaryWorktree worktree = manager.create(repository, "HEAD", true)) { worktreePath = worktree.path(); assertEquals("class A { int value = 2; }", Files.readString(worktree.path().resolve("app-core/src/main/java/a/A.java"), StandardCharsets.UTF_8)); @@ -30,4 +30,34 @@ class TemporaryWorktreeManagerTest { assertFalse(Files.exists(worktreePath)); assertEquals("class A { int value = 2; }", Files.readString(source, StandardCharsets.UTF_8)); } + + @Test + void createsWorktreeFromExplicitTargetBranch(@TempDir Path repository) throws Exception { + GitFixture.init(repository); + git(repository, "branch", "-M", "master"); + Path source = repository.resolve("app-core/src/main/java/a/A.java"); + git(repository, "checkout", "-b", "DEV_1156339215001460262"); + GitFixture.write(source, "class A { int branchValue = 7; }"); + git(repository, "add", "."); + git(repository, "commit", "-m", "dev change"); + git(repository, "checkout", "master"); + + TemporaryWorktreeManager manager = new TemporaryWorktreeManager(new DefaultProcessExecutor()); + try (TemporaryWorktree worktree = manager.create(repository, "DEV_1156339215001460262", false)) { + assertEquals("class A { int branchValue = 7; }", + Files.readString(worktree.path().resolve("app-core/src/main/java/a/A.java"))); + } + assertEquals("master", git(repository, "branch", "--show-current").trim()); + } + + private static String git(Path directory, String... arguments) throws Exception { + java.util.List command = new java.util.ArrayList<>(); + command.add("git"); + command.addAll(java.util.List.of(arguments)); + Process process = new ProcessBuilder(command).directory(directory.toFile()) + .redirectErrorStream(true).start(); + String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + assertEquals(0, process.waitFor(), output); + return output; + } } diff --git a/src/test/java/com/example/workutils/biz/aitest/project/FtLoanCapitalCoreRegressionTest.java b/src/test/java/com/example/workutils/biz/aitest/project/FtLoanCapitalCoreRegressionTest.java new file mode 100644 index 0000000..8990b56 --- /dev/null +++ b/src/test/java/com/example/workutils/biz/aitest/project/FtLoanCapitalCoreRegressionTest.java @@ -0,0 +1,35 @@ +package com.example.workutils.biz.aitest.project; + +import com.example.workutils.biz.aitest.git.GitIncrementAnalyzer; +import com.example.workutils.biz.aitest.git.IncrementAnalysis; +import com.example.workutils.biz.aitest.process.DefaultProcessExecutor; +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +class FtLoanCapitalCoreRegressionTest { + + private static final Path PROJECT = Path.of("D:/bestpay-srdcloud/ft-loancapital-core"); + + @Test + void detectsStartModuleAndMasterToDevIncrementInRealProject() { + assumeTrue(Files.isRegularFile(PROJECT.resolve("pom.xml")), + "real ft-loancapital-core checkout is not available"); + + MavenProjectModel model = new MavenProjectAnalyzer().analyze(PROJECT); + MavenModule start = model.moduleByArtifactId("ft-loancapital-core-start").orElseThrow(); + IncrementAnalysis increment = new GitIncrementAnalyzer(new DefaultProcessExecutor()) + .analyze(PROJECT, "master", "DEV_1156339215001460262", false); + + assertTrue(Files.isRegularFile(start.directory().resolve("pom.xml"))); + assertFalse(increment.files().isEmpty()); + assertTrue(increment.files().stream().anyMatch(file -> file.sourcePath().toString() + .replace('\\', '/').equals("ft-loancapital-core-service/src/main/java/com/bestpay/ft/" + + "loancapital/core/service/transfer/service/impl/OrgTransferManagementServiceImpl.java"))); + } +}