From 2b015a56d520c27d3ff7d5e3ff5018717a203774 Mon Sep 17 00:00:00 2001 From: Alex Dunmow Date: Wed, 19 Aug 2026 23:23:01 +0800 Subject: [PATCH] feat(plugin): preserve admin API compatibility --- cmd/ninja/cmd/plugin.go | 12 +++-- cmd/ninja/cmd/plugin_test.go | 88 +++++++++++++++++++++++++++++++----- go.mod | 2 +- go.sum | 4 +- 4 files changed, 88 insertions(+), 18 deletions(-) diff --git a/cmd/ninja/cmd/plugin.go b/cmd/ninja/cmd/plugin.go index dc666e9..6e7fedc 100644 --- a/cmd/ninja/cmd/plugin.go +++ b/cmd/ninja/cmd/plugin.go @@ -17,10 +17,10 @@ import ( core "git.dev.alexdunmow.com/block/pluginsdk/plugin" + v1 "git.dev.alexdunmow.com/block/cli/internal/api/orchestrator/v1" "git.dev.alexdunmow.com/block/cli/internal/bnp" "git.dev.alexdunmow.com/block/cli/internal/creds" "git.dev.alexdunmow.com/block/cli/internal/orchclient" - v1 "git.dev.alexdunmow.com/block/cli/internal/api/orchestrator/v1" ) func newPluginCmd() *cobra.Command { @@ -1144,9 +1144,15 @@ func writeMod(path string, m *core.ModFile) error { if m.Plugin.Sitemap { b.WriteString("sitemap = true\n") } - if m.Compatibility != nil { + if m.Compatibility != nil && + (m.Compatibility.BlockCore != "" || m.Compatibility.AdminAPI != "") { b.WriteString("\n[compatibility]\n") - fmt.Fprintf(&b, "block_core = %q\n", m.Compatibility.BlockCore) + if m.Compatibility.BlockCore != "" { + fmt.Fprintf(&b, "block_core = %q\n", m.Compatibility.BlockCore) + } + if m.Compatibility.AdminAPI != "" { + fmt.Fprintf(&b, "admin_api = %q\n", m.Compatibility.AdminAPI) + } } for _, r := range m.Requires { b.WriteString("\n[[requires]]\n") diff --git a/cmd/ninja/cmd/plugin_test.go b/cmd/ninja/cmd/plugin_test.go index 866ef3b..13c9e93 100644 --- a/cmd/ninja/cmd/plugin_test.go +++ b/cmd/ninja/cmd/plugin_test.go @@ -364,6 +364,55 @@ func TestWriteMod_PrivateFalseOmitted(t *testing.T) { } } +func TestWriteMod_CompatibilityFields(t *testing.T) { + tests := []struct { + name string + compat *core.ModCompat + want []string + doNotWant []string + }{ + { + name: "admin api only", + compat: &core.ModCompat{AdminAPI: ">=0.1.2"}, + want: []string{"[compatibility]", `admin_api = ">=0.1.2"`}, + doNotWant: []string{"block_core"}, + }, + { + name: "empty compatibility omitted", + compat: &core.ModCompat{}, + doNotWant: []string{"[compatibility]", "block_core", "admin_api"}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + path := filepath.Join(t.TempDir(), "plugin.mod") + m := &core.ModFile{ + Plugin: core.ModPlugin{Name: "myplugin", Version: "0.1.0"}, + Compatibility: test.compat, + } + if err := writeMod(path, m); err != nil { + t.Fatalf("writeMod: %v", err) + } + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read back: %v", err) + } + got := string(data) + for _, want := range test.want { + if !strings.Contains(got, want) { + t.Errorf("missing %q:\n%s", want, got) + } + } + for _, doNotWant := range test.doNotWant { + if strings.Contains(got, doNotWant) { + t.Errorf("unexpected %q:\n%s", doNotWant, got) + } + } + }) + } +} + // TestWriteMod_RoundTripsFirstClassFields guards the writeMod ⇄ ParseModFull // round-trip for the first-class fields the hand-rolled writer must emit: a // bump/init/tags edit re-serializes plugin.mod, and any field the writer forgets @@ -373,19 +422,25 @@ func TestWriteMod_PrivateFalseOmitted(t *testing.T) { func TestWriteMod_RoundTripsFirstClassFields(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "plugin.mod") - m := &core.ModFile{Plugin: core.ModPlugin{ - Name: "myplugin", - Scope: "ninja", - Version: "1.2.3", - AllowedHosts: []string{"api.cal.com", "*.example.com"}, - MaxResponseMB: 25, - RequiredIconPacks: []string{"tabler"}, - PublicRoutes: []core.ModPublicRoute{ - {Path: "/area", Prefix: true}, - {Path: "/api/semantic-search"}, + m := &core.ModFile{ + Plugin: core.ModPlugin{ + Name: "myplugin", + Scope: "ninja", + Version: "1.2.3", + AllowedHosts: []string{"api.cal.com", "*.example.com"}, + MaxResponseMB: 25, + RequiredIconPacks: []string{"tabler"}, + PublicRoutes: []core.ModPublicRoute{ + {Path: "/area", Prefix: true}, + {Path: "/api/semantic-search"}, + }, + Sitemap: true, }, - Sitemap: true, - }} + Compatibility: &core.ModCompat{ + BlockCore: ">=0.3.4", + AdminAPI: ">=0.1.2", + }, + } if err := writeMod(path, m); err != nil { t.Fatalf("writeMod: %v", err) } @@ -414,6 +469,15 @@ func TestWriteMod_RoundTripsFirstClassFields(t *testing.T) { if !back.Plugin.Sitemap { t.Errorf("sitemap not preserved\n%s", data) } + if back.Compatibility == nil { + t.Fatalf("compatibility not preserved\n%s", data) + } + if back.Compatibility.BlockCore != ">=0.3.4" { + t.Errorf("block_core not preserved: %q\n%s", back.Compatibility.BlockCore, data) + } + if back.Compatibility.AdminAPI != ">=0.1.2" { + t.Errorf("admin_api not preserved: %q\n%s", back.Compatibility.AdminAPI, data) + } } func TestParsePrivateCoord(t *testing.T) { diff --git a/go.mod b/go.mod index 66db554..ed451b6 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.26.4 require ( connectrpc.com/connect v1.20.0 git.dev.alexdunmow.com/block/core v0.18.2 - git.dev.alexdunmow.com/block/pluginsdk v0.3.3 + git.dev.alexdunmow.com/block/pluginsdk v0.3.6 github.com/chromedp/cdproto v0.0.0-20260321001828-e3e3800016bc github.com/chromedp/chromedp v0.15.1 github.com/klauspost/compress v1.18.6 diff --git a/go.sum b/go.sum index 52956f9..1a6a84a 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ= connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4= git.dev.alexdunmow.com/block/core v0.18.2 h1:+3OfZ424yoc1k1CucXYARk8TBkh8Z693HRkhf5Ci2wU= git.dev.alexdunmow.com/block/core v0.18.2/go.mod h1:GGuUu826AoJepC/hKLGJ7BX3PQaDss9ueCT0se6Ao2w= -git.dev.alexdunmow.com/block/pluginsdk v0.3.3 h1:SEvKqCcvYmqy6ToA9j9oaSORmQFplOeQFbqAZfrJeUE= -git.dev.alexdunmow.com/block/pluginsdk v0.3.3/go.mod h1:Z+eG+WZxAP0jfreLqlGcc0kkWKt8RWevzWyWn8d+dhM= +git.dev.alexdunmow.com/block/pluginsdk v0.3.6 h1:wK5gUAmK1gFrCWLqR4zRaq+aSAPgNmpTKHDK8nPX4OI= +git.dev.alexdunmow.com/block/pluginsdk v0.3.6/go.mod h1:Z+eG+WZxAP0jfreLqlGcc0kkWKt8RWevzWyWn8d+dhM= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/chromedp/cdproto v0.0.0-20260321001828-e3e3800016bc h1:wkN/LMi5vc60pBRWx6qpbk/aEvq3/ZVNpnMvsw8PVVU=