not->toBeFalse(); expect($launcherSource)->not->toBeFalse(); expect($composeSource)->not->toBeFalse(); expect($agentSource)->not->toBeFalse(); expect($edgeDockerfileSource)->not->toBeFalse(); expect($workerDockerfileSource)->not->toBeFalse(); expect($workerSource)->not->toBeFalse(); expect($autoUpdaterSource)->not->toBeFalse(); expect($autoUpdaterDockerfileSource)->not->toBeFalse(); expect($managerSource)->toContain("'operationPollTimeoutSeconds' => self::COMMAND_POLL_TIMEOUT_SECONDS"); expect($managerSource)->toContain("public const DEFAULT_INSTALL_VERSION = 'compose-php-agent-v3'"); expect($managerSource)->toContain("'installedVersion' => self::DEFAULT_INSTALL_VERSION"); expect($managerSource)->toContain('fetch_http "Verify install token" "__VERIFY_URL__"'); expect($managerSource)->toContain('fetch_http "Download artifact manifest" "__MANIFEST_URL__" "$INSTALL_DIR/manifest.json"'); 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('verify_manifest_artifact()'); expect($managerSource)->toContain('begin_install_phase "VERIFY_ARTIFACTS" "Verifying edge gateway artifacts"'); expect($managerSource)->toContain('run_step "Verifying PHP edge agent" verify_manifest_artifact "$INSTALL_DIR/manifest.json" "agent.php" "$INSTALL_DIR/agent.php"'); 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('begin_install_phase "REMOVE_EXISTING_INSTALL" "Removing existing edge gateway installation"'); expect($managerSource)->toContain('run_step "Removing existing edge gateway installation" cleanup_existing_installation'); expect($managerSource)->toContain('cleanup_existing_installation()'); expect($managerSource)->toContain('systemctl stop truckwash-edge-gateway-stack.service'); expect($managerSource)->toContain('systemctl stop truckwash-edge-agent.service'); expect($managerSource)->toContain('TRUCKWASH_INSTALL_DIR="$INSTALL_DIR" "$INSTALL_DIR/gateway-launcher.sh" down >/dev/null 2>&1 || true'); expect($managerSource)->toContain('docker rm -f'); expect($managerSource)->toContain('remove_edge_gateway_containers()'); expect($managerSource)->toContain('remove_docker_containers_matching_filter "name=${name}"'); expect($managerSource)->toContain('remove_docker_containers_matching_filter "label=com.docker.compose.project=${project_name}"'); expect($managerSource)->toContain('remove_docker_containers_matching_filter "label=com.docker.compose.project.working_dir=${INSTALL_DIR}"'); expect($managerSource)->toContain('truckwash-minio'); expect($managerSource)->toContain('rm -f "$STACK_SERVICE_PATH" "$LEGACY_SERVICE_PATH"'); expect($managerSource)->toContain('rm -rf "$INSTALL_DIR"'); expect($managerSource)->toContain('systemctl reset-failed truckwash-edge-agent.service >/dev/null 2>&1 || true'); expect($managerSource)->toContain('return 0'); 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('print_collected_diagnostics()'); expect($managerSource)->toContain('log_error "Diagnostic: ${DIAGNOSTIC_NAMES[$index]}"'); expect($managerSource)->toContain("collect_install_diagnostics\n diagnostics_json=\"$(emit_diagnostic_json)\""); expect($managerSource)->not->toContain('diagnostics_json="$(collect_install_diagnostics)"'); expect($managerSource)->toContain('print_collected_diagnostics'); expect($managerSource)->toContain('run_step "Writing fresh agent config" cp "$CONFIG_TEMPLATE_PATH" "$CONFIG_PATH"'); expect($managerSource)->not->toContain('REUSE_EXISTING_CREDENTIALS'); expect($managerSource)->not->toContain('merge_agent_config()'); 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)->not->toContain('run_step "Waiting for post-reinstall heartbeat"'); expect($managerSource)->not->toContain('Gateway reconnected using preserved credentials.'); expect($managerSource)->toContain('journalctl -a -u truckwash-edge-gateway-stack.service -n 80 --no-pager'); expect($managerSource)->toContain("docker ps -a --format '{{.Names}} {{.Status}} {{.Ports}}'"); expect($managerSource)->toContain('logs --no-color --no-log-prefix --tail=120'); expect($managerSource)->not->toContain('agent.mjs'); expect($managerSource)->toContain("'brokerUrl' => \$this->buildBrokerPublicUrl()"); expect($installServiceSource)->toContain('normalizeLineEndings($this->manager()->buildInstallScript($plainToken))'); expect($installServiceSource)->toContain("'manifest.json' => 'application/json; charset=utf-8'"); expect($installServiceSource)->toContain("'version' => edge_gateway_manager::DEFAULT_INSTALL_VERSION"); expect($installServiceSource)->toContain('str_replace(["\r\n", "\r"], "\n", $contents)'); 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('ensure_stack_env'); expect($launcherSource)->toContain('REDIS_PASSWORD'); expect($launcherSource)->toContain('chmod 0600 "$ENV_FILE"'); expect($launcherSource)->toContain('staged_update_value()'); expect($launcherSource)->toContain('window="$(staged_update_value update_window \'\')"'); expect($launcherSource)->toContain('Staged update already applied; nothing to reconcile'); expect($launcherSource)->toContain('AUTO_UPDATER_BASE_IMAGE="$auto_updater_base_image"'); expect($launcherSource)->toContain("docker ps -a --format '{{.Names}} {{.Status}} {{.Ports}}'"); expect($launcherSource)->toContain('COMPOSE_PROJECT_NAME="$compose_project_name" compose_cmd -f "$COMPOSE_FILE" ps -a --no-trunc || true'); expect($launcherSource)->toContain('COMPOSE_PROJECT_NAME="$compose_project_name" compose_cmd -f "$COMPOSE_FILE" logs --no-color --no-log-prefix --tail=120 || 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[\\"started_at\\"]'); expect($composeSource)->not->toContain('$$data[\\"last_successful_sync_at\\"]'); expect($composeSource)->toContain('<= 120'); 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('./config.json:/config/config.json:ro'); 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('--requirepass'); expect($composeSource)->toContain('${REDIS_PASSWORD:?set REDIS_PASSWORD}'); expect($composeSource)->toContain('${MARIADB_PASSWORD:?set MARIADB_PASSWORD}'); expect($composeSource)->toContain('${MARIADB_ROOT_PASSWORD:?set MARIADB_ROOT_PASSWORD}'); expect($composeSource)->toContain('${MINIO_ROOT_USER:?set MINIO_ROOT_USER}'); expect($composeSource)->toContain('${MINIO_ROOT_PASSWORD:?set MINIO_ROOT_PASSWORD}'); expect($composeSource)->not->toContain('MARIADB_PASSWORD: truckwash_edge'); expect($composeSource)->not->toContain('MARIADB_ROOT_PASSWORD: truckwash_edge_root'); expect($composeSource)->not->toContain('MINIO_ROOT_USER: truckwashminio'); expect($composeSource)->not->toContain('MINIO_ROOT_PASSWORD: truckwash_edge_storage'); 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('compose-php-agent-v3'); 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($agentSource)->toContain( "private function reloadConfigFromDisk(): void\n" . " {\n" . " \$reloaded = AgentConfig::load(\$this->config->path);\n" . " \$this->config = \$reloaded;\n" . " \$this->workerHttp = new HttpJsonClient(\n" . " (string)\$this->config->get('workerBaseUrl', self::DEFAULT_WORKER_BASE_URL),\n" . " \$this->workerAuthorizationHeaders()\n" . ' );' ); expect($edgeDockerfileSource)->toContain('FROM ${BASE_IMAGE}'); expect($edgeDockerfileSource)->not->toContain('docker-php-ext-install'); expect($edgeDockerfileSource)->toContain('extension_loaded($extension)'); expect($edgeDockerfileSource)->toContain('"pdo_sqlite"'); 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)->not->toContain('docker-php-ext-install'); expect($workerDockerfileSource)->toContain('extension_loaded($extension)'); expect($workerDockerfileSource)->toContain('"pdo_sqlite"'); expect($workerDockerfileSource)->toContain('Missing PHP extension: {$extension}'); expect($workerDockerfileSource)->toContain('COPY lan-worker.php /opt/truckwash-edge-agent/lan-worker.php'); expect($workerSource)->toContain('worker_require_authorization'); expect($agentSource)->toContain('X-Truckwash-Worker-Token: '); 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;'); expect($autoUpdaterDockerfileSource)->not->toContain('docker-php-ext-install'); expect($autoUpdaterDockerfileSource)->toContain('extension_loaded($extension)'); expect($autoUpdaterDockerfileSource)->toContain('"pdo_sqlite"'); 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('private function normalizeBrokerUrlForComparison(?string $value): ?string'); expect($agentSource)->toContain('$currentNormalized = $this->normalizeBrokerUrlForComparison($current);'); 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::OUTBOX_OPERATION_COMPLETE_REPLAY_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'"); });