feat(plugin): preserve admin API compatibility

This commit is contained in:
Alex Dunmow 2026-08-19 23:23:01 +08:00
parent d3e2bc65a2
commit 2b015a56d5
4 changed files with 88 additions and 18 deletions

View File

@ -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")

View File

@ -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) {

2
go.mod
View File

@ -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

4
go.sum
View File

@ -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=