返回 DeepSeek-Reasonix
plugin_runtime_test.go
根目录 / internal / installsource / plugin_runtime_test.go
1 package installsource
2
3 import (
4 "encoding/json"
5 "path/filepath"
6 "strings"
7 "testing"
8 )
9
10 // writeRuntimePlugin writes a Manifest v1 plugin package with a runtime
11 // declaration plus prompt and theme contributions.
12 func writeRuntimePlugin(t *testing.T, root string) {
13 t.Helper()
14 writeFile(t, filepath.Join(root, "reasonix-plugin.json"), `{
15 "apiVersion": "reasonix.io/plugin/v1",
16 "name": "rtsync",
17 "version": "1.0.0",
18 "contributes": {
19 "prompts": ["prompts"],
20 "themes": ["themes/*.reasonix-theme"]
21 },
22 "runtime": {
23 "command": "${REASONIX_PLUGIN_ROOT}/bin/rtsync",
24 "args": ["--serve"],
25 "required": true,
26 "intercepts": ["input.receive", "tool.before"],
27 "replaces": ["system_prompt"],
28 "capabilities": ["interceptors", "ui"]
29 }
30 }`)
31 writeFile(t, filepath.Join(root, "prompts", "plan.md"), "---\ndescription: plan\n---\nPlan $ARGUMENTS")
32 writeFile(t, filepath.Join(root, "themes", "neon.reasonix-theme"), "theme bytes")
33 writeFile(t, filepath.Join(root, "bin", "rtsync"), "#!/bin/sh\n")
34 }
35
36 func TestPluginRuntimePlanCarriesFullTrust(t *testing.T) {
37 src := t.TempDir()
38 writeRuntimePlugin(t, src)
39
40 project := t.TempDir()
41 home := t.TempDir()
42 tl := NewTool(Options{ProjectRoot: project, HomeDir: home})
43
44 resp := execInstall(t, tl, map[string]any{
45 "source": src,
46 "kind": "plugin",
47 "apply": false,
48 })
49 if !resp.OK || len(resp.Actions) != 1 {
50 t.Fatalf("response = %+v", resp)
51 }
52 act := resp.Actions[0]
53 if act.RiskLevel != RiskHigh {
54 t.Fatalf("RiskLevel = %q, want high for a runtime package", act.RiskLevel)
55 }
56 fullTrust := false
57 for _, reason := range act.RiskReasons {
58 if strings.HasPrefix(reason, "FULL TRUST:") {
59 fullTrust = true
60 if !strings.Contains(reason, "${REASONIX_PLUGIN_ROOT}/bin/rtsync --serve") {
61 t.Fatalf("FULL TRUST reason should describe the runtime command line: %q", reason)
62 }
63 }
64 }
65 if !fullTrust {
66 t.Fatalf("RiskReasons = %v, want a FULL TRUST: entry", act.RiskReasons)
67 }
68 rt := act.Runtime
69 if rt == nil {
70 t.Fatal("action.Runtime is nil, want the runtime plan info")
71 }
72 if rt.Command != "${REASONIX_PLUGIN_ROOT}/bin/rtsync" || !rt.FullTrust {
73 t.Fatalf("Runtime = %+v", rt)
74 }
75 if len(rt.Args) != 1 || rt.Args[0] != "--serve" {
76 t.Fatalf("Runtime.Args = %v", rt.Args)
77 }
78 if len(rt.Intercepts) != 2 || len(rt.Replaces) != 1 || len(rt.Capabilities) != 2 {
79 t.Fatalf("Runtime = %+v", rt)
80 }
81 if act.PromptCount != 1 || act.ThemeCount != 1 {
82 t.Fatalf("PromptCount/ThemeCount = %d/%d, want 1/1", act.PromptCount, act.ThemeCount)
83 }
84
85 // The plan JSON must carry the runtime block so frontends can render it.
86 raw, err := json.Marshal(act)
87 if err != nil {
88 t.Fatal(err)
89 }
90 var encoded map[string]any
91 if err := json.Unmarshal(raw, &encoded); err != nil {
92 t.Fatal(err)
93 }
94 rtJSON, ok := encoded["runtime"].(map[string]any)
95 if !ok {
96 t.Fatalf("plan JSON missing runtime block: %s", raw)
97 }
98 if rtJSON["fullTrust"] != true || rtJSON["command"] != "${REASONIX_PLUGIN_ROOT}/bin/rtsync" {
99 t.Fatalf("runtime JSON = %v", rtJSON)
100 }
101 }
102
103 func TestPluginLegacyPlanOmitsRuntimeFields(t *testing.T) {
104 src := t.TempDir()
105 writeFile(t, filepath.Join(src, "reasonix-plugin.json"), `{"name": "legacy", "skills": ["skills"]}`)
106 writeFile(t, filepath.Join(src, "skills", "s", "SKILL.md"), "---\ndescription: s\n---\nS")
107
108 project := t.TempDir()
109 home := t.TempDir()
110 tl := NewTool(Options{ProjectRoot: project, HomeDir: home})
111
112 resp := execInstall(t, tl, map[string]any{
113 "source": src,
114 "kind": "plugin",
115 "apply": false,
116 })
117 if !resp.OK || len(resp.Actions) != 1 {
118 t.Fatalf("response = %+v", resp)
119 }
120 act := resp.Actions[0]
121 if act.Runtime != nil {
122 t.Fatalf("legacy package gained a runtime: %+v", act.Runtime)
123 }
124 if act.RiskLevel != RiskMedium {
125 t.Fatalf("RiskLevel = %q, want medium for a plain legacy package", act.RiskLevel)
126 }
127 raw, err := json.Marshal(act)
128 if err != nil {
129 t.Fatal(err)
130 }
131 for _, key := range []string{`"runtime"`, `"promptCount"`, `"themeCount"`} {
132 if strings.Contains(string(raw), key) {
133 t.Fatalf("legacy plan JSON should omit %s: %s", key, raw)
134 }
135 }
136 }
137
137 lines GO