package main import ( "encoding/base64" "encoding/json" "os" "path/filepath" "strings" "testing" "git.eidonix.com/eidonix/cletus-plugins/sdk/manifest" ) func newTestHost(t *testing.T, perms ...string) *testHost { t.Helper() root := t.TempDir() files, ws := filepath.Join(root, "files"), filepath.Join(root, "ws") for _, d := range []string{files, ws} { if err := os.MkdirAll(d, 0o755); err != nil { t.Fatal(err) } } if err := os.WriteFile(filepath.Join(files, "a.txt"), []byte("a"), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(ws, "ref.csv"), []byte("x,y"), 0o644); err != nil { t.Fatal(err) } return &testHost{plugin: &manifest.Plugin{ID: "acme.demo", Permissions: perms}, filesDir: files, workspaceDir: ws, dataDir: filepath.Join(root, "data"), capabilities: map[string]bool{}} } func ask(t *testing.T, h *testHost, method string, params any) (any, error) { t.Helper() raw, _ := json.Marshal(params) return h.answer(method, raw) } func TestPermissionsGateHostCalls(t *testing.T) { h := newTestHost(t) _, err := ask(t, h, "host.files.resolve", map[string]string{"path": "a.txt"}) if err == nil || !strings.Contains(err.Error(), "needs the files permission") { t.Fatalf("undeclared files call = %v", err) } if _, err := ask(t, h, "host.code.llm.start", nil); err == nil || !strings.Contains(err.Error(), "unknown plugin host operation") { t.Fatalf("first-party call = %v", err) } if _, err := ask(t, h, "host.event", map[string]any{"name": "other:x"}); err == nil { t.Fatal("an event outside the plugin's prefix was allowed") } if _, err := ask(t, h, "host.event", map[string]any{"name": "acme.demo:x"}); err != nil { t.Fatalf("own event = %v", err) } } func TestFilesStayInsideTheOpenedFolder(t *testing.T) { h := newTestHost(t, "files") got, err := ask(t, h, "host.files.resolve", map[string]string{"path": "a.txt"}) if err != nil || got != filepath.Join(h.filesDir, "a.txt") { t.Fatalf("resolve = %v, %v", got, err) } for _, path := range []string{"../escape.txt", "/etc/hosts"} { if _, err := ask(t, h, "host.files.resolve", map[string]string{"path": path}); err == nil { t.Errorf("resolve(%s) was allowed", path) } } if got, _ := ask(t, h, "host.files.can_write", map[string]string{"path": "new.txt"}); got != false { t.Fatal("can_write without -edit should be false") } h.edit = true if got, _ := ask(t, h, "host.files.can_write", map[string]string{"path": "new.txt"}); got != true { t.Fatal("can_write with -edit should be true") } } func TestWorkspaceSimulation(t *testing.T) { h := newTestHost(t, "files", "workspace.read", "workspace.write") listed, err := ask(t, h, "host.workspace.list", map[string]string{"workspace": testWorkspaceName}) if err != nil || len(listed.([]map[string]any)) != 1 { t.Fatalf("list = %v, %v", listed, err) } key := testWorkspaceID + "/files/ref.csv" fetched, err := ask(t, h, "host.workspace.fetch", map[string]string{"key": key}) if err != nil { t.Fatal(err) } path := fetched.(map[string]string)["path"] if _, err := ask(t, h, "host.files.resolve", map[string]string{"path": path}); err != nil { t.Fatalf("a fetched copy should resolve: %v", err) } data := base64.StdEncoding.EncodeToString([]byte("new")) if _, err := ask(t, h, "host.workspace.put", map[string]string{"workspace": testWorkspaceID, "key": "out/r.txt", "data": data}); err != nil { t.Fatal(err) } if _, err := ask(t, h, "host.workspace.put", map[string]string{"workspace": testWorkspaceID, "key": "../x", "data": data}); err == nil { t.Fatal("a key with .. was accepted") } if _, err := ask(t, h, "host.workspace.read", map[string]string{"key": testWorkspaceID + "/files/out/r.txt"}); err != nil { t.Fatal(err) } }