Files
kandev/apps/backend/internal/plugins/service_install_test.go
T
2026-09-15 23:16:06 +01:00

678 lines
25 KiB
Go

package plugins
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"path/filepath"
goruntime "runtime"
"strings"
"sync"
"testing"
"github.com/kandev/kandev/internal/plugins/manifest"
"github.com/kandev/kandev/internal/plugins/pkgtar"
"github.com/kandev/kandev/internal/plugins/pkgtar/pkgtartest"
"github.com/kandev/kandev/internal/plugins/store"
)
func TestServiceInstallActivatesOnSuccessfulSpawn(t *testing.T) {
svc, _, rt := newTestService(t)
rec := installTestPlugin(t, svc, "kandev-plugin-slack")
if rec.Status != StatusActive {
t.Fatalf("Install() Status = %q, want %q", rec.Status, StatusActive)
}
if !rt.Running("kandev-plugin-slack") {
t.Fatal("Install() did not spawn the plugin via the runtime manager")
}
got, err := svc.Get("kandev-plugin-slack")
if err != nil {
t.Fatalf("Get() unexpected error: %v", err)
}
if got.InstallPath == "" {
t.Fatal("Get().InstallPath is empty after Install()")
}
}
func TestServiceInstallDuplicateVersionReturnsErrVersionExists(t *testing.T) {
svc, _, _ := newTestService(t)
installTestPlugin(t, svc, "kandev-plugin-slack")
_, err := svc.Install(context.Background(), testPackage(t, "kandev-plugin-slack", "1.0.0", false))
if !errors.Is(err, pkgtar.ErrVersionExists) {
t.Fatalf("Install() duplicate error = %v, want pkgtar.ErrVersionExists", err)
}
}
func TestServiceInstallSpawnFailureLeavesRecordInErrorStatus(t *testing.T) {
svc, fsStore, rt := newTestService(t)
rt.setStartErr("kandev-plugin-slack", errors.New("spawn failed"))
rec, err := svc.Install(context.Background(), testPackage(t, "kandev-plugin-slack", "1.0.0", false))
if err == nil {
t.Fatal("Install() expected an error when the spawn fails")
}
if rec == nil {
t.Fatal("Install() expected a non-nil record even when the spawn fails (package was extracted)")
}
if rec.Status != StatusError {
t.Fatalf("Install() Status = %q, want %q", rec.Status, StatusError)
}
onDisk, getErr := fsStore.Get("kandev-plugin-slack")
if getErr != nil {
t.Fatalf("store.Get(): %v", getErr)
}
if onDisk.Status != StatusError {
t.Fatalf("store.Get().Status = %q, want %q", onDisk.Status, StatusError)
}
}
// TestServiceInstallOverActivePluginRestartsWithNewVersion pins the fix for
// installing a new version over an already-active plugin: activate's
// "already running" short-circuit previously skipped spawning entirely, so
// the live subprocess kept running the OLD version's binary even though the
// record/install_path now pointed at the new one. Install must stop the old
// process and start the new install_path so the running process matches the
// installed version.
func TestServiceInstallOverActivePluginRestartsWithNewVersion(t *testing.T) {
svc, _, rt := newTestService(t)
rec1 := installTestPlugin(t, svc, "kandev-plugin-slack") // v1.0.0, active + running
if !rt.Running("kandev-plugin-slack") {
t.Fatal("sanity check: plugin not running after first install")
}
rec2, err := svc.Install(context.Background(), testPackage(t, "kandev-plugin-slack", "1.1.0", false))
if err != nil {
t.Fatalf("second Install() unexpected error: %v", err)
}
if rec2.Version != "1.1.0" {
t.Fatalf("rec2.Version = %q, want %q", rec2.Version, "1.1.0")
}
if rec2.InstallPath == rec1.InstallPath {
t.Fatal("rec2.InstallPath == rec1.InstallPath, want the new version's own install dir")
}
if !rt.stopped("kandev-plugin-slack") {
t.Fatal("Install() over an active plugin did not stop the old runtime process")
}
if got := rt.startCallCount("kandev-plugin-slack"); got != 2 {
t.Fatalf("runtime Start called %d times, want 2 (initial install + restart on reinstall)", got)
}
if !rt.Running("kandev-plugin-slack") {
t.Fatal("plugin not running after reinstalling over an already-active version")
}
if rec2.Status != StatusActive {
t.Fatalf("rec2.Status = %q, want %q", rec2.Status, StatusActive)
}
}
func TestServiceInstallUpgradeReviewsExistingCapabilityApprovals(t *testing.T) {
svc, _, _ := newTestService(t)
rec1, err := svc.Install(context.Background(), testPackageWithAPIRead(t, "kandev-plugin-slack", "1.0.0", "tasks", "messages"))
if err != nil {
t.Fatalf("install initial plugin: %v", err)
}
originalDigest := ManifestCapabilityDigest(rec1.Manifest)
if _, err := svc.approvalGrant(rec1.InstallationID, "ws-1", 1, originalDigest, []string{"host.v2.read:tasks", "host.v2.read:messages"}, "human", "grant", "audit-1"); err != nil {
t.Fatalf("grant initial approval: %v", err)
}
rec2, err := svc.Install(context.Background(), testPackageWithAPIRead(t, "kandev-plugin-slack", "1.1.0", "tasks"))
if err != nil {
t.Fatalf("upgrade plugin: %v", err)
}
if rec2.InstallationID != rec1.InstallationID {
t.Fatalf("installation id changed on upgrade: %q -> %q", rec1.InstallationID, rec2.InstallationID)
}
row, ok, err := svc.approvalCurrent(rec1.InstallationID, "ws-1")
if err != nil || !ok {
t.Fatalf("approvalCurrent after upgrade: ok=%v err=%v", ok, err)
}
if row.Revision != 2 {
t.Fatalf("revision after upgrade = %d, want 2", row.Revision)
}
if row.ManifestDigest != ManifestCapabilityDigest(rec2.Manifest) {
t.Fatalf("manifest digest after upgrade = %q, want %q", row.ManifestDigest, ManifestCapabilityDigest(rec2.Manifest))
}
if got, want := row.CapabilityIDs, []string{"host.v2.read:tasks"}; !equalStrings(got, want) {
t.Fatalf("capabilities after narrowing upgrade = %#v, want %#v", got, want)
}
if _, err := svc.approvalGrant(rec1.InstallationID, "ws-1", 3, row.ManifestDigest, []string{"host.v2.read:tasks"}, "human", "renew", "audit-2"); err != nil {
t.Fatalf("renew approval: %v", err)
}
rec3, err := svc.Install(context.Background(), testPackageWithAPIRead(t, "kandev-plugin-slack", "1.2.0", "tasks", "messages"))
if err != nil {
t.Fatalf("rollback-equivalent install: %v", err)
}
if rec3.InstallationID != rec1.InstallationID {
t.Fatalf("installation id changed on rollback-equivalent replacement: %q -> %q", rec1.InstallationID, rec3.InstallationID)
}
row, ok, err = svc.approvalCurrent(rec1.InstallationID, "ws-1")
if err != nil || !ok {
t.Fatalf("approvalCurrent after rollback-equivalent replacement: ok=%v err=%v", ok, err)
}
if row.Revision != 4 {
t.Fatalf("revision after rollback-equivalent replacement = %d, want 4", row.Revision)
}
if got, want := row.CapabilityIDs, []string{"host.v2.read:tasks"}; !equalStrings(got, want) {
t.Fatalf("capabilities after rollback-equivalent replacement = %#v, want %#v", got, want)
}
file, err := svc.approvalLedger().load()
if err != nil {
t.Fatalf("load approvals: %v", err)
}
if len(file.Events) != 4 || file.Events[1].Type != CapabilityApprovalEventUpgradeReview ||
file.Events[1].BeforeDigest != originalDigest || file.Events[1].AfterDigest != ManifestCapabilityDigest(rec2.Manifest) ||
file.Events[2].Type != CapabilityApprovalEventGrant || file.Events[3].Type != CapabilityApprovalEventUpgradeReview ||
file.Events[3].BeforeDigest != ManifestCapabilityDigest(rec2.Manifest) || file.Events[3].AfterDigest != ManifestCapabilityDigest(rec3.Manifest) {
t.Fatalf("approval events after upgrade = %#v", file.Events)
}
}
func TestServiceInstallUpgradeReviewFailureRestartsPreviousRuntime(t *testing.T) {
svc, _, rt := newTestService(t)
rec1, err := svc.Install(context.Background(), testPackageWithAPIRead(t, "kandev-plugin-slack", "1.0.0", "tasks"))
if err != nil {
t.Fatalf("install initial plugin: %v", err)
}
if _, err := svc.approvalGrant(rec1.InstallationID, "ws-1", 1, ManifestCapabilityDigest(rec1.Manifest), []string{"host.v2.read:tasks"}, "human", "grant", "audit-1"); err != nil {
t.Fatalf("grant approval: %v", err)
}
// An empty manifest resource is accepted by package extraction but is
// rejected while deriving the exact approval capability set. This fails
// after the old runtime has been stopped and the new record persisted.
_, err = svc.Install(context.Background(), testPackageWithAPIRead(t, "kandev-plugin-slack", "1.1.0", "*"))
if err == nil {
t.Fatal("upgrade with invalid capability expected an approval review error")
}
if !rt.Running("kandev-plugin-slack") {
t.Fatal("previous runtime was not restarted after approval review failure")
}
if got := rt.startCallCount("kandev-plugin-slack"); got != 2 {
t.Fatalf("runtime Start called %d times, want initial start plus rollback restart", got)
}
onDisk, err := svc.Get("kandev-plugin-slack")
if err != nil {
t.Fatalf("get restored plugin: %v", err)
}
if onDisk.Version != "1.0.0" {
t.Fatalf("restored plugin version = %q, want 1.0.0", onDisk.Version)
}
}
func TestServiceInstallUpgradeReviewRollbackFailureKeepsNewRecordCoherent(t *testing.T) {
dir := t.TempDir()
fsStore := store.NewFSStore(dir)
failing := &failMatchingSaveStore{
Store: fsStore,
match: func(rec *store.Record) bool { return rec.Version == "1.0.0" },
failErr: errors.New("simulated compensating save failure"),
}
svc := NewService(failing, NewRegistry(), nil, testLogger(t))
if err := svc.SetPluginsDir(dir); err != nil {
t.Fatalf("SetPluginsDir: %v", err)
}
svc.SetRuntime(newFakeRuntime())
t.Cleanup(func() { _ = svc.Close() })
if _, err := svc.Install(context.Background(), testPackageWithAPIRead(t, "kandev-plugin-slack", "1.0.0", "tasks")); err != nil {
t.Fatalf("install initial plugin: %v", err)
}
failing.arm()
_, err := svc.Install(context.Background(), testPackageWithAPIRead(t, "kandev-plugin-slack", "1.1.0", "*"))
if err == nil || !strings.Contains(err.Error(), "compensating save failure") {
t.Fatalf("upgrade error = %v, want compensating save failure", err)
}
onDisk, getErr := fsStore.Get("kandev-plugin-slack")
if getErr != nil {
t.Fatalf("store.Get(): %v", getErr)
}
if onDisk.Version != "1.1.0" {
t.Fatalf("persisted version = %q, want retained new version 1.1.0", onDisk.Version)
}
if _, statErr := os.Stat(onDisk.InstallPath); statErr != nil {
t.Fatalf("retained new install path missing: %v", statErr)
}
current, getErr := svc.Get("kandev-plugin-slack")
if getErr != nil || current.Version != "1.1.0" {
t.Fatalf("registry record = %#v, err=%v; want coherent new record", current, getErr)
}
}
func TestServiceInstallEquivalentUpgradeBumpsApprovalRevision(t *testing.T) {
svc, _, _ := newTestService(t)
rec1, err := svc.Install(context.Background(), testPackageWithAPIRead(t, "kandev-plugin-slack", "1.0.0", "tasks"))
if err != nil {
t.Fatalf("install initial plugin: %v", err)
}
digest := ManifestCapabilityDigest(rec1.Manifest)
if _, err := svc.approvalGrant(rec1.InstallationID, "ws-1", 1, digest, []string{"host.v2.read:tasks"}, "human", "grant", "audit-1"); err != nil {
t.Fatalf("grant: %v", err)
}
if _, err := svc.Install(context.Background(), testPackageWithAPIRead(t, "kandev-plugin-slack", "1.1.0", "tasks")); err != nil {
t.Fatalf("equivalent upgrade: %v", err)
}
row, ok, err := svc.approvalCurrent(rec1.InstallationID, "ws-1")
if err != nil || !ok {
t.Fatalf("approvalCurrent: ok=%v err=%v", ok, err)
}
if row.Revision != 2 {
t.Fatalf("revision = %d, want 2", row.Revision)
}
file, err := svc.approvalLedger().load()
if err != nil {
t.Fatalf("load approvals: %v", err)
}
if len(file.Events) != 2 || file.Events[1].Type != CapabilityApprovalEventUpgradeReview {
t.Fatalf("events = %#v, want grant and upgrade review", file.Events)
}
}
// failingSaveStore wraps a real store.Store, letting a test arm exactly one
// Save call to fail with a simulated error — used to exercise Install's
// cleanup-on-persist-failure path without a real disk-full/permission
// simulation.
type failingSaveStore struct {
store.Store
mu sync.Mutex
failNext bool
}
type failMatchingSaveStore struct {
store.Store
mu sync.Mutex
armed bool
match func(*store.Record) bool
failErr error
}
func (s *failMatchingSaveStore) Save(rec *store.Record) error {
s.mu.Lock()
fail := s.armed && s.match(rec)
if fail {
s.armed = false
}
s.mu.Unlock()
if fail {
return s.failErr
}
return s.Store.Save(rec)
}
func (s *failMatchingSaveStore) arm() {
s.mu.Lock()
s.armed = true
s.mu.Unlock()
}
func (s *failingSaveStore) Save(rec *store.Record) error {
s.mu.Lock()
fail := s.failNext
s.failNext = false
s.mu.Unlock()
if fail {
return errors.New("simulated save failure")
}
return s.Store.Save(rec)
}
func (s *failingSaveStore) armFailNextSave() {
s.mu.Lock()
s.failNext = true
s.mu.Unlock()
}
// TestServiceInstallUpgradeSaveFailurePreservesOldVersionAndData pins the
// fix for a failed upgrade destroying every installed version plus the
// plugin's writable data directory: cleanup on a store.Save failure must
// remove only the freshly extracted version directory, and must restart the
// previous version's process (already stopped to make way for the new
// spawn) rather than leaving the plugin down.
func TestServiceInstallUpgradeSaveFailurePreservesOldVersionAndData(t *testing.T) {
dir := t.TempDir()
fsStore := store.NewFSStore(dir)
failing := &failingSaveStore{Store: fsStore}
reg := NewRegistry()
svc := NewService(failing, reg, nil, testLogger(t))
svc.SetPluginsDir(dir)
rt := newFakeRuntime()
svc.SetRuntime(rt)
t.Cleanup(func() { _ = svc.Close() })
rec1 := installTestPlugin(t, svc, "kandev-plugin-slack") // v1.0.0, active + running
if !rt.Running("kandev-plugin-slack") {
t.Fatal("sanity check: plugin not running after first install")
}
// Simulate the old version's writable data directory holding user data.
dataDir := filepath.Join(dir, "kandev-plugin-slack", "data")
if err := os.MkdirAll(dataDir, 0o755); err != nil {
t.Fatalf("MkdirAll(dataDir): %v", err)
}
marker := filepath.Join(dataDir, "marker.txt")
if err := os.WriteFile(marker, []byte("keep me"), 0o644); err != nil {
t.Fatalf("WriteFile(marker): %v", err)
}
failing.armFailNextSave()
_, err := svc.Install(context.Background(), testPackage(t, "kandev-plugin-slack", "1.1.0", false))
if err == nil {
t.Fatal("Install() expected an error from the simulated Save failure")
}
if _, statErr := os.Stat(rec1.InstallPath); statErr != nil {
t.Fatalf("old version dir %q was removed by cleanup: %v", rec1.InstallPath, statErr)
}
if _, statErr := os.Stat(marker); statErr != nil {
t.Fatalf("plugin data dir marker was removed by cleanup: %v", statErr)
}
newVersionDir := filepath.Join(dir, "kandev-plugin-slack", "1.1.0")
if _, statErr := os.Stat(newVersionDir); !os.IsNotExist(statErr) {
t.Fatalf("new version dir %q still exists after the Save failure: stat err = %v", newVersionDir, statErr)
}
if !rt.Running("kandev-plugin-slack") {
t.Fatal("the previous version's process was not restarted after the failed upgrade")
}
onDisk, getErr := fsStore.Get("kandev-plugin-slack")
if getErr != nil {
t.Fatalf("store.Get(): %v", getErr)
}
if onDisk.Version != "1.0.0" {
t.Fatalf("store record Version = %q, want %q (the old version's record must survive unchanged)", onDisk.Version, "1.0.0")
}
}
// minKandevVersionPackage builds a valid, runtime-managed plugin tar.gz
// declaring min_kandev_version, for TestServiceInstall_MinKandevVersion*.
func minKandevVersionPackage(t *testing.T, id, version, minKandevVersion string) *bytes.Buffer {
t.Helper()
platformKey := goruntime.GOOS + "-" + goruntime.GOARCH
manifestYAML := fmt.Sprintf(`
id: %s
api_version: 1
version: %s
display_name: Test Plugin
min_kandev_version: %q
runtime:
type: binary
executables:
%s: server/plugin
`, id, version, minKandevVersion, platformKey)
var buf bytes.Buffer
files := map[string][]byte{
"manifest.yaml": []byte(manifestYAML),
"server/plugin": []byte("#!/bin/sh\necho fake\n"),
}
if err := pkgtartest.WritePackage(&buf, files); err != nil {
t.Fatalf("WritePackage: %v", err)
}
return &buf
}
// TestServiceInstall_RejectsPackageNewerThanRunningKandevVersion pins
// enforcement of manifest.min_kandev_version: once a running kandev version
// is wired via SetKandevVersion, a package that requires a newer kandev
// must be rejected (and its extracted package directory cleaned up) rather
// than installed and left to fail confusingly at spawn time.
func TestServiceInstall_RejectsPackageNewerThanRunningKandevVersion(t *testing.T) {
svc, dir, _, _ := newTestServiceWithDir(t)
svc.SetKandevVersion("1.0.0")
_, err := svc.Install(context.Background(), minKandevVersionPackage(t, "kandev-plugin-slack", "1.0.0", "2.0.0"))
if err == nil {
t.Fatal("Install() expected an error for a package requiring a newer kandev version")
}
if _, statErr := os.Stat(filepath.Join(dir, "kandev-plugin-slack", "1.0.0")); !os.IsNotExist(statErr) {
t.Fatalf("Install() left the extracted package on disk after rejecting it: stat err = %v", statErr)
}
if _, getErr := svc.Get("kandev-plugin-slack"); getErr == nil {
t.Fatal("Install() registered a record for a package it rejected on min_kandev_version")
}
}
// TestServiceInstall_AllowsPackageAtOrBelowRunningKandevVersion pins the
// non-blocking side of the same check.
func TestServiceInstall_AllowsPackageAtOrBelowRunningKandevVersion(t *testing.T) {
svc, _, _ := newTestService(t)
svc.SetKandevVersion("2.0.0")
rec, err := svc.Install(context.Background(), minKandevVersionPackage(t, "kandev-plugin-slack", "1.0.0", "1.5.0"))
if err != nil {
t.Fatalf("Install() unexpected error: %v", err)
}
if rec.Status != StatusActive {
t.Fatalf("Install() Status = %q, want %q", rec.Status, StatusActive)
}
}
func TestServiceInstall_NormalizesReleaseTagBeforeEnforcingMinKandevVersion(t *testing.T) {
svc, _, _ := newTestService(t)
svc.SetKandevVersion("v1.0.0")
_, err := svc.Install(context.Background(), minKandevVersionPackage(t, "kandev-plugin-slack", "1.0.0", "2.0.0"))
if err == nil {
t.Fatal("Install() expected a tagged running version below min_kandev_version to be rejected")
}
}
func TestServiceInstall_SkipsMinKandevVersionForNonReleaseBuildVersion(t *testing.T) {
for _, version := range []string{"v1.2.3-4-gabcdef"} {
t.Run(version, func(t *testing.T) {
svc, _, _ := newTestService(t)
svc.SetKandevVersion(version)
rec, err := svc.Install(context.Background(), minKandevVersionPackage(t, "kandev-plugin-slack", "1.0.0", "999.0.0"))
if err != nil {
t.Fatalf("Install() with non-release build version %q: %v", version, err)
}
if rec.Status != StatusActive {
t.Fatalf("Install() Status = %q, want %q", rec.Status, StatusActive)
}
})
}
}
// TestServiceInstall_NoEnforcementOnDevBuild pins the dev-build escape
// hatch. An un-stamped local build reports the "dev" sentinel, and a
// developer running from source must still be able to install any package —
// exactly as before this check was wired up in production.
//
// The escape hatch is explicit because "dev" is not a release boundary. It
// must skip enforcement regardless of whether the manifest minimum is bare
// or carries the leading `v` accepted for release values.
func TestServiceInstall_NoEnforcementOnDevBuild(t *testing.T) {
for _, minVersion := range []string{"999.0.0", "v1.0.0"} {
t.Run(minVersion, func(t *testing.T) {
svc, _, _ := newTestService(t)
svc.SetKandevVersion(DevKandevVersion)
rec, err := svc.Install(context.Background(), minKandevVersionPackage(t, "kandev-plugin-slack", "1.0.0", minVersion))
if err != nil {
t.Fatalf("Install() unexpected error on a dev build (min_kandev_version %q): %v", minVersion, err)
}
if rec.Status != StatusActive {
t.Fatalf("Install() Status = %q, want %q", rec.Status, StatusActive)
}
})
}
}
// TestServiceInstall_NoEnforcementWithoutKandevVersionWired pins the
// no-op default: a Service that never called SetKandevVersion (e.g. every
// other test in this file, and any caller that hasn't wired it yet) must
// keep installing packages that declare min_kandev_version exactly as
// before.
func TestServiceInstall_NoEnforcementWithoutKandevVersionWired(t *testing.T) {
svc, _, _ := newTestService(t)
rec, err := svc.Install(context.Background(), minKandevVersionPackage(t, "kandev-plugin-slack", "1.0.0", "999.0.0"))
if err != nil {
t.Fatalf("Install() unexpected error: %v", err)
}
if rec.Status != StatusActive {
t.Fatalf("Install() Status = %q, want %q", rec.Status, StatusActive)
}
}
func TestValidateInstallURL_AcceptsHTTPAndHTTPS(t *testing.T) {
for _, raw := range []string{
"https://example.com/plugin.tar.gz",
"http://example.com/plugin.tar.gz",
} {
if err := validateInstallURL(raw); err != nil {
t.Fatalf("validateInstallURL(%q) unexpected error: %v", raw, err)
}
}
}
func TestValidateInstallURL_RejectsNonHTTPScheme(t *testing.T) {
for _, raw := range []string{
"file:///etc/passwd",
"gopher://example.com/plugin",
"ftp://example.com/plugin.tar.gz",
} {
if err := validateInstallURL(raw); err == nil {
t.Fatalf("validateInstallURL(%q) expected error, got nil", raw)
}
}
}
func TestValidateInstallURL_RejectsEmptyHost(t *testing.T) {
if err := validateInstallURL("https:///plugin.tar.gz"); err == nil {
t.Fatal("validateInstallURL() expected error for empty host, got nil")
}
}
func TestValidateInstallURL_RejectsMalformedURL(t *testing.T) {
if err := validateInstallURL("://not-a-url"); err == nil {
t.Fatal("validateInstallURL() expected error for malformed URL, got nil")
}
}
func TestServiceInstallFromURL_RejectsNonHTTPSchemeBeforeAnyRequest(t *testing.T) {
svc, _, _ := newTestService(t)
_, err := svc.InstallFromURL(context.Background(), "file:///etc/passwd")
if err == nil {
t.Fatal("InstallFromURL() expected error for file:// scheme, got nil")
}
}
func testPackageWithAPIRead(t *testing.T, id, version string, resources ...string) *bytes.Buffer {
t.Helper()
platformKey := goruntime.GOOS + "-" + goruntime.GOARCH
quotedResources := make([]string, 0, len(resources))
for _, resource := range resources {
quotedResources = append(quotedResources, fmt.Sprintf("%q", resource))
}
minimumVersion := ""
for _, resource := range resources {
if resource == "messages" {
minimumVersion = fmt.Sprintf("min_kandev_version: %q\n", manifest.MinimumMessagesCapabilityVersion)
break
}
}
manifestYAML := fmt.Sprintf(`
id: %s
api_version: 1
version: %s
display_name: Test Plugin
%s
capabilities:
api_read: [%s]
runtime:
type: binary
executables:
%s: server/plugin
`, id, version, minimumVersion, strings.Join(quotedResources, ", "), platformKey)
var buf bytes.Buffer
if err := pkgtartest.WritePackage(&buf, map[string][]byte{
"manifest.yaml": []byte(manifestYAML),
"server/plugin": []byte("#!/bin/sh\necho fake\n"),
}); err != nil {
t.Fatalf("WritePackage: %v", err)
}
return &buf
}
// messagesMinKandevVersionPackage is like minKandevVersionPackage but
// declares capabilities.api_read: messages, which subjects the manifest to
// the capability minimum floor (MinimumMessagesCapabilityVersion).
func messagesMinKandevVersionPackage(t *testing.T, id, version, minKandevVersion string) *bytes.Buffer {
t.Helper()
platformKey := goruntime.GOOS + "-" + goruntime.GOARCH
manifestYAML := fmt.Sprintf(`
id: %s
api_version: 1
version: %s
display_name: Test Plugin
min_kandev_version: %q
capabilities:
api_read:
- messages
runtime:
type: binary
executables:
%s: server/plugin
`, id, version, minKandevVersion, platformKey)
var buf bytes.Buffer
files := map[string][]byte{
"manifest.yaml": []byte(manifestYAML),
"server/plugin": []byte("#!/bin/sh\necho fake\n"),
}
if err := pkgtartest.WritePackage(&buf, files); err != nil {
t.Fatalf("WritePackage: %v", err)
}
return &buf
}
// TestServiceInstall_EnforcesMessagesCapabilityFloorOnDevAndUnwiredBuilds
// pins the design contract that the api_read:messages minimum (0.91.1) is a
// manifest-level floor enforced in every build - including unstamped dev
// builds where the newer-than-running release gate is intentionally a no-op.
// The dev escape hatch applies only to the release-boundary comparison, never
// to the capability floor.
func TestServiceInstall_EnforcesMessagesCapabilityFloorOnDevAndUnwiredBuilds(t *testing.T) {
for _, tc := range []struct {
name string
want string
}{
{name: "below floor is rejected", want: "0.9.0"},
{name: "malformed minimum is rejected", want: "not-a-release"},
} {
t.Run("dev/"+tc.name, func(t *testing.T) {
svc, _, _ := newTestService(t)
svc.SetKandevVersion(DevKandevVersion)
if _, err := svc.Install(context.Background(), messagesMinKandevVersionPackage(t, "kandev-plugin-messages", "1.0.0", tc.want)); err == nil {
t.Fatalf("Install() on a dev build accepted a messages manifest with min %q", tc.want)
}
})
t.Run("unwired/"+tc.name, func(t *testing.T) {
svc, _, _ := newTestService(t)
if _, err := svc.Install(context.Background(), messagesMinKandevVersionPackage(t, "kandev-plugin-messages", "1.0.0", tc.want)); err == nil {
t.Fatalf("Install() with no running version accepted a messages manifest with min %q", tc.want)
}
})
}
t.Run("dev/floor allowed", func(t *testing.T) {
svc, _, _ := newTestService(t)
svc.SetKandevVersion(DevKandevVersion)
rec, err := svc.Install(context.Background(), messagesMinKandevVersionPackage(t, "kandev-plugin-messages", "1.0.0", "0.91.1"))
if err != nil {
t.Fatalf("Install() on a dev build rejected the floor minimum: %v", err)
}
if rec.Status != StatusActive {
t.Fatalf("Install() Status = %q, want %q", rec.Status, StatusActive)
}
})
}