Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v5) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v6) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v7) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
POEditor import / update-translations (push) Has been cancelled
282 lines
8.7 KiB
Go
282 lines
8.7 KiB
Go
package agents
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/utils/slice"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
// MockPluginLoader implements PluginLoader for testing
|
|
type MockPluginLoader struct {
|
|
pluginNames []string
|
|
loadedAgents map[string]*MockAgent
|
|
pluginCallCount map[string]int
|
|
}
|
|
|
|
func NewMockPluginLoader() *MockPluginLoader {
|
|
return &MockPluginLoader{
|
|
pluginNames: []string{},
|
|
loadedAgents: make(map[string]*MockAgent),
|
|
pluginCallCount: make(map[string]int),
|
|
}
|
|
}
|
|
|
|
func (m *MockPluginLoader) PluginNames(serviceName string) []string {
|
|
return m.pluginNames
|
|
}
|
|
|
|
func (m *MockPluginLoader) LoadMediaAgent(name string) (Interface, bool) {
|
|
m.pluginCallCount[name]++
|
|
agent, exists := m.loadedAgents[name]
|
|
return agent, exists
|
|
}
|
|
|
|
// MockAgent is a mock agent implementation for testing
|
|
type MockAgent struct {
|
|
name string
|
|
mbid string
|
|
}
|
|
|
|
func (m *MockAgent) AgentName() string {
|
|
return m.name
|
|
}
|
|
|
|
func (m *MockAgent) GetArtistMBID(ctx context.Context, id string, name string) (string, error) {
|
|
return m.mbid, nil
|
|
}
|
|
|
|
var _ Interface = (*MockAgent)(nil)
|
|
var _ ArtistMBIDRetriever = (*MockAgent)(nil)
|
|
|
|
var _ PluginLoader = (*MockPluginLoader)(nil)
|
|
|
|
var _ = Describe("Agents with Plugin Loading", func() {
|
|
var mockLoader *MockPluginLoader
|
|
var agents *Agents
|
|
|
|
BeforeEach(func() {
|
|
mockLoader = NewMockPluginLoader()
|
|
|
|
// Create the agents instance with our mock loader
|
|
agents = createAgents(nil, mockLoader)
|
|
})
|
|
|
|
Context("Dynamic agent discovery", func() {
|
|
It("should include ONLY local agent when no config is specified", func() {
|
|
// Ensure no specific agents are configured
|
|
conf.Server.Agents = ""
|
|
|
|
// Add some plugin agents that should be ignored
|
|
mockLoader.pluginNames = append(mockLoader.pluginNames, "plugin_agent", "another_plugin")
|
|
|
|
// Should only include the local agent
|
|
enabledAgents := agents.getEnabledAgentNames()
|
|
Expect(enabledAgents).To(HaveLen(1))
|
|
Expect(enabledAgents[0].name).To(Equal(LocalAgentName))
|
|
Expect(enabledAgents[0].isPlugin).To(BeFalse()) // LocalAgent is built-in, not plugin
|
|
})
|
|
|
|
It("should NOT include plugin agents when no config is specified", func() {
|
|
// Ensure no specific agents are configured
|
|
conf.Server.Agents = ""
|
|
|
|
// Add a plugin agent
|
|
mockLoader.pluginNames = append(mockLoader.pluginNames, "plugin_agent")
|
|
|
|
// Should only include the local agent
|
|
enabledAgents := agents.getEnabledAgentNames()
|
|
Expect(enabledAgents).To(HaveLen(1))
|
|
Expect(enabledAgents[0].name).To(Equal(LocalAgentName))
|
|
Expect(enabledAgents[0].isPlugin).To(BeFalse()) // LocalAgent is built-in, not plugin
|
|
})
|
|
|
|
It("should include plugin agents in the enabled agents list ONLY when explicitly configured", func() {
|
|
// Add a plugin agent
|
|
mockLoader.pluginNames = append(mockLoader.pluginNames, "plugin_agent")
|
|
|
|
// With no config, should not include plugin
|
|
conf.Server.Agents = ""
|
|
enabledAgents := agents.getEnabledAgentNames()
|
|
Expect(enabledAgents).To(HaveLen(1))
|
|
Expect(enabledAgents[0].name).To(Equal(LocalAgentName))
|
|
|
|
// When explicitly configured, should include plugin
|
|
conf.Server.Agents = "plugin_agent"
|
|
enabledAgents = agents.getEnabledAgentNames()
|
|
var agentNames []string
|
|
var pluginAgentFound bool
|
|
for _, agent := range enabledAgents {
|
|
agentNames = append(agentNames, agent.name)
|
|
if agent.name == "plugin_agent" {
|
|
pluginAgentFound = true
|
|
Expect(agent.isPlugin).To(BeTrue()) // plugin_agent is a plugin
|
|
}
|
|
}
|
|
Expect(agentNames).To(ContainElements(LocalAgentName, "plugin_agent"))
|
|
Expect(pluginAgentFound).To(BeTrue())
|
|
})
|
|
|
|
It("should only include configured plugin agents when config is specified", func() {
|
|
// Add two plugin agents
|
|
mockLoader.pluginNames = append(mockLoader.pluginNames, "plugin_one", "plugin_two")
|
|
|
|
// Configure only one of them
|
|
conf.Server.Agents = "plugin_one"
|
|
|
|
// Verify only the configured one is included
|
|
enabledAgents := agents.getEnabledAgentNames()
|
|
var agentNames []string
|
|
var pluginOneFound bool
|
|
for _, agent := range enabledAgents {
|
|
agentNames = append(agentNames, agent.name)
|
|
if agent.name == "plugin_one" {
|
|
pluginOneFound = true
|
|
Expect(agent.isPlugin).To(BeTrue()) // plugin_one is a plugin
|
|
}
|
|
}
|
|
Expect(agentNames).To(ContainElements(LocalAgentName, "plugin_one"))
|
|
Expect(agentNames).NotTo(ContainElement("plugin_two"))
|
|
Expect(pluginOneFound).To(BeTrue())
|
|
})
|
|
|
|
It("should load plugin agents on demand", func() {
|
|
ctx := context.Background()
|
|
|
|
// Configure to use our plugin
|
|
conf.Server.Agents = "plugin_agent"
|
|
|
|
// Add a plugin agent
|
|
mockLoader.pluginNames = append(mockLoader.pluginNames, "plugin_agent")
|
|
mockLoader.loadedAgents["plugin_agent"] = &MockAgent{
|
|
name: "plugin_agent",
|
|
mbid: "plugin-mbid",
|
|
}
|
|
|
|
// Try to get data from it
|
|
mbid, err := agents.GetArtistMBID(ctx, "123", "Artist")
|
|
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(mbid).To(Equal("plugin-mbid"))
|
|
Expect(mockLoader.pluginCallCount["plugin_agent"]).To(Equal(1))
|
|
})
|
|
|
|
It("should try both built-in and plugin agents", func() {
|
|
// Create a mock built-in agent
|
|
Register("built_in", func(ds model.DataStore) Interface {
|
|
return &MockAgent{
|
|
name: "built_in",
|
|
mbid: "built-in-mbid",
|
|
}
|
|
})
|
|
defer func() {
|
|
delete(Map, "built_in")
|
|
}()
|
|
|
|
// Configure to use both built-in and plugin
|
|
conf.Server.Agents = "built_in,plugin_agent"
|
|
|
|
// Add a plugin agent
|
|
mockLoader.pluginNames = append(mockLoader.pluginNames, "plugin_agent")
|
|
mockLoader.loadedAgents["plugin_agent"] = &MockAgent{
|
|
name: "plugin_agent",
|
|
mbid: "plugin-mbid",
|
|
}
|
|
|
|
// Verify that both are in the enabled list
|
|
enabledAgents := agents.getEnabledAgentNames()
|
|
var agentNames []string
|
|
var builtInFound, pluginFound bool
|
|
for _, agent := range enabledAgents {
|
|
agentNames = append(agentNames, agent.name)
|
|
if agent.name == "built_in" {
|
|
builtInFound = true
|
|
Expect(agent.isPlugin).To(BeFalse()) // built-in agent
|
|
}
|
|
if agent.name == "plugin_agent" {
|
|
pluginFound = true
|
|
Expect(agent.isPlugin).To(BeTrue()) // plugin agent
|
|
}
|
|
}
|
|
Expect(agentNames).To(ContainElements("built_in", "plugin_agent", LocalAgentName))
|
|
Expect(builtInFound).To(BeTrue())
|
|
Expect(pluginFound).To(BeTrue())
|
|
})
|
|
|
|
It("should respect the order specified in configuration", func() {
|
|
// Create mock built-in agents
|
|
Register("agent_a", func(ds model.DataStore) Interface {
|
|
return &MockAgent{name: "agent_a"}
|
|
})
|
|
Register("agent_b", func(ds model.DataStore) Interface {
|
|
return &MockAgent{name: "agent_b"}
|
|
})
|
|
defer func() {
|
|
delete(Map, "agent_a")
|
|
delete(Map, "agent_b")
|
|
}()
|
|
|
|
// Add plugin agents
|
|
mockLoader.pluginNames = append(mockLoader.pluginNames, "plugin_x", "plugin_y")
|
|
|
|
// Configure specific order - plugin first, then built-ins
|
|
conf.Server.Agents = "plugin_y,agent_b,plugin_x,agent_a"
|
|
|
|
// Get the agent names
|
|
enabledAgents := agents.getEnabledAgentNames()
|
|
|
|
// Extract just the names to verify the order
|
|
agentNames := slice.Map(enabledAgents, func(a enabledAgent) string { return a.name })
|
|
|
|
// Verify the order matches configuration, with LocalAgentName at the end
|
|
Expect(agentNames).To(HaveExactElements("plugin_y", "agent_b", "plugin_x", "agent_a", LocalAgentName))
|
|
})
|
|
|
|
It("should NOT call LoadMediaAgent for built-in agents", func() {
|
|
ctx := context.Background()
|
|
|
|
// Create a mock built-in agent
|
|
Register("builtin_agent", func(ds model.DataStore) Interface {
|
|
return &MockAgent{
|
|
name: "builtin_agent",
|
|
mbid: "builtin-mbid",
|
|
}
|
|
})
|
|
defer func() {
|
|
delete(Map, "builtin_agent")
|
|
}()
|
|
|
|
// Configure to use only built-in agents
|
|
conf.Server.Agents = "builtin_agent"
|
|
|
|
// Call GetArtistMBID which should only use the built-in agent
|
|
mbid, err := agents.GetArtistMBID(ctx, "123", "Artist")
|
|
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(mbid).To(Equal("builtin-mbid"))
|
|
|
|
// Verify LoadMediaAgent was NEVER called (no plugin loading for built-in agents)
|
|
Expect(mockLoader.pluginCallCount).To(BeEmpty())
|
|
})
|
|
|
|
It("should NOT call LoadMediaAgent for invalid agent names", func() {
|
|
ctx := context.Background()
|
|
|
|
// Configure with an invalid agent name (not built-in, not a plugin)
|
|
conf.Server.Agents = "invalid_agent"
|
|
|
|
// This should only result in using the local agent (as the invalid one is ignored)
|
|
_, err := agents.GetArtistMBID(ctx, "123", "Artist")
|
|
|
|
// Should get ErrNotFound since only local agent is available and it returns not found for this operation
|
|
Expect(err).To(MatchError(ErrNotFound))
|
|
|
|
// Verify LoadMediaAgent was NEVER called for the invalid agent
|
|
Expect(mockLoader.pluginCallCount).To(BeEmpty())
|
|
})
|
|
})
|
|
})
|