mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 14:57:38 +00:00
## 死锁(这是 CI 连续两次红的真因) `StopAll` 与 `StopPlugin` 都在持有 `r.mu` **写锁**时调用 `RemoveBuiltinSkill` → `skillDirOf` → `r.mu.RLock()` ⇒ **自死锁**。 后果不是报错而是**卡死**: - internal/plugins 卡 300s 超时(TestCapability_RestrictedPluginStillLoads) - internal/plugin 卡 840s 超时(TestStopPluginDoesNotTouchPersistentState) - CI 的 `go test ./...` 与 `go test -race` 双双红 修法:拆出**不加锁**的内部版本,持锁路径只走它: - `skillDirFrom(dataDir, name)`(纯函数) - `removeBuiltinSkillLocked(pluginName, p, dataDir)`(显式传 dataDir, 避免"看起来没取锁、一改就取锁"的隐患) - `announceSkillRemovedLocked`(持锁下直读 r.evBus) 修后:internal/plugins 300s → 12s,internal/plugin 840s → 0.7s。 ## 防回归判据 `TestStopAllAndStopPluginDoNotDeadlock`:死锁**不返回**,普通断言抓不到, 所以用**超时**当判据(5 秒未返回即失败),并指明应改用 Locked 版本。 ## tools_ready 事件声明 新增的 tools_ready 触发 `TestEveryPublishedEventHasAnOutwardPath` (该判据防"事件发了、客户端收不到"的静默失效)。 tools_ready 是**内核内部**就绪信号(plugin-host → seq 等订阅者), 不是对话流程事件,前端无需感知 ⇒ 在 allowed 里显式声明理由。 ## 验证 go build ./... / go vet ./... / go test -p 1 ./... / -race 全通过。
87 lines
2.9 KiB
Go
87 lines
2.9 KiB
Go
package seq
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/JianFeeeee/HologramAgent/internal/plugin"
|
||
)
|
||
|
||
// 编译期断言:seq 必须实现 plugin.SkillProvider,
|
||
// 否则内核的 InstallBuiltinSkill 会因类型断言失败而静默跳过。
|
||
var _ plugin.SkillProvider = (*Plugin)(nil)
|
||
|
||
func TestSkillProviderContract(t *testing.T) {
|
||
p := New("seq")
|
||
skills := p.BuiltinSkills()
|
||
if len(skills) == 0 {
|
||
t.Fatal("BuiltinSkills 返回空 —— model 永远看不到 seq")
|
||
}
|
||
body, ok := skills[builtinSkillName]
|
||
if !ok {
|
||
t.Fatalf("缺 %q 条目", builtinSkillName)
|
||
}
|
||
if len(body) < 200 {
|
||
t.Fatalf("SKILL 正文过短(%d 字节),不足以让模型知道怎么用", len(body))
|
||
}
|
||
}
|
||
|
||
// ── 使用边界必须写清楚(B 方案的判据)──
|
||
//
|
||
// 决策背景:曾试图"让模型多用 seq"(加系统提示词块、加推销式措辞),
|
||
// e2e 实测证明那是错的方向 —— 对一次性的多文件读,模型并行调
|
||
// files_read 就是最优解,包成序列纯亏。模型不用它是对的。
|
||
//
|
||
// 所以文档改为**直说边界**:多数时候不该用,只在"要反复跑同一套流程"时才划算。
|
||
// 这条判据守的就是这个立场不被后续"优化"悄悄推回推销腔。
|
||
func TestSkillStatesNonUseCases(t *testing.T) {
|
||
body := New("seq").BuiltinSkills()[builtinSkillName]
|
||
|
||
// 必须明确写"多数时候不该用"这个立场
|
||
for _, want := range []string{"多数时候不该用", "不划算", "只做一次"} {
|
||
if !strings.Contains(body, want) {
|
||
t.Fatalf("SKILL 必须写清不适用情形(缺 %q)——否则会重新变成推销", want)
|
||
}
|
||
}
|
||
// 必须点出"并行调工具已经最优"这个真实反例(e2e 实测结论)
|
||
if !strings.Contains(body, "并行") {
|
||
t.Fatal("SKILL 应点明「几个工具可同批并行时,包进序列只是多一层壳」这个反例")
|
||
}
|
||
// 适用判据必须是「要反复跑」而不是「操作多」
|
||
if !strings.Contains(body, "反复") {
|
||
t.Fatal("SKILL 的核心判据应是「这套流程要反复跑」,不是「操作数量多」")
|
||
}
|
||
}
|
||
|
||
// 索引行(模型每轮唯一能看到的一行)也必须带边界,不能只有卖点。
|
||
func TestSkillIndexLineStatesBoundary(t *testing.T) {
|
||
body := New("seq").BuiltinSkills()[builtinSkillName]
|
||
// 取正文首段(内核 extractDescription 的取值方式)
|
||
lines := strings.Split(body, "\n")
|
||
inFM, started := false, false
|
||
desc := ""
|
||
for i, l := range lines {
|
||
t := strings.TrimSpace(l)
|
||
if i == 0 && t == "---" {
|
||
inFM = true
|
||
continue
|
||
}
|
||
if inFM {
|
||
if t == "---" {
|
||
inFM, started = false, true
|
||
}
|
||
continue
|
||
}
|
||
if started && t != "" && !strings.HasPrefix(t, "#") {
|
||
desc = t
|
||
break
|
||
}
|
||
}
|
||
if desc == "" {
|
||
t.Fatal("取不到索引行(正文首段为空)")
|
||
}
|
||
if !strings.Contains(desc, "反复") || !strings.Contains(desc, "一次性") {
|
||
t.Fatalf("索引行必须同时说清适用与不适用,实际:%q", desc)
|
||
}
|
||
}
|