test(addons): move the remaining add-on tests to Ginkgo

The built-in catalog, tool arguments, loopback carrier, host operation
contract, packaging, application, config, CLI and endpoint tests used
the testing package calls that forbidigo rejects. They now run as Ginkgo
specs. The endpoint tests also drop the unchecked Close errors that
errcheck reports when the auth tag is on.

Assisted-by: Claude Code:claude-sonnet-5-5 go
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto committed 2026-10-21 02:20:12 +00:00
1 parent 50a0b5c8cd
commit c24de4fc21
23 files changed
+686 -749

No files matched your search

+45 -55
View File
@@ -10,13 +10,14 @@ import (
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"time"
"github.com/mudler/LocalAI/core/http/auth"
"github.com/mudler/LocalAI/core/schema"
"github.com/mudler/LocalAI/core/services/addons/hostops"
"github.com/mudler/LocalAI/pkg/addons/contract"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
type recordingExporter struct {
@@ -34,62 +35,51 @@ func (r *recordingExporter) ExportModel(context.Context, string, string, schema.
// With an account database the role decides in every mode, so the local-owner
// mode does not make a non-administrator an administrator.
func TestTheAdminCheckWithAnAccountDatabaseFollowsTheRoleInEveryMode(t *testing.T) {
db, err := auth.InitDB(filepath.Join(t.TempDir(), "auth.db"))
if err != nil {
t.Fatal(err)
}
mk := func(id, role string) {
if err := db.Create(&auth.User{ID: id, Email: id + "@example.com", Role: role, Status: auth.StatusActive}).Error; err != nil {
t.Fatal(err)
var _ = Describe("the admin check with an account database", func() {
It("follows the role in every mode", func() {
db, err := auth.InitDB(filepath.Join(GinkgoT().TempDir(), "auth.db"))
Expect(err).ToNot(HaveOccurred())
mk := func(id, role string) {
Expect(db.Create(&auth.User{ID: id, Email: id + "@example.com", Role: role, Status: auth.StatusActive}).Error).To(Succeed())
}
}
mk("admin", auth.RoleAdmin)
mk("granted", auth.RoleUser)
if err := auth.UpdateUserPermissions(db, "granted", auth.PermissionMap{"fine_tuning": true}); err != nil {
t.Fatal(err)
}
mk("admin", auth.RoleAdmin)
mk("granted", auth.RoleUser)
Expect(auth.UpdateUserPermissions(db, "granted", auth.PermissionMap{"fine_tuning": true})).To(Succeed())
check := addonAdminCheck(db)
if check(contract.Actor{UserID: "granted", Mode: "local-owner"}) {
t.Fatal("a non-administrator is an administrator in local-owner mode with an account database")
}
if !check(contract.Actor{UserID: "admin", Mode: "local-owner"}) {
t.Fatal("an administrator is refused in local-owner mode")
}
check := addonAdminCheck(db)
Expect(check(contract.Actor{UserID: "granted", Mode: "local-owner"})).To(BeFalse(),
"a non-administrator is an administrator in local-owner mode with an account database")
Expect(check(contract.Actor{UserID: "admin", Mode: "local-owner"})).To(BeTrue(),
"an administrator is refused in local-owner mode")
in := hostops.NewInFlight()
b := hostops.New(in, addonFeatureCheck(db), contract.DefaultLimits())
b.SetAdminCheck(check)
ex := &recordingExporter{}
if err := hostops.RegisterEngineModels(b, ex, nil); err != nil {
t.Fatal(err)
}
in := hostops.NewInFlight()
b := hostops.New(in, addonFeatureCheck(db), contract.DefaultLimits())
b.SetAdminCheck(check)
ex := &recordingExporter{}
Expect(hostops.RegisterEngineModels(b, ex, nil)).To(Succeed())
serve := func(user string) (int, contract.HostResponse) {
end := in.Begin(contract.Envelope{
Request: contract.Request{AddonID: "fine-tuning", OperationID: "op1", Generation: 1, Action: "start", Arguments: json.RawMessage(`{}`)},
Authority: contract.Authority{Actor: contract.Actor{UserID: user, CredentialID: "c1", Mode: "local-owner"}, Scope: "apply", ExpiresAt: time.Now().Add(time.Minute)},
})
defer end()
body, _ := json.Marshal(contract.HostRequest{OperationID: "op1", Op: "finetune.export", Arguments: json.RawMessage(`{"jobId":"j1"}`)})
req := httptest.NewRequest(http.MethodPost, "/host", bytes.NewReader(body))
req.Header.Set(contract.HostCredentialHeader, "secret")
rec := httptest.NewRecorder()
b.Handler("fine-tuning", "secret", []string{"finetune."}).ServeHTTP(rec, req)
var out contract.HostResponse
_ = json.Unmarshal(rec.Body.Bytes(), &out)
return rec.Code, out
}
serve := func(user string) (int, contract.HostResponse) {
end := in.Begin(contract.Envelope{
Request: contract.Request{AddonID: "fine-tuning", OperationID: "op1", Generation: 1, Action: "start", Arguments: json.RawMessage(`{}`)},
Authority: contract.Authority{Actor: contract.Actor{UserID: user, CredentialID: "c1", Mode: "local-owner"}, Scope: "apply", ExpiresAt: time.Now().Add(time.Minute)},
})
defer end()
body, _ := json.Marshal(contract.HostRequest{OperationID: "op1", Op: "finetune.export", Arguments: json.RawMessage(`{"jobId":"j1"}`)})
req := httptest.NewRequest(http.MethodPost, "/host", bytes.NewReader(body))
req.Header.Set(contract.HostCredentialHeader, "secret")
rec := httptest.NewRecorder()
b.Handler("fine-tuning", "secret", []string{"finetune."}).ServeHTTP(rec, req)
var out contract.HostResponse
_ = json.Unmarshal(rec.Body.Bytes(), &out)
return rec.Code, out
}
code, out := serve("granted")
if code != http.StatusForbidden || out.Error == nil || out.Error.Code != contract.HostFeature {
t.Fatalf("feature-holding non-admin: %d %+v", code, out.Error)
}
if ex.calls != 0 {
t.Fatalf("service called %d times for a non-administrator", ex.calls)
}
if code, out := serve("admin"); code != http.StatusOK {
t.Fatalf("administrator: %d %+v", code, out.Error)
}
}
code, out := serve("granted")
Expect(code).To(Equal(http.StatusForbidden))
Expect(out.Error).ToNot(BeNil())
Expect(out.Error.Code).To(Equal(contract.HostFeature))
Expect(ex.calls).To(BeZero(), "service called for a non-administrator")
code, out = serve("admin")
Expect(code).To(Equal(http.StatusOK), "administrator: %+v", out.Error)
})
})
+11 -11
View File
@@ -2,19 +2,19 @@
package application
import (
"testing"
"github.com/mudler/LocalAI/pkg/addons/contract"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
// Without an account database the local owner is the administrator, and no
// other actor is.
func TestTheAdminCheckWithoutAnAccountDatabaseTrustsOnlyTheLocalOwner(t *testing.T) {
check := addonAdminCheck(nil)
if !check(contract.Actor{UserID: "local-owner", Mode: "local-owner"}) {
t.Fatal("the local owner is not the administrator without an account database")
}
if check(contract.Actor{UserID: "u1", Mode: "named-user"}) {
t.Fatal("a named user is an administrator without an account database")
}
}
var _ = Describe("the admin check without an account database", func() {
It("trusts only the local owner", func() {
check := addonAdminCheck(nil)
Expect(check(contract.Actor{UserID: "local-owner", Mode: "local-owner"})).To(BeTrue(),
"the local owner is not the administrator without an account database")
Expect(check(contract.Actor{UserID: "u1", Mode: "named-user"})).To(BeFalse(),
"a named user is an administrator without an account database")
})
})
+16 -19
View File
@@ -3,11 +3,12 @@ package application
import (
"slices"
"testing"
"github.com/mudler/LocalAI/core/services/addons/hostops"
addonruntime "github.com/mudler/LocalAI/core/services/addons/runtime"
"github.com/mudler/LocalAI/pkg/addons/contract"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
// Embedding the interfaces satisfies them; registration never calls a method.
@@ -15,23 +16,19 @@ type noFineTune struct{ hostops.FineTuner }
type noQuant struct{ hostops.Quantizer }
type noBackends struct{ hostops.BackendLister }
func TestRegisterAddonEngineJobsWithoutABrokerRegistersNothing(t *testing.T) {
a := &Application{}
if err := a.RegisterAddonEngineJobs(noFineTune{}, noQuant{}, noBackends{}); err != nil {
t.Fatal(err)
}
}
var _ = Describe("RegisterAddonEngineJobs", func() {
It("registers nothing without a broker", func() {
a := &Application{}
Expect(a.RegisterAddonEngineJobs(noFineTune{}, noQuant{}, noBackends{})).To(Succeed())
})
func TestRegisterAddonEngineJobsRegistersThroughTheBroker(t *testing.T) {
b := hostops.New(hostops.NewInFlight(), nil, contract.DefaultLimits())
a := &Application{addonHostOps: &addonruntime.HostOps{Broker: b}}
if err := a.RegisterAddonEngineJobs(noFineTune{}, noQuant{}, noBackends{}); err != nil {
t.Fatal(err)
}
ops := b.Ops()
for _, want := range []string{"finetune.start", "finetune.get", "finetune.list", "finetune.checkpoints", "quantize.start", "quantize.get", "quantize.list"} {
if !slices.Contains(ops, want) {
t.Fatalf("%s is not registered: %v", want, ops)
It("registers through the broker", func() {
b := hostops.New(hostops.NewInFlight(), nil, contract.DefaultLimits())
a := &Application{addonHostOps: &addonruntime.HostOps{Broker: b}}
Expect(a.RegisterAddonEngineJobs(noFineTune{}, noQuant{}, noBackends{})).To(Succeed())
ops := b.Ops()
for _, want := range []string{"finetune.start", "finetune.get", "finetune.list", "finetune.checkpoints", "quantize.start", "quantize.get", "quantize.list"} {
Expect(slices.Contains(ops, want)).To(BeTrue(), "%s is not registered: %v", want, ops)
}
}
}
})
})
+19 -26
View File
@@ -5,35 +5,28 @@ package application
import (
"path/filepath"
"testing"
"github.com/mudler/LocalAI/core/http/auth"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestTheFeatureCheckFollowsTheAccountPermissions(t *testing.T) {
db, err := auth.InitDB(filepath.Join(t.TempDir(), "auth.db"))
if err != nil {
t.Fatal(err)
}
mk := func(id, role, status string) {
if err := db.Create(&auth.User{ID: id, Email: id + "@example.com", Role: role, Status: status}).Error; err != nil {
t.Fatal(err)
var _ = Describe("the feature check", func() {
It("follows the account permissions", func() {
db, err := auth.InitDB(filepath.Join(GinkgoT().TempDir(), "auth.db"))
Expect(err).ToNot(HaveOccurred())
mk := func(id, role, status string) {
Expect(db.Create(&auth.User{ID: id, Email: id + "@example.com", Role: role, Status: status}).Error).To(Succeed())
}
}
mk("admin", auth.RoleAdmin, auth.StatusActive)
mk("plain", auth.RoleUser, auth.StatusActive)
mk("granted", auth.RoleUser, auth.StatusActive)
mk("gone", auth.RoleAdmin, "disabled")
if err := auth.UpdateUserPermissions(db, "granted", auth.PermissionMap{"fine_tuning": true}); err != nil {
t.Fatal(err)
}
if err := auth.UpdateUserPermissions(db, "plain", auth.PermissionMap{"fine_tuning": false}); err != nil {
t.Fatal(err)
}
has := addonFeatureCheck(db)
for user, want := range map[string]bool{"admin": true, "granted": true, "plain": false, "gone": false, "nobody": false, "": false} {
if got := has(user, "fine_tuning"); got != want {
t.Errorf("%q: got %v want %v", user, got, want)
mk("admin", auth.RoleAdmin, auth.StatusActive)
mk("plain", auth.RoleUser, auth.StatusActive)
mk("granted", auth.RoleUser, auth.StatusActive)
mk("gone", auth.RoleAdmin, "disabled")
Expect(auth.UpdateUserPermissions(db, "granted", auth.PermissionMap{"fine_tuning": true})).To(Succeed())
Expect(auth.UpdateUserPermissions(db, "plain", auth.PermissionMap{"fine_tuning": false})).To(Succeed())
has := addonFeatureCheck(db)
for user, want := range map[string]bool{"admin": true, "granted": true, "plain": false, "gone": false, "nobody": false, "": false} {
Expect(has(user, "fine_tuning")).To(Equal(want), "user %q", user)
}
}
}
})
})
+18 -24
View File
@@ -2,34 +2,28 @@
package application
import (
"testing"
"github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/pkg/addons/contract"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestAFrontendWithoutTheAddonProcessOffersNoHostOperations(t *testing.T) {
named := map[string][]string{"fine-tuning": {"finetune."}}
a := &Application{applicationConfig: &config.ApplicationConfig{AddonHostOps: named}}
if a.newAddonHostOps(nil, contract.DefaultLimits()) != nil {
t.Fatal("a process without the add-on manager built a broker")
}
if a.AddonHostOps() != nil {
t.Fatal("a process that never started the supervisor offers host operations")
}
}
var _ = Describe("add-on host operations", func() {
It("are not offered by a frontend without the add-on process", func() {
named := map[string][]string{"fine-tuning": {"finetune."}}
a := &Application{applicationConfig: &config.ApplicationConfig{AddonHostOps: named}}
Expect(a.newAddonHostOps(nil, contract.DefaultLimits())).To(BeNil(), "a process without the add-on manager built a broker")
Expect(a.AddonHostOps()).To(BeNil(), "a process that never started the supervisor offers host operations")
})
func TestAHostThatNamesNoAddonOffersNoHostOperations(t *testing.T) {
for _, ops := range []map[string][]string{nil, {}} {
a := &Application{applicationConfig: &config.ApplicationConfig{AddonHostOps: ops}}
if a.newAddonHostOps(nil, contract.DefaultLimits()) != nil {
t.Fatalf("%v built a broker", ops)
It("are not offered by a host that names no add-on", func() {
for _, ops := range []map[string][]string{nil, {}} {
a := &Application{applicationConfig: &config.ApplicationConfig{AddonHostOps: ops}}
Expect(a.newAddonHostOps(nil, contract.DefaultLimits())).To(BeNil(), "%v built a broker", ops)
}
}
}
})
func TestTheFeatureCheckRefusesWithoutAnAccountDatabase(t *testing.T) {
if addonFeatureCheck(nil)("u1", "fine_tuning") {
t.Fatal("no database must mean no feature")
}
}
It("refuses the feature check without an account database", func() {
Expect(addonFeatureCheck(nil)("u1", "fine_tuning")).To(BeFalse(), "no database must mean no feature")
})
})
@@ -5,33 +5,30 @@ import (
"bytes"
"log/slog"
"path/filepath"
"testing"
"github.com/mudler/xlog"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func capture(t *testing.T) *bytes.Buffer {
func capture() *bytes.Buffer {
var buf bytes.Buffer
xlog.SetLogger(xlog.NewLoggerWithHandler(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug}), xlog.LogLevelDebug))
return &buf
}
func TestMemoryOnlyPinsLogOneWarning(t *testing.T) {
buf := capture(t)
if _, err := pinStoreFor(nil, ""); err != nil {
t.Fatal(err)
}
if got := bytes.Count(buf.Bytes(), []byte("memory only")); got != 1 {
t.Fatalf("want one memory-only warning, got %d in %q", got, buf.String())
}
}
var _ = Describe("pinStoreFor", func() {
It("logs one warning for memory-only pins", func() {
buf := capture()
_, err := pinStoreFor(nil, "")
Expect(err).ToNot(HaveOccurred())
Expect(bytes.Count(buf.Bytes(), []byte("memory only"))).To(Equal(1), "log: %q", buf.String())
})
func TestPinsFileDoesNotWarn(t *testing.T) {
buf := capture(t)
if _, err := pinStoreFor(nil, filepath.Join(t.TempDir(), "addon-pins.json")); err != nil {
t.Fatal(err)
}
if buf.Len() != 0 {
t.Fatalf("unexpected log: %q", buf.String())
}
}
It("does not warn with a pins file", func() {
buf := capture()
_, err := pinStoreFor(nil, filepath.Join(GinkgoT().TempDir(), "addon-pins.json"))
Expect(err).ToNot(HaveOccurred())
Expect(buf.Len()).To(BeZero(), "unexpected log: %q", buf.String())
})
})
+35 -37
View File
@@ -5,11 +5,12 @@ import (
"context"
"errors"
"slices"
"testing"
"time"
"github.com/mudler/LocalAI/core/services/addons/builtin"
"github.com/mudler/LocalAI/core/services/addons/pins"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
type fakeOwner struct {
@@ -19,12 +20,9 @@ type fakeOwner struct {
func (f fakeOwner) InstalledIDs(context.Context) ([]string, error) { return f.ids, f.err }
func pinnedIDs(t *testing.T, s pins.Store, user string) []string {
t.Helper()
func pinnedIDs(s pins.Store, user string) []string {
list, err := s.List(context.Background(), user)
if err != nil {
t.Fatal(err)
}
Expect(err).ToNot(HaveOccurred())
out := []string{}
for _, p := range list {
out = append(out, p.AddonID)
@@ -32,35 +30,35 @@ func pinnedIDs(t *testing.T, s pins.Store, user string) []string {
return out
}
func TestSweepNeverDeletesTheBuiltinPins(t *testing.T) {
// No page is built in today, so the test makes up two.
defer builtin.SetCatalogForTest([]builtin.Entry{
{ID: "guide", Kind: builtin.KindPage, TitleKey: "items.chat", Icon: "chat", MainPath: "/app/chat"},
{ID: "help", Kind: builtin.KindPage, TitleKey: "items.models", Icon: "boxes", MainPath: "/app/models"},
})()
ctx := context.Background()
s, _ := pins.NewFileStore("")
_ = s.Replace(ctx, "u1", []string{"guide", "help", "notes"})
t0 := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)
owner := fakeOwner{ids: []string{}}
sweepAddonPins(ctx, s, owner, t0)
sweepAddonPins(ctx, s, owner, t0.Add(pins.Retention+time.Hour))
// notes is not installed and expired; the built-ins are always present.
if got := pinnedIDs(t, s, "u1"); !slices.Equal(got, []string{"guide", "help"}) {
t.Fatalf("unexpected pins %v", got)
}
}
var _ = Describe("sweepAddonPins", func() {
It("never deletes the built-in pins", func() {
// No page is built in today, so the test makes up two.
DeferCleanup(builtin.SetCatalogForTest([]builtin.Entry{
{ID: "guide", Kind: builtin.KindPage, TitleKey: "items.chat", Icon: "chat", MainPath: "/app/chat"},
{ID: "help", Kind: builtin.KindPage, TitleKey: "items.models", Icon: "boxes", MainPath: "/app/models"},
}))
ctx := context.Background()
s, _ := pins.NewFileStore("")
_ = s.Replace(ctx, "u1", []string{"guide", "help", "notes"})
t0 := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)
owner := fakeOwner{ids: []string{}}
sweepAddonPins(ctx, s, owner, t0)
sweepAddonPins(ctx, s, owner, t0.Add(pins.Retention+time.Hour))
// notes is not installed and expired; the built-ins are always present.
got := pinnedIDs(s, "u1")
Expect(slices.Equal(got, []string{"guide", "help"})).To(BeTrue(), "unexpected pins %v", got)
})
func TestSweepIsSkippedWhenTheOwnerDoesNotAnswer(t *testing.T) {
ctx := context.Background()
s, _ := pins.NewFileStore("")
_ = s.Replace(ctx, "u1", []string{"notes"})
t0 := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)
owner := fakeOwner{err: errors.New("owner unavailable")}
sweepAddonPins(ctx, s, owner, t0)
sweepAddonPins(ctx, s, owner, t0.Add(pins.Retention+time.Hour))
list, _ := s.List(ctx, "u1")
if len(list) != 1 || list[0].MissingSince != nil {
t.Fatalf("a sweep without an answer must neither mark nor delete: %+v", list)
}
}
It("is skipped when the owner does not answer", func() {
ctx := context.Background()
s, _ := pins.NewFileStore("")
_ = s.Replace(ctx, "u1", []string{"notes"})
t0 := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)
owner := fakeOwner{err: errors.New("owner unavailable")}
sweepAddonPins(ctx, s, owner, t0)
sweepAddonPins(ctx, s, owner, t0.Add(pins.Retention+time.Hour))
list, _ := s.List(ctx, "u1")
Expect(list).To(HaveLen(1), "a sweep without an answer must neither mark nor delete: %+v", list)
Expect(list[0].MissingSince).To(BeNil())
})
})
+10 -13
View File
@@ -3,20 +3,17 @@ package cli
import (
"reflect"
"testing"
"github.com/mudler/LocalAI/core/config"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestTheAddonHostOpsFlagDefaultIsTheDocumentedSetting(t *testing.T) {
f, ok := reflect.TypeOf(RunCMD{}).FieldByName("AddonHostOps")
if !ok {
t.Fatal("the flag is missing")
}
if got := f.Tag.Get("default"); got != config.DefaultAddonHostOps {
t.Fatalf("flag default %q differs from %q", got, config.DefaultAddonHostOps)
}
if f.Tag.Get("env") != "LOCALAI_ADDON_HOST_OPS" {
t.Fatalf("env %q", f.Tag.Get("env"))
}
}
var _ = Describe("the add-on host operations flag", func() {
It("defaults to the documented setting", func() {
f, ok := reflect.TypeOf(RunCMD{}).FieldByName("AddonHostOps")
Expect(ok).To(BeTrue(), "the flag is missing")
Expect(f.Tag.Get("default")).To(Equal(config.DefaultAddonHostOps))
Expect(f.Tag.Get("env")).To(Equal("LOCALAI_ADDON_HOST_OPS"))
})
})
+43 -31
View File
@@ -2,37 +2,49 @@
package config
import (
"reflect"
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestParseAddonHostOps(t *testing.T) {
got, err := ParseAddonHostOps(DefaultAddonHostOps)
want := map[string][]string{"fine-tuning": {"finetune."}, "quantization": {"quantize."}}
if err != nil || !reflect.DeepEqual(got, want) {
t.Fatalf("%v %v", got, err)
}
got, err = ParseAddonHostOps(" a-b = x. , a-b=y. ,c=z.")
if err != nil || !reflect.DeepEqual(got, map[string][]string{"a-b": {"x.", "y."}, "c": {"z."}}) {
t.Fatalf("%v %v", got, err)
}
if got, err := ParseAddonHostOps(""); err != nil || len(got) != 0 {
t.Fatalf("an empty setting names nobody: %v %v", got, err)
}
for _, bad := range []string{"fine-tuning", "=finetune.", "fine-tuning=", "fine-tuning=finetune", "Fine=finetune.", "fine-tuning=Finetune.", "a/b=finetune."} {
if _, err := ParseAddonHostOps(bad); err == nil {
t.Errorf("%q: accepted", bad)
}
}
}
var _ = Describe("ParseAddonHostOps", func() {
It("parses the default setting", func() {
got, err := ParseAddonHostOps(DefaultAddonHostOps)
Expect(err).ToNot(HaveOccurred())
Expect(got).To(Equal(map[string][]string{"fine-tuning": {"finetune."}, "quantization": {"quantize."}}))
})
func TestWithAddonHostOpsReplacesTheDefault(t *testing.T) {
o := &ApplicationConfig{}
if o.AddonHostOps != nil {
t.Fatal("the field starts unset")
}
WithAddonHostOps(map[string][]string{"x": {"y."}})(o)
if len(o.AddonHostOps) != 1 {
t.Fatalf("%v", o.AddonHostOps)
}
}
It("trims blanks and merges repeated names", func() {
got, err := ParseAddonHostOps(" a-b = x. , a-b=y. ,c=z.")
Expect(err).ToNot(HaveOccurred())
Expect(got).To(Equal(map[string][]string{"a-b": {"x.", "y."}, "c": {"z."}}))
})
It("names nobody for an empty setting", func() {
got, err := ParseAddonHostOps("")
Expect(err).ToNot(HaveOccurred())
Expect(got).To(BeEmpty())
})
DescribeTable("rejects a malformed setting",
func(bad string) {
_, err := ParseAddonHostOps(bad)
Expect(err).To(HaveOccurred(), "%q: accepted", bad)
},
Entry("no operations", "fine-tuning"),
Entry("no add-on name", "=finetune."),
Entry("empty prefix", "fine-tuning="),
Entry("prefix without a dot", "fine-tuning=finetune"),
Entry("uppercase add-on name", "Fine=finetune."),
Entry("uppercase prefix", "fine-tuning=Finetune."),
Entry("slash in the name", "a/b=finetune."),
)
})
var _ = Describe("WithAddonHostOps", func() {
It("replaces the default", func() {
o := &ApplicationConfig{}
Expect(o.AddonHostOps).To(BeNil(), "the field starts unset")
WithAddonHostOps(map[string][]string{"x": {"y."}})(o)
Expect(o.AddonHostOps).To(HaveLen(1))
})
})
+1 -1
View File
@@ -167,7 +167,7 @@ func fetch(url string, mutate func(*http.Request)) fetched {
client := &http.Client{Timeout: 10 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
resp, err := client.Do(req)
Expect(err).NotTo(HaveOccurred())
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()
b, rerr := io.ReadAll(resp.Body)
return fetched{code: resp.StatusCode, header: resp.Header, body: string(b), err: rerr}
}
+23 -31
View File
@@ -5,29 +5,25 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/labstack/echo/v4"
"github.com/mudler/LocalAI/core/services/addons/builtin"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func listBuiltin(t *testing.T) (*httptest.ResponseRecorder, []string) {
t.Helper()
func listBuiltin() (*httptest.ResponseRecorder, []string) {
e := echo.New()
e.GET("/api/addons/builtin", ListBuiltin(nil))
rec := httptest.NewRecorder()
e.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/addons/builtin", nil))
if rec.Code != http.StatusOK {
t.Fatalf("status %d", rec.Code)
}
Expect(rec.Code).To(Equal(http.StatusOK))
var body struct {
Builtin []struct {
ID string `json:"id"`
} `json:"builtin"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
Expect(json.Unmarshal(rec.Body.Bytes(), &body)).To(Succeed())
ids := []string{}
for _, e := range body.Builtin {
ids = append(ids, e.ID)
@@ -35,26 +31,22 @@ func listBuiltin(t *testing.T) (*httptest.ResponseRecorder, []string) {
return rec, ids
}
func TestListBuiltinIsEmptyBecauseStudioAndTalkArePackagedAddOns(t *testing.T) {
rec, ids := listBuiltin(t)
if len(ids) != 0 {
t.Fatalf("no page is built in, got %v", ids)
}
if rec.Header().Get("Cache-Control") != "private, no-store" {
t.Fatalf("the list depends on the caller and must not be cached, got %q", rec.Header().Get("Cache-Control"))
}
}
var _ = Describe("ListBuiltin", func() {
It("is empty because Studio and Talk are packaged add-ons", func() {
rec, ids := listBuiltin()
Expect(ids).To(BeEmpty(), "no page is built in")
Expect(rec.Header().Get("Cache-Control")).To(Equal("private, no-store"),
"the list depends on the caller and must not be cached")
})
func TestListBuiltinWithoutAnAccountDatabaseListsEveryEntry(t *testing.T) {
defer builtin.SetCatalogForTest([]builtin.Entry{
{ID: "guide", Kind: builtin.KindPage, TitleKey: "items.chat", Icon: "chat", MainPath: "/app/chat"},
{ID: "notes-page", Kind: builtin.KindPage, TitleKey: "items.models", Icon: "boxes", MainPath: "/app/models"},
})()
rec, ids := listBuiltin(t)
if len(ids) != 2 || ids[0] != "guide" || ids[1] != "notes-page" {
t.Fatalf("unexpected body %s", rec.Body.String())
}
if rec.Header().Get("Cache-Control") != "private, no-store" {
t.Fatalf("the list depends on the caller and must not be cached, got %q", rec.Header().Get("Cache-Control"))
}
}
It("lists every entry without an account database", func() {
DeferCleanup(builtin.SetCatalogForTest([]builtin.Entry{
{ID: "guide", Kind: builtin.KindPage, TitleKey: "items.chat", Icon: "chat", MainPath: "/app/chat"},
{ID: "notes-page", Kind: builtin.KindPage, TitleKey: "items.models", Icon: "boxes", MainPath: "/app/models"},
}))
rec, ids := listBuiltin()
Expect(ids).To(Equal([]string{"guide", "notes-page"}), "unexpected body %s", rec.Body.String())
Expect(rec.Header().Get("Cache-Control")).To(Equal("private, no-store"),
"the list depends on the caller and must not be cached")
})
})
@@ -121,7 +121,7 @@ var _ = Describe("alternating authenticated frontends", func() {
resp, e := client.Do(req)
Expect(e).NotTo(HaveOccurred())
b, e := io.ReadAll(resp.Body)
resp.Body.Close()
_ = resp.Body.Close()
Expect(e).NotTo(HaveOccurred())
Expect(resp.StatusCode).To(Equal(200))
var result contract.Result
@@ -134,7 +134,7 @@ var _ = Describe("alternating authenticated frontends", func() {
req.Header.Set("Authorization", "Bearer "+token)
resp, e := client.Do(req)
Expect(e).NotTo(HaveOccurred())
resp.Body.Close()
_ = resp.Body.Close()
Expect(resp.StatusCode).To(Equal(401))
})
})
+63 -68
View File
@@ -7,19 +7,19 @@ import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/labstack/echo/v4"
"github.com/mudler/LocalAI/core/http/auth"
"github.com/mudler/LocalAI/core/services/addons/builtin"
"github.com/mudler/LocalAI/core/services/addons/pins"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
// No page is built in today, so these tests make up two built-in pages to check
// how the routes filter and keep pins.
func madeUpPages(t *testing.T) {
t.Helper()
t.Cleanup(builtin.SetCatalogForTest([]builtin.Entry{
func madeUpPages() {
DeferCleanup(builtin.SetCatalogForTest([]builtin.Entry{
{ID: "guide", Kind: builtin.KindPage, TitleKey: "items.chat", Icon: "chat", MainPath: "/app/chat"},
{ID: "help", Kind: builtin.KindPage, TitleKey: "items.models", Icon: "boxes", MainPath: "/app/models"},
}))
@@ -28,16 +28,13 @@ func madeUpPages(t *testing.T) {
// pinsApp builds the two routes over a store. With seeded set, the upgrade seed
// has run for the local owner, as it does at start in a deployment that
// existed before this release.
func pinsApp(t *testing.T, seeded bool) *echo.Echo {
madeUpPages(t)
func pinsApp(seeded bool) *echo.Echo {
madeUpPages()
store, err := pins.NewFileStore("")
if err != nil {
t.Fatal(err)
}
Expect(err).ToNot(HaveOccurred())
if seeded {
if _, err := store.SeedExisting(context.Background(), []string{auth.AddonLocalOwnerID}, []string{"guide", "help"}); err != nil {
t.Fatal(err)
}
_, err := store.SeedExisting(context.Background(), []string{auth.AddonLocalOwnerID}, []string{"guide", "help"})
Expect(err).ToNot(HaveOccurred())
}
svc := &pins.Service{Store: store}
e := echo.New()
@@ -56,64 +53,62 @@ func call(e *echo.Echo, method, body string) (int, []string) {
return rec.Code, out
}
func TestAnAccountCreatedAfterTheUpgradeGetsNoPins(t *testing.T) {
e := pinsApp(t, false)
code, got := call(e, http.MethodGet, "")
if code != 200 || len(got) != 0 {
t.Fatalf("no seed has run for this account, so it has no pins: %d %v", code, got)
}
}
var _ = Describe("the pins routes", func() {
It("gives no pins to an account created after the upgrade", func() {
e := pinsApp(false)
code, got := call(e, http.MethodGet, "")
Expect(code).To(Equal(200), "no seed has run for this account, so it has no pins")
Expect(got).To(BeEmpty())
})
func TestPinsRoundTripForTheLocalOwner(t *testing.T) {
e := pinsApp(t, true)
code, got := call(e, http.MethodGet, "")
if code != 200 || len(got) != 2 || got[0] != "guide" || got[1] != "help" {
t.Fatalf("seed: %d %v", code, got)
}
code, got = call(e, http.MethodPut, `["help","guide"]`)
if code != 200 || got[0] != "help" {
t.Fatalf("put: %d %v", code, got)
}
_, got = call(e, http.MethodGet, "")
if len(got) != 2 || got[0] != "help" {
t.Fatalf("round trip: %v", got)
}
}
It("round-trips pins for the local owner", func() {
e := pinsApp(true)
code, got := call(e, http.MethodGet, "")
Expect(code).To(Equal(200))
Expect(got).To(Equal([]string{"guide", "help"}), "seed")
code, got = call(e, http.MethodPut, `["help","guide"]`)
Expect(code).To(Equal(200))
Expect(got).To(HaveLen(2))
Expect(got[0]).To(Equal("help"), "put")
_, got = call(e, http.MethodGet, "")
Expect(got).To(HaveLen(2))
Expect(got[0]).To(Equal("help"), "round trip")
})
func TestPutDropsIDsTheCallerCannotSee(t *testing.T) {
e := pinsApp(t, false)
_, got := call(e, http.MethodPut, `["guide","not-installed"]`)
if len(got) != 1 || got[0] != "guide" {
t.Fatalf("an unknown id must be dropped without a hint: %v", got)
}
}
It("drops IDs the caller cannot see", func() {
e := pinsApp(false)
_, got := call(e, http.MethodPut, `["guide","not-installed"]`)
Expect(got).To(Equal([]string{"guide"}), "an unknown id must be dropped without a hint")
})
func TestPutRejectsABodyThatIsNotAListOfStrings(t *testing.T) {
e := pinsApp(t, false)
for _, body := range []string{`{"a":1}`, `[1,2]`, `not json`, ``, `null`} {
if code, _ := call(e, http.MethodPut, body); code != 400 {
t.Fatalf("body %q: want 400, got %d", body, code)
DescribeTable("rejects a body that is not a list of strings",
func(body string) {
e := pinsApp(false)
code, _ := call(e, http.MethodPut, body)
Expect(code).To(Equal(400), "body %q", body)
},
Entry("an object", `{"a":1}`),
Entry("numbers", `[1,2]`),
Entry("not JSON", `not json`),
Entry("empty", ``),
Entry("null", `null`),
)
It("rejects a list over the hard limit", func() {
e := pinsApp(false)
items := make([]string, pins.HardLimit+1)
for i := range items {
items[i] = `"guide"`
}
}
}
code, _ := call(e, http.MethodPut, "["+strings.Join(items, ",")+"]")
Expect(code).To(Equal(400))
})
func TestPutRejectsAListOverTheHardLimit(t *testing.T) {
e := pinsApp(t, false)
items := make([]string, pins.HardLimit+1)
for i := range items {
items[i] = `"guide"`
}
if code, _ := call(e, http.MethodPut, "["+strings.Join(items, ",")+"]"); code != 400 {
t.Fatalf("want 400, got %d", code)
}
}
func TestStudioAndTalkAreNotVisiblePinsWithoutTheAddOns(t *testing.T) {
// Studio and Talk are packaged add-ons: the routes list them only for a
// caller whose owner lists them as installed, never as built-in pages.
e := pinsApp(t, false)
_, got := call(e, http.MethodPut, `["studio","talk","guide"]`)
if len(got) != 1 || got[0] != "guide" {
t.Fatalf("studio and talk are not built in, so they are dropped: %v", got)
}
}
It("does not show Studio and Talk as pins without the add-ons", func() {
// Studio and Talk are packaged add-ons: the routes list them only for a
// caller whose owner lists them as installed, never as built-in pages.
e := pinsApp(false)
_, got := call(e, http.MethodPut, `["studio","talk","guide"]`)
Expect(got).To(Equal([]string{"guide"}), "studio and talk are not built in, so they are dropped")
})
})
+1 -1
View File
@@ -137,7 +137,7 @@ var _ = Describe("server-side tool declarations over HTTP", Label("tools"), func
req.Header.Set("Content-Type", "application/json")
resp, e := hc.Do(req)
Expect(e).NotTo(HaveOccurred())
defer resp.Body.Close()
defer func() { _ = resp.Body.Close() }()
b, _ := io.ReadAll(resp.Body)
return resp.StatusCode, string(b)
}
@@ -3,11 +3,12 @@ package localai
import (
"context"
"errors"
"testing"
"github.com/mudler/LocalAI/core/schema"
"github.com/mudler/LocalAI/core/services/addons/hostops"
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
type fakeFTJobs struct {
@@ -55,72 +56,68 @@ func (f *fakeQJobs) ImportModel(context.Context, string, string, schema.Quantiza
var errNoJob = errors.New("job not found: j9")
func TestFineTuneModelsMapsAMissingJobToNotFound(t *testing.T) {
f := FineTuneModels{Svc: &fakeFTJobs{getErr: errNoJob}}
if _, err := f.ExportModel(context.Background(), "u1", "j9", schema.ExportRequest{}); !errors.Is(err, hostops.ErrNotFound) || !errors.Is(err, errNoJob) {
t.Fatalf("export: got %v, want not found", err)
}
if _, err := f.Checkpoints(context.Background(), "u1", "j9"); !errors.Is(err, hostops.ErrNotFound) {
t.Fatalf("checkpoints: got %v, want not found", err)
}
}
var _ = Describe("FineTuneModels", func() {
It("maps a missing job to not found", func() {
f := FineTuneModels{Svc: &fakeFTJobs{getErr: errNoJob}}
_, err := f.ExportModel(context.Background(), "u1", "j9", schema.ExportRequest{})
Expect(err).To(MatchError(hostops.ErrNotFound), "export")
Expect(err).To(MatchError(errNoJob), "export")
_, err = f.Checkpoints(context.Background(), "u1", "j9")
Expect(err).To(MatchError(hostops.ErrNotFound), "checkpoints")
})
func TestFineTuneModelsRefusesAnExportThatIsRunningAsAConflict(t *testing.T) {
svc := &fakeFTJobs{job: &schema.FineTuneJob{ID: "j1", ExportStatus: "exporting"}, exported: "/models/m"}
f := FineTuneModels{Svc: svc}
if _, err := f.ExportModel(context.Background(), "u1", "j1", schema.ExportRequest{}); !errors.Is(err, hostops.ErrConflict) {
t.Fatalf("got %v, want conflict", err)
}
if svc.exports != 0 {
t.Fatalf("service export ran %d times for a running export", svc.exports)
}
}
It("refuses an export that is running as a conflict", func() {
svc := &fakeFTJobs{job: &schema.FineTuneJob{ID: "j1", ExportStatus: "exporting"}, exported: "/models/m"}
f := FineTuneModels{Svc: svc}
_, err := f.ExportModel(context.Background(), "u1", "j1", schema.ExportRequest{})
Expect(err).To(MatchError(hostops.ErrConflict))
Expect(svc.exports).To(BeZero(), "service export ran for a running export")
})
func TestFineTuneModelsPassesAFinishedJobToTheService(t *testing.T) {
svc := &fakeFTJobs{job: &schema.FineTuneJob{ID: "j1", ExportStatus: ""}, exported: "/models/m"}
f := FineTuneModels{Svc: svc}
got, err := f.ExportModel(context.Background(), "u1", "j1", schema.ExportRequest{})
if err != nil || got != "/models/m" || svc.exports != 1 {
t.Fatalf("got %q %v exports=%d", got, err, svc.exports)
}
}
It("passes a finished job to the service", func() {
svc := &fakeFTJobs{job: &schema.FineTuneJob{ID: "j1", ExportStatus: ""}, exported: "/models/m"}
f := FineTuneModels{Svc: svc}
got, err := f.ExportModel(context.Background(), "u1", "j1", schema.ExportRequest{})
Expect(err).ToNot(HaveOccurred())
Expect(got).To(Equal("/models/m"))
Expect(svc.exports).To(Equal(1))
})
func TestFineTuneModelsListsCheckpointsWithTheirPaths(t *testing.T) {
svc := &fakeFTJobs{job: &schema.FineTuneJob{ID: "j1"}, cps: []*pb.CheckpointInfo{{Step: 7, Path: "/out/ckpt-7"}}}
got, err := FineTuneModels{Svc: svc}.Checkpoints(context.Background(), "u1", "j1")
if err != nil || len(got) != 1 || got[0].Step != 7 || got[0].Path != "/out/ckpt-7" {
t.Fatalf("got %+v %v", got, err)
}
}
It("lists checkpoints with their paths", func() {
svc := &fakeFTJobs{job: &schema.FineTuneJob{ID: "j1"}, cps: []*pb.CheckpointInfo{{Step: 7, Path: "/out/ckpt-7"}}}
got, err := FineTuneModels{Svc: svc}.Checkpoints(context.Background(), "u1", "j1")
Expect(err).ToNot(HaveOccurred())
Expect(got).To(HaveLen(1))
Expect(got[0].Step).To(BeEquivalentTo(7))
Expect(got[0].Path).To(Equal("/out/ckpt-7"))
})
})
func TestQuantizationModelsMapsAMissingJobToNotFound(t *testing.T) {
q := QuantizationModels{Svc: &fakeQJobs{getErr: errNoJob}}
if _, err := q.ImportModel(context.Background(), "u1", "j9", schema.QuantizationImportRequest{}); !errors.Is(err, hostops.ErrNotFound) {
t.Fatalf("got %v, want not found", err)
}
}
var _ = Describe("QuantizationModels", func() {
It("maps a missing job to not found", func() {
q := QuantizationModels{Svc: &fakeQJobs{getErr: errNoJob}}
_, err := q.ImportModel(context.Background(), "u1", "j9", schema.QuantizationImportRequest{})
Expect(err).To(MatchError(hostops.ErrNotFound))
})
func TestQuantizationModelsRefusesAJobThatIsNotCompletedAsAConflict(t *testing.T) {
svc := &fakeQJobs{job: &schema.QuantizationJob{ID: "q1", Status: "running"}, imported: "/models/q"}
if _, err := (QuantizationModels{Svc: svc}).ImportModel(context.Background(), "u1", "q1", schema.QuantizationImportRequest{}); !errors.Is(err, hostops.ErrConflict) {
t.Fatalf("got %v, want conflict", err)
}
if svc.imports != 0 {
t.Fatalf("service import ran %d times for an unfinished job", svc.imports)
}
}
It("refuses a job that is not completed as a conflict", func() {
svc := &fakeQJobs{job: &schema.QuantizationJob{ID: "q1", Status: "running"}, imported: "/models/q"}
_, err := (QuantizationModels{Svc: svc}).ImportModel(context.Background(), "u1", "q1", schema.QuantizationImportRequest{})
Expect(err).To(MatchError(hostops.ErrConflict))
Expect(svc.imports).To(BeZero(), "service import ran for an unfinished job")
})
func TestQuantizationModelsRefusesAnImportThatIsRunningAsAConflict(t *testing.T) {
svc := &fakeQJobs{job: &schema.QuantizationJob{ID: "q1", Status: "completed", ImportStatus: "importing"}, imported: "/models/q"}
if _, err := (QuantizationModels{Svc: svc}).ImportModel(context.Background(), "u1", "q1", schema.QuantizationImportRequest{}); !errors.Is(err, hostops.ErrConflict) {
t.Fatalf("got %v, want conflict", err)
}
}
It("refuses an import that is running as a conflict", func() {
svc := &fakeQJobs{job: &schema.QuantizationJob{ID: "q1", Status: "completed", ImportStatus: "importing"}, imported: "/models/q"}
_, err := (QuantizationModels{Svc: svc}).ImportModel(context.Background(), "u1", "q1", schema.QuantizationImportRequest{})
Expect(err).To(MatchError(hostops.ErrConflict))
})
func TestQuantizationModelsPassesACompletedJobToTheService(t *testing.T) {
svc := &fakeQJobs{job: &schema.QuantizationJob{ID: "q1", Status: "completed"}, imported: "/models/q"}
got, err := (QuantizationModels{Svc: svc}).ImportModel(context.Background(), "u1", "q1", schema.QuantizationImportRequest{})
if err != nil || got != "/models/q" || svc.imports != 1 {
t.Fatalf("got %q %v imports=%d", got, err, svc.imports)
}
}
It("passes a completed job to the service", func() {
svc := &fakeQJobs{job: &schema.QuantizationJob{ID: "q1", Status: "completed"}, imported: "/models/q"}
got, err := (QuantizationModels{Svc: svc}).ImportModel(context.Background(), "u1", "q1", schema.QuantizationImportRequest{})
Expect(err).ToNot(HaveOccurred())
Expect(got).To(Equal("/models/q"))
Expect(svc.imports).To(Equal(1))
})
})
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: MIT
package builtin
import (
"testing"
"github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestBuiltin(t *testing.T) {
RegisterFailHandler(ginkgo.Fail)
ginkgo.RunSpecs(t, "Built-in add-ons suite")
}
+42 -47
View File
@@ -3,7 +3,9 @@ package builtin
import (
"slices"
"testing"
"github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func ids(es []Entry) []string {
@@ -14,51 +16,44 @@ func ids(es []Entry) []string {
return out
}
func TestNoAddonIsBuiltIn(t *testing.T) {
// Studio and Talk are packaged add-ons, installed and removed like the
// others, so nothing is listed for a viewer, whatever the viewer holds.
if got := ids(Visible(Viewer{Admin: true})); len(got) != 0 {
t.Fatalf("no add-on is built in, got %v", got)
}
if got := ids(Visible(Viewer{Has: func(string) bool { return true }})); len(got) != 0 {
t.Fatalf("no add-on is built in, got %v", got)
}
if len(IDs()) != 0 || len(Catalog()) != 0 {
t.Fatalf("the catalog must be empty, got %v", IDs())
}
}
var _ = ginkgo.Describe("the built-in catalog", func() {
ginkgo.It("holds no add-on", func() {
// Studio and Talk are packaged add-ons, installed and removed like the
// others, so nothing is listed for a viewer, whatever the viewer holds.
Expect(ids(Visible(Viewer{Admin: true}))).To(BeEmpty(), "no add-on is built in")
Expect(ids(Visible(Viewer{Has: func(string) bool { return true }}))).To(BeEmpty(), "no add-on is built in")
Expect(IDs()).To(BeEmpty(), "the catalog must be empty")
Expect(Catalog()).To(BeEmpty(), "the catalog must be empty")
})
func TestStudioAndTalkAreNotBuiltIn(t *testing.T) {
for _, id := range []string{"studio", "talk"} {
if slices.Contains(IDs(), id) {
t.Fatalf("%s is a packaged add-on and must not be in the built-in catalog", id)
ginkgo.DescribeTable("does not list a packaged add-on",
func(id string) {
Expect(slices.Contains(IDs(), id)).To(BeFalse(), "%s is a packaged add-on and must not be in the built-in catalog", id)
},
ginkgo.Entry("studio", "studio"),
ginkgo.Entry("talk", "talk"),
)
ginkgo.It("hides a gated entry from a viewer without the feature", func() {
gated := Entry{ID: "x", Feature: "fine_tuning"}
adminOnly := Entry{ID: "y", AdminOnly: true}
none := Viewer{Has: func(string) bool { return false }}
Expect(none.allows(gated)).To(BeFalse(), "a viewer without the feature must not see a gated entry")
Expect(none.allows(adminOnly)).To(BeFalse(), "a viewer without the role must not see a gated entry")
has := Viewer{Has: func(f string) bool { return f == "fine_tuning" }}
Expect(has.allows(gated)).To(BeTrue(), "the feature opens a feature gate")
Expect(has.allows(adminOnly)).To(BeFalse(), "the feature opens a feature gate and nothing else")
admin := Viewer{Admin: true}
Expect(admin.allows(gated)).To(BeTrue(), "an administrator passes every gate")
Expect(admin.allows(adminOnly)).To(BeTrue(), "an administrator passes every gate")
})
ginkgo.It("has unique and well-formed IDs", func() {
seen := map[string]bool{}
for _, e := range Catalog() {
Expect(seen[e.ID] || e.ID == "" || e.TitleKey == "" || e.MainPath == "" || e.Kind != KindPage).To(BeFalse(),
"invalid or duplicate entry %+v", e)
seen[e.ID] = true
}
}
}
func TestGatesHideAnEntryFromAViewerWithoutTheFeature(t *testing.T) {
gated := Entry{ID: "x", Feature: "fine_tuning"}
adminOnly := Entry{ID: "y", AdminOnly: true}
none := Viewer{Has: func(string) bool { return false }}
if none.allows(gated) || none.allows(adminOnly) {
t.Fatal("a viewer without the feature or the role must not see a gated entry")
}
has := Viewer{Has: func(f string) bool { return f == "fine_tuning" }}
if !has.allows(gated) || has.allows(adminOnly) {
t.Fatal("the feature opens a feature gate and nothing else")
}
admin := Viewer{Admin: true}
if !admin.allows(gated) || !admin.allows(adminOnly) {
t.Fatal("an administrator passes every gate")
}
}
func TestIDsAreUniqueAndWellFormed(t *testing.T) {
seen := map[string]bool{}
for _, e := range Catalog() {
if seen[e.ID] || e.ID == "" || e.TitleKey == "" || e.MainPath == "" || e.Kind != KindPage {
t.Fatalf("invalid or duplicate entry %+v", e)
}
seen[e.ID] = true
}
}
})
})
+60 -92
View File
@@ -6,102 +6,70 @@ import (
"os"
"path/filepath"
"strings"
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestLoopbackAddressIsStableAcrossStarts(t *testing.T) {
root := filepath.Join(t.TempDir(), "addons")
_, first, err := NewLoopbackCarrierConfig("local", root)
if err != nil {
t.Fatal(err)
}
cfg, second, err := NewLoopbackCarrierConfig("local", root)
if err != nil {
t.Fatal(err)
}
if first != second {
t.Fatalf("address changed between starts: %s then %s", first, second)
}
if cfg.OwnerAddress != "https://"+second {
t.Fatalf("owner address %q does not match listen address %q", cfg.OwnerAddress, second)
}
info, err := os.Stat(filepath.Join(root, loopbackPortFile))
if err != nil {
t.Fatal(err)
}
if info.Mode().Perm() != 0o600 {
t.Fatalf("port file mode is %v, want 0600", info.Mode().Perm())
}
}
var _ = Describe("the loopback carrier config", func() {
It("keeps its address stable across starts", func() {
root := filepath.Join(GinkgoT().TempDir(), "addons")
_, first, err := NewLoopbackCarrierConfig("local", root)
Expect(err).ToNot(HaveOccurred())
cfg, second, err := NewLoopbackCarrierConfig("local", root)
Expect(err).ToNot(HaveOccurred())
Expect(second).To(Equal(first), "address changed between starts")
Expect(cfg.OwnerAddress).To(Equal("https://"+second), "owner address does not match listen address")
info, err := os.Stat(filepath.Join(root, loopbackPortFile))
Expect(err).ToNot(HaveOccurred())
Expect(info.Mode().Perm()).To(Equal(os.FileMode(0o600)), "port file mode")
})
func TestLoopbackDifferentRootsDoNotShareState(t *testing.T) {
_, a, err := NewLoopbackCarrierConfig("local", filepath.Join(t.TempDir(), "addons"))
if err != nil {
t.Fatal(err)
}
if _, _, err := NewLoopbackCarrierConfig("local", filepath.Join(t.TempDir(), "addons")); err != nil {
t.Fatal(err)
}
if a == "" {
t.Fatal("empty address")
}
}
It("does not share state between different roots", func() {
_, a, err := NewLoopbackCarrierConfig("local", filepath.Join(GinkgoT().TempDir(), "addons"))
Expect(err).ToNot(HaveOccurred())
_, _, err = NewLoopbackCarrierConfig("local", filepath.Join(GinkgoT().TempDir(), "addons"))
Expect(err).ToNot(HaveOccurred())
Expect(a).ToNot(BeEmpty(), "empty address")
})
func TestLoopbackPortInUseFailsClearly(t *testing.T) {
root := filepath.Join(t.TempDir(), "addons")
_, address, err := NewLoopbackCarrierConfig("local", root)
if err != nil {
t.Fatal(err)
}
held, err := net.Listen("tcp", address)
if err != nil {
t.Fatal(err)
}
defer func() { _ = held.Close() }()
_, _, err = NewLoopbackCarrierConfig("local", root)
if err == nil {
t.Fatal("expected an error while the port is in use")
}
_, port, _ := net.SplitHostPort(address)
if !strings.Contains(err.Error(), port) || !strings.Contains(err.Error(), "--addons-enabled=false") {
t.Fatalf("error does not name the port and the option: %v", err)
}
}
It("fails clearly when the port is in use", func() {
root := filepath.Join(GinkgoT().TempDir(), "addons")
_, address, err := NewLoopbackCarrierConfig("local", root)
Expect(err).ToNot(HaveOccurred())
held, err := net.Listen("tcp", address)
Expect(err).ToNot(HaveOccurred())
defer func() { _ = held.Close() }()
_, _, err = NewLoopbackCarrierConfig("local", root)
Expect(err).To(HaveOccurred(), "expected an error while the port is in use")
_, port, _ := net.SplitHostPort(address)
Expect(err.Error()).To(ContainSubstring(port), "error does not name the port")
Expect(err.Error()).To(ContainSubstring("--addons-enabled=false"), "error does not name the option")
})
func TestLoopbackRejectsInvalidPortFile(t *testing.T) {
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, loopbackPortFile), []byte("nope"), 0o600); err != nil {
t.Fatal(err)
}
if _, _, err := NewLoopbackCarrierConfig("local", root); err == nil {
t.Fatal("expected an error for an invalid port file")
}
}
It("rejects an invalid port file", func() {
root := GinkgoT().TempDir()
Expect(os.WriteFile(filepath.Join(root, loopbackPortFile), []byte("nope"), 0o600)).To(Succeed())
_, _, err := NewLoopbackCarrierConfig("local", root)
Expect(err).To(HaveOccurred(), "expected an error for an invalid port file")
})
func TestLoopbackRejectsLoosePortFileMode(t *testing.T) {
root := t.TempDir()
path := filepath.Join(root, loopbackPortFile)
if err := os.WriteFile(path, []byte("40123\n"), 0o600); err != nil {
t.Fatal(err)
}
if err := os.Chmod(path, 0o644); err != nil {
t.Fatal(err)
}
if _, _, err := NewLoopbackCarrierConfig("local", root); err == nil || !strings.Contains(err.Error(), "0600") {
t.Fatalf("expected a mode error, got %v", err)
}
}
It("rejects a port file with a loose mode", func() {
root := GinkgoT().TempDir()
path := filepath.Join(root, loopbackPortFile)
Expect(os.WriteFile(path, []byte("40123\n"), 0o600)).To(Succeed())
Expect(os.Chmod(path, 0o644)).To(Succeed())
_, _, err := NewLoopbackCarrierConfig("local", root)
Expect(err).To(HaveOccurred())
Expect(strings.Contains(err.Error(), "0600")).To(BeTrue(), "expected a mode error, got %v", err)
})
func TestLoopbackRejectsPortFileSymlinkOutsideRoot(t *testing.T) {
root := t.TempDir()
outside := filepath.Join(t.TempDir(), "port")
if err := os.WriteFile(outside, []byte("40123\n"), 0o600); err != nil {
t.Fatal(err)
}
if err := os.Symlink(outside, filepath.Join(root, loopbackPortFile)); err != nil {
t.Fatal(err)
}
if _, _, err := NewLoopbackCarrierConfig("local", root); err == nil {
t.Fatal("expected an error for a port file that links outside the root")
}
}
It("rejects a port file that links outside the root", func() {
root := GinkgoT().TempDir()
outside := filepath.Join(GinkgoT().TempDir(), "port")
Expect(os.WriteFile(outside, []byte("40123\n"), 0o600)).To(Succeed())
Expect(os.Symlink(outside, filepath.Join(root, loopbackPortFile))).To(Succeed())
_, _, err := NewLoopbackCarrierConfig("local", root)
Expect(err).To(HaveOccurred(), "expected an error for a port file that links outside the root")
})
})
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: MIT
package toolargs
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestToolargs(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Tool arguments suite")
}
+18 -20
View File
@@ -3,9 +3,10 @@ package toolargs
import (
"encoding/json"
"errors"
"strings"
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
// args builds an argument object of exactly n bytes with one string property.
@@ -15,21 +16,18 @@ func args(n int) json.RawMessage {
return json.RawMessage(`{"content":"` + strings.Repeat("a", n-len(frame)) + `"}`)
}
func TestTheArgumentLimitIsOneMiBAndOneByteMoreIsRefused(t *testing.T) {
if MaxArgumentBytes != 1<<20 {
t.Fatalf("limit %d, want 1 MiB", MaxArgumentBytes)
}
params := map[string]any{"properties": map[string]any{"content": map[string]any{"type": "string"}}}
// The limit is on what the add-on receives: the tool key counts.
const toolKey = len(`,"tool":"t"`)
_, body, err := Bind("t", params, args(MaxArgumentBytes-toolKey), false)
if err != nil || len(body) != MaxArgumentBytes {
t.Fatalf("a body of exactly 1 MiB must be accepted: %v (%d bytes)", err, len(body))
}
if _, _, err := Bind("t", params, args(MaxArgumentBytes-toolKey+1), false); !errors.Is(err, ErrInvalid) {
t.Fatalf("a body of 1 MiB + 1 byte must be refused, got %v", err)
}
if _, _, err := Bind("t", params, args(MaxArgumentBytes+1), false); !errors.Is(err, ErrInvalid) {
t.Fatalf("an input over 1 MiB must be refused, got %v", err)
}
}
var _ = Describe("the argument limit", func() {
It("is one MiB and one byte more is refused", func() {
Expect(MaxArgumentBytes).To(Equal(1<<20), "limit must be 1 MiB")
params := map[string]any{"properties": map[string]any{"content": map[string]any{"type": "string"}}}
// The limit is on what the add-on receives: the tool key counts.
const toolKey = len(`,"tool":"t"`)
_, body, err := Bind("t", params, args(MaxArgumentBytes-toolKey), false)
Expect(err).ToNot(HaveOccurred(), "a body of exactly 1 MiB must be accepted")
Expect(body).To(HaveLen(MaxArgumentBytes))
_, _, err = Bind("t", params, args(MaxArgumentBytes-toolKey+1), false)
Expect(err).To(MatchError(ErrInvalid), "a body of 1 MiB + 1 byte must be refused")
_, _, err = Bind("t", params, args(MaxArgumentBytes+1), false)
Expect(err).To(MatchError(ErrInvalid), "an input over 1 MiB must be refused")
})
})
+61 -50
View File
@@ -4,57 +4,68 @@ package contract
import (
"encoding/json"
"strings"
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestHostWireTypesRoundTrip(t *testing.T) {
req := HostRequest{OperationID: "op1", Op: "finetune.get", Arguments: json.RawMessage(`{"jobId":"j1"}`)}
b, err := json.Marshal(req)
if err != nil {
t.Fatal(err)
}
got, err := DecodeHostRequest(b, 1<<20)
if err != nil || got.OperationID != "op1" || got.Op != "finetune.get" || string(got.Arguments) != `{"jobId":"j1"}` {
t.Fatalf("%+v %v", got, err)
}
resp := HostResponse{Error: &HostError{Code: "scope", Message: "no"}}
b, _ = json.Marshal(resp)
var back HostResponse
if err := json.Unmarshal(b, &back); err != nil || back.Error == nil || back.Error.Code != "scope" || back.Error.Error() == "" {
t.Fatalf("%+v %v", back, err)
}
b, _ = json.Marshal(HostResponse{Result: json.RawMessage(`{"ok":true}`)})
if err := json.Unmarshal(b, &back); err != nil || string(back.Result) != `{"ok":true}` {
t.Fatalf("%s %v", b, err)
}
}
var _ = Describe("host operation wire types", func() {
It("round-trip", func() {
req := HostRequest{OperationID: "op1", Op: "finetune.get", Arguments: json.RawMessage(`{"jobId":"j1"}`)}
b, err := json.Marshal(req)
Expect(err).ToNot(HaveOccurred())
got, err := DecodeHostRequest(b, 1<<20)
Expect(err).ToNot(HaveOccurred())
Expect(got.OperationID).To(Equal("op1"))
Expect(got.Op).To(Equal("finetune.get"))
Expect(string(got.Arguments)).To(Equal(`{"jobId":"j1"}`))
func TestTheStrictDecoderRefusesWhatItDoesNotKnow(t *testing.T) {
for name, body := range map[string]string{
"unknown field": `{"operationId":"a","op":"finetune.get","arguments":{},"userId":"x"}`,
"duplicate key": `{"operationId":"a","operationId":"b","op":"finetune.get"}`,
"trailing": `{"operationId":"a","op":"finetune.get"} {}`,
"not an object": `[]`,
"empty": ``,
} {
if _, err := DecodeHostRequest([]byte(body), 1<<20); err == nil {
t.Errorf("%s: accepted", name)
}
}
if _, err := DecodeHostRequest([]byte(`{"operationId":"a","op":"finetune.get"}`), 10); err == nil {
t.Error("an oversized body was accepted")
}
}
resp := HostResponse{Error: &HostError{Code: "scope", Message: "no"}}
b, _ = json.Marshal(resp)
var back HostResponse
Expect(json.Unmarshal(b, &back)).To(Succeed())
Expect(back.Error).ToNot(BeNil())
Expect(back.Error.Code).To(Equal("scope"))
Expect(back.Error.Error()).ToNot(BeEmpty())
func TestAnOperationNameHasTheDottedShape(t *testing.T) {
for op, want := range map[string]bool{
"finetune.get": true, "quantize.start": true, "finetune.a.b": true,
"": false, "finetune": false, ".get": false, "Finetune.get": false, "finetune.get ": false,
"finetune/get": false, "finetune.get\n": false, "fine-tune.get": false, "finetune.": false,
strings.Repeat("a", 70) + ".get": false,
} {
if got := ValidHostOp(op); got != want {
t.Errorf("%q: got %v want %v", op, got, want)
}
}
}
b, _ = json.Marshal(HostResponse{Result: json.RawMessage(`{"ok":true}`)})
Expect(json.Unmarshal(b, &back)).To(Succeed())
Expect(string(back.Result)).To(Equal(`{"ok":true}`))
})
DescribeTable("the strict decoder refuses what it does not know",
func(body string) {
_, err := DecodeHostRequest([]byte(body), 1<<20)
Expect(err).To(HaveOccurred(), "accepted")
},
Entry("unknown field", `{"operationId":"a","op":"finetune.get","arguments":{},"userId":"x"}`),
Entry("duplicate key", `{"operationId":"a","operationId":"b","op":"finetune.get"}`),
Entry("trailing", `{"operationId":"a","op":"finetune.get"} {}`),
Entry("not an object", `[]`),
Entry("empty", ``),
)
It("refuses an oversized body", func() {
_, err := DecodeHostRequest([]byte(`{"operationId":"a","op":"finetune.get"}`), 10)
Expect(err).To(HaveOccurred(), "an oversized body was accepted")
})
DescribeTable("an operation name has the dotted shape",
func(op string, want bool) {
Expect(ValidHostOp(op)).To(Equal(want), "%q", op)
},
Entry("finetune.get", "finetune.get", true),
Entry("quantize.start", "quantize.start", true),
Entry("finetune.a.b", "finetune.a.b", true),
Entry("empty", "", false),
Entry("no dot", "finetune", false),
Entry("leading dot", ".get", false),
Entry("uppercase", "Finetune.get", false),
Entry("trailing space", "finetune.get ", false),
Entry("slash", "finetune/get", false),
Entry("trailing newline", "finetune.get\n", false),
Entry("hyphen", "fine-tune.get", false),
Entry("empty verb", "finetune.", false),
Entry("too long", strings.Repeat("a", 70)+".get", false),
)
})
+67 -72
View File
@@ -12,7 +12,6 @@ import (
"os/exec"
"path/filepath"
"strings"
"testing"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -1091,86 +1090,82 @@ var _ = Describe("workflow policy", func() {
// written out so a new add-on is a deliberate change here and in the workflow.
var firstPartyAddons = []string{"tables", "voice", "face", "fine-tuning", "quantization", "studio", "talk"}
func readWorkflow(t *testing.T) string {
t.Helper()
func readWorkflow() string {
GinkgoHelper()
b, err := os.ReadFile("../../../.github/workflows/addons-package.yml")
if err != nil {
t.Fatal(err)
}
Expect(err).ToNot(HaveOccurred())
return string(b)
}
func TestTheWorkflowHasNoHardCodedAddonPath(t *testing.T) {
text := readWorkflow(t)
for i, line := range strings.Split(text, "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "#") {
continue
var _ = Describe("the add-on package workflow", func() {
It("has no hard-coded add-on path", func() {
text := readWorkflow()
for i, line := range strings.Split(text, "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "#") {
continue
}
Expect(line).ToNot(ContainSubstring("addons/tables/"), "line %d names addons/tables/ outside the add-on list: %s", i+1, trimmed)
}
if strings.Contains(line, "addons/tables/") {
t.Errorf("line %d names addons/tables/ outside the add-on list: %s", i+1, trimmed)
}
}
}
})
func TestTheWorkflowRunsEveryFirstPartyAddon(t *testing.T) {
var doc struct {
Jobs map[string]struct {
Strategy struct {
Matrix any `yaml:"matrix"`
} `yaml:"strategy"`
Steps []map[string]any `yaml:"steps"`
} `yaml:"jobs"`
}
if err := yaml.Unmarshal([]byte(readWorkflow(t)), &doc); err != nil {
t.Fatal(err)
}
// validate lists the add-ons literally. The other jobs take the add-ons
// that have an enabled platform from the plan.
list := func(v any) []string {
var out []string
items, _ := v.([]any)
for _, x := range items {
out = append(out, fmt.Sprint(x))
Context("for every first-party add-on", func() {
var doc struct {
Jobs map[string]struct {
Strategy struct {
Matrix any `yaml:"matrix"`
} `yaml:"strategy"`
Steps []map[string]any `yaml:"steps"`
} `yaml:"jobs"`
}
return out
}
matrixOf := func(job string) map[string]any { m, _ := doc.Jobs[job].Strategy.Matrix.(map[string]any); return m }
got := list(matrixOf("validate")["addon"])
if strings.Join(got, ",") != strings.Join(firstPartyAddons, ",") {
t.Fatalf("validate matrix lists %v, want %v", got, firstPartyAddons)
}
for _, job := range []string{"publish", "promote"} {
if _, ok := matrixOf(job)["addon"]; !ok {
t.Errorf("%s has no addon matrix", job)
matrixOf := func(job string) map[string]any { m, _ := doc.Jobs[job].Strategy.Matrix.(map[string]any); return m }
BeforeEach(func() {
Expect(yaml.Unmarshal([]byte(readWorkflow()), &doc)).To(Succeed())
})
It("lists the add-ons in the validate matrix", func() {
// validate lists the add-ons literally. The other jobs take the add-ons
// that have an enabled platform from the plan.
var got []string
items, _ := matrixOf("validate")["addon"].([]any)
for _, x := range items {
got = append(got, fmt.Sprint(x))
}
Expect(strings.Join(got, ",")).To(Equal(strings.Join(firstPartyAddons, ",")), "validate matrix lists %v, want %v", got, firstPartyAddons)
})
for _, job := range []string{"publish", "promote"} {
It("gives "+job+" an add-on matrix", func() {
Expect(matrixOf(job)).To(HaveKey("addon"), "%s has no addon matrix", job)
})
}
}
if !strings.Contains(fmt.Sprint(doc.Jobs["package"].Strategy.Matrix), "needs.plan.outputs.matrix") {
t.Error("package does not take its legs from the plan")
}
// The plan loops over the same list.
plan := readWorkflow(t)
if !strings.Contains(plan, "for id in "+strings.Join(firstPartyAddons, " ")+"; do") {
t.Errorf("plan does not loop over exactly %v", firstPartyAddons)
}
for _, id := range firstPartyAddons {
file := "../../../addons/" + id + "/package/platforms.yaml"
cfg, err := loadPlatforms(file)
if err != nil {
t.Errorf("%s: %v", id, err)
continue
It("takes the package legs from the plan", func() {
Expect(fmt.Sprint(doc.Jobs["package"].Strategy.Matrix)).To(ContainSubstring("needs.plan.outputs.matrix"),
"package does not take its legs from the plan")
})
It("loops the plan over the same list", func() {
plan := readWorkflow()
Expect(plan).To(ContainSubstring("for id in "+strings.Join(firstPartyAddons, " ")+"; do"),
"plan does not loop over exactly %v", firstPartyAddons)
})
for _, id := range firstPartyAddons {
It("has consistent package files for "+id, func() {
file := "../../../addons/" + id + "/package/platforms.yaml"
cfg, err := loadPlatforms(file)
Expect(err).ToNot(HaveOccurred(), id)
Expect(cfg.Addon).To(Equal(id), "platforms file names add-on %q", cfg.Addon)
Expect(cfg.Manifest).To(Equal("addons/"+id+"/manifest.json"), "platforms file names manifest %q", cfg.Manifest)
Expect(cfg.UI).To(Equal("addons/"+id+"/ui/dist"), "ui is %q", cfg.UI)
_, err = os.Stat("../../../addons/" + id + "/cmd/" + cfg.Executable + "/main.go")
Expect(err).ToNot(HaveOccurred(), "no executable source for %q", cfg.Executable)
})
}
if cfg.Addon != id || cfg.Manifest != "addons/"+id+"/manifest.json" {
t.Errorf("%s: platforms file names add-on %q and manifest %q", id, cfg.Addon, cfg.Manifest)
}
if cfg.UI != "addons/"+id+"/ui/dist" {
t.Errorf("%s: ui is %q", id, cfg.UI)
}
if _, err := os.Stat("../../../addons/" + id + "/cmd/" + cfg.Executable + "/main.go"); err != nil {
t.Errorf("%s: no executable source for %q: %v", id, cfg.Executable, err)
}
}
}
})
})
// documentedAddonRepository is the value the documentation gives for the
// ADDON_OCI_REPOSITORY variable: the Tables repository in the add-on namespace.
+46 -66
View File
@@ -6,7 +6,9 @@ import (
"path/filepath"
"sort"
"strings"
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
// The release platforms of the packaged first-party add-ons, with the runner
@@ -26,72 +28,50 @@ var nativeChecked = map[string]bool{"linux/amd64": true, "linux/arm64": true, "d
var packagedAddons = []string{"voice", "face", "fine-tuning", "quantization", "studio", "talk"}
func TestEveryPackagedAddonListsEveryReleasePlatformWithItsRunner(t *testing.T) {
var _ = Describe("the packaged add-ons", func() {
for _, id := range packagedAddons {
p, err := loadPlatforms(filepath.Join("..", "..", "..", "addons", id, "package", "platforms.yaml"))
if err != nil {
t.Fatalf("%s: %v", id, err)
}
if p.Addon != id || p.Executable != id {
t.Fatalf("%s: addon %q executable %q", id, p.Addon, p.Executable)
}
got := map[string]string{}
for _, e := range p.Platforms {
got[e.Platform] = e.Runner
if e.NativeCheck != NativeCheckKind {
t.Fatalf("%s %s: native check %q", id, e.Platform, e.NativeCheck)
}
}
if len(got) != len(releasePlatforms) {
t.Fatalf("%s lists %d platforms, want %d", id, len(got), len(releasePlatforms))
}
for platform, runner := range releasePlatforms {
if got[platform] != runner {
t.Fatalf("%s: platform %s has runner %q, want %q", id, platform, got[platform], runner)
}
}
}
}
Context(id, func() {
It("lists every release platform with its runner", func() {
p, err := loadPlatforms(filepath.Join("..", "..", "..", "addons", id, "package", "platforms.yaml"))
Expect(err).ToNot(HaveOccurred())
Expect(p.Addon).To(Equal(id))
Expect(p.Executable).To(Equal(id))
got := map[string]string{}
for _, e := range p.Platforms {
got[e.Platform] = e.Runner
Expect(e.NativeCheck).To(Equal(NativeCheckKind), "%s: native check", e.Platform)
}
Expect(got).To(HaveLen(len(releasePlatforms)))
for platform, runner := range releasePlatforms {
Expect(got[platform]).To(Equal(runner), "platform %s", platform)
}
})
func TestAPlatformIsEnabledExactlyWhenItsNativeCheckIsRecorded(t *testing.T) {
for _, id := range packagedAddons {
p, err := loadPlatforms(filepath.Join("..", "..", "..", "addons", id, "package", "platforms.yaml"))
if err != nil {
t.Fatal(err)
}
var enabled []string
for _, e := range p.Platforms {
if e.Enabled {
enabled = append(enabled, e.Platform)
}
}
var recorded []string
for platform := range nativeChecked {
recorded = append(recorded, platform)
}
sort.Strings(enabled)
sort.Strings(recorded)
if len(enabled) != len(recorded) {
t.Fatalf("%s: enabled %v, native check recorded for %v", id, enabled, recorded)
}
for i := range enabled {
if enabled[i] != recorded[i] {
t.Fatalf("%s: enabled %v, native check recorded for %v", id, enabled, recorded)
}
}
}
}
It("enables a platform exactly when its native check is recorded", func() {
p, err := loadPlatforms(filepath.Join("..", "..", "..", "addons", id, "package", "platforms.yaml"))
Expect(err).ToNot(HaveOccurred())
var enabled []string
for _, e := range p.Platforms {
if e.Enabled {
enabled = append(enabled, e.Platform)
}
}
var recorded []string
for platform := range nativeChecked {
recorded = append(recorded, platform)
}
sort.Strings(enabled)
sort.Strings(recorded)
Expect(enabled).To(Equal(recorded), "enabled platforms and recorded native checks differ")
})
func TestEveryPackagedAddonHasACatalogTemplate(t *testing.T) {
for _, id := range packagedAddons {
b, err := os.ReadFile(filepath.Join("..", "..", "..", "gallery", "addons", id+".yaml.tmpl"))
if err != nil {
t.Fatalf("%s: %v", id, err)
}
for _, want := range []string{`addonId: "{{addon}}"`, "requireSignature: true"} {
if !strings.Contains(string(b), want) {
t.Fatalf("%s template lacks %q", id, want)
}
}
It("has a catalog template", func() {
b, err := os.ReadFile(filepath.Join("..", "..", "..", "gallery", "addons", id+".yaml.tmpl"))
Expect(err).ToNot(HaveOccurred())
for _, want := range []string{`addonId: "{{addon}}"`, "requireSignature: true"} {
Expect(strings.Contains(string(b), want)).To(BeTrue(), "template lacks %q", want)
}
})
})
}
}
})