feat: architecture refactor — validators, positions, menuUtils, ESLint, tests, CI
Major improvements across the codebase: - Extract validationUtils.js (548→217 lines) into strategy pattern validators - Extract menuUtils.js (543→35 lines) into 6 focused menu modules - Adopt POSITIONS constants across 23 files (183 replacements) - Eliminate all 71 ESLint warnings (0 errors, 0 warnings) - Add 158 unit tests for ConfigManager, MeasurementContainer, ValidationUtils - Add architecture documentation with Mermaid diagrams - Add CI pipeline (Docker, ESLint, Jest, Makefile) - Add E2E infrastructure (docker-compose.e2e.yml) Test results: 377 total (230 Jest + 23 node:test + 124 legacy), all passing Lint: 0 errors, 0 warnings Closes #2, #3, #9, #13, #14, #18 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
71
test/e2e/flows.json
Normal file
71
test/e2e/flows.json
Normal file
@@ -0,0 +1,71 @@
|
||||
[
|
||||
{
|
||||
"id": "e2e-flow-tab",
|
||||
"type": "tab",
|
||||
"label": "E2E Test Flow",
|
||||
"disabled": false,
|
||||
"info": "End-to-end test flow that verifies EVOLV nodes are loaded and messages can flow through the pipeline."
|
||||
},
|
||||
{
|
||||
"id": "inject-trigger",
|
||||
"type": "inject",
|
||||
"z": "e2e-flow-tab",
|
||||
"name": "Trigger after 2s",
|
||||
"props": [
|
||||
{
|
||||
"p": "payload"
|
||||
},
|
||||
{
|
||||
"p": "topic",
|
||||
"vt": "str"
|
||||
}
|
||||
],
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": true,
|
||||
"onceDelay": "2",
|
||||
"topic": "e2e-test",
|
||||
"payload": "",
|
||||
"payloadType": "date",
|
||||
"x": 150,
|
||||
"y": 100,
|
||||
"wires": [
|
||||
["simulate-measurement"]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "simulate-measurement",
|
||||
"type": "function",
|
||||
"z": "e2e-flow-tab",
|
||||
"name": "Simulate Measurement",
|
||||
"func": "msg.payload = {\n measurement: 'e2e_test',\n tags: { source: 'evolv-e2e', node: 'measurement-sim' },\n fields: { value: Math.random() * 100, status: 1 },\n timestamp: Date.now()\n};\nmsg.topic = 'e2e/measurement';\nnode.status({ fill: 'green', shape: 'dot', text: 'sent' });\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"timeout": "",
|
||||
"noerr": 0,
|
||||
"initialize": "",
|
||||
"finalize": "",
|
||||
"libs": [],
|
||||
"x": 370,
|
||||
"y": 100,
|
||||
"wires": [
|
||||
["debug-output"]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "debug-output",
|
||||
"type": "debug",
|
||||
"z": "e2e-flow-tab",
|
||||
"name": "E2E Debug Output",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"console": true,
|
||||
"tostatus": true,
|
||||
"complete": "true",
|
||||
"targetType": "full",
|
||||
"statusVal": "",
|
||||
"statusType": "auto",
|
||||
"x": 580,
|
||||
"y": 100,
|
||||
"wires": []
|
||||
}
|
||||
]
|
||||
142
test/e2e/run-e2e.sh
Executable file
142
test/e2e/run-e2e.sh
Executable file
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# End-to-end test runner for EVOLV Node-RED stack.
|
||||
# Starts Node-RED + InfluxDB + Grafana via Docker Compose,
|
||||
# verifies that EVOLV nodes are registered in the palette,
|
||||
# and tears down the stack on exit.
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
COMPOSE_FILE="$PROJECT_ROOT/docker-compose.e2e.yml"
|
||||
|
||||
NODERED_URL="http://localhost:1880"
|
||||
MAX_WAIT=120 # seconds to wait for Node-RED to become healthy
|
||||
|
||||
# EVOLV node types that must appear in the palette (from package.json node-red.nodes)
|
||||
EXPECTED_NODES=(
|
||||
"dashboardapi"
|
||||
"machineGroupControl"
|
||||
"measurement"
|
||||
"monster"
|
||||
"pumpingstation"
|
||||
"reactor"
|
||||
"rotatingMachine"
|
||||
"settler"
|
||||
"valve"
|
||||
"valveGroupControl"
|
||||
)
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||||
|
||||
# Determine docker compose command (handle permission via sg docker if needed)
|
||||
DOCKER_COMPOSE="docker compose"
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
if sg docker -c "docker info" >/dev/null 2>&1; then
|
||||
DOCKER_COMPOSE="sg docker -c 'docker compose'"
|
||||
log_info "Using sg docker for Docker access"
|
||||
else
|
||||
log_error "Docker is not accessible. Please ensure Docker is running and you have permissions."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
cleanup() {
|
||||
log_info "Tearing down E2E stack..."
|
||||
eval $DOCKER_COMPOSE -f "$COMPOSE_FILE" down --volumes --remove-orphans 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Always clean up on exit
|
||||
trap cleanup EXIT
|
||||
|
||||
# --- Step 1: Build and start the stack ---
|
||||
log_info "Building and starting E2E stack..."
|
||||
eval $DOCKER_COMPOSE -f "$COMPOSE_FILE" up -d --build
|
||||
|
||||
# --- Step 2: Wait for Node-RED to be healthy ---
|
||||
log_info "Waiting for Node-RED to become healthy (max ${MAX_WAIT}s)..."
|
||||
elapsed=0
|
||||
while [ $elapsed -lt $MAX_WAIT ]; do
|
||||
if curl -sf "$NODERED_URL/" >/dev/null 2>&1; then
|
||||
log_info "Node-RED is up after ${elapsed}s"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
elapsed=$((elapsed + 2))
|
||||
done
|
||||
|
||||
if [ $elapsed -ge $MAX_WAIT ]; then
|
||||
log_error "Node-RED did not become healthy within ${MAX_WAIT}s"
|
||||
log_error "Container logs:"
|
||||
eval $DOCKER_COMPOSE -f "$COMPOSE_FILE" logs nodered
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Give Node-RED a few extra seconds to finish loading all nodes
|
||||
sleep 5
|
||||
|
||||
# --- Step 3: Verify EVOLV nodes are registered in the palette ---
|
||||
log_info "Querying Node-RED for registered nodes..."
|
||||
NODES_RESPONSE=$(curl -sf "$NODERED_URL/nodes" 2>&1) || {
|
||||
log_error "Failed to query Node-RED /nodes endpoint"
|
||||
exit 1
|
||||
}
|
||||
|
||||
FAILURES=0
|
||||
for node_type in "${EXPECTED_NODES[@]}"; do
|
||||
if echo "$NODES_RESPONSE" | grep -qi "$node_type"; then
|
||||
log_info " [PASS] Node type '$node_type' found in palette"
|
||||
else
|
||||
log_error " [FAIL] Node type '$node_type' NOT found in palette"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
# --- Step 4: Verify flows are deployed ---
|
||||
log_info "Checking deployed flows..."
|
||||
FLOWS_RESPONSE=$(curl -sf "$NODERED_URL/flows" 2>&1) || {
|
||||
log_error "Failed to query Node-RED /flows endpoint"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if echo "$FLOWS_RESPONSE" | grep -q "e2e-flow-tab"; then
|
||||
log_info " [PASS] E2E test flow is deployed"
|
||||
else
|
||||
log_warn " [WARN] E2E test flow not found in deployed flows (may need manual deploy)"
|
||||
fi
|
||||
|
||||
# --- Step 5: Verify InfluxDB is reachable ---
|
||||
log_info "Checking InfluxDB health..."
|
||||
INFLUX_HEALTH=$(curl -sf "http://localhost:8086/health" 2>&1) || {
|
||||
log_error "Failed to reach InfluxDB health endpoint"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
}
|
||||
|
||||
if echo "$INFLUX_HEALTH" | grep -q '"status":"pass"'; then
|
||||
log_info " [PASS] InfluxDB is healthy"
|
||||
else
|
||||
log_error " [FAIL] InfluxDB health check failed"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
|
||||
# --- Step 6: Summary ---
|
||||
echo ""
|
||||
if [ $FAILURES -eq 0 ]; then
|
||||
log_info "========================================="
|
||||
log_info " E2E tests PASSED - all checks green"
|
||||
log_info "========================================="
|
||||
exit 0
|
||||
else
|
||||
log_error "========================================="
|
||||
log_error " E2E tests FAILED - $FAILURES check(s) failed"
|
||||
log_error "========================================="
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user