Files
HomeAgent/internal/plugins/integration_test.go
JianFeeeee 5836c2ce5c refactor(memory): 拆除描述式媒体索引,媒体成为一等块并按原生向量融合
背景:此前媒体是靠「生成的描述文本」将就进记忆的——写 marker 进正文、
再由正则反解成 media_refs 与图库里的 type=Media 实体。这条链路有三个
致命缺陷:描述由异步模型生成(未生成前媒体等于不存在)、语义检索实质上
只搜描述文字、图库里的「媒体节点」是描述文本的投影而不是媒体本身。

本提交把这条链路整体拆除,媒体改为按自己的原生向量参与记忆:

一、描述链彻底删除(无残留、无兼容分支)
- media.Item 去掉 Description/DescribedBy 与对应列;
- 删除 Store.Describe / Store.Search / Store.Pending;
- 删除 Agent.mediaDescribeLoop / describePendingMedia 与配置项
  core.memory.media.describe_on_ingest;
- SDK 侧 MediaAttachment 去掉 Description(见 SDK 仓独立提交)。

二、marker 机制删除,媒体归属改为结构化块边
- 删除 mediaMarkerLine/parseMediaMarkers/mediaEntityName/mediaTriplesFromText/
  extractMediaDigests/sentenceWithMediaMarkers/docMediaContext;
- memory.Triple 新增 MediaDigests 结构化字段;句子文本保持原样,
  不再被 marker 污染;
- 块以 sentence --contains--> block / document --contains--> block 结构边
  挂到承载节点(新增 documents 表与 document 节点种类);
- 模型未给原句时用「主谓宾。」拼一句自然语言作落点,不造 marker 文本。

三、旧数据迁移(幂等)
- 新增 GraphDB.MigrateLegacyMediaEntities:把 type=Media 的旧实体按短 digest
  还原成原生块、挂回原句子、删除旧实体与描述关系;Agent 启动时执行;
- CleanupOrphanedSentences 同时看关系引用与块边,避免把只靠块存活的句子
  连同块边一起删掉。

四、向量融合:媒体按图本身被召回
- 新增 vector.FuseVectors(逐维求和 + L2 归一化);
- Doc.DenseVec = 文本向量 ⊕ 文档块的媒体向量(同 fingerprint 才融合),
  新增 Doc.DenseFP,指纹变化触发重算;
- ContextEvent.DenseVec 同理融合事件块;事件新增 DenseFP,Prune 只在
  同一统一空间内比稠密余弦;
- 跨模态视觉路只召回「仍被某层记忆块持有」的媒体,CAS 全库字节不再
  直接充当记忆检索结果。

五、同时纳入本分支既有的嵌入基础改造(此前工作区未提交,缺它 HEAD 不可构建)
- internal/tfidf 懒回退包、千问三段式多模态 ONNX 空间的 Go 侧
  (qwen/embedder.go、image.go、model_input.go)、CLIP 移除、
  sdk.NewStore 分词器签名与调用点、embed 侧车 systemd 单元。

验证:go build ./... 、go vet ./...(含 -tags medialive)均通过;
在 HEAD 的独立 worktree 上重放本次暂存集后 go test -short ./internal/...
全部通过(端口冲突类用例在隔离环境中亦通过)。未提交工作区中与本改造
无关的改动(HarmonyOS、waiter、devicebridge、plan.md 等)。
2026-09-11 11:45:24 +08:00

589 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package plugins
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"time"
agentAPI "gitcode.com/JianFeeeee/HomeAgent/internal/agent/api"
agentCore "gitcode.com/JianFeeeee/HomeAgent/internal/agent/core"
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
"gitcode.com/JianFeeeee/HomeAgent/internal/knowledge"
luaVM "gitcode.com/JianFeeeee/HomeAgent/internal/lua"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
doc "gitcode.com/JianFeeeee/HomeAgent/internal/memory/document"
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
cli "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/cli"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
type testPluginEnv struct {
tmpDir string
stageHost *agentCore.StageHost
iom *agentIO.IOManager
pluginReg *plugin.Registry
memDB *memory.GraphDB
ks *knowledge.Store
docStore *doc.Store
}
func setupIntegration(t *testing.T) *testPluginEnv {
return setupIntegrationWithProvider(t, nil)
}
func setupIntegrationWithProvider(t *testing.T, pm *agentAPI.ProviderManager) *testPluginEnv {
t.Helper()
tmpDir, err := os.MkdirTemp("", "hc_integration_*")
if err != nil {
t.Fatal(err)
}
stageHost := agentCore.NewStageHost()
iom := agentIO.NewIOManager()
pluginReg := plugin.NewRegistry()
pluginReg.SetIOManager(iom)
pluginReg.SetMemory(nil)
pluginReg.SetToolRegistrar(func(name string, def sdk.ToolDef, handler sdk.ToolHandler) error {
return stageHost.RegisterTool(name, def, handler)
})
pluginReg.SetStageRegistrar(func(stage sdk.Stage, handler sdk.StageHandler) {
stageHost.RegisterStage(stage, handler)
})
pluginReg.SetAPIRegistrar(func(name string) error {
return nil
})
memDB, err := memory.NewGraphDB(filepath.Join(tmpDir, "test.db"))
if err != nil {
t.Fatal(err)
}
pluginReg.SetMemory(memDB)
ks := knowledge.NewStore(filepath.Join(tmpDir, "knowledge"))
if err := ks.Start(); err != nil {
t.Fatal(err)
}
docStore := doc.NewStore(filepath.Join(tmpDir, "documents"), memory.TokenizeWords)
if err := docStore.Start(); err != nil {
t.Fatal(err)
}
pluginReg.SetStageHost(stageHost)
pluginReg.SetProviderManager(pm)
pluginReg.SetKnowledge(ks)
pluginReg.SetDocStore(docStore)
cli.DefaultSocket = filepath.Join(tmpDir, "cli.sock")
// 经 ConfigRegistry 装配内核路径配置clawhubadapter/pluginmgr 等经 SDK settings 读取)
cfgReg := internalConfig.NewConfigRegistry("")
cfgReg.SeedDefaults(tmpDir)
pluginReg.SetConfigRegistry(cfgReg)
plgDir := filepath.Join(tmpDir, "plugins")
os.MkdirAll(plgDir, 0755)
if err := pluginReg.Load(plgDir); err != nil {
t.Fatal(err)
}
return &testPluginEnv{
tmpDir: tmpDir,
stageHost: stageHost,
iom: iom,
pluginReg: pluginReg,
memDB: memDB,
ks: ks,
docStore: docStore,
}
}
func (e *testPluginEnv) cleanup() {
e.pluginReg.StopAll()
e.memDB.Close()
e.ks.Stop()
e.docStore.Stop()
os.RemoveAll(e.tmpDir)
}
// ---------------------------------------------------------------------------
// Registration
// ---------------------------------------------------------------------------
func TestIntegrationAllPluginsRegister(t *testing.T) {
env := setupIntegration(t)
defer env.cleanup()
expectedTools := []string{
"cmd_run",
"terminal_create", "terminal_write", "terminal_read",
"terminal_resize", "terminal_close", "terminal_list",
"healthcheck", "healthcheck_plugins", "healthcheck_tools", "healthcheck_memory", "healthcheck_perf",
"timer_set",
}
defs := env.stageHost.GetToolDefs()
registered := make(map[string]bool)
for _, def := range defs {
registered[def.Name] = true
}
var missing []string
for _, tool := range expectedTools {
if !registered[tool] {
missing = append(missing, tool)
}
}
if len(missing) > 0 {
all := make([]string, 0, len(defs))
for _, d := range defs {
all = append(all, d.Name)
}
t.Fatalf("missing tools: %v\nall registered: %v", missing, all)
}
t.Logf("all %d expected tools registered (total: %d)", len(expectedTools), len(defs))
}
// ---------------------------------------------------------------------------
// cmd_run
// ---------------------------------------------------------------------------
func TestIntegrationCmdRun(t *testing.T) {
env := setupIntegration(t)
defer env.cleanup()
result, err := env.stageHost.ExecuteTool("cmd_run", map[string]interface{}{
"command": "echo hello_world",
})
if err != nil {
t.Fatal(err)
}
data, _ := json.Marshal(result)
var resp map[string]interface{}
json.Unmarshal(data, &resp)
if resp["exit_code"] != 0.0 {
t.Fatalf("expected exit_code 0, got %v", resp["exit_code"])
}
stdout, ok := resp["stdout"].(string)
if !ok || stdout != "hello_world" {
t.Fatalf("expected stdout 'hello_world', got %q", stdout)
}
t.Logf("cmd_run OK: exit_code=0 stdout=%q", stdout)
}
func TestIntegrationCmdRunWithWorkdir(t *testing.T) {
env := setupIntegration(t)
defer env.cleanup()
result, err := env.stageHost.ExecuteTool("cmd_run", map[string]interface{}{
"command": "pwd",
"workdir": "/tmp",
})
if err != nil {
t.Fatal(err)
}
data, _ := json.Marshal(result)
var resp map[string]interface{}
json.Unmarshal(data, &resp)
if resp["exit_code"] != 0.0 {
t.Fatalf("expected exit_code 0, got %v", resp["exit_code"])
}
stdout := resp["stdout"].(string)
if stdout != "/tmp" {
t.Fatalf("expected stdout '/tmp', got %q", stdout)
}
t.Logf("cmd_run workdir OK: stdout=%q", stdout)
}
func TestIntegrationCmdRunInvalidTimeout(t *testing.T) {
env := setupIntegration(t)
defer env.cleanup()
result, err := env.stageHost.ExecuteTool("cmd_run", map[string]interface{}{
"command": "echo ok",
"timeout": "not-a-duration",
})
if err != nil {
t.Fatal(err)
}
data, _ := json.Marshal(result)
var resp map[string]interface{}
json.Unmarshal(data, &resp)
errMsg, ok := resp["error"].(string)
if !ok || errMsg == "" {
t.Fatalf("expected error for invalid timeout, got %v", resp)
}
t.Logf("cmd_run invalid_timeout OK: error=%q", errMsg)
}
func TestIntegrationCmdRunStderr(t *testing.T) {
env := setupIntegration(t)
defer env.cleanup()
result, err := env.stageHost.ExecuteTool("cmd_run", map[string]interface{}{
"command": "sh -c \"echo stderr_test >&2\"",
})
if err != nil {
t.Fatal(err)
}
data, _ := json.Marshal(result)
var resp map[string]interface{}
json.Unmarshal(data, &resp)
if resp["exit_code"] != 0.0 {
t.Fatalf("expected exit_code 0, got %v", resp["exit_code"])
}
stderr := resp["stderr"].(string)
if stderr != "stderr_test" {
t.Fatalf("expected stderr 'stderr_test', got %q", stderr)
}
t.Logf("cmd_run stderr OK: stderr=%q", stderr)
}
// ---------------------------------------------------------------------------
// Terminal (PTY)
// ---------------------------------------------------------------------------
func TestIntegrationPtyCreateListClose(t *testing.T) {
env := setupIntegration(t)
defer env.cleanup()
// Create
result, err := env.stageHost.ExecuteTool("terminal_create", map[string]interface{}{
"shell": "/bin/sh",
"name": "hci_test_shell",
})
if err != nil {
t.Fatal(err)
}
data, _ := json.Marshal(result)
var resp map[string]interface{}
json.Unmarshal(data, &resp)
id, ok := resp["id"].(string)
if !ok || id == "" {
status := resp["status"]
detail := resp["detail"]
if status == "error" {
t.Skipf("PTY not available: %v", detail)
}
t.Fatalf("expected non-empty terminal id, got id=%q status=%v detail=%v", id, status, detail)
}
t.Logf("terminal_create OK: id=%s", id)
// List
result2, err := env.stageHost.ExecuteTool("terminal_list", map[string]interface{}{})
if err != nil {
t.Fatal(err)
}
data2, _ := json.Marshal(result2)
var resp2 map[string]interface{}
json.Unmarshal(data2, &resp2)
terminals, ok := resp2["terminals"].([]interface{})
if !ok {
t.Fatalf("expected terminals array, got %T", resp2["terminals"])
}
if len(terminals) < 1 {
t.Fatal("expected at least 1 terminal")
}
t.Logf("terminal_list OK: %d terminals", len(terminals))
// Close
_, err = env.stageHost.ExecuteTool("terminal_close", map[string]interface{}{
"id": id,
})
if err != nil {
t.Fatal(err)
}
t.Logf("terminal_close OK: id=%s", id)
}
func TestIntegrationPtyInteractive(t *testing.T) {
env := setupIntegration(t)
defer env.cleanup()
createResult, err := env.stageHost.ExecuteTool("terminal_create", map[string]interface{}{
"shell": "/bin/sh",
"name": "hci_interactive",
})
if err != nil {
t.Fatal(err)
}
cdata, _ := json.Marshal(createResult)
var cresp map[string]interface{}
json.Unmarshal(cdata, &cresp)
id, ok := cresp["id"].(string)
if !ok || id == "" {
status := cresp["status"]
if status == "error" {
t.Skipf("PTY not available: %v", cresp["detail"])
}
t.Fatalf("expected terminal id, got %v", cresp)
}
// Write a command
_, err = env.stageHost.ExecuteTool("terminal_write", map[string]interface{}{
"id": id,
"input": "echo pty_works\n",
})
if err != nil {
t.Fatal(err)
}
time.Sleep(500 * time.Millisecond)
// Read output
readResult, err := env.stageHost.ExecuteTool("terminal_read", map[string]interface{}{
"id": id,
})
if err != nil {
t.Fatal(err)
}
rdata, _ := json.Marshal(readResult)
var rresp map[string]interface{}
json.Unmarshal(rdata, &rresp)
output, ok := rresp["output"].(string)
if !ok || output == "" {
t.Fatalf("expected output, got output=%q response=%v", output, rresp)
}
t.Logf("terminal_write+read OK: output=%q", output)
// Close
env.stageHost.ExecuteTool("terminal_close", map[string]interface{}{
"id": id,
})
}
func TestIntegrationPtyResize(t *testing.T) {
env := setupIntegration(t)
defer env.cleanup()
createResult, err := env.stageHost.ExecuteTool("terminal_create", map[string]interface{}{
"shell": "/bin/sh",
"name": "hci_resize_test",
})
if err != nil {
t.Fatal(err)
}
cdata, _ := json.Marshal(createResult)
var cresp map[string]interface{}
json.Unmarshal(cdata, &cresp)
id, ok := cresp["id"].(string)
if !ok || id == "" {
status := cresp["status"]
if status == "error" {
t.Skipf("PTY not available: %v", cresp["detail"])
}
t.Fatalf("expected terminal id, got %v", cresp)
}
_, err = env.stageHost.ExecuteTool("terminal_resize", map[string]interface{}{
"id": id,
"rows": 80.0,
"cols": 200.0,
})
if err != nil {
t.Fatal(err)
}
t.Logf("terminal_resize OK: id=%s rows=80 cols=200", id)
env.stageHost.ExecuteTool("terminal_close", map[string]interface{}{
"id": id,
})
}
// ---------------------------------------------------------------------------
// Healthcheck
// ---------------------------------------------------------------------------
func TestIntegrationHealthcheck(t *testing.T) {
env := setupIntegration(t)
defer env.cleanup()
result, err := env.stageHost.ExecuteTool("healthcheck", map[string]interface{}{})
if err != nil {
t.Fatal(err)
}
data, _ := json.Marshal(result)
var resp map[string]interface{}
json.Unmarshal(data, &resp)
if resp["status"] != "ok" {
t.Fatalf("expected status=ok, got %v", resp["status"])
}
checks := resp["checks"].([]interface{})
if len(checks) == 0 {
t.Fatal("expected non-empty checks array")
}
t.Logf("healthcheck OK: %d checks, status=%v", len(checks), resp["status"])
}
func TestIntegrationHealthcheckPlugins(t *testing.T) {
env := setupIntegration(t)
defer env.cleanup()
result, err := env.stageHost.ExecuteTool("healthcheck_plugins", map[string]interface{}{})
if err != nil {
t.Fatal(err)
}
data, _ := json.Marshal(result)
var resp map[string]interface{}
json.Unmarshal(data, &resp)
if resp["status"] != "ok" {
t.Fatalf("expected status=ok, got %v", resp["status"])
}
t.Logf("healthcheck_plugins OK: status=%v", resp["status"])
}
func TestIntegrationHealthcheckToolsList(t *testing.T) {
env := setupIntegration(t)
defer env.cleanup()
result, err := env.stageHost.ExecuteTool("healthcheck_tools", map[string]interface{}{})
if err != nil {
t.Fatal(err)
}
data, _ := json.Marshal(result)
var resp map[string]interface{}
json.Unmarshal(data, &resp)
if resp["status"] != "ok" {
t.Fatalf("expected status=ok, got %v", resp["status"])
}
t.Logf("healthcheck_tools OK: status=%v", resp["status"])
}
func TestIntegrationHealthcheckMemory(t *testing.T) {
env := setupIntegration(t)
defer env.cleanup()
result, err := env.stageHost.ExecuteTool("healthcheck_memory", map[string]interface{}{})
if err != nil {
t.Fatal(err)
}
data, _ := json.Marshal(result)
var resp map[string]interface{}
json.Unmarshal(data, &resp)
if resp["status"] != "ok" {
t.Fatalf("expected status=ok, got %v", resp["status"])
}
t.Logf("healthcheck_memory OK: status=%v", resp["status"])
}
func TestIntegrationToolNotFound(t *testing.T) {
env := setupIntegration(t)
defer env.cleanup()
_, err := env.stageHost.ExecuteTool("nonexistent_tool_xyz", nil)
if err == nil {
t.Fatal("expected error for nonexistent tool")
}
t.Logf("tool_not_found OK: err=%v", err)
}
func TestIntegrationLLMDrivenDiscoveryWithRealKey(t *testing.T) {
apiKey := os.Getenv("DEEPSEEK_API_KEY")
if apiKey == "" {
t.Skip("DEEPSEEK_API_KEY not set")
}
// Lua VM + DeepSeek Provider
tmpVM := t.TempDir()
vm := luaVM.NewVM(tmpVM + "/adapters")
if err := vm.Start(); err != nil {
t.Fatal(err)
}
defer vm.Stop()
pm := agentAPI.NewProviderManager()
pm.Register("deepseek", agentAPI.NewLuaAdaptedProvider(agentAPI.BaseConfig{
Model: "deepseek-v4-flash",
BaseURL: "https://api.deepseek.com",
APIKey: apiKey,
}, vm, "deepseek", "deepseek"))
// Setup — 加载所有真实内置插件
env := setupIntegrationWithProvider(t, pm)
defer env.cleanup()
// 列出已加载的真实工具
defs := env.stageHost.GetToolDefs()
t.Logf("共有 %d 个已注册的真实工具:", len(defs))
for _, d := range defs {
t.Logf(" - %s", d.Name)
}
// 调用 healthcheck 进行全面检查(含 LLM 驱动阶段)
result, err := env.stageHost.ExecuteTool("healthcheck", map[string]interface{}{})
if err != nil {
t.Fatal(err)
}
data, _ := json.Marshal(result)
var resp map[string]interface{}
json.Unmarshal(data, &resp)
t.Logf("===== Full Healthcheck Result =====")
t.Logf("status: %v", resp["status"])
t.Logf("summary: %v", resp["summary"])
t.Logf("total: %v", resp["total"])
t.Logf("passed: %v", resp["passed"])
t.Logf("failed: %v", resp["failed"])
checks := resp["checks"].([]interface{})
for _, c := range checks {
cr := c.(map[string]interface{})
prefix := "✅"
if cr["status"] == "fail" {
prefix = "❌"
}
t.Logf(" %s %s: %s %s", prefix, cr["name"], cr["status"], cr["detail"])
}
if resp["status"] != "ok" {
t.Fatalf("expected status=ok, got %v", resp["status"])
}
// 验证 LLM 发现阶段的存在
foundDiscovery := false
for _, c := range checks {
cr := c.(map[string]interface{})
if cr["name"] == "llm_discovery" {
foundDiscovery = true
if cr["status"] != "ok" {
t.Fatalf("LLM discovery failed: status=%s detail=%s", cr["status"], cr["detail"])
}
break
}
}
if !foundDiscovery {
t.Fatal("expected llm_discovery check in results")
}
t.Logf("✅ LLM-driven tool discovery test PASSED")
}