Files
homeagent-sdk/example/commondev/workdir_test.go
JianFeeeee c2474f57a2 feat(commondev): 主管式多仓库开发流水线 + sidecar 后端抽象(pi/zcode)
## 是什么

agent 统筹开发流程的插件:登记项目 → 开工程线(每条线一个隔离工作目录 +
一个 sidecar 子进程 + 一对通道)→ 投递任务 → 同会话续投 → 评审/合入。

38 个工具:project_*/branch_*/workdir_*/job_*/review_*/test_*/
endpoint_*/agent_*/pi_*/conflict_check。

## sidecar 后端抽象(本轮核心)

传输层(local/ssh)与后端(pi/zcode/自定义)**正交**,四个自由组合:

  driver.json 描述一个后端:script / cliPath / env / args / llmConfig
  三级查找:插件数据目录 → **随包 rawfile** → 内置 pi 描述
  占位符:{workdir} {dataDir} {pluginDir} {rawfile} {sessionId}

★ 内置 pi 回退**与默认后端解耦**(2026-10-17 修):原实现写
`if cli == defaultAgentCLI { return builtinPiDriver() }`,把"给谁兜底"
和"默认选谁"耦合到同一常量 —— 一旦有人改默认,pi 当场失去兜底,
显式 cli="pi" 开始报「驱动不存在」。现在判断字面 "pi"。

默认后端保持 **pi**(我们本地主用);交付方用 zcode 走两种局部开关:
 · 单线:commondev_branch_open(cli="zcode")
 · 按机器:commondev_endpoint_set(host=..., cli="zcode")

## zcode 接入(三条真机判据,全部要求真改工程文件)

 · TestZcodeFullPipeline(29-34s):随包 driver + 真实 ZCode CLI
 · TestZcodeOverSSH(39s):真 ssh + scp,远端 node 跑 zcode
 · TestZcodeOverSSHWithPerHostEndpoints(68s):两个 host 别名配不同
   cli_path,走端点表而不是全局 settings

★ 会话库必须隔离到工作区(sidecar.mjs 的 ensureWorkspaceSessionStore):
实测 ZCode 的会话库路径**硬编码 homedir()**,`ZCODE_DATA_BASE_DIR` 对它
完全无效(26 个会话全落全局库)。但 `<workdir>/.zcode/config.json` 的
`storage.sessionDbPath` **确实生效**。所以启动前写一份工作区配置,
一举解决:多线不共用历史 / 不污染全局 ~/.zcode / 删线不留孤儿会话。
只在不存在时写 —— 用户手工放的要尊重。

## 其他

 · rawfile:项目根 rawfile/ 默认全收,解到 <插件目录>/rawfile/,绝对路径拒绝
 · 凭证:命名引用 + ${ENV_VAR},独立 0600 文件,绝不进 config.db(0644)
 · secretshell:给不受信 agent 用的安全 shell(ackey + safeshell)
 · 冲突检查 / 评审判据 / 镜像 / 每条线独立 workdir(hash 命名,合并后删除)

判据 30+ 条,含 4 条「判据本身的判据」(防止判据假绿)。
2026-10-17 16:16:04 +08:00

72 lines
2.4 KiB
Go

package main
import (
"path/filepath"
"testing"
)
// ★ 核心判据(用户 2026-10-14 明确要求):
// **同一个仓库的不同分支,必然是不同的目录** —— 否则两个 agent 会共用工作树,
// 未提交改动互相污染,"独立 clone" 的前提就不成立了。
func TestSameRepoDifferentBranchesGetDistinctDirs(t *testing.T) {
root := "/data/program"
cases := []struct{ project, branch string }{
{"TrueAgent", "feat/a"},
{"TrueAgent", "feat/b"}, // ← 同仓库、不同分支:必须不同
{"TrueAgent", "main"},
{"TrueAgent", "feat/a"}, // ← 与第一条同:必须相同(可重算)
{"Other", "feat/a"}, // ← 不同仓库、同名分支:必须不同
{"TrueAgent", "feat/a/b"}, // 嵌套斜杠
{"TrueAgent", "feat.a"}, // 点(与 feat/a 不同名,应不同目录)
}
seen := map[string]string{}
for _, c := range cases {
got := branchDir(root, c.project, c.branch)
key := c.project + "|" + c.branch
if prev, dup := seen[got]; dup && prev != key {
t.Errorf("目录冲突:%s 与 %s 都得到 %s", prev, key, got)
}
seen[got] = key
}
// 明确钉住用户关心的那条
a := branchDir(root, "TrueAgent", "feat/a")
b := branchDir(root, "TrueAgent", "feat/b")
if a == b {
t.Fatalf("同仓库不同分支得到同一目录:%s", a)
}
// 可重算:同输入必同输出(清理孤儿目录依赖这一点)
if branchDir(root, "TrueAgent", "feat/a") != a {
t.Fatal("目录名不可重算")
}
}
// 目录名形态:<hash>-<name>,且 name 可读(含分支信息)。
func TestDirNameShape(t *testing.T) {
d := dirName("TrueAgent", "feat/cool-thing")
hash, name, ok := parseDirName(d)
if !ok {
t.Fatalf("目录名 %q 不可解析", d)
}
if len(hash) != identityHashLen {
t.Errorf("hash 位数不对: %q", hash)
}
if name != "feat_cool-thing" {
t.Errorf("name 应可读(分支派生): %q", name)
}
// 目录内的路径应落在 program/ 下
if filepath.Dir(branchDir("/x/program", "P", "b")) != "/x/program" {
t.Error("目录未落在 program/ 下")
}
}
// 非法字符被派生掉(目录名必须能安全用于文件系统与通道名)。
func TestDirNameSanitized(t *testing.T) {
for _, br := range []string{"feat/a b", "中文", "a.b", "a/b"} {
d := dirName("P", br)
if !validName(d) {
t.Errorf("分支 %q 派生的目录名 %q 含非法字符", br, d)
}
}
}