171 lines
6.8 KiB
Go
171 lines
6.8 KiB
Go
package golem
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/sm-golem-plugin/console"
|
|
"git.golem15.com/golem15/sm-golem-plugin/controllers"
|
|
"git.golem15.com/golem15/sm-golem-plugin/internal/pgtest"
|
|
"git.golem15.com/golem15/sm-golem-plugin/models"
|
|
"git.golem15.com/golem15/summercms/modules/backpack"
|
|
"git.golem15.com/golem15/summercms/modules/bonfire"
|
|
"git.golem15.com/golem15/summercms/modules/cabana"
|
|
"git.golem15.com/golem15/summercms/modules/lagoon"
|
|
)
|
|
|
|
// TestGolemAdminSchemas compiles the AI models admin YAML under cabana: the
|
|
// list shows no key column, the form's api_key is a password input (write
|
|
// only, T-14-24), the screen requires golem15.golem.access_settings, the
|
|
// model hides its key from every serialization, and the plugin hands its
|
|
// migrations and models to the framework.
|
|
func TestGolemAdminSchemas(t *testing.T) {
|
|
p := &Plugin{}
|
|
ctl := p.AdminControllers()[0]
|
|
list, err := cabana.CompileList("golem15.golem", ctl, p.AdminFS())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
form, err := cabana.CompileForm("golem15.golem", ctl, p.AdminFS())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
listJSON, _ := json.Marshal(list)
|
|
formJSON, _ := json.Marshal(form)
|
|
if bytes.Contains(listJSON, []byte("api_key")) {
|
|
t.Fatalf("the list shows the key: %s", listJSON)
|
|
}
|
|
for _, col := range []string{`"name"`, `"adapter"`, `"model"`, `"is_enabled"`, `"sort_order"`} {
|
|
if !bytes.Contains(listJSON, []byte(col)) {
|
|
t.Errorf("the list lacks %s: %s", col, listJSON)
|
|
}
|
|
}
|
|
if !bytes.Contains(formJSON, []byte(`"api_key"`)) || !bytes.Contains(formJSON, []byte(`"password"`)) {
|
|
t.Fatalf("the form's api_key is not a password input: %s", formJSON)
|
|
}
|
|
type permissioned interface{ RequiredPermissions() []string }
|
|
if pc, ok := ctl.(permissioned); !ok || strings.Join(pc.RequiredPermissions(), ",") != controllers.PermissionAccessSettings {
|
|
t.Fatalf("the models screen permissions = %v", ctl)
|
|
}
|
|
if hidden := (models.AiModel{}).Hidden(); strings.Join(hidden, ",") != "api_key" {
|
|
t.Errorf("Hidden() = %v", hidden)
|
|
}
|
|
m := models.AiModel{Name: "Hidden", APIKey: lagoon.NewEncrypted("sk-hidden-0001")}
|
|
raw, _ := json.Marshal(m)
|
|
if strings.Contains(string(raw), "sk-hidden-0001") || strings.Contains(string(raw), "api_key") {
|
|
t.Fatalf("the model serializes its key, even redacted: %s", raw)
|
|
}
|
|
if len(models.All()) == 0 || len(p.Models()) == 0 || len(p.Migrations()) == 0 {
|
|
t.Fatal("the plugin hands no models or migrations to the framework")
|
|
}
|
|
|
|
// The admin hooks refuse a record of another type, and an update
|
|
// outside cabana's transaction.
|
|
c := controllers.ModelsAdminController{}
|
|
if err := c.FormBeforeCreate(context.Background(), &struct{}{}); err == nil {
|
|
t.Error("FormBeforeCreate accepted a foreign record")
|
|
}
|
|
if err := c.FormBeforeUpdate(context.Background(), &struct{}{}); err == nil {
|
|
t.Error("FormBeforeUpdate accepted a foreign record")
|
|
}
|
|
if err := c.FormBeforeUpdate(context.Background(), &models.AiModel{}); err == nil {
|
|
t.Error("FormBeforeUpdate ran without a transaction")
|
|
}
|
|
if err := c.FormBeforeUpdate(context.Background(), &models.AiModel{APIKey: lagoon.NewEncrypted("sk-new-0001")}); err != nil {
|
|
t.Errorf("a new key needs no stored one: %v", err)
|
|
}
|
|
}
|
|
|
|
// stubInput is a bonfire.Input with one --file flag.
|
|
type stubInput struct{ file string }
|
|
|
|
func (s stubInput) Args() []string { return nil }
|
|
func (s stubInput) Argument(string) (string, bool) { return "", false }
|
|
func (s stubInput) Flags(string) []string { return nil }
|
|
func (s stubInput) Flag(name string) (string, bool) { return s.file, name == "file" && s.file != "" }
|
|
|
|
// TestGolemImportCommand runs golem:import-settings as the CLI does: it
|
|
// refuses a missing app or database, reports a clean notice as info and
|
|
// exit 0, imports a file, and fails on unreadable or malformed settings,
|
|
// reading every repeater shape and scalar form PHP stores.
|
|
func TestGolemImportCommand(t *testing.T) {
|
|
ctx := context.Background()
|
|
run := func(app *backpack.App, file string) (string, error) {
|
|
var buf bytes.Buffer
|
|
err := console.ImportSettings(app).Run(ctx, stubInput{file: file}, bonfire.NewOutput(nil, &buf, &buf))
|
|
return buf.String(), err
|
|
}
|
|
if _, err := run(nil, ""); err == nil {
|
|
t.Fatal("a nil app must fail")
|
|
}
|
|
bare := backpack.New(testConfig(t))
|
|
if _, err := run(bare, ""); err == nil {
|
|
t.Fatal("an app without a database must fail")
|
|
}
|
|
db := pgtest.DB(t)
|
|
app := backpack.New(testConfig(t))
|
|
if err := lagoon.Publish(app, pgtest.SQL(t), db); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Exec(`DELETE FROM golem15_golem_models`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Exec(`DROP TABLE IF EXISTS system_settings`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if out, err := run(app, ""); err != nil || !strings.Contains(out, "nothing to import") {
|
|
t.Fatalf("a missing settings table = %q %v", out, err)
|
|
}
|
|
dir := t.TempDir()
|
|
write := func(name, body string) string {
|
|
path := filepath.Join(dir, name)
|
|
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
}
|
|
// A repeater saved with numbered keys, scalars as numbers and booleans.
|
|
numbered := write("numbered.json", `{"models":{"0":{"name":"Zero","model":"m0","api_key":"k0","is_enabled":1,"is_default":true,"accepts_images":"1","max_completion_tokens":2048},`+
|
|
`"1":{"name":7,"model":true,"api_key":"k1","is_enabled":0,"generates_images":"0","max_completion_tokens":"x"},"2":"not an item"}}`)
|
|
out, err := run(app, numbered)
|
|
if err != nil || !strings.Contains(out, "Imported 2 AI models.") {
|
|
t.Fatalf("numbered import = %q %v", out, err)
|
|
}
|
|
var rows []models.AiModel
|
|
if err := db.Order("sort_order").Find(&rows).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(rows) != 2 || !rows[0].IsEnabled || !rows[0].IsDefault || !rows[0].AcceptsImages || *rows[0].MaxCompletionTokens != 2048 ||
|
|
rows[1].Name != "7" || rows[1].Model != "1" || rows[1].IsEnabled || rows[1].MaxCompletionTokens != nil {
|
|
t.Fatalf("numbered rows = %+v", rows)
|
|
}
|
|
if out, err := run(app, numbered); !errors.Is(err, console.ErrModelsNotEmpty) || !strings.Contains(out, "already has rows") {
|
|
t.Fatalf("a second import = %q %v", out, err)
|
|
}
|
|
if err := db.Exec(`DELETE FROM golem15_golem_models`).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for name, body := range map[string]string{"empty.json": `{"models":[]}`, "null.json": `{"models":null}`} {
|
|
if out, err := run(app, write(name, body)); err != nil || !strings.Contains(out, "no AI models") {
|
|
t.Errorf("%s = %q %v", name, out, err)
|
|
}
|
|
}
|
|
for name, body := range map[string]string{"bad.json": `{not json`, "scalar.json": `{"models":"nope"}`} {
|
|
if _, err := run(app, write(name, body)); err == nil {
|
|
t.Errorf("%s was imported", name)
|
|
}
|
|
}
|
|
if _, err := run(app, filepath.Join(dir, "missing.json")); err == nil {
|
|
t.Error("a missing file was imported")
|
|
}
|
|
if pgtest.SQL(t) == nil {
|
|
t.Fatal("pgtest.SQL")
|
|
}
|
|
}
|