From 556ab32e5fc0b383fcca15bcf2c2934f39732d6a 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 17:28:58 +0800 Subject: [PATCH] docs: finalize AI test plugin usage and CodeFree protocol --- docs/ai-unit-test-plugin.md | 51 +++++++++++++++++++ .../ai/CodeFreeCliTestGenerationProvider.java | 2 +- .../biz/aitest/ai/CodeFreeResultParser.java | 10 +++- ...CodeFreeCliTestGenerationProviderTest.java | 7 ++- .../aitest/ai/CodeFreeResultParserTest.java | 10 ++-- 5 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 docs/ai-unit-test-plugin.md diff --git a/docs/ai-unit-test-plugin.md b/docs/ai-unit-test-plugin.md new file mode 100644 index 0000000..e38b3f0 --- /dev/null +++ b/docs/ai-unit-test-plugin.md @@ -0,0 +1,51 @@ +# AI 单测生成插件 V1 + +## 适用范围 + +该插件面向 `project_paths.txt` 中登记的 Git + Maven 项目,目标运行环境固定为 JDK 8,测试框架为 JUnit 5。V1 只使用已登录的全局 `codefree` CLI,不指定模型,并保留 `TestGenerationProvider` 接口供后续接入其他模型。 + +## 使用流程 + +1. 在左侧菜单打开“AI 单测生成”。 +2. 配置 JDK 8 Home、Maven Home 和 CodeFree 命令,点击“检查并保存工具链”。 +3. 选择项目。插件会分析全部 Maven 模块:仅有一个 `artifactId` 以 `-start` 结尾时自动选择,否则由用户选择。选择结果按项目记忆。 +4. 填写基线分支或 Commit;需要覆盖未提交代码时勾选“包含未提交改动”。 +5. 编辑并保存默认质量规范及当前项目规范。 +6. 点击“分析增量”,确认增量范围后点击“生成并验证”。 +7. 插件在临时 Git worktree 中调用 CodeFree,并按顺序执行测试编译、JUnit 5 测试和 JaCoCo 报告。默认增量行覆盖率门槛为 85%,最多修复 3 轮。 +8. 在“生成文件”“覆盖率”和“运行日志”页审核结果。确认代码后勾选审核框,填写目标本地分支并合并。 + +## AI 修复边界 + +CodeFree 只能写入所选模块的 `src/test/java/**/*.java`。生产源码、`pom.xml`、配置和其他模块的变更会被拒绝。 + +以下错误允许进入下一轮 AI 修复: + +- 测试代码编译错误; +- 测试失败; +- 测试上下文启动失败; +- 增量覆盖率未达到门槛。 + +工具链、依赖/POM、生产代码编译、超时和未知基础设施错误只诊断,不允许 AI 修改生产工程来绕过问题。 + +## 本地分支合并 + +合并前,插件把候选测试提交到临时 worktree,再在目标分支的独立临时 worktree 中执行 cherry-pick 和 Maven 二次验证。验证成功且目标分支未发生并发变化时,才原子更新本地分支引用。 + +插件不会执行 `fetch`、`pull`、`push`,也不会自动修改当前检出的业务分支。若目标分支正被某个 worktree 使用,需要先切换该 worktree 的分支。 + +## 本地数据 + +以下文件保存在 WorkUtils 应用目录的 `data/ai-test/` 下,不写入业务仓库: + +- `settings.properties`:JDK、Maven、CodeFree、覆盖率和轮次设置; +- `default-test-spec.md`:可编辑默认质量规范; +- `projects//test-spec.md`:项目专属规范; +- `module-preferences.properties`:每个项目上次选择的模块; +- `runs//run.json`:运行摘要。 + +运行历史不保存提示词、生产源码或生成代码正文。V1 历史只用于查看摘要,不支持从中恢复中断任务。 + +## 前置条件 + +WorkUtils 发布包自带运行应用所需的 Java 运行时,但目标项目的单测编译仍需要用户配置一个 JDK 8;Maven 可使用全局安装或在界面中配置 Maven Home;CodeFree CLI 必须已全局安装并登录。 diff --git a/src/main/java/com/example/workutils/biz/aitest/ai/CodeFreeCliTestGenerationProvider.java b/src/main/java/com/example/workutils/biz/aitest/ai/CodeFreeCliTestGenerationProvider.java index 2deb5e4..d0e0078 100644 --- a/src/main/java/com/example/workutils/biz/aitest/ai/CodeFreeCliTestGenerationProvider.java +++ b/src/main/java/com/example/workutils/biz/aitest/ai/CodeFreeCliTestGenerationProvider.java @@ -27,9 +27,9 @@ public class CodeFreeCliTestGenerationProvider implements TestGenerationProvider command.add("/c"); } command.add(request.codeFreeCommand()); - command.add(request.prompt()); command.addAll(List.of("--channel", "CI", "--approval-mode", "plan", "--max-session-turns", "1", "--output-format", "json")); + command.add(request.prompt()); CommandResult result = processExecutor.execute(new CommandSpec(request.workingDirectory(), command, Map.of(), request.timeout()), output); if (!result.successful()) { diff --git a/src/main/java/com/example/workutils/biz/aitest/ai/CodeFreeResultParser.java b/src/main/java/com/example/workutils/biz/aitest/ai/CodeFreeResultParser.java index efeb074..3c9d8b9 100644 --- a/src/main/java/com/example/workutils/biz/aitest/ai/CodeFreeResultParser.java +++ b/src/main/java/com/example/workutils/biz/aitest/ai/CodeFreeResultParser.java @@ -31,8 +31,14 @@ public class CodeFreeResultParser { throw new IllegalArgumentException("CodeFree output has no successful result record"); } String innerText = resultRecord.getString("result"); - if (innerText == null || innerText.contains("```")) { - throw new IllegalArgumentException("CodeFree result must be plain JSON without Markdown fences"); + if (innerText == null) { + throw new IllegalArgumentException("CodeFree result payload is required"); + } + innerText = innerText.trim(); + if (innerText.startsWith("```json") && innerText.endsWith("```")) { + innerText = innerText.substring("```json".length(), innerText.length() - 3).trim(); + } else if (innerText.contains("```")) { + throw new IllegalArgumentException("CodeFree result contains an unsupported Markdown fence"); } final JSONObject inner; try { diff --git a/src/test/java/com/example/workutils/biz/aitest/ai/CodeFreeCliTestGenerationProviderTest.java b/src/test/java/com/example/workutils/biz/aitest/ai/CodeFreeCliTestGenerationProviderTest.java index 10999a5..3a52325 100644 --- a/src/test/java/com/example/workutils/biz/aitest/ai/CodeFreeCliTestGenerationProviderTest.java +++ b/src/test/java/com/example/workutils/biz/aitest/ai/CodeFreeCliTestGenerationProviderTest.java @@ -32,15 +32,18 @@ class CodeFreeCliTestGenerationProviderTest { }; CodeFreeCliTestGenerationProvider provider = new CodeFreeCliTestGenerationProvider(executor, new CodeFreeResultParser()); + String prompt = "return-json"; TestGenerationResult result = provider.generate(new TestGenerationRequest( - workingDirectory, "codefree", "只返回JSON", Duration.ofMinutes(5)), ignored -> { }); + workingDirectory, "codefree", prompt, Duration.ofMinutes(5)), ignored -> { }); assertTrue(result.successful()); assertTrue(captured.get().command().containsAll(java.util.List.of( "--channel", "CI", "--approval-mode", "plan", "--max-session-turns", "1", "--output-format", "json"))); assertFalse(captured.get().command().contains("--model")); - assertTrue(captured.get().command().contains("只返回JSON")); + assertTrue(captured.get().command().contains(prompt)); + assertTrue(captured.get().command().indexOf("--output-format") + < captured.get().command().indexOf(prompt)); } } diff --git a/src/test/java/com/example/workutils/biz/aitest/ai/CodeFreeResultParserTest.java b/src/test/java/com/example/workutils/biz/aitest/ai/CodeFreeResultParserTest.java index 8276d7c..ca78a43 100644 --- a/src/test/java/com/example/workutils/biz/aitest/ai/CodeFreeResultParserTest.java +++ b/src/test/java/com/example/workutils/biz/aitest/ai/CodeFreeResultParserTest.java @@ -28,10 +28,14 @@ class CodeFreeResultParserTest { } @Test - void rejectsMarkdownWrappedOrMalformedInnerResults() { - String outer = "[{\"type\":\"result\",\"subtype\":\"success\",\"result\":\"```json {} ```\"}]"; + void acceptsSingleJsonFenceButRejectsProseOrMalformedResults() { + String fenced = "```json\n{\"action\":\"DIAGNOSE_ONLY\",\"tests\":[]}\n```"; + String outer = "[{\"type\":\"result\",\"subtype\":\"success\",\"result\":" + + com.alibaba.fastjson.JSON.toJSONString(fenced) + "}]"; + String prose = "[{\"type\":\"result\",\"subtype\":\"success\",\"result\":\"Here: {}\"}]"; - assertThrows(IllegalArgumentException.class, () -> parser.parse(outer)); + assertEquals("DIAGNOSE_ONLY", parser.parse(outer).action()); + assertThrows(IllegalArgumentException.class, () -> parser.parse(prose)); assertThrows(IllegalArgumentException.class, () -> parser.parse("not-json")); } }