mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-02 23:33:58 +00:00
Phase 0.1/1/3/6: 核心生产问题修复
Healthcheck 隔离 (Phase 0.1): - 新增 internal/sdk/selftest.go: VirtualInstance 完全隔离自检空间 - PluginSDK.Selftest()/SelftestReset() 暴露隔离实例 (含 mutex) - LLM 自检只读白名单 isSafeReadonlyTool 防写类工具污染生产 - 单测验证: healthcheck 后生产实例内容不变 + 无残留 - 存量清理: 删除 gotest/luatest 残留目录 GraphDB 去重 (Phase 1): - migrateRelationUnique: 启动自动重建 relations 表加 UNIQUE 约束并去重 - Commit 改为存在性检查, 重复三元组仅刷新 confidence 不重复插入 - 3 个 dedup 单测全绿 配置时长解析 (Phase 3): - parseDurationExtended 支持 2d/1w/3h 等人类可读单位 - GetDuration 全局生效, 防 2d 静默回退 30m Agentcli 通知风暴治理 (Phase 6): - 语义通知: 累积 notify_bytes(2KB) 或间隔 notify_interval(2s) 触发 - 生命周期即时通知: 启动/进程退出/EOF 立即通知 - 可配置 settings, 保留通知机制保证 agent 感知终端存在 - 运维止血: 已杀掉幽灵 PID 3716282 (bash git sparse clone 运行 16h) Plan.md: 新增设计意图备忘(插件即App/分层记忆), 更新各 Phase 进度
This commit is contained in:
@ -3,6 +3,7 @@ package healthcheck
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
agentCore "gitcode.com/JianFeeeee/HomeAgent/internal/agent/core"
|
||||
@ -196,6 +197,20 @@ func TestHealthcheckWithMemory(t *testing.T) {
|
||||
}
|
||||
defer memDB.Close()
|
||||
|
||||
// 预置一条生产数据,验证 healthcheck 自检后不触碰它
|
||||
_, _, err = memDB.Commit([]memory.Triple{{Subject: "用户", Relation: "喜欢", Object: "咖啡"}}, "test", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
memSnapshot := func() (int, int) {
|
||||
m, _ := memDB.Introspect()
|
||||
ents, _ := m["entity_count"].(int)
|
||||
rels, _ := m["relation_count"].(int)
|
||||
return ents, rels
|
||||
}
|
||||
be, br := memSnapshot()
|
||||
|
||||
_, tc, err := setupPluginWith(sdk.SDKConfig{Memory: sdk.NewGraphMemory(memDB)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@ -212,11 +227,25 @@ func TestHealthcheckWithMemory(t *testing.T) {
|
||||
json.Unmarshal(data, &resp)
|
||||
|
||||
if resp["status"] != "ok" {
|
||||
t.Fatalf("expected status ok, got %v", resp["status"])
|
||||
t.Fatalf("expected status ok, got %v (detail=%v)", resp["status"], resp["detail"])
|
||||
}
|
||||
if pass, ok := resp["pass"].(bool); !ok || !pass {
|
||||
t.Fatalf("expected pass=true, got pass=%v status=%v detail=%v", pass, resp["status"], resp["detail"])
|
||||
}
|
||||
|
||||
// 关键:生产记忆内容必须保持不变(未被 healthcheck 污染)
|
||||
ae, ar := memSnapshot()
|
||||
if ae != be || ar != br {
|
||||
t.Fatalf("production memory polluted by healthcheck self-test: before=(%d,%d) after=(%d,%d)", be, br, ae, ar)
|
||||
}
|
||||
|
||||
// 再次确认:注入实例中不应出现 _hc_ 测试实体
|
||||
relResult, _ := memDB.Recall([]string{"_hc_"}, nil, 1, "")
|
||||
for _, e := range relResult.Entities {
|
||||
if len(e.Name) >= 4 && e.Name[:4] == "_hc_" {
|
||||
t.Fatalf("healthcheck left _hc_ entity in production memory: %q", e.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealthcheckWithKnowledge(t *testing.T) {
|
||||
@ -232,6 +261,12 @@ func TestHealthcheckWithKnowledge(t *testing.T) {
|
||||
}
|
||||
defer ks.Stop()
|
||||
|
||||
// 预置一条真实知识,验证 healthcheck 自检后不触碰它
|
||||
if err := ks.Add("生产知识点", "这是生产知识,不应被健康检查破坏"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
before := len(ks.List())
|
||||
|
||||
_, tc, err := setupPluginWith(sdk.SDKConfig{Knowledge: sdk.NewKnowledge(ks)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@ -264,6 +299,20 @@ func TestHealthcheckWithKnowledge(t *testing.T) {
|
||||
if knowledgeCheck == nil {
|
||||
t.Fatal("expected knowledge check in results")
|
||||
}
|
||||
if knowledgeCheck["status"] != "ok" {
|
||||
t.Fatalf("expected knowledge check ok, got %v (detail=%v)", knowledgeCheck["status"], knowledgeCheck["detail"])
|
||||
}
|
||||
|
||||
// 生产知识库内容必须保持不变(未被 healthcheck 污染)
|
||||
after := len(ks.List())
|
||||
if after != before {
|
||||
t.Fatalf("production knowledge polluted: before=%d after=%d", before, after)
|
||||
}
|
||||
for _, name := range ks.List() {
|
||||
if strings.HasPrefix(name, "_hc_knowledge_test_") {
|
||||
t.Fatalf("healthcheck left test knowledge in production: %q", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealthcheckWithDocStore(t *testing.T) {
|
||||
@ -279,6 +328,12 @@ func TestHealthcheckWithDocStore(t *testing.T) {
|
||||
}
|
||||
defer ds.Stop()
|
||||
|
||||
// 预置一篇生产文档,验证 healthcheck 自检后不触碰它
|
||||
if err := ds.Insert(&doc.Doc{ID: "prod_doc", Summary: "生产文档", Content: "这是生产文档,不应被健康检查破坏"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
before := len(ds.Query("生产文档", 10))
|
||||
|
||||
_, tc, err := setupPluginWith(sdk.SDKConfig{DocMemory: sdk.NewDocMemory(ds)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@ -311,6 +366,15 @@ func TestHealthcheckWithDocStore(t *testing.T) {
|
||||
if docCheck == nil {
|
||||
t.Fatal("expected documents check in results")
|
||||
}
|
||||
if docCheck["status"] != "ok" {
|
||||
t.Fatalf("expected documents check ok, got %v (detail=%v)", docCheck["status"], docCheck["detail"])
|
||||
}
|
||||
|
||||
// 生产文档必须保持不变(未被 healthcheck 污染)
|
||||
after := len(ds.Query("生产文档", 10))
|
||||
if after != before {
|
||||
t.Fatalf("production doc store polluted: before=%d after=%d", before, after)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLLMReportCollection(t *testing.T) {
|
||||
@ -330,4 +394,88 @@ func TestLLMReportCollection(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSafeReadonlyTool 验证 LLM 自检工具过滤:写类/副作用工具被拒绝,只读工具被放行。
|
||||
func TestSafeReadonlyTool(t *testing.T) {
|
||||
// 只读:应放行
|
||||
readonly := []string{
|
||||
"memory_recall", "memory_introspect", "doc_query",
|
||||
"knowledge_search", "knowledge_list", "person_query",
|
||||
"person_network", "llm_list_sources", "output_list_channels",
|
||||
"terminal_list", "files_list",
|
||||
}
|
||||
for _, name := range readonly {
|
||||
if !isSafeReadonlyTool(name) {
|
||||
t.Errorf("expected readonly tool %q to be safe, but rejected", name)
|
||||
}
|
||||
}
|
||||
|
||||
// 写/删/改/副作用:应被拒绝
|
||||
mutating := []string{
|
||||
"memory_commit", "memory_edit", "memory_purge", "memory_delete_entity",
|
||||
"memory_merge", "memory_block_merge",
|
||||
"knowledge_create", "knowledge_delete",
|
||||
"doc_commit", "doc_delete",
|
||||
"person_set_trait", "person_relate",
|
||||
"output_send__qq", "output_send__cli",
|
||||
"llm_set_source", "config_set",
|
||||
"timer_set", "plgreload", "plugin_disable",
|
||||
"cmd_run", "files_write", "files_delete", "terminal_create", "terminal_write",
|
||||
"terminal_close", "spawn_child",
|
||||
}
|
||||
for _, name := range mutating {
|
||||
if isSafeReadonlyTool(name) {
|
||||
t.Errorf("expected mutating tool %q to be rejected, but allowed", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollectToolDefsForLLMNoMutating 验证 collectToolDefsForLLM 不会把写类工具交给 LLM 自检。
|
||||
func TestCollectToolDefsForLLMNoMutating(t *testing.T) {
|
||||
p := &Plugin{name: "healthcheck", selfToolNames: map[string]bool{"healthcheck": true, "healthcheck_report": true}}
|
||||
|
||||
// 构造一个包含写类工具的 ToolDef 集合,注入工具注册表
|
||||
stage := agentCore.NewStageHost()
|
||||
defs := []sdk.ToolDef{
|
||||
{Name: "memory_recall", Plugin: "memory", Description: "recall"},
|
||||
{Name: "memory_commit", Plugin: "memory", Description: "commit"},
|
||||
{Name: "doc_query", Plugin: "doc", Description: "query"},
|
||||
{Name: "doc_commit", Plugin: "doc", Description: "commit doc"},
|
||||
{Name: "knowledge_search", Plugin: "knowledge", Description: "search"},
|
||||
{Name: "knowledge_create", Plugin: "knowledge", Description: "create"},
|
||||
{Name: "cmd_run", Plugin: "cmd", Description: "run cmd"},
|
||||
{Name: "files_list", Plugin: "files", Description: "list"},
|
||||
}
|
||||
for _, d := range defs {
|
||||
d := d
|
||||
stage.RegisterTool(d.Name, d, func(map[string]interface{}) (interface{}, error) { return nil, nil })
|
||||
}
|
||||
|
||||
tc := newToolCapture()
|
||||
s := newTestSDK(sdk.SDKConfig{
|
||||
RegTool: tc.RegisterTool,
|
||||
RegStage: tc.RegisterStage,
|
||||
RegAPI: tc.RegisterAPI,
|
||||
Tool: sdk.NewTool(stage, agentIO.NewIOManager()),
|
||||
})
|
||||
|
||||
got := p.collectToolDefsForLLM(s)
|
||||
allowed := map[string]bool{}
|
||||
for _, d := range got {
|
||||
allowed[d.Name] = true
|
||||
}
|
||||
|
||||
// 只读工具应被包含
|
||||
for _, name := range []string{"memory_recall", "doc_query", "knowledge_search", "files_list"} {
|
||||
if !allowed[name] {
|
||||
t.Errorf("expected readonly tool %q in LLM selftest set, missing", name)
|
||||
}
|
||||
}
|
||||
// 写类/副作用工具绝不能被交给 LLM
|
||||
for _, name := range []string{"memory_commit", "doc_commit", "knowledge_create", "cmd_run"} {
|
||||
if allowed[name] {
|
||||
t.Errorf("mutating tool %q must NOT be in LLM selftest set", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user