fix(connectors): preserve disabled add-on discovery (#13030)

fix(connectors): preserve discovery when add-ons are disabled

Return healthy empty connector discovery for explicitly disabled add-ons while keeping missing configured gateways and unreachable nonowners unavailable. Cover offline Telegram persistence through the real connector transport and synchronize the exact lifecycle/apps test inventories.

Assisted-by: nib:gpt-6.1-sol

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
mudler-agentandEttore Di Giacinto authored and GitHub committed 2026-11-02 08:36:45 +01:00
1 parent 9206959564
commit a6589e4e60
3 files changed
+75 -3

No files matched your search

@@ -16,6 +16,7 @@ import (
"sync/atomic"
"time"
"github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/http/auth"
"github.com/mudler/LocalAI/core/services/addons"
"github.com/mudler/LocalAI/core/services/addons/gateway"
@@ -109,6 +110,71 @@ func (s *ownerConnectorSource) requests() []contract.ConnectorRequest {
}
var _ = Describe("connector owner routing", func() {
It("reports healthy empty connector kinds when add-ons are explicitly disabled", func() {
app := &Application{applicationConfig: &config.ApplicationConfig{}}
Expect(app.startAddons()).To(Succeed())
Expect(app.addonGateway).To(BeNil())
kinds, err := (connectorTransport{app}).Kinds(context.Background())
Expect(err).NotTo(HaveOccurred())
Expect(kinds).To(BeEmpty())
})
It("persists an offline Telegram connector with the real transport when add-ons are disabled", func() {
ctx := context.Background()
app := &Application{applicationConfig: &config.ApplicationConfig{}}
Expect(app.startAddons()).To(Succeed())
tr := connectorTransport{app}
chatStore, err := store.NewJSON("")
Expect(err).NotTo(HaveOccurred())
agentStore, err := agents.NewJSON("")
Expect(err).NotTo(HaveOccurred())
svc, err := studio.New(studio.Deps{Store: chatStore, Agents: agentStore, Provider: provider.New(), Harness: studiotest.NewGateHarness(), StateDir: GinkgoT().TempDir()})
Expect(err).NotTo(HaveOccurred())
DeferCleanup(func() { Expect(svc.Close(ctx)).To(Succeed()) })
dir := GinkgoT().TempDir()
bindings, err := connectors.NewJSON(dir)
Expect(err).NotTo(HaveOccurred())
client, err := studio.NewConnectors(svc, studio.ConnectorOptions{Store: bindings, Transport: tr, VerifyToken: studio.VerifyTokenWithMe(tr)})
Expect(err).NotTo(HaveOccurred())
DeferCleanup(client.Close)
ag, err := svc.SaveAgent(ctx, studio.AnonymousPrincipal, agents.Agent{Name: "Offline", Model: "fake"})
Expect(err).NotTo(HaveOccurred())
created, err := client.CreateConnector(ctx, studio.AnonymousPrincipal, studio.ConnectorInput{Kind: connectors.KindTelegram, Name: "Telegram", AgentID: ag.ID, Token: "offline-secret", AcceptAny: true})
Expect(err).NotTo(HaveOccurred())
Expect(created.Active).To(BeFalse())
reopened, err := connectors.NewJSON(dir)
Expect(err).NotTo(HaveOccurred())
persisted, err := reopened.GetBinding(ctx, created.ID)
Expect(err).NotTo(HaveOccurred())
Expect(persisted.Kind).To(Equal(connectors.KindTelegram))
Expect(persisted.AddonID).To(Equal("telegram"))
Expect(persisted.AgentID).To(Equal(ag.ID))
Expect(persisted.Token).NotTo(BeEmpty())
})
It("keeps configured missing gateways and unreachable nonowners unavailable", func() {
ctx := context.Background()
for _, app := range []*Application{
{}, // A fixture without config is not an explicitly disabled install.
{applicationConfig: &config.ApplicationConfig{Addons: config.AddonConfig{Enabled: true, InstanceID: "owner", OwnerID: "owner"}}},
{applicationConfig: &config.ApplicationConfig{Addons: config.AddonConfig{Enabled: true, Frontend: true, InstanceID: "frontend", OwnerID: "owner"}}},
} {
_, err := (connectorTransport{app}).Kinds(ctx)
Expect(err).To(MatchError(gateway.ErrUnavailable))
}
keys := filepath.Join(GinkgoT().TempDir(), "keys")
Expect(gateway.GenerateCarrierKeys(keys, []string{"127.0.0.1"})).To(Succeed())
server := httptest.NewServer(http.NotFoundHandler())
server.Close()
cfg, err := gateway.LoadCarrierConfig(keys, "frontend", strings.Replace(server.URL, "http://", "https://", 1))
Expect(err).NotTo(HaveOccurred())
carrier, err := gateway.NewCarrier(*cfg)
Expect(err).NotTo(HaveOccurred())
app := &Application{applicationConfig: &config.ApplicationConfig{Addons: config.AddonConfig{Enabled: true, Frontend: true}}, addonGateway: &gateway.Gateway{Forward: carrier}}
_, err = (connectorTransport{app}).Kinds(ctx)
Expect(err).To(MatchError(gateway.ErrUnavailable))
})
It("uses real peer admission, owner authority and stored credentials through a runtime-free frontend", func() {
ctx := context.Background()
keys := filepath.Join(GinkgoT().TempDir(), "keys")
+3
View File
@@ -36,6 +36,9 @@ type connectorTransport struct{ a *Application }
func (t connectorTransport) Kinds(ctx context.Context) ([]studio.ConnectorKind, error) {
g := t.a.addonGateway
if g == nil {
if cfg := t.a.ApplicationConfig(); cfg != nil && !cfg.Addons.Enabled {
return []studio.ConnectorKind{}, nil
}
return nil, gateway.ErrUnavailable
}
return g.ConnectorKinds(ctx)
+6 -3
View File
@@ -487,7 +487,8 @@
"refuses the upgrade when the tag moved again after the check, and installs nothing",
"stamps the first install, keeps the time over an upgrade and stamps it again after an uninstall",
"needs a state root for the owner only",
"starts the enabled add-ons side by side, so the start takes the slowest ones and not the sum"
"starts the enabled add-ons side by side, so the start takes the slowest ones and not the sum",
"distinguishes healthy empty connector discovery from loss of runtime ownership"
],
"report": ".cache/addons/logs/lifecycle.json"
},
@@ -2569,7 +2570,8 @@
"logs the reason behind a generic denial and leaves the response unchanged",
"names every typed cause of a failed install in the 403 of the dispatch route",
"records the chat with the envelope of a read and of an action while it runs",
"sends the add-on an envelope without a new field"
"sends the add-on an envelope without a new field",
"distinguishes healthy empty connector discovery from loss of runtime ownership"
],
"report": ".cache/addons/logs/apps.json",
"reports": [
@@ -2658,7 +2660,8 @@
"does not upgrade to a release the host cannot run",
"does nothing without an update checker",
"keeps the manual upgrade closed to a caller who is not an administrator",
"starts the enabled add-ons side by side, so the start takes the slowest ones and not the sum"
"starts the enabled add-ons side by side, so the start takes the slowest ones and not the sum",
"distinguishes healthy empty connector discovery from loss of runtime ownership"
]
},
{