Initial commit: AetherForge Linux (forge-mesh) v0.1.0-dev
Some checks failed
Test / test (push) Has been cancelled
Some checks failed
Test / test (push) Has been cancelled
This commit is contained in:
109
scripts/LAUNCH.sh
Executable file
109
scripts/LAUNCH.sh
Executable file
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env bash
|
||||
# Portable AetherForge / forge-mesh command deck launcher (USB edition).
|
||||
# Starts optional cloudflared sidecar, sets AF_TUNNEL_EXTERNAL=1, opens the deck UI,
|
||||
# and runs forge-mesh-server against ./data.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
if [[ "$(basename "$SCRIPT_DIR")" == "scripts" ]]; then
|
||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
else
|
||||
ROOT="$SCRIPT_DIR"
|
||||
fi
|
||||
cd "$ROOT"
|
||||
|
||||
DATA_DIR="${AF_DATA_DIR:-$ROOT/data}"
|
||||
CONFIG="${AF_CONFIG:-$DATA_DIR/config.json}"
|
||||
SERVER_BIN="${AF_SERVER_BIN:-$ROOT/bin/forge-mesh-server}"
|
||||
DECK_URL="${AF_DECK_URL:-http://localhost:8989}"
|
||||
CLOUDFLARED_PID=""
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "$CLOUDFLARED_PID" ]] && kill -0 "$CLOUDFLARED_PID" 2>/dev/null; then
|
||||
kill "$CLOUDFLARED_PID" 2>/dev/null || true
|
||||
wait "$CLOUDFLARED_PID" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
resolve_cloudflared() {
|
||||
if command -v cloudflared >/dev/null 2>&1; then
|
||||
command -v cloudflared
|
||||
return 0
|
||||
fi
|
||||
for candidate in "$ROOT/bin/cloudflared" "$ROOT/cloudflared"; do
|
||||
if [[ -x "$candidate" ]]; then
|
||||
echo "$candidate"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
read_tunnel_token() {
|
||||
if [[ -n "${AF_TUNNEL_TOKEN:-}" ]]; then
|
||||
printf '%s' "$AF_TUNNEL_TOKEN"
|
||||
return 0
|
||||
fi
|
||||
local token_file="$DATA_DIR/cloudflared-token.txt"
|
||||
if [[ -f "$token_file" ]]; then
|
||||
tr -d '[:space:]' <"$token_file"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
start_tunnel_sidecar() {
|
||||
local token cf_bin
|
||||
token="$(read_tunnel_token)" || return 0
|
||||
cf_bin="$(resolve_cloudflared)" || {
|
||||
echo "WARN: tunnel token present but cloudflared not found (PATH or $ROOT/bin/cloudflared)" >&2
|
||||
return 0
|
||||
}
|
||||
|
||||
echo "Starting cloudflared sidecar..."
|
||||
"$cf_bin" tunnel --no-autoupdate run --token "$token" &
|
||||
CLOUDFLARED_PID=$!
|
||||
export AF_TUNNEL_EXTERNAL=1
|
||||
echo "AF_TUNNEL_EXTERNAL=1 (cloudflared pid $CLOUDFLARED_PID)"
|
||||
}
|
||||
|
||||
open_browser() {
|
||||
local url="$1"
|
||||
if [[ -n "${AF_NO_BROWSER:-}" ]]; then
|
||||
echo "AF_NO_BROWSER set; deck at $url"
|
||||
return 0
|
||||
fi
|
||||
for opener in xdg-open sensible-browser x-www-browser gnome-open kde-open; do
|
||||
if command -v "$opener" >/dev/null 2>&1; then
|
||||
"$opener" "$url" >/dev/null 2>&1 &
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
echo "Open your browser to $url"
|
||||
}
|
||||
|
||||
ensure_server_binary() {
|
||||
if [[ -x "$SERVER_BIN" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if command -v forge-mesh-server >/dev/null 2>&1; then
|
||||
SERVER_BIN="$(command -v forge-mesh-server)"
|
||||
return 0
|
||||
fi
|
||||
echo "ERROR: forge-mesh-server not found. Build with 'make server' or place binary in $ROOT/bin/" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
mkdir -p "$DATA_DIR"
|
||||
if [[ ! -f "$CONFIG" ]]; then
|
||||
echo "ERROR: missing config at $CONFIG" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
start_tunnel_sidecar
|
||||
ensure_server_binary
|
||||
|
||||
open_browser "$DECK_URL"
|
||||
echo "Starting forge-mesh-server (config: $CONFIG)"
|
||||
exec "$SERVER_BIN" -config "$CONFIG"
|
||||
112
scripts/README-USB.md
Normal file
112
scripts/README-USB.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Forge Mesh — Portable Deck (USB Edition)
|
||||
|
||||
Self-contained command deck you can run from a USB stick or extracted tarball without a system install.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
tar -xzf forge-mesh-portable-*.tar.gz
|
||||
cd forge-mesh-portable-*
|
||||
# Edit credentials and wallet before production use:
|
||||
${EDITOR:-nano} data/config.json
|
||||
./LAUNCH.sh
|
||||
```
|
||||
|
||||
`LAUNCH.sh` starts the control plane on **http://localhost:8989**, opens your default browser, and uses `./data` for SQLite, artifacts, and secrets.
|
||||
|
||||
Default login (change in `data/config.json`):
|
||||
|
||||
- Username: `admin`
|
||||
- Password: `changeme`
|
||||
|
||||
## Cloudflare Tunnel (optional sidecar)
|
||||
|
||||
Linux uses an **external** cloudflared process (`AF_TUNNEL_EXTERNAL=1`), not in-process tunneling.
|
||||
|
||||
1. Place your tunnel token in `data/cloudflared-token.txt` (single line, no quotes), **or**
|
||||
2. Export `AF_TUNNEL_TOKEN` before launch.
|
||||
|
||||
`LAUNCH.sh` looks for `cloudflared` on `PATH` or in `bin/cloudflared`. When a token is present it starts the sidecar and sets `AF_TUNNEL_EXTERNAL=1` for agents that honor that flag.
|
||||
|
||||
```bash
|
||||
# Example
|
||||
echo 'YOUR_CF_TUNNEL_TOKEN' > data/cloudflared-token.txt
|
||||
./LAUNCH.sh
|
||||
```
|
||||
|
||||
Skip browser auto-open (headless / SSH):
|
||||
|
||||
```bash
|
||||
AF_NO_BROWSER=1 ./LAUNCH.sh
|
||||
```
|
||||
|
||||
## WireGuard mesh (optional, operator-managed)
|
||||
|
||||
When you control the network, WireGuard is preferred over Cloudflare: agents dial the deck on a private mesh without a public tunnel.
|
||||
|
||||
Forge Mesh does **not** auto-install WireGuard. Configure it on the deck host and enrolled agents yourself:
|
||||
|
||||
1. Install WireGuard (`wireguard`, `wireguard-tools`) on deck and agents.
|
||||
2. Generate keys: `wg genkey | tee privatekey | wg pubkey > publickey`
|
||||
3. Create `/etc/wireguard/forge-mesh.conf` (paths may vary):
|
||||
|
||||
```ini
|
||||
[Interface]
|
||||
PrivateKey = <deck-private-key>
|
||||
Address = 10.66.0.1/24
|
||||
ListenPort = 51820
|
||||
|
||||
[Peer]
|
||||
# Example agent
|
||||
PublicKey = <agent-public-key>
|
||||
AllowedIPs = 10.66.0.2/32
|
||||
```
|
||||
|
||||
4. Enable: `sudo systemctl enable --now wg-quick@forge-mesh`
|
||||
5. Point agents at the deck **WireGuard IP** (e.g. `http://10.66.0.1:8989`) in summon URL or agent env — not `localhost`.
|
||||
|
||||
Triple-onion tier **T9** (mTLS mesh join via WireGuard) assumes this overlay exists. See `docs/PROBLEMS.md` for limits.
|
||||
|
||||
## Summon agents from this deck
|
||||
|
||||
With the deck listening (and reachable on your LAN or tunnel):
|
||||
|
||||
```bash
|
||||
curl -fsSL http://localhost:8989/install.sh | sudo bash
|
||||
```
|
||||
|
||||
Replace `localhost` with your tunnel hostname or WireGuard address when summoning remote hosts.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
.
|
||||
├── LAUNCH.sh # Entry point (symlink to scripts/LAUNCH.sh)
|
||||
├── scripts/LAUNCH.sh
|
||||
├── bin/
|
||||
│ ├── forge-mesh-server # Included if built before pack-usb.sh
|
||||
│ └── cloudflared # Optional
|
||||
├── data/
|
||||
│ ├── config.json
|
||||
│ ├── cloudflared-token.txt # Optional (gitignore in real deployments)
|
||||
│ └── forge-mesh.db # Created on first run
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Purpose |
|
||||
|----------|---------|
|
||||
| `AF_TUNNEL_TOKEN` | Cloudflare tunnel token (overrides file) |
|
||||
| `AF_TUNNEL_EXTERNAL` | Set to `1` automatically when sidecar runs |
|
||||
| `AF_NO_BROWSER` | Skip opening a browser |
|
||||
| `AF_CONFIG` | Override config path (default `./data/config.json`) |
|
||||
| `AF_DATA_DIR` | Data directory (default `./data`) |
|
||||
| `AF_SERVER_BIN` | Path to `forge-mesh-server` binary |
|
||||
| `AF_DECK_URL` | Browser URL (default `http://localhost:8989`) |
|
||||
|
||||
## Security notes
|
||||
|
||||
- Change `auth.basic_password` and `auth.fleet_secret` before exposing the deck.
|
||||
- Do not commit `data/cloudflared-token.txt` or `data/signing.key` to version control.
|
||||
- USB copies carry your fleet secret — treat the stick like a key.
|
||||
25
scripts/ci-test.sh
Executable file
25
scripts/ci-test.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
# CI entrypoint — mirrors .github/workflows/test.yml locally
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
export GOROOT="${GOROOT:-/usr/local/go}"
|
||||
export PATH="${GOROOT}/bin:${PATH}"
|
||||
export CGO_ENABLED=1
|
||||
|
||||
echo "==> [ci] environment"
|
||||
go version
|
||||
node --version 2>/dev/null || echo "node: not installed (frontend may skip)"
|
||||
npm --version 2>/dev/null || true
|
||||
|
||||
echo "==> [ci] go mod tidy + build"
|
||||
go mod tidy
|
||||
go build ./...
|
||||
|
||||
echo "==> [ci] full test suite"
|
||||
SKIP_E2E="${SKIP_E2E:-0}" SKIP_FRONTEND="${SKIP_FRONTEND:-0}" \
|
||||
CONTINUE_ON_FAIL=0 ./scripts/test-all.sh
|
||||
|
||||
echo "✓ ci-test complete"
|
||||
92
scripts/e2e-lib.sh
Executable file
92
scripts/e2e-lib.sh
Executable file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared helpers for e2e-test.sh
|
||||
set -euo pipefail
|
||||
|
||||
e2e_log() { echo "==> [e2e] $*"; }
|
||||
e2e_pass() { echo " ✓ $*"; }
|
||||
|
||||
e2e_wait_health() {
|
||||
local base="$1" tries="${2:-60}"
|
||||
for _ in $(seq 1 "$tries"); do
|
||||
if curl -fsS "${base}/api/v1/health" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep 0.25
|
||||
done
|
||||
echo "server did not become healthy at ${base}" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
e2e_wait_port() {
|
||||
local host="$1" port="$2" tries="${3:-40}"
|
||||
for _ in $(seq 1 "$tries"); do
|
||||
if (echo >/dev/tcp/"${host}"/"${port}") 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
sleep 0.25
|
||||
done
|
||||
echo "port ${host}:${port} not open" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
e2e_json_field() {
|
||||
local json="$1" field="$2"
|
||||
echo "${json}" | sed -n "s/.*\"${field}\":\"\([^\"]*\)\".*/\1/p" | head -1
|
||||
}
|
||||
|
||||
e2e_curl_auth() {
|
||||
curl -fsS -u "${E2E_USER}:${E2E_PASS}" "$@"
|
||||
}
|
||||
|
||||
e2e_curl_fleet() {
|
||||
curl -fsS -H "Authorization: Bearer ${FLEET_SECRET}" "$@"
|
||||
}
|
||||
|
||||
e2e_write_config() {
|
||||
local path="$1" port="$2" clearance="$3" xmr_port="$4" rvn_port="$5"
|
||||
mkdir -p "$(dirname "${path}")" "/tmp/forge-mesh-e2e"
|
||||
cat > "${path}" <<EOF
|
||||
{
|
||||
"listen_addr": ":${port}",
|
||||
"data_dir": "/tmp/forge-mesh-e2e",
|
||||
"database_path": "/tmp/forge-mesh-e2e/test-${port}.db",
|
||||
"operator_clearance": ${clearance},
|
||||
"auth": {
|
||||
"basic_username": "${E2E_USER}",
|
||||
"basic_password": "${E2E_PASS}",
|
||||
"fleet_secret": "${FLEET_SECRET}"
|
||||
},
|
||||
"wallet_policy": {
|
||||
"default_wallet": "e2e-test-wallet",
|
||||
"currency": "XMR"
|
||||
},
|
||||
"stratum": {
|
||||
"xmr_listen": ":${xmr_port}",
|
||||
"rvn_listen": ":${rvn_port}",
|
||||
"upstream_xmr": "",
|
||||
"upstream_rvn": ""
|
||||
},
|
||||
"forge": {
|
||||
"signing_key_path": "/tmp/forge-mesh-e2e/signing-${port}.key",
|
||||
"artifacts_dir": "/tmp/forge-mesh-e2e/artifacts-${port}"
|
||||
},
|
||||
"court": { "enabled": false, "ollama_url": "http://127.0.0.1:11434" },
|
||||
"telegram": { "enabled": false, "bot_token": "", "chat_id": "" }
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
e2e_start_server() {
|
||||
local config="$1"
|
||||
./bin/forge-mesh-server -config "${config}" -install-tmpl scripts/install.sh.tpl &
|
||||
SERVER_PID=$!
|
||||
e2e_wait_health "${BASE}"
|
||||
}
|
||||
|
||||
e2e_stop_server() {
|
||||
if [[ -n "${SERVER_PID:-}" ]] && kill -0 "${SERVER_PID}" 2>/dev/null; then
|
||||
kill "${SERVER_PID}" 2>/dev/null || true
|
||||
wait "${SERVER_PID}" 2>/dev/null || true
|
||||
fi
|
||||
SERVER_PID=""
|
||||
}
|
||||
201
scripts/e2e-test.sh
Executable file
201
scripts/e2e-test.sh
Executable file
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env bash
|
||||
# AetherForge Linux end-to-end operator flow tests
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
# shellcheck source=scripts/e2e-lib.sh
|
||||
source "${ROOT}/scripts/e2e-lib.sh"
|
||||
|
||||
PORT="${E2E_PORT:-18989}"
|
||||
STRATUM_XMR_PORT="${E2E_STRATUM_XMR:-33333}"
|
||||
STRATUM_RVN_PORT="${E2E_STRATUM_RVN:-33388}"
|
||||
BASE="http://127.0.0.1:${PORT}"
|
||||
CONFIG="${E2E_CONFIG:-/tmp/forge-mesh-e2e-config.json}"
|
||||
FLEET_SECRET="${E2E_FLEET_SECRET:-e2e-fleet-secret-test}"
|
||||
E2E_USER="${E2E_USER:-admin}"
|
||||
E2E_PASS="${E2E_PASS:-changeme}"
|
||||
SERVER_PID=""
|
||||
AGENT_PID=""
|
||||
MOCK_OLLAMA_PID=""
|
||||
MOCK_TG_PID=""
|
||||
|
||||
cleanup() {
|
||||
[[ -n "${AGENT_PID}" ]] && kill "${AGENT_PID}" 2>/dev/null || true
|
||||
[[ -n "${MOCK_OLLAMA_PID}" ]] && kill "${MOCK_OLLAMA_PID}" 2>/dev/null || true
|
||||
[[ -n "${MOCK_TG_PID}" ]] && kill "${MOCK_TG_PID}" 2>/dev/null || true
|
||||
e2e_stop_server
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
start_mock_ollama() {
|
||||
if curl -fsS http://127.0.0.1:11434/api/tags >/dev/null 2>&1; then
|
||||
e2e_pass "Ollama already available — using live instance"
|
||||
return 0
|
||||
fi
|
||||
python3 - <<'PY' &
|
||||
import json
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
class H(BaseHTTPRequestHandler):
|
||||
def log_message(self, *a): pass
|
||||
def do_GET(self):
|
||||
if self.path.startswith("/api/tags"):
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"models": [{"name": "mock"}]}).encode())
|
||||
else:
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
|
||||
HTTPServer(("127.0.0.1", 11434), H).serve_forever()
|
||||
PY
|
||||
MOCK_OLLAMA_PID=$!
|
||||
sleep 0.5
|
||||
e2e_pass "mock Ollama listening on :11434"
|
||||
}
|
||||
|
||||
start_mock_telegram() {
|
||||
# Telegram is disabled in e2e config; mock not required unless enabled.
|
||||
e2e_pass "Telegram disabled in test config (mock skipped)"
|
||||
}
|
||||
|
||||
e2e_log "building server + agent"
|
||||
make server agent
|
||||
|
||||
e2e_log "writing L4 test config"
|
||||
e2e_write_config "${CONFIG}" "${PORT}" 4 "${STRATUM_XMR_PORT}" "${STRATUM_RVN_PORT}"
|
||||
|
||||
e2e_log "starting server (start/stop lifecycle)"
|
||||
e2e_start_server "${CONFIG}"
|
||||
e2e_pass "server started pid=${SERVER_PID}"
|
||||
|
||||
e2e_log "health check"
|
||||
HEALTH="$(curl -fsS "${BASE}/api/v1/health")"
|
||||
echo "${HEALTH}" | grep -q '"status":"ok"' || { echo "health failed: ${HEALTH}"; exit 1; }
|
||||
e2e_pass "GET /api/v1/health"
|
||||
|
||||
e2e_log "basic auth rejection/acceptance"
|
||||
CODE="$(curl -s -o /dev/null -w '%{http_code}' "${BASE}/api/v1/fleet")"
|
||||
[[ "${CODE}" == "401" ]] || { echo "expected 401 without auth, got ${CODE}"; exit 1; }
|
||||
e2e_pass "unauthenticated fleet → 401"
|
||||
FLEET="$(e2e_curl_auth "${BASE}/api/v1/fleet")"
|
||||
echo "${FLEET}" | grep -q '"hosts"' || { echo "fleet list failed: ${FLEET}"; exit 1; }
|
||||
e2e_pass "authenticated fleet → 200"
|
||||
|
||||
e2e_log "public install.sh render + bash syntax"
|
||||
INSTALL="$(curl -fsS "${BASE}/install.sh")"
|
||||
echo "${INSTALL}" | grep -q 'forge-mesh' || { echo "install.sh missing marker"; exit 1; }
|
||||
TMP_INSTALL="$(mktemp)"
|
||||
echo "${INSTALL}" > "${TMP_INSTALL}"
|
||||
bash -n "${TMP_INSTALL}"
|
||||
rm -f "${TMP_INSTALL}"
|
||||
e2e_pass "GET /install.sh valid bash"
|
||||
|
||||
e2e_log "forge build + public download"
|
||||
BUILD_RESP="$(e2e_curl_auth -X POST "${BASE}/api/v1/forge/builds/trigger" \
|
||||
-H "Content-Type: application/json" -d '{"public":true}')"
|
||||
echo "${BUILD_RESP}" | grep -q '"ok":true' || { echo "forge trigger failed: ${BUILD_RESP}"; exit 1; }
|
||||
LATEST="$(curl -fsS "${BASE}/api/v1/public/builds/latest?os=linux&arch=amd64")"
|
||||
BUILD_ID="$(e2e_json_field "${LATEST}" id)"
|
||||
[[ -n "${BUILD_ID}" ]] || { echo "no build id in ${LATEST}"; exit 1; }
|
||||
DOWNLOAD_BYTES="$(curl -fsS "${BASE}/api/v1/public/download/${BUILD_ID}" | wc -c | tr -d ' ')"
|
||||
[[ "${DOWNLOAD_BYTES}" -gt 100 ]] || { echo "download too small: ${DOWNLOAD_BYTES}"; exit 1; }
|
||||
e2e_pass "forge build + public download (${BUILD_ID})"
|
||||
|
||||
e2e_log "agent register via REST + WS subprocess"
|
||||
REGISTER="$(e2e_curl_fleet -X POST "${BASE}/api/v1/fleet/register" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"hostname":"e2e-rest-host","fingerprint":"127.0.0.2","arch":"amd64"}')"
|
||||
HOST_ID="$(e2e_json_field "${REGISTER}" host_id)"
|
||||
[[ -n "${HOST_ID}" ]] || { echo "register failed: ${REGISTER}"; exit 1; }
|
||||
e2e_pass "POST /api/v1/fleet/register"
|
||||
|
||||
AGENT_HOST="e2e-ws-agent"
|
||||
FORGE_FLEET_SECRET="${FLEET_SECRET}" FORGE_HOST_ID="${AGENT_HOST}" \
|
||||
./bin/forge-mesh-agent -deck "${BASE}" -secret "${FLEET_SECRET}" -host-id "${AGENT_HOST}" &
|
||||
AGENT_PID=$!
|
||||
sleep 3
|
||||
|
||||
e2e_log "fleet appears in API"
|
||||
FLEET2="$(e2e_curl_auth "${BASE}/api/v1/fleet/hosts")"
|
||||
echo "${FLEET2}" | grep -q "${AGENT_HOST}" || echo "${FLEET2}" | grep -q 'e2e-rest-host' || {
|
||||
echo "fleet missing enrolled hosts: ${FLEET2}"; exit 1
|
||||
}
|
||||
WS_HOST_ID="$(echo "${FLEET2}" | sed -n 's/.*"hostname":"\('"${AGENT_HOST}"'\)".*"id":"\([^"]*\)".*/\1/p')"
|
||||
[[ -z "${WS_HOST_ID}" ]] && WS_HOST_ID="${HOST_ID}"
|
||||
e2e_pass "fleet hosts visible"
|
||||
|
||||
e2e_log "mining profile push"
|
||||
PROFILE_RESP="$(e2e_curl_auth -X POST "${BASE}/api/v1/fleet/${WS_HOST_ID}/mining-profile" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"wallet_address":"e2e-wallet-addr","name":"E2E profile"}')"
|
||||
echo "${PROFILE_RESP}" | grep -q '"ok":true' || { echo "profile push failed: ${PROFILE_RESP}"; exit 1; }
|
||||
e2e_pass "POST /api/v1/fleet/{id}/mining-profile"
|
||||
|
||||
e2e_log "stratum port open"
|
||||
e2e_wait_port 127.0.0.1 "${STRATUM_XMR_PORT}"
|
||||
e2e_wait_port 127.0.0.1 "${STRATUM_RVN_PORT}"
|
||||
# Send minimal stratum-ish payload (accept-only mode)
|
||||
(printf '{"id":1,"method":"mining.subscribe","params":[]}\n' | nc -w 2 127.0.0.1 "${STRATUM_XMR_PORT}") >/dev/null 2>&1 || true
|
||||
e2e_pass "stratum XMR:${STRATUM_XMR_PORT} RVN:${STRATUM_RVN_PORT} listening"
|
||||
|
||||
e2e_log "/spread/ lander serves"
|
||||
SPREAD="$(curl -fsS "${BASE}/spread/?c=e2e-campaign")"
|
||||
echo "${SPREAD}" | grep -qi 'spread\|summon\|install' || { echo "spread lander unexpected: ${SPREAD}"; exit 1; }
|
||||
e2e_pass "GET /spread/"
|
||||
|
||||
e2e_log "policy snapshot"
|
||||
SNAP="$(e2e_curl_auth -X POST "${BASE}/api/v1/policy/snapshot")"
|
||||
SNAP_TOKEN="$(e2e_json_field "${SNAP}" token)"
|
||||
[[ -n "${SNAP_TOKEN}" ]] || { echo "snapshot create failed: ${SNAP}"; exit 1; }
|
||||
SNAP_BODY="$(curl -fsS "${BASE}/api/v1/public/policy-snapshot/${SNAP_TOKEN}")"
|
||||
echo "${SNAP_BODY}" | grep -q 'policy_from_server' || { echo "snapshot fetch failed: ${SNAP_BODY}"; exit 1; }
|
||||
e2e_pass "policy snapshot create + public fetch"
|
||||
|
||||
e2e_log "crucible endpoint (L4)"
|
||||
CRUC="$(e2e_curl_auth -X POST "${BASE}/api/v1/crucible/batch" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"command\":\"status\",\"host_ids\":[\"${WS_HOST_ID}\"]}")"
|
||||
echo "${CRUC}" | grep -q '"status":"completed"' || echo "${CRUC}" | grep -q '"status":"running"' || {
|
||||
echo "crucible failed: ${CRUC}"; exit 1
|
||||
}
|
||||
e2e_pass "POST /api/v1/crucible/batch"
|
||||
|
||||
e2e_log "ws ticket"
|
||||
TICKET="$(e2e_curl_auth -X POST "${BASE}/api/v1/ws/ticket")"
|
||||
echo "${TICKET}" | grep -q 'ticket' || { echo "ws ticket failed: ${TICKET}"; exit 1; }
|
||||
e2e_pass "POST /api/v1/ws/ticket"
|
||||
|
||||
start_mock_ollama
|
||||
start_mock_telegram
|
||||
|
||||
e2e_log "clearance gate blocks L0 actions — restart with operator_clearance=0"
|
||||
kill "${AGENT_PID}" 2>/dev/null || true
|
||||
AGENT_PID=""
|
||||
e2e_stop_server
|
||||
|
||||
L0_CONFIG="/tmp/forge-mesh-e2e-l0.json"
|
||||
e2e_write_config "${L0_CONFIG}" "${PORT}" 0 "${STRATUM_XMR_PORT}" "${STRATUM_RVN_PORT}"
|
||||
e2e_start_server "${L0_CONFIG}"
|
||||
|
||||
ME="$(e2e_curl_auth "${BASE}/api/v1/operator/me")"
|
||||
echo "${ME}" | grep -q '"clearance_level":0' || { echo "operator/me: ${ME}"; exit 1; }
|
||||
|
||||
DENIED_CODE="$(e2e_curl_auth -X POST "${BASE}/api/v1/fleet/${HOST_ID}/command" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"action":"shell","args":{"command":"id"}}' \
|
||||
-o /dev/null -w '%{http_code}')"
|
||||
[[ "${DENIED_CODE}" == "403" ]] || { echo "expected 403 for L0 shell, got ${DENIED_CODE}"; exit 1; }
|
||||
|
||||
CRUC_CODE="$(e2e_curl_auth -X POST "${BASE}/api/v1/crucible/batch" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"command\":\"status\",\"host_ids\":[\"${HOST_ID}\"]}" \
|
||||
-o /dev/null -w '%{http_code}')"
|
||||
[[ "${CRUC_CODE}" == "403" ]] || { echo "expected 403 for L0 crucible, got ${CRUC_CODE}"; exit 1; }
|
||||
e2e_pass "L0 operator blocked from shell + crucible"
|
||||
|
||||
echo ""
|
||||
echo "✓ e2e full operator flow passed ($(grep -c 'e2e_pass\|✓' "$0" || echo 18) checks)"
|
||||
112
scripts/install.sh.tpl
Normal file
112
scripts/install.sh.tpl
Normal file
@@ -0,0 +1,112 @@
|
||||
#!/bin/bash
|
||||
# forge-mesh agent installer — rendered by deck at GET /install.sh
|
||||
set -euo pipefail
|
||||
|
||||
DECK_URL="{{.DeckURL}}"
|
||||
PUBLIC_KEY="{{.PublicKey}}"
|
||||
FLEET_SECRET="{{.FleetSecret}}"
|
||||
PIN="{{.Pin}}"
|
||||
CAMPAIGN="{{.Campaign}}"
|
||||
INSTALL_DIR="${FORGE_MESH_INSTALL_DIR:-/opt/forge-mesh}"
|
||||
AGENT_BIN="${INSTALL_DIR}/agent"
|
||||
SERVICE_NAME="forge-mesh-agent"
|
||||
|
||||
ARCH_RAW="$(uname -m)"
|
||||
case "${ARCH_RAW}" in
|
||||
x86_64|amd64) ARCH="amd64" ;;
|
||||
aarch64|arm64) ARCH="arm64" ;;
|
||||
*) echo "unsupported arch: ${ARCH_RAW}" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
echo "[forge-mesh] deck=${DECK_URL} arch=${ARCH}"
|
||||
|
||||
META_URL="${DECK_URL}/api/v1/public/builds/latest?os=linux&arch=${ARCH}"
|
||||
META="$(curl -fsSL "${META_URL}")"
|
||||
|
||||
BUILD_ID="$(echo "${META}" | sed -n 's/.*"id":"\([^"]*\)".*/\1/p')"
|
||||
CHECKSUM="$(echo "${META}" | sed -n 's/.*"checksum":"\([^"]*\)".*/\1/p')"
|
||||
SIGNATURE="$(echo "${META}" | sed -n 's/.*"signature":"\([^"]*\)".*/\1/p')"
|
||||
|
||||
if [[ -z "${BUILD_ID}" || -z "${CHECKSUM}" ]]; then
|
||||
echo "failed to fetch build metadata from ${META_URL}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TMP="$(mktemp)"
|
||||
trap 'rm -f "${TMP}"' EXIT
|
||||
|
||||
curl -fsSL "${DECK_URL}/api/v1/public/download/${BUILD_ID}" -o "${TMP}"
|
||||
|
||||
ACTUAL="$(sha256sum "${TMP}" | awk '{print $1}')"
|
||||
if [[ "${ACTUAL}" != "${CHECKSUM}" ]]; then
|
||||
echo "checksum mismatch: expected ${CHECKSUM}, got ${ACTUAL}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${SIGNATURE}" && -n "${PUBLIC_KEY}" ]]; then
|
||||
echo "[forge-mesh] signed build (sig verified by agent on self-update with pubkey ${PUBLIC_KEY:0:16}...)"
|
||||
fi
|
||||
|
||||
SECRET="${FORGE_MESH_FLEET_SECRET:-${FORGE_FLEET_SECRET:-${FLEET_SECRET}}}"
|
||||
if [[ -z "${SECRET}" ]]; then
|
||||
echo "set FORGE_MESH_FLEET_SECRET before install" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
echo "[forge-mesh] not root — installing user systemd unit"
|
||||
mkdir -p "${HOME}/.local/bin"
|
||||
install -m 0755 "${TMP}" "${HOME}/.local/bin/forge-mesh-agent"
|
||||
UNIT_DIR="${HOME}/.config/systemd/user"
|
||||
mkdir -p "${UNIT_DIR}"
|
||||
cat > "${UNIT_DIR}/${SERVICE_NAME}.service" <<UNIT
|
||||
[Unit]
|
||||
Description=Forge Mesh Agent
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=${HOME}/.local/bin/forge-mesh-agent -deck-url ${DECK_URL} -secret ${SECRET} -pubkey ${PUBLIC_KEY}
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
Environment=FORGE_MESH_DECK_URL=${DECK_URL}
|
||||
Environment=FORGE_MESH_FLEET_SECRET=${SECRET}
|
||||
Environment=FORGE_MESH_PUBKEY=${PUBLIC_KEY}
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
UNIT
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now "${SERVICE_NAME}.service"
|
||||
echo "[forge-mesh] started user unit ${SERVICE_NAME}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "${INSTALL_DIR}"
|
||||
install -m 0755 "${TMP}" "${AGENT_BIN}"
|
||||
|
||||
cat > "/etc/systemd/system/${SERVICE_NAME}.service" <<UNIT
|
||||
[Unit]
|
||||
Description=Forge Mesh Agent
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=${INSTALL_DIR}
|
||||
ExecStart=${AGENT_BIN} -deck-url ${DECK_URL} -secret ${SECRET} -pubkey ${PUBLIC_KEY}
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
Environment=FORGE_MESH_DECK_URL=${DECK_URL}
|
||||
Environment=FORGE_MESH_FLEET_SECRET=${SECRET}
|
||||
Environment=FORGE_MESH_PUBKEY=${PUBLIC_KEY}
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now "${SERVICE_NAME}.service"
|
||||
echo "[forge-mesh] agent installed to ${AGENT_BIN} (build ${BUILD_ID})"
|
||||
[[ -n "${CAMPAIGN}" ]] && echo "[forge-mesh] campaign=${CAMPAIGN}"
|
||||
[[ -n "${PIN}" ]] && echo "[forge-mesh] pin accepted"
|
||||
84
scripts/pack-usb.sh
Executable file
84
scripts/pack-usb.sh
Executable file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build a portable USB tarball: LAUNCH.sh, data template, README, optional binaries.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
OUT_DIR="${1:-$ROOT/dist}"
|
||||
STAMP="$(date -u +%Y%m%d)"
|
||||
PKG_NAME="forge-mesh-portable-${STAMP}"
|
||||
STAGE="$OUT_DIR/$PKG_NAME"
|
||||
ARCHIVE="$OUT_DIR/${PKG_NAME}.tar.gz"
|
||||
|
||||
echo "==> Staging portable deck at $STAGE"
|
||||
rm -rf "$STAGE"
|
||||
mkdir -p "$STAGE"/{bin,data,scripts}
|
||||
|
||||
cp scripts/LAUNCH.sh "$STAGE/scripts/LAUNCH.sh"
|
||||
chmod +x "$STAGE/scripts/LAUNCH.sh"
|
||||
ln -sf scripts/LAUNCH.sh "$STAGE/LAUNCH.sh"
|
||||
|
||||
if [[ -f data/config.json ]]; then
|
||||
cp data/config.json "$STAGE/data/config.json"
|
||||
else
|
||||
cat >"$STAGE/data/config.json" <<'EOF'
|
||||
{
|
||||
"listen_addr": ":8989",
|
||||
"data_dir": "./data",
|
||||
"database_path": "./data/forge-mesh.db",
|
||||
"auth": {
|
||||
"basic_username": "admin",
|
||||
"basic_password": "changeme",
|
||||
"fleet_secret": "change-me-fleet-secret"
|
||||
},
|
||||
"wallet_policy": {
|
||||
"default_wallet": "",
|
||||
"currency": "XMR"
|
||||
},
|
||||
"stratum": {
|
||||
"xmr_listen": ":3333",
|
||||
"rvn_listen": ":3388",
|
||||
"upstream_xmr": "",
|
||||
"upstream_rvn": ""
|
||||
},
|
||||
"forge": {
|
||||
"signing_key_path": "./data/signing.key",
|
||||
"artifacts_dir": "./data/artifacts"
|
||||
},
|
||||
"court": {
|
||||
"ollama_url": "http://127.0.0.1:11434",
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Optional: include cloudflared token placeholder (empty = no tunnel until operator fills in).
|
||||
touch "$STAGE/data/cloudflared-token.txt.example"
|
||||
cat >"$STAGE/data/cloudflared-token.txt.example" <<'EOF'
|
||||
# Paste your Cloudflare Tunnel token on a single line, then rename to cloudflared-token.txt
|
||||
# Or set AF_TUNNEL_TOKEN in the environment before running LAUNCH.sh
|
||||
EOF
|
||||
|
||||
cp scripts/README-USB.md "$STAGE/README.md"
|
||||
|
||||
if [[ -x bin/forge-mesh-server ]]; then
|
||||
cp bin/forge-mesh-server "$STAGE/bin/forge-mesh-server"
|
||||
echo " included bin/forge-mesh-server"
|
||||
else
|
||||
echo " WARN: bin/forge-mesh-server missing — run 'make server' before packing for a self-contained bundle"
|
||||
fi
|
||||
|
||||
if [[ -x bin/cloudflared ]]; then
|
||||
cp bin/cloudflared "$STAGE/bin/cloudflared"
|
||||
echo " included bin/cloudflared"
|
||||
fi
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
tar -C "$OUT_DIR" -czf "$ARCHIVE" "$PKG_NAME"
|
||||
rm -rf "$STAGE"
|
||||
|
||||
echo "==> Created $ARCHIVE"
|
||||
echo " Extract anywhere, edit data/config.json, optionally data/cloudflared-token.txt, then:"
|
||||
echo " ./LAUNCH.sh"
|
||||
16
scripts/sync-webroot.sh
Executable file
16
scripts/sync-webroot.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copy Vite build output into cmd/server/webroot for go:embed.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
DIST="$ROOT/web/dist"
|
||||
WEBROOT="$ROOT/cmd/server/webroot"
|
||||
|
||||
if [[ ! -f "$DIST/index.html" ]]; then
|
||||
echo "ERROR: $DIST/index.html missing — run: cd web && npm run build" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf "$WEBROOT"
|
||||
mkdir -p "$WEBROOT"
|
||||
cp -a "$DIST"/. "$WEBROOT/"
|
||||
echo "Synced web/dist -> cmd/server/webroot ($(find "$WEBROOT" -type f | wc -l) files)"
|
||||
78
scripts/test-all.sh
Executable file
78
scripts/test-all.sh
Executable file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run full AetherForge Linux test suite with summary
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
export GOROOT="${GOROOT:-/usr/local/go}"
|
||||
export PATH="${GOROOT}/bin:${PATH}"
|
||||
|
||||
SUITE=(
|
||||
"unit:scripts/test-unit.sh"
|
||||
"integration:scripts/test-integration.sh"
|
||||
"cli:scripts/test-cli.sh"
|
||||
"frontend:scripts/test-frontend.sh"
|
||||
"e2e:scripts/e2e-test.sh"
|
||||
)
|
||||
|
||||
PASSED=()
|
||||
FAILED=()
|
||||
SKIPPED=()
|
||||
|
||||
run_suite() {
|
||||
local name="$1" script="$2"
|
||||
echo ""
|
||||
echo "════════════════════════════════════════"
|
||||
echo " Running ${name} tests"
|
||||
echo "════════════════════════════════════════"
|
||||
if [[ ! -x "${script}" ]]; then
|
||||
chmod +x "${script}"
|
||||
fi
|
||||
if "${script}"; then
|
||||
PASSED+=("${name}")
|
||||
else
|
||||
FAILED+=("${name}")
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
CONTINUE_ON_FAIL="${CONTINUE_ON_FAIL:-0}"
|
||||
OVERALL=0
|
||||
|
||||
for entry in "${SUITE[@]}"; do
|
||||
name="${entry%%:*}"
|
||||
script="${entry#*:}"
|
||||
if [[ "${SKIP_E2E:-0}" == "1" && "${name}" == "e2e" ]]; then
|
||||
SKIPPED+=("${name}")
|
||||
echo "⊘ skipping e2e (SKIP_E2E=1)"
|
||||
continue
|
||||
fi
|
||||
if [[ "${SKIP_FRONTEND:-0}" == "1" && "${name}" == "frontend" ]]; then
|
||||
SKIPPED+=("${name}")
|
||||
echo "⊘ skipping frontend (SKIP_FRONTEND=1)"
|
||||
continue
|
||||
fi
|
||||
if run_suite "${name}" "${script}"; then
|
||||
:
|
||||
else
|
||||
OVERALL=1
|
||||
[[ "${CONTINUE_ON_FAIL}" == "1" ]] || break
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "════════════════════════════════════════"
|
||||
echo " Test summary"
|
||||
echo "════════════════════════════════════════"
|
||||
echo " Passed : ${#PASSED[@]} (${PASSED[*]:-none})"
|
||||
echo " Failed : ${#FAILED[@]} (${FAILED[*]:-none})"
|
||||
echo " Skipped: ${#SKIPPED[@]} (${SKIPPED[*]:-none})"
|
||||
echo "════════════════════════════════════════"
|
||||
|
||||
if [[ "${OVERALL}" -ne 0 ]]; then
|
||||
echo "✗ test-all FAILED"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ test-all PASSED"
|
||||
exit 0
|
||||
48
scripts/test-cli.sh
Executable file
48
scripts/test-cli.sh
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
# CLI smoke tests for forge-mesh forge and agent binaries
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
export GOROOT="${GOROOT:-/usr/local/go}"
|
||||
export PATH="${GOROOT}/bin:${PATH}"
|
||||
|
||||
CLI_CONFIG="/tmp/forge-mesh-cli-config.json"
|
||||
CLI_DIR="/tmp/forge-mesh-cli"
|
||||
mkdir -p "${CLI_DIR}"
|
||||
|
||||
cat > "${CLI_CONFIG}" <<EOF
|
||||
{
|
||||
"listen_addr": ":0",
|
||||
"data_dir": "${CLI_DIR}",
|
||||
"database_path": "${CLI_DIR}/cli.db",
|
||||
"auth": {
|
||||
"basic_username": "admin",
|
||||
"basic_password": "changeme",
|
||||
"fleet_secret": "cli-test-secret"
|
||||
},
|
||||
"forge": {
|
||||
"signing_key_path": "${CLI_DIR}/signing.key",
|
||||
"artifacts_dir": "${CLI_DIR}/artifacts"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "==> [cli] building forge CLI + agent"
|
||||
make -C "${ROOT}" agent
|
||||
go build -o "${CLI_DIR}/forge" ./cmd/forge
|
||||
|
||||
echo "==> [cli] forge pubkey"
|
||||
PUBKEY="$("${CLI_DIR}/forge" pubkey --config "${CLI_CONFIG}")"
|
||||
[[ -n "${PUBKEY}" ]] || { echo "empty pubkey"; exit 1; }
|
||||
echo " pubkey: ${PUBKEY:0:16}..."
|
||||
|
||||
echo "==> [cli] forge build (public artifacts)"
|
||||
BUILD_OUT="$("${CLI_DIR}/forge" build --config "${CLI_CONFIG}" --public 2>&1)"
|
||||
echo "${BUILD_OUT}" | grep -q 'built linux/amd64' || { echo "${BUILD_OUT}"; exit 1; }
|
||||
|
||||
echo "==> [cli] agent --help exits cleanly"
|
||||
./bin/forge-mesh-agent -h 2>&1 | grep -q 'deck' || ./bin/forge-mesh-agent 2>&1 | grep -q 'fleet secret'
|
||||
|
||||
echo "✓ cli tests passed"
|
||||
18
scripts/test-frontend.sh
Executable file
18
scripts/test-frontend.sh
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
# Frontend unit tests (Vitest + Testing Library)
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
WEB="${ROOT}/web"
|
||||
|
||||
cd "${WEB}"
|
||||
|
||||
if [[ ! -d node_modules ]]; then
|
||||
echo "==> [frontend] installing dependencies"
|
||||
npm ci 2>/dev/null || npm install
|
||||
fi
|
||||
|
||||
echo "==> [frontend] vitest run"
|
||||
npm run test
|
||||
|
||||
echo "✓ frontend tests passed"
|
||||
15
scripts/test-integration.sh
Executable file
15
scripts/test-integration.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# Integration tests — HTTP router, stratum, fleet store against temp SQLite
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
export GOROOT="${GOROOT:-/usr/local/go}"
|
||||
export PATH="${GOROOT}/bin:${PATH}"
|
||||
export CGO_ENABLED="${CGO_ENABLED:-1}"
|
||||
|
||||
echo "==> [integration] api router + stratum + fleet store"
|
||||
go test -count=1 -v ./internal/api/... ./internal/stratum/... ./internal/fleet/...
|
||||
|
||||
echo "✓ integration tests passed"
|
||||
14
scripts/test-unit.sh
Executable file
14
scripts/test-unit.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# Go unit tests (fast, no network)
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
export GOROOT="${GOROOT:-/usr/local/go}"
|
||||
export PATH="${GOROOT}/bin:${PATH}"
|
||||
|
||||
echo "==> [unit] go test ./internal/..."
|
||||
go test -count=1 -short ./internal/...
|
||||
|
||||
echo "✓ unit tests passed"
|
||||
Reference in New Issue
Block a user