mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-10-31 22:37:25 +00:00
* 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 条「判据本身的判据」(防止判据假绿)。
* fix(sdk): 协议通道纪律 + hmapdev 模板升级 + 示例插件 stdout 污染
## 核心缺陷:插件往 stdout 打日志 = 污染 JSON-RPC 协议通道
子进程插件与内核用 stdout 传 NDJSON 帧,stderr 才是日志通道。
脚手架模板(及 4 个示例插件)用 `fmt.Printf` 打日志 ⇒ 内核收到非 JSON 文本:
[proc] weather 收到非法 JSON 帧(21 字节): invalid character 'i'
**生产实测 1038 条**(huawei_smarthome 720 / weather 113 / rss 103 / calendar 102)。
后果不只是日志噪音:那一行日志被**丢弃**,插件看起来"没启动"。
修法:
- hmapdev 模板 `fmt.Printf` → `log.Printf`(9 处)
- 示例插件 4 个 14 处:ai_image(2) calendar(5) rss(4) weather(3)
+ 补 "log" import
- 内核侧(主仓)同步让报错可定位:内容摘要 + 可能原因 + 修法
判别法:`package main` = 子进程(协议通道,禁用 stdout 打日志);
普通库包 = 进程内插件(编译进内核,写真实终端,**不是**缺陷)——
`internal/plugins/cli` 的 `fmt.Println` 属后者。
## hmapdev
- `cmd_build.go` +136:平台产物精确匹配(`--target` 是**append** 语义,
原脚本 `ls dist/*.hmap | head -1` 会按字典序抓到 amd64,
**导致 arm64 包里装的是 x86-64 二进制**,且退出码 0、校验和通过)
- `cmd_init.go` +46:脚手架 stdio 纪律
- `templates/proc_main.go.tmpl`:补齐 mgrStop/mgrStart/mgrIsRuntimeStopped
三态能力(插件管理器运行态停启)
- `templates/plg.json.tmpl` 删除(合并进 cmd_init)
## 判据
- `scaffold_stdout_test.go`(3 条):扫模板生成的代码不得写 stdout。
★ 判据自身先假红过一次:**模板的注释里就写了 `fmt.Printf`** ⇒
必须 `stripTemplateComments()` 之后才判
- `example_stdout_test.go`(2 条):扫全部 20 个子进程示例插件,
报错给**文件:行号**;变异咬住并给出准确行号
- `package_examples_test.go`(2 条):**执行真实打包脚本**
(第一版测的是 hmapdev 而不是脚本 ⇒ 变异不咬)
- `cmd_build` / `cmd_init` 新判据
## 其他
- sdk/settings.go +15(三态能力透传)
- `docs/versions.md` +§示例插件协议通道纪律(含生产 1038 条实测表格)
- `skills/homeagent-plugin-dev/SKILL.md` +§协议通道纪律(正误代码对比)
* fix(examples): 恢复 go.mod 相对路径 + 移除 plg.json 的 sdk 固定(修 CI 红)
CI 上 `Examples (linux/amd64)` 与 `(darwin/arm64)` 双红,只有两个失败:
✗ commondev 构建失败
✗ editdoc 构建失败
error: 项目声明 sdk=1.4.0,但本地 SDK 存储里没有这个版本
其余 20 个示例全部成功 —— 说明不是通用问题,而是这两个被改坏了。
## 根因(两个叠加)
**① go.mod 被本机绝对路径污染**
我在上一个提交里用 `git add -A`,把本机 `go build` **自动改写**过的
go.mod 一并提交了:
require github.com/JianFeeeee/homeagentsdk v1.4.0
replace github.com/JianFeeeee/homeagentsdk => /root/.homeagent/hmapdev/sdk/v1.4.0
基线(origin/main)是:
require github.com/JianFeeeee/homeagentsdk v0.0.0
replace github.com/JianFeeeee/homeagentsdk => ../../
本机那个目录真的存在 ⇒ 本地全绿;CI 没有 ⇒ 必红。
**② plg.json 声明了 `"sdk": "1.4.0"`**
一声明版本,hmapdev 就去**本地 SDK store** 找它,找不到直接拒绝构建。
全仓 20+ 示例**都不声明**,只有我改过的这两个声明了。
示例的正确依赖表达是 go.mod 的 `v0.0.0` + `replace ../../`。
## 修法
两个文件都恢复到与其余示例一致的写法。
## 判据(3 条,都是新加的)
· `TestExampleGoModHasNoAbsoluteReplace`:任何示例 go.mod 不得含
`/root/`、`/home/`、`/Users/`、`C:\`
· `TestExampleGoModUsesRelativeSDKReplace`:replace 必须是相对路径 `../../`
· `TestExamplePlgJsonDoesNotPinSDKVersion`:plg.json 不得声明 sdk
变异 2/2 咬住(重新引入绝对路径 / 重新声明 sdk,都准确报出文件路径)。
## 本地验证(模拟 CI 干净环境)
用干净 HOME 跑 `package/build-examples.sh`:
· 修复前:成功 18 / 失败 4(commondev, editdoc, browser, rss)
· 修复后:成功 20 / 失败 2(browser, rss —— **本机代理不通**,
`proxy.golang.org` 超时;用本地 modcache 验证这两个可正常构建)
即 22/22 源码都能构建,CI 有网络不受影响。
## 教训
`git add -A` 会把「构建工具自动改写的文件」一起提交。
提交前必须 `git status` 逐个看,尤其 go.mod / go.sum。
---------
Co-authored-by: JianFeeeee <JianFeeeee@users.noreply.gitcode.com>
388 lines
14 KiB
Go
388 lines
14 KiB
Go
package main
|
||
|
||
// pi 运行时配置类的判据(llmsource / pi_plugin / pi_mcp / pi_config)。
|
||
//
|
||
// ## 为什么补这批
|
||
//
|
||
// 覆盖度审计显示这批 14 个工具**没有任何机制级测试**,而它们
|
||
// **直接写 pi 的配置文件**(models.json / auth.json / settings.json /
|
||
// mcp.json)—— 是本插件里风险最高的一组:
|
||
// - 写坏 models.json 会让内层 pi 完全起不来;
|
||
// - 把 key 泄漏进返回值会经由 agent 记忆外泄;
|
||
// - 改错目录会污染用户的全局 pi(`~/.pi`)。
|
||
//
|
||
// 已有 audit_test.go 覆盖了两条关键纪律(损坏不覆盖、备份回滚),
|
||
// 这里补齐其余:**不泄漏 key**、**作用域隔离**、**幂等**、**错误可诊断**。
|
||
|
||
import (
|
||
"encoding/json"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
func homeDir() string {
|
||
h, _ := os.UserHomeDir()
|
||
return h
|
||
}
|
||
|
||
// newPiPlugin 造一个 piAgentDir 落在临时目录的插件。
|
||
func newPiPlugin(t *testing.T) *Plugin {
|
||
t.Helper()
|
||
p := newFlowPlugin(t)
|
||
// piAgentDir = <插件数据>/runtime/agentdir;p.path 的父目录就是插件数据目录
|
||
if err := os.MkdirAll(p.piAgentDir(), 0o755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return p
|
||
}
|
||
|
||
// writePiModels 写一个 models.json。
|
||
func writePiModels(t *testing.T, dir string, m map[string]interface{}) {
|
||
t.Helper()
|
||
b, _ := json.MarshalIndent(m, "", " ")
|
||
if err := os.WriteFile(filepath.Join(dir, "models.json"), b, 0o600); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:`pi_source_list` **绝不返回 api key**。
|
||
//
|
||
// 这是全插件最硬的一条纪律(设计 §7.2):key 一旦进返回值,
|
||
// 就会经由 agent 记忆、日志、UI 三处外泄,且**事后无法收回**。
|
||
func TestPiSourceListNeverReturnsKey(t *testing.T) {
|
||
p := newPiPlugin(t)
|
||
dir := p.piAgentDir()
|
||
writePiModels(t, dir, map[string]interface{}{
|
||
"providers": map[string]interface{}{
|
||
"myprov": map[string]interface{}{
|
||
"baseUrl": "https://api.example.com",
|
||
"api": "openai",
|
||
"apiKey": "sk-SUPER-SECRET-VALUE-12345",
|
||
"models": []interface{}{map[string]interface{}{"id": "m1"}},
|
||
},
|
||
},
|
||
})
|
||
|
||
res, err := p.handlePiSourceList(map[string]interface{}{})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// 把整个返回值序列化,确认秘密字符串**任何形态都不出现**
|
||
blob, _ := json.Marshal(res)
|
||
s := string(blob)
|
||
if strings.Contains(s, "sk-SUPER-SECRET") {
|
||
t.Fatalf("★ pi_source_list 泄漏了 api key: %s", s)
|
||
}
|
||
// 也不该泄漏 auth.json 里的凭据(这里没建,但格式化检查同样适用)
|
||
if strings.Contains(strings.ToLower(s), "secret-value") {
|
||
t.Fatalf("★ 返回值含疑似凭据: %s", s)
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:**作用域隔离** —— 一律指向插件自己的 agentdir,绝不碰用户全局 pi。
|
||
//
|
||
// 纪律 4(设计 §3.5.2):piScopeDir 必须带显式 agentdir。
|
||
// 违反的话会改掉用户 `~/.pi` 的配置 —— 那是**跨项目污染**,很难查。
|
||
func TestPiScopeNeverTouchesUserGlobalPi(t *testing.T) {
|
||
p := newPiPlugin(t)
|
||
// plugin 作用域(默认)→ 插件自己的 agentdir
|
||
got, err := p.piScopeDir(map[string]interface{}{"scope": "plugin"})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got != p.piAgentDir() {
|
||
t.Errorf("plugin 作用域应指向插件 agentdir: %s vs %s", got, p.piAgentDir())
|
||
}
|
||
// 必须是**插件数据目录**下的 runtime/agentdir,不是用户全局 ~/.pi
|
||
if strings.HasPrefix(got, filepath.Join(homeDir(), ".pi")) {
|
||
t.Errorf("★ plugin 作用域指向了用户全局 pi: %s", got)
|
||
}
|
||
// project 作用域 → **必须显式给 workdir**(不给就报错,不猜)
|
||
if _, err := p.piScopeDir(map[string]interface{}{"scope": "project"}); err == nil {
|
||
t.Error("★ scope=project 缺 workdir 应报错(不猜一个目录)")
|
||
}
|
||
// ★ 更强的约束:workdir 必须落在**插件的 program/ 工程目录**下。
|
||
// 任意目录不行 —— 否则 scope=project 会往用户随便什么目录里写 .pi。
|
||
if _, err := p.piScopeDir(map[string]interface{}{
|
||
"scope": "project", "workdir": t.TempDir(),
|
||
}); err == nil {
|
||
t.Error("★ 任意目录不该被接受(必须在 program/ 下)")
|
||
}
|
||
wd := filepath.Join(p.programRoot(), "testwd")
|
||
if err := os.MkdirAll(wd, 0o755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
gp, err := p.piScopeDir(map[string]interface{}{"scope": "project", "workdir": wd})
|
||
if err != nil {
|
||
t.Fatalf("program/ 下的 workdir 应被接受: %v", err)
|
||
}
|
||
if !strings.HasPrefix(gp, wd) {
|
||
t.Errorf("★ project 作用域应在工作目录内: %s", gp)
|
||
}
|
||
}
|
||
|
||
// ★★ 判据:`pi_source_default` 写入的是 settings.json 的**正确键名**。
|
||
//
|
||
// 键名写错的表现是"设了默认源但不生效",且没有任何报错 ——
|
||
// 属于典型的静默失效。
|
||
func TestPiSourceDefaultWritesExpectedKeys(t *testing.T) {
|
||
p := newPiPlugin(t)
|
||
dir := p.piAgentDir()
|
||
writePiModels(t, dir, map[string]interface{}{
|
||
"providers": map[string]interface{}{
|
||
"prov1": map[string]interface{}{
|
||
"baseUrl": "http://x", "api": "openai", "models": []interface{}{map[string]interface{}{"id": "m1"}},
|
||
},
|
||
},
|
||
})
|
||
res, err := p.handlePiSourceDefault(map[string]interface{}{"name": "prov1"})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if m, ok := res.(map[string]interface{}); ok {
|
||
if e, bad := m["error"]; bad {
|
||
t.Fatalf("设默认源失败: %v", e)
|
||
}
|
||
}
|
||
// 读回 settings.json 确认键名
|
||
b, err := os.ReadFile(filepath.Join(dir, "settings.json"))
|
||
if err != nil {
|
||
t.Fatalf("settings.json 未生成: %v", err)
|
||
}
|
||
var st map[string]interface{}
|
||
if err := json.Unmarshal(b, &st); err != nil {
|
||
t.Fatalf("settings.json 不是合法 JSON: %v\n%s", err, b)
|
||
}
|
||
if st["defaultProvider"] != "prov1" {
|
||
t.Errorf("★ defaultProvider 未正确写入: %v", st["defaultProvider"])
|
||
}
|
||
// 必须同时给默认 model(只给 provider 的话 pi 不知道用哪个模型)
|
||
if _, has := st["defaultModel"]; !has {
|
||
t.Errorf("★ 缺 defaultModel(只给 provider,pi 不知用哪个模型): %v", st)
|
||
}
|
||
}
|
||
|
||
// ★★ 判据:设默认源指向**不存在的源**必须报错(不静默写进去)。
|
||
func TestPiSourceDefaultRejectsUnknownSource(t *testing.T) {
|
||
p := newPiPlugin(t)
|
||
writePiModels(t, p.piAgentDir(), map[string]interface{}{
|
||
"providers": map[string]interface{}{
|
||
"only": map[string]interface{}{"baseUrl": "http://x", "api": "openai", "models": []interface{}{map[string]interface{}{"id": "m"}}},
|
||
},
|
||
})
|
||
res, _ := p.handlePiSourceDefault(map[string]interface{}{"name": "nonexistent"})
|
||
m, ok := res.(map[string]interface{})
|
||
if !ok {
|
||
t.Skipf("返回形状未知: %T", res)
|
||
}
|
||
if _, isErr := m["error"]; !isErr {
|
||
t.Errorf("★ 指向不存在的源应报错: %v", m)
|
||
}
|
||
}
|
||
|
||
// ★★ 判据:`pi_source_remove` 幂等(删不存在的源不报致命错)+ 真的删掉。
|
||
func TestPiSourceRemoveIsIdempotentAndEffective(t *testing.T) {
|
||
p := newPiPlugin(t)
|
||
dir := p.piAgentDir()
|
||
writePiModels(t, dir, map[string]interface{}{
|
||
"providers": map[string]interface{}{
|
||
"kill": map[string]interface{}{"baseUrl": "http://x", "api": "openai", "models": []interface{}{map[string]interface{}{"id": "m"}}},
|
||
"keep": map[string]interface{}{"baseUrl": "http://y", "api": "openai", "models": []interface{}{map[string]interface{}{"id": "m"}}},
|
||
},
|
||
})
|
||
if _, err := p.handlePiSourceRemove(map[string]interface{}{"name": "kill"}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
b, _ := os.ReadFile(filepath.Join(dir, "models.json"))
|
||
if strings.Contains(string(b), `"kill"`) {
|
||
t.Errorf("★ 源未被删除:\n%s", b)
|
||
}
|
||
if !strings.Contains(string(b), `"keep"`) {
|
||
t.Errorf("★ 误删了别的源:\n%s", b)
|
||
}
|
||
// 再删一次:不该炸(幂等)
|
||
if _, err := p.handlePiSourceRemove(map[string]interface{}{"name": "kill"}); err != nil {
|
||
t.Errorf("重复删除应幂等: %v", err)
|
||
}
|
||
}
|
||
|
||
// ★★ 判据:`pi_source_list` 在**配置不存在**时给出空列表而不是报错。
|
||
//
|
||
// 新部署上 models.json 尚不存在是常态;报错会让调用方以为坏了。
|
||
func TestPiSourceListOnFreshInstall(t *testing.T) {
|
||
p := newPiPlugin(t) // 不写任何配置
|
||
res, err := p.handlePiSourceList(map[string]interface{}{})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
m, ok := res.(map[string]interface{})
|
||
if !ok {
|
||
t.Skipf("返回形状未知: %T", res)
|
||
}
|
||
if e, bad := m["error"]; bad {
|
||
t.Errorf("★ 无配置时应返回空列表而非报错: %v", e)
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:**损坏的 mcp.json 必须拒绝覆盖**(与 models.json 同一纪律)。
|
||
//
|
||
// 静默重建会抹掉用户已配的 MCP 服务器,且用户不会立刻发现。
|
||
func TestCorruptPiMcpIsNotSilentlyOverwritten(t *testing.T) {
|
||
p := newPiPlugin(t)
|
||
dir := p.piAgentDir()
|
||
orig := []byte("{ this is broken json")
|
||
if err := os.WriteFile(filepath.Join(dir, "mcp.json"), orig, 0o600); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
res, _ := p.handlePiMCPAdd(map[string]interface{}{
|
||
"name": "x", "url": "http://localhost:1234",
|
||
})
|
||
if m, ok := res.(map[string]interface{}); ok {
|
||
if _, isErr := m["error"]; !isErr {
|
||
t.Errorf("★ 损坏的 mcp.json 应被拒绝(不静默覆盖): %v", m)
|
||
}
|
||
}
|
||
// 原文件必须**未被改动**
|
||
after, _ := os.ReadFile(filepath.Join(dir, "mcp.json"))
|
||
if string(after) != string(orig) {
|
||
t.Errorf("★ 损坏文件被覆盖了:\nbefore=%s\nafter=%s", orig, after)
|
||
}
|
||
}
|
||
|
||
// ★★ 判据:`pi_mcp_list` 在无配置时给空列表。
|
||
func TestPiMcpListOnFreshInstall(t *testing.T) {
|
||
p := newPiPlugin(t)
|
||
res, err := p.handlePiMCPList(map[string]interface{}{})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if m, ok := res.(map[string]interface{}); ok {
|
||
if _, bad := m["error"]; bad {
|
||
t.Errorf("★ 无配置时不该报错: %v", m)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ★★ 判据:`pi_config_set` 拒绝**不存在的键**或返回明确错误。
|
||
//
|
||
// 静默接受未知键的后果是"我设了但没生效",且没有任何线索。
|
||
func TestPiConfigSetUnknownKeyIsDiagnosable(t *testing.T) {
|
||
p := newPiPlugin(t)
|
||
res, _ := p.handlePiConfigSet(map[string]interface{}{
|
||
"key": "definitely_not_a_real_pi_key_xyz", "value": "v",
|
||
})
|
||
m, ok := res.(map[string]interface{})
|
||
if !ok {
|
||
return // 返回形状不同则不判
|
||
}
|
||
// 要么报错,要么明确说明写了什么 —— 但不能是"静默成功且无痕迹"
|
||
if _, isErr := m["error"]; !isErr {
|
||
if _, hasKey := m["key"]; !hasKey {
|
||
t.Errorf("★ 未知键既未报错也未回显,调用方无法判断是否生效: %v", m)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ★★ 判据:`project_open` 是 `project_add` 的**真别名**(行为一致)。
|
||
func TestProjectOpenIsAliasOfAdd(t *testing.T) {
|
||
p := newFlowPlugin(t)
|
||
r1, _ := p.handleProjectOpen(map[string]interface{}{
|
||
"name": "aliased", "remote_url": "/tmp/a.git",
|
||
})
|
||
if m, ok := r1.(map[string]interface{}); ok {
|
||
if _, bad := m["error"]; bad {
|
||
t.Fatalf("project_open 失败: %v", m)
|
||
}
|
||
}
|
||
// 台账里必须真的登记了(别名不能是空壳)
|
||
if _, ok := p.ledger.Projects["aliased"]; !ok {
|
||
t.Error("★ project_open 未真正登记项目(别名是空壳)")
|
||
}
|
||
// 与 add 的重复检测行为一致
|
||
r2, _ := p.handleProjectOpen(map[string]interface{}{"name": "aliased", "remote_url": "/tmp/b.git"})
|
||
if _, isErr := r2.(map[string]interface{})["error"]; !isErr {
|
||
t.Error("★ 别名未继承 add 的重复检测")
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:写进 auth.json 的凭据格式**必须是 pi 认的 schema**。
|
||
//
|
||
// 这是实测踩出来的真缺陷(2026-10-15):`pi_source_add` / `pi_source_import`
|
||
// 原先写 `{"apiKey": "..."}`,而 pi 认的是 `{"type":"api_key","key":"..."}`
|
||
// (权威定义:pi 的 dist/core/auth-storage.js:185 `value.type === "api_key"`)。
|
||
//
|
||
// 后果**完全静默**:three 个配置文件看起来都正常,但 pi 的 auth 读取返回空,
|
||
// 于是报 `Provider is not configured: <name>` —— 内嵌 pi 谁都调不动。
|
||
//
|
||
// 定位过程值得记:同一个 pi API、同一份 models.json,只切
|
||
// `PI_CODING_AGENT_DIR` 就一边跑通一边跑不通,逐字段对比才落到键名上。
|
||
// 所以判据必须**断言键名与取值**,不能只看"文件写成功了"。
|
||
func TestPiAuthWrittenInPiSchema(t *testing.T) {
|
||
p := newPiPlugin(t)
|
||
dir := p.piAgentDir()
|
||
res, _ := p.handlePiSourceAdd(map[string]interface{}{
|
||
"name": "prov",
|
||
"base_url": "http://127.0.0.1:8081/v1",
|
||
"api": "openai-completions",
|
||
"api_key": "sk-test-SCHEMA-CHECK",
|
||
"model": "AUTO",
|
||
})
|
||
if m, ok := res.(map[string]interface{}); ok {
|
||
if e, bad := m["error"]; bad {
|
||
t.Fatalf("添加源失败: %v (%v)", e, m["details"])
|
||
}
|
||
}
|
||
|
||
b, err := os.ReadFile(filepath.Join(dir, "auth.json"))
|
||
if err != nil {
|
||
t.Fatalf("auth.json 未生成: %v", err)
|
||
}
|
||
var auth map[string]map[string]interface{}
|
||
if err := json.Unmarshal(b, &auth); err != nil {
|
||
t.Fatalf("auth.json 不是合法 JSON: %v\n%s", err, b)
|
||
}
|
||
entry, ok := auth["prov"]
|
||
if !ok {
|
||
t.Fatalf("auth.json 里没有 prov: %s", b)
|
||
}
|
||
// ★ 核心断言:必须是 pi 的 schema
|
||
if entry["type"] != "api_key" {
|
||
t.Errorf("★ entry.type 必须是 \"api_key\"(pi 的 auth-storage 按它判别),实际 %#v", entry["type"])
|
||
}
|
||
if entry["key"] != "sk-test-SCHEMA-CHECK" {
|
||
t.Errorf("★ entry.key 不对: %#v", entry["key"])
|
||
}
|
||
// 旧格式的键不该出现(否则说明还有一处没改)
|
||
if _, bad := entry["apiKey"]; bad {
|
||
t.Errorf("★ auth.json 仍有旧的 \"apiKey\" 键(pi 不认): %s", b)
|
||
}
|
||
}
|
||
|
||
// ★★ 判据:**导入内核源**也写 pi 的 schema(两条路径必须一致)。
|
||
//
|
||
// source_add 与 source_import 是两个入口,写同一份 auth.json。
|
||
// 只改一处的话另一条路径仍然静默失效。
|
||
func TestPiAuthImportAlsoUsesPiSchema(t *testing.T) {
|
||
// 直接验证构造器(import 路径的写入最终也走它)
|
||
newPiPlugin(t)
|
||
e := newPiAuthEntry("sk-x")
|
||
if e["type"] != "api_key" || e["key"] != "sk-x" {
|
||
t.Errorf("★ newPiAuthEntry 未产出 pi schema: %#v", e)
|
||
}
|
||
// 反向:确保没有留下产出旧格式的地方
|
||
src, err := os.ReadFile("llmsource.go")
|
||
if err == nil {
|
||
if strings.Contains(string(src), `"apiKey": apiKey`) {
|
||
t.Error("★ llmsource.go 里仍有写旧格式 apiKey 的地方")
|
||
}
|
||
}
|
||
src2, err2 := os.ReadFile("pi_tools.go")
|
||
if err2 == nil {
|
||
if strings.Contains(string(src2), `map[string]string{"apiKey"`) {
|
||
t.Error("★ pi_tools.go 里仍有写旧格式的地方")
|
||
}
|
||
}
|
||
}
|