新增: Git worktree生命周期管理(创建/提交/合并/冲突检测/清理)
- create_plan_worktree: 创建Plan级工作分支 - create_worktree: 创建SubTask专属worktree(天然文件隔离) - commit_worktree: worktree内提交改动(含user.email兜底) - merge_branch: merge-tree预检+merge实际合并,返回Clean/Conflict - remove_plan_worktrees: 批量清理Plan所有worktree+分支 - 7个集成测试全绿(隔离/并行/提交/无冲突合并/冲突检测/非Git降级/清理) - 使用真实Git+tempdir测试(参考df-execute/tests/shell.rs模式)
This commit is contained in:
@@ -18,3 +18,6 @@ reqwest = { version = "0.12", features = ["stream", "json"] }
|
||||
futures = "0.3"
|
||||
eventsource-stream = "0.2"
|
||||
rand = "0.8"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
542
crates/df-ai/src/git_worktree.rs
Normal file
542
crates/df-ai/src/git_worktree.rs
Normal file
@@ -0,0 +1,542 @@
|
||||
//! Git worktree 生命周期管理 — 多 Agent 并行执行的文件隔离基础
|
||||
//!
|
||||
//! 设计依据:docs/02-架构设计/专项设计/多Agent并行执行与仲裁合并设计-2026-07-01.md §4.2
|
||||
//!
|
||||
//! 每个 SubTask 创建独立 worktree(git worktree add + 新分支),
|
||||
//! 写操作天然隔离。合并时用 git merge-tree 预检 + git merge 实际合并。
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
/// worktree 管理错误
|
||||
#[derive(Debug)]
|
||||
pub enum WorktreeError {
|
||||
/// Git 命令执行失败(stdout/stderr 含错误信息)
|
||||
GitFailed(String),
|
||||
/// 工程根目录无 .git(非 Git 工程,应降级串行)
|
||||
NotGitRepo,
|
||||
/// worktree 目录已存在(可能上次未清理)
|
||||
AlreadyExists(PathBuf),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for WorktreeError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::GitFailed(msg) => write!(f, "Git 命令失败: {}", msg),
|
||||
Self::NotGitRepo => write!(f, "非 Git 工程(无 .git 目录)"),
|
||||
Self::AlreadyExists(p) => write!(f, "worktree 目录已存在: {}", p.display()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for WorktreeError {}
|
||||
|
||||
/// worktree 创建结果
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WorktreeHandle {
|
||||
/// worktree 物理路径(SubTask 工具操作的 cwd)
|
||||
pub path: PathBuf,
|
||||
/// worktree 对应的分支名(merge 时用)
|
||||
pub branch: String,
|
||||
/// 所属 Plan id(清理时按 plan 批量)
|
||||
pub plan_id: String,
|
||||
/// SubTask id
|
||||
pub subtask_id: String,
|
||||
}
|
||||
|
||||
/// 在指定工程根目录下创建 SubTask 专属 worktree。
|
||||
///
|
||||
/// - `project_root`: 工程根目录(含 .git)
|
||||
/// - `plan_id`: 所属 Plan id
|
||||
/// - `subtask_id`: SubTask id
|
||||
/// - `base_branch`: fork 基点分支(plan 分支或主分支)
|
||||
///
|
||||
/// 返回 WorktreeHandle(path + branch)。
|
||||
/// worktree 路径:`{project_root}/.devflow/wt/{plan_id}/{subtask_id}`
|
||||
/// 分支名:`subtask/{plan_id}/{subtask_id}`
|
||||
pub fn create_worktree(
|
||||
project_root: &Path,
|
||||
plan_id: &str,
|
||||
subtask_id: &str,
|
||||
base_branch: &str,
|
||||
) -> Result<WorktreeHandle, WorktreeError> {
|
||||
if !project_root.join(".git").exists() {
|
||||
return Err(WorktreeError::NotGitRepo);
|
||||
}
|
||||
|
||||
let wt_path = project_root.join(".devflow/wt").join(plan_id).join(subtask_id);
|
||||
if wt_path.exists() {
|
||||
return Err(WorktreeError::AlreadyExists(wt_path));
|
||||
}
|
||||
|
||||
let branch = format!("subtask/{}/{}", plan_id, subtask_id);
|
||||
|
||||
// git worktree add <path> -b <branch> <base_branch>
|
||||
let output = Command::new("git")
|
||||
.args([
|
||||
"worktree",
|
||||
"add",
|
||||
wt_path.to_str().unwrap_or(""),
|
||||
"-b",
|
||||
&branch,
|
||||
base_branch,
|
||||
])
|
||||
.current_dir(project_root)
|
||||
.output()
|
||||
.map_err(|e| WorktreeError::GitFailed(format!("git worktree add 执行失败: {}", e)))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(WorktreeError::GitFailed(format!(
|
||||
"git worktree add 失败: {}",
|
||||
stderr.trim()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(WorktreeHandle {
|
||||
path: wt_path,
|
||||
branch,
|
||||
plan_id: plan_id.to_string(),
|
||||
subtask_id: subtask_id.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// 在 worktree 内提交所有改动(git add -A + git commit)。
|
||||
///
|
||||
/// - `wt_path`: worktree 物理路径
|
||||
/// - `message`: commit 消息
|
||||
pub fn commit_worktree(wt_path: &Path, message: &str) -> Result<(), WorktreeError> {
|
||||
// git add -A
|
||||
let add = Command::new("git")
|
||||
.args(["add", "-A"])
|
||||
.current_dir(wt_path)
|
||||
.output()
|
||||
.map_err(|e| WorktreeError::GitFailed(format!("git add 失败: {}", e)))?;
|
||||
if !add.status.success() {
|
||||
return Err(WorktreeError::GitFailed(format!(
|
||||
"git add 失败: {}",
|
||||
String::from_utf8_lossy(&add.stderr).trim()
|
||||
)));
|
||||
}
|
||||
|
||||
// git commit -m (允许空提交,--allow-empty 防无改动时报错)
|
||||
let commit = Command::new("git")
|
||||
.args(["commit", "--allow-empty", "-m", message])
|
||||
.current_dir(wt_path)
|
||||
.output()
|
||||
.map_err(|e| WorktreeError::GitFailed(format!("git commit 失败: {}", e)))?;
|
||||
if !commit.status.success() {
|
||||
// commit 失败可能因无 user.email 配置(测试环境),尝试自动配
|
||||
let stderr = String::from_utf8_lossy(&commit.stderr);
|
||||
if stderr.contains("user.email") || stderr.contains("user.name") {
|
||||
// 降级:设置临时配置后重试
|
||||
Command::new("git")
|
||||
.args(["config", "user.email", "devflow@ai.local"])
|
||||
.current_dir(wt_path)
|
||||
.output()
|
||||
.ok();
|
||||
Command::new("git")
|
||||
.args(["config", "user.name", "DevFlow Agent"])
|
||||
.current_dir(wt_path)
|
||||
.output()
|
||||
.ok();
|
||||
let retry = Command::new("git")
|
||||
.args(["commit", "--allow-empty", "-m", message])
|
||||
.current_dir(wt_path)
|
||||
.output()
|
||||
.map_err(|e| WorktreeError::GitFailed(format!("git commit 重试失败: {}", e)))?;
|
||||
if !retry.status.success() {
|
||||
return Err(WorktreeError::GitFailed(format!(
|
||||
"git commit 重试仍失败: {}",
|
||||
String::from_utf8_lossy(&retry.stderr).trim()
|
||||
)));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
return Err(WorktreeError::GitFailed(format!("git commit 失败: {}", stderr.trim())));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 将 SubTask 分支 merge 到 plan 分支(在 plan worktree 内执行)。
|
||||
///
|
||||
/// 返回 Ok(true) 表示有改动被合并,Ok(false) 表示无改动(空分支)。
|
||||
/// 返回 Err 表示 merge 冲突。
|
||||
pub fn merge_branch(
|
||||
plan_wt_path: &Path,
|
||||
subtask_branch: &str,
|
||||
) -> Result<MergeResult, WorktreeError> {
|
||||
// 先用 merge-tree 预检
|
||||
let precheck = Command::new("git")
|
||||
.args(["merge-tree", "--write-tree", "--messages", "HEAD", subtask_branch])
|
||||
.current_dir(plan_wt_path)
|
||||
.output()
|
||||
.map_err(|e| WorktreeError::GitFailed(format!("git merge-tree 失败: {}", e)))?;
|
||||
|
||||
// merge-tree --write-tree 成功(exit 0)= 无冲突可自动合并;
|
||||
// 非0(冲突)且 stdout 非空 = 有冲突
|
||||
if !precheck.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&precheck.stdout);
|
||||
if !stdout.is_empty() {
|
||||
return Ok(MergeResult::Conflict {
|
||||
branch: subtask_branch.to_string(),
|
||||
details: stdout.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 无冲突,执行实际 merge
|
||||
let merge = Command::new("git")
|
||||
.args(["merge", "--no-edit", subtask_branch])
|
||||
.current_dir(plan_wt_path)
|
||||
.output()
|
||||
.map_err(|e| WorktreeError::GitFailed(format!("git merge 失败: {}", e)))?;
|
||||
|
||||
if !merge.status.success() {
|
||||
// merge 冲突(git merge 返回非0)
|
||||
let stderr = String::from_utf8_lossy(&merge.stderr);
|
||||
let stdout = String::from_utf8_lossy(&merge.stdout);
|
||||
// abort 部分合并状态(防 worktree 处于 conflicted 状态)
|
||||
let _ = Command::new("git")
|
||||
.args(["merge", "--abort"])
|
||||
.current_dir(plan_wt_path)
|
||||
.output();
|
||||
return Ok(MergeResult::Conflict {
|
||||
branch: subtask_branch.to_string(),
|
||||
details: format!("{}\n{}", stdout, stderr),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(MergeResult::Clean)
|
||||
}
|
||||
|
||||
/// merge 结果
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MergeResult {
|
||||
/// 干净合并(无冲突)
|
||||
Clean,
|
||||
/// 合并冲突(details 含 conflict 信息)
|
||||
Conflict { branch: String, details: String },
|
||||
}
|
||||
|
||||
/// 清理单个 SubTask 的 worktree(git worktree remove --force + 删分支)。
|
||||
pub fn remove_worktree(project_root: &Path, handle: &WorktreeHandle) -> Result<(), WorktreeError> {
|
||||
// git worktree remove --force <path>
|
||||
let _ = Command::new("git")
|
||||
.args([
|
||||
"worktree",
|
||||
"remove",
|
||||
"--force",
|
||||
handle.path.to_str().unwrap_or(""),
|
||||
])
|
||||
.current_dir(project_root)
|
||||
.output();
|
||||
|
||||
// 删除分支(-D 强制,已 merge 的分支正常删,未 merge 的也删)
|
||||
let _ = Command::new("git")
|
||||
.args(["branch", "-D", &handle.branch])
|
||||
.current_dir(project_root)
|
||||
.output();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 清理某 Plan 的所有 worktree(Plan 完成/失败/取消时批量清理)。
|
||||
pub fn remove_plan_worktrees(project_root: &Path, plan_id: &str) -> Result<(), WorktreeError> {
|
||||
let plan_wt_dir = project_root.join(".devflow/wt").join(plan_id);
|
||||
if !plan_wt_dir.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// git worktree list 获取该 plan 下所有 worktree
|
||||
let list = Command::new("git")
|
||||
.args(["worktree", "list", "--porcelain"])
|
||||
.current_dir(project_root)
|
||||
.output()
|
||||
.map_err(|e| WorktreeError::GitFailed(format!("git worktree list 失败: {}", e)))?;
|
||||
|
||||
let list_str = String::from_utf8_lossy(&list.stdout);
|
||||
for line in list_str.lines() {
|
||||
if let Some(path_str) = line.strip_prefix("worktree ") {
|
||||
let path = PathBuf::from(path_str);
|
||||
// 仅清理属于本 plan 的 worktree
|
||||
if path.starts_with(&plan_wt_dir) {
|
||||
let _ = Command::new("git")
|
||||
.args(["worktree", "remove", "--force", path_str])
|
||||
.current_dir(project_root)
|
||||
.output();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除该 plan 的所有子任务分支
|
||||
let branches = Command::new("git")
|
||||
.args(["branch", "--list", &format!("subtask/{}/*", plan_id)])
|
||||
.current_dir(project_root)
|
||||
.output()
|
||||
.map_err(|e| WorktreeError::GitFailed(format!("git branch --list 失败: {}", e)))?;
|
||||
|
||||
let branches_str = String::from_utf8_lossy(&branches.stdout);
|
||||
for line in branches_str.lines() {
|
||||
let branch = line.trim();
|
||||
if !branch.is_empty() {
|
||||
let _ = Command::new("git")
|
||||
.args(["branch", "-D", branch])
|
||||
.current_dir(project_root)
|
||||
.output();
|
||||
}
|
||||
}
|
||||
|
||||
// 清理空目录
|
||||
let _ = std::fs::remove_dir_all(&plan_wt_dir);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 检测工程是否为 Git 仓库(有 .git 目录)。
|
||||
pub fn is_git_repo(project_root: &Path) -> bool {
|
||||
project_root.join(".git").exists()
|
||||
}
|
||||
|
||||
/// 获取工程当前分支名(用于作为 plan worktree 的 fork 基点)。
|
||||
pub fn current_branch(project_root: &Path) -> Result<String, WorktreeError> {
|
||||
let output = Command::new("git")
|
||||
.args(["rev-parse", "--abbrev-ref", "HEAD"])
|
||||
.current_dir(project_root)
|
||||
.output()
|
||||
.map_err(|e| WorktreeError::GitFailed(format!("git rev-parse 失败: {}", e)))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(WorktreeError::GitFailed(
|
||||
String::from_utf8_lossy(&output.stderr).trim().to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||
}
|
||||
|
||||
/// 创建 Plan worktree(Plan 级工作分支,所有 SubTask 分支 merge 到这里)。
|
||||
///
|
||||
/// 返回 (plan_worktree_path, plan_branch)。
|
||||
pub fn create_plan_worktree(
|
||||
project_root: &Path,
|
||||
plan_id: &str,
|
||||
) -> Result<(PathBuf, String), WorktreeError> {
|
||||
if !is_git_repo(project_root) {
|
||||
return Err(WorktreeError::NotGitRepo);
|
||||
}
|
||||
|
||||
let base = current_branch(project_root)?;
|
||||
let plan_branch = format!("plan/{}", plan_id);
|
||||
let plan_wt_path = project_root.join(".devflow/wt").join(plan_id);
|
||||
|
||||
if plan_wt_path.exists() {
|
||||
return Err(WorktreeError::AlreadyExists(plan_wt_path));
|
||||
}
|
||||
|
||||
let output = Command::new("git")
|
||||
.args([
|
||||
"worktree",
|
||||
"add",
|
||||
plan_wt_path.to_str().unwrap_or(""),
|
||||
"-b",
|
||||
&plan_branch,
|
||||
&base,
|
||||
])
|
||||
.current_dir(project_root)
|
||||
.output()
|
||||
.map_err(|e| WorktreeError::GitFailed(format!("git worktree add(plan) 失败: {}", e)))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(WorktreeError::GitFailed(format!(
|
||||
"git worktree add(plan) 失败: {}",
|
||||
String::from_utf8_lossy(&output.stderr).trim()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok((plan_wt_path, plan_branch))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 创建临时 Git 仓库 + 初始 commit
|
||||
fn setup_repo() -> tempfile::TempDir {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path();
|
||||
|
||||
Command::new("git").args(["init"]).current_dir(path).output().unwrap();
|
||||
Command::new("git").args(["config", "user.email", "test@test.com"]).current_dir(path).output().unwrap();
|
||||
Command::new("git").args(["config", "user.name", "test"]).current_dir(path).output().unwrap();
|
||||
|
||||
std::fs::write(path.join("main.rs"), "fn main() {}\n").unwrap();
|
||||
Command::new("git").args(["add", "."]).current_dir(path).output().unwrap();
|
||||
Command::new("git").args(["commit", "-m", "init"]).current_dir(path).output().unwrap();
|
||||
|
||||
dir
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wt_01_create_and_isolate() {
|
||||
let repo = setup_repo();
|
||||
let root = repo.path();
|
||||
let base = current_branch(root).unwrap();
|
||||
|
||||
let handle = create_worktree(root, "plan-test", "A", &base).unwrap();
|
||||
assert!(handle.path.exists(), "worktree 目录应存在");
|
||||
assert_eq!(handle.branch, "subtask/plan-test/A");
|
||||
|
||||
// 在 worktree 内写文件
|
||||
std::fs::write(handle.path.join("new_a.rs"), "// A 的改动").unwrap();
|
||||
// 主目录不应有此文件
|
||||
assert!(!root.join("new_a.rs").exists(), "主目录不应有 A 的改动");
|
||||
|
||||
// 清理
|
||||
remove_worktree(root, &handle).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wt_02_parallel_isolation() {
|
||||
let repo = setup_repo();
|
||||
let root = repo.path();
|
||||
|
||||
let plan_wt = create_plan_worktree(root, "plan-iso").unwrap();
|
||||
let plan_branch = plan_wt.1;
|
||||
|
||||
let a = create_worktree(root, "plan-iso", "A", &plan_branch).unwrap();
|
||||
let b = create_worktree(root, "plan-iso", "B", &plan_branch).unwrap();
|
||||
|
||||
// A 写 new_a.rs
|
||||
std::fs::write(a.path.join("new_a.rs"), "// A").unwrap();
|
||||
// B 写 new_b.rs
|
||||
std::fs::write(b.path.join("new_b.rs"), "// B").unwrap();
|
||||
|
||||
// A 的 worktree 不应有 B 的文件
|
||||
assert!(!a.path.join("new_b.rs").exists(), "A 不应看到 B 的改动");
|
||||
// B 的 worktree 不应有 A 的文件
|
||||
assert!(!b.path.join("new_a.rs").exists(), "B 不应看到 A 的改动");
|
||||
|
||||
remove_plan_worktrees(root, "plan-iso").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wt_03_commit_in_worktree() {
|
||||
let repo = setup_repo();
|
||||
let root = repo.path();
|
||||
|
||||
let plan_wt = create_plan_worktree(root, "plan-commit").unwrap();
|
||||
let a = create_worktree(root, "plan-commit", "A", &plan_wt.1).unwrap();
|
||||
|
||||
// 写文件 + commit
|
||||
std::fs::write(a.path.join("feature.rs"), "pub fn feature() {}").unwrap();
|
||||
commit_worktree(&a.path, "Add feature").unwrap();
|
||||
|
||||
// 验证分支有新 commit
|
||||
let log = Command::new("git")
|
||||
.args(["log", "--oneline", "-1"])
|
||||
.current_dir(&a.path)
|
||||
.output()
|
||||
.unwrap();
|
||||
let log_str = String::from_utf8_lossy(&log.stdout);
|
||||
assert!(log_str.contains("Add feature"), "commit 消息应在 log 中");
|
||||
|
||||
remove_plan_worktrees(root, "plan-commit").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wt_04_merge_no_conflict() {
|
||||
let repo = setup_repo();
|
||||
let root = repo.path();
|
||||
|
||||
let plan_wt = create_plan_worktree(root, "plan-merge").unwrap();
|
||||
let plan_wt_path = plan_wt.0.clone();
|
||||
let plan_branch = plan_wt.1;
|
||||
|
||||
// A 改 file_a.rs, B 改 file_b.rs(不同文件,无冲突)
|
||||
let a = create_worktree(root, "plan-merge", "A", &plan_branch).unwrap();
|
||||
std::fs::write(a.path.join("file_a.rs"), "// A").unwrap();
|
||||
commit_worktree(&a.path, "A changes").unwrap();
|
||||
|
||||
let b = create_worktree(root, "plan-merge", "B", &plan_branch).unwrap();
|
||||
std::fs::write(b.path.join("file_b.rs"), "// B").unwrap();
|
||||
commit_worktree(&b.path, "B changes").unwrap();
|
||||
|
||||
// merge A → plan
|
||||
let result_a = merge_branch(&plan_wt_path, &a.branch).unwrap();
|
||||
assert!(matches!(result_a, MergeResult::Clean), "A merge 应无冲突");
|
||||
|
||||
// merge B → plan
|
||||
let result_b = merge_branch(&plan_wt_path, &b.branch).unwrap();
|
||||
assert!(matches!(result_b, MergeResult::Clean), "B merge 应无冲突");
|
||||
|
||||
// plan worktree 应同时有 file_a.rs 和 file_b.rs
|
||||
assert!(plan_wt_path.join("file_a.rs").exists(), "plan 应含 A 的改动");
|
||||
assert!(plan_wt_path.join("file_b.rs").exists(), "plan 应含 B 的改动");
|
||||
|
||||
remove_plan_worktrees(root, "plan-merge").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wt_05_merge_conflict_same_file() {
|
||||
let repo = setup_repo();
|
||||
let root = repo.path();
|
||||
|
||||
// 写一个共享文件
|
||||
std::fs::write(root.join("shared.rs"), "line1\nline2\n").unwrap();
|
||||
Command::new("git").args(["add", "-A"]).current_dir(root).output().unwrap();
|
||||
Command::new("git").args(["commit", "-m", "add shared"]).current_dir(root).output().unwrap();
|
||||
|
||||
let plan_wt = create_plan_worktree(root, "plan-conf").unwrap();
|
||||
let plan_wt_path = plan_wt.0.clone();
|
||||
let plan_branch = plan_wt.1;
|
||||
|
||||
// A 和 B 改同一文件的同一行
|
||||
let a = create_worktree(root, "plan-conf", "A", &plan_branch).unwrap();
|
||||
std::fs::write(a.path.join("shared.rs"), "A_version\n").unwrap();
|
||||
commit_worktree(&a.path, "A change shared").unwrap();
|
||||
|
||||
let b = create_worktree(root, "plan-conf", "B", &plan_branch).unwrap();
|
||||
std::fs::write(b.path.join("shared.rs"), "B_version\n").unwrap();
|
||||
commit_worktree(&b.path, "B change shared").unwrap();
|
||||
|
||||
// merge A → 无冲突
|
||||
let result_a = merge_branch(&plan_wt_path, &a.branch).unwrap();
|
||||
assert!(matches!(result_a, MergeResult::Clean), "首个 merge 应无冲突");
|
||||
|
||||
// merge B → 冲突
|
||||
let result_b = merge_branch(&plan_wt_path, &b.branch).unwrap();
|
||||
assert!(
|
||||
matches!(result_b, MergeResult::Conflict { .. }),
|
||||
"同文件同行改应产生冲突"
|
||||
);
|
||||
|
||||
remove_plan_worktrees(root, "plan-conf").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wt_06_non_git_returns_error() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
// 不 init git
|
||||
let result = create_plan_worktree(dir.path(), "plan-ng");
|
||||
assert!(matches!(result, Err(WorktreeError::NotGitRepo)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wt_07_cleanup_removes_worktree() {
|
||||
let repo = setup_repo();
|
||||
let root = repo.path();
|
||||
|
||||
let plan_wt = create_plan_worktree(root, "plan-clean").unwrap();
|
||||
let a = create_worktree(root, "plan-clean", "A", &plan_wt.1).unwrap();
|
||||
assert!(a.path.exists());
|
||||
|
||||
remove_plan_worktrees(root, "plan-clean").unwrap();
|
||||
|
||||
// worktree 目录应被清理
|
||||
assert!(!a.path.exists(), "worktree 目录应已清理");
|
||||
// plan wt 目录也应被清理
|
||||
assert!(!plan_wt.0.exists(), "plan worktree 目录应已清理");
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ pub mod coordinator;
|
||||
// 会话意图识别层(纯函数,不接入 agentic loop)。依据 docs/02-架构设计/构想审查/
|
||||
// 意图识别层论证-2026-06-19.md。提供 recognize / tool_subset_for / suggested_model_tier。
|
||||
// 待 Phase B+C 完成 + 模型模态管理落地后再接入 agentic loop。
|
||||
pub mod git_worktree;
|
||||
pub mod intent;
|
||||
pub mod model_fetch;
|
||||
// model_fetch 的纯逻辑子模块(URL 拼接 / 噪音过滤 / 响应反序列化),
|
||||
|
||||
Reference in New Issue
Block a user