mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-03-30 14:45:52 +00:00
Compare commits
5 Commits
fix/ci-she
...
ci/e2e-use
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
155f6beb3f | ||
|
|
ea30b24cd4 | ||
|
|
0f70cd1ac0 | ||
|
|
5bb9bc146e | ||
|
|
12d1174e39 |
126
.github/workflows/deploy.yml
vendored
126
.github/workflows/deploy.yml
vendored
@@ -10,20 +10,11 @@ on:
|
||||
- 'docs/**'
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
- '.gitignore'
|
||||
- 'docs/**'
|
||||
|
||||
concurrency:
|
||||
group: deploy-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
@@ -46,8 +37,23 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Skip if docs-only change
|
||||
id: docs-check
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
|
||||
NON_DOCS=$(echo "$CHANGED" | grep -cvE '\.(md)$|^LICENSE$|^\.gitignore$|^docs/' || true)
|
||||
if [ "$NON_DOCS" -eq 0 ]; then
|
||||
echo "docs_only=true" >> $GITHUB_OUTPUT
|
||||
echo "📄 Docs-only PR — skipping heavy CI"
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Set up Go 1.22
|
||||
if: steps.docs-check.outputs.docs_only != 'true'
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.22'
|
||||
@@ -56,6 +62,7 @@ jobs:
|
||||
cmd/ingestor/go.sum
|
||||
|
||||
- name: Build and test Go server (with coverage)
|
||||
if: steps.docs-check.outputs.docs_only != 'true'
|
||||
run: |
|
||||
set -e -o pipefail
|
||||
cd cmd/server
|
||||
@@ -65,6 +72,7 @@ jobs:
|
||||
go tool cover -func=server-coverage.out | tail -1
|
||||
|
||||
- name: Build and test Go ingestor (with coverage)
|
||||
if: steps.docs-check.outputs.docs_only != 'true'
|
||||
run: |
|
||||
set -e -o pipefail
|
||||
cd cmd/ingestor
|
||||
@@ -74,6 +82,7 @@ jobs:
|
||||
go tool cover -func=ingestor-coverage.out | tail -1
|
||||
|
||||
- name: Verify proto syntax (all .proto files compile)
|
||||
if: steps.docs-check.outputs.docs_only != 'true'
|
||||
run: |
|
||||
set -e
|
||||
echo "Installing protoc..."
|
||||
@@ -88,7 +97,7 @@ jobs:
|
||||
echo "✅ All .proto files are syntactically valid"
|
||||
|
||||
- name: Generate Go coverage badges
|
||||
if: always()
|
||||
if: always() && steps.docs-check.outputs.docs_only != 'true'
|
||||
run: |
|
||||
mkdir -p .badges
|
||||
|
||||
@@ -148,21 +157,53 @@ jobs:
|
||||
node-test:
|
||||
name: "🧪 Node.js Tests"
|
||||
runs-on: [self-hosted, Linux]
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 2
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Skip if docs-only change
|
||||
id: docs-check
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
|
||||
NON_DOCS=$(echo "$CHANGED" | grep -cvE '\.(md)$|^LICENSE$|^\.gitignore$|^docs/' || true)
|
||||
if [ "$NON_DOCS" -eq 0 ]; then
|
||||
echo "docs_only=true" >> $GITHUB_OUTPUT
|
||||
echo "📄 Docs-only PR — skipping heavy CI"
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Set up Node.js 22
|
||||
if: steps.docs-check.outputs.docs_only != 'true'
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Set up Go 1.22
|
||||
if: steps.docs-check.outputs.docs_only != 'true'
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: '1.22'
|
||||
cache-dependency-path: cmd/server/go.sum
|
||||
|
||||
- name: Build Go server for E2E tests
|
||||
if: steps.docs-check.outputs.docs_only != 'true'
|
||||
run: |
|
||||
cd cmd/server
|
||||
go build -o ../../corescope-server .
|
||||
echo "Go server built successfully"
|
||||
|
||||
- name: Install npm dependencies
|
||||
if: steps.docs-check.outputs.docs_only != 'true'
|
||||
run: npm ci --production=false
|
||||
|
||||
- name: Detect changed files
|
||||
if: steps.docs-check.outputs.docs_only != 'true'
|
||||
id: changes
|
||||
run: |
|
||||
BACKEND=$(git diff --name-only HEAD~1 | grep -cE '^(server|db|decoder|packet-store|server-helpers|iata-coords)\.js$' || true)
|
||||
@@ -178,7 +219,7 @@ jobs:
|
||||
echo "Changes: backend=$BACKEND frontend=$FRONTEND tests=$TESTS ci=$CI"
|
||||
|
||||
- name: Run backend tests with coverage
|
||||
if: steps.changes.outputs.backend == 'true'
|
||||
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.backend == 'true'
|
||||
run: |
|
||||
npx c8 --reporter=text-summary --reporter=text sh test-all.sh 2>&1 | tee test-output.txt
|
||||
|
||||
@@ -196,24 +237,28 @@ jobs:
|
||||
echo "## Backend: ${TOTAL_PASS} tests, ${BE_COVERAGE}% coverage" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Run backend tests (quick, no coverage)
|
||||
if: steps.changes.outputs.backend == 'false'
|
||||
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.backend == 'false'
|
||||
run: npm run test:unit
|
||||
|
||||
- name: Install Playwright browser
|
||||
if: steps.changes.outputs.frontend == 'true'
|
||||
run: npx playwright install chromium --with-deps 2>/dev/null || true
|
||||
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'true'
|
||||
run: |
|
||||
# Install chromium (skips download if already cached on self-hosted runner)
|
||||
npx playwright install chromium 2>/dev/null || true
|
||||
# Install system deps only if missing (apt-get is slow)
|
||||
npx playwright install-deps chromium 2>/dev/null || true
|
||||
|
||||
- name: Instrument frontend JS for coverage
|
||||
if: steps.changes.outputs.frontend == 'true'
|
||||
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'true'
|
||||
run: sh scripts/instrument-frontend.sh
|
||||
|
||||
- name: Start instrumented test server on port 13581
|
||||
if: steps.changes.outputs.frontend == 'true'
|
||||
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'true'
|
||||
run: |
|
||||
# Kill any stale server on 13581
|
||||
fuser -k 13581/tcp 2>/dev/null || true
|
||||
sleep 2
|
||||
COVERAGE=1 PORT=13581 node server.js &
|
||||
./corescope-server -port 13581 -public public-instrumented &
|
||||
echo $! > .server.pid
|
||||
echo "Server PID: $(cat .server.pid)"
|
||||
# Health-check poll loop (up to 30s)
|
||||
@@ -225,25 +270,38 @@ jobs:
|
||||
if [ "$i" -eq 30 ]; then
|
||||
echo "Server failed to start within 30s"
|
||||
echo "Last few lines from server logs:"
|
||||
ps aux | grep "PORT=13581" || echo "No server process found"
|
||||
ps aux | grep "corescope-server" || echo "No server process found"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
- name: Run Playwright E2E tests
|
||||
if: steps.changes.outputs.frontend == 'true'
|
||||
run: BASE_URL=http://localhost:13581 node test-e2e-playwright.js 2>&1 | tee e2e-output.txt
|
||||
|
||||
- name: Collect frontend coverage report
|
||||
if: always() && steps.changes.outputs.frontend == 'true'
|
||||
- name: Run Playwright E2E + coverage collection concurrently
|
||||
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'true'
|
||||
run: |
|
||||
# Run E2E tests and coverage collection in parallel — both use the same server
|
||||
BASE_URL=http://localhost:13581 node test-e2e-playwright.js 2>&1 | tee e2e-output.txt &
|
||||
E2E_PID=$!
|
||||
BASE_URL=http://localhost:13581 node scripts/collect-frontend-coverage.js 2>&1 | tee fe-coverage-output.txt &
|
||||
COV_PID=$!
|
||||
|
||||
# Wait for both — E2E must pass, coverage is best-effort
|
||||
E2E_EXIT=0
|
||||
wait $E2E_PID || E2E_EXIT=$?
|
||||
wait $COV_PID || true
|
||||
|
||||
# Fail if E2E failed
|
||||
[ $E2E_EXIT -ne 0 ] && exit $E2E_EXIT
|
||||
true
|
||||
|
||||
- name: Generate frontend coverage badges
|
||||
if: always() && steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'true'
|
||||
run: |
|
||||
BASE_URL=http://localhost:13581 node scripts/collect-frontend-coverage.js 2>&1 | tee fe-coverage-output.txt
|
||||
|
||||
E2E_PASS=$(grep -oP '[0-9]+(?=/)' e2e-output.txt | tail -1)
|
||||
|
||||
mkdir -p .badges
|
||||
if [ -f .nyc_output/frontend-coverage.json ]; then
|
||||
# Merge E2E + coverage collector data if both exist
|
||||
if [ -f .nyc_output/frontend-coverage.json ] || [ -f .nyc_output/e2e-coverage.json ]; then
|
||||
npx nyc report --reporter=text-summary --reporter=text 2>&1 | tee fe-report.txt
|
||||
FE_COVERAGE=$(grep 'Statements' fe-report.txt | head -1 | grep -oP '[\d.]+(?=%)' || echo "0")
|
||||
FE_COVERAGE=${FE_COVERAGE:-0}
|
||||
@@ -256,7 +314,7 @@ jobs:
|
||||
echo "{\"schemaVersion\":1,\"label\":\"frontend tests\",\"message\":\"${E2E_PASS:-0} E2E passed\",\"color\":\"brightgreen\"}" > .badges/frontend-tests.json
|
||||
|
||||
- name: Stop test server
|
||||
if: always() && steps.changes.outputs.frontend == 'true'
|
||||
if: always() && steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'true'
|
||||
run: |
|
||||
if [ -f .server.pid ]; then
|
||||
kill $(cat .server.pid) 2>/dev/null || true
|
||||
@@ -265,12 +323,16 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Run frontend E2E (quick, no coverage)
|
||||
if: steps.changes.outputs.frontend == 'false'
|
||||
if: steps.docs-check.outputs.docs_only != 'true' && steps.changes.outputs.frontend == 'false'
|
||||
run: |
|
||||
fuser -k 13581/tcp 2>/dev/null || true
|
||||
PORT=13581 node server.js &
|
||||
./corescope-server -port 13581 -public public &
|
||||
SERVER_PID=$!
|
||||
sleep 5
|
||||
# Wait for server to be ready (up to 15s)
|
||||
for i in $(seq 1 15); do
|
||||
curl -sf http://localhost:13581/api/stats > /dev/null 2>&1 && break
|
||||
sleep 1
|
||||
done
|
||||
BASE_URL=http://localhost:13581 node test-e2e-playwright.js || true
|
||||
kill $SERVER_PID 2>/dev/null || true
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
> High-performance mesh network analyzer powered by Go. Sub-millisecond packet queries, ~300 MB memory for 56K+ packets, real-time WebSocket broadcast, full channel decryption.
|
||||
|
||||
Self-hosted, open-source MeshCore packet analyzer — a community alternative to the closed-source `analyzer.letsmesh.net`. Collects MeshCore packets via MQTT, decodes them in real time, and presents a full web UI with live packet feed, interactive maps, channel chat, packet tracing, and per-node analytics.
|
||||
Self-hosted, open-source MeshCore packet analyzer. Collects MeshCore packets via MQTT, decodes them in real time, and presents a full web UI with live packet feed, interactive maps, channel chat, packet tracing, and per-node analytics.
|
||||
|
||||
## ⚡ Performance
|
||||
|
||||
|
||||
@@ -60,10 +60,10 @@ func (c *Config) NodeDaysOrDefault() int {
|
||||
}
|
||||
|
||||
type HealthThresholds struct {
|
||||
InfraDegradedMs int `json:"infraDegradedMs"`
|
||||
InfraSilentMs int `json:"infraSilentMs"`
|
||||
NodeDegradedMs int `json:"nodeDegradedMs"`
|
||||
NodeSilentMs int `json:"nodeSilentMs"`
|
||||
InfraDegradedHours float64 `json:"infraDegradedHours"`
|
||||
InfraSilentHours float64 `json:"infraSilentHours"`
|
||||
NodeDegradedHours float64 `json:"nodeDegradedHours"`
|
||||
NodeSilentHours float64 `json:"nodeSilentHours"`
|
||||
}
|
||||
|
||||
// ThemeFile mirrors theme.json overlay.
|
||||
@@ -126,34 +126,46 @@ func LoadTheme(baseDirs ...string) *ThemeFile {
|
||||
|
||||
func (c *Config) GetHealthThresholds() HealthThresholds {
|
||||
h := HealthThresholds{
|
||||
InfraDegradedMs: 86400000,
|
||||
InfraSilentMs: 259200000,
|
||||
NodeDegradedMs: 3600000,
|
||||
NodeSilentMs: 86400000,
|
||||
InfraDegradedHours: 24,
|
||||
InfraSilentHours: 72,
|
||||
NodeDegradedHours: 1,
|
||||
NodeSilentHours: 24,
|
||||
}
|
||||
if c.HealthThresholds != nil {
|
||||
if c.HealthThresholds.InfraDegradedMs > 0 {
|
||||
h.InfraDegradedMs = c.HealthThresholds.InfraDegradedMs
|
||||
if c.HealthThresholds.InfraDegradedHours > 0 {
|
||||
h.InfraDegradedHours = c.HealthThresholds.InfraDegradedHours
|
||||
}
|
||||
if c.HealthThresholds.InfraSilentMs > 0 {
|
||||
h.InfraSilentMs = c.HealthThresholds.InfraSilentMs
|
||||
if c.HealthThresholds.InfraSilentHours > 0 {
|
||||
h.InfraSilentHours = c.HealthThresholds.InfraSilentHours
|
||||
}
|
||||
if c.HealthThresholds.NodeDegradedMs > 0 {
|
||||
h.NodeDegradedMs = c.HealthThresholds.NodeDegradedMs
|
||||
if c.HealthThresholds.NodeDegradedHours > 0 {
|
||||
h.NodeDegradedHours = c.HealthThresholds.NodeDegradedHours
|
||||
}
|
||||
if c.HealthThresholds.NodeSilentMs > 0 {
|
||||
h.NodeSilentMs = c.HealthThresholds.NodeSilentMs
|
||||
if c.HealthThresholds.NodeSilentHours > 0 {
|
||||
h.NodeSilentHours = c.HealthThresholds.NodeSilentHours
|
||||
}
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// GetHealthMs returns degraded/silent thresholds for a given role.
|
||||
// GetHealthMs returns degraded/silent thresholds in ms for a given role.
|
||||
func (h HealthThresholds) GetHealthMs(role string) (degradedMs, silentMs int) {
|
||||
const hourMs = 3600000
|
||||
if role == "repeater" || role == "room" {
|
||||
return h.InfraDegradedMs, h.InfraSilentMs
|
||||
return int(h.InfraDegradedHours * hourMs), int(h.InfraSilentHours * hourMs)
|
||||
}
|
||||
return int(h.NodeDegradedHours * hourMs), int(h.NodeSilentHours * hourMs)
|
||||
}
|
||||
|
||||
// ToClientMs returns the thresholds as ms for the frontend.
|
||||
func (h HealthThresholds) ToClientMs() map[string]int {
|
||||
const hourMs = 3600000
|
||||
return map[string]int{
|
||||
"infraDegradedMs": int(h.InfraDegradedHours * hourMs),
|
||||
"infraSilentMs": int(h.InfraSilentHours * hourMs),
|
||||
"nodeDegradedMs": int(h.NodeDegradedHours * hourMs),
|
||||
"nodeSilentMs": int(h.NodeSilentHours * hourMs),
|
||||
}
|
||||
return h.NodeDegradedMs, h.NodeSilentMs
|
||||
}
|
||||
|
||||
func (c *Config) ResolveDBPath(baseDir string) string {
|
||||
|
||||
@@ -23,10 +23,10 @@ func TestLoadConfigValidJSON(t *testing.T) {
|
||||
"SJC": "San Jose",
|
||||
},
|
||||
"healthThresholds": map[string]interface{}{
|
||||
"infraDegradedMs": 100000,
|
||||
"infraSilentMs": 200000,
|
||||
"nodeDegradedMs": 50000,
|
||||
"nodeSilentMs": 100000,
|
||||
"infraDegradedHours": 2,
|
||||
"infraSilentHours": 4,
|
||||
"nodeDegradedHours": 0.5,
|
||||
"nodeSilentHours": 2,
|
||||
},
|
||||
"liveMap": map[string]interface{}{
|
||||
"propagationBufferMs": 3000,
|
||||
@@ -178,68 +178,68 @@ func TestGetHealthThresholdsDefaults(t *testing.T) {
|
||||
cfg := &Config{}
|
||||
ht := cfg.GetHealthThresholds()
|
||||
|
||||
if ht.InfraDegradedMs != 86400000 {
|
||||
t.Errorf("expected 86400000, got %d", ht.InfraDegradedMs)
|
||||
if ht.InfraDegradedHours != 24 {
|
||||
t.Errorf("expected 24, got %v", ht.InfraDegradedHours)
|
||||
}
|
||||
if ht.InfraSilentMs != 259200000 {
|
||||
t.Errorf("expected 259200000, got %d", ht.InfraSilentMs)
|
||||
if ht.InfraSilentHours != 72 {
|
||||
t.Errorf("expected 72, got %v", ht.InfraSilentHours)
|
||||
}
|
||||
if ht.NodeDegradedMs != 3600000 {
|
||||
t.Errorf("expected 3600000, got %d", ht.NodeDegradedMs)
|
||||
if ht.NodeDegradedHours != 1 {
|
||||
t.Errorf("expected 1, got %v", ht.NodeDegradedHours)
|
||||
}
|
||||
if ht.NodeSilentMs != 86400000 {
|
||||
t.Errorf("expected 86400000, got %d", ht.NodeSilentMs)
|
||||
if ht.NodeSilentHours != 24 {
|
||||
t.Errorf("expected 24, got %v", ht.NodeSilentHours)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetHealthThresholdsCustom(t *testing.T) {
|
||||
cfg := &Config{
|
||||
HealthThresholds: &HealthThresholds{
|
||||
InfraDegradedMs: 100000,
|
||||
InfraSilentMs: 200000,
|
||||
NodeDegradedMs: 50000,
|
||||
NodeSilentMs: 100000,
|
||||
InfraDegradedHours: 2,
|
||||
InfraSilentHours: 4,
|
||||
NodeDegradedHours: 0.5,
|
||||
NodeSilentHours: 2,
|
||||
},
|
||||
}
|
||||
ht := cfg.GetHealthThresholds()
|
||||
|
||||
if ht.InfraDegradedMs != 100000 {
|
||||
t.Errorf("expected 100000, got %d", ht.InfraDegradedMs)
|
||||
if ht.InfraDegradedHours != 2 {
|
||||
t.Errorf("expected 2, got %v", ht.InfraDegradedHours)
|
||||
}
|
||||
if ht.InfraSilentMs != 200000 {
|
||||
t.Errorf("expected 200000, got %d", ht.InfraSilentMs)
|
||||
if ht.InfraSilentHours != 4 {
|
||||
t.Errorf("expected 4, got %v", ht.InfraSilentHours)
|
||||
}
|
||||
if ht.NodeDegradedMs != 50000 {
|
||||
t.Errorf("expected 50000, got %d", ht.NodeDegradedMs)
|
||||
if ht.NodeDegradedHours != 0.5 {
|
||||
t.Errorf("expected 0.5, got %v", ht.NodeDegradedHours)
|
||||
}
|
||||
if ht.NodeSilentMs != 100000 {
|
||||
t.Errorf("expected 100000, got %d", ht.NodeSilentMs)
|
||||
if ht.NodeSilentHours != 2 {
|
||||
t.Errorf("expected 2, got %v", ht.NodeSilentHours)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetHealthThresholdsPartialCustom(t *testing.T) {
|
||||
cfg := &Config{
|
||||
HealthThresholds: &HealthThresholds{
|
||||
InfraDegradedMs: 100000,
|
||||
InfraDegradedHours: 2,
|
||||
// Others left as zero → should use defaults
|
||||
},
|
||||
}
|
||||
ht := cfg.GetHealthThresholds()
|
||||
|
||||
if ht.InfraDegradedMs != 100000 {
|
||||
t.Errorf("expected 100000, got %d", ht.InfraDegradedMs)
|
||||
if ht.InfraDegradedHours != 2 {
|
||||
t.Errorf("expected 2, got %v", ht.InfraDegradedHours)
|
||||
}
|
||||
if ht.InfraSilentMs != 259200000 {
|
||||
t.Errorf("expected default 259200000, got %d", ht.InfraSilentMs)
|
||||
if ht.InfraSilentHours != 72 {
|
||||
t.Errorf("expected default 72, got %v", ht.InfraSilentHours)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetHealthMs(t *testing.T) {
|
||||
ht := HealthThresholds{
|
||||
InfraDegradedMs: 86400000,
|
||||
InfraSilentMs: 259200000,
|
||||
NodeDegradedMs: 3600000,
|
||||
NodeSilentMs: 86400000,
|
||||
InfraDegradedHours: 24,
|
||||
InfraSilentHours: 72,
|
||||
NodeDegradedHours: 1,
|
||||
NodeSilentHours: 24,
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
|
||||
@@ -513,10 +513,10 @@ func TestGetNetworkStatus(t *testing.T) {
|
||||
seedTestData(t, db)
|
||||
|
||||
ht := HealthThresholds{
|
||||
InfraDegradedMs: 86400000,
|
||||
InfraSilentMs: 259200000,
|
||||
NodeDegradedMs: 3600000,
|
||||
NodeSilentMs: 86400000,
|
||||
InfraDegradedHours: 24,
|
||||
InfraSilentHours: 72,
|
||||
NodeDegradedHours: 1,
|
||||
NodeSilentHours: 24,
|
||||
}
|
||||
result, err := db.GetNetworkStatus(ht)
|
||||
if err != nil {
|
||||
@@ -1050,10 +1050,10 @@ func TestGetNetworkStatusDateFormats(t *testing.T) {
|
||||
VALUES ('node4444', 'NodeBad', 'sensor', 'not-a-date')`)
|
||||
|
||||
ht := HealthThresholds{
|
||||
InfraDegradedMs: 86400000,
|
||||
InfraSilentMs: 259200000,
|
||||
NodeDegradedMs: 3600000,
|
||||
NodeSilentMs: 86400000,
|
||||
InfraDegradedHours: 24,
|
||||
InfraSilentHours: 72,
|
||||
NodeDegradedHours: 1,
|
||||
NodeSilentHours: 24,
|
||||
}
|
||||
result, err := db.GetNetworkStatus(ht)
|
||||
if err != nil {
|
||||
|
||||
@@ -213,7 +213,7 @@ func (s *Server) handleConfigCache(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Server) handleConfigClient(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, ClientConfigResponse{
|
||||
Roles: s.cfg.Roles,
|
||||
HealthThresholds: s.cfg.HealthThresholds,
|
||||
HealthThresholds: s.cfg.GetHealthThresholds().ToClientMs(),
|
||||
Tiles: s.cfg.Tiles,
|
||||
SnrThresholds: s.cfg.SnrThresholds,
|
||||
DistThresholds: s.cfg.DistThresholds,
|
||||
|
||||
@@ -98,6 +98,13 @@
|
||||
"#bookclub",
|
||||
"#shtf"
|
||||
],
|
||||
"healthThresholds": {
|
||||
"infraDegradedHours": 24,
|
||||
"infraSilentHours": 72,
|
||||
"nodeDegradedHours": 1,
|
||||
"nodeSilentHours": 24,
|
||||
"_comment": "How long (hours) before nodes show as degraded/silent. 'infra' = repeaters & rooms, 'node' = companions & others."
|
||||
},
|
||||
"defaultRegion": "SJC",
|
||||
"mapDefaults": {
|
||||
"center": [
|
||||
|
||||
@@ -89,7 +89,8 @@
|
||||
|
||||
function getStatusTooltip(role, status) {
|
||||
const isInfra = role === 'repeater' || role === 'room';
|
||||
const threshold = isInfra ? '72h' : '24h';
|
||||
const threshMs = isInfra ? HEALTH_THRESHOLDS.infraSilentMs : HEALTH_THRESHOLDS.nodeSilentMs;
|
||||
const threshold = threshMs >= 3600000 ? Math.round(threshMs / 3600000) + 'h' : Math.round(threshMs / 60000) + 'm';
|
||||
if (status === 'active') {
|
||||
return 'Active \u2014 heard within the last ' + threshold + '.' + (isInfra ? ' Repeaters typically advertise every 12-24h.' : '');
|
||||
}
|
||||
|
||||
@@ -64,9 +64,9 @@ async function collectCoverage() {
|
||||
// ══════════════════════════════════════════════
|
||||
console.log(' [coverage] Home page — chooser...');
|
||||
// Clear localStorage to get chooser
|
||||
await page.goto(BASE, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
||||
await page.goto(BASE, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||
await page.evaluate(() => localStorage.clear()).catch(() => {});
|
||||
await page.goto(`${BASE}/#/home`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
||||
await page.goto(`${BASE}/#/home`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||
|
||||
// Click "I'm new"
|
||||
await safeClick('#chooseNew');
|
||||
@@ -105,7 +105,7 @@ async function collectCoverage() {
|
||||
|
||||
// Switch to experienced mode
|
||||
await page.evaluate(() => localStorage.clear()).catch(() => {});
|
||||
await page.goto(`${BASE}/#/home`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
||||
await page.goto(`${BASE}/#/home`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||
await safeClick('#chooseExp');
|
||||
|
||||
// Interact with experienced home page
|
||||
@@ -120,7 +120,7 @@ async function collectCoverage() {
|
||||
// NODES PAGE
|
||||
// ══════════════════════════════════════════════
|
||||
console.log(' [coverage] Nodes page...');
|
||||
await page.goto(`${BASE}/#/nodes`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
||||
await page.goto(`${BASE}/#/nodes`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||
|
||||
// Sort by EVERY column
|
||||
for (const col of ['name', 'public_key', 'role', 'last_seen', 'advert_count']) {
|
||||
@@ -168,7 +168,7 @@ async function collectCoverage() {
|
||||
try {
|
||||
const firstNodeKey = await page.$eval('#nodesBody tr td:nth-child(2)', el => el.textContent.trim());
|
||||
if (firstNodeKey) {
|
||||
await page.goto(`${BASE}/#/nodes/${firstNodeKey}`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
||||
await page.goto(`${BASE}/#/nodes/${firstNodeKey}`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||
|
||||
// Click tabs on detail page
|
||||
await clickAll('.tab-btn, [data-tab]', 10);
|
||||
@@ -191,7 +191,7 @@ async function collectCoverage() {
|
||||
try {
|
||||
const firstKey = await page.$eval('#nodesBody tr td:nth-child(2)', el => el.textContent.trim()).catch(() => null);
|
||||
if (firstKey) {
|
||||
await page.goto(`${BASE}/#/nodes/${firstKey}?scroll=paths`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
||||
await page.goto(`${BASE}/#/nodes/${firstKey}?scroll=paths`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||
}
|
||||
} catch {}
|
||||
|
||||
@@ -199,7 +199,7 @@ async function collectCoverage() {
|
||||
// PACKETS PAGE
|
||||
// ══════════════════════════════════════════════
|
||||
console.log(' [coverage] Packets page...');
|
||||
await page.goto(`${BASE}/#/packets`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
||||
await page.goto(`${BASE}/#/packets`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||
|
||||
// Open filter bar
|
||||
await safeClick('#filterToggleBtn');
|
||||
@@ -285,13 +285,13 @@ async function collectCoverage() {
|
||||
} catch {}
|
||||
|
||||
// Navigate to specific packet by hash
|
||||
await page.goto(`${BASE}/#/packets/deadbeef`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
||||
await page.goto(`${BASE}/#/packets/deadbeef`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// MAP PAGE
|
||||
// ══════════════════════════════════════════════
|
||||
console.log(' [coverage] Map page...');
|
||||
await page.goto(`${BASE}/#/map`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
||||
await page.goto(`${BASE}/#/map`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||
|
||||
// Toggle controls panel
|
||||
await safeClick('#mapControlsToggle');
|
||||
@@ -345,7 +345,7 @@ async function collectCoverage() {
|
||||
// ANALYTICS PAGE
|
||||
// ══════════════════════════════════════════════
|
||||
console.log(' [coverage] Analytics page...');
|
||||
await page.goto(`${BASE}/#/analytics`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
||||
await page.goto(`${BASE}/#/analytics`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||
|
||||
// Click EVERY analytics tab
|
||||
const analyticsTabs = ['overview', 'rf', 'topology', 'channels', 'hashsizes', 'collisions', 'subpaths', 'nodes', 'distance'];
|
||||
@@ -383,7 +383,7 @@ async function collectCoverage() {
|
||||
|
||||
// Deep-link to each analytics tab via URL
|
||||
for (const tab of analyticsTabs) {
|
||||
await page.goto(`${BASE}/#/analytics?tab=${tab}`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
||||
await page.goto(`${BASE}/#/analytics?tab=${tab}`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||
}
|
||||
|
||||
// Region filter on analytics
|
||||
@@ -396,7 +396,7 @@ async function collectCoverage() {
|
||||
// CUSTOMIZE
|
||||
// ══════════════════════════════════════════════
|
||||
console.log(' [coverage] Customizer...');
|
||||
await page.goto(BASE, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
||||
await page.goto(BASE, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||
await safeClick('#customizeToggle');
|
||||
|
||||
// Click EVERY customizer tab
|
||||
@@ -503,7 +503,7 @@ async function collectCoverage() {
|
||||
// CHANNELS PAGE
|
||||
// ══════════════════════════════════════════════
|
||||
console.log(' [coverage] Channels page...');
|
||||
await page.goto(`${BASE}/#/channels`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
||||
await page.goto(`${BASE}/#/channels`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||
// Click channel rows/items
|
||||
await clickAll('.channel-item, .channel-row, .channel-card', 3);
|
||||
await clickAll('table tbody tr', 3);
|
||||
@@ -512,7 +512,7 @@ async function collectCoverage() {
|
||||
try {
|
||||
const channelHash = await page.$eval('table tbody tr td:first-child', el => el.textContent.trim()).catch(() => null);
|
||||
if (channelHash) {
|
||||
await page.goto(`${BASE}/#/channels/${channelHash}`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
||||
await page.goto(`${BASE}/#/channels/${channelHash}`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||
}
|
||||
} catch {}
|
||||
|
||||
@@ -520,7 +520,7 @@ async function collectCoverage() {
|
||||
// LIVE PAGE
|
||||
// ══════════════════════════════════════════════
|
||||
console.log(' [coverage] Live page...');
|
||||
await page.goto(`${BASE}/#/live`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
||||
await page.goto(`${BASE}/#/live`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||
|
||||
// VCR controls
|
||||
await safeClick('#vcrPauseBtn');
|
||||
@@ -603,14 +603,14 @@ async function collectCoverage() {
|
||||
// TRACES PAGE
|
||||
// ══════════════════════════════════════════════
|
||||
console.log(' [coverage] Traces page...');
|
||||
await page.goto(`${BASE}/#/traces`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
||||
await page.goto(`${BASE}/#/traces`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||
await clickAll('table tbody tr', 3);
|
||||
|
||||
// ══════════════════════════════════════════════
|
||||
// OBSERVERS PAGE
|
||||
// ══════════════════════════════════════════════
|
||||
console.log(' [coverage] Observers page...');
|
||||
await page.goto(`${BASE}/#/observers`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
||||
await page.goto(`${BASE}/#/observers`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||
// Click observer rows
|
||||
const obsRows = await page.$$('table tbody tr, .observer-card, .observer-row');
|
||||
for (let i = 0; i < Math.min(obsRows.length, 3); i++) {
|
||||
@@ -631,7 +631,7 @@ async function collectCoverage() {
|
||||
// PERF PAGE
|
||||
// ══════════════════════════════════════════════
|
||||
console.log(' [coverage] Perf page...');
|
||||
await page.goto(`${BASE}/#/perf`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
||||
await page.goto(`${BASE}/#/perf`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||
await safeClick('#perfRefresh');
|
||||
await safeClick('#perfReset');
|
||||
|
||||
@@ -641,14 +641,14 @@ async function collectCoverage() {
|
||||
console.log(' [coverage] App.js — router + global...');
|
||||
|
||||
// Navigate to bad route to trigger error/404
|
||||
await page.goto(`${BASE}/#/nonexistent-route`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
||||
await page.goto(`${BASE}/#/nonexistent-route`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||
|
||||
// Navigate to every route via hash
|
||||
const allRoutes = ['home', 'nodes', 'packets', 'map', 'live', 'channels', 'traces', 'observers', 'analytics', 'perf'];
|
||||
for (const route of allRoutes) {
|
||||
try {
|
||||
await page.evaluate((r) => { location.hash = '#/' + r; }, route);
|
||||
await page.waitForLoadState('networkidle').catch(() => {});
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -788,14 +788,14 @@ async function collectCoverage() {
|
||||
console.log(' [coverage] Region filter...');
|
||||
try {
|
||||
// Open region filter on nodes page
|
||||
await page.goto(`${BASE}/#/nodes`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
||||
await page.goto(`${BASE}/#/nodes`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||
await safeClick('#nodesRegionFilter');
|
||||
await clickAll('#nodesRegionFilter input[type="checkbox"]', 3);
|
||||
} catch {}
|
||||
|
||||
// Region filter on packets
|
||||
try {
|
||||
await page.goto(`${BASE}/#/packets`, { waitUntil: 'networkidle', timeout: 15000 }).catch(() => {});
|
||||
await page.goto(`${BASE}/#/packets`, { waitUntil: 'domcontentloaded', timeout: 10000 }).catch(() => {});
|
||||
await safeClick('#packetsRegionFilter');
|
||||
await clickAll('#packetsRegionFilter input[type="checkbox"]', 3);
|
||||
} catch {}
|
||||
@@ -807,7 +807,7 @@ async function collectCoverage() {
|
||||
for (const route of allRoutes) {
|
||||
try {
|
||||
await page.evaluate((r) => { location.hash = '#/' + r; }, route);
|
||||
await page.waitForLoadState('networkidle').catch(() => {});
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,18 +36,19 @@ function loadThemeFile(themePaths) {
|
||||
function buildHealthConfig(config) {
|
||||
const _ht = (config && config.healthThresholds) || {};
|
||||
return {
|
||||
infraDegradedMs: _ht.infraDegradedMs || 86400000,
|
||||
infraSilentMs: _ht.infraSilentMs || 259200000,
|
||||
nodeDegradedMs: _ht.nodeDegradedMs || 3600000,
|
||||
nodeSilentMs: _ht.nodeSilentMs || 86400000
|
||||
infraDegraded: _ht.infraDegradedHours || 24,
|
||||
infraSilent: _ht.infraSilentHours || 72,
|
||||
nodeDegraded: _ht.nodeDegradedHours || 1,
|
||||
nodeSilent: _ht.nodeSilentHours || 24
|
||||
};
|
||||
}
|
||||
|
||||
function getHealthMs(role, HEALTH) {
|
||||
const H = 3600000;
|
||||
const isInfra = role === 'repeater' || role === 'room';
|
||||
return {
|
||||
degradedMs: isInfra ? HEALTH.infraDegradedMs : HEALTH.nodeDegradedMs,
|
||||
silentMs: isInfra ? HEALTH.infraSilentMs : HEALTH.nodeSilentMs
|
||||
degradedMs: (isInfra ? HEALTH.infraDegraded : HEALTH.nodeDegraded) * H,
|
||||
silentMs: (isInfra ? HEALTH.infraSilent : HEALTH.nodeSilent) * H
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -307,7 +307,12 @@ app.get('/api/config/cache', (req, res) => {
|
||||
app.get('/api/config/client', (req, res) => {
|
||||
res.json({
|
||||
roles: config.roles || null,
|
||||
healthThresholds: config.healthThresholds || null,
|
||||
healthThresholds: {
|
||||
infraDegradedMs: HEALTH.infraDegraded * 3600000,
|
||||
infraSilentMs: HEALTH.infraSilent * 3600000,
|
||||
nodeDegradedMs: HEALTH.nodeDegraded * 3600000,
|
||||
nodeSilentMs: HEALTH.nodeSilent * 3600000
|
||||
},
|
||||
tiles: config.tiles || null,
|
||||
snrThresholds: config.snrThresholds || null,
|
||||
distThresholds: config.distThresholds || null,
|
||||
|
||||
@@ -354,10 +354,17 @@ async function run() {
|
||||
await test('Packets clicking row shows detail pane', async () => {
|
||||
// Fresh navigation to avoid stale row references from previous test
|
||||
await page.goto(`${BASE}/#/packets`, { waitUntil: 'domcontentloaded' });
|
||||
// Wait for table rows AND initial API data to settle
|
||||
await page.waitForSelector('table tbody tr[data-action]', { timeout: 15000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
const firstRow = await page.$('table tbody tr[data-action]');
|
||||
assert(firstRow, 'No clickable packet rows found');
|
||||
await firstRow.click();
|
||||
// Click the row and wait for the /packets/{hash} API response
|
||||
const [response] = await Promise.all([
|
||||
page.waitForResponse(resp => resp.url().includes('/packets/') && resp.status() === 200, { timeout: 15000 }),
|
||||
firstRow.click(),
|
||||
]);
|
||||
assert(response, 'API response for packet detail not received');
|
||||
await page.waitForFunction(() => {
|
||||
const panel = document.getElementById('pktRight');
|
||||
return panel && !panel.classList.contains('empty');
|
||||
@@ -375,12 +382,16 @@ async function run() {
|
||||
if (!pktRight) {
|
||||
await page.goto(`${BASE}/#/packets`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForSelector('table tbody tr[data-action]', { timeout: 15000 });
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
const panelOpen = await page.$eval('#pktRight', el => !el.classList.contains('empty'));
|
||||
if (!panelOpen) {
|
||||
const firstRow = await page.$('table tbody tr[data-action]');
|
||||
if (!firstRow) { console.log(' ⏭️ Skipped (no clickable rows)'); return; }
|
||||
await firstRow.click();
|
||||
await Promise.all([
|
||||
page.waitForResponse(resp => resp.url().includes('/packets/') && resp.status() === 200, { timeout: 15000 }),
|
||||
firstRow.click(),
|
||||
]);
|
||||
await page.waitForFunction(() => {
|
||||
const panel = document.getElementById('pktRight');
|
||||
return panel && !panel.classList.contains('empty');
|
||||
@@ -829,17 +840,7 @@ async function run() {
|
||||
assert(content.length > 10, 'Perf content should still be present after refresh');
|
||||
});
|
||||
|
||||
// Test: Node.js perf page shows Event Loop metrics (not Go Runtime)
|
||||
await test('Perf page shows Event Loop on Node server', async () => {
|
||||
const perfText = await page.$eval('#perfContent', el => el.textContent);
|
||||
// Node.js server should show Event Loop metrics
|
||||
const hasEventLoop = perfText.includes('Event Loop') || perfText.includes('event loop');
|
||||
const hasMemory = perfText.includes('Memory') || perfText.includes('RSS');
|
||||
assert(hasEventLoop || hasMemory, 'Node perf page should show Event Loop or Memory metrics');
|
||||
// Should NOT show Go Runtime section on Node.js server
|
||||
const hasGoRuntime = perfText.includes('Go Runtime');
|
||||
assert(!hasGoRuntime, 'Node perf page should NOT show Go Runtime section');
|
||||
});
|
||||
|
||||
|
||||
// Test: Go perf page shows Go Runtime section (goroutines, GC)
|
||||
// NOTE: This test requires GO_BASE_URL pointing to Go staging (port 82)
|
||||
@@ -909,6 +910,19 @@ async function run() {
|
||||
assert(hexDump, 'Hex dump should be visible after selecting a packet');
|
||||
});
|
||||
|
||||
// Extract frontend coverage if instrumented server is running
|
||||
try {
|
||||
const coverage = await page.evaluate(() => window.__coverage__);
|
||||
if (coverage) {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const outDir = path.join(__dirname, '.nyc_output');
|
||||
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(outDir, 'e2e-coverage.json'), JSON.stringify(coverage));
|
||||
console.log(`Frontend coverage from E2E: ${Object.keys(coverage).length} files`);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
await browser.close();
|
||||
|
||||
// Summary
|
||||
|
||||
@@ -1322,7 +1322,7 @@ console.log('\n=== app.js: formatVersionBadge ===');
|
||||
assert.ok(result.includes('>v2.6.0</a>'), 'version text has v prefix');
|
||||
assert.ok(result.includes(`href="${GH}/commit/abc1234def5678"`), 'commit links to full hash');
|
||||
assert.ok(result.includes('>abc1234</a>'), 'commit display is truncated to 7');
|
||||
assert.ok(result.includes('[node]'), 'should show engine');
|
||||
assert.ok(result.includes('engine-badge'), 'should show engine badge'); assert.ok(result.includes('>node<'), 'should show engine name');
|
||||
});
|
||||
test('prod port 80: shows version', () => {
|
||||
const { formatVersionBadge } = makeBadgeSandbox('80');
|
||||
@@ -1348,7 +1348,7 @@ console.log('\n=== app.js: formatVersionBadge ===');
|
||||
assert.ok(!result.includes('v2.6.0'), 'staging should NOT show version');
|
||||
assert.ok(result.includes('>abc1234</a>'), 'should show commit hash');
|
||||
assert.ok(result.includes(`href="${GH}/commit/abc1234def5678"`), 'commit is linked');
|
||||
assert.ok(result.includes('[go]'), 'should show engine');
|
||||
assert.ok(result.includes('engine-badge'), 'should show engine badge'); assert.ok(result.includes('>go<'), 'should show engine name');
|
||||
});
|
||||
test('staging port 81: hides version', () => {
|
||||
const { formatVersionBadge } = makeBadgeSandbox('81');
|
||||
@@ -1369,18 +1369,18 @@ console.log('\n=== app.js: formatVersionBadge ===');
|
||||
const result = formatVersionBadge('2.6.0', 'unknown', 'node');
|
||||
assert.ok(result.includes('>v2.6.0</a>'), 'should show version');
|
||||
assert.ok(!result.includes('unknown'), 'should not show unknown commit');
|
||||
assert.ok(result.includes('[node]'), 'should show engine');
|
||||
assert.ok(result.includes('engine-badge'), 'should show engine badge'); assert.ok(result.includes('>node<'), 'should show engine name');
|
||||
});
|
||||
test('skips commit when missing', () => {
|
||||
const { formatVersionBadge } = makeBadgeSandbox('');
|
||||
const result = formatVersionBadge('2.6.0', null, 'go');
|
||||
assert.ok(result.includes('>v2.6.0</a>'), 'should show version');
|
||||
assert.ok(result.includes('[go]'), 'should show engine');
|
||||
assert.ok(result.includes('engine-badge'), 'should show engine badge'); assert.ok(result.includes('>go<'), 'should show engine name');
|
||||
});
|
||||
test('shows only engine when version/commit missing', () => {
|
||||
const { formatVersionBadge } = makeBadgeSandbox('3000');
|
||||
const result = formatVersionBadge(null, null, 'go');
|
||||
assert.ok(result.includes('[go]'), 'should show engine');
|
||||
assert.ok(result.includes('engine-badge'), 'should show engine badge'); assert.ok(result.includes('>go<'), 'should show engine name');
|
||||
assert.ok(result.includes('version-badge'), 'should use version-badge class');
|
||||
});
|
||||
test('short commit not truncated in display', () => {
|
||||
@@ -1398,7 +1398,7 @@ console.log('\n=== app.js: formatVersionBadge ===');
|
||||
const { formatVersionBadge } = makeBadgeSandbox('8080');
|
||||
const result = formatVersionBadge('2.6.0', null, 'go');
|
||||
assert.ok(!result.includes('2.6.0'), 'no version on staging');
|
||||
assert.ok(result.includes('[go]'), 'engine shown');
|
||||
assert.ok(result.includes('engine-badge'), 'engine badge shown'); assert.ok(result.includes('>go<'), 'engine name shown');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -59,17 +59,17 @@ console.log('\nloadThemeFile:');
|
||||
console.log('\nbuildHealthConfig:');
|
||||
{
|
||||
const h = helpers.buildHealthConfig({});
|
||||
assert(h.infraDegradedMs === 86400000, 'default infraDegradedMs');
|
||||
assert(h.infraSilentMs === 259200000, 'default infraSilentMs');
|
||||
assert(h.nodeDegradedMs === 3600000, 'default nodeDegradedMs');
|
||||
assert(h.nodeSilentMs === 86400000, 'default nodeSilentMs');
|
||||
assert(h.infraDegraded === 24, 'default infraDegraded');
|
||||
assert(h.infraSilent === 72, 'default infraSilent');
|
||||
assert(h.nodeDegraded === 1, 'default nodeDegraded');
|
||||
assert(h.nodeSilent === 24, 'default nodeSilent');
|
||||
|
||||
const h2 = helpers.buildHealthConfig({ healthThresholds: { infraDegradedMs: 1000 } });
|
||||
assert(h2.infraDegradedMs === 1000, 'custom infraDegradedMs');
|
||||
assert(h2.nodeDegradedMs === 3600000, 'other defaults preserved');
|
||||
const h2 = helpers.buildHealthConfig({ healthThresholds: { infraDegradedHours: 2 } });
|
||||
assert(h2.infraDegraded === 2, 'custom infraDegraded');
|
||||
assert(h2.nodeDegraded === 1, 'other defaults preserved');
|
||||
|
||||
const h3 = helpers.buildHealthConfig(null);
|
||||
assert(h3.infraDegradedMs === 86400000, 'handles null config');
|
||||
assert(h3.infraDegraded === 24, 'handles null config');
|
||||
}
|
||||
|
||||
// --- getHealthMs ---
|
||||
@@ -78,21 +78,21 @@ console.log('\ngetHealthMs:');
|
||||
const HEALTH = helpers.buildHealthConfig({});
|
||||
|
||||
const rep = helpers.getHealthMs('repeater', HEALTH);
|
||||
assert(rep.degradedMs === 86400000, 'repeater uses infra degraded');
|
||||
assert(rep.silentMs === 259200000, 'repeater uses infra silent');
|
||||
assert(rep.degradedMs === 24 * 3600000, 'repeater uses infra degraded');
|
||||
assert(rep.silentMs === 72 * 3600000, 'repeater uses infra silent');
|
||||
|
||||
const room = helpers.getHealthMs('room', HEALTH);
|
||||
assert(room.degradedMs === 86400000, 'room uses infra degraded');
|
||||
assert(room.degradedMs === 24 * 3600000, 'room uses infra degraded');
|
||||
|
||||
const comp = helpers.getHealthMs('companion', HEALTH);
|
||||
assert(comp.degradedMs === 3600000, 'companion uses node degraded');
|
||||
assert(comp.silentMs === 86400000, 'companion uses node silent');
|
||||
assert(comp.degradedMs === 1 * 3600000, 'companion uses node degraded');
|
||||
assert(comp.silentMs === 24 * 3600000, 'companion uses node silent');
|
||||
|
||||
const sensor = helpers.getHealthMs('sensor', HEALTH);
|
||||
assert(sensor.degradedMs === 3600000, 'sensor uses node degraded');
|
||||
assert(sensor.degradedMs === 1 * 3600000, 'sensor uses node degraded');
|
||||
|
||||
const undef = helpers.getHealthMs(undefined, HEALTH);
|
||||
assert(undef.degradedMs === 3600000, 'undefined role uses node degraded');
|
||||
assert(undef.degradedMs === 1 * 3600000, 'undefined role uses node degraded');
|
||||
}
|
||||
|
||||
// --- isHashSizeFlipFlop ---
|
||||
|
||||
Reference in New Issue
Block a user