Files
summercms/party/registry_test.go
Jakub Zych f824e55c81 feat(04-02): load namespaced translations from embedded plugins
- Walk HasLang FS for lang/<locale>/<group>.yaml and flatten nested keys
- Publish one phrasebook.Translator before Boot with en/en defaults
- Hello plugin resolves en/pl vendor.plugin::group.key after Activate

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-18 13:47:45 +02:00

458 lines
13 KiB
Go

package party
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"testing"
"testing/fstest"
"git.golem15.com/golem15/summercms/backpack"
"git.golem15.com/golem15/summercms/bonfire"
"git.golem15.com/golem15/summercms/compass"
"git.golem15.com/golem15/summercms/pact"
"git.golem15.com/golem15/summercms/phrasebook"
)
type tracePlugin struct {
id string
req []string
log *[]string
}
func (p *tracePlugin) ID() string { return p.id }
func (p *tracePlugin) Requires() []string { return p.req }
func (p *tracePlugin) Register(*backpack.App) error {
*p.log = append(*p.log, p.id+":register")
return nil
}
func (p *tracePlugin) Boot(*backpack.App) error {
*p.log = append(*p.log, p.id+":boot")
return nil
}
type configPlugin struct {
id string
fsys fs.FS
got *int
}
func (p *configPlugin) ID() string { return p.id }
func (p *configPlugin) Requires() []string { return nil }
func (p *configPlugin) ConfigFS() fs.FS { return p.fsys }
func (p *configPlugin) Register(app *backpack.App) error {
if app != nil && app.Config != nil {
*p.got = app.Config.Int("golem15.hello.posts_per_page")
}
return nil
}
func (p *configPlugin) Boot(*backpack.App) error { return nil }
func TestActivateMergesHasConfigBeforeRegister(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "app.yaml"), []byte("name: app\n"), 0o644); err != nil {
t.Fatal(err)
}
cfg, err := compass.Load(dir)
if err != nil {
t.Fatalf("Load: %v", err)
}
var got int
p := &configPlugin{
id: "golem15.hello",
got: &got,
fsys: fstest.MapFS{
"config/config.yaml": {Data: []byte("posts_per_page: 10\n")},
},
}
app := backpack.New(cfg)
if _, err := activate([]Plugin{p}, app, []string{"golem15.hello"}); err != nil {
t.Fatalf("activate: %v", err)
}
if got != 10 {
t.Fatalf("posts_per_page during Register = %d, want 10 (HasConfig must merge before Register)", got)
}
}
func TestActivateMissingRequiresNamesIDsAndSkipsBoot(t *testing.T) {
var log []string
p := &tracePlugin{id: "golem15.greeter", req: []string{"golem15.hello"}, log: &log}
app := backpack.New(&compass.Config{})
_, err := activate([]Plugin{p}, app, []string{"golem15.greeter"})
if err == nil {
t.Fatal("expected missing dependency error")
}
msg := err.Error()
if !strings.Contains(msg, "golem15.greeter") || !strings.Contains(msg, "golem15.hello") {
t.Fatalf("error %q should name both plugin IDs", msg)
}
if strings.Contains(strings.Join(log, ","), "boot") {
t.Fatalf("Boot ran despite missing dependency: %v", log)
}
}
func TestActivateCycleNamesIDsAndSkipsBoot(t *testing.T) {
var log []string
a := &tracePlugin{id: "golem15.a", req: []string{"golem15.b"}, log: &log}
b := &tracePlugin{id: "golem15.b", req: []string{"golem15.a"}, log: &log}
app := backpack.New(&compass.Config{})
_, err := activate([]Plugin{a, b}, app, []string{"golem15.a", "golem15.b"})
if err == nil {
t.Fatal("expected cycle error")
}
msg := err.Error()
if !strings.Contains(msg, "golem15.a") || !strings.Contains(msg, "golem15.b") {
t.Fatalf("error %q should name cycle members", msg)
}
if strings.Contains(strings.Join(log, ","), "boot") || strings.Contains(strings.Join(log, ","), "register") {
t.Fatalf("lifecycle ran despite cycle: %v", log)
}
}
func TestActivateDuplicateIDRejected(t *testing.T) {
var log []string
a := &tracePlugin{id: "golem15.hello", log: &log}
dup := &tracePlugin{id: "golem15.hello", log: &log}
app := backpack.New(&compass.Config{})
_, err := activate([]Plugin{a, dup}, app, []string{"golem15.hello"})
if err == nil {
t.Fatal("expected duplicate id error")
}
if !strings.Contains(err.Error(), "golem15.hello") {
t.Fatalf("error %q should name the duplicate id", err)
}
}
type bootProbe struct {
id string
req []string
log *[]string
sawOpt *bool
booted *[]string
}
func (p *bootProbe) ID() string { return p.id }
func (p *bootProbe) Requires() []string { return p.req }
func (p *bootProbe) Register(*backpack.App) error {
*p.log = append(*p.log, p.id+":register")
return nil
}
func (p *bootProbe) Boot(app *backpack.App) error {
if p.sawOpt != nil && p.id == "golem15.hello" {
*p.sawOpt = app.HasPlugin("golem15.optional")
}
*p.log = append(*p.log, p.id+":boot")
if p.booted != nil {
*p.booted = append(*p.booted, p.id)
}
return nil
}
func TestActivateHasPluginSeesNotYetBootedPlugins(t *testing.T) {
var log []string
var saw bool
hello := &bootProbe{id: "golem15.hello", log: &log, sawOpt: &saw}
opt := &bootProbe{id: "golem15.optional", log: &log}
app := backpack.New(&compass.Config{})
if _, err := activate([]Plugin{opt, hello}, app, []string{"golem15.hello", "golem15.optional"}); err != nil {
t.Fatalf("activate: %v", err)
}
if !saw {
t.Fatal("HasPlugin(golem15.optional) was false during hello Boot; complete set must be visible before Boot")
}
want := []string{
"golem15.hello:register",
"golem15.optional:register",
"golem15.hello:boot",
"golem15.optional:boot",
}
if strings.Join(log, ",") != strings.Join(want, ",") {
t.Fatalf("lifecycle = %v, want %v", log, want)
}
}
func TestActivateIndependentPluginsKeepManifestOrder(t *testing.T) {
var log []string
a := &tracePlugin{id: "golem15.a", log: &log}
b := &tracePlugin{id: "golem15.b", log: &log}
c := &tracePlugin{id: "golem15.c", log: &log}
app := backpack.New(&compass.Config{})
got, err := activate([]Plugin{c, a, b}, app, []string{"golem15.c", "golem15.a", "golem15.b"})
if err != nil {
t.Fatalf("activate: %v", err)
}
if len(got) != 3 || got[0].ID() != "golem15.c" || got[1].ID() != "golem15.a" || got[2].ID() != "golem15.b" {
t.Fatalf("order = %v", idsOf(got))
}
}
func idsOf(plugins []Plugin) []string {
out := make([]string, len(plugins))
for i, p := range plugins {
out[i] = p.ID()
}
return out
}
func TestActivateRunsAllRegisterBeforeAnyBoot(t *testing.T) {
var log []string
hello := &tracePlugin{id: "golem15.hello", log: &log}
greeter := &tracePlugin{id: "golem15.greeter", req: []string{"golem15.hello"}, log: &log}
app := backpack.New(&compass.Config{})
got, err := activate([]Plugin{greeter, hello}, app, []string{"golem15.greeter", "golem15.hello"})
if err != nil {
t.Fatalf("activate: %v", err)
}
if len(got) != 2 {
t.Fatalf("got %d plugins, want 2", len(got))
}
if got[0].ID() != "golem15.hello" || got[1].ID() != "golem15.greeter" {
t.Fatalf("order = %s, %s", got[0].ID(), got[1].ID())
}
want := []string{
"golem15.hello:register",
"golem15.greeter:register",
"golem15.hello:boot",
"golem15.greeter:boot",
}
if strings.Join(log, ",") != strings.Join(want, ",") {
t.Fatalf("lifecycle = %v, want %v", log, want)
}
}
func TestActivateReorderedManifestStillTopoSorts(t *testing.T) {
var log []string
hello := &tracePlugin{id: "golem15.hello", log: &log}
greeter := &tracePlugin{id: "golem15.greeter", req: []string{"golem15.hello"}, log: &log}
extra := &tracePlugin{id: "golem15.extra", req: []string{"golem15.greeter"}, log: &log}
app := backpack.New(&compass.Config{})
got, err := activate(
[]Plugin{extra, greeter, hello},
app,
[]string{"golem15.extra", "golem15.hello", "golem15.greeter"},
)
if err != nil {
t.Fatalf("activate: %v", err)
}
if len(got) != 3 || got[0].ID() != "golem15.hello" || got[1].ID() != "golem15.greeter" || got[2].ID() != "golem15.extra" {
t.Fatalf("order = %v, want hello, greeter, extra", idsOf(got))
}
want := []string{
"golem15.hello:register",
"golem15.greeter:register",
"golem15.extra:register",
"golem15.hello:boot",
"golem15.greeter:boot",
"golem15.extra:boot",
}
if strings.Join(log, ",") != strings.Join(want, ",") {
t.Fatalf("lifecycle = %v, want %v", log, want)
}
}
func TestActivateDuplicateManifestIDRejected(t *testing.T) {
var log []string
p := &tracePlugin{id: "golem15.hello", log: &log}
app := backpack.New(&compass.Config{})
_, err := activate([]Plugin{p}, app, []string{"golem15.hello", "golem15.hello"})
if err == nil {
t.Fatal("expected duplicate manifest id error")
}
if !strings.Contains(err.Error(), "golem15.hello") {
t.Fatalf("error %q should name the duplicate id", err)
}
if len(log) != 0 {
t.Fatalf("lifecycle ran despite duplicate manifest: %v", log)
}
}
func TestActivateUnregisteredPluginNamesID(t *testing.T) {
app := backpack.New(&compass.Config{})
_, err := activate(nil, app, []string{"golem15.missing"})
if err == nil {
t.Fatal("expected missing registered plugin error")
}
if !strings.Contains(err.Error(), "golem15.missing") {
t.Fatalf("error %q should name the missing id", err)
}
}
func TestActivateEmptyManifestIDRejected(t *testing.T) {
var log []string
p := &tracePlugin{id: "golem15.hello", log: &log}
app := backpack.New(&compass.Config{})
_, err := activate([]Plugin{p}, app, []string{""})
if err == nil {
t.Fatal("expected empty plugin id error")
}
}
func TestActivateNilAppRejected(t *testing.T) {
_, err := Activate(nil, []string{"golem15.hello"})
if err == nil {
t.Fatal("expected nil app error")
}
}
func TestRegisterNilIsNoop(t *testing.T) {
Register(nil)
}
func TestActivateHasPluginFalseForUnselected(t *testing.T) {
var log []string
hello := &tracePlugin{id: "golem15.hello", log: &log}
opt := &tracePlugin{id: "golem15.optional", log: &log}
app := backpack.New(&compass.Config{})
if _, err := activate([]Plugin{hello, opt}, app, []string{"golem15.hello"}); err != nil {
t.Fatalf("activate: %v", err)
}
if !app.HasPlugin("golem15.hello") {
t.Fatal("selected plugin must be present")
}
if app.HasPlugin("golem15.optional") {
t.Fatal("unselected registered plugin must not appear in HasPlugin")
}
}
type commandsPlugin struct {
tracePlugin
}
func (p *commandsPlugin) Commands() []bonfire.Command {
return []bonfire.Command{{
Name: "demo:run",
Description: "demo command",
}}
}
func TestActivateDiscoversHasConfigAndHasCommands(t *testing.T) {
var log []string
var got int
cfgPlugin := &configPlugin{
id: "golem15.hello",
got: &got,
fsys: fstest.MapFS{
"config/config.yaml": {Data: []byte("posts_per_page: 4\n")},
},
}
cmdPlugin := &commandsPlugin{tracePlugin: tracePlugin{id: "golem15.demo", log: &log}}
plain := &tracePlugin{id: "golem15.plain", log: &log}
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "app.yaml"), []byte("name: app\n"), 0o644); err != nil {
t.Fatal(err)
}
cfg, err := compass.Load(dir)
if err != nil {
t.Fatalf("Load: %v", err)
}
app := backpack.New(cfg)
gotPlugins, err := activate([]Plugin{cfgPlugin, cmdPlugin, plain}, app, []string{"golem15.hello", "golem15.demo", "golem15.plain"})
if err != nil {
t.Fatalf("activate: %v", err)
}
if got != 4 {
t.Fatalf("HasConfig value during Register = %d, want 4", got)
}
var sawConfig, sawCommands, sawPlainCommands bool
for _, p := range gotPlugins {
if _, ok := p.(pact.HasConfig); ok && p.ID() == "golem15.hello" {
sawConfig = true
}
if hc, ok := p.(pact.HasCommands); ok {
if p.ID() == "golem15.demo" {
cmds := hc.Commands()
if len(cmds) != 1 || cmds[0].Name != "demo:run" {
t.Fatalf("HasCommands commands = %+v", cmds)
}
sawCommands = true
}
if p.ID() == "golem15.plain" {
sawPlainCommands = true
}
}
}
if !sawConfig {
t.Fatal("golem15.hello should satisfy HasConfig")
}
if !sawCommands {
t.Fatal("golem15.demo should satisfy HasCommands")
}
if sawPlainCommands {
t.Fatal("plain plugin must not satisfy HasCommands")
}
if _, ok := any(plain).(pact.HasConfig); ok {
t.Fatal("plain plugin must not satisfy HasConfig")
}
}
type langPlugin struct {
id string
fsys fs.FS
got *string
}
func (p *langPlugin) ID() string { return p.id }
func (p *langPlugin) Requires() []string { return nil }
func (p *langPlugin) Register(*backpack.App) error { return nil }
func (p *langPlugin) LangFS() fs.FS { return p.fsys }
func (p *langPlugin) Boot(app *backpack.App) error {
if p.got == nil {
return nil
}
tr, ok := app.Lookup[*phrasebook.Translator]()
if !ok {
return fmt.Errorf("translator missing at boot")
}
*p.got = tr.GetIn("en", p.id+"::lang.greeting", nil)
return nil
}
func TestActivatePublishesTranslatorBeforeBoot(t *testing.T) {
var got string
p := &langPlugin{
id: "golem15.hello",
got: &got,
fsys: fstest.MapFS{
"lang/en/lang.yaml": {Data: []byte("greeting: Hello\n")},
},
}
app := backpack.New(&compass.Config{})
if _, err := activate([]Plugin{p}, app, []string{"golem15.hello"}); err != nil {
t.Fatalf("activate: %v", err)
}
if got != "Hello" {
t.Fatalf("boot lookup = %q, want Hello (translator must be published before Boot)", got)
}
}
func TestActivateMalformedLangFailsWithPluginContext(t *testing.T) {
p := &langPlugin{
id: "golem15.hello",
fsys: fstest.MapFS{
"lang/en.yaml": {Data: []byte("greeting: Hello\n")},
},
}
app := backpack.New(&compass.Config{})
_, err := activate([]Plugin{p}, app, []string{"golem15.hello"})
if err == nil {
t.Fatal("expected malformed catalog error")
}
msg := err.Error()
if !strings.Contains(msg, "golem15.hello") || !strings.Contains(msg, "malformed path") {
t.Fatalf("error %q should name the plugin and path problem", msg)
}
}