diff --git a/internal/plugins/remotedevice/test/registry_contract_test.go b/internal/plugins/remotedevice/test/registry_contract_test.go new file mode 100644 index 00000000..28b60383 --- /dev/null +++ b/internal/plugins/remotedevice/test/registry_contract_test.go @@ -0,0 +1,60 @@ +package test + +// remotedevice Registry 黑盒判据:通过公开方法观察结果保存、能力查询与媒体落盘。 + +import ( + "encoding/base64" + "os" + "path/filepath" + "testing" + + "github.com/JianFeeeee/HologramAgent/internal/plugins/remotedevice" +) + +func TestSaveResultThenGetResult(t *testing.T) { + r := remotedevice.NewRegistry() + want := map[string]interface{}{"status": "ok", "output": "done"} + r.SaveResult("req-123", want) + got, ok := r.GetResult("req-123") + if !ok || got["status"] != "ok" || got["output"] != "done" { + t.Fatalf("GetResult=%v,%v", got, ok) + } + if _, ok := r.GetResult("missing"); ok { + t.Fatal("unknown request unexpectedly found") + } +} + +func TestSupportsToolForUnknownAndUnknownCapability(t *testing.T) { + r := remotedevice.NewRegistry() + if r.SupportsTool("missing-device", "screensee") { + t.Fatal("unknown device supports tool") + } +} + +func TestSaveInlineMediaRequiresDirectoryAndPersistsBytes(t *testing.T) { + r := remotedevice.NewRegistry() + payload := base64.StdEncoding.EncodeToString([]byte("media bytes")) + if _, err := r.SaveInlineMedia("capture", payload, "application/octet-stream"); err == nil { + t.Fatal("media saved without configured directory") + } + dir := t.TempDir() + r.SetMediaDir(dir) + meta, err := r.SaveInlineMedia("capture", payload, "application/octet-stream") + if err != nil { + t.Fatalf("SaveInlineMedia: %v", err) + } + path, ok := meta["file"].(string) + if !ok || filepath.Dir(path) != dir { + t.Fatalf("file metadata=%v", meta) + } + got, err := os.ReadFile(path) + if err != nil { + t.Fatalf("media file missing: %v", err) + } + if string(got) != "media bytes" || meta["size"] != len(got) || meta["mime"] != "application/octet-stream" { + t.Fatalf("saved payload/metadata mismatch: %q / %v", got, meta) + } + if _, ok := meta["sha256"].(string); !ok { + t.Fatalf("missing digest: %v", meta) + } +} diff --git a/internal/plugins/skillmgr/test/descriptions_contract_test.go b/internal/plugins/skillmgr/test/descriptions_contract_test.go new file mode 100644 index 00000000..6ebb2ff3 --- /dev/null +++ b/internal/plugins/skillmgr/test/descriptions_contract_test.go @@ -0,0 +1,54 @@ +package test + +// skillmgr 黑盒判据:技能目录输入 → SkillDescriptions 可观察文本。 + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/JianFeeeee/HologramAgent/internal/plugins/skillmgr" + sdkpkg "github.com/JianFeeeee/HologramAgent/internal/sdk" +) + +func TestSkillDescriptionsListsScannedSkillsInStableOrder(t *testing.T) { + dir := t.TempDir() + for name, desc := range map[string]string{"zeta": "处理 zeta 的工作流。", "alpha": "处理 alpha 的工作流。"} { + p := filepath.Join(dir, name) + if err := os.MkdirAll(p, 0o755); err != nil { + t.Fatal(err) + } + body := "# " + name + "\n\n" + desc + "\n" + if err := os.WriteFile(filepath.Join(p, "SKILL.md"), []byte(body), 0o644); err != nil { + t.Fatal(err) + } + } + p := skillmgr.New("contract-skillmgr", dir) + // Start performs the public directory scan; provide only the two public + // registrar callbacks required by the plugin SDK and no hidden test hooks. + sdk := sdkpkg.New("contract-skillmgr", sdkpkg.SDKConfig{ + RegTool: func(string, sdkpkg.ToolDef, sdkpkg.ToolHandler) error { return nil }, + }) + if err := p.Start(sdk); err != nil { + t.Fatalf("Start: %v", err) + } + t.Cleanup(func() { _ = p.Stop() }) + got := p.SkillDescriptions() + if !strings.Contains(got, "alpha: 处理 alpha 的工作流。") || !strings.Contains(got, "zeta: 处理 zeta 的工作流。") { + t.Fatalf("scanned descriptions missing: %q", got) + } + if strings.Index(got, "alpha:") > strings.Index(got, "zeta:") { + t.Fatalf("descriptions not in stable name order: %q", got) + } + if p.Name() != "contract-skillmgr" { + t.Fatalf("Name=%q", p.Name()) + } +} + +func TestSkillDescriptionsEmptyBeforeStart(t *testing.T) { + p := skillmgr.New("empty", t.TempDir()) + if got := p.SkillDescriptions(); got != "" { + t.Fatalf("unexpected descriptions: %q", got) + } +} diff --git a/internal/sdk/test/public_contract_test.go b/internal/sdk/test/public_contract_test.go new file mode 100644 index 00000000..5531bea0 --- /dev/null +++ b/internal/sdk/test/public_contract_test.go @@ -0,0 +1,68 @@ +package test + +// internal/sdk 的模块级黑盒判据:公开 SDK 包装 API 的输入→输出。 + +import ( + "testing" + + "github.com/JianFeeeee/HologramAgent/internal/sdk" + "github.com/JianFeeeee/HologramAgent/pkg/types" +) + +func TestEffectiveProxyAuthNormalizesLegacyAndEmpty(t *testing.T) { + for _, in := range []string{"", sdk.ProxyAuthAgentLegacy} { + if got := sdk.EffectiveProxyAuth(in); got != sdk.ProxyAuthAgent { + t.Errorf("EffectiveProxyAuth(%q)=%q", in, got) + } + } + if got := sdk.EffectiveProxyAuth(sdk.ProxyAuthNone); got != sdk.ProxyAuthNone { + t.Fatalf("explicit no-auth changed: %q", got) + } +} + +func TestProxyHostNormalizationAndValidation(t *testing.T) { + label := sdk.NormalizeProxyHost("My Plugin") + if label == "" || !sdk.ValidProxyHostLabel(label) { + t.Fatalf("normalized host %q is not a valid DNS label", label) + } + if sdk.ValidProxyHostLabel("bad..label") || sdk.ValidProxyHostLabel("-leading") { + t.Fatal("invalid DNS host label accepted") + } +} + +func TestNewConfigGetPutRoundTrip(t *testing.T) { + first := &types.Config{LLM: types.LLMConfig{Provider: "first"}} + api := sdk.NewConfig(first) + if got := api.Get(); got != first { + t.Fatal("Get did not return configured value") + } + second := &types.Config{LLM: types.LLMConfig{Provider: "second"}} + api.Put(second) + if got := api.Get(); got != second { + t.Fatal("Put value not observable via Get") + } +} + +func TestBuiltinProxyRegistrySnapshotsAndClear(t *testing.T) { + const name = "contract-sdk-proxy" + sdk.ClearBuiltinProxyDefs(name) + t.Cleanup(func() { sdk.ClearBuiltinProxyDefs(name) }) + before := sdk.BuiltinProxyVersion() + sdk.RegisterBuiltinProxy(name, "web", sdk.ProxyDef{Target: "http://127.0.0.1:1234"}) + defs := sdk.BuiltinProxyDefs() + if len(defs[name]) != 1 || defs[name][0].Name != "web" { + t.Fatalf("registered proxy not observable: %+v", defs[name]) + } + if sdk.BuiltinProxyVersion() <= before { + t.Fatal("registration did not advance public version") + } + // Returned slice is a snapshot; mutating it must not mutate the registry. + defs[name][0].Target = "mutated" + if got := sdk.BuiltinProxyDefs()[name][0].Target; got != "http://127.0.0.1:1234" { + t.Fatalf("snapshot mutation leaked into registry: %q", got) + } + sdk.ClearBuiltinProxyDefs(name) + if _, ok := sdk.BuiltinProxyDefs()[name]; ok { + t.Fatal("ClearBuiltinProxyDefs did not remove entry") + } +}