test: the AI models admin schemas compile with a write-only key, and golem:import-settings runs as the CLI runs it
- TestGolemAdminSchemas: no key column in the list, a password api_key field, the access_settings permission, a model that never serializes its key, the admin hooks' refusals - TestGolemImportCommand: missing app or database, the notice path, a numbered repeater with PHP's scalar forms, a refused second run and malformed or missing files
This commit is contained in:
170
golem_admin_test.go
Normal file
170
golem_admin_test.go
Normal file
@@ -0,0 +1,170 @@
|
||||
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") {
|
||||
t.Fatalf("the model serializes its key: %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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user