Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/agents/version-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ description: 项目级版本记录领域。任务涉及版本记录/工作段(
47. **`ProjectMemberService` 的构造器被两个测试类手工 `new`,新依赖一律走字段注入**(dev-board#444)——查人卡片的头像要 `AccountBindingRepository` 与 `ai.account.base-url`,两样都用 `@Autowired(required=false)` / `@Value` **字段**注入,包内可见的 `setAccountLookupForTest` 给手工 new 出来的实例补上(`ProjectRepoService.maxTrackedFileSizeBytes` 的同款先例)。改成构造器参数就要同步改 `ProjectMemberAddByContactTest` 与 `ProjectMemberServiceAddMemberRaceTest`,是纯 churn。

48. **hutool 的 `JSONObject`/`JSONArray` 绝不能原样当控制器返回值——`JSONNull` 会把整条响应打成 500**(dev-board#444,app-e2e J11 抓到;`proxyMembers` 那一半在 master 上就有)——hutool 把 JSON null 解析成 `cn.hutool.json.JSONNull` 单例(不是 Java `null`,是一个没有任何属性的对象),Jackson 没有它的序列化器,于是 `HttpMessageConversionException: No serializer found for class cn.hutool.json.JSONNull`。`CloudSyncService.proxyMembers`/`proxyMemberLookup` 原来把上游回包的 `JSONObject` 直接塞进控制器返回值,**只要某个字段是 null 就整条 500**——而 `avatarUrl` 为 null 正是没绑官网、没传头像的默认状态,即绝大多数账号,所以用户侧表现是「案件参与人」列表永远只有自己一个人、查人永远弹「服务器内部错误」,一个能用的账号都碰不到。现在两处都过私有纯函数 `toPlain(Object)`(递归 `JSONObject`→`LinkedHashMap`、`JSONArray`→`ArrayList`、`JSONUtil.isNull`→Java `null`,其余原样)。**刻意没有改成让 Jackson 直接解析上游**——那会牵动 `httpGet` seam 与一整排既有测试,换不来行为收益。同一个类里的 `listRemoteProjects` 不受影响(它用 `getStr`/`getLong` 取值后自己拼 `HashMap`,出来的已经是 Java 原生类型),**这正是判断某处安不安全的判据:看返回值里有没有未经转换的 hutool 对象,不是看这个方法有没有 import hutool**。护栏 `CloudMemberProxyJsonTest`(两条用例都拿 `ObjectMapper.writeValueAsString` 断言序列化不抛且 `"avatarUrl":null` 原样在,去掉 `toPlain` 立刻转红)。**今后任何「透传上游 JSON」的新端点,返回前都要过 `toPlain` 这一关。**
49. **测试里的 file:// 裸仓库必须关 `receive.autogc`,否则 JGit 后台 gc 与 JUnit `@TempDir` 清理赛跑**(dev-board#500,CI run 34168686921 抓到)——往 file:// 远端 push 时 JGit 在本进程内跑 `ReceivePack`,收包收尾按远端仓库配置 `receive.autogc`(默认 true)调 `Repository.autoGC`;auto gc 又按 `gc.autoDetach`(默认 true)丢到 `WorkQueue` 后台线程,**`gc.log.lock` 在调用线程上同步建、在后台线程上异步删**——仓库再小、根本不需要 gc 也一样。测试方法一返回 JUnit 就递归删 `@TempDir`,撞上那几毫秒就是 `IO Failed to delete temp directory ... hub.git/gc.log.lock`,表现为 `MemoryLwwMergeTest.mergeNeverLeavesRepositoryMidMerge` 之类随机一条红。修法不是等后台任务(JGit 没暴露可等的句柄),而是在建裸仓时把 `receive.autogc=false` 写进它的 config:**一律用 `com.checkba.version.BareHub.init(dir)` 建测试裸仓**,已把 `MemoryLwwMergeTest`/`MemorySyncRoundTripTest`/`CloudSyncUploadTest`/`CloudSyncUpdateTest`/`ProjectRepoRemoteTest` 五处收拢。新写任何「裸仓 + push」的测试直接调它,别再手写 `Git.init().setBare(true)`。生产侧不受影响:团队服务器的 `GitHttpController` 收 push 后不清理目录,后台 gc 正是想要的。

## 验证

Expand Down
30 changes: 30 additions & 0 deletions backend/src/test/java/com/checkba/version/BareHub.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.checkba.version;

import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.StoredConfig;

import java.nio.file.Path;

/**
* 测试用 file:// 裸仓库(模拟云端 / 记忆 hub)的唯一建法。
*
* 必须关掉 {@code receive.autogc}:往 file:// 远端 push 时 JGit 在本进程内跑 ReceivePack,
* 收包结束会按这个开关(默认 true)触发 auto gc,而 auto gc 默认 {@code gc.autoDetach}
* 在后台线程里跑、期间持有 {@code gc.log.lock}。测试方法一结束 JUnit 就清理 @TempDir,
* 与后台 gc 撞上就报「Failed to delete temp directory ... gc.log.lock」(dev-board#500)。
*/
public final class BareHub {

private BareHub() {}

/** 建裸仓(初始分支 master,与 JGit 默认一致),返回其 file:// 地址。 */
public static String init(Path dir) throws Exception {
try (Git git = Git.init().setBare(true).setDirectory(dir.toFile())
.setInitialBranch("master").call()) {
StoredConfig cfg = git.getRepository().getConfig();
cfg.setBoolean("receive", null, "autogc", false);
cfg.save();
}
return dir.toUri().toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,7 @@ protected String httpGet(String url, String sessionToken) {
// ---- helpers ------------------------------------------------------

private String bareRemote(Path dir) throws Exception {
Git.init().setBare(true).setDirectory(dir.toFile())
.setInitialBranch("master").call().close();
return dir.toUri().toString();
return BareHub.init(dir);
}

/** 建一个 file:// 裸仓当云端 + CloudConnection/ProjectRemote 行,setRemoteOrigin 指过去。 */
Expand Down Expand Up @@ -534,7 +532,7 @@ void abortCloudMergeRefusesForeignMergeWindow() throws Exception {
private Path bareCloudRemote(long remoteProjectId, Path cloudRoot) throws Exception {
Path bareRepoDir = cloudRoot.resolve("git").resolve(remoteProjectId + ".git");
Files.createDirectories(bareRepoDir.getParent());
Git.init().setBare(true).setDirectory(bareRepoDir.toFile()).setInitialBranch("master").call().close();
BareHub.init(bareRepoDir);
return bareRepoDir;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,7 @@ protected String httpPost(String url, String body, String sessionToken) {
// ---- helpers ------------------------------------------------------

private String bareRemote(Path dir) throws Exception {
Git.init().setBare(true).setDirectory(dir.toFile())
.setInitialBranch("master").call().close();
return dir.toUri().toString();
return BareHub.init(dir);
}

/** 建一个 file:// 裸仓当云端 + CloudConnection/ProjectRemote 行,setRemoteOrigin 指过去。 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@ private ProjectRepoService seeded(Path root) throws Exception {
}

private String bareRemote(Path dir) throws Exception {
Git.init().setBare(true).setDirectory(dir.toFile())
.setInitialBranch("master").call().close();
return dir.toUri().toString();
return BareHub.init(dir);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.checkba.version.memory;

import com.checkba.model.entity.MemoryEntry;
import org.eclipse.jgit.api.Git;
import com.checkba.version.BareHub;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
Expand Down Expand Up @@ -32,8 +32,7 @@ class MemoryLwwMergeTest {
@BeforeEach
void setUp() throws Exception {
Path hub = tmp.resolve("hub.git");
Git.init().setBare(true).setDirectory(hub.toFile()).call().close();
String hubUrl = hub.toUri().toString();
String hubUrl = BareHub.init(hub);
a = new MemorySyncTestMachine(tmp.resolve("machine-a"), hubUrl, MemoryRealm.project(1));
b = new MemorySyncTestMachine(tmp.resolve("machine-b"), hubUrl, MemoryRealm.project(2));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.checkba.version.memory;

import com.checkba.model.entity.MemoryEntry;
import org.eclipse.jgit.api.Git;
import com.checkba.version.BareHub;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
Expand All @@ -28,8 +28,7 @@ class MemorySyncRoundTripTest {
@BeforeEach
void setUp() throws Exception {
Path hub = tmp.resolve("hub.git");
Git.init().setBare(true).setDirectory(hub.toFile()).call().close();
hubUrl = hub.toUri().toString();
hubUrl = BareHub.init(hub);
a = new MemorySyncTestMachine(tmp.resolve("machine-a"), hubUrl, MemoryRealm.project(1));
b = new MemorySyncTestMachine(tmp.resolve("machine-b"), hubUrl, MemoryRealm.project(2));
}
Expand Down
Loading