mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 18:44:53 +00:00
Refactor: AI provider and executor multi-tenancy support
- Updated AI providers and tests for context/tenant awareness - Refactored tool executor for multi-tenant state handling - Added new tests for Docker control and update tools
This commit is contained in:
parent
f378a36a5e
commit
defe298ddd
8 changed files with 590 additions and 0 deletions
|
|
@ -393,3 +393,78 @@ func TestAnthropicClient_ModelsEndpoint_Invalid(t *testing.T) {
|
|||
t.Errorf("Expected default models endpoint for invalid baseURL, got %s", endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnthropicClient_ChatStream_ToolUse(t *testing.T) {
|
||||
stream := []string{
|
||||
`{"type":"message_start","message":{"usage":{"input_tokens":5}}}`,
|
||||
`{"type":"content_block_start","content_block":{"type":"text"}}`,
|
||||
`{"type":"content_block_delta","delta":{"type":"text_delta","text":"Hi "}}`,
|
||||
`{"type":"content_block_stop"}`,
|
||||
`{"type":"content_block_start","content_block":{"type":"tool_use","id":"tool_1","name":"get_time"}}`,
|
||||
`{"type":"content_block_delta","delta":{"type":"input_json_delta","partial_json":"{\"tz\":\"UTC\"}"}}`,
|
||||
`{"type":"content_block_stop"}`,
|
||||
`{"type":"message_delta","delta":{"stop_reason":"tool_use"},"usage":{"output_tokens":7}}`,
|
||||
`{"type":"message_stop"}`,
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
for _, event := range stream {
|
||||
_, _ = w.Write([]byte("data: " + event + "\n\n"))
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewAnthropicClientWithBaseURL("test-key", "claude-3-5-sonnet", server.URL, 0)
|
||||
|
||||
var content string
|
||||
var done DoneEvent
|
||||
var doneCalled bool
|
||||
var toolStarts int
|
||||
|
||||
err := client.ChatStream(context.Background(), ChatRequest{
|
||||
Messages: []Message{{Role: "user", Content: "Hi"}},
|
||||
}, func(event StreamEvent) {
|
||||
switch event.Type {
|
||||
case "content":
|
||||
if data, ok := event.Data.(ContentEvent); ok {
|
||||
content += data.Text
|
||||
}
|
||||
case "tool_start":
|
||||
toolStarts++
|
||||
case "done":
|
||||
if data, ok := event.Data.(DoneEvent); ok {
|
||||
done = data
|
||||
doneCalled = true
|
||||
}
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ChatStream: %v", err)
|
||||
}
|
||||
|
||||
if content != "Hi " {
|
||||
t.Fatalf("content = %q", content)
|
||||
}
|
||||
if toolStarts != 1 {
|
||||
t.Fatalf("toolStarts = %d, want 1", toolStarts)
|
||||
}
|
||||
if !doneCalled {
|
||||
t.Fatalf("done event not called")
|
||||
}
|
||||
if done.StopReason != "tool_use" || len(done.ToolCalls) != 1 {
|
||||
t.Fatalf("unexpected done: %+v", done)
|
||||
}
|
||||
if done.ToolCalls[0].Name != "get_time" || done.InputTokens != 5 || done.OutputTokens != 7 {
|
||||
t.Fatalf("unexpected tool call or usage: %+v", done)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnthropicClient_SupportsThinking(t *testing.T) {
|
||||
client := NewAnthropicClient("test-key", "claude-3-5-sonnet", 0)
|
||||
if client.SupportsThinking("claude-3-5-sonnet") {
|
||||
t.Fatal("expected SupportsThinking to be false")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,76 @@ func TestGeminiClient_Name(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGeminiClient_ChatStream(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.Contains(r.URL.Path, "streamGenerateContent") {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
events := []string{
|
||||
`{"candidates":[{"content":{"parts":[{"text":"Hello"}]}}],"usageMetadata":{"promptTokenCount":2,"candidatesTokenCount":3}}`,
|
||||
`{"candidates":[{"content":{"parts":[{"functionCall":{"name":"get_time","args":{"tz":"UTC"}}}]},"finishReason":"STOP"}]}`,
|
||||
}
|
||||
|
||||
for _, event := range events {
|
||||
w.Write([]byte("data: " + event + "\n\n"))
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewGeminiClient("test-key", "gemini-pro", server.URL, 0)
|
||||
|
||||
var content string
|
||||
var done DoneEvent
|
||||
var doneCalled bool
|
||||
var toolStarts int
|
||||
|
||||
err := client.ChatStream(context.Background(), ChatRequest{
|
||||
Messages: []Message{{Role: "user", Content: "Hi"}},
|
||||
}, func(event StreamEvent) {
|
||||
switch event.Type {
|
||||
case "content":
|
||||
if data, ok := event.Data.(ContentEvent); ok {
|
||||
content += data.Text
|
||||
}
|
||||
case "tool_start":
|
||||
toolStarts++
|
||||
case "done":
|
||||
if data, ok := event.Data.(DoneEvent); ok {
|
||||
done = data
|
||||
doneCalled = true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("ChatStream: %v", err)
|
||||
}
|
||||
if content != "Hello" {
|
||||
t.Fatalf("content = %q", content)
|
||||
}
|
||||
if toolStarts != 1 {
|
||||
t.Fatalf("toolStarts = %d, want 1", toolStarts)
|
||||
}
|
||||
if !doneCalled {
|
||||
t.Fatalf("done event not called")
|
||||
}
|
||||
if done.StopReason != "tool_use" || len(done.ToolCalls) != 1 {
|
||||
t.Fatalf("unexpected done: %+v", done)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeminiClient_SupportsThinking(t *testing.T) {
|
||||
client := NewGeminiClient("key", "gemini-pro", "", 0)
|
||||
if client.SupportsThinking("gemini-pro") {
|
||||
t.Fatal("expected SupportsThinking to be false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeminiClient_Chat_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
|
|
|
|||
|
|
@ -71,3 +71,43 @@ func TestIsNotable_Integration(t *testing.T) {
|
|||
t.Logf("Cache size: %d", len(cache.data))
|
||||
cache.mu.RUnlock()
|
||||
}
|
||||
|
||||
func TestGetModelFamilyAliases(t *testing.T) {
|
||||
tests := []struct {
|
||||
model string
|
||||
want []string
|
||||
}{
|
||||
{"claude-3-5-haiku", []string{"claude-haiku-4-5", "claude-3-5-haiku-latest"}},
|
||||
{"claude-3-5-sonnet", []string{"claude-sonnet-4-5", "claude-3-5-sonnet-latest"}},
|
||||
{"claude-3-opus", []string{"claude-opus-4-5", "claude-opus-4-1", "claude-opus-4-0"}},
|
||||
{"gpt-4o", []string{"gpt-4o", "gpt-4o-2024-11-20"}},
|
||||
{"o1-mini", []string{"o1", "o1-pro", "o3-mini"}},
|
||||
{"unknown", nil},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := getModelFamilyAliases(tt.model)
|
||||
if len(tt.want) == 0 && len(got) != 0 {
|
||||
t.Fatalf("model %s: expected no aliases, got %+v", tt.model, got)
|
||||
}
|
||||
if len(tt.want) > 0 && len(got) != len(tt.want) {
|
||||
t.Fatalf("model %s: expected %d aliases, got %d", tt.model, len(tt.want), len(got))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFlexibleDate_Invalid(t *testing.T) {
|
||||
_, err := parseFlexibleDate("not-a-date")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid date")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsRecentlyReleased(t *testing.T) {
|
||||
if !isRecentlyReleased("2100-01-01", "") {
|
||||
t.Fatal("expected future release date to be notable")
|
||||
}
|
||||
if isRecentlyReleased("2000-01-01", "") {
|
||||
t.Fatal("expected old release date to be not notable")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,3 +157,74 @@ func TestNewOllamaClient_Normalization(t *testing.T) {
|
|||
assert.Equal(t, tt.expect, client.baseURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOllamaClient_Chat_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/chat", r.URL.Path)
|
||||
|
||||
var req ollamaRequest
|
||||
require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
|
||||
assert.False(t, req.Stream)
|
||||
assert.Equal(t, "llama3", req.Model)
|
||||
require.Len(t, req.Tools, 1)
|
||||
assert.Equal(t, "function", req.Tools[0].Type)
|
||||
assert.Equal(t, "get_time", req.Tools[0].Function.Name)
|
||||
|
||||
_ = json.NewEncoder(w).Encode(ollamaResponse{
|
||||
Model: "llama3",
|
||||
Message: ollamaMessageResp{
|
||||
Role: "assistant",
|
||||
Content: "Hello",
|
||||
ToolCalls: []ollamaToolCall{
|
||||
{Function: ollamaFunctionCall{Name: "get_time", Arguments: map[string]interface{}{"tz": "UTC"}}},
|
||||
},
|
||||
},
|
||||
DoneReason: "stop",
|
||||
PromptEvalCount: 2,
|
||||
EvalCount: 3,
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewOllamaClient("llama3", server.URL, 0)
|
||||
resp, err := client.Chat(context.Background(), ChatRequest{
|
||||
Messages: []Message{{Role: "user", Content: "Hi"}},
|
||||
Tools: []Tool{
|
||||
{
|
||||
Name: "get_time",
|
||||
Description: "get time",
|
||||
InputSchema: map[string]interface{}{"type": "object"},
|
||||
},
|
||||
{
|
||||
Type: "web_search_20250305",
|
||||
Name: "web_search",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Hello", resp.Content)
|
||||
assert.Equal(t, "tool_use", resp.StopReason)
|
||||
require.Len(t, resp.ToolCalls, 1)
|
||||
assert.Equal(t, "get_time", resp.ToolCalls[0].Name)
|
||||
assert.Equal(t, 2, resp.InputTokens)
|
||||
assert.Equal(t, 3, resp.OutputTokens)
|
||||
}
|
||||
|
||||
func TestOllamaClient_TestConnection(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/api/version", r.URL.Path)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewOllamaClient("llama3", server.URL, 0)
|
||||
err := client.TestConnection(context.Background())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestOllamaClient_SupportsThinking(t *testing.T) {
|
||||
client := NewOllamaClient("llama3", "http://localhost:11434", 0)
|
||||
if client.SupportsThinking("llama3") {
|
||||
t.Fatal("expected SupportsThinking to be false")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -213,3 +213,129 @@ func TestOpenAIClient_ListModels(t *testing.T) {
|
|||
assert.Equal(t, "gpt-4", models[0].ID)
|
||||
assert.Equal(t, "gpt-3.5-turbo", models[1].ID)
|
||||
}
|
||||
|
||||
func TestOpenAIClient_Chat_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/v1/chat/completions", r.URL.Path)
|
||||
assert.Equal(t, "Bearer sk-test", r.Header.Get("Authorization"))
|
||||
|
||||
var req openaiRequest
|
||||
require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
|
||||
assert.Equal(t, "gpt-4", req.Model)
|
||||
assert.Equal(t, 123, req.MaxCompletionTokens)
|
||||
assert.Equal(t, 0.7, req.Temperature)
|
||||
require.Len(t, req.Tools, 1)
|
||||
assert.Equal(t, "function", req.Tools[0].Type)
|
||||
assert.Equal(t, "get_time", req.Tools[0].Function.Name)
|
||||
assert.Equal(t, "auto", req.ToolChoice)
|
||||
|
||||
_ = json.NewEncoder(w).Encode(openaiResponse{
|
||||
ID: "chatcmpl-1",
|
||||
Model: "gpt-4",
|
||||
Choices: []openaiChoice{
|
||||
{
|
||||
Message: openaiRespMsg{Role: "assistant", Content: "Hello"},
|
||||
FinishReason: "stop",
|
||||
},
|
||||
},
|
||||
Usage: openaiUsage{PromptTokens: 2, CompletionTokens: 3},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewOpenAIClient("sk-test", "gpt-4", server.URL, 0)
|
||||
resp, err := client.Chat(context.Background(), ChatRequest{
|
||||
System: "You are helpful",
|
||||
MaxTokens: 123,
|
||||
Temperature: 0.7,
|
||||
Messages: []Message{
|
||||
{Role: "user", Content: "Hi"},
|
||||
},
|
||||
Tools: []Tool{
|
||||
{
|
||||
Name: "get_time",
|
||||
Description: "get time",
|
||||
InputSchema: map[string]interface{}{"type": "object"},
|
||||
},
|
||||
{
|
||||
Type: "web_search_20250305",
|
||||
Name: "web_search",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Hello", resp.Content)
|
||||
assert.Equal(t, 2, resp.InputTokens)
|
||||
assert.Equal(t, 3, resp.OutputTokens)
|
||||
}
|
||||
|
||||
func TestOpenAIClient_Chat_GPT52NonChat(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/v1/chat/completions", r.URL.Path)
|
||||
|
||||
var req openaiCompletionsRequest
|
||||
require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
|
||||
assert.Equal(t, "gpt-5.2-pro", req.Model)
|
||||
assert.Contains(t, req.Prompt, "System: sys")
|
||||
assert.Contains(t, req.Prompt, "user: hi")
|
||||
assert.Equal(t, 55, req.MaxCompletionTokens)
|
||||
|
||||
_ = json.NewEncoder(w).Encode(openaiResponse{
|
||||
ID: "cmpl-1",
|
||||
Model: "gpt-5.2-pro",
|
||||
Choices: []openaiChoice{
|
||||
{
|
||||
Text: "Answer",
|
||||
FinishReason: "stop",
|
||||
},
|
||||
},
|
||||
Usage: openaiUsage{PromptTokens: 3, CompletionTokens: 4},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewOpenAIClient("sk-test", "gpt-5.2-pro", server.URL, 0)
|
||||
resp, err := client.Chat(context.Background(), ChatRequest{
|
||||
System: "sys",
|
||||
MaxTokens: 55,
|
||||
Messages: []Message{
|
||||
{Role: "user", Content: "hi"},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Answer", resp.Content)
|
||||
}
|
||||
|
||||
func TestOpenAIClient_HelperFlags(t *testing.T) {
|
||||
client := NewOpenAIClient("sk", "gpt-4", "https://api.openai.com", 0)
|
||||
assert.True(t, client.requiresMaxCompletionTokens("o1-mini"))
|
||||
assert.False(t, client.requiresMaxCompletionTokens("gpt-4"))
|
||||
|
||||
assert.True(t, client.isGPT52NonChat("gpt-5.2-pro"))
|
||||
assert.False(t, client.isGPT52NonChat("gpt-5.2-chat-latest"))
|
||||
}
|
||||
|
||||
func TestOpenAIClient_SupportsThinking(t *testing.T) {
|
||||
client := NewOpenAIClient("sk", "deepseek-reasoner", "https://api.deepseek.com", 0)
|
||||
assert.True(t, client.SupportsThinking("deepseek-reasoner"))
|
||||
|
||||
client = NewOpenAIClient("sk", "gpt-4", "https://api.openai.com", 0)
|
||||
assert.False(t, client.SupportsThinking("gpt-4"))
|
||||
}
|
||||
|
||||
func TestOpenAIClient_TestConnection(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/v1/models", r.URL.Path)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"data": []map[string]interface{}{
|
||||
{"id": "gpt-4", "object": "model", "created": 123, "owned_by": "openai"},
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := NewOpenAIClient("sk-test", "gpt-4", server.URL, 0)
|
||||
err := client.TestConnection(context.Background())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
|
|
|||
130
internal/ai/tools/control_docker_helpers_test.go
Normal file
130
internal/ai/tools/control_docker_helpers_test.go
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
package tools
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/agentexec"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestResolveDockerContainer(t *testing.T) {
|
||||
t.Run("NoStateProvider", func(t *testing.T) {
|
||||
exec := NewPulseToolExecutor(ExecutorConfig{})
|
||||
_, _, err := exec.resolveDockerContainer("web", "")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "state provider not available")
|
||||
})
|
||||
|
||||
t.Run("HostMatch", func(t *testing.T) {
|
||||
state := models.StateSnapshot{
|
||||
DockerHosts: []models.DockerHost{
|
||||
{
|
||||
ID: "host1",
|
||||
Hostname: "dock1",
|
||||
Containers: []models.DockerContainer{
|
||||
{ID: "abc123", Name: "web"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
exec := NewPulseToolExecutor(ExecutorConfig{StateProvider: &mockStateProvider{state: state}})
|
||||
|
||||
container, host, err := exec.resolveDockerContainer("web", "dock1")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "web", container.Name)
|
||||
assert.Equal(t, "host1", host.ID)
|
||||
})
|
||||
|
||||
t.Run("HostNotFound", func(t *testing.T) {
|
||||
state := models.StateSnapshot{
|
||||
DockerHosts: []models.DockerHost{
|
||||
{
|
||||
ID: "host1",
|
||||
Hostname: "dock1",
|
||||
Containers: []models.DockerContainer{
|
||||
{ID: "abc123", Name: "web"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
exec := NewPulseToolExecutor(ExecutorConfig{StateProvider: &mockStateProvider{state: state}})
|
||||
|
||||
_, _, err := exec.resolveDockerContainer("web", "missing")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not found on host")
|
||||
})
|
||||
|
||||
t.Run("MultipleHosts", func(t *testing.T) {
|
||||
state := models.StateSnapshot{
|
||||
DockerHosts: []models.DockerHost{
|
||||
{
|
||||
ID: "host1",
|
||||
DisplayName: "Dock One",
|
||||
Containers: []models.DockerContainer{
|
||||
{ID: "abcdef", Name: "web"},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "host2",
|
||||
DisplayName: "Dock Two",
|
||||
Containers: []models.DockerContainer{
|
||||
{ID: "abc999", Name: "web"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
exec := NewPulseToolExecutor(ExecutorConfig{StateProvider: &mockStateProvider{state: state}})
|
||||
|
||||
_, _, err := exec.resolveDockerContainer("web", "")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "multiple Docker hosts")
|
||||
assert.Contains(t, err.Error(), "Dock One")
|
||||
})
|
||||
|
||||
t.Run("IDPrefixMatch", func(t *testing.T) {
|
||||
state := models.StateSnapshot{
|
||||
DockerHosts: []models.DockerHost{
|
||||
{
|
||||
ID: "host1",
|
||||
Hostname: "dock1",
|
||||
Containers: []models.DockerContainer{
|
||||
{ID: "abcdef", Name: "web"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
exec := NewPulseToolExecutor(ExecutorConfig{StateProvider: &mockStateProvider{state: state}})
|
||||
|
||||
container, _, err := exec.resolveDockerContainer("abc", "")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "abcdef", container.ID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetAgentHostnameForDockerHost(t *testing.T) {
|
||||
t.Run("NoAgentServer", func(t *testing.T) {
|
||||
exec := NewPulseToolExecutor(ExecutorConfig{})
|
||||
host := &models.DockerHost{Hostname: "dock1"}
|
||||
assert.Equal(t, "dock1", exec.getAgentHostnameForDockerHost(host))
|
||||
})
|
||||
|
||||
t.Run("AgentIDMatch", func(t *testing.T) {
|
||||
agentSrv := &mockAgentServer{agents: []agentexec.ConnectedAgent{
|
||||
{AgentID: "agent1", Hostname: "node1"},
|
||||
}}
|
||||
exec := NewPulseToolExecutor(ExecutorConfig{AgentServer: agentSrv})
|
||||
host := &models.DockerHost{AgentID: "agent1", Hostname: "dock1"}
|
||||
assert.Equal(t, "node1", exec.getAgentHostnameForDockerHost(host))
|
||||
})
|
||||
|
||||
t.Run("FallbackHostname", func(t *testing.T) {
|
||||
agentSrv := &mockAgentServer{agents: []agentexec.ConnectedAgent{
|
||||
{AgentID: "agent2", Hostname: "node2"},
|
||||
}}
|
||||
exec := NewPulseToolExecutor(ExecutorConfig{AgentServer: agentSrv})
|
||||
host := &models.DockerHost{AgentID: "agent1", Hostname: "dock1"}
|
||||
assert.Equal(t, "dock1", exec.getAgentHostnameForDockerHost(host))
|
||||
})
|
||||
}
|
||||
77
internal/ai/tools/docker_updates_test.go
Normal file
77
internal/ai/tools/docker_updates_test.go
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExecuteCheckDockerUpdates(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
exec := NewPulseToolExecutor(ExecutorConfig{})
|
||||
result, err := exec.executeCheckDockerUpdates(ctx, map[string]interface{}{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Docker update checking not available. Ensure updates provider is configured.", result.Content[0].Text)
|
||||
|
||||
updatesProvider := &mockUpdatesProvider{}
|
||||
exec = NewPulseToolExecutor(ExecutorConfig{UpdatesProvider: updatesProvider})
|
||||
result, err = exec.executeCheckDockerUpdates(ctx, map[string]interface{}{})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.IsError)
|
||||
assert.Contains(t, result.Content[0].Text, "host is required")
|
||||
|
||||
state := models.StateSnapshot{
|
||||
DockerHosts: []models.DockerHost{
|
||||
{ID: "host1", Hostname: "dock1", DisplayName: "Dock One"},
|
||||
},
|
||||
}
|
||||
|
||||
exec = NewPulseToolExecutor(ExecutorConfig{
|
||||
UpdatesProvider: updatesProvider,
|
||||
StateProvider: &mockStateProvider{state: state},
|
||||
ControlLevel: ControlLevelSuggest,
|
||||
})
|
||||
result, err = exec.executeCheckDockerUpdates(ctx, map[string]interface{}{
|
||||
"host": "dock1",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, result.Content[0].Text, "POST /api/agents/docker/hosts/host1/check-updates")
|
||||
|
||||
updatesProvider.On("TriggerUpdateCheck", "host1").Return(DockerCommandStatus{
|
||||
ID: "cmd1",
|
||||
Type: "check",
|
||||
Status: "queued",
|
||||
}, nil).Once()
|
||||
|
||||
exec = NewPulseToolExecutor(ExecutorConfig{
|
||||
UpdatesProvider: updatesProvider,
|
||||
StateProvider: &mockStateProvider{state: state},
|
||||
})
|
||||
result, err = exec.executeCheckDockerUpdates(ctx, map[string]interface{}{
|
||||
"host": "dock1",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
var resp DockerCheckUpdatesResponse
|
||||
require.NoError(t, json.Unmarshal([]byte(result.Content[0].Text), &resp))
|
||||
assert.Equal(t, "host1", resp.HostID)
|
||||
assert.Equal(t, "Dock One", resp.HostName)
|
||||
assert.Equal(t, "cmd1", resp.CommandID)
|
||||
|
||||
updatesProvider.On("TriggerUpdateCheck", "host1").Return(DockerCommandStatus{}, errors.New("boom")).Once()
|
||||
exec = NewPulseToolExecutor(ExecutorConfig{
|
||||
UpdatesProvider: updatesProvider,
|
||||
StateProvider: &mockStateProvider{state: state},
|
||||
})
|
||||
result, err = exec.executeCheckDockerUpdates(ctx, map[string]interface{}{
|
||||
"host": "dock1",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, result.Content[0].Text, "Failed to trigger update check")
|
||||
}
|
||||
|
|
@ -189,6 +189,7 @@ func NewPulseToolExecutor(cfg ExecutorConfig) *PulseToolExecutor {
|
|||
backupProvider: cfg.BackupProvider,
|
||||
storageProvider: cfg.StorageProvider,
|
||||
diskHealthProvider: cfg.DiskHealthProvider,
|
||||
updatesProvider: cfg.UpdatesProvider,
|
||||
metadataUpdater: cfg.MetadataUpdater,
|
||||
findingsManager: cfg.FindingsManager,
|
||||
agentProfileManager: cfg.AgentProfileManager,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue