Files
summercms/internal/build/manifest_append_test.go
Jakub Zych e6b3aa2222 fix(34): WR-11 accept an inline plugins list in plugin:add again
The comment-keeping summer.yaml edit refused a flow-style plugins list
(plugins: [{id: a, module: b}]) that plugin:add used to handle with a
full rewrite. The edit now replaces an inline list, over one or several
lines, with a block list of its items plus the new ones and keeps every
other line. A list it cannot edit in place (a tagged one, say) falls
back to the full SaveManifest rewrite with a note that the comments were
not kept. plugin:install gets the same behaviour.
2026-10-29 22:15:09 +01:00

163 lines
7.4 KiB
Go

package build
import (
"os"
"path/filepath"
"strings"
"testing"
)
// TestAppendManifestPlugins pins the in-place summer.yaml edit of
// plugin:add and plugin:install: comments, blank lines and key order stay,
// new items take the existing indentation, a missing plugins block is
// appended, and an edit that does not parse back leaves the file as it was.
func TestAppendManifestPlugins(t *testing.T) {
add := []Plugin{{ID: "acme.shop", Module: "example.test/sm-shop-plugin"}, {ID: "acme.pgsubs", Module: "example.test/pg/subs"}}
cases := []struct {
name, src, want string
}{
{
name: "comments, blank lines and later keys stay",
src: "# The application.\nmodule: example.test/app # app module\nbinary: app\n\nplugins:\n" +
" # Core plugins.\n - id: acme.user\n module: example.test/sm-user-plugin # pinned\n\n" +
" - id: acme.blog\n module: example.test/sm-blog-plugin\n\n# Build tags.\ntags:\n - summercms_mcp\n",
want: "# The application.\nmodule: example.test/app # app module\nbinary: app\n\nplugins:\n" +
" # Core plugins.\n - id: acme.user\n module: example.test/sm-user-plugin # pinned\n\n" +
" - id: acme.blog\n module: example.test/sm-blog-plugin\n" +
" - id: acme.shop\n module: example.test/sm-shop-plugin\n - id: acme.pgsubs\n module: example.test/pg/subs\n" +
"\n# Build tags.\ntags:\n - summercms_mcp\n",
},
{
name: "indentation is matched",
src: "module: example.test/app\nbinary: app\nplugins:\n- id: acme.user\n module: example.test/sm-user-plugin\n",
want: "module: example.test/app\nbinary: app\nplugins:\n- id: acme.user\n module: example.test/sm-user-plugin\n" +
"- id: acme.shop\n module: example.test/sm-shop-plugin\n- id: acme.pgsubs\n module: example.test/pg/subs\n",
},
{
name: "four-space indentation",
src: "module: example.test/app\nbinary: app\nplugins:\n - id: acme.user\n module: example.test/sm-user-plugin\n",
want: "module: example.test/app\nbinary: app\nplugins:\n - id: acme.user\n module: example.test/sm-user-plugin\n" +
" - id: acme.shop\n module: example.test/sm-shop-plugin\n - id: acme.pgsubs\n module: example.test/pg/subs\n",
},
{
name: "dash alone on its line",
src: "module: example.test/app\nbinary: app\nplugins:\n -\n id: acme.user\n module: example.test/sm-user-plugin\n",
want: "module: example.test/app\nbinary: app\nplugins:\n -\n id: acme.user\n module: example.test/sm-user-plugin\n" +
" - id: acme.shop\n module: example.test/sm-shop-plugin\n - id: acme.pgsubs\n module: example.test/pg/subs\n",
},
{
name: "missing plugins block is appended",
src: "# App.\nmodule: example.test/app\nbinary: app",
want: "# App.\nmodule: example.test/app\nbinary: app\nplugins:\n" +
" - id: acme.shop\n module: example.test/sm-shop-plugin\n - id: acme.pgsubs\n module: example.test/pg/subs\n",
},
{
name: "inline list becomes a block with the new items",
src: "# App.\nmodule: example.test/app\nbinary: app\nplugins: [{id: acme.user, module: example.test/u}, {id: acme.blog, module: \"example.test/b\"}] # core\n# Tags.\ntags: []\n",
want: "# App.\nmodule: example.test/app\nbinary: app\nplugins:\n" +
" - id: acme.user\n module: example.test/u\n - id: acme.blog\n module: example.test/b\n" +
" - id: acme.shop\n module: example.test/sm-shop-plugin\n - id: acme.pgsubs\n module: example.test/pg/subs\n" +
"# Tags.\ntags: []\n",
},
{
name: "inline list over several lines",
src: "module: example.test/app\nbinary: app\nplugins: [\n {id: acme.user, module: example.test/u}, # first\n {id: acme.blog,\n module: example.test/b},\n]\ntags: []\n",
want: "module: example.test/app\nbinary: app\nplugins:\n" +
" - id: acme.user\n module: example.test/u\n - id: acme.blog\n module: example.test/b\n" +
" - id: acme.shop\n module: example.test/sm-shop-plugin\n - id: acme.pgsubs\n module: example.test/pg/subs\n" +
"tags: []\n",
},
{
name: "empty inline list becomes a block",
src: "module: example.test/app\nbinary: app\nplugins: [] # none yet\ntags: []\n",
want: "module: example.test/app\nbinary: app\nplugins:\n" +
" - id: acme.shop\n module: example.test/sm-shop-plugin\n - id: acme.pgsubs\n module: example.test/pg/subs\n" +
"tags: []\n",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
path := filepath.Join(t.TempDir(), manifestName)
write(t, path, c.src)
if _, err := appendManifestPlugins(path, add); err != nil {
t.Fatalf("appendManifestPlugins: %v", err)
}
got, _ := os.ReadFile(path)
if string(got) != c.want {
t.Errorf("summer.yaml =\n%s\nwant\n%s", got, c.want)
}
m, err := LoadManifest(path)
if err != nil {
t.Fatal(err)
}
n := len(m.Plugins)
if n < 2 || m.Plugins[n-2] != add[0] || m.Plugins[n-1] != add[1] {
t.Errorf("plugins = %+v", m.Plugins)
}
})
}
t.Run("refusals leave the file unchanged", func(t *testing.T) {
refusals := []struct {
name, src string
plugins []Plugin
want string
}{
{"bad id", "module: example.test/app\nbinary: app\n", []Plugin{{ID: "Acme", Module: "example.test/x"}}, "plugins[0]"},
{"bad module", "module: example.test/app\nbinary: app\n", []Plugin{{ID: "acme.x", Module: "-x"}}, "module"},
{"broken file", "module: [\n", add, ""},
// A second YAML document after the plugins block: the items
// land in it, the first document does not gain them, and the
// original bytes are restored.
{"edit does not parse back", "module: example.test/app\nbinary: app\nplugins:\n- id: acme.user\n module: example.test/sm-user-plugin\n---\nother: 1\n", add, "left unchanged"},
}
for _, r := range refusals {
path := filepath.Join(t.TempDir(), manifestName)
write(t, path, r.src)
_, err := appendManifestPlugins(path, r.plugins)
if err == nil || !strings.Contains(err.Error(), r.want) {
t.Errorf("%s: err = %v, want %q", r.name, err, r.want)
}
if got, _ := os.ReadFile(path); string(got) != r.src {
t.Errorf("%s: file changed:\n%s", r.name, got)
}
}
})
t.Run("an inline list it cannot edit is written again", func(t *testing.T) {
// A tagged flow list: the text edit gives up and the manifest is
// written again, as plugin:add did before it kept comments.
path := filepath.Join(t.TempDir(), manifestName)
write(t, path, "# gone\nmodule: example.test/app\nbinary: app\nplugins: !!seq [{id: acme.user, module: example.test/u}]\n")
if _, err := LoadManifest(path); err != nil {
t.Fatalf("fixture does not parse: %v", err)
}
rewrote, err := appendManifestPlugins(path, add)
if err != nil || !rewrote {
t.Fatalf("rewrote %v, err %v", rewrote, err)
}
m, err := LoadManifest(path)
if err != nil || len(m.Plugins) != 3 || m.Plugins[0].ID != "acme.user" || m.Plugins[2] != add[1] {
t.Errorf("plugins = %+v, %v", m.Plugins, err)
}
})
t.Run("nothing to add", func(t *testing.T) {
path := filepath.Join(t.TempDir(), manifestName)
src := "module: example.test/app\nbinary: app\n"
write(t, path, src)
if _, err := appendManifestPlugins(path, nil); err != nil {
t.Fatal(err)
}
if got, _ := os.ReadFile(path); string(got) != src {
t.Errorf("file changed:\n%s", got)
}
})
t.Run("missing file", func(t *testing.T) {
if _, err := appendManifestPlugins(filepath.Join(t.TempDir(), manifestName), add); err == nil {
t.Error("appendManifestPlugins on a missing file succeeded")
}
})
}