返回 DeepSeek-Reasonix
manifest_v2_test.go
根目录 / internal / pluginpkg / manifest_v2_test.go
1 package pluginpkg
2
3 import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8 )
9
10 func TestManifestV2RequiresProvides(t *testing.T) {
11 root := t.TempDir()
12 writeV2Plugin(t, root, `{
13 "apiVersion": "reasonix.io/plugin/v2",
14 "name": "deps",
15 "version": "2.0.0",
16 "requires": [
17 {
18 "namespace": "reasonix",
19 "kind": "provider",
20 "id": "deepseek/v4",
21 "versionRange": ">=1.0.0",
22 "optional": false
23 }
24 ],
25 "provides": [
26 {
27 "namespace": "plugin/deps",
28 "kind": "provider",
29 "id": "fake/x",
30 "version": "1.0.0",
31 "schemaHash": "sha256:abc"
32 }
33 ]
34 }`)
35 pkg, _, err := ParseDir(root)
36 if err != nil {
37 t.Fatal(err)
38 }
39 if len(pkg.Manifest.Requires) != 1 || len(pkg.Manifest.Provides) != 1 {
40 t.Fatalf("requires/provides = %d/%d", len(pkg.Manifest.Requires), len(pkg.Manifest.Provides))
41 }
42 if err := pkg.ProvidesCapabilities()[0].Validate(); err != nil {
43 t.Fatal(err)
44 }
45 }
46
47 func TestManifestV2ProviderRequiresSchemaHash(t *testing.T) {
48 root := t.TempDir()
49 writeV2Plugin(t, root, `{
50 "apiVersion": "reasonix.io/plugin/v2",
51 "name": "bad",
52 "provides": [
53 {"namespace": "plugin/bad", "kind": "provider", "id": "x", "version": "1.0.0"}
54 ]
55 }`)
56 _, _, err := ParseDir(root)
57 if err == nil || !strings.Contains(err.Error(), "schemaHash") {
58 t.Fatalf("err = %v, want schemaHash", err)
59 }
60 }
61
62 func TestManifestV2LoadsOnlyExplicitlyDeclaredResources(t *testing.T) {
63 root := t.TempDir()
64 writeV2Plugin(t, root, `{
65 "apiVersion": "reasonix.io/plugin/v2",
66 "name": "explicit-only",
67 "contributes": {
68 "skills": ["skills"],
69 "commands": ["commands"],
70 "agents": ["agents"]
71 }
72 }`)
73 writeTestFile(t, filepath.Join(root, "skills", "plan", "SKILL.md"), "---\ndescription: plan work\n---\nPlan carefully.")
74 writeTestFile(t, filepath.Join(root, "commands", "spec.md"), "---\ndescription: write a spec\n---\nWrite a spec.")
75 writeTestFile(t, filepath.Join(root, "agents", "reviewer.md"), "---\nname: reviewer\ndescription: review changes\n---\nReview carefully.")
76
77 // These Claude compatibility sidecars are present in many multi-host
78 // packages, but a native v2 manifest is an explicit capability boundary.
79 writeTestFile(t, filepath.Join(root, "CLAUDE.md"), "Repository maintenance instructions.")
80 writeTestFile(t, filepath.Join(root, "hooks", "hooks.json"), `{
81 "hooks": {"SessionStart": [{"hooks": [{"type": "command", "command": "bin/start"}]}]}
82 }`)
83 writeTestFile(t, filepath.Join(root, ".mcp.json"), `{
84 "mcpServers": {"helper": {"command": "bin/helper"}}
85 }`)
86
87 pkg, warnings, err := ParseDir(root)
88 if err != nil {
89 t.Fatalf("ParseDir: %v", err)
90 }
91 if len(warnings) != 0 {
92 t.Fatalf("warnings = %v, want none", warnings)
93 }
94 inv := pkg.Inventory()
95 if len(inv.Skills) != 1 || len(inv.Commands) != 1 || len(inv.Agents) != 1 {
96 t.Fatalf("inventory skills/commands/agents = %d/%d/%d, want 1/1/1", len(inv.Skills), len(inv.Commands), len(inv.Agents))
97 }
98 if len(pkg.Manifest.Hooks) != 0 {
99 t.Fatalf("hooks = %+v, want only explicitly declared resources", pkg.Manifest.Hooks)
100 }
101 if len(pkg.Manifest.MCPServers) != 0 {
102 t.Fatalf("MCP servers = %+v, want only explicitly declared resources", pkg.Manifest.MCPServers)
103 }
104 }
105
106 func TestMigrateLegacyManifestToV2(t *testing.T) {
107 root := t.TempDir()
108 legacy := `{
109 "name": "oldplug",
110 "version": "1.2.3",
111 "skills": []
112 }`
113 if err := os.WriteFile(filepath.Join(root, NativeManifest), []byte(legacy), 0o644); err != nil {
114 t.Fatal(err)
115 }
116 pkg, _, err := ParseNativeForMigrate(root)
117 if err != nil {
118 t.Fatal(err)
119 }
120 data, err := MigrateManifestToV2(pkg)
121 if err != nil {
122 t.Fatal(err)
123 }
124 if err := WriteMigratedManifestV2(root, data); err != nil {
125 t.Fatal(err)
126 }
127 if _, err := os.Stat(filepath.Join(root, NativeManifest+".bak")); err != nil {
128 t.Fatal(err)
129 }
130 got, _, err := ParseDir(root)
131 if err != nil {
132 t.Fatalf("parse migrated: %v", err)
133 }
134 if got.Manifest.APIVersion != ManifestAPIVersionV2 || got.Manifest.Name != "oldplug" {
135 t.Fatalf("migrated = %+v", got.Manifest)
136 }
137 }
138
139 func TestMigrateProvidersRequiresExplicitProvides(t *testing.T) {
140 pkg := Package{ManifestKind: "reasonix", Manifest: Manifest{
141 Name: "p", Version: "1.0.0",
142 Runtime: &RuntimeSpec{Command: "./x", Capabilities: []string{"providers"}},
143 }}
144 if _, err := MigrateManifestToV2(pkg); err == nil || !strings.Contains(err.Error(), "cannot infer schemaHash") {
145 t.Fatalf("err = %v", err)
146 }
147 }
148
149 func TestMigrateRejectsVersionedManifest(t *testing.T) {
150 root := t.TempDir()
151 writeV2Plugin(t, root, `{
152 "apiVersion": "reasonix.io/plugin/v2",
153 "name": "already-v2"
154 }`)
155 if _, _, err := ParseNativeForMigrate(root); err == nil || !strings.Contains(err.Error(), "already uses reasonix.io/plugin/v2") {
156 t.Fatalf("ParseNativeForMigrate v2 = %v, want already-v2 rejection", err)
157 }
158
159 pkg := Package{ManifestKind: "reasonix", Manifest: Manifest{
160 APIVersion: ManifestAPIVersionV2,
161 Name: "already-v2",
162 }}
163 if _, err := MigrateManifestToV2(pkg); err == nil || !strings.Contains(err.Error(), "without apiVersion") {
164 t.Fatalf("MigrateManifestToV2 v2 = %v, want versioned rejection", err)
165 }
166 }
167
167 lines GO