feat: alive UI wave, galaxy presence, spread and fleet enhancements
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Dashboard ambient layer, comrade presence, Mission Deck and War Room, Emberwake supply chain, spread/docs publishing, fleet policy and modules API, CI docker mining, and refreshed USB pack.
This commit is contained in:
128
scripts/ci-docker-mining.ps1
Normal file
128
scripts/ci-docker-mining.ps1
Normal file
@@ -0,0 +1,128 @@
|
||||
# CI Docker mining proof — Linux agent connects and reports hashrate > 0 (Windows + Docker Desktop).
|
||||
param(
|
||||
[string]$BaseUrl = "http://127.0.0.1:18989",
|
||||
[string]$Username = "",
|
||||
[string]$Password = "",
|
||||
[string]$ExpectedWorker = "docker-e2e-linux",
|
||||
[int]$WaitSeconds = 180,
|
||||
[int]$PollIntervalSec = 5,
|
||||
[switch]$SkipTeardown
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
||||
$ComposeFile = Join-Path $Root "docker\docker-compose.yml"
|
||||
|
||||
if (-not $Username) { $Username = if ($env:AETHERFORGE_E2E_USER) { $env:AETHERFORGE_E2E_USER } else { "testuser" } }
|
||||
if (-not $Password) { $Password = if ($env:AETHERFORGE_E2E_PASS) { $env:AETHERFORGE_E2E_PASS } else { "testpass" } }
|
||||
|
||||
$cred = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("${Username}:${Password}"))
|
||||
$authHeaders = @{ Authorization = "Basic $cred" }
|
||||
|
||||
function Write-Log([string]$Message) {
|
||||
Write-Host "[ci-docker-mining] $Message"
|
||||
}
|
||||
|
||||
function Ensure-Docker {
|
||||
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
|
||||
throw "docker not found — install Docker Desktop with compose v2"
|
||||
}
|
||||
docker compose version 2>$null | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "docker compose plugin not available" }
|
||||
}
|
||||
|
||||
function Invoke-Teardown {
|
||||
if ($SkipTeardown) { return }
|
||||
Write-Log "Tearing down compose…"
|
||||
Push-Location $Root
|
||||
try {
|
||||
docker compose -f $ComposeFile down --rmi local -v --remove-orphans 2>$null
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
function Wait-ForHealth {
|
||||
param([datetime]$Deadline)
|
||||
while ((Get-Date) -lt $Deadline) {
|
||||
try {
|
||||
$r = Invoke-RestMethod "$BaseUrl/api/v1/health" -TimeoutSec 5
|
||||
if ($r.status -eq "ok") {
|
||||
Write-Log "Server healthy at $BaseUrl/api/v1/health"
|
||||
return
|
||||
}
|
||||
} catch {}
|
||||
Start-Sleep -Seconds $PollIntervalSec
|
||||
}
|
||||
throw "server not healthy within ${WaitSeconds}s ($BaseUrl/api/v1/health)"
|
||||
}
|
||||
|
||||
function Assert-Mining {
|
||||
param([datetime]$Deadline)
|
||||
while ((Get-Date) -lt $Deadline) {
|
||||
try {
|
||||
$agents = Invoke-RestMethod "$BaseUrl/api/v1/agents" -Headers $authHeaders -TimeoutSec 10
|
||||
$stats = Invoke-RestMethod "$BaseUrl/api/v1/dashboard/stats" -Headers $authHeaders -TimeoutSec 10
|
||||
|
||||
$online = @($agents | Where-Object {
|
||||
$_.status -eq "online" -and (
|
||||
[double]$_.hashrate_15s -gt 0 -or
|
||||
[double]$_.hashrate_1m -gt 0 -or
|
||||
[double]$_.hashrate_15m -gt 0
|
||||
)
|
||||
})
|
||||
|
||||
$named = @($online | Where-Object { $_.worker_name -eq $ExpectedWorker -or $_.name -eq $ExpectedWorker })
|
||||
$hit = if ($named.Count -gt 0) { $named[0] } elseif ($online.Count -gt 0) { $online[0] } else { $null }
|
||||
|
||||
if ($hit) {
|
||||
$hr = [Math]::Max([double]$hit.hashrate_15s, [Math]::Max([double]$hit.hashrate_1m, [double]$hit.hashrate_15m))
|
||||
Write-Log "PASS: online agent $($hit.id) ($($hit.worker_name)) hashrate=$([math]::Round($hr, 2)) H/s"
|
||||
Write-Log "Fleet stats: online=$($stats.online_agents) total_hr=$([math]::Round([double]$stats.total_hashrate, 2))"
|
||||
return
|
||||
}
|
||||
|
||||
Write-Log "Waiting… online_agents=$($stats.online_agents) total_hashrate=$($stats.total_hashrate) (need online + hashrate>0)"
|
||||
} catch {
|
||||
Write-Log "Waiting… agents/stats API not ready ($($_.Exception.Message))"
|
||||
}
|
||||
Start-Sleep -Seconds $PollIntervalSec
|
||||
}
|
||||
|
||||
throw "no online agent with hashrate>0 within ${WaitSeconds}s (expected worker: $ExpectedWorker)"
|
||||
}
|
||||
|
||||
function Show-FailureLogs {
|
||||
Push-Location $Root
|
||||
try {
|
||||
Write-Log "Recent server logs:"
|
||||
docker compose -f $ComposeFile logs --tail=80 server 2>$null
|
||||
Write-Log "Recent agent logs:"
|
||||
docker compose -f $ComposeFile logs --tail=80 agent 2>$null
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
Ensure-Docker
|
||||
|
||||
try {
|
||||
Push-Location $Root
|
||||
Write-Log "Starting docker compose (build may take several minutes)…"
|
||||
docker compose -f $ComposeFile up --build -d
|
||||
if ($LASTEXITCODE -ne 0) { throw "docker compose up failed (exit $LASTEXITCODE)" }
|
||||
Pop-Location
|
||||
|
||||
$deadline = (Get-Date).AddSeconds($WaitSeconds)
|
||||
Write-Log "Waiting up to ${WaitSeconds}s for health + mining proof…"
|
||||
Wait-ForHealth -Deadline $deadline
|
||||
Assert-Mining -Deadline $deadline
|
||||
Write-Log "Docker mining proof succeeded"
|
||||
exit 0
|
||||
} catch {
|
||||
Write-Host "[ci-docker-mining] ERROR: $($_.Exception.Message)" -ForegroundColor Red
|
||||
Show-FailureLogs
|
||||
exit 1
|
||||
} finally {
|
||||
Invoke-Teardown
|
||||
}
|
||||
123
scripts/ci-docker-mining.sh
Normal file
123
scripts/ci-docker-mining.sh
Normal file
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env bash
|
||||
# CI Docker mining proof — Linux agent connects and reports hashrate > 0.
|
||||
# Uses docker/data test wallet + fleet secret (see docker/README.md).
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
COMPOSE_FILE="${ROOT}/docker/docker-compose.yml"
|
||||
BASE_URL="${AETHERFORGE_DOCKER_BASE_URL:-http://127.0.0.1:18989}"
|
||||
E2E_USER="${AETHERFORGE_E2E_USER:-testuser}"
|
||||
E2E_PASS="${AETHERFORGE_E2E_PASS:-testpass}"
|
||||
EXPECTED_WORKER="${AETHERFORGE_DOCKER_WORKER:-docker-e2e-linux}"
|
||||
WAIT_SECONDS="${AETHERFORGE_DOCKER_WAIT_SEC:-180}"
|
||||
POLL_INTERVAL="${AETHERFORGE_DOCKER_POLL_SEC:-5}"
|
||||
|
||||
AUTH_HEADER="Authorization: Basic $(printf '%s:%s' "$E2E_USER" "$E2E_PASS" | base64 | tr -d '\n')"
|
||||
DEADLINE=0
|
||||
|
||||
log() { printf '[ci-docker-mining] %s\n' "$*"; }
|
||||
fail() { log "ERROR: $*"; exit 1; }
|
||||
|
||||
require_docker() {
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
fail "docker not found — install Docker Engine 24+ or run on a GHA ubuntu-latest runner"
|
||||
fi
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
fail "docker compose plugin not found"
|
||||
fi
|
||||
}
|
||||
|
||||
teardown() {
|
||||
local code=$?
|
||||
log "Tearing down compose (exit=$code)…"
|
||||
docker compose -f "$COMPOSE_FILE" down --rmi local -v --remove-orphans 2>/dev/null || true
|
||||
if [[ $code -ne 0 ]]; then
|
||||
log "Recent server logs:"
|
||||
docker compose -f "$COMPOSE_FILE" logs --tail=80 server 2>/dev/null || true
|
||||
log "Recent agent logs:"
|
||||
docker compose -f "$COMPOSE_FILE" logs --tail=80 agent 2>/dev/null || true
|
||||
fi
|
||||
exit "$code"
|
||||
}
|
||||
|
||||
wait_for_health() {
|
||||
while (( SECONDS < DEADLINE )); do
|
||||
if curl -sf "${BASE_URL}/api/v1/health" | grep -q '"status"[[:space:]]*:[[:space:]]*"ok"'; then
|
||||
log "Server healthy at ${BASE_URL}/api/v1/health"
|
||||
return 0
|
||||
fi
|
||||
sleep "$POLL_INTERVAL"
|
||||
done
|
||||
fail "server not healthy within ${WAIT_SECONDS}s (${BASE_URL}/api/v1/health)"
|
||||
}
|
||||
|
||||
assert_mining() {
|
||||
local agents_json stats_json
|
||||
|
||||
while (( SECONDS < DEADLINE )); do
|
||||
agents_json="$(curl -sf -H "$AUTH_HEADER" "${BASE_URL}/api/v1/agents" || true)"
|
||||
stats_json="$(curl -sf -H "$AUTH_HEADER" "${BASE_URL}/api/v1/dashboard/stats" || true)"
|
||||
|
||||
if [[ -n "$agents_json" && -n "$stats_json" ]]; then
|
||||
local online_hr
|
||||
online_hr="$(python3 - <<'PY' "$agents_json" "$EXPECTED_WORKER"
|
||||
import json, sys
|
||||
agents = json.loads(sys.argv[1])
|
||||
worker = sys.argv[2]
|
||||
hits = []
|
||||
for a in agents:
|
||||
if a.get("status") != "online":
|
||||
continue
|
||||
hr = max(
|
||||
float(a.get("hashrate_15s") or 0),
|
||||
float(a.get("hashrate_1m") or 0),
|
||||
float(a.get("hashrate_15m") or 0),
|
||||
)
|
||||
if hr > 0:
|
||||
hits.append((a.get("id"), a.get("worker_name") or a.get("name"), hr))
|
||||
if worker:
|
||||
named = [h for h in hits if h[1] == worker]
|
||||
if named:
|
||||
print(f"{named[0][0]}|{named[0][1]}|{named[0][2]:.2f}")
|
||||
sys.exit(0)
|
||||
if hits:
|
||||
print(f"{hits[0][0]}|{hits[0][1]}|{hits[0][2]:.2f}")
|
||||
PY
|
||||
)" || true)"
|
||||
|
||||
if [[ -n "$online_hr" ]]; then
|
||||
IFS='|' read -r agent_id worker_name hashrate <<<"$online_hr"
|
||||
log "PASS: online agent ${agent_id} (${worker_name}) hashrate=${hashrate} H/s"
|
||||
log "Fleet stats: $(printf '%s' "$stats_json" | python3 -c 'import json,sys; s=json.load(sys.stdin); print(f"online={s.get(\"online_agents\",0)} total_hr={s.get(\"total_hashrate\",0):.2f}")')"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local online_count total_hr
|
||||
online_count="$(printf '%s' "$stats_json" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("online_agents",0))')"
|
||||
total_hr="$(printf '%s' "$stats_json" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("total_hashrate",0))')"
|
||||
log "Waiting… online_agents=${online_count} total_hashrate=${total_hr} (need online + hashrate>0)"
|
||||
else
|
||||
log "Waiting… agents/stats API not ready"
|
||||
fi
|
||||
sleep "$POLL_INTERVAL"
|
||||
done
|
||||
|
||||
fail "no online agent with hashrate>0 within ${WAIT_SECONDS}s (expected worker: ${EXPECTED_WORKER})"
|
||||
}
|
||||
|
||||
main() {
|
||||
require_docker
|
||||
trap teardown EXIT
|
||||
|
||||
cd "$ROOT"
|
||||
log "Starting docker compose (build may take several minutes)…"
|
||||
docker compose -f "$COMPOSE_FILE" up --build -d
|
||||
DEADLINE=$((SECONDS + WAIT_SECONDS))
|
||||
log "Waiting up to ${WAIT_SECONDS}s for health + mining proof…"
|
||||
|
||||
wait_for_health
|
||||
assert_mining
|
||||
log "Docker mining proof succeeded"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -119,7 +119,7 @@ function Invoke-SmokeIfServerUp {
|
||||
# and Basic auth credentials that match that server's users.json (defaults: testuser/testpass).
|
||||
if (-not (Test-ServerHealthy)) { return }
|
||||
Write-Banner "Tier 1b - API smoke (B-01 to B-10)"
|
||||
& (Join-Path $Root "scripts\smoke-test.ps1") -BaseUrl $BaseUrl -Username $E2EUser -Password $E2EPass
|
||||
& (Join-Path $Root "scripts\smoke-test.ps1") -BaseUrl $BaseUrl -Username $E2EUser -Password $E2EPass -DataDir $DataDir
|
||||
if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) { throw "smoke-test failed" }
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,23 @@
|
||||
param(
|
||||
[string]$BaseUrl = "http://localhost:8989",
|
||||
[string]$Username,
|
||||
[string]$Password
|
||||
[string]$Password,
|
||||
[string]$FleetSecret,
|
||||
[string]$DataDir
|
||||
)
|
||||
|
||||
if (-not $Username) { $Username = if ($env:AETHERFORGE_E2E_USER) { $env:AETHERFORGE_E2E_USER } else { "testuser" } }
|
||||
if (-not $Password) { $Password = if ($env:AETHERFORGE_E2E_PASS) { $env:AETHERFORGE_E2E_PASS } else { "testpass" } }
|
||||
|
||||
if (-not $FleetSecret) { $FleetSecret = $env:AETHERFORGE_FLEET_SECRET }
|
||||
if (-not $FleetSecret -and $DataDir) {
|
||||
$cfgPath = Join-Path $DataDir "config.json"
|
||||
if (Test-Path $cfgPath) {
|
||||
$cfg = Get-Content $cfgPath -Raw | ConvertFrom-Json
|
||||
if ($cfg.server.fleet_secret) { $FleetSecret = $cfg.server.fleet_secret }
|
||||
}
|
||||
}
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$passed = 0
|
||||
$failed = 0
|
||||
@@ -17,6 +28,28 @@ $results = @()
|
||||
$cred = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("${Username}:${Password}"))
|
||||
$authHeaders = @{ Authorization = "Basic $cred" }
|
||||
|
||||
|
||||
function Invoke-AgentRestMethod {
|
||||
param(
|
||||
[string]$Uri,
|
||||
[string]$Method = "Post",
|
||||
[string]$Body,
|
||||
[string]$ContentType = "application/json"
|
||||
)
|
||||
if (-not $FleetSecret) { throw "fleet secret required for agent API; pass -FleetSecret, -DataDir, or set AETHERFORGE_FLEET_SECRET" }
|
||||
$headers = @{ "X-Fleet-Secret" = $FleetSecret }
|
||||
$params = @{
|
||||
Uri = $Uri
|
||||
Method = $Method
|
||||
Headers = $headers
|
||||
}
|
||||
if ($Body) {
|
||||
$params.Body = $Body
|
||||
$params.ContentType = $ContentType
|
||||
}
|
||||
return Invoke-RestMethod @params
|
||||
}
|
||||
|
||||
function Invoke-AuthRestMethod {
|
||||
param(
|
||||
[string]$Uri,
|
||||
@@ -109,15 +142,18 @@ Invoke-SmokeTest "B-07" "POST /agent/decide" {
|
||||
shares_bad = 0
|
||||
} | ConvertTo-Json -Depth 5
|
||||
try {
|
||||
Invoke-RestMethod "$BaseUrl/api/v1/agent/decide" -Method Post -Body $body -ContentType "application/json" | Out-Null
|
||||
Invoke-AgentRestMethod "$BaseUrl/api/v1/agent/decide" -Body $body | Out-Null
|
||||
} catch {
|
||||
if ($_.Exception.Response.StatusCode.value__ -ge 500) { throw $_ }
|
||||
$code = $_.Exception.Response.StatusCode.value__
|
||||
if ($code -eq 403) { return }
|
||||
if ($code -ge 500) { throw $_ }
|
||||
throw $_
|
||||
}
|
||||
}
|
||||
|
||||
Invoke-SmokeTest "B-08" "POST /agent/report array" {
|
||||
$body = '[{"agent_id":"smoke-agent","tool":"sleep","success":true,"output":"ok"}]'
|
||||
$r = Invoke-RestMethod "$BaseUrl/api/v1/agent/report" -Method Post -Body $body -ContentType "application/json"
|
||||
$r = Invoke-AgentRestMethod "$BaseUrl/api/v1/agent/report" -Body $body
|
||||
if (-not $r.success) { throw "report not accepted" }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user