167 lines
15 KiB
PHP
167 lines
15 KiB
PHP
<?php
|
|
|
|
it('builds the installer around the compose stack artifacts and management polling config', function (): void {
|
|
$managerSource = file_get_contents(app_path('classes/edge_gateway_manager.php'));
|
|
$serviceSource = file_get_contents(app_path('resources/edge-gateway-agent/truckwash-edge-agent.service'));
|
|
$stackServiceSource = file_get_contents(app_path('resources/edge-gateway-agent/truckwash-edge-gateway-stack.service'));
|
|
$launcherSource = file_get_contents(app_path('resources/edge-gateway-agent/gateway-launcher.sh'));
|
|
$composeSource = file_get_contents(app_path('resources/edge-gateway-agent/docker-compose.gateway.yml'));
|
|
$agentSource = file_get_contents(app_path('resources/edge-gateway-agent/agent.php'));
|
|
$edgeDockerfileSource = file_get_contents(app_path('resources/edge-gateway-agent/Dockerfile.edge-agent'));
|
|
$workerDockerfileSource = file_get_contents(app_path('resources/edge-gateway-agent/Dockerfile.lan-worker'));
|
|
$autoUpdaterSource = file_get_contents(app_path('resources/edge-gateway-agent/auto-updater.php'));
|
|
$autoUpdaterDockerfileSource = file_get_contents(app_path('resources/edge-gateway-agent/Dockerfile.auto-updater'));
|
|
|
|
expect($managerSource)->not->toBeFalse();
|
|
expect($launcherSource)->not->toBeFalse();
|
|
expect($composeSource)->not->toBeFalse();
|
|
expect($agentSource)->not->toBeFalse();
|
|
expect($edgeDockerfileSource)->not->toBeFalse();
|
|
expect($workerDockerfileSource)->not->toBeFalse();
|
|
expect($autoUpdaterSource)->not->toBeFalse();
|
|
expect($autoUpdaterDockerfileSource)->not->toBeFalse();
|
|
expect($managerSource)->toContain("'operationPollTimeoutSeconds' => self::COMMAND_POLL_TIMEOUT_SECONDS");
|
|
expect($managerSource)->toContain('fetch_http "Verify install token" "__VERIFY_URL__"');
|
|
expect($managerSource)->toContain('fetch_http "Download PHP edge agent" "__AGENT_URL__" "$INSTALL_DIR/agent.php"');
|
|
expect($managerSource)->toContain('fetch_http "Download LAN worker" "__WORKER_URL__" "$INSTALL_DIR/lan-worker.php"');
|
|
expect($managerSource)->toContain('fetch_http "Download auto-updater" "__AUTO_UPDATER_URL__" "$INSTALL_DIR/auto-updater.php"');
|
|
expect($managerSource)->toContain('fetch_http "Download compose stack" "__COMPOSE_URL__" "$INSTALL_DIR/docker-compose.gateway.yml"');
|
|
expect($managerSource)->toContain('fetch_http "Download auto-updater Dockerfile" "__AUTO_UPDATER_DOCKERFILE_URL__" "$INSTALL_DIR/Dockerfile.auto-updater"');
|
|
expect($managerSource)->toContain('fetch_http "Download compose stack service unit" "__STACK_SERVICE_URL__" "$INSTALL_DIR/truckwash-edge-gateway-stack.service"');
|
|
expect($managerSource)->toContain('log_error "Request: GET ${url}"');
|
|
expect($managerSource)->toContain('log_error "Response body preview (first 400 bytes):"');
|
|
expect($managerSource)->toContain('run_step "Installing base packages" apt-get install -y curl ca-certificates docker.io php-cli php-curl php-mbstring php-sqlite3');
|
|
expect($managerSource)->toContain('run_step "Installing Docker Compose runtime" install_compose_runtime');
|
|
expect($managerSource)->toContain('apt-get install -y docker-compose-plugin');
|
|
expect($managerSource)->toContain('apt-get install -y docker-compose');
|
|
expect($managerSource)->toContain('Unable to install Docker Compose using docker-compose-plugin or docker-compose.');
|
|
expect($managerSource)->toContain('Existing claimed gateway detected; reinstall will reuse saved gateway credentials.');
|
|
expect($managerSource)->toContain('report_install_status() {');
|
|
expect($managerSource)->toContain('begin_install_phase "START_STACK" "Starting edge gateway stack"');
|
|
expect($managerSource)->toContain('report_install_status "RUNNING" "$CURRENT_STEP_CODE" "$CURRENT_STEP"');
|
|
expect($managerSource)->toContain('report_install_status "FAILED" "FAILED" "$failure_message" "$diagnostics_json" "$gateway_id"');
|
|
expect($managerSource)->toContain('run_step "Writing agent config" merge_agent_config "$CONFIG_TEMPLATE_PATH" "$CONFIG_PATH"');
|
|
expect($managerSource)->toContain('chmod 0755 "$INSTALL_DIR/agent.php" "$INSTALL_DIR/lan-worker.php" "$INSTALL_DIR/auto-updater.php" "$INSTALL_DIR/gateway-launcher.sh"');
|
|
expect($managerSource)->toContain('systemctl enable truckwash-edge-gateway-stack.service');
|
|
expect($managerSource)->toContain('systemctl restart truckwash-edge-gateway-stack.service');
|
|
expect($managerSource)->toContain('systemctl is-active --quiet truckwash-edge-gateway-stack.service');
|
|
expect($managerSource)->toContain('run_step "Waiting for gateway claim" wait_for_gateway_claim "$CONFIG_PATH" "$HEARTBEAT_MARKER_PATH" "$INSTALL_STARTED_AT" 180');
|
|
expect($managerSource)->toContain('run_step "Waiting for post-reinstall heartbeat" wait_for_post_restart_heartbeat "$HEARTBEAT_MARKER_PATH" "$INSTALL_STARTED_AT" 180');
|
|
expect($managerSource)->toContain('journalctl -u truckwash-edge-gateway-stack.service -n 60 --no-pager || true');
|
|
expect($managerSource)->not->toContain('agent.mjs');
|
|
expect($managerSource)->toContain("'brokerUrl' => \$this->buildBrokerPublicUrl()");
|
|
expect($serviceSource)->toContain('Description=TruckWash Edge Agent Compatibility Unit');
|
|
expect($serviceSource)->toContain('ExecStart=/opt/truckwash-edge-agent/gateway-launcher.sh up');
|
|
expect($serviceSource)->toContain('ExecReload=/opt/truckwash-edge-agent/gateway-launcher.sh reconcile');
|
|
expect($serviceSource)->toContain('ExecStop=/opt/truckwash-edge-agent/gateway-launcher.sh down');
|
|
expect($serviceSource)->not->toContain('/usr/bin/php /opt/truckwash-edge-agent/agent.php');
|
|
expect($stackServiceSource)->toContain('ExecStart=/opt/truckwash-edge-agent/gateway-launcher.sh up');
|
|
expect($stackServiceSource)->toContain('TimeoutStartSec=900');
|
|
expect($launcherSource)->toContain('STACK_HEALTHCHECK_TIMEOUT_SECONDS="${STACK_HEALTHCHECK_TIMEOUT_SECONDS:-300}"');
|
|
expect($launcherSource)->toContain('wait_for_stack_health');
|
|
expect($launcherSource)->toContain('container_is_healthy truckwash-auto-updater');
|
|
expect($launcherSource)->toContain('compose_project_name="$(config_value composeProjectName \'truckwash-edge-gateway\')"');
|
|
expect($launcherSource)->toContain('COMPOSE_PROJECT_NAME="$compose_project_name"');
|
|
expect($launcherSource)->toContain('AUTO_UPDATER_BASE_IMAGE="$auto_updater_base_image"');
|
|
expect($launcherSource)->toContain('compose_cmd -f "$COMPOSE_FILE" logs --tail=80 || true');
|
|
expect($launcherSource)->toContain('log "Compose rollout failed during build/startup"');
|
|
expect($launcherSource)->toContain('write_rollback_status "FAILED" "compose_up_failed" "$installed_version"');
|
|
expect($launcherSource)->toContain('write_rollback_status "FAILED" "rollback_apply_failed" "$installed_version"');
|
|
expect($launcherSource)->toContain('write_rollback_status "ROLLED_BACK" "healthcheck_failed" "$installed_version"');
|
|
$normalizedComposeSource = str_replace("\r\n", "\n", $composeSource);
|
|
expect($normalizedComposeSource)->toContain('version: "2.4"');
|
|
expect($normalizedComposeSource)->toContain('condition: service_healthy');
|
|
expect($normalizedComposeSource)->toContain("minio:\n condition: service_started");
|
|
expect($normalizedComposeSource)->toContain("mariadb:\n condition: service_started");
|
|
expect($composeSource)->toContain('/opt/truckwash-edge-agent/runtime/control-plane-status.json');
|
|
expect($composeSource)->toContain('$$data[\\"last_loop_at\\"]');
|
|
expect($composeSource)->toContain('$$data[\\"last_successful_sync_at\\"]');
|
|
expect($composeSource)->toContain('<= 30');
|
|
expect($composeSource)->toContain('<= 90');
|
|
expect($composeSource)->toContain("http://127.0.0.1:8090/health");
|
|
expect($composeSource)->toContain('$$json=@file_get_contents(\'http://127.0.0.1:8090/health\');');
|
|
expect($composeSource)->toContain('$$path=\"/opt/truckwash-edge-agent/runtime/auto-updater-heartbeat.json\"');
|
|
expect($composeSource)->not->toContain("curl\", \"-fsS\", \"http://127.0.0.1:9000/minio/health/live");
|
|
expect($composeSource)->not->toContain('mysqladmin ping -h 127.0.0.1 -uroot -ptruckwash_edge_root --silent');
|
|
expect($composeSource)->toContain('container_name: truckwash-redis');
|
|
expect($composeSource)->toContain('container_name: truckwash-mariadb');
|
|
expect($composeSource)->toContain('container_name: truckwash-minio');
|
|
expect($composeSource)->toContain('container_name: truckwash-auto-updater');
|
|
expect($agentSource)->toContain('private string $controlPlaneStatusPath;');
|
|
expect($agentSource)->toContain('control-plane-status.json');
|
|
expect($agentSource)->toContain("'control_plane_status' => \$this->buildControlPlaneStatusPayload()");
|
|
expect($agentSource)->toContain("'last_heartbeat_attempt_at'");
|
|
expect($agentSource)->toContain("'last_heartbeat_success_at'");
|
|
expect($agentSource)->toContain("'last_successful_sync_at'");
|
|
expect($agentSource)->toContain("'last_transport_failure_at'");
|
|
expect($agentSource)->toContain("'last_transport_error'");
|
|
expect($agentSource)->toContain('private function recordTransportFailure(string $context, Throwable $throwable): void');
|
|
expect($edgeDockerfileSource)->toContain('FROM ${BASE_IMAGE}');
|
|
expect($edgeDockerfileSource)->toContain('apt-get install -y --no-install-recommends libcurl4-openssl-dev libsqlite3-dev;');
|
|
expect($edgeDockerfileSource)->toContain('docker-php-ext-install -j"$(nproc)" curl sqlite3 pdo_sqlite;');
|
|
expect($edgeDockerfileSource)->toContain('extension_loaded($extension)');
|
|
expect($edgeDockerfileSource)->toContain('Missing PHP extension: {$extension}');
|
|
expect($edgeDockerfileSource)->toContain('COPY agent.php /opt/truckwash-edge-agent/agent.php');
|
|
expect($workerDockerfileSource)->toContain('FROM ${BASE_IMAGE}');
|
|
expect($workerDockerfileSource)->toContain('apt-get install -y --no-install-recommends libcurl4-openssl-dev libsqlite3-dev;');
|
|
expect($workerDockerfileSource)->toContain('docker-php-ext-install -j"$(nproc)" curl sqlite3 pdo_sqlite;');
|
|
expect($workerDockerfileSource)->toContain('extension_loaded($extension)');
|
|
expect($workerDockerfileSource)->toContain('Missing PHP extension: {$extension}');
|
|
expect($workerDockerfileSource)->toContain('COPY lan-worker.php /opt/truckwash-edge-agent/lan-worker.php');
|
|
expect($autoUpdaterSource)->toContain("'/bin/bash ' . escapeshellarg(\$launcherPath) . ' reconcile 2>&1'");
|
|
expect($autoUpdaterDockerfileSource)->toContain('COPY auto-updater.php /usr/local/bin/auto-updater.php');
|
|
expect($autoUpdaterDockerfileSource)->toContain('apt-get install -y --no-install-recommends bash ca-certificates curl docker.io docker-compose libcurl4-openssl-dev libsqlite3-dev;');
|
|
expect($autoUpdaterDockerfileSource)->toContain('docker-php-ext-install -j"$(nproc)" curl sqlite3 pdo_sqlite;');
|
|
expect($autoUpdaterDockerfileSource)->toContain('extension_loaded($extension)');
|
|
expect($serviceSource)->not->toContain('node /opt/truckwash-edge-agent/agent.mjs');
|
|
});
|
|
|
|
it('exposes update payload, credential rotation, cancel endpoints, and operation endpoints without shell transport wiring', function (): void {
|
|
$managerSource = file_get_contents(app_path('classes/edge_gateway_manager.php'));
|
|
$routeSource = file_get_contents(app_path('routes/edgeGatewaysRoute.php'));
|
|
$agentSource = file_get_contents(app_path('resources/edge-gateway-agent/agent.php'));
|
|
|
|
expect($managerSource)->toContain('public function buildUpdateOperationRequest');
|
|
expect($managerSource)->toContain('public function rotateGatewayCredentials');
|
|
expect($managerSource)->toContain("'autoUpdaterArtifactUrl' => \$this->buildAgentArtifactUrl(self::DEFAULT_AUTO_UPDATER_ARTIFACT)");
|
|
expect($managerSource)->toContain("'redisBaseImage' => self::DEFAULT_REDIS_BASE_IMAGE");
|
|
expect($managerSource)->toContain("'brokerUrl' => \$this->buildBrokerPublicUrl()");
|
|
expect($routeSource)->toContain("'/edge-gateways/{id}/rotate-credentials'");
|
|
expect($routeSource)->toContain("'/edge-gateways/{id}/operations/{operationId}/cancel'");
|
|
expect($routeSource)->toContain("'/edge-agent/gateways/{id}/operations/next'");
|
|
expect($routeSource)->toContain("'/edge-gateways/{id}/shell-sessions'");
|
|
expect($agentSource)->toContain("/operations/next");
|
|
expect($agentSource)->toContain("/operations/' . \$operationId . '/complete");
|
|
expect($agentSource)->toContain('final class OperationAbortException extends RuntimeException');
|
|
expect($agentSource)->toContain('private BrokerWebSocketClient $brokerClient;');
|
|
expect($agentSource)->toContain('private AgentShellBridge $shellBridge;');
|
|
expect($agentSource)->toContain('private function dispatchBrokerControlPlaneEvent(string $endpoint, array $payload): ?bool');
|
|
expect($agentSource)->toContain('private function shouldPersistControlPlaneEventOverHttp(string $endpoint): bool');
|
|
expect($agentSource)->toContain('if ($brokerDispatch === true && !$this->shouldPersistControlPlaneEventOverHttp($endpoint))');
|
|
expect($agentSource)->toContain('if ($brokerDispatch !== true || $this->shouldPersistControlPlaneEventOverHttp($endpoint))');
|
|
expect($agentSource)->toContain('Broker dispatched \' . $type . \' but HTTP persistence failed on \' . $endpoint');
|
|
expect($agentSource)->toContain("'type' => 'TELEMETRY'");
|
|
expect($agentSource)->toContain("'type' => 'TASK_EVENT'");
|
|
expect($agentSource)->toContain("'type' => 'TASK_RESULT'");
|
|
expect($agentSource)->toContain("'type' => 'LOG_FRAME'");
|
|
expect($agentSource)->toContain('private function requestControlPlaneEvent(string $endpoint, array $payload, string $type, int $timeoutSeconds = 20): ?array');
|
|
expect($agentSource)->toContain('throw new OperationAbortException(\'Operation cancelled by operator\', true);');
|
|
expect($agentSource)->toContain('private const OPERATION_COMPLETE_TIMEOUT_SECONDS = 120;');
|
|
expect($agentSource)->toContain('if ($this->resumePendingOperationCompletion()) {');
|
|
expect($agentSource)->toContain('$this->finalizeOperationCompletion(');
|
|
expect($agentSource)->toContain('private function finalizeOperationCompletion(');
|
|
expect($agentSource)->toContain('private function resumePendingOperationCompletion(): bool');
|
|
expect($agentSource)->toContain('private function dispatchOperationCompletion(array $completion, bool $queueOnFailure): bool');
|
|
expect($agentSource)->toContain("\$state['status'] = 'COMPLETION_PENDING';");
|
|
expect($agentSource)->toContain("\$state['stage'] = 'awaiting_completion_ack';");
|
|
expect($agentSource)->toContain("? self::OPERATION_COMPLETE_TIMEOUT_SECONDS");
|
|
expect($agentSource)->toContain("unset(\$state['completion']);");
|
|
expect($agentSource)->toContain('$services[] = $this->probeTcpService(\'redis\', \'redis\', 6379);');
|
|
expect($agentSource)->toContain('final class HttpRequestTimeoutException extends RuntimeException');
|
|
expect($agentSource)->toContain('], $this->pollRequestTimeoutSeconds($waitSeconds));');
|
|
expect($agentSource)->toContain('} catch (HttpRequestTimeoutException) {');
|
|
expect($agentSource)->toContain('private function pollRequestTimeoutSeconds(int $waitSeconds): int');
|
|
expect($agentSource)->toContain('last-heartbeat-ok.txt');
|
|
expect($routeSource)->not->toContain("'/edge-agent/gateways/{id}/shell-actions/poll'");
|
|
});
|