返回 DeepSeek-Reasonix
plugin_v1_test.go
根目录 / internal / cli / plugin_v1_test.go
1 package cli
2
3 import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8
9 "reasonix/internal/pluginpkg"
10 )
11
12 // installV1RuntimePlugin writes a Manifest v1 package with a runtime into
13 // the test Reasonix home and registers it in plugin state.
14 func installV1RuntimePlugin(t *testing.T, home string) string {
15 t.Helper()
16 root := filepath.Join(home, "plugins", "example")
17 writePluginTestFile(t, filepath.Join(root, pluginpkg.NativeManifest), `{
18 "apiVersion": "reasonix.io/plugin/v1",
19 "name": "example",
20 "version": "1.0.0",
21 "contributes": {
22 "prompts": ["prompts"],
23 "themes": ["themes/*.reasonix-theme"]
24 },
25 "runtime": {
26 "command": "${REASONIX_PLUGIN_ROOT}/bin/example",
27 "args": ["--serve"],
28 "required": true,
29 "intercepts": ["input.receive", "tool.before"],
30 "replaces": ["system_prompt"],
31 "capabilities": ["interceptors", "ui"]
32 }
33 }`)
34 writePluginTestFile(t, filepath.Join(root, "prompts", "plan.md"), "---\ndescription: plan\n---\nPlan $ARGUMENTS")
35 writePluginTestFile(t, filepath.Join(root, "themes", "neon.reasonix-theme"), "theme bytes")
36 writePluginTestFile(t, filepath.Join(root, "bin", "example"), "#!/bin/sh\n")
37 if err := pluginpkg.Upsert(home, pluginpkg.InstalledPlugin{
38 Name: "example", Root: "plugins/example", Version: "1.0.0", ManifestKind: "reasonix", Enabled: true,
39 }); err != nil {
40 t.Fatal(err)
41 }
42 return root
43 }
44
45 func TestPluginShowRendersRuntimeFullTrust(t *testing.T) {
46 home := t.TempDir()
47 t.Setenv("REASONIX_HOME", home)
48 installV1RuntimePlugin(t, home)
49
50 out := captureStdout(t, func() {
51 if rc := pluginCommand([]string{"show", "example"}); rc != 0 {
52 t.Fatalf("plugin show rc = %d, want 0", rc)
53 }
54 })
55 for _, want := range []string{
56 "prompts: 1",
57 "themes: 1",
58 "runtime: FULL TRUST",
59 "command: ${REASONIX_PLUGIN_ROOT}/bin/example --serve",
60 "intercepts: input.receive, tool.before",
61 "replaces: system_prompt",
62 "capabilities: interceptors, ui",
63 "bypass permissions",
64 "prompts:\n plan",
65 "themes:\n neon",
66 } {
67 if !strings.Contains(out, want) {
68 t.Fatalf("plugin show output missing %q:\n%s", want, out)
69 }
70 }
71 }
72
73 func TestPluginDoctorValidatesV1Runtime(t *testing.T) {
74 home := t.TempDir()
75 t.Setenv("REASONIX_HOME", home)
76 installV1RuntimePlugin(t, home)
77
78 out := captureStdout(t, func() {
79 if rc := pluginCommand([]string{"doctor", "example"}); rc != 0 {
80 t.Fatalf("plugin doctor rc = %d, want 0", rc)
81 }
82 })
83 if !strings.Contains(out, "runtime: FULL TRUST") || !strings.Contains(out, "ok: example") {
84 t.Fatalf("doctor output missing runtime block or ok line:\n%s", out)
85 }
86 }
87
88 func TestPluginDoctorFailsForMissingRuntimeCommand(t *testing.T) {
89 home := t.TempDir()
90 t.Setenv("REASONIX_HOME", home)
91 root := installV1RuntimePlugin(t, home)
92 // Remove the runtime binary so the command no longer resolves.
93 if err := os.Remove(filepath.Join(root, "bin", "example")); err != nil {
94 t.Fatal(err)
95 }
96
97 stderr := captureStderr(t, func() {
98 if rc := pluginCommand([]string{"doctor", "example"}); rc != 1 {
99 t.Fatalf("plugin doctor rc = %d, want 1 for a missing runtime command", rc)
100 }
101 })
102 if !strings.Contains(stderr, "runtime command not found:") {
103 t.Fatalf("doctor stderr missing the runtime command diagnostic:\n%s", stderr)
104 }
105 }
106
107 func TestPluginDoctorReportsMissingV1PathsAsWarnings(t *testing.T) {
108 home := t.TempDir()
109 t.Setenv("REASONIX_HOME", home)
110 root := filepath.Join(home, "plugins", "gap")
111 writePluginTestFile(t, filepath.Join(root, pluginpkg.NativeManifest), `{
112 "apiVersion": "reasonix.io/plugin/v1",
113 "name": "gap",
114 "contributes": {
115 "themes": ["themes/*.reasonix-theme"]
116 }
117 }`)
118 if err := pluginpkg.Upsert(home, pluginpkg.InstalledPlugin{
119 Name: "gap", Root: "plugins/gap", ManifestKind: "reasonix", Enabled: true,
120 }); err != nil {
121 t.Fatal(err)
122 }
123
124 out := captureStdout(t, func() {
125 if rc := pluginCommand([]string{"doctor", "gap"}); rc != 0 {
126 t.Fatalf("plugin doctor rc = %d, want 0 (missing themes are warnings, not failures)", rc)
127 }
128 })
129 if !strings.Contains(out, `theme glob "themes/*.reasonix-theme" matched no files`) {
130 t.Fatalf("doctor output missing the glob warning:\n%s", out)
131 }
132 }
133
133 lines GO