feat: analyze AI tests against explicit target ref
This commit is contained in:
@@ -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`。**
|
||||
@@ -43,7 +43,8 @@ public class AiTestPane extends BorderPane {
|
||||
|
||||
private final ComboBox<AiTestProject> projectCombo = new ComboBox<>();
|
||||
private final ComboBox<MavenModule> 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<MavenModule> 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<MavenModule> 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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<Path, Set<Integer>> 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<ChangedLines> files = changed.entrySet().stream()
|
||||
|
||||
@@ -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) { }
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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仓库认证失败");
|
||||
|
||||
@@ -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<TestGenerationRequest> 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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
|
||||
+31
-1
@@ -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<String> 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;
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -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")));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user