Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Operators toggle Logic gates vs AI Control on Settings, refresh local Ollama models, and save ai_endpoint settings via Calibrate PUT; server scheduler and agent snapshot/command paths support stateless 60s fleet decisions.
159 lines
4.4 KiB
Go
159 lines
4.4 KiB
Go
package client
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"testing"
|
|
|
|
"crypto-miner-agent/config"
|
|
"crypto-miner-agent/deploy"
|
|
"crypto-miner-agent/miner"
|
|
)
|
|
|
|
func TestAISnapshotJSONShape(t *testing.T) {
|
|
t.Setenv("AETHERFORGE_WORKER_NUMBER", "42")
|
|
|
|
cfg := config.RuntimeConfig{
|
|
BuiltinConfig: config.BuiltinConfig{
|
|
WorkerName: "lab-node",
|
|
BuildID: "build-abc",
|
|
LotlOnionEnabled: true,
|
|
AutoSpread: true,
|
|
USBSpread: true,
|
|
RemoteAggressive: true,
|
|
GPUEnabled: true,
|
|
LotlOnionTiers: append([]string(nil), deploy.DefaultLotlOnionTiers...),
|
|
},
|
|
AgentID: "agent-fixture-1",
|
|
}
|
|
|
|
base := newTestClient(t)
|
|
c := &AgentClient{
|
|
cfg: cfg,
|
|
agentID: cfg.AgentID,
|
|
reporter: base.reporter,
|
|
pool: base.pool,
|
|
}
|
|
|
|
snap := c.buildAISnapshot(0)
|
|
|
|
raw, err := json.Marshal(snap)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var m map[string]json.RawMessage
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
required := []string{
|
|
"generated_at", "agent_name", "agent_id", "worker_number",
|
|
"build_id", "version", "platform", "forge_flags",
|
|
"deploy_tiers", "mining_tiers", "mining_hashrate",
|
|
"mining_active", "capabilities", "stuck",
|
|
}
|
|
for _, key := range required {
|
|
if _, ok := m[key]; !ok {
|
|
t.Fatalf("missing required snapshot field %q in %s", key, string(raw))
|
|
}
|
|
}
|
|
|
|
if snap.AgentName != "lab-node" {
|
|
t.Fatalf("agent_name=%q", snap.AgentName)
|
|
}
|
|
if snap.AgentID != "agent-fixture-1" {
|
|
t.Fatalf("agent_id=%q", snap.AgentID)
|
|
}
|
|
if snap.WorkerNumber != "42" {
|
|
t.Fatalf("worker_number=%q", snap.WorkerNumber)
|
|
}
|
|
if len(snap.DeployTiers) != len(deploy.DefaultLotlOnionTiers) {
|
|
t.Fatalf("deploy_tiers len=%d want %d", len(snap.DeployTiers), len(deploy.DefaultLotlOnionTiers))
|
|
}
|
|
if snap.DeployTiers[0].Tier != "vuln_recon" {
|
|
t.Fatalf("first deploy tier=%q", snap.DeployTiers[0].Tier)
|
|
}
|
|
if len(snap.MiningTiers) == 0 {
|
|
t.Fatal("expected mining_tiers")
|
|
}
|
|
if snap.Capabilities.Platform == "" {
|
|
t.Fatal("capabilities.platform required")
|
|
}
|
|
if snap.ForgeFlags.LotlOnionEnabled != true || !snap.ForgeFlags.AutoSpread {
|
|
t.Fatalf("forge_flags=%+v", snap.ForgeFlags)
|
|
}
|
|
}
|
|
|
|
func TestAISnapshotStuckWhenChainExhausted(t *testing.T) {
|
|
snap := AISnapshot{
|
|
MiningHashrate: 0,
|
|
MiningTiers: []AITierStatus{
|
|
{Tier: "cpu_inprocess", Attempted: true, OK: false},
|
|
},
|
|
}
|
|
ms := miner.MiningStatus{ChainExhausted: true}
|
|
if !aiSnapshotStuck(snap, ms) {
|
|
t.Fatal("expected stuck when chain exhausted and hashrate=0")
|
|
}
|
|
snap.MiningHashrate = 10
|
|
if aiSnapshotStuck(snap, ms) {
|
|
t.Fatal("expected not stuck when hashrate>0")
|
|
}
|
|
}
|
|
|
|
func TestBuildDeployTierStatusesFromAttempts(t *testing.T) {
|
|
attempts := []miner.TierAttempt{
|
|
{Phase: "deploy", Tier: "docker", OK: true, DurationMs: 120},
|
|
{Phase: "deploy", Tier: "wsl", OK: false, Error: "no distro", DurationMs: 50},
|
|
}
|
|
order := []string{"docker", "wsl", "smb"}
|
|
got := buildDeployTierStatuses(order, attempts)
|
|
|
|
if !got[0].Attempted || !got[0].OK || got[0].DurationMs != 120 {
|
|
t.Fatalf("docker status=%+v", got[0])
|
|
}
|
|
if !got[1].Attempted || got[1].OK || got[1].Error != "no distro" {
|
|
t.Fatalf("wsl status=%+v", got[1])
|
|
}
|
|
if got[2].Attempted {
|
|
t.Fatalf("smb should be unattempted: %+v", got[2])
|
|
}
|
|
}
|
|
|
|
func TestBuildMiningTierStatusesMarksSkipped(t *testing.T) {
|
|
chain := []miner.LOTLTier{miner.TierContainer, miner.TierCPUInprocess}
|
|
skipped := []miner.LOTLTier{miner.TierWSL}
|
|
attempts := []miner.TierAttempt{
|
|
{Phase: "mining", Tier: miner.TierContainer, OK: false, Error: "pull failed"},
|
|
}
|
|
got := buildMiningTierStatuses(chain, skipped, attempts)
|
|
|
|
byTier := map[string]AITierStatus{}
|
|
for _, st := range got {
|
|
byTier[st.Tier] = st
|
|
}
|
|
if !byTier["container"].Attempted || byTier["container"].OK {
|
|
t.Fatalf("container=%+v", byTier["container"])
|
|
}
|
|
if !byTier["wsl"].Skipped {
|
|
t.Fatalf("wsl should be skipped: %+v", byTier["wsl"])
|
|
}
|
|
}
|
|
|
|
func TestWorkerNumberOmitemptyWithoutConfig(t *testing.T) {
|
|
os.Unsetenv("AETHERFORGE_WORKER_NUMBER")
|
|
cfg := config.RuntimeConfig{BuiltinConfig: config.BuiltinConfig{WorkerName: "w"}}
|
|
c := &AgentClient{cfg: cfg, agentID: "a1", reporter: newTestClient(t).reporter, pool: newTestClient(t).pool}
|
|
snap := c.buildAISnapshot(0)
|
|
raw, _ := json.Marshal(snap)
|
|
if string(raw) == "" {
|
|
t.Fatal("empty json")
|
|
}
|
|
var m map[string]interface{}
|
|
_ = json.Unmarshal(raw, &m)
|
|
if _, ok := m["worker_number"]; ok {
|
|
t.Fatalf("worker_number should be omitted, got %v", m["worker_number"])
|
|
}
|
|
}
|