From 9cfb19a3ba17f31d67486e183fe2c8f39d6e8649 Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Wed, 22 Apr 2026 12:03:08 +0200 Subject: [PATCH] Replace CI PHP suite execution script with Composer commands and integrate Edge Gateway Agent stack artifacts --- .github/workflows/tests.yml | 190 +++-- README.md | 4 +- docker-compose.yml | 33 +- scripts/ci/run-php-suite.sh | 184 ----- scripts/test-gateway.mjs | 18 +- services/edge-agent/Dockerfile.test-gateway | 3 +- services/edge-agent/dist/agent.mjs | 51 +- .../nginx/app/.phpunit.cache/test-results | 2 +- .../classes/edge_gateway_install_service.php | 6 + .../app/classes/edge_gateway_manager.php | 358 +++++++- .../edge_gateway_operation_service.php | 27 +- services/nginx/app/objects/users_o.php | 40 +- .../edge-gateway-agent/Dockerfile.edge-agent | 19 + .../edge-gateway-agent/Dockerfile.lan-worker | 17 + .../resources/edge-gateway-agent/agent.php | 775 +++++++++++++++--- .../docker-compose.gateway.yml | 38 + .../edge-gateway-agent/gateway-launcher.sh | 194 +++++ .../edge-gateway-agent/lan-worker.php | 146 ++++ .../truckwash-edge-gateway-stack.service | 16 + .../nginx/app/routes/edgeGatewaysRoute.php | 6 + .../EdgeGatewayArtifactLocatorTest.php | 7 +- .../Selfserve/EdgeGatewayManagerUrlTest.php | 9 +- .../Selfserve/EdgeGatewayRouteWiringTest.php | 6 + .../EdgeGatewayUpdateLifecycleTest.php | 18 +- .../Users/UsersCustomerNamesCacheTest.php | 38 + services/php/Dockerfile | 4 +- 26 files changed, 1722 insertions(+), 487 deletions(-) delete mode 100755 scripts/ci/run-php-suite.sh create mode 100644 services/nginx/app/resources/edge-gateway-agent/Dockerfile.edge-agent create mode 100644 services/nginx/app/resources/edge-gateway-agent/Dockerfile.lan-worker create mode 100644 services/nginx/app/resources/edge-gateway-agent/docker-compose.gateway.yml create mode 100644 services/nginx/app/resources/edge-gateway-agent/gateway-launcher.sh create mode 100644 services/nginx/app/resources/edge-gateway-agent/lan-worker.php create mode 100644 services/nginx/app/resources/edge-gateway-agent/truckwash-edge-gateway-stack.service create mode 100644 services/nginx/app/tests/Unit/Users/UsersCustomerNamesCacheTest.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 71286101..dc16e91e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,26 +22,25 @@ jobs: - name: Check AI workflow sync run: node scripts/sync-ai-workflow.mjs --check - - name: Materialize compose env files - env: - COMPOSE_ENV: ${{ secrets.COMPOSE_ENV }} - COMPOSE_ENV_STAGING: ${{ secrets.COMPOSE_ENV_STAGING }} - run: | - set -euo pipefail - if [ -z "${COMPOSE_ENV}" ]; then - echo "Required GitHub secret COMPOSE_ENV is not configured." >&2 - exit 1 - fi - if [ -z "${COMPOSE_ENV_STAGING}" ]; then - echo "Required GitHub secret COMPOSE_ENV_STAGING is not configured." >&2 - exit 1 - fi - printf '%s\n' "$COMPOSE_ENV" > .env - printf '%s\n' "$COMPOSE_ENV_STAGING" > .env.staging - chmod +x scripts/ci/run-php-suite.sh + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + extensions: mysqli, curl, openssl, json, redis, xdebug + coverage: xdebug + ini-values: variables_order=EGPCS,xdebug.mode=coverage - - name: Run unit suite in php1 - run: scripts/ci/run-php-suite.sh unit + - name: Resolve dependencies + working-directory: services/nginx/app + run: composer update --no-interaction --prefer-dist + + - name: Run unit tests + working-directory: services/nginx/app + run: composer test:unit + + - name: Generate coverage report + working-directory: services/nginx/app + run: composer test:coverage - name: Upload coverage artifact if: ${{ github.event_name == 'pull_request' }} @@ -126,57 +125,134 @@ jobs: runs-on: [self-hosted, Linux, X64, default] continue-on-error: true + services: + redis: + image: redis:7 + ports: + - 6379:6379 + mysql: + image: mysql:8 + env: + MYSQL_DATABASE: app_test + MYSQL_ROOT_PASSWORD: root + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping -h 127.0.0.1 -proot" + --health-interval=10s + --health-timeout=5s + --health-retries=10 + steps: - name: Checkout uses: actions/checkout@v4 - - name: Materialize compose env files - env: - COMPOSE_ENV: ${{ secrets.COMPOSE_ENV }} - COMPOSE_ENV_STAGING: ${{ secrets.COMPOSE_ENV_STAGING }} - run: | - set -euo pipefail - if [ -z "${COMPOSE_ENV}" ]; then - echo "Required GitHub secret COMPOSE_ENV is not configured." >&2 - exit 1 - fi - if [ -z "${COMPOSE_ENV_STAGING}" ]; then - echo "Required GitHub secret COMPOSE_ENV_STAGING is not configured." >&2 - exit 1 - fi - printf '%s\n' "$COMPOSE_ENV" > .env - printf '%s\n' "$COMPOSE_ENV_STAGING" > .env.staging - chmod +x scripts/ci/run-php-suite.sh + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + extensions: mysqli, curl, openssl, json, redis + ini-values: variables_order=EGPCS - - name: Run integration suite in php1 - run: scripts/ci/run-php-suite.sh integration + - name: Resolve dependencies + working-directory: services/nginx/app + run: composer update --no-interaction --prefer-dist + + - name: Run integration tests + working-directory: services/nginx/app + env: + RUN_INTEGRATION_TESTS: '1' + USE_ENV: 'true' + DEBUG: '0' + ENCRYPTION_KEY: test-key + CORS: '*' + CONFIG_TIMEZONE: Europe/Copenhagen + ECONOMIC_API_APP_ACCESS_GRANT: test + ECONOMIC_API_APP_ACCESS_GRANT2: test + ECONOMIC_API_APP_SECRET_TOKEN: test + WORDPRESS_STATIC_TOKEN: '' + EMAIL_WASH_CERTIFICATE_TOKEN: '' + WORDPRESS_API_URL: http://localhost + MINIO_ENDPOINT: '' + MINIO_ACCESS_KEY: '' + MINIO_SECRET_KEY: '' + SLACK_DEFAULT_WEBHOOK: '' + REDIS_CONFIG_HOST: 127.0.0.1 + REDIS_CONFIG_DATABASE: '0' + REDIS_CONFIG_PASSWORD: '' + REDIS_CONFIG_PORT: '6379' + CONFIG_DB_HOST: 127.0.0.1 + CONFIG_DB_USER: root + CONFIG_DB_PASSWORD: root + CONFIG_DB_DATABASE: app_test + CONFIG_DB_PORT: '3306' + run: composer test:integration api: name: API (advisory) runs-on: [self-hosted, Linux, X64, default] continue-on-error: true + services: + redis: + image: redis:7 + ports: + - 6379:6379 + mysql: + image: mysql:8 + env: + MYSQL_DATABASE: app_test + MYSQL_ROOT_PASSWORD: root + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping -h 127.0.0.1 -proot" + --health-interval=10s + --health-timeout=5s + --health-retries=10 + steps: - name: Checkout uses: actions/checkout@v4 - - name: Materialize compose env files - env: - COMPOSE_ENV: ${{ secrets.COMPOSE_ENV }} - COMPOSE_ENV_STAGING: ${{ secrets.COMPOSE_ENV_STAGING }} - run: | - set -euo pipefail - if [ -z "${COMPOSE_ENV}" ]; then - echo "Required GitHub secret COMPOSE_ENV is not configured." >&2 - exit 1 - fi - if [ -z "${COMPOSE_ENV_STAGING}" ]; then - echo "Required GitHub secret COMPOSE_ENV_STAGING is not configured." >&2 - exit 1 - fi - printf '%s\n' "$COMPOSE_ENV" > .env - printf '%s\n' "$COMPOSE_ENV_STAGING" > .env.staging - chmod +x scripts/ci/run-php-suite.sh + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + extensions: mysqli, curl, openssl, json, redis + ini-values: variables_order=EGPCS - - name: Run API suite in php1 - run: scripts/ci/run-php-suite.sh api + - name: Resolve dependencies + working-directory: services/nginx/app + run: composer update --no-interaction --prefer-dist + + - name: Run API tests + working-directory: services/nginx/app + env: + RUN_API_TESTS: '1' + API_TEST_BOOTSTRAP_SCHEMA: '1' + USE_ENV: 'true' + DEBUG: '0' + ENCRYPTION_KEY: test-key + CORS: '*' + CONFIG_TIMEZONE: Europe/Copenhagen + ECONOMIC_API_APP_ACCESS_GRANT: test + ECONOMIC_API_APP_ACCESS_GRANT2: test + ECONOMIC_API_APP_SECRET_TOKEN: test + WORDPRESS_STATIC_TOKEN: '' + EMAIL_WASH_CERTIFICATE_TOKEN: '' + WORDPRESS_API_URL: http://localhost + MINIO_ENDPOINT: '' + MINIO_ACCESS_KEY: '' + MINIO_SECRET_KEY: '' + SLACK_DEFAULT_WEBHOOK: '' + REDIS_CONFIG_HOST: 127.0.0.1 + REDIS_CONFIG_DATABASE: '0' + REDIS_CONFIG_PASSWORD: '' + REDIS_CONFIG_PORT: '6379' + CONFIG_DB_HOST: 127.0.0.1 + CONFIG_DB_USER: root + CONFIG_DB_PASSWORD: root + CONFIG_DB_DATABASE: app_test + CONFIG_DB_PORT: '3306' + run: composer test:api diff --git a/README.md b/README.md index 9db033df..db36d3d2 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@ Backend API for Copenhagen Truck Wash services. - PHP 8.2 CLI (optional, for host-side testing) ### Local Development -To bring up the minimal development stack (Traefik, Redis, MySQL debug DB, Edge Broker, Caddy, and one PHP worker): +To bring up the minimal development stack (Traefik, Redis, MySQL debug DB, Caddy, and one PHP worker): ```powershell -docker compose up -d traefik redis mysql-debug edge-broker php1 caddy +docker compose up -d traefik redis mysql-debug php1 caddy ``` The API is accessible at: diff --git a/docker-compose.yml b/docker-compose.yml index 5b220835..52a27f92 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -80,16 +80,6 @@ services: retries: 10 start_period: 20s - edge-broker: - build: - context: . - dockerfile: services/edge-broker/Dockerfile - container_name: edge-broker - environment: - EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} - ports: - - "4300:4300" - caddy: image: caddy:2.7.6-alpine container_name: caddy @@ -206,14 +196,11 @@ services: container_name: php1 depends_on: - redis - - edge-broker command: ["php-fpm"] env_file: - .env environment: - AUTO_COMPOSER_INSTALL: "${AUTO_COMPOSER_INSTALL:-true}" - EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300} - EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} + AUTO_COMPOSER_INSTALL: "true" volumes: - ./services/nginx/app:/var/www/html - ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro @@ -226,14 +213,11 @@ services: container_name: php2 depends_on: - redis - - edge-broker command: ["php-fpm"] env_file: - .env environment: AUTO_COMPOSER_INSTALL: "false" - EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300} - EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} volumes: - ./services/nginx/app:/var/www/html - ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro @@ -246,14 +230,11 @@ services: container_name: php3 depends_on: - redis - - edge-broker command: ["php-fpm"] env_file: - .env environment: AUTO_COMPOSER_INSTALL: "false" - EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300} - EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} volumes: - ./services/nginx/app:/var/www/html - ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro @@ -266,14 +247,11 @@ services: container_name: php4 depends_on: - redis - - edge-broker command: ["php-fpm"] env_file: - .env environment: AUTO_COMPOSER_INSTALL: "false" - EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300} - EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} volumes: - ./services/nginx/app:/var/www/html - ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro @@ -286,14 +264,11 @@ services: container_name: php5 depends_on: - redis - - edge-broker command: ["php-fpm"] env_file: - .env environment: AUTO_COMPOSER_INSTALL: "false" - EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300} - EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} volumes: - ./services/nginx/app:/var/www/html - ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro @@ -306,14 +281,11 @@ services: container_name: php-staging depends_on: - redis-staging - - edge-broker command: ["php-fpm"] env_file: - .env.staging environment: AUTO_COMPOSER_INSTALL: "false" - EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300} - EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} volumes: - ./services/nginx/app:/var/www/html - ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro @@ -326,14 +298,11 @@ services: container_name: php-cron depends_on: - redis - - edge-broker command: ["sh", "-c", "while true; do php index.php run cron; sleep 60; done"] env_file: - .env environment: AUTO_COMPOSER_INSTALL: "false" - EDGE_BROKER_URL: ${EDGE_BROKER_URL:-http://edge-broker:4300} - EDGE_BROKER_SHARED_SECRET: ${EDGE_BROKER_SHARED_SECRET:-truckwash-edge-dev} volumes: - ./services/nginx/app:/var/www/html - ./services/php/php.ini:/usr/local/etc/php/conf.d/zz-custom.ini:ro diff --git a/scripts/ci/run-php-suite.sh b/scripts/ci/run-php-suite.sh deleted file mode 100755 index a0793cb0..00000000 --- a/scripts/ci/run-php-suite.sh +++ /dev/null @@ -1,184 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -suite="${1:-}" - -if [[ -z "$suite" ]]; then - echo "Usage: $0 " >&2 - exit 1 -fi - -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -cd "$repo_root" - -if [[ -f .env ]]; then - set -a - # shellcheck disable=SC1091 - . ./.env - set +a -fi - -export AUTO_COMPOSER_INSTALL=false - -cleanup() { - docker compose down -v --remove-orphans || true -} - -wait_for_container_running() { - local name="$1" - local status="" - - for _ in $(seq 1 90); do - status="$(docker inspect -f '{{.State.Status}}' "$name" 2>/dev/null || true)" - if [[ "$status" == "running" ]]; then - return 0 - fi - sleep 1 - done - - docker logs "$name" || true - echo "Container $name did not reach running state." >&2 - exit 1 -} - -wait_for_container_healthy() { - local name="$1" - local status="" - - for _ in $(seq 1 90); do - status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$name" 2>/dev/null || true)" - if [[ "$status" == "healthy" || "$status" == "running" ]]; then - return 0 - fi - sleep 2 - done - - docker logs "$name" || true - echo "Container $name did not reach a healthy state." >&2 - exit 1 -} - -docker_exec_php1() { - docker exec "$@" php1 sh -lc "cd /var/www/html && ${PHP_COMMAND}" -} - -run_php_command() { - local -a env_args=() - while [[ $# -gt 0 ]]; do - env_args+=(-e "$1") - shift - done - - docker exec "${env_args[@]}" php1 sh -lc "cd /var/www/html && ${PHP_COMMAND}" -} - -sync_php_app_into_container() { - docker exec php1 sh -lc 'mkdir -p /var/www/html && find /var/www/html -mindepth 1 -maxdepth 1 -exec rm -rf {} +' - docker cp "${repo_root}/services/nginx/app/." php1:/var/www/html/ -} - -install_php_dependencies() { - PHP_COMMAND="composer install --no-interaction --prefer-dist" - docker_exec_php1 - docker exec php1 sh -lc ' - module_dir="/var/www/html/modules/washcertificates" - if [ -f "$module_dir/composer.json" ]; then - cd "$module_dir" - composer install --no-interaction --prefer-dist - fi - ' -} - -docker compose down -v --remove-orphans || true -trap cleanup EXIT - -services=(redis edge-broker php1) -if [[ "$suite" != "unit" ]]; then - services+=(mysql-debug) -fi - -docker compose up -d --build "${services[@]}" -wait_for_container_running php1 -wait_for_container_healthy redis -if [[ "$suite" != "unit" ]]; then - wait_for_container_healthy mysql-debug -fi - -sync_php_app_into_container -install_php_dependencies - -case "$suite" in - unit) - PHP_COMMAND="composer test:unit" - docker_exec_php1 - PHP_COMMAND="composer test:coverage" - docker_exec_php1 - mkdir -p "${repo_root}/services/nginx/app/build/logs" - docker cp php1:/var/www/html/build/logs/clover.xml "${repo_root}/services/nginx/app/build/logs/clover.xml" 2>/dev/null || true - ;; - integration) - PHP_COMMAND="composer test:integration" - run_php_command \ - "RUN_INTEGRATION_TESTS=1" \ - "USE_ENV=true" \ - "DEBUG=0" \ - "ENCRYPTION_KEY=test-key" \ - "CORS=*" \ - "CONFIG_TIMEZONE=Europe/Copenhagen" \ - "ECONOMIC_API_APP_ACCESS_GRANT=test" \ - "ECONOMIC_API_APP_ACCESS_GRANT2=test" \ - "ECONOMIC_API_APP_SECRET_TOKEN=test" \ - "WORDPRESS_STATIC_TOKEN=" \ - "EMAIL_WASH_CERTIFICATE_TOKEN=" \ - "WORDPRESS_API_URL=http://localhost" \ - "MINIO_ENDPOINT=" \ - "MINIO_ACCESS_KEY=" \ - "MINIO_SECRET_KEY=" \ - "SLACK_DEFAULT_WEBHOOK=" \ - "REDIS_CONFIG_HOST=redis" \ - "REDIS_CONFIG_DATABASE=0" \ - "REDIS_CONFIG_PASSWORD=" \ - "REDIS_CONFIG_PORT=6379" \ - "CONFIG_DB_TARGET=debug" \ - "CONFIG_DB_DEBUG_HOST=mysql-debug" \ - "CONFIG_DB_DEBUG_USER=root" \ - "CONFIG_DB_DEBUG_PASSWORD=${CONFIG_DB_DEBUG_PASSWORD:-debug_root_password}" \ - "CONFIG_DB_DEBUG_DATABASE=${CONFIG_DB_DEBUG_DATABASE:-nnks_db_debug}" \ - "CONFIG_DB_DEBUG_PORT=3306" - ;; - api) - PHP_COMMAND="composer test:api" - run_php_command \ - "RUN_API_TESTS=1" \ - "API_TEST_BOOTSTRAP_SCHEMA=1" \ - "USE_ENV=true" \ - "DEBUG=0" \ - "ENCRYPTION_KEY=test-key" \ - "CORS=*" \ - "CONFIG_TIMEZONE=Europe/Copenhagen" \ - "ECONOMIC_API_APP_ACCESS_GRANT=test" \ - "ECONOMIC_API_APP_ACCESS_GRANT2=test" \ - "ECONOMIC_API_APP_SECRET_TOKEN=test" \ - "WORDPRESS_STATIC_TOKEN=" \ - "EMAIL_WASH_CERTIFICATE_TOKEN=" \ - "WORDPRESS_API_URL=http://localhost" \ - "MINIO_ENDPOINT=" \ - "MINIO_ACCESS_KEY=" \ - "MINIO_SECRET_KEY=" \ - "SLACK_DEFAULT_WEBHOOK=" \ - "REDIS_CONFIG_HOST=redis" \ - "REDIS_CONFIG_DATABASE=0" \ - "REDIS_CONFIG_PASSWORD=" \ - "REDIS_CONFIG_PORT=6379" \ - "CONFIG_DB_TARGET=debug" \ - "CONFIG_DB_DEBUG_HOST=mysql-debug" \ - "CONFIG_DB_DEBUG_USER=root" \ - "CONFIG_DB_DEBUG_PASSWORD=${CONFIG_DB_DEBUG_PASSWORD:-debug_root_password}" \ - "CONFIG_DB_DEBUG_DATABASE=${CONFIG_DB_DEBUG_DATABASE:-nnks_db_debug}" \ - "CONFIG_DB_DEBUG_PORT=3306" - ;; - *) - echo "Unsupported suite: $suite" >&2 - exit 1 - ;; -esac diff --git a/scripts/test-gateway.mjs b/scripts/test-gateway.mjs index 6a70bf61..5f98dbce 100644 --- a/scripts/test-gateway.mjs +++ b/scripts/test-gateway.mjs @@ -14,6 +14,9 @@ export const DEFAULT_HOST_API_URL = "http://localhost/api"; export const DEFAULT_CONTAINER_API_URL = "http://caddy"; export const DEFAULT_CONTAINER_BROKER_URL = "http://edge-broker:4300"; export const DEFAULT_INSTALL_DIR = "/opt/truckwash-edge-agent"; +export const DEFAULT_RUNTIME_DIR = `${DEFAULT_INSTALL_DIR}/runtime`; +export const DEFAULT_STATE_DATABASE_PATH = `${DEFAULT_RUNTIME_DIR}/gateway-state.sqlite`; +export const DEFAULT_STACK_SERVICE_NAME = "truckwash-edge-gateway-stack.service"; export const DEFAULT_HEARTBEAT_INTERVAL_SECONDS = 15; export const DEFAULT_INSTALLED_VERSION = "php-agent-v1"; const DEFAULT_COMPOSE_SERVICES = ["traefik", "redis", "mysql-debug", "edge-broker", "caddy"]; @@ -91,15 +94,24 @@ export function buildGatewayConfig({ installedVersion, targetVersion, installDir: DEFAULT_INSTALL_DIR, + runtimeDir: DEFAULT_RUNTIME_DIR, + stateDatabasePath: DEFAULT_STATE_DATABASE_PATH, agentPath: `${DEFAULT_INSTALL_DIR}/agent.php`, + lanWorkerPath: `${DEFAULT_INSTALL_DIR}/lan-worker.php`, serviceUnitPath: `${DEFAULT_INSTALL_DIR}/truckwash-edge-agent.service`, + stackServiceUnitPath: `${DEFAULT_INSTALL_DIR}/${DEFAULT_STACK_SERVICE_NAME}`, serviceName: String(existingConfig.serviceName || hostname || DEFAULT_CONTAINER_NAME), + stackServiceName: String(existingConfig.stackServiceName || DEFAULT_STACK_SERVICE_NAME), + composeFileName: String(existingConfig.composeFileName || "docker-compose.gateway.yml"), + composeProjectName: String(existingConfig.composeProjectName || "truckwash-edge-gateway"), + launcherScriptName: String(existingConfig.launcherScriptName || "gateway-launcher.sh"), + workerBaseUrl: String(existingConfig.workerBaseUrl || "http://lan-worker:8090"), + updateWindow: String(existingConfig.updateWindow || "02:00-04:00"), + runtimeMode: String(existingConfig.runtimeMode || "compose"), restartMode: "spawn", heartbeatIntervalSeconds, commandPollTimeoutSeconds: Number(existingConfig.commandPollTimeoutSeconds || 20), - shellActionPollTimeoutSeconds: Number(existingConfig.shellActionPollTimeoutSeconds || 20), commandPollRetryDelayMs: Number(existingConfig.commandPollRetryDelayMs || 1000), - shellActionPollRetryDelayMs: Number(existingConfig.shellActionPollRetryDelayMs || 1000), brokerReconnectDelayMs: Number(existingConfig.brokerReconnectDelayMs || 1500), }; } @@ -387,8 +399,10 @@ async function printStatus({ imageTag, configDir, containerName }) { configFilePath, gatewayId: config.gatewayId ?? null, installDir: config.installDir ?? null, + runtimeDir: config.runtimeDir ?? null, agentPath: config.agentPath ?? null, serviceUnitPath: config.serviceUnitPath ?? null, + stackServiceUnitPath: config.stackServiceUnitPath ?? null, containerStatus: container?.State?.Status ?? "missing", running: Boolean(container?.State?.Running), image: container?.Config?.Image ?? imageTag, diff --git a/services/edge-agent/Dockerfile.test-gateway b/services/edge-agent/Dockerfile.test-gateway index 2f81fc4d..aa86b667 100644 --- a/services/edge-agent/Dockerfile.test-gateway +++ b/services/edge-agent/Dockerfile.test-gateway @@ -6,8 +6,9 @@ RUN set -eux; \ apt-get update; \ apt-get install -y --no-install-recommends \ libcurl4-openssl-dev \ + libsqlite3-dev \ ca-certificates; \ - docker-php-ext-install curl; \ + docker-php-ext-install curl sqlite3; \ rm -rf /var/lib/apt/lists/* COPY services/nginx/app/resources/edge-gateway-agent/ ./ diff --git a/services/edge-agent/dist/agent.mjs b/services/edge-agent/dist/agent.mjs index 317c3059..cb3cd5cf 100644 --- a/services/edge-agent/dist/agent.mjs +++ b/services/edge-agent/dist/agent.mjs @@ -137,7 +137,6 @@ function buildTransportHeartbeatState(brokerState = {}) { status: "ONLINE", metadata: { command_transport: brokerConnected ? "BROKER_FAST_PATH" : "API_POLLING", - shell_transport: brokerConnected ? "BROKER_FAST_PATH" : "API_POLLING", broker_connected: brokerConnected, broker_url: brokerState.url || null, broker_last_error: brokerState.lastError || null, @@ -1762,22 +1761,22 @@ export async function startAgent({ }); config.configPath = configPath; const intervalMs = Number(config.heartbeatIntervalSeconds || 15) * 1000; - const commandPollTimeoutSeconds = Number(config.commandPollTimeoutSeconds ?? 20); - const commandPollRetryDelayMs = Number(config.commandPollRetryDelayMs ?? 1000); - const shellActionPollTimeoutSeconds = Number(config.shellActionPollTimeoutSeconds ?? 20); - const shellActionPollRetryDelayMs = Number(config.shellActionPollRetryDelayMs ?? 1000); + const commandPollTimeoutSeconds = Number(config.commandPollTimeoutSeconds || 20); + const commandPollRetryDelayMs = Number(config.commandPollRetryDelayMs || 1000); const brokerReconnectDelayMs = Number(config.brokerReconnectDelayMs || DEFAULT_BROKER_RECONNECT_DELAY_MS); let stopped = false; let cpuSnapshot = null; let lastHeartbeatLatencyMs = null; + void createShellBridgeImpl; let brokerBridge = null; - const shellEventPublisher = createShellEventPublisher(config, fetchImpl); - const sendShellMessage = (message) => { - shellEventPublisher.publish(message); - brokerBridge?.send(message); + const shell = { + open: async () => {}, + input: () => {}, + resize: () => {}, + close: () => {}, + dispose: () => {}, }; - const shell = createShellBridgeImpl(sendShellMessage); brokerBridge = createBrokerBridge({ config, shell, @@ -1842,37 +1841,8 @@ export async function startAgent({ } }; - const runShellActionPollLoop = async () => { - while (!stopped) { - try { - if (brokerBridge?.state?.connected) { - await new Promise((resolve) => setTimeout(resolve, shellActionPollRetryDelayMs)); - continue; - } - - const action = await pollShellActionJob(config, fetchImpl, shellActionPollTimeoutSeconds); - if (stopped) { - break; - } - - if (!action) { - continue; - } - - await processPolledShellAction(config, action, shell, fetchImpl); - } catch { - if (stopped) { - break; - } - - await new Promise((resolve) => setTimeout(resolve, shellActionPollRetryDelayMs)); - } - } - }; - await sendTransportHeartbeat(); const commandPollPromise = runCommandPollLoop(); - const shellActionPollPromise = runShellActionPollLoop(); const timer = setInterval(() => { sendTransportHeartbeat().catch(() => {}); @@ -1884,8 +1854,7 @@ export async function startAgent({ clearInterval(timer); brokerBridge?.stop(); shell.dispose(); - await Promise.allSettled([commandPollPromise, shellActionPollPromise]); - await shellEventPublisher.drain(); + await Promise.allSettled([commandPollPromise]); }; return { diff --git a/services/nginx/app/.phpunit.cache/test-results b/services/nginx/app/.phpunit.cache/test-results index f00eda96..f9320658 100644 --- a/services/nginx/app/.phpunit.cache/test-results +++ b/services/nginx/app/.phpunit.cache/test-results @@ -1 +1 @@ -{"version":"pest_3.8.6","defects":{"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_self_serve_machine_type__eligibility__summary__and_webhook_endpoints":1,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_defines_reusable_self_serve_wash_and_machine_type_schemas":1,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_redistributes_booked_department_75_net_amounts_using_fixed_pricing_and_wash_subscription_weights":7,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_keeps_classified_booked_department_75_amounts_undistributed_when_no_monthly_basis_exists":8,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_keeps_unclassified_booked_department_75_lines_undistributed_with_warnings":8,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_documents_v2_historical_distribution_and_pricing_history_paths_in_openapi":1,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_defines_new_reusable_v2_schemas_for_normalization_comparison_versioning_and_distribution":1,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_supports_bulk_booked_invoice_line_payloads_with_top_level_department_numbers":7,"P\\Tests\\Unit\\Redis\\RedisAtomicReservationTest::__pest_evaluable_it_claims_a_slot_atomically_with_nx_and_expiration":8,"P\\Tests\\Unit\\Redis\\RedisAtomicReservationTest::__pest_evaluable_it_returns_false_when_the_slot_is_already_claimed_and_clamps_ttl_to_one_second":8,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_documents_economic_v2_invoice_paths_in_openapi":1,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_aligns_legacy_compare_schema_with_runtime_payload_by_removing_stale_required_order__ids":1,"P\\Tests\\Unit\\Search\\SystemSearchEntityTypeCoverageTest::__pest_evaluable_it_documents_every_supported_search_entity_type_in_openapi_enum":1,"P\\Tests\\Unit\\Search\\SystemSearchOpenApiSpecTest::__pest_evaluable_it_documents_system_wide_search_endpoints_in_openapi":1,"P\\Tests\\Unit\\Search\\SystemSearchOpenApiSpecTest::__pest_evaluable_it_documents_debug__intent_and_parser_metadata_schema_in_openapi":1,"P\\Tests\\Unit\\Search\\SystemSearchOpenApiSpecTest::__pest_evaluable_it_documents_e_conomic_indexed_customer_matching_and_synonym_behavior":1,"P\\Tests\\Integration\\Database\\DbConnectionTest::__pest_evaluable_it_can_connect_to_a_configured_MySQL_instance_in_integration_mode":1,"P\\Tests\\Integration\\Invoicing\\EconomicV2BackfillAndDistributionIntegrationTest::__pest_evaluable_it_runs_best_effort_backfill_repeatedly_without_introducing_duplicate_same_start_rows":1,"P\\Tests\\Integration\\Invoicing\\EconomicV2BackfillAndDistributionIntegrationTest::__pest_evaluable_it_resolves_version_aware_distribution_payload_shapes_over_a_real_date_range":8,"P\\Tests\\Integration\\Invoicing\\EconomicV2VersioningServiceIntegrationTest::__pest_evaluable_it_creates_closes_and_rotates_fixed_pricing_versions_without_overlap":8,"P\\Tests\\Integration\\Invoicing\\EconomicV2VersioningServiceIntegrationTest::__pest_evaluable_it_tracks_vehicle_and_discount_version_lifecycles_with_closure_semantics":8,"P\\Tests\\Integration\\Permissions\\LegacyPermissionRedisCacheScriptTest::__pest_evaluable_it_keeps_legacy_route_permission_redis_cache_script_green":7,"P\\Tests\\Unit\\Goals\\GoalsOpenApiSpecTest::__pest_evaluable_it_documents_advanced_goals_target_duration_fields_in_openapi":1,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineSchemaConformanceTest::__pest_evaluable_it_documents_the_date_key_in_the_openapi_department_weather_timeline_schema":1,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_builds_timeline_entries_with_mostly__clear_weather_when_forecast_payload_is_empty":8,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_wires_departments_weather_route_through_the_forecast_fallback_path":7,"P\\Tests\\Unit\\Orders\\AttachmentsListManyTest::__pest_evaluable_it_groups_attachments_by_object_id_and_preserves_empty_object_groups":8,"P\\Tests\\Unit\\Orders\\AttachmentsListManyTest::__pest_evaluable_it_keeps_listMany_payload_parity_with_list_for_one_object_id":8,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTargetsOpenApiSpecTest::__pest_evaluable_it_documents_department_weather_target_endpoints_and_reusable_schemas_in_openapi":1,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTargetsOpenApiSpecTest::__pest_evaluable_it_documents_unknown_weather_status_behavior_when_targets_are_missing":1,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_respects_the_1_request_per_second_Shelly_gate_for_back_to_back_requests":8,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_in_progress_self_serve_wash_start_and_machine_relay_fields":1,"P\\Tests\\Unit\\Selfserve\\SelfserveConfigVersioningTest::__pest_evaluable_it_stores_config_json_with_apostrophes_without_violating_json_constraints":8,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_adds_vehicle_type_product_on_STOP_when_program_selector_is_on__then_turns_off_cleaner_machine_and_selector_relays":7,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_skips_vehicle_type_product_add_when_program_selector_is_off_and_only_disables_configured_relays":7,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_self_serve_machine_wash_included_minutes_in_config_schemas":1,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_fast_when_workspace_id_is_missing_for_gate_calls":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_fast_when_channel_id_is_missing_for_gate_calls":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_with_terminal_status_details_when_call_never_reaches_accepted_state":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_hangs_up_when_call_reaches_accepted_state":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_hangs_up_immediately_when_status_transitions_to_accepted":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_maps_legacy_timeout_option_to_documented_ringTimeout_payload_field":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_clamps_derived_ringTimeout_to_Bird_documented_max_when_gate_timeout_is_high":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_passes_documented_hangup_cause_when_provided":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_drops_unsupported_hangup_cause_values_from_request_payload":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_fast_when_workspace_id_is_missing_for_gate_flash_calls":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_creates_gate_flash_call_with_documented_ringTimeout_payload":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_polls_flash_call_and_succeeds_once_accepted":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_flash_gate_flow_when_terminal_failure_status_is_returned":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_does_not_fallback_to_regular_gate_call_when_flash_succeeds":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_falls_back_to_regular_gate_call_when_flash_fails":8,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_property_gate_lane_commands_and_sanitized_gate_failure_responses":1,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_returns_raw_payload_for_malformed_JSON_responses_and_null_for_empty_responses":8,"P\\Tests\\Unit\\Bird\\BirdRouteWiringTest::__pest_evaluable_it_keeps_Bird_number_and_webhook_routes_intact":7,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_handles_malformed_and_empty_response_bodies_without_throwing_transport_errors":8,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherStatusTargetsTest::__pest_evaluable_it_aggregates_multi_department_slot_statuses_using_worst_severity_and_unknown_fallback_rules":7,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_adds_vehicle_type_product_on_STOP_when_program_selector_relay_is_online__then_turns_off_cleaner_and_machine_relays":7,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_bills_primary_product_when_selector_relay_is_online_even_if_relay_output_is_off":7,"P\\Tests\\Unit\\Bird\\BirdOpenApiSpecTest::__pest_evaluable_it_documents_all_Bird_voice_call_parity_endpoints_including_gather_recordings_insights_and_log":1,"P\\Tests\\Unit\\Bird\\BirdOpenApiSpecTest::__pest_evaluable_it_documents_flash_hangup_endpoint_and_marks_end_alias_as_deprecated":1,"P\\Tests\\Unit\\Bird\\BirdOpenApiSpecTest::__pest_evaluable_it_defines_request_and_response_schemas_for_Bird_call_command_recording_insight_log_and_flash_payloads":1,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueRouteRegistrationTest::__pest_evaluable_it_registers_collected_invoice_queue_endpoints_in_orderInvoicesRoute":7,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_uses_a_consistent_unavailable_service_contract_for_missing_queue_dependencies":7,"P\\Tests\\Unit\\Invoicing\\EconomicTransferOrderItemSkipTest::__pest_evaluable_it_wires_zero_cost_and_zero_quantity_skip_guard_into_transfer_line_builder":7,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_processes_queued_transfer_jobs_to_completion":1,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_deduplicates_active_jobs_per_transfer_target":1,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_rejects_invalid_payloads_without_inserting_queue_rows":1,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_supports_fail_retry_and_reprocess_lifecycle_transitions":1,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_supports_synchronous_fallback_branches_before_queue_enqueue_on_economic_invoice_export_routes":7,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_keeps_queue_status_endpoints_guarded_while_collected_invoice_export_routes_support_synchronous_fallback":7,"P\\Tests\\Unit\\Invoicing\\UserCollectedInvoiceUpdateRouteValidationTest::__pest_evaluable_it_supports_independent_po__number_and_closed__at_updates_for_PUT__collected_invoices_in_user_route":7,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueRouteHardeningTest::__pest_evaluable_it_returns_pagination_metadata_and_strict_status_handling_for_collected_invoice_queue_list":7,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueRouteHardeningTest::__pest_evaluable_it_enforces_retry_constraints_for_collected_invoice_queue_jobs_before_retry_execution":7,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueOpenApiSpecTest::__pest_evaluable_it_documents_collected_queue_list_metadata_and_strict_status_filter_enum_in_openapi":7,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_builds_a_completed_collected_invoice_queue_summary_from_payload_and_result":8,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_prefers_queue_error_messages_for_failed_jobs_and_keeps_null_safe_outcome_fields":8,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_uses_deterministic_status_message_fallback_when_no_explicit_message_exists":8,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_handles_object_payload_result_values_and_malformed_fields_without_throwing":8,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueWorkerTickRouteTest::__pest_evaluable_it_ticks_collected_invoice_transfer_queue_from_queue_list_and_status_routes":7,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueWorkerTickRouteTest::__pest_evaluable_it_ticks_order_transfer_queue_from_draft_and_invoice_status_routes":7,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodQueueOverlayTest::__pest_evaluable_it_marks_queued_transactions_and_clears_requires__action_when_all_actionable_work_is_already_queued":8,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodQueueOverlayTest::__pest_evaluable_it_keeps_requires__action_true_when_a_customer_still_has_unqueued_actionable_transactions":8,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodQueueOverlayTest::__pest_evaluable_it_blocks_fixed_pricing_or_subscription_customers_with_no_transactions_when_a_relevant_queue_job_exists":8,"P\\Tests\\Unit\\DailyReports\\DepartmentDailyReportsOverviewRouteTest::__pest_evaluable_it_builds_the_overview_payload_from_batched_repository_data_with_deterministic_tile_states":7,"P\\Tests\\Unit\\DailyReports\\DepartmentDailyReportsOverviewRouteTest::__pest_evaluable_it_normalizes_department_id_input_from_csv_strings_and_nested_values":7,"P\\Tests\\Unit\\DailyReports\\DepartmentDailyReportsOutsideHoursOpenApiSpecTest::__pest_evaluable_it_documents_outside_hours_summary_and_trend_schemas_in_openapi":7,"P\\Tests\\Integration\\DailyReports\\DepartmentOutsideHoursStatisticsServiceIntegrationTest::__pest_evaluable_it_integrates_orders__xlvask__and_self_serve_into_one_outside_hours_summary_with_missing_hours_diagnostics":8,"P\\Tests\\Integration\\DailyReports\\DepartmentDailyReportComplaintsIntegrationTest::__pest_evaluable_it_counts_complaint_rows_by_department_and_created__at_reporting_range":1,"P\\Tests\\Integration\\DailyReports\\DepartmentDailyReportComplaintsIntegrationTest::__pest_evaluable_it_updates_and_deletes_complaint_rows":1,"P\\Tests\\Integration\\DailyReports\\DepartmentDailyReportComplaintsIntegrationTest::__pest_evaluable_it_parses_created__by__name_from_the_users_table":1,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_reads_dtmf_values_from_alternate_gather_payload_shapes":7,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_treats_a_bare_completed_callback_during_selection_as_caller_finished":7,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_keeps_provider_managed_inbound_calls_passive_until_a_terminal_webhook_arrives":7,"P\\Tests\\Unit\\Invoicing\\EconomicCustomersDiscountFallbackTest::__pest_evaluable_it_falls_back_to_a_later_customer_template_product_when_earlier_probes_fail_on_missing_currency_prices":8,"P\\Tests\\Unit\\Invoicing\\EconomicCustomersDiscountFallbackTest::__pest_evaluable_it_returns_zero_when_every_customer_template_lookup_fails_due_to_missing_currency_prices":8,"P\\Tests\\Unit\\Invoicing\\EconomicCustomersDiscountFallbackTest::__pest_evaluable_it_rethrows_unrelated_discount_lookup_failures":8,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_logs_in_with_valid_customer_credentials":1,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_rejects_invalid_login_payloads_and_credentials":1,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_returns_the_cached_auth_session_payload_for_a_valid_token":1,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_rejects_invalid_auth_session_tokens":1,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_logs_out_and_invalidates_the_token_for_future_session_calls":1,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_rejects_invalid_logout_tokens":1,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_lists_only_visible_departments_and_can_return_a_single_department_with_the_slack_webhook":8,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_department_listing_when_the_permission_is_missing":7,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_creates_departments_through_the_real_endpoint":8,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_invalid_department_create_requests":8,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_updates_departments_through_the_real_endpoint":8,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_invalid_department_update_requests":8,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_lists_department_categories_for_a_department":8,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_invalid_department_category_requests":8,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_lists_orders_for_an_admin_scoped_user_and_limits_the_results_to_the_permitted_departments":1,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_lists_only_the_targeted_customer_orders_for_subuser_sessions":1,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_returns_the_current_auth_and_permission_failures_when_order_listing_is_not_allowed":1,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_creates_orders_through_the_real_endpoint":1,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_order_creation_requests":1,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_updates_orders_through_the_primary_endpoint":1,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_updates_through_the_primary_order_endpoint":1,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_updates_orders_through_the_legacy_alias_endpoint":1,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_updates_through_the_legacy_alias_endpoint":1,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_deletes_orders_through_the_real_endpoint":1,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_order_delete_requests":1,"P\\Tests\\Unit\\Goals\\GoalsLegacyRendererScriptsTest::__pest_evaluable_it_keeps_legacy_department_daily_renderer_script_green":8,"P\\Tests\\Unit\\Orders\\OrdersRegistrationDateRangeQueryTest::__pest_evaluable_it_applies_created__at_bounds_directly_in_SQL_when_filtering_orders_by_registration_number":7,"P\\Tests\\Unit\\Orders\\OrdersRegistrationDateRangeQueryTest::__pest_evaluable_it_rejects_an_inverted_date_range_for_registration_lookups":7,"P\\Tests\\Unit\\Orders\\OrdersRegistrationDateRangeQueryTest::__pest_evaluable_it_returns_early_when_registration_number_is_blank":8,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_processes_only_collected_invoice_jobs_and_respects_the_manual_batch_limit":1,"P\\Tests\\Integration\\SystemStatus\\SuperuserSystemStatusInfrastructureProbeTest::__pest_evaluable_it_can_probe_the_configured_database_in_integration_mode":1,"P\\Tests\\Integration\\SystemStatus\\SuperuserSystemStatusInfrastructureProbeTest::__pest_evaluable_it_can_probe_redis_in_integration_mode_when_configured":1,"P\\Tests\\Integration\\SystemStatus\\SuperuserSystemStatusInfrastructureProbeTest::__pest_evaluable_it_can_probe_minio_in_integration_mode_when_configured":1,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_supports_legacy_field_value_metadata_updates_through_the_primary_endpoint":1,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_supports_legacy_field_value_metadata_updates_through_the_alias_endpoint":1,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_unsupported_legacy_field_value_updates_on_both_update_endpoints":1,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_uses_a_multi_digit_gather_contract_when_10_departments_are_eligible":7,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_returns_a_second_stage_raw_202_gather_response_after_department_selection":7,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_returns_native_flow_gather_data_after_a_top_level_department_selection":7,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_opens_the_gate_immediately_after_a_top_level_department_selection_when_entrance_and_exit_share_the_same_gate":7,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_returns_native_flow_gather_data_after_a_top_level_department_selection_when_distinct_gate_choices_exist":7,"P\\Tests\\Api\\VehiclesApiTest::__pest_evaluable_it_returns_the_newest_vehicle_last__order__id_that_still_has_order_items":7,"P\\Tests\\Api\\VehiclesApiTest::__pest_evaluable_it_returns_a_null_vehicle_last__order__id_when_no_order_with_items_exists":7,"P\\Tests\\Unit\\Subusers\\SubusersRouteManagementContractTest::__pest_evaluable_it_links_grant_disable_operations_to_SUBUSERS__DELETE_for_own_customer_managers":7,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_logs_out_and_invalidates_cached_subuser_sessions":7,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_returns_a_setup_required_error_when_department_terminal_readers_are_requested_without_terminal_setup":1,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_returns_a_setup_required_error_when_creating_a_payment_intent_for_a_department_without_terminal_setup":1},"times":{"P\\Tests\\Unit\\Auth\\CreateTokenTest::__pest_evaluable_it_creates_a_64_char_auth_token_for_an_existing_user":0.278,"P\\Tests\\Unit\\Auth\\CreateTokenTest::__pest_evaluable_it_throws_a_clear_exception_when_customer_user_is_missing":0.039,"P\\Tests\\Unit\\Auth\\LegacyAuthScriptParityTest::__pest_evaluable_it_keeps_passkey_challenge_legacy_script_green_during_migration":0.059,"P\\Tests\\Unit\\Auth\\LegacyAuthScriptParityTest::__pest_evaluable_it_keeps_register_CVR_legacy_script_green_during_migration":0.06,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_builds_deterministic_department_digit_map_and_caps_to_9_options":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_resolves_department_ids_by_selected_digit":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_resolves_gate_type_digit_to_entrance_or_exit":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_stores__loads_and_clears_ivr_state_with_ttl":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_validates_state_caller_fingerprint_matching":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_builds_department_and_gate_prompts":0,"P\\Tests\\Unit\\Permissions\\PermissionNodeDefinitionTest::__pest_evaluable_it_creates_permission_nodes_linked_to_subuser_permission_keys":0,"P\\Tests\\Unit\\Permissions\\PermissionNodeDefinitionTest::__pest_evaluable_it_rejects_empty_permission_definitions":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_normalizes_button_arrays__json_and_csv_inputs":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_rejects_invalid_button_inputs":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_normalizes_vehicle_type_values_and_null_semantics":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_rejects_invalid_vehicle_type_values":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_rejects_non_numeric_vehicle_type_strings":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_keeps_lane_service_enum_contract_for_MACHINE":0,"P\\Tests\\Unit\\Subusers\\SubuserGrantInitializationTest::__pest_evaluable_it_initializes_booking_node_grants_without_DB_dependency":0.001,"P\\Tests\\Unit\\Subusers\\SubuserGrantInitializationTest::__pest_evaluable_it_initializes_selfserve_node_grants_without_DB_dependency":0,"P\\Tests\\Unit\\Subusers\\SubusersRoutePermissionLinkTest::__pest_evaluable_it_keeps_subusers_route_list_permission_linked_to_SUBUSERS__LIST_node":0.008,"P\\Tests\\Unit\\Search\\SystemSearchEntityTypeCoverageTest::__pest_evaluable_it_keeps_route_and_service_entity_type_registries_in_sync_with_expanded_coverage":0,"P\\Tests\\Unit\\Search\\SystemSearchEntityTypeCoverageTest::__pest_evaluable_it_documents_every_supported_search_entity_type_in_openapi_enum":0.027,"P\\Tests\\Unit\\Search\\SystemSearchEconomicCustomerIndexWiringTest::__pest_evaluable_it_wires_local_e_conomic_customer_index_into_customer_related_search_entities":0.006,"P\\Tests\\Unit\\Search\\SystemSearchEconomicCustomerIndexWiringTest::__pest_evaluable_it_registers_cron_tasks_that_keep_the_e_conomic_search_index_refreshed":0.006,"P\\Tests\\Unit\\Search\\SystemSearchInvalidationHooksTest::__pest_evaluable_it_marks_system_search_cache_dirty_from_generic_db_object_mutation_flows":0.005,"P\\Tests\\Unit\\Search\\SystemSearchInvalidationHooksTest::__pest_evaluable_it_marks_system_search_cache_dirty_from_object_property_mutations":0.005,"P\\Tests\\Unit\\Search\\SystemSearchInvalidationHooksTest::__pest_evaluable_it_marks_system_search_cache_dirty_when_module_config_values_change":0.003,"P\\Tests\\Unit\\Search\\SystemSearchInvalidationHooksTest::__pest_evaluable_it_registers_cron_maintenance_task_for_system_search_cache":0.002,"P\\Tests\\Unit\\Search\\SystemSearchOpenAiIntentParserTest::__pest_evaluable_it_redacts_obvious_sensitive_fragments_before_sending_query_to_intent_parser":0.001,"P\\Tests\\Unit\\Search\\SystemSearchOpenAiIntentParserTest::__pest_evaluable_it_builds_payload_with_redacted_query_and_parses_strict_JSON_output":0,"P\\Tests\\Unit\\Search\\SystemSearchOpenAiIntentParserTest::__pest_evaluable_it_falls_back_safely_when_OpenAI_is_disabled":0,"P\\Tests\\Unit\\Search\\SystemSearchOpenAiIntentParserTest::__pest_evaluable_it_handles_malformed_OpenAI_response_payloads_without_throwing":0,"P\\Tests\\Unit\\Search\\SystemSearchOpenAiIntentParserTest::__pest_evaluable_it_uses_parser_cache_for_identical_query_and_allowed_type_combinations":0,"P\\Tests\\Unit\\Search\\SystemSearchOpenAiIntentParserTest::__pest_evaluable_it_caps_alias_and_hint_payloads_from_OpenAI_and_filters_hints_to_allowed_types":0.013,"P\\Tests\\Unit\\Search\\SystemSearchOpenApiSpecTest::__pest_evaluable_it_documents_system_wide_search_endpoints_in_openapi":0.008,"P\\Tests\\Unit\\Search\\SystemSearchOpenApiSpecTest::__pest_evaluable_it_documents_debug__intent_and_parser_metadata_schema_in_openapi":0.007,"P\\Tests\\Unit\\Search\\SystemSearchOpenApiSpecTest::__pest_evaluable_it_documents_e_conomic_indexed_customer_matching_and_synonym_behavior":0.007,"P\\Tests\\Unit\\Search\\SystemSearchRouteBehaviorTest::__pest_evaluable_it_normalizes_type_lists_from_csv_json_and_arrays":0,"P\\Tests\\Unit\\Search\\SystemSearchRouteBehaviorTest::__pest_evaluable_it_parses_booleans_and_clamps_integers_using_route_defaults":0,"P\\Tests\\Unit\\Search\\SystemSearchRouteBehaviorTest::__pest_evaluable_it_exposes_expected_searchable_entity_types":0,"P\\Tests\\Unit\\Search\\SystemSearchRouteWiringTest::__pest_evaluable_it_registers_system_wide_search_GET_and_POST_endpoints_with_intent_debug_support":0.007,"P\\Tests\\Unit\\Search\\SystemSearchRouteWiringTest::__pest_evaluable_it_registers_superuser_cache_clear_and_rebuild_endpoints_for_system_search":0.005,"P\\Tests\\Unit\\Search\\SystemSearchRouteWiringTest::__pest_evaluable_it_passes_permission_and_own_scope_context_into_system_search_service":0.005,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_does_not_invoke_intent_parser_when_lexical_confidence_is_already_high":0.001,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_invokes_parser_on_low_confidence_lexical_results_and_applies_hints_only_boosts":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_never_lets_parser_entity_hints_override_explicit_include_filters":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_returns_fallback_parser_metadata_when_parser_fails_gracefully":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_scopes_query_cache_by_permission_context_to_avoid_cross_user_cache_leakage":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_caps_AI_driven_expanded_terms_to_prevent_query_amplification":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_expands_danish_discount_wording_into_lexical_discount_synonyms":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_invokes_parser_for_intent_driven_natural_language_queries_even_when_lexical_score_is_high":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_uses_association_hints_to_pull_related_customer_records_from_non_customer_matches":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_prefers_newer_records_when_relevance_scores_are_comparable":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_keeps_explicit_identifier_matches_ahead_of_newer_but_weaker_records":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_promotes_invoices_orders_order_bookings_and_customers_in_ranking":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_never_prioritizes_cancelled_bookings_over_active_bookings":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_heavily_demotes_configured_low_priority_entity_types_in_ranking":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_tokenizes_unicode_names_without_stripping_non_ascii_letters":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_does_not_treat_explicit_identifier_queries_as_intent_driven":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_requires_broader_term_coverage_for_multi_word_scoring":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_enriches_object_attachment_results_with_associated_customer_context":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_includes_the_economic_customer_index_in_cache_dependencies_for_customer_scoped_results":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_falls_back_to_invoice_date_ranges_when_invoice_names_are_missing":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_derives_xlvask_customer_numbers_only_from_digits_only_extern_ids":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_replaces_unnamed_user_titles_with_the_customer_context_name":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_uses_the_goal_criteria_label_for_department_goal_titles":0,"P\\Tests\\Integration\\Search\\SystemSearchCacheIntegrationTest::__pest_evaluable_it_reuses_parser_cache_entries_for_repeated_natural_language_intent_requests":0.001,"P\\Tests\\Integration\\Search\\SystemSearchCacheIntegrationTest::__pest_evaluable_it_clears_parser_cache_namespace_via_clearAll_to_force_a_fresh_parse":0,"P\\Tests\\Integration\\Search\\SystemSearchCacheIntegrationTest::__pest_evaluable_it_bumps_per_table_cache_versions_when_a_dirty_table_marker_is_registered":0.034,"P\\Tests\\Unit\\Invoicing\\EconomicV2DistributionServiceFallbackTest::__pest_evaluable_it_falls_back_to_system_orders_for_fixed_pricing_when_regular_orders_yield_no_customers":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2DistributionServiceFallbackTest::__pest_evaluable_it_falls_back_to_system_orders_for_wash_subscriptions_when_regular_orders_yield_no_customers":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2CliBackfillCommandTest::__pest_evaluable_it_registers_economic_v2_backfill_command_in_cli_dispatcher":0.011,"P\\Tests\\Unit\\Invoicing\\EconomicV2CliBackfillCommandTest::__pest_evaluable_it_provides_a_backfill_cron_script_entrypoint":0.012,"P\\Tests\\Unit\\Invoicing\\EconomicV2CompareEngineTest::__pest_evaluable_it_returns_exact__match_when_totals_lines_and_departments_are_identical":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2CompareEngineTest::__pest_evaluable_it_returns_total__mismatch_when_only_totals_differ":0.036,"P\\Tests\\Unit\\Invoicing\\EconomicV2CompareEngineTest::__pest_evaluable_it_detects_line_level_mismatches_for_quantity_and_price":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2CompareEngineTest::__pest_evaluable_it_detects_departmental_distribution_mismatches":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2CompareEngineTest::__pest_evaluable_it_returns_missing__target_when_draft_or_booked_target_is_unavailable":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2LineNormalizerTest::__pest_evaluable_it_normalizes_draft_invoice_lines_including_departmental_distributions":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2LineNormalizerTest::__pest_evaluable_it_marks_text_only_zero_value_lines_as_non_billable_and_keeps_deterministic_key":0.022,"P\\Tests\\Unit\\Invoicing\\EconomicV2LineNormalizerTest::__pest_evaluable_it_normalizes_booked_invoices_and_computes_net_total_delta_from_lines":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2LineNormalizerTest::__pest_evaluable_it_contains_internal_normalization_path_with_departmental_metadata_support":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_documents_economic_v2_invoice_paths_in_openapi":0.019,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_documents_v2_historical_distribution_and_pricing_history_paths_in_openapi":0.012,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_aligns_legacy_compare_schema_with_runtime_payload_by_removing_stale_required_order__ids":0.011,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_defines_new_reusable_v2_schemas_for_normalization_comparison_versioning_and_distribution":0.01,"P\\Tests\\Unit\\Invoicing\\EconomicV2RevenueAndBarredSupportTest::__pest_evaluable_it_supports_barred_filtering_when_listing_e_conomic_customers":0.015,"P\\Tests\\Unit\\Invoicing\\EconomicV2RevenueAndBarredSupportTest::__pest_evaluable_it_implements_a_dedicated_v2_e_conomic_revenue_statistics_service_and_route":0.016,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_registers_all_collected_invoice_economic_v2_routes_with_explicit_permissions":0.006,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_registers_version_aware_distribution_and_pricing_history_v2_routes":0.006,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_caches_v2_distribution_responses_with_a_configurable_redis_ttl":0.007,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_writes_fixed_pricing_versions_from_create_and_delete_flows":0.012,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_writes_vehicle_subscription_versions_for_create_update_delete_flows":0.006,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_writes_discount_override_versions_from_superuser_discounts_route":0.006,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_keeps_legacy_compare_endpoint_path_for_backward_compatibility":0.005,"P\\Tests\\Unit\\Invoicing\\EconomicV2VersioningServiceStructureTest::__pest_evaluable_it_implements_effective_range_lifecycle_methods_for_all_versioned_entities":0.007,"P\\Tests\\Unit\\Invoicing\\EconomicV2VersioningServiceStructureTest::__pest_evaluable_it_closes_previous_active_interval_before_inserting_a_new_version":0.012,"P\\Tests\\Unit\\Invoicing\\EconomicV2VersioningServiceStructureTest::__pest_evaluable_it_includes_best_effort_backfill_with_provenance_and_confidence_metadata":0.005,"P\\Tests\\Unit\\Invoicing\\EconomicV2VersioningServiceStructureTest::__pest_evaluable_it_anchors_historical_resolution_on_order_created__at_timestamps_in_distribution_service":0.006,"P\\Tests\\Unit\\Invoicing\\InvoicingOrdersCalculationsHardeningTest::__pest_evaluable_it_guards_against_empty_order_ids_in_net_amount_calculation":0.014,"P\\Tests\\Unit\\Invoicing\\InvoicingOrdersCalculationsHardeningTest::__pest_evaluable_it_guards_against_empty_customer_arrays_in_date_range_transaction_fetches":0.007,"P\\Tests\\Unit\\Invoicing\\InvoicingOrdersCalculationsHardeningTest::__pest_evaluable_it_uses_centralized_duplicate_filtering_for_possible_duplicate_detection":0.005,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodRouteCacheHelpersTest::__pest_evaluable_it_uses_sane_ttl_defaults_and_clamps_negative_ttl_to_zero":0,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodRouteCacheHelpersTest::__pest_evaluable_it_builds_deterministic_cache_keys_per_scope_and_date_range":0,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodRouteCacheHelpersTest::__pest_evaluable_it_falls_back_to_resolver_directly_when_cache_ttl_is_disabled":0,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodRouteGuardsTest::__pest_evaluable_it_requires_superuser_permission_for_invoicing_period_distribution_all_endpoint":0.01,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodRouteGuardsTest::__pest_evaluable_it_uses_shared_date_range_normalization_across_invoicing_period_endpoints":0.06,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodUtilsTest::__pest_evaluable_it_normalizes_a_valid_invoicing_date_range_to_full_day_timestamps":0,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodUtilsTest::__pest_evaluable_it_rejects_invalid_date_formats":0,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodUtilsTest::__pest_evaluable_it_rejects_descending_date_ranges":0,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodUtilsTest::__pest_evaluable_it_finds_duplicate_orders_regardless_of_original_input_order":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2DistributionServiceFallbackTest::__pest_evaluable_it_runs_best_effort_backfill_when_fixed_pricing_version_history_is_empty":0,"P\\Tests\\Unit\\Selfserve\\SelfserveConditionEvaluatorTest::__pest_evaluable_it_evaluates_self_serve_conditions_with_combined_AND_and_OR_semantics":0,"P\\Tests\\Unit\\Selfserve\\SelfserveConditionEvaluatorTest::__pest_evaluable_it_prefers_condition_results_over_question_answers_when_evaluating_task_gates":0,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_self_serve_machine_type__eligibility__summary__and_webhook_endpoints":0.009,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_defines_reusable_self_serve_wash_and_machine_type_schemas":0.006,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_self_serve_machine_types__eligibility__summaries__and_machine_start_webhook_routes":0.013,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_keeps_machine_type_support_wired_into_lanes__tasks__and_conditions_routes":0.016,"P\\Tests\\Unit\\N8n\\N8nRouteHelpersTest::__pest_evaluable_it_normalizes_webhook_methods_and_falls_back_to_POST_for_unsupported_verbs":0,"P\\Tests\\Unit\\N8n\\N8nRouteHelpersTest::__pest_evaluable_it_filters_request_parameters_down_to_the_allowed_n8n_query_keys":0,"P\\Tests\\Unit\\N8n\\N8nRouteWiringTest::__pest_evaluable_it_registers_workflow_lifecycle_endpoints_for_the_n8n_module":0.007,"P\\Tests\\Unit\\N8n\\N8nRouteWiringTest::__pest_evaluable_it_registers_execution_and_webhook_trigger_endpoints_for_the_n8n_module":0.008,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_redistributes_booked_department_75_net_amounts_using_fixed_pricing_and_wash_subscription_weights":0.005,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_keeps_classified_booked_department_75_amounts_undistributed_when_no_monthly_basis_exists":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_keeps_unclassified_booked_department_75_lines_undistributed_with_warnings":0.001,"P\\Tests\\Unit\\Invoicing\\EconomicEndpointUrlEncodingTest::__pest_evaluable_it_encodes_raw_filter_query_values_that_include_timestamps":0,"P\\Tests\\Unit\\Invoicing\\EconomicEndpointUrlEncodingTest::__pest_evaluable_it_avoids_double_encoding_query_values_that_are_already_escaped":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_supports_bulk_booked_invoice_line_payloads_with_top_level_department_numbers":0,"P\\Tests\\Unit\\Redis\\RedisAtomicReservationTest::__pest_evaluable_it_claims_a_slot_atomically_with_nx_and_expiration":0,"P\\Tests\\Unit\\Redis\\RedisAtomicReservationTest::__pest_evaluable_it_returns_false_when_the_slot_is_already_claimed_and_clamps_ttl_to_one_second":0,"P\\Tests\\Unit\\Bookings\\OrderBookingRouteIdempotencyGuardTest::__pest_evaluable_it_guards_order_booking_creation_with_redis_backed_idempotency":0.007,"P\\Tests\\Unit\\DynamicImages\\DynamicImagePreRenderCronWiringTest::__pest_evaluable_it_registers_dynamic_image_pre_render_cron_task_and_related_helpers":0.007,"P\\Tests\\Integration\\Database\\DbConnectionTest::__pest_evaluable_it_can_connect_to_a_configured_MySQL_instance_in_integration_mode":0.324,"P\\Tests\\Integration\\Invoicing\\EconomicV2BackfillAndDistributionIntegrationTest::__pest_evaluable_it_runs_best_effort_backfill_repeatedly_without_introducing_duplicate_same_start_rows":0.408,"P\\Tests\\Integration\\Invoicing\\EconomicV2BackfillAndDistributionIntegrationTest::__pest_evaluable_it_resolves_version_aware_distribution_payload_shapes_over_a_real_date_range":1381.432,"P\\Tests\\Integration\\Invoicing\\EconomicV2VersioningServiceIntegrationTest::__pest_evaluable_it_creates_closes_and_rotates_fixed_pricing_versions_without_overlap":0.846,"P\\Tests\\Integration\\Invoicing\\EconomicV2VersioningServiceIntegrationTest::__pest_evaluable_it_tracks_vehicle_and_discount_version_lifecycles_with_closure_semantics":1.298,"P\\Tests\\Integration\\Permissions\\LegacyPermissionRedisCacheScriptTest::__pest_evaluable_it_keeps_legacy_route_permission_redis_cache_script_green":1.741,"P\\Tests\\Unit\\Permissions\\AllowOwnOrDepartmentAccessForbiddenTest::__pest_evaluable_it_returns_both_own_and_elevated_permissions_when_both_are_missing":0,"P\\Tests\\Unit\\Permissions\\AllowOwnOrDepartmentAccessForbiddenTest::__pest_evaluable_it_returns_only_elevated_permission_when_own_permission_exists_but_own_context_fails":0,"P\\Tests\\Unit\\Permissions\\AllowOwnOrDepartmentAccessForbiddenTest::__pest_evaluable_it_allows_elevated_permission_path_without_forbidden_and_validates_department_access":0,"P\\Tests\\Unit\\Permissions\\ForbiddenResponseWiringTest::__pest_evaluable_it_routes_and_trait_use_forbidden_responses_for_permission_and_ownership_denials":0.013,"P\\Tests\\Unit\\Permissions\\GroupPermissionSessionInvalidationWiringTest::__pest_evaluable_it_invalidates_related_user_permission_and_auth_session_caches_when_group_permissions_change":0.005,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetDurationTest::__pest_evaluable_it_parses_advanced_target_duration_from_snake__case_fields":0.133,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetDurationTest::__pest_evaluable_it_parses_advanced_target_duration_from_camelCase_aliases":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetDurationTest::__pest_evaluable_it_normalizes_ENTIRE__DURATION_to_ignore_target__duration__every":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetDurationTest::__pest_evaluable_it_defaults_recurring_target__duration__every_to_1_when_omitted":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetDurationTest::__pest_evaluable_it_keeps_strict_legacy_mode_when_target__duration_is_invalid":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetMathTest::__pest_evaluable_it_computes_weekly_target_using_touched_ISO_weeks_for_March_2026":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetMathTest::__pest_evaluable_it_applies_target__duration__every_for_weekly_cadence":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetMathTest::__pest_evaluable_it_prorates_ENTIRE__DURATION_target_by_overlap_days":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetMathTest::__pest_evaluable_it_splits_advanced_target_by_override_weights_per_department":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetMathTest::__pest_evaluable_it_preserves_legacy_target_behavior_when_target__duration_is_missing":0,"P\\Tests\\Unit\\Goals\\GoalsLegacyRendererScriptsTest::__pest_evaluable_it_keeps_legacy_monthly_renderer_script_green":0.215,"P\\Tests\\Unit\\Goals\\GoalsLegacyRendererScriptsTest::__pest_evaluable_it_keeps_legacy_department_daily_renderer_script_green":0.169,"P\\Tests\\Unit\\Goals\\GoalsOpenApiSpecTest::__pest_evaluable_it_documents_advanced_goals_target_duration_fields_in_openapi":0.008,"P\\Tests\\Unit\\Workfeed\\WorkfeedClientConformanceTest::__pest_evaluable_it_uses_company_scoped_workfeed_endpoints_and_raw_Authorization_header":0.006,"P\\Tests\\Unit\\Workfeed\\WorkfeedClientConformanceTest::__pest_evaluable_it_normalizes_documented_shift_query_parameters":0.003,"P\\Tests\\Unit\\Workfeed\\WorkfeedConfigRouteWiringTest::__pest_evaluable_it_registers_module_config_endpoints_for_workfeed":0.004,"P\\Tests\\Unit\\Workfeed\\WorkfeedRouteHelpersTest::__pest_evaluable_it_filters_request_parameters_down_to_the_allowed_workfeed_query_keys":0,"P\\Tests\\Unit\\Workfeed\\WorkfeedRouteWiringTest::__pest_evaluable_it_registers_employee_and_department_endpoints_for_the_workfeed_module":0.003,"P\\Tests\\Unit\\Workfeed\\WorkfeedRouteWiringTest::__pest_evaluable_it_registers_shift_endpoints_for_the_workfeed_module":0.003,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_calculates_workfeed_employee_hours_for_the_hour_slot_based_on_overlap":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_extracts_department_id_from_supported_workfeed_shift_shapes":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_normalizes_wrapped_workfeed_collections_from_common_response_keys":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineSchemaConformanceTest::__pest_evaluable_it_includes_a_date_key_in_department_weather_timeline_entries":0.005,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineSchemaConformanceTest::__pest_evaluable_it_documents_the_date_key_in_the_openapi_department_weather_timeline_schema":0.007,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineRangeTest::__pest_evaluable_it_builds_a_timeline_range_from_start_of_yesterday_to_end_of_today":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineRangeTest::__pest_evaluable_it_builds_a_timeline_range_relative_to_an_explicit_selected_date_override":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_calculates_workfeed_employee_hours_across_multiple_departments_for_one_hour_slot":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_normalizes_department_id_input_from_scalar_csv_and_nested_array_values":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineRangeTest::__pest_evaluable_it_builds_a_timeline_range_relative_to_explicit_date__from_and_date__to_override":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineRangeTest::__pest_evaluable_it_resolves_forecast_day_count_for_a_given_timeline_range_with_sane_limits":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_resolves_weather_coordinates_from_valid_coordinates_only":0.001,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_returns_null_weather_coordinates_when_all_department_locations_are_invalid":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_classifies_weatherapi_no_matching_location_errors_as_location_lookup_failures":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_does_not_classify_non_location_weatherapi_errors_as_location_lookup_failures":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_returns_an_empty_forecast_fallback_when_coordinates_are_unavailable":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_builds_timeline_entries_with_mostly__clear_weather_when_forecast_payload_is_empty":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_wires_departments_weather_route_through_the_forecast_fallback_path":0.006,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_uses_sane_ttl_defaults_and_clamps_negative_ttl_to_zero":0.001,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_builds_deterministic_cache_keys_for_department_sets_and_timeline_ranges":0.001,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_builds_order_insensitive_cache_keys_for_equivalent_department_id_sets":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_falls_back_to_resolver_directly_when_cache_ttl_is_disabled":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineRangeTest::__pest_evaluable_it_marks_non_started_slots_as_unknown_regardless_of_wash_hour_ratio":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherPreloadCronWiringTest::__pest_evaluable_it_registers_and_implements_cron_preloading_for_department_weather_cache":0.012,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_uses_sane_stale_ttl_defaults_and_clamps_stale_ttl_below_fresh_ttl":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_uses_sane_hot_activity_ttl_defaults_and_clamps_to_a_positive_value":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheRuntimeBehaviorTest::__pest_evaluable_it_serves_fresh_cached_payloads_without_invoking_the_resolver":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheRuntimeBehaviorTest::__pest_evaluable_it_serves_stale_cached_payloads_and_enqueues_a_refresh_signal":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheRuntimeBehaviorTest::__pest_evaluable_it_recomputes_and_rewrites_cache_payloads_when_stale_window_is_exceeded":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheRuntimeBehaviorTest::__pest_evaluable_it_records_hot_keys_order_insensitively_and_applies_preload_target_caps":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_normalizes_batched_wash_count_rows_into_hourly_slot_totals":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_wires_department_weather_route_to_use_batched_wash_aggregation":0.005,"P\\Tests\\Unit\\Orders\\AttachmentsListManyTest::__pest_evaluable_it_groups_attachments_by_object_id_and_preserves_empty_object_groups":0,"P\\Tests\\Unit\\Orders\\AttachmentsListManyTest::__pest_evaluable_it_keeps_listMany_payload_parity_with_list_for_one_object_id":0,"P\\Tests\\Unit\\Orders\\EconomicModuleOrdersBatchHelpersTest::__pest_evaluable_it_ensures_economic_module_rows_in_one_sanitized_batch":0,"P\\Tests\\Unit\\Orders\\EconomicModuleOrdersBatchHelpersTest::__pest_evaluable_it_returns_id_keyed_economic_module_payload_with_null_defaults":0,"P\\Tests\\Unit\\Orders\\OrdersRouteListBatchingWiringTest::__pest_evaluable_it_wires_GET__orders_through_batched_enrichment_and_stripe_snapshot_caching":0.009,"P\\Tests\\Unit\\Orders\\UsersCashierNamesBatchTest::__pest_evaluable_it_returns_cashier_names_from_cache_and_fetches_missing_ones_in_batch":0,"P\\Tests\\Unit\\Orders\\UsersCashierNamesBatchTest::__pest_evaluable_it_returns_deterministic_map_for_duplicate_and_unsorted_cashier_ids":0,"P\\Tests\\Unit\\Orders\\UsersCashierNamesBatchWiringTest::__pest_evaluable_it_implements_batched_cashier_name_lookup_with_cache_first_fallback_behavior":0.004,"P\\Tests\\Unit\\Orders\\UsersCashierNamesBatchWiringTest::__pest_evaluable_it_sanitizes_and_deduplicates_cashier_ids_before_batch_lookup":0.004,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_counts_overtime_minutes_when_a_shift_carries_an_extended_approval_end_timestamp":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_counts_unapproved_overtime_from_a_bounded_shift_updateTime_fallback":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_does_not_treat_late_unapproved_administrative_edits_as_overtime":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_changes_weather_timeline_cache_key_when_department_weather_targets_change":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherStatusTargetsTest::__pest_evaluable_it_evaluates_healthy_degraded_and_unhealthy_statuses_from_configured_department_targets":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherStatusTargetsTest::__pest_evaluable_it_returns_unknown_when_configured_targets_are_missing_for_department_status_evaluation":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherStatusTargetsTest::__pest_evaluable_it_aggregates_multi_department_slot_statuses_using_worst_severity_and_unknown_fallback_rules":0.217,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTargetsOpenApiSpecTest::__pest_evaluable_it_documents_department_weather_target_endpoints_and_reusable_schemas_in_openapi":0.015,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTargetsOpenApiSpecTest::__pest_evaluable_it_documents_unknown_weather_status_behavior_when_targets_are_missing":0.008,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTargetsRouteWiringTest::__pest_evaluable_it_registers_department_weather_target_routes_with_explicit_read_and_manage_permissions":0.008,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTargetsRouteWiringTest::__pest_evaluable_it_persists_department_weather_targets_using_canonical_department_variable_keys":0.009,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_machine_relay_status_get_and_set_endpoints":0.004,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_batches_sequential_machine_relay_status_requests_into_a_single_Shelly_get_call":0.001,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_respects_the_1_request_per_second_Shelly_gate_for_back_to_back_requests":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_retries_missing_Shelly_status_payloads_until_relay_status_becomes_ready":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_times_out_with_a_clear_Shelly_readiness_error_when_payload_remains_missing":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_uses_direct_set_switch_and_seeds_cache_so_immediate_status_read_does_not_call_Shelly_get":0,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_self_serve_lane_gate_open_endpoint":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_in_progress_self_serve_wash_details_endpoint":0.004,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_adds_vehicle_type_product_on_STOP_when_program_selector_is_on__then_turns_off_cleaner_machine_and_selector_relays":0.132,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_skips_vehicle_type_product_add_when_program_selector_is_off_and_only_disables_configured_relays":0.001,"P\\Tests\\Unit\\Selfserve\\DepartmentSelfServeEnabledRelaySyncWiringTest::__pest_evaluable_it_syncs_lane_relay_states_when_department_self_serve_enabled_flag_changes":0.007,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_supports_hard_relay_set_even_when_lane_status_is_CLOSED":0,"P\\Tests\\Unit\\Selfserve\\SelfserveStartCleanerRelayWiringTest::__pest_evaluable_it_enables_cleaner_relay_on_wash_start_command_and_webhook_flow":0.01,"P\\Tests\\Unit\\Selfserve\\SelfserveWashCompletionRelayWiringTest::__pest_evaluable_it_forces_machine_relay_off_when_a_self_serve_wash_session_is_completed":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePortControllerTest::__pest_evaluable_it_opens_exit_port_by_switching_relay__in__id_on":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePortControllerTest::__pest_evaluable_it_opens_entrance_port_by_switching_relay__out__id_on":0,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_in_progress_self_serve_wash_start_and_machine_relay_fields":0.006,"P\\Tests\\Unit\\Selfserve\\SelfserveConditionEvaluatorTest::__pest_evaluable_it_evaluates_typed_task_gates_with_strict_semantics":0,"P\\Tests\\Unit\\Selfserve\\SelfserveConfigVersioningTest::__pest_evaluable_it_validates_typed_task_gates_for_known_references":0,"P\\Tests\\Unit\\Selfserve\\SelfserveConfigVersioningTest::__pest_evaluable_it_fails_validation_when_typed_task_gates_reference_unknown_entities":0,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_self_serve_config_draft_publish_rollback_lifecycle_endpoints":0.004,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_keeps_legacy_self_serve_CRUD_routes_syncing_canonical_drafts":0.011,"P\\Tests\\Unit\\Selfserve\\SelfserveStartCleanerRelayWiringTest::__pest_evaluable_it_keeps_cleaner_relay_enable_wired_into_machine_relay_start_paths":0.008,"P\\Tests\\Unit\\Selfserve\\SelfserveConfigVersioningTest::__pest_evaluable_it_stores_config_json_with_apostrophes_without_violating_json_constraints":0.277,"P\\Tests\\Unit\\Selfserve\\SelfserveConfigVersioningTest::__pest_evaluable_it_encodes_config_json_payloads_with_apostrophes_before_persistence":0,"P\\Tests\\Unit\\Selfserve\\SelfserveWashCompletionRelayWiringTest::__pest_evaluable_it_forces_machine_and_cleaner_relays_off_when_a_self_serve_wash_session_is_completed":0.016,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_vehicle_type_override_into_self_serve_preview_and_synchronization_routes":0.01,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_uses_local_demo_responses_and_skips_Shelly_cloud_calls_for_demo_relay_ids":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_returns_default_OFF_status_for_demo_relay_ids_without_Shelly_lookups":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_excludes_demo_relay_ids_from_Shelly_status_batch_payloads":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePortControllerTest::__pest_evaluable_it_keeps_demo_relay_gate_open_queued_but_skips_Shelly_switch_calls":0,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_allowed_services_route_through_machine_relay_visibility_sync":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_enables_MACHINE_relay_when_MACHINE_is_visible_in_allowed_services":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_turns_MACHINE_relay_off_immediately_when_MACHINE_is_not_visible":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_does_not_enable_MACHINE_relay_when_allowEnable_is_false_even_if_MACHINE_is_visible":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_is_a_safe_no_op_when_MACHINE_relay_is_not_configured":0,"P\\Tests\\Unit\\Selfserve\\SelfserveRelayVisibilitySyncWiringTest::__pest_evaluable_it_synchronizes_machine_relay_from_visible_services_during_session_synchronization":0.005,"P\\Tests\\Unit\\Selfserve\\SelfserveRelayVisibilitySyncWiringTest::__pest_evaluable_it_adds_explicit_relay_disable_session_helper_for_visibility_driven_OFF_transitions":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_self_serve_property_gate_command_permissions":0.007,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneCommandEnumTest::__pest_evaluable_it_parses_property_gate_lane_commands":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePropertyGateCommandTest::__pest_evaluable_it_opens_entrance_gate_for_OPEN__PROPERTY__ACCESS__GATE_command":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePropertyGateCommandTest::__pest_evaluable_it_opens_exit_gate_for_OPEN__PROPERTY__EXIT__GATE_command":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePropertyGateCommandTest::__pest_evaluable_it_blocks_property_gate_commands_when_department_self_serve_is_disabled":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePropertyGateCommandTest::__pest_evaluable_it_throws_a_clear_error_when_entrance_gate_is_missing_for_property_access_command":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePropertyGateCommandTest::__pest_evaluable_it_wraps_low_level_gate_errors_for_property_access_command_failures":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveWashFlowMachineAllowedWiringTest::__pest_evaluable_it_reads_machine_allowed_state_from_session_summary_payload_safely":0.009,"P\\Tests\\Unit\\Selfserve\\DbObjectPaginationLegacyColumnCompatibilityTest::__pest_evaluable_it_guards_pagination_against_searchable_fields_missing_from_legacy_schemas":0.004,"P\\Tests\\Unit\\Selfserve\\SelfserveSchemaBootstrapCompatibilityTest::__pest_evaluable_it_adds_dynamic__images__vehicle__type_column_for_legacy_selfserve_wash_session_task_schemas":0.005,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneInvoiceIncludedMinutesTest::__pest_evaluable_it_computes_billable_minutes_when_elapsed_minutes_exceed_included_minutes":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneInvoiceIncludedMinutesTest::__pest_evaluable_it_computes_zero_billable_minutes_when_elapsed_minutes_equal_included_minutes":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneInvoiceIncludedMinutesTest::__pest_evaluable_it_computes_zero_billable_minutes_when_included_minutes_exceed_elapsed_minutes":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneInvoiceIncludedMinutesTest::__pest_evaluable_it_handles_zero_and_low_elapsed_wash_time_edge_cases":0,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_self_serve_machine_wash_included_minutes_in_config_schemas":0.006,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_machine_wash_included_minutes_into_self_serve_module_config":0.004,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_blocks_explicit_relay_writes_when_department_self_serve_is_disabled":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_returns_disabled_no_op_from_machine_relay_visibility_sync_when_department_self_serve_is_disabled":0,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_property_gate_lane_commands_and_sanitized_gate_failure_responses":0.006,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_fast_when_workspace_id_is_missing_for_gate_calls":0.03,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_fast_when_channel_id_is_missing_for_gate_calls":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_with_terminal_status_details_when_call_never_reaches_accepted_state":0.078,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_hangs_up_when_call_reaches_accepted_state":0.02,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_hangs_up_immediately_when_status_transitions_to_accepted":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_maps_legacy_timeout_option_to_documented_ringTimeout_payload_field":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_clamps_derived_ringTimeout_to_Bird_documented_max_when_gate_timeout_is_high":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_passes_documented_hangup_cause_when_provided":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_drops_unsupported_hangup_cause_values_from_request_payload":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_fast_when_workspace_id_is_missing_for_gate_flash_calls":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_creates_gate_flash_call_with_documented_ringTimeout_payload":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_polls_flash_call_and_succeeds_once_accepted":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_flash_gate_flow_when_terminal_failure_status_is_returned":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_does_not_fallback_to_regular_gate_call_when_flash_succeeds":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_falls_back_to_regular_gate_call_when_flash_fails":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePropertyGateCommandTest::__pest_evaluable_it_wraps_low_level_gate_errors_for_property_exit_command_failures":0.002,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_falls_back_to_regular_gate_call_when_flash_caller_id_is_not_confirmed":0,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_maps_voice_call_facade_methods_to_documented_endpoints_and_methods":0.049,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_maps_recordings_insights_log_and_flash_methods_to_documented_resources":0,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_encodes_path_segments_before_building_outbound_endpoints":0,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_serializes_query_parameters_for_GET_transport_requests":0,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_maps_4xx_and_5xx_transport_failures_into_informative_exceptions":0,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_returns_raw_payload_for_malformed_JSON_responses_and_null_for_empty_responses":0.007,"P\\Tests\\Unit\\Bird\\BirdOpenApiSpecTest::__pest_evaluable_it_documents_all_Bird_voice_call_parity_endpoints_including_gather_recordings_insights_and_log":0.007,"P\\Tests\\Unit\\Bird\\BirdOpenApiSpecTest::__pest_evaluable_it_documents_flash_hangup_endpoint_and_marks_end_alias_as_deprecated":0.007,"P\\Tests\\Unit\\Bird\\BirdOpenApiSpecTest::__pest_evaluable_it_defines_request_and_response_schemas_for_Bird_call_command_recording_insight_log_and_flash_payloads":0.009,"P\\Tests\\Unit\\Bird\\BirdRequestValidationTest::__pest_evaluable_it_accepts_valid_payloads_for_create_and_nested_call_flow_commands":0,"P\\Tests\\Unit\\Bird\\BirdRequestValidationTest::__pest_evaluable_it_rejects_unknown_fields_and_invalid_enum_values_in_strict_schemas":0.008,"P\\Tests\\Unit\\Bird\\BirdRequestValidationTest::__pest_evaluable_it_supports_CSV_normalization_in_schema_validation_for_log_filters":0,"P\\Tests\\Unit\\Bird\\BirdRequestValidationTest::__pest_evaluable_it_validates_flash_hangup_payloads_for_both_supported_request_shapes":0,"P\\Tests\\Unit\\Bird\\BirdRequestValidationTest::__pest_evaluable_it_fails_before_forward_step_when_strict_validation_fails_and_forwards_when_valid":0,"P\\Tests\\Unit\\Bird\\BirdRouteWiringTest::__pest_evaluable_it_registers_full_Bird_voice_call_route_surface_with_strict_validation_and_permissions":0.01,"P\\Tests\\Unit\\Bird\\BirdRouteWiringTest::__pest_evaluable_it_registers_flash_hangup_endpoint_and_keeps_end_alias_wired_as_compatibility_path":0.007,"P\\Tests\\Unit\\Bird\\BirdRouteWiringTest::__pest_evaluable_it_keeps_Bird_number_and_webhook_routes_intact":0.016,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_handles_malformed_and_empty_response_bodies_without_throwing_transport_errors":0.005,"P\\Tests\\Unit\\Selfserve\\DepartmentGatesRelaysRouteWiringTest::__pest_evaluable_it_validates_department_gate_config_on_both_create_and_update_routes":0.007,"P\\Tests\\Unit\\Selfserve\\SelfserveSchemaBootstrapCompatibilityTest::__pest_evaluable_it_adds_wash__started__at_column_for_legacy_selfserve_wash_session_schemas":0.01,"P\\Tests\\Unit\\Selfserve\\SelfserveWashStartedAtWiringTest::__pest_evaluable_it_stores_wash__started__at_in_self_serve_wash_sessions_when_machine_start_is_triggered":0.005,"P\\Tests\\Unit\\Selfserve\\SelfserveWashStartedAtWiringTest::__pest_evaluable_it_passes_lane_wash_start_time_into_the_session_machine_start_marker":0.009,"P\\Tests\\Unit\\Selfserve\\SelfserveWashStartedAtWiringTest::__pest_evaluable_it_resolves_in_progress_wash__started__at_from_session_with_compatibility_fallbacks":0.004,"P\\Tests\\Unit\\Bird\\BirdPayloadClassesTest::__pest_evaluable_it_normalizes_say_payload_and_applies_hangup_default":0.026,"P\\Tests\\Unit\\Bird\\BirdPayloadClassesTest::__pest_evaluable_it_normalizes_log_query_csv_and_integer_fields":0,"P\\Tests\\Unit\\Bird\\BirdPayloadClassesTest::__pest_evaluable_it_normalizes_nested_create_call_payload_sections_and_drops_unknown_keys":0,"P\\Tests\\Unit\\Bird\\BirdPayloadClassesTest::__pest_evaluable_it_keeps_gather_nested_say_payload_unchanged_when_hangup_is_omitted":0,"P\\Tests\\Unit\\Bird\\BirdPayloadClassesTest::__pest_evaluable_it_supports_no_body_and_flash_hangup_payload_normalization":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_respects_the_2_second_Shelly_gate_for_back_to_back_requests":0.023,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_enforces_a_2_second_gap_between_sequential_switch_requests_used_by_wash_start_and_stop_flows":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_keeps_back_to_back_get_switch_requests_ordered_through_the_relay_controller":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_executes_sequential_switch_requests_used_by_wash_start_and_stop_flows":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePortControllerTest::__pest_evaluable_it_does_not_print_Shelly_switch_responses_to_output":0,"P\\Tests\\Unit\\Selfserve\\ShellyGlobalRateLimitWiringTest::__pest_evaluable_it_enforces_a_global_2_second_Shelly_gate_in_sendPostRequest":0.004,"P\\Tests\\Unit\\Selfserve\\ShellyGlobalRateLimitWiringTest::__pest_evaluable_it_uses_Redis_NX_PX_semantics_for_cross_request_Shelly_rate_limiting":0.003,"P\\Tests\\Unit\\Selfserve\\ShellyGlobalRateLimitBehaviorTest::__pest_evaluable_it_enforces_the_2_second_Shelly_gate_across_separate_request_contexts":0,"P\\Tests\\Unit\\Selfserve\\ShellyGlobalRateLimitBehaviorTest::__pest_evaluable_it_does_not_delay_when_the_Shelly_gate_is_already_expired":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_adds_vehicle_type_product_on_STOP_when_program_selector_relay_is_online__then_turns_off_cleaner_and_machine_relays":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_skips_vehicle_type_product_add_when_program_selector_relay_is_offline_and_only_disables_configured_relays":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_bills_primary_product_when_selector_relay_is_online_even_if_relay_output_is_off":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneInvoiceModeBillingTest::__pest_evaluable_it_bills_manual_self_serve_stop_using_full_elapsed_minutes_without_included_minute_reduction":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneInvoiceModeBillingTest::__pest_evaluable_it_keeps_included_minute_reduction_for_automatic_mode":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_sends_Slack_on_first_dtmf_input_captured_within_the_300_second_window":0.005,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_does_not_send_Slack_when_dtmf_input_repeats_without_change":0.008,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_sends_Slack_again_when_dtmf_input_changes_within_the_window":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_keeps_gathering_and_does_not_hang_up_before_300_seconds_have_elapsed":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_announces_timeout_waits_10_seconds_and_sends_hangup_once_at_or_after_300_seconds":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_keeps_polling_until_terminal_status_and_only_then_finalizes":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_finalizes_immediately_when_webhook_payload_is_already_terminal_before_timeout_logic":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_does_not_duplicate_timeout_or_hangup_actions_across_retries_and_lock_contention":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_keeps_state_when_polling_fails_so_completion_is_not_falsely_reported":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_sends_Slack_when_dtmf_input_repeats_without_change":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_answers_immediately_once_and_does_not_re_answer_on_subsequent_webhook_retries":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_configures_gather_with_retry_loop_semantics_until_input_is_entered":0.004,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_sends_Slack_when_gather_result_provides_keys_field_instead_of_dtmf":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_sends_Slack_when_gather_conditions_variable_keys_carries_the_entered_value":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_configures_gather_to_check_every_2_seconds_with_retry_loop_semantics_until_input_is_entered":0,"P\\Tests\\Unit\\Users\\UsersAutomaticGetTargetUserFromRequestTest::__pest_evaluable_it_ignores_non_numeric_customer__number_values_from_request":0.042,"P\\Tests\\Unit\\Users\\UsersAutomaticGetTargetUserFromRequestTest::__pest_evaluable_it_casts_numeric_customer__number_strings_to_int_before_lookup":0,"P\\Tests\\Unit\\Users\\UsersAutomaticGetTargetUserFromRequestTest::__pest_evaluable_it_prefers_user__id_when_both_user__id_and_customer__number_are_valid":0,"P\\Tests\\Unit\\Users\\UsersAutomaticGetTargetUserFromRequestTest::__pest_evaluable_it_falls_back_to_customer__number_when_user__id_is_invalid":0,"P\\Tests\\Unit\\Users\\UsersAutomaticGetTargetUserFromRequestTest::__pest_evaluable_it_normalizes_request_identifiers_before_user_lookups":0.006,"P\\Tests\\Unit\\Users\\UsersAutomaticGetTargetUserFromRequestTest::__pest_evaluable_it_rejects_non_positive_and_non_digit_request_values":0.008,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_uses_pagination_results_when_present":0.006,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_falls_back_to_collection_count_when_pagination_metadata_is_missing":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_returns_zero_when_neither_pagination_nor_collection_is_available":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_counts_object_based_collections_when_pagination_metadata_is_missing":0,"P\\Tests\\Unit\\Invoicing\\EconomicPaymentTermsRouteCollectionExtractionTest::__pest_evaluable_it_uses_collection_when_present":0,"P\\Tests\\Unit\\Invoicing\\EconomicPaymentTermsRouteCollectionExtractionTest::__pest_evaluable_it_falls_back_to_paymentTerms_when_collection_is_missing":0,"P\\Tests\\Unit\\Invoicing\\EconomicPaymentTermsRouteCollectionExtractionTest::__pest_evaluable_it_supports_top_level_array_payloads":0,"P\\Tests\\Unit\\Invoicing\\EconomicPaymentTermsRouteCollectionExtractionTest::__pest_evaluable_it_returns_an_empty_array_when_no_known_collection_shape_exists":0,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_uses_grant__2_in_economic__endpoint__t_when_explicitly_requested_and_configured":0,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_falls_back_to_grant__1_in_economic__endpoint__t_when_grant__2_is_missing":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_throws_for_missing_required_primary_grant_in_economic__endpoint__t":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_throws_for_missing_app_secret_in_economic__endpoint__t":0,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_uses_grant__2_in_legacy_economic__m_when_available":0,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_falls_back_to_grant__1_in_legacy_economic__m_when_grant__2_is_missing":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_throws_for_missing_required_primary_grant_in_legacy_economic__m":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_throws_for_missing_app_secret_in_legacy_economic__m":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_uses_unfiltered_total_when_no_customer_filters_are_active":0.004,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_uses_filtered_total_when_search_is_active":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_uses_filtered_total_when_barred_filter_is_active":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_treats__null__search_and_barred_values_as_no_filter":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_falls_back_to_filtered_total_when_unfiltered_total_is_missing":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRouteEconomicFailureHandlingTest::__pest_evaluable_it_maps_e_conomic_integration_failures_in_customer_listing_to_explicit_502_responses":0.006,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRouteEconomicFailureHandlingTest::__pest_evaluable_it_logs_LIST__CUSTOMERS_success_only_after_pagination_and_customer_mapping_are_completed":0.004,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRouteEconomicFailureHandlingTest::__pest_evaluable_it_guards_against_malformed_customer_list_payloads_before_calling_paginate":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_throws_deterministic_upstream_exceptions_for_non_2xx_endpoint_trait_responses":0,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_throws_deterministic_upstream_exceptions_for_non_2xx_legacy_economic__m_responses":0,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_returns_raw_payload_unchanged_on_successful_HTTP_statuses":0,"P\\Tests\\Unit\\Invoicing\\EconomicCustomersListResponseValidationTest::__pest_evaluable_it_accepts_valid_list_payloads_that_include_collection_and_pagination_results":0,"P\\Tests\\Unit\\Invoicing\\EconomicCustomersListResponseValidationTest::__pest_evaluable_it_throws_when_pagination_is_missing_from_the_response_payload":0,"P\\Tests\\Unit\\Invoicing\\EconomicCustomersListResponseValidationTest::__pest_evaluable_it_throws_when_upstream_responds_with_an_error_shaped_payload":0,"P\\Tests\\Unit\\Router\\RouterThrowableHandlingTest::__pest_evaluable_it_catches_throwables_during_auto_route_loading_and_maps_them_to_internal_server_errors":0.005,"P\\Tests\\Unit\\Users\\EconomicCustomerZeroHandlingTest::__pest_evaluable_it_does_not_request_e_conomic_customer_data_when_customer_number_is_zero":0.005,"P\\Tests\\Unit\\Users\\EconomicCustomerZeroHandlingTest::__pest_evaluable_it_short_circuits_e_conomic_customer_lookup_for_non_positive_customer_numbers":0.006,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueRouteRegistrationTest::__pest_evaluable_it_registers_queued_economic_invoice_endpoints_in_economicInvoiceRoute":0.008,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueRouteRegistrationTest::__pest_evaluable_it_registers_collected_invoice_queue_endpoints_in_orderInvoicesRoute":0.006,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueSchemaBootstrapTest::__pest_evaluable_it_defines_economic_transfer_queue_jobs_schema_bootstrap_table_and_tracking_columns":0.01,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueSchemaBootstrapTest::__pest_evaluable_it_provides_queue_processor_class_constants_and_processing_entrypoint":0.015,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueCronIntegrationTest::__pest_evaluable_it_registers_economic_transfer_queue_cron_task_and_handler":0.007,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueOpenApiSpecTest::__pest_evaluable_it_documents_economic_transfer_queue_paths_in_openapi":0.015,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueOpenApiSpecTest::__pest_evaluable_it_documents_economic_transfer_queue_schemas_in_openapi":0.012,"P\\Tests\\Unit\\Invoicing\\EconomicTransferOrderItemSkipTest::__pest_evaluable_it_skips_order_items_with_zero_quantity_for_e_conomic_export":0,"P\\Tests\\Unit\\Invoicing\\EconomicTransferOrderItemSkipTest::__pest_evaluable_it_skips_order_items_with_zero_price_for_e_conomic_export":0,"P\\Tests\\Unit\\Invoicing\\EconomicTransferOrderItemSkipTest::__pest_evaluable_it_keeps_positive_quantity_and_price_items_billable_for_e_conomic_export":0,"P\\Tests\\Unit\\Invoicing\\EconomicTransferOrderItemSkipTest::__pest_evaluable_it_wires_zero_cost_and_zero_quantity_skip_guard_into_transfer_line_builder":0.005,"P\\Tests\\Unit\\Invoicing\\EconomicInvoiceDraftZeroItemSkipWiringTest::__pest_evaluable_it_skips_zero_cost_and_zero_quantity_items_in_legacy_economic_draft_helper":0.007,"P\\Tests\\Unit\\Invoicing\\EconomicTransferOrderItemSkipTest::__pest_evaluable_it_skips_malformed_order_item_payloads_for_e_conomic_export_safety":0,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueHardeningTest::__pest_evaluable_it_hardens_transfer_queue_with_type_validation_retry_caps_and_stale_lock_recovery":0.005,"P\\Tests\\Unit\\Router\\AutoloadRedisCacheValidationTest::__pest_evaluable_it_validates_cached_autoload_paths_before_returning_and_clears_stale_cache_entries":0.003,"P\\Tests\\Unit\\Orders\\OrdersRegistrationDateRangeQueryTest::__pest_evaluable_it_applies_created__at_bounds_directly_in_SQL_when_filtering_orders_by_registration_number":0,"P\\Tests\\Unit\\Orders\\OrdersRegistrationDateRangeQueryTest::__pest_evaluable_it_rejects_an_inverted_date_range_for_registration_lookups":0,"P\\Tests\\Unit\\Orders\\OrdersRegistrationDateRangeQueryTest::__pest_evaluable_it_returns_early_when_registration_number_is_blank":0,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueCronIntegrationTest::__pest_evaluable_it_wires_queue_worker_class_loading_for_CLI_queue_action":0.005,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_guards_all_economic_invoice_queue_endpoints_before_constructing_queue_service":0.012,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_guards_all_collected_invoice_queue_endpoints_before_constructing_queue_service":0.011,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_uses_a_consistent_unavailable_service_contract_for_missing_queue_dependencies":0.009,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_gracefully_handles_unavailable_queue_dependencies_on_collected_invoice_queue_endpoints":0.005,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceEconomicQueueResponseContractTest::__pest_evaluable_it_returns_the_queue_job_id_in_POST__collected_invoices_economic_enqueue_response":0.204,"P\\Tests\\Unit\\Invoicing\\UserCollectedInvoiceUpdateRouteValidationTest::__pest_evaluable_it_requires_id_and_at_least_one_mutable_field_for_PUT__collected_invoices_in_user_route":0.009,"P\\Tests\\Unit\\Invoicing\\UserCollectedInvoiceUpdateRouteValidationTest::__pest_evaluable_it_supports_independent_po__number_and_closed__at_updates_for_PUT__collected_invoices_in_user_route":0.021,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceEconomicQueueResponseContractTest::__pest_evaluable_it_returns_queued_contract_for_POST__collected_invoices_stripe_book":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicDraftQueueOnlyRouteBehaviorTest::__pest_evaluable_it_keeps_order_draft_export_route_queue_only":0.006,"P\\Tests\\Unit\\Invoicing\\EconomicDraftQueueOnlyRouteBehaviorTest::__pest_evaluable_it_keeps_collected_invoice_draft_producing_routes_queue_only_in_orderInvoicesRoute":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_guards_collected_invoice_and_stripe_draft_producing_endpoints_before_constructing_queue_service":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueOpenApiSpecTest::__pest_evaluable_it_documents_unavailable_queue_dependency_responses_for_async_economic_transfer_endpoints":0.007,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueuePayloadValidationTest::__pest_evaluable_it_normalizes_order_payloads_to_strict_positive_integer_ids":0,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueuePayloadValidationTest::__pest_evaluable_it_normalizes_collected_invoice_payload_booleans_to_strict_bool_values":0,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueuePayloadValidationTest::__pest_evaluable_it_rejects_invalid_transfer_payloads_before_enqueue_write_attempts":0,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_processes_queued_transfer_jobs_to_completion":1.581,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_deduplicates_active_jobs_per_transfer_target":0.807,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_rejects_invalid_payloads_without_inserting_queue_rows":0.372,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_supports_fail_retry_and_reprocess_lifecycle_transitions":2.115,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_keeps_queue_guards_on_economic_invoice_queue_status_and_retry_routes":0.006,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_supports_synchronous_fallback_branches_before_queue_enqueue_on_economic_invoice_export_routes":0.005,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_keeps_queue_status_endpoints_guarded_while_collected_invoice_export_routes_support_synchronous_fallback":0.006,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_uses_a_consistent_unavailable_service_contract_for_queue_status_lifecycle_endpoints":0.009,"P\\Tests\\Unit\\Invoicing\\EconomicDraftQueueOnlyRouteBehaviorTest::__pest_evaluable_it_supports_synchronous_fallback_and_queued_processing_for_order_draft_export_route":0.005,"P\\Tests\\Unit\\Invoicing\\EconomicDraftQueueOnlyRouteBehaviorTest::__pest_evaluable_it_supports_synchronous_fallback_and_queued_processing_for_collected_invoice_draft_producing_routes":0.006,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceEconomicQueueResponseContractTest::__pest_evaluable_it_documents_both_synchronous_fallback_and_queued_response_contracts_for_POST__collected_invoices_economic":0.014,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceEconomicQueueResponseContractTest::__pest_evaluable_it_documents_both_synchronous_fallback_and_queued_response_contracts_for_POST__collected_invoices_stripe_book":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueOpenApiSpecTest::__pest_evaluable_it_documents_200_fallback_plus_202_queue_contracts_for_export_endpoints_and_keeps_503_on_queue_lifecycle_endpoints":0.01,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueRouteHardeningTest::__pest_evaluable_it_returns_pagination_metadata_and_strict_status_handling_for_collected_invoice_queue_list":0.004,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueRouteHardeningTest::__pest_evaluable_it_enforces_retry_constraints_for_collected_invoice_queue_jobs_before_retry_execution":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueOpenApiSpecTest::__pest_evaluable_it_documents_collected_queue_list_metadata_and_strict_status_filter_enum_in_openapi":0,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_builds_a_completed_collected_invoice_queue_summary_from_payload_and_result":0.005,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_prefers_queue_error_messages_for_failed_jobs_and_keeps_null_safe_outcome_fields":0,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_uses_deterministic_status_message_fallback_when_no_explicit_message_exists":0,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_handles_object_payload_result_values_and_malformed_fields_without_throwing":0,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueWorkerTickRouteTest::__pest_evaluable_it_ticks_collected_invoice_transfer_queue_from_queue_list_and_status_routes":0.008,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueWorkerTickRouteTest::__pest_evaluable_it_ticks_order_transfer_queue_from_draft_and_invoice_status_routes":0.009,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueCronEntrypointWiringTest::__pest_evaluable_it_wires_root_cron_entrypoint_to_the_full_cron_scheduler_with_queue_worker_tasks":0.01,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueWorkerTickRouteTest::__pest_evaluable_it_exposes_an_explicit_collected_invoice_transfer_queue_run_route_instead_of_ticking_read_endpoints":0.007,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueWorkerTickRouteTest::__pest_evaluable_it_keeps_order_transfer_queue_status_routes_read_only":0.023,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueOpenApiSpecTest::__pest_evaluable_it_documents_additive_collected_invoice_queue_pagination_metadata_and_retry_conflict_semantics":0.022,"P\\Tests\\Unit\\Orders\\OrdersRouteStripePaymentIntentLifecycleWiringTest::__pest_evaluable_it_wires_mobile_stripe_payment_intent_routes_to_normalized_lifecycle_handling":0.004,"P\\Tests\\Unit\\Orders\\StripePaymentIntentsPersistenceWiringTest::__pest_evaluable_it_wires_stripe_payment_intent_persistence_to_prune_duplicates_and_clear_reader_state_safely":0.005,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheRuntimeBehaviorTest::__pest_evaluable_it_builds_department_weather_preload_targets_in_cli_without_a_request_uri":0,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueRouteHardeningTest::__pest_evaluable_it_runs_collected_invoice_queue_batches_through_an_explicit_manual_endpoint":0.004,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodQueueOverlayTest::__pest_evaluable_it_marks_queued_transactions_and_clears_requires__action_when_all_actionable_work_is_already_queued":0,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodQueueOverlayTest::__pest_evaluable_it_keeps_requires__action_true_when_a_customer_still_has_unqueued_actionable_transactions":0,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodQueueOverlayTest::__pest_evaluable_it_blocks_fixed_pricing_or_subscription_customers_with_no_transactions_when_a_relevant_queue_job_exists":0,"P\\Tests\\Unit\\DailyReports\\DepartmentDailyReportsOverviewRouteTest::__pest_evaluable_it_builds_the_overview_payload_from_batched_repository_data_with_deterministic_tile_states":0,"P\\Tests\\Unit\\DailyReports\\DepartmentDailyReportsOverviewRouteTest::__pest_evaluable_it_marks_overtime_unavailable_when_not_every_selected_department_can_be_mapped_to_workfeed":0,"P\\Tests\\Unit\\DailyReports\\DepartmentDailyReportsOverviewRouteTest::__pest_evaluable_it_normalizes_department_id_input_from_csv_strings_and_nested_values":0,"P\\Tests\\Unit\\DailyReports\\DepartmentDailyReportsOverviewRouteTest::__pest_evaluable_it_limits_overtime_counting_to_the_selected_reporting_range":0,"P\\Tests\\Unit\\DailyReports\\DepartmentDailyReportsOverviewRouteTest::__pest_evaluable_it_wires_the_overview_route_to_batched_repository_methods_and_overview_path":0.01,"P\\Tests\\Unit\\DailyReports\\DepartmentDailyReportsOverviewOpenApiSpecTest::__pest_evaluable_it_documents_the_daily_report_overview_endpoint_and_reusable_schemas_in_openapi":0.006,"P\\Tests\\Unit\\DailyReports\\DepartmentOutsideHoursStatisticsServiceTest::__pest_evaluable_it_counts_only_outside_hours_washes_and_flags_missing_opening_hours_without_counting_them_as_closed":0,"P\\Tests\\Unit\\DailyReports\\DepartmentOutsideHoursStatisticsServiceTest::__pest_evaluable_it_deduplicates_linked_washes_with_self_serve_first__then_xlvask__then_orders":0,"P\\Tests\\Unit\\DailyReports\\DepartmentOutsideHoursStatisticsServiceTest::__pest_evaluable_it_builds_daily_trend_points_with_per_day_missing_hours_diagnostics":0,"P\\Tests\\Unit\\DailyReports\\DepartmentDailyReportsOutsideHoursRouteContractTest::__pest_evaluable_it_wires_outside_hours_summary_and_trend_endpoints_through_the_dedicated_statistics_service":0.007,"P\\Tests\\Unit\\DailyReports\\DepartmentDailyReportsOutsideHoursOpenApiSpecTest::__pest_evaluable_it_documents_outside_hours_summary_and_trend_schemas_in_openapi":0.008,"P\\Tests\\Integration\\DailyReports\\DepartmentOutsideHoursStatisticsServiceIntegrationTest::__pest_evaluable_it_integrates_orders__xlvask__and_self_serve_into_one_outside_hours_summary_with_missing_hours_diagnostics":3.487,"P\\Tests\\Unit\\Selfserve\\EdgeGatewaySchemaBootstrapTest::__pest_evaluable_it_defines_the_edge_gateway_runtime_schema_bootstrap_tables":0.007,"P\\Tests\\Unit\\Selfserve\\EdgeGatewaySchemaBootstrapTest::__pest_evaluable_it_stores_edge_gateway_heartbeats__bindings__and_shell_transcript_data":0.002,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayRouteWiringTest::__pest_evaluable_it_registers_the_edge_gateway_management_REST_endpoints":0.005,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayRouteWiringTest::__pest_evaluable_it_registers_public_installer__claim__heartbeat__and_internal_broker_auth_endpoints":0.003,"P\\Tests\\Unit\\Selfserve\\GatewayShellyTransportTest::__pest_evaluable_it_maps_gateway_relay_status_responses_into_the_Shelly_cloud_payload_shape":0.001,"P\\Tests\\Unit\\Selfserve\\GatewayShellyTransportTest::__pest_evaluable_it_maps_gateway_relay_switch_responses_into_the_Shelly_cloud_payload_shape":0,"P\\Tests\\Integration\\SystemStatus\\SuperuserSystemStatusInfrastructureProbeTest::__pest_evaluable_it_can_probe_the_configured_database_in_integration_mode":0.058,"P\\Tests\\Integration\\SystemStatus\\SuperuserSystemStatusInfrastructureProbeTest::__pest_evaluable_it_can_probe_redis_in_integration_mode_when_configured":0.001,"P\\Tests\\Integration\\SystemStatus\\SuperuserSystemStatusInfrastructureProbeTest::__pest_evaluable_it_can_probe_minio_in_integration_mode_when_configured":2.707,"P\\Tests\\Unit\\DailyReports\\DepartmentDailyReportsComplaintsOpenApiSpecTest::__pest_evaluable_it_documents_the_daily_report_complaints_create_endpoint_and_complaint_schemas_in_openapi":0.099,"P\\Tests\\Unit\\DailyReports\\DepartmentDailyReportsComplaintsRouteContractTest::__pest_evaluable_it_wires_complaint_creation_through_a_dedicated_route_with_validation_and_customer_checks":0.003,"P\\Tests\\Integration\\DailyReports\\DepartmentDailyReportComplaintsIntegrationTest::__pest_evaluable_it_counts_complaint_rows_by_department_and_created__at_reporting_range":1.645,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_reduces_overall_status_using_down_and_degraded_precedence":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_classifies_runtime_usage_percentages_consistently":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_reuses_cached_module_probes_only_when_the_ttl_is_still_valid_and_force_is_false":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_classifies_authenticated_http_probe_responses_conservatively with data set \"dataset \"success\"\"":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_classifies_authenticated_http_probe_responses_conservatively with data set \"dataset \"unauthorized\"\"":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_classifies_authenticated_http_probe_responses_conservatively with data set \"dataset \"forbidden\"\"":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_classifies_authenticated_http_probe_responses_conservatively with data set \"dataset \"rate limited\"\"":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_classifies_authenticated_http_probe_responses_conservatively with data set \"dataset \"server error\"\"":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_classifies_authenticated_http_probe_responses_conservatively with data set \"dataset \"no response\"\"":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_classifies_transport_errors_as_down":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_interprets_recaptcha_probe_payloads_safely with data set \"dataset \"dummy token rejected but credentials valid\"\"":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_interprets_recaptcha_probe_payloads_safely with data set \"dataset \"invalid secret is down\"\"":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_interprets_recaptcha_probe_payloads_safely with data set \"dataset \"unexpected validation errors degrade\"\"":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_builds_the_expected_http_probe_requests_for_newly_supported_modules with data set \"dataset \"recaptcha\"\"":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_builds_the_expected_http_probe_requests_for_newly_supported_modules with data set \"dataset \"email\"\"":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_builds_the_expected_http_probe_requests_for_newly_supported_modules with data set \"dataset \"motorapi\"\"":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_builds_the_expected_http_probe_requests_for_newly_supported_modules with data set \"dataset \"fxratesapi\"\"":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_builds_the_expected_http_probe_requests_for_newly_supported_modules with data set \"dataset \"weatherapi\"\"":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_builds_the_expected_http_probe_requests_for_newly_supported_modules with data set \"dataset \"workfeed\"\"":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_builds_the_expected_http_probe_requests_for_newly_supported_modules with data set \"dataset \"gatewayapi\"\"":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_builds_the_expected_http_probe_requests_for_newly_supported_modules with data set \"dataset \"xlvask\"\"":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_builds_the_expected_http_probe_requests_for_newly_supported_modules with data set \"dataset \"limble\"\"":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_builds_the_expected_http_probe_requests_for_newly_supported_modules with data set \"dataset \"license plate recognizer\"\"":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_uses_runtime_economic_credentials_for_the_economic_probe":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_returns_down_without_attempting_economic_http_calls_when_runtime_credentials_are_missing":0.048,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_reports_backup_probe_failures_from_local_validation":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_validates_selfserve_schema_and_minute_product_configuration":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_reports_invalid_selfserve_minute_configuration_before_touching_the_schema":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_reports_missing_selfserve_minute_products":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_surfaces_selfserve_bootstrap_failures":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_marks_newly_supported_modules_as_probe_backed_and_leaves_only_truly_unsupported_modules_as_configuration_only":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusOpenApiSpecTest::__pest_evaluable_it_documents_the_superuser_system_status_snapshot_endpoint_in_openapi":0.01,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusOpenApiSpecTest::__pest_evaluable_it_defines_the_reusable_system_status_schemas_and_enums":0.007,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusRouteWiringTest::__pest_evaluable_it_registers_the_aggregated_superuser_system_status_endpoint_and_permission":0.025,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusRouteWiringTest::__pest_evaluable_it_keeps_the_legacy_database_status_endpoint_wired_through_the_shared_snapshot_service":0.005,"P\\Tests\\Unit\\SystemStatus\\SystemSessionActivityTrackerTest::__pest_evaluable_it_detects_device_types_from_common_user_agents":0,"P\\Tests\\Unit\\SystemStatus\\SystemSessionActivityTrackerTest::__pest_evaluable_it_treats_recent_sessions_as_active_within_the_configured_activity_window":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_builds_install_script_urls_with_forwarded_https_scheme_when_proxied":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_appends_forwarded_ports_when_the_forwarded_host_omits_them":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_prefers_EDGE__PUBLIC__API__URL_when_explicitly_configured":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_builds_the_expected_http_probe_requests_for_newly_supported_modules with data set \"dataset \"bird\"\"":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_returns_configured_for_shelly_when_no_known_device_id_is_available_for_probing":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_builds_an_authenticated_shelly_status_probe_when_a_known_device_id_exists":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_infers_https_for_the_staging_api_host_when_only_the_https_port_is_present":0,"P\\Tests\\Unit\\DailyReports\\DepartmentDailyReportsOutsideHoursRouteContractTest::__pest_evaluable_it_initializes_the_outside_hours_statistics_service_before_building_the_transaction_count_summary_payload":0.004,"P\\Tests\\Unit\\Selfserve\\EdgeBrokerClientConfigTest::__pest_evaluable_it_uses_the_same_default_broker_url_and_shared_secret_fallback_as_the_docker_stack":0.293,"P\\Tests\\Integration\\DailyReports\\DepartmentDailyReportComplaintsIntegrationTest::__pest_evaluable_it_updates_and_deletes_complaint_rows":1.493,"P\\Tests\\Integration\\DailyReports\\DepartmentDailyReportComplaintsIntegrationTest::__pest_evaluable_it_parses_created__by__name_from_the_users_table":1.205,"P\\Tests\\Unit\\SystemStatus\\SystemSessionActivityTrackerTest::__pest_evaluable_it_converts_database_utc_datetimes_into_timezone_aware_iso_strings":0,"P\\Tests\\Unit\\SystemStatus\\SystemSessionActivityTrackerTest::__pest_evaluable_it_treats_timezone_aware_iso_timestamps_as_active_using_absolute_time":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_normalizes_menu_digit_input_from_Bird_dtmf_payload_values":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_captures_input_and_keeps_prompting_while_the_call_is_still_in_the_menu_flow":0.029,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_reads_dtmf_values_from_alternate_gather_payload_shapes":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_opens_the_selected_gate_after_department_and_gate_type_have_been_entered":0.074,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_auto_opens_the_only_eligible_gate_without_asking_for_input":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_reprompts_when_a_gate_type_selection_is_invalid":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_completes_immediately_when_no_phone_call_gates_are_configured":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_reports_gate_open_failures_without_reopening_on_retries":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_keeps_completed_state_until_a_terminal_webhook_status_arrives__then_clears_it":0,"P\\Tests\\Unit\\Bird\\DepartmentGatesPhoneCallOpenTest::__pest_evaluable_it_forwards_phone_number_and_call_duration_threshold_to_the_Bird_gate_helper":0.002,"P\\Tests\\Unit\\Bird\\DepartmentGatesPhoneCallOpenTest::__pest_evaluable_it_wraps_Bird_helper_failures_and_reports_them_to_Slack":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_normalizes_and_deduplicates_warning_entries_for_snapshots":0,"P\\Tests\\Unit\\SystemStatus\\SuperuserSystemStatusServiceTest::__pest_evaluable_it_adds_localization_metadata_for_disabled_and_missing_config_modules":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_treats_gather_callbacks_with_completed_status_and_dtmf_as_menu_input_instead_of_terminal_completion":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_hangs_up_when_the_caller_presses_pound_to_finish":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_hangs_up_when_Bird_reports_a_gather_completion_without_digits":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_hangs_up_when_the_caller_presses_0_to_finish":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_treats_a_bare_completed_callback_during_selection_as_caller_finished":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_installs_a_provider_managed_Bird_call_flow_for_menu_driven_inbound_calls_when_enabled":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_keeps_provider_managed_inbound_calls_passive_until_a_terminal_webhook_arrives":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_ignores_outgoing_bridged_child_call_callbacks_on_the_inbound_webhook_route":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_effective_gateway_status_from_heartbeat_freshness with data set \"dataset \"recent heartbeat stays online\"\"":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_effective_gateway_status_from_heartbeat_freshness with data set \"dataset \"late heartbeat degrades after one minute\"\"":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_effective_gateway_status_from_heartbeat_freshness with data set \"dataset \"stale heartbeat goes offline after five minutes\"\"":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_effective_gateway_status_from_heartbeat_freshness with data set \"dataset \"explicit offline reports stay offline even when heartbeat is fresh\"\"":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_effective_gateway_status_from_heartbeat_freshness with data set \"dataset \"missing heartbeat is treated as offline\"\"":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_marks_ready_discovery_as_stale_when_the_gateway_heartbeat_has_expired":0,"P\\Tests\\Unit\\Orders\\OrdersInputNormalizerTest::__pest_evaluable_it_normalizes_sql_and_datetime_local_created__at_values":0,"P\\Tests\\Unit\\Orders\\OrdersInputNormalizerTest::__pest_evaluable_it_rejects_invalid_created__at_values":0,"P\\Tests\\Unit\\Orders\\OrdersInputNormalizerTest::__pest_evaluable_it_normalizes_include__in__invoice_tri_state_inputs":0,"P\\Tests\\Unit\\Orders\\OrdersInputNormalizerTest::__pest_evaluable_it_rejects_invalid_include__in__invoice_values":0,"P\\Tests\\Unit\\Orders\\OrdersIncludeInInvoiceOverrideTest::__pest_evaluable_it_lets_the_order_level_override_include_an_otherwise_excluded_order":0,"P\\Tests\\Unit\\Orders\\OrdersIncludeInInvoiceOverrideTest::__pest_evaluable_it_lets_the_order_level_override_exclude_an_otherwise_included_order":0,"P\\Tests\\Unit\\Orders\\OrdersIncludeInInvoiceOverrideTest::__pest_evaluable_it_falls_back_to_the_department_invoicing_rule_when_the_override_is_null":0,"P\\Tests\\Unit\\Orders\\OrdersIncludeInInvoiceOverrideTest::__pest_evaluable_it_serializes_both_raw_and_effective_include__in__invoice_values":0,"P\\Tests\\Unit\\Orders\\OrdersRouteSettingsUpdateWiringTest::__pest_evaluable_it_wires_order_create_and_update_routes_through_the_settings_normalizers":0.006,"P\\Tests\\Unit\\Orders\\OrdersRouteSettingsUpdateWiringTest::__pest_evaluable_it_keeps_line_item_invoice_filtering_in_the_order_net_amount_calculation":0.008,"P\\Tests\\Unit\\Invoicing\\EconomicV2DistributionServiceOrderOverrideTest::__pest_evaluable_it_inherits_department_eligibility_when_no_order_override_is_set":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2DistributionServiceOrderOverrideTest::__pest_evaluable_it_allows_an_order_level_include_override_to_overrule_department_exclusion":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2DistributionServiceOrderOverrideTest::__pest_evaluable_it_allows_an_order_level_exclude_override_to_overrule_department_inclusion":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_includes_node_pty_build_prerequisites_in_the_generated_install_script":0,"P\\Tests\\Unit\\DailyReports\\DepartmentDailyReportsComplaintsRouteContractTest::__pest_evaluable_it_wires_complaint_create__lookup__list__edit__and_delete_routes_with_validation_and_parsing":0.004,"P\\Tests\\Unit\\DailyReports\\DepartmentDailyReportsComplaintsOpenApiSpecTest::__pest_evaluable_it_documents_the_daily_report_complaints_CRUD_and_customer_lookup_endpoints_in_openapi":0.009,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerCommandQueueTest::__pest_evaluable_it_queues_admin_commands__exposes_agent_poll_result_handlers__and_keeps_heartbeats_from_mutating_discovery_state":0.004,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayRouteWiringTest::__pest_evaluable_it_registers_public_installer__claim__heartbeat__command_polling__and_internal_broker_auth_endpoints":0.004,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_normalizes_public_broker_overrides_to_https_and_browser_shell_urls_to_wss":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_keeps_localhost_broker_overrides_on_plain_http_and_ws_for_local_development":0,"P\\Tests\\Unit\\Invoicing\\EconomicCustomersDiscountFallbackTest::__pest_evaluable_it_falls_back_to_a_later_customer_template_product_when_earlier_probes_fail_on_missing_currency_prices":0,"P\\Tests\\Unit\\Invoicing\\EconomicCustomersDiscountFallbackTest::__pest_evaluable_it_returns_zero_when_every_customer_template_lookup_fails_due_to_missing_currency_prices":0,"P\\Tests\\Unit\\Invoicing\\EconomicCustomersDiscountFallbackTest::__pest_evaluable_it_rethrows_unrelated_discount_lookup_failures":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerCommandQueueTest::__pest_evaluable_it_defines_the_dispatchable_gateway_guard_on_the_loaded_manager_class":0.074,"P\\Tests\\Unit\\Auth\\RegisterCvrLegacyScriptTest::__pest_evaluable_it_keeps_the_legacy_register_cvr_route_harness_passing":0.05,"P\\Tests\\Unit\\Auth\\EconomicCreateCustomerResponseTest::__pest_evaluable_it_returns_the_raw_upstream_create_response_and_preserves_the_requested_payload":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherStatusTargetsTest::__pest_evaluable_it_aggregates_multi_department_slot_statuses_using_worst_severity_when_any_department_is_unhealthy":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherStatusTargetsTest::__pest_evaluable_it_aggregates_all_unhealthy_multi_department_slot_statuses_as_unhealthy":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherStatusTargetsTest::__pest_evaluable_it_returns_unknown_when_aggregated_multi_department_slot_targets_are_missing":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherStatusTargetsTest::__pest_evaluable_it_returns_unknown_when_aggregated_multi_department_slot_has_no_evaluable_hours":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherStatusTargetsTest::__pest_evaluable_it_returns_unknown_when_aggregated_multi_department_slot_has_not_started_yet":0,"P\\Tests\\Api\\ApiCoverageManifestTest::__pest_evaluable_it_keeps_every_selected_API_operation_covered_by_happy_path_and_failure_tests":0.24,"P\\Tests\\Api\\ApiCoverageManifestTest::__pest_evaluable_it_keeps_the_OpenAPI_manifest_entries_aligned_with_the_API_spec":0.094,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_logs_in_with_valid_customer_credentials":3.336,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_rejects_invalid_login_payloads_and_credentials":3.542,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_returns_the_cached_auth_session_payload_for_a_valid_token":2.334,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_rejects_invalid_auth_session_tokens":0.645,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_logs_out_and_invalidates_the_token_for_future_session_calls":3.23,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_rejects_invalid_logout_tokens":0.695,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_lists_only_visible_departments_and_can_return_a_single_department_with_the_slack_webhook":13.102,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_department_listing_when_the_permission_is_missing":6.216,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_creates_departments_through_the_real_endpoint":5.079,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_invalid_department_create_requests":9.316,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_updates_departments_through_the_real_endpoint":6.519,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_invalid_department_update_requests":10.094,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_lists_department_categories_for_a_department":7.627,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_invalid_department_category_requests":10.419,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_lists_orders_for_an_admin_scoped_user_and_limits_the_results_to_the_permitted_departments":14.918,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_lists_only_the_targeted_customer_orders_for_subuser_sessions":14.741,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_returns_the_current_auth_and_permission_failures_when_order_listing_is_not_allowed":6.836,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_creates_orders_through_the_real_endpoint":12.445,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_order_creation_requests":13.421,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_updates_orders_through_the_primary_endpoint":13.978,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_updates_through_the_primary_order_endpoint":7.085,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_updates_orders_through_the_legacy_alias_endpoint":14.545,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_updates_through_the_legacy_alias_endpoint":4.878,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_deletes_orders_through_the_real_endpoint":9.444,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_order_delete_requests":7.25,"P\\Tests\\Api\\PingApiTest::__pest_evaluable_it_returns_the_ping_contract":0.506,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerCommandQueueTest::__pest_evaluable_it_defines_the_dispatchable_gateway_guard_and_api_polled_shell_queue_on_the_loaded_manager_class":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_merges_incoming_heartbeat_metadata_with_existing_gateway_metadata":0.006,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayRouteWiringTest::__pest_evaluable_it_registers_public_installer__claim__heartbeat__command_polling__and_shell_polling_endpoints":0.004,"P\\Tests\\Unit\\Selfserve\\EdgeGatewaySchemaBootstrapTest::__pest_evaluable_it_stores_edge_gateway_heartbeats__bindings__shell_transcripts__and_shell_polling_queues":0.004,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayShellPollingTransportTest::__pest_evaluable_it_rewrites_edge_gateway_shell_transport_to_API_polling_queues":0.011,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_processes_only_collected_invoice_jobs_and_respects_the_manual_batch_limit":2.032,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_returns_a_raw_202_gather_response_for_initial_department_selection":0.012,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_returns_a_second_stage_raw_202_gather_response_after_department_selection":0.015,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_opens_the_selected_gate_and_clears_redis_state_on_final_selection":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_reprompts_with_invalid_selection_while_keeping_webhook_state":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_uses_fallback_dtmf_extraction_when_event_gather_keys_are_missing":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_auto_opens_the_only_eligible_gate_without_returning_a_gather_command":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_returns_a_direct_raw_200_completion_when_no_departments_are_eligible":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_returns_a_business_failure_raw_200_response_when_gate_opening_fails":0,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_returns_the_raw_202_gather_webhook_contract_over_HTTP":7.629,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_returns_a_raw_400_transport_error_for_malformed_webhook_payloads":2.803,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_supports_legacy_field_value_metadata_updates_through_the_primary_endpoint":28.013,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_supports_legacy_field_value_metadata_updates_through_the_alias_endpoint":28.214,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_unsupported_legacy_field_value_updates_on_both_update_endpoints":9.781,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_still_prompts_for_department_selection_when_only_one_department_is_eligible":5.421,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_uses_a_multi_digit_gather_contract_when_10_departments_are_eligible":20.061,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_builds_deterministic_department_digit_map_without_capping_options":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_builds_compact_gate_option_maps_and_resolves_selected_gate_type_by_digit":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_builds_department_prompts_with_multi_digit_guidance_and_compact_gate_prompts":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_prompts_for_department_selection_even_when_only_one_eligible_department_exists":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_prompts_for_a_single_available_gate_type_and_opens_only_after_explicit_confirmation":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_supports_multi_digit_department_selections_before_gate_confirmation":0,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_accepts_the_legacy_initial_webhook_body_with_top_level_call_identifiers":5.806,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_returns_native_flow_gather_data_after_a_top_level_department_selection":6.884,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_accepts_legacy_initial_webhook_payloads_with_top_level_call_identifiers":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_returns_a_flow_data_gather_payload_after_department_selection_in_native_flow_mode":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_returns_a_flow_data_completion_payload_after_gate_confirmation_in_native_flow_mode":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_continues_with_the_first_gather_prompt_when_backend_call_acceptance_fails":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_returns_a_flow_data_gather_payload_after_department_selection_in_native_flow_mode_when_distinct_gate_choices_exist":0.001,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_opens_the_gate_immediately_after_department_selection_when_entrance_and_exit_resolve_to_the_same_gate_in_native_flow_mode":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_returns_a_second_stage_raw_202_gather_response_after_department_selection_when_distinct_gate_choices_exist":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_opens_the_shared_gate_immediately_after_department_selection_in_raw_command_mode":0,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_returns_native_flow_gather_data_after_a_top_level_department_selection_when_distinct_gate_choices_exist":10.765,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_opens_the_gate_immediately_after_a_top_level_department_selection_when_entrance_and_exit_share_the_same_gate":13.11,"P\\Tests\\Api\\VehiclesApiTest::__pest_evaluable_it_returns_the_newest_vehicle_last__order__id_that_still_has_order_items":18.499,"P\\Tests\\Api\\VehiclesApiTest::__pest_evaluable_it_returns_a_null_vehicle_last__order__id_when_no_order_with_items_exists":18.324,"P\\Tests\\Unit\\Bookings\\VehicleSearchBookedMetadataRouteContractTest::__pest_evaluable_it_keeps_booked_vehicle_search_metadata_aligned_with_filtered_pending_order_bookings":0.008,"P\\Tests\\Unit\\Orders\\OrdersAutoWashCertificateCompletionTest::__pest_evaluable_it_auto_attaches_a_wash_certificate_on_order_completion_when_a_wash_certificate_item_is_present":0,"P\\Tests\\Unit\\Orders\\OrdersAutoWashCertificateCompletionTest::__pest_evaluable_it_keeps_order_completion_idempotent_when_a_wash_certificate_is_already_attached":0,"P\\Tests\\Unit\\Orders\\OrdersAutoWashCertificateCompletionTest::__pest_evaluable_it_allows_blank_safety_seal_values_when_auto_attaching_a_wash_certificate_on_completion":0,"P\\Tests\\Unit\\Orders\\OrderBookingsCompletionDedupTest::__pest_evaluable_it_does_not_create_or_email_a_duplicate_wash_certificate_when_a_booking_is_already_linked_to_a_pos_order":0,"P\\Tests\\Unit\\Orders\\OrderBookingsCompletionDedupTest::__pest_evaluable_it_keeps_standalone_booking_completion_behavior_unchanged_for_wash_certificates":0,"P\\Tests\\Unit\\Subusers\\SubusersRouteManagementContractTest::__pest_evaluable_it_exposes_chauffeur_management_endpoints_on_the_subusers_route":0.006,"P\\Tests\\Unit\\Subusers\\SubusersRouteManagementContractTest::__pest_evaluable_it_includes_grant_management_fields_in_the_subusers_payload_builder":0.004,"P\\Tests\\Unit\\Subusers\\SubusersRouteManagementContractTest::__pest_evaluable_it_links_grant_disable_operations_to_SUBUSERS__DELETE_for_own_customer_managers":0.005,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_logs_out_and_invalidates_cached_subuser_sessions":7.985,"P\\Tests\\Unit\\Subusers\\SubusersRouteManagementContractTest::__pest_evaluable_it_prevents_own_customer_managers_from_editing_driver_owned_account_profiles":0.004,"P\\Tests\\Unit\\Subusers\\SubusersRouteManagementContractTest::__pest_evaluable_it_only_allows_invite_resend_while_setup_is_still_pending":0.004,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_returns_a_setup_required_error_when_department_terminal_readers_are_requested_without_terminal_setup":5.816,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_returns_a_setup_required_error_when_creating_a_payment_intent_for_a_department_without_terminal_setup":7.123}} \ No newline at end of file +{"version":"pest_3.8.6","defects":{"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_self_serve_machine_type__eligibility__summary__and_webhook_endpoints":1,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_defines_reusable_self_serve_wash_and_machine_type_schemas":1,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_redistributes_booked_department_75_net_amounts_using_fixed_pricing_and_wash_subscription_weights":7,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_keeps_classified_booked_department_75_amounts_undistributed_when_no_monthly_basis_exists":8,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_keeps_unclassified_booked_department_75_lines_undistributed_with_warnings":8,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_documents_v2_historical_distribution_and_pricing_history_paths_in_openapi":1,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_defines_new_reusable_v2_schemas_for_normalization_comparison_versioning_and_distribution":1,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_supports_bulk_booked_invoice_line_payloads_with_top_level_department_numbers":7,"P\\Tests\\Unit\\Redis\\RedisAtomicReservationTest::__pest_evaluable_it_claims_a_slot_atomically_with_nx_and_expiration":8,"P\\Tests\\Unit\\Redis\\RedisAtomicReservationTest::__pest_evaluable_it_returns_false_when_the_slot_is_already_claimed_and_clamps_ttl_to_one_second":8,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_documents_economic_v2_invoice_paths_in_openapi":1,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_aligns_legacy_compare_schema_with_runtime_payload_by_removing_stale_required_order__ids":1,"P\\Tests\\Unit\\Search\\SystemSearchEntityTypeCoverageTest::__pest_evaluable_it_documents_every_supported_search_entity_type_in_openapi_enum":1,"P\\Tests\\Unit\\Search\\SystemSearchOpenApiSpecTest::__pest_evaluable_it_documents_system_wide_search_endpoints_in_openapi":1,"P\\Tests\\Unit\\Search\\SystemSearchOpenApiSpecTest::__pest_evaluable_it_documents_debug__intent_and_parser_metadata_schema_in_openapi":1,"P\\Tests\\Unit\\Search\\SystemSearchOpenApiSpecTest::__pest_evaluable_it_documents_e_conomic_indexed_customer_matching_and_synonym_behavior":1,"P\\Tests\\Integration\\Database\\DbConnectionTest::__pest_evaluable_it_can_connect_to_a_configured_MySQL_instance_in_integration_mode":1,"P\\Tests\\Integration\\Invoicing\\EconomicV2BackfillAndDistributionIntegrationTest::__pest_evaluable_it_runs_best_effort_backfill_repeatedly_without_introducing_duplicate_same_start_rows":1,"P\\Tests\\Integration\\Invoicing\\EconomicV2BackfillAndDistributionIntegrationTest::__pest_evaluable_it_resolves_version_aware_distribution_payload_shapes_over_a_real_date_range":1,"P\\Tests\\Integration\\Invoicing\\EconomicV2VersioningServiceIntegrationTest::__pest_evaluable_it_creates_closes_and_rotates_fixed_pricing_versions_without_overlap":1,"P\\Tests\\Integration\\Invoicing\\EconomicV2VersioningServiceIntegrationTest::__pest_evaluable_it_tracks_vehicle_and_discount_version_lifecycles_with_closure_semantics":1,"P\\Tests\\Integration\\Permissions\\LegacyPermissionRedisCacheScriptTest::__pest_evaluable_it_keeps_legacy_route_permission_redis_cache_script_green":1,"P\\Tests\\Unit\\Goals\\GoalsOpenApiSpecTest::__pest_evaluable_it_documents_advanced_goals_target_duration_fields_in_openapi":1,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineSchemaConformanceTest::__pest_evaluable_it_documents_the_date_key_in_the_openapi_department_weather_timeline_schema":1,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_builds_timeline_entries_with_mostly__clear_weather_when_forecast_payload_is_empty":8,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_wires_departments_weather_route_through_the_forecast_fallback_path":7,"P\\Tests\\Unit\\Orders\\AttachmentsListManyTest::__pest_evaluable_it_groups_attachments_by_object_id_and_preserves_empty_object_groups":8,"P\\Tests\\Unit\\Orders\\AttachmentsListManyTest::__pest_evaluable_it_keeps_listMany_payload_parity_with_list_for_one_object_id":8,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTargetsOpenApiSpecTest::__pest_evaluable_it_documents_department_weather_target_endpoints_and_reusable_schemas_in_openapi":1,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTargetsOpenApiSpecTest::__pest_evaluable_it_documents_unknown_weather_status_behavior_when_targets_are_missing":1,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_respects_the_1_request_per_second_Shelly_gate_for_back_to_back_requests":8,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_in_progress_self_serve_wash_start_and_machine_relay_fields":1,"P\\Tests\\Unit\\Selfserve\\SelfserveConfigVersioningTest::__pest_evaluable_it_stores_config_json_with_apostrophes_without_violating_json_constraints":8,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_adds_vehicle_type_product_on_STOP_when_program_selector_is_on__then_turns_off_cleaner_machine_and_selector_relays":7,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_skips_vehicle_type_product_add_when_program_selector_is_off_and_only_disables_configured_relays":7,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_self_serve_machine_wash_included_minutes_in_config_schemas":1,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_fast_when_workspace_id_is_missing_for_gate_calls":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_fast_when_channel_id_is_missing_for_gate_calls":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_with_terminal_status_details_when_call_never_reaches_accepted_state":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_hangs_up_when_call_reaches_accepted_state":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_hangs_up_immediately_when_status_transitions_to_accepted":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_maps_legacy_timeout_option_to_documented_ringTimeout_payload_field":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_clamps_derived_ringTimeout_to_Bird_documented_max_when_gate_timeout_is_high":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_passes_documented_hangup_cause_when_provided":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_drops_unsupported_hangup_cause_values_from_request_payload":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_fast_when_workspace_id_is_missing_for_gate_flash_calls":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_creates_gate_flash_call_with_documented_ringTimeout_payload":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_polls_flash_call_and_succeeds_once_accepted":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_flash_gate_flow_when_terminal_failure_status_is_returned":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_does_not_fallback_to_regular_gate_call_when_flash_succeeds":8,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_falls_back_to_regular_gate_call_when_flash_fails":8,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_property_gate_lane_commands_and_sanitized_gate_failure_responses":1,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_returns_raw_payload_for_malformed_JSON_responses_and_null_for_empty_responses":8,"P\\Tests\\Unit\\Bird\\BirdRouteWiringTest::__pest_evaluable_it_keeps_Bird_number_and_webhook_routes_intact":7,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_handles_malformed_and_empty_response_bodies_without_throwing_transport_errors":8,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherStatusTargetsTest::__pest_evaluable_it_aggregates_multi_department_slot_statuses_using_worst_severity_and_unknown_fallback_rules":7,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_adds_vehicle_type_product_on_STOP_when_program_selector_relay_is_online__then_turns_off_cleaner_and_machine_relays":7,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_bills_primary_product_when_selector_relay_is_online_even_if_relay_output_is_off":7,"P\\Tests\\Unit\\Bird\\BirdOpenApiSpecTest::__pest_evaluable_it_documents_all_Bird_voice_call_parity_endpoints_including_gather_recordings_insights_and_log":1,"P\\Tests\\Unit\\Bird\\BirdOpenApiSpecTest::__pest_evaluable_it_documents_flash_hangup_endpoint_and_marks_end_alias_as_deprecated":1,"P\\Tests\\Unit\\Bird\\BirdOpenApiSpecTest::__pest_evaluable_it_defines_request_and_response_schemas_for_Bird_call_command_recording_insight_log_and_flash_payloads":1,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueRouteRegistrationTest::__pest_evaluable_it_registers_collected_invoice_queue_endpoints_in_orderInvoicesRoute":7,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_uses_a_consistent_unavailable_service_contract_for_missing_queue_dependencies":7,"P\\Tests\\Unit\\Invoicing\\EconomicTransferOrderItemSkipTest::__pest_evaluable_it_wires_zero_cost_and_zero_quantity_skip_guard_into_transfer_line_builder":7,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_processes_queued_transfer_jobs_to_completion":1,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_deduplicates_active_jobs_per_transfer_target":1,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_rejects_invalid_payloads_without_inserting_queue_rows":1,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_supports_fail_retry_and_reprocess_lifecycle_transitions":1,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_supports_synchronous_fallback_branches_before_queue_enqueue_on_economic_invoice_export_routes":7,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_keeps_queue_status_endpoints_guarded_while_collected_invoice_export_routes_support_synchronous_fallback":7,"P\\Tests\\Unit\\Invoicing\\UserCollectedInvoiceUpdateRouteValidationTest::__pest_evaluable_it_supports_independent_po__number_and_closed__at_updates_for_PUT__collected_invoices_in_user_route":7,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueRouteHardeningTest::__pest_evaluable_it_returns_pagination_metadata_and_strict_status_handling_for_collected_invoice_queue_list":7,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueRouteHardeningTest::__pest_evaluable_it_enforces_retry_constraints_for_collected_invoice_queue_jobs_before_retry_execution":7,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueOpenApiSpecTest::__pest_evaluable_it_documents_collected_queue_list_metadata_and_strict_status_filter_enum_in_openapi":7,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_builds_a_completed_collected_invoice_queue_summary_from_payload_and_result":8,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_prefers_queue_error_messages_for_failed_jobs_and_keeps_null_safe_outcome_fields":8,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_uses_deterministic_status_message_fallback_when_no_explicit_message_exists":8,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_handles_object_payload_result_values_and_malformed_fields_without_throwing":8,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueWorkerTickRouteTest::__pest_evaluable_it_ticks_collected_invoice_transfer_queue_from_queue_list_and_status_routes":7,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueWorkerTickRouteTest::__pest_evaluable_it_ticks_order_transfer_queue_from_draft_and_invoice_status_routes":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_builds_the_installer_around_the_PHP_agent_artifacts_and_management_polling_config":8,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_exposes_update_payload__credential_rotation__and_operation_endpoints_without_shell_transport_wiring":8,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_relay_fallback_and_transport_health_details_without_shell_or_update_runtime_state":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_resolves_edge_agent_artifacts_from_the_source_tree_layout":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_prioritizes_mounted_and_baked_in_artifact_directories_before_repo_fallbacks":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_prioritizes_router_resources_before_mounted_and_baked_in_artifact_directories":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_lists_orders_for_an_admin_scoped_user_and_limits_the_results_to_the_permitted_departments":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_lists_only_the_targeted_customer_orders_for_subuser_sessions":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_returns_the_current_auth_and_permission_failures_when_order_listing_is_not_allowed":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_creates_orders_through_the_real_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_order_creation_requests":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_updates_orders_through_the_primary_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_reassigns_invoice_collections_when_changing_an_order_across_the_draft_customer_boundary":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_supports_legacy_field_value_metadata_updates_through_the_primary_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_updates_through_the_primary_order_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_updates_orders_through_the_legacy_alias_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_supports_legacy_field_value_metadata_updates_through_the_alias_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_updates_through_the_legacy_alias_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_unsupported_legacy_field_value_updates_on_both_update_endpoints":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_deletes_orders_through_the_real_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_order_delete_requests":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_regenerates_attached_wash_certificates_when_certificate_metadata_changes_through_the_primary_endpoint":7,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_regenerates_attached_wash_certificates_for_legacy_field_value_updates_through_the_alias_endpoint":7,"P\\Tests\\Api\\ApiCoverageManifestTest::__pest_evaluable_it_keeps_every_selected_API_operation_covered_by_happy_path_and_failure_tests":1,"P\\Tests\\Api\\ApiCoverageManifestTest::__pest_evaluable_it_keeps_the_OpenAPI_manifest_entries_aligned_with_the_API_spec":1,"P\\Tests\\Api\\ApiFixturesCleanupTest::__pest_evaluable_it_removes_generated_customer_traces_during_fixture_cleanup":1,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_logs_in_with_valid_customer_credentials":7,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_rejects_invalid_login_payloads_and_credentials":7,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_returns_the_cached_auth_session_payload_for_a_valid_token":7,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_includes_economic_runtime_config_for_uncached_auth_sessions":7,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_rejects_invalid_auth_session_tokens":1,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_logs_out_and_invalidates_the_token_for_future_session_calls":7,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_logs_out_and_invalidates_cached_subuser_sessions":7,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_rejects_invalid_logout_tokens":1,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_returns_the_raw_202_gather_webhook_contract_over_HTTP":1,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_still_prompts_for_department_selection_when_only_one_department_is_eligible":7,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_accepts_the_legacy_initial_webhook_body_with_top_level_call_identifiers":7,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_returns_native_flow_gather_data_after_a_top_level_department_selection_when_distinct_gate_choices_exist":7,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_opens_the_gate_immediately_after_a_top_level_department_selection_when_entrance_and_exit_share_the_same_gate":7,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_uses_a_multi_digit_gather_contract_when_10_departments_are_eligible":7,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_returns_a_raw_400_transport_error_for_malformed_webhook_payloads":1,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_lists_only_visible_departments_and_can_return_a_single_department_with_the_slack_webhook":7,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_department_listing_when_the_permission_is_missing":7,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_creates_departments_through_the_real_endpoint":7,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_invalid_department_create_requests":7,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_updates_departments_through_the_real_endpoint":7,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_invalid_department_update_requests":7,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_lists_department_categories_for_a_department":7,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_invalid_department_category_requests":7,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_lists_the_draft_customer_config_entry_in_economic_config_responses":7,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_round_trips_the_draft_customer_config_value_through_economic_config_updates":7,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_rejects_order_draft_exports_for_the_configured_draft_customer":7,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_rejects_booked_invoice_exports_for_the_configured_draft_customer":7,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_rejects_collected_invoice_exports_for_the_configured_draft_customer":7,"P\\Tests\\Api\\PingApiTest::__pest_evaluable_it_returns_the_ping_contract":1,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_returns_a_setup_required_error_when_department_terminal_readers_are_requested_without_terminal_setup":8,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_sends_a_Stripe_invoice_by_email_and_persists_the_hosted_invoice_association":8,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_returns_a_conflict_when_a_Stripe_hosted_invoice_is_already_active_for_the_order":8,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_allows_sending_a_new_Stripe_hosted_invoice_when_the_existing_association_is_already_terminal":8,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_voids_an_unpaid_Stripe_hosted_invoice_and_clears_the_local_order_association":8,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_refuses_to_cancel_a_paid_Stripe_hosted_invoice":8,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_returns_a_setup_required_error_when_creating_a_payment_intent_for_a_department_without_terminal_setup":8,"P\\Tests\\Api\\VehiclesApiTest::__pest_evaluable_it_returns_the_newest_vehicle_last__order__id_that_still_has_order_items":7,"P\\Tests\\Api\\VehiclesApiTest::__pest_evaluable_it_returns_a_null_vehicle_last__order__id_when_no_order_with_items_exists":7},"times":{"P\\Tests\\Unit\\Auth\\CreateTokenTest::__pest_evaluable_it_creates_a_64_char_auth_token_for_an_existing_user":0.11,"P\\Tests\\Unit\\Auth\\CreateTokenTest::__pest_evaluable_it_throws_a_clear_exception_when_customer_user_is_missing":0.017,"P\\Tests\\Unit\\Auth\\LegacyAuthScriptParityTest::__pest_evaluable_it_keeps_passkey_challenge_legacy_script_green_during_migration":0.028,"P\\Tests\\Unit\\Auth\\LegacyAuthScriptParityTest::__pest_evaluable_it_keeps_register_CVR_legacy_script_green_during_migration":0.029,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_builds_deterministic_department_digit_map_and_caps_to_9_options":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_resolves_department_ids_by_selected_digit":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_resolves_gate_type_digit_to_entrance_or_exit":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_stores__loads_and_clears_ivr_state_with_ttl":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_validates_state_caller_fingerprint_matching":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteIvrTest::__pest_evaluable_it_builds_department_and_gate_prompts":0,"P\\Tests\\Unit\\Permissions\\PermissionNodeDefinitionTest::__pest_evaluable_it_creates_permission_nodes_linked_to_subuser_permission_keys":0,"P\\Tests\\Unit\\Permissions\\PermissionNodeDefinitionTest::__pest_evaluable_it_rejects_empty_permission_definitions":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_normalizes_button_arrays__json_and_csv_inputs":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_rejects_invalid_button_inputs":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_normalizes_vehicle_type_values_and_null_semantics":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_rejects_invalid_vehicle_type_values":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_rejects_non_numeric_vehicle_type_strings":0,"P\\Tests\\Unit\\Selfserve\\NormalizationTest::__pest_evaluable_it_keeps_lane_service_enum_contract_for_MACHINE":0,"P\\Tests\\Unit\\Subusers\\SubuserGrantInitializationTest::__pest_evaluable_it_initializes_booking_node_grants_without_DB_dependency":0,"P\\Tests\\Unit\\Subusers\\SubuserGrantInitializationTest::__pest_evaluable_it_initializes_selfserve_node_grants_without_DB_dependency":0,"P\\Tests\\Unit\\Subusers\\SubusersRoutePermissionLinkTest::__pest_evaluable_it_keeps_subusers_route_list_permission_linked_to_SUBUSERS__LIST_node":0.005,"P\\Tests\\Unit\\Search\\SystemSearchEntityTypeCoverageTest::__pest_evaluable_it_keeps_route_and_service_entity_type_registries_in_sync_with_expanded_coverage":0,"P\\Tests\\Unit\\Search\\SystemSearchEntityTypeCoverageTest::__pest_evaluable_it_documents_every_supported_search_entity_type_in_openapi_enum":0.009,"P\\Tests\\Unit\\Search\\SystemSearchEconomicCustomerIndexWiringTest::__pest_evaluable_it_wires_local_e_conomic_customer_index_into_customer_related_search_entities":0.003,"P\\Tests\\Unit\\Search\\SystemSearchEconomicCustomerIndexWiringTest::__pest_evaluable_it_registers_cron_tasks_that_keep_the_e_conomic_search_index_refreshed":0.003,"P\\Tests\\Unit\\Search\\SystemSearchInvalidationHooksTest::__pest_evaluable_it_marks_system_search_cache_dirty_from_generic_db_object_mutation_flows":0.004,"P\\Tests\\Unit\\Search\\SystemSearchInvalidationHooksTest::__pest_evaluable_it_marks_system_search_cache_dirty_from_object_property_mutations":0.003,"P\\Tests\\Unit\\Search\\SystemSearchInvalidationHooksTest::__pest_evaluable_it_marks_system_search_cache_dirty_when_module_config_values_change":0.002,"P\\Tests\\Unit\\Search\\SystemSearchInvalidationHooksTest::__pest_evaluable_it_registers_cron_maintenance_task_for_system_search_cache":0.002,"P\\Tests\\Unit\\Search\\SystemSearchOpenAiIntentParserTest::__pest_evaluable_it_redacts_obvious_sensitive_fragments_before_sending_query_to_intent_parser":0.001,"P\\Tests\\Unit\\Search\\SystemSearchOpenAiIntentParserTest::__pest_evaluable_it_builds_payload_with_redacted_query_and_parses_strict_JSON_output":0,"P\\Tests\\Unit\\Search\\SystemSearchOpenAiIntentParserTest::__pest_evaluable_it_falls_back_safely_when_OpenAI_is_disabled":0,"P\\Tests\\Unit\\Search\\SystemSearchOpenAiIntentParserTest::__pest_evaluable_it_handles_malformed_OpenAI_response_payloads_without_throwing":0,"P\\Tests\\Unit\\Search\\SystemSearchOpenAiIntentParserTest::__pest_evaluable_it_uses_parser_cache_for_identical_query_and_allowed_type_combinations":0,"P\\Tests\\Unit\\Search\\SystemSearchOpenAiIntentParserTest::__pest_evaluable_it_caps_alias_and_hint_payloads_from_OpenAI_and_filters_hints_to_allowed_types":0.006,"P\\Tests\\Unit\\Search\\SystemSearchOpenApiSpecTest::__pest_evaluable_it_documents_system_wide_search_endpoints_in_openapi":0.007,"P\\Tests\\Unit\\Search\\SystemSearchOpenApiSpecTest::__pest_evaluable_it_documents_debug__intent_and_parser_metadata_schema_in_openapi":0.007,"P\\Tests\\Unit\\Search\\SystemSearchOpenApiSpecTest::__pest_evaluable_it_documents_e_conomic_indexed_customer_matching_and_synonym_behavior":0.005,"P\\Tests\\Unit\\Search\\SystemSearchRouteBehaviorTest::__pest_evaluable_it_normalizes_type_lists_from_csv_json_and_arrays":0,"P\\Tests\\Unit\\Search\\SystemSearchRouteBehaviorTest::__pest_evaluable_it_parses_booleans_and_clamps_integers_using_route_defaults":0,"P\\Tests\\Unit\\Search\\SystemSearchRouteBehaviorTest::__pest_evaluable_it_exposes_expected_searchable_entity_types":0,"P\\Tests\\Unit\\Search\\SystemSearchRouteWiringTest::__pest_evaluable_it_registers_system_wide_search_GET_and_POST_endpoints_with_intent_debug_support":0.003,"P\\Tests\\Unit\\Search\\SystemSearchRouteWiringTest::__pest_evaluable_it_registers_superuser_cache_clear_and_rebuild_endpoints_for_system_search":0.003,"P\\Tests\\Unit\\Search\\SystemSearchRouteWiringTest::__pest_evaluable_it_passes_permission_and_own_scope_context_into_system_search_service":0.003,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_does_not_invoke_intent_parser_when_lexical_confidence_is_already_high":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_invokes_parser_on_low_confidence_lexical_results_and_applies_hints_only_boosts":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_never_lets_parser_entity_hints_override_explicit_include_filters":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_returns_fallback_parser_metadata_when_parser_fails_gracefully":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_scopes_query_cache_by_permission_context_to_avoid_cross_user_cache_leakage":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_caps_AI_driven_expanded_terms_to_prevent_query_amplification":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_expands_danish_discount_wording_into_lexical_discount_synonyms":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_invokes_parser_for_intent_driven_natural_language_queries_even_when_lexical_score_is_high":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_uses_association_hints_to_pull_related_customer_records_from_non_customer_matches":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_prefers_newer_records_when_relevance_scores_are_comparable":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_keeps_explicit_identifier_matches_ahead_of_newer_but_weaker_records":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_promotes_invoices_orders_order_bookings_and_customers_in_ranking":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_never_prioritizes_cancelled_bookings_over_active_bookings":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_heavily_demotes_configured_low_priority_entity_types_in_ranking":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_tokenizes_unicode_names_without_stripping_non_ascii_letters":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_does_not_treat_explicit_identifier_queries_as_intent_driven":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_requires_broader_term_coverage_for_multi_word_scoring":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_enriches_object_attachment_results_with_associated_customer_context":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_includes_the_economic_customer_index_in_cache_dependencies_for_customer_scoped_results":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_falls_back_to_invoice_date_ranges_when_invoice_names_are_missing":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_derives_xlvask_customer_numbers_only_from_digits_only_extern_ids":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_replaces_unnamed_user_titles_with_the_customer_context_name":0,"P\\Tests\\Unit\\Search\\SystemSearchServiceIntentFlowTest::__pest_evaluable_it_uses_the_goal_criteria_label_for_department_goal_titles":0,"P\\Tests\\Integration\\Search\\SystemSearchCacheIntegrationTest::__pest_evaluable_it_reuses_parser_cache_entries_for_repeated_natural_language_intent_requests":0.106,"P\\Tests\\Integration\\Search\\SystemSearchCacheIntegrationTest::__pest_evaluable_it_clears_parser_cache_namespace_via_clearAll_to_force_a_fresh_parse":0,"P\\Tests\\Integration\\Search\\SystemSearchCacheIntegrationTest::__pest_evaluable_it_bumps_per_table_cache_versions_when_a_dirty_table_marker_is_registered":0.081,"P\\Tests\\Unit\\Invoicing\\EconomicV2DistributionServiceFallbackTest::__pest_evaluable_it_falls_back_to_system_orders_for_fixed_pricing_when_regular_orders_yield_no_customers":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2DistributionServiceFallbackTest::__pest_evaluable_it_falls_back_to_system_orders_for_wash_subscriptions_when_regular_orders_yield_no_customers":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2CliBackfillCommandTest::__pest_evaluable_it_registers_economic_v2_backfill_command_in_cli_dispatcher":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicV2CliBackfillCommandTest::__pest_evaluable_it_provides_a_backfill_cron_script_entrypoint":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicV2CompareEngineTest::__pest_evaluable_it_returns_exact__match_when_totals_lines_and_departments_are_identical":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2CompareEngineTest::__pest_evaluable_it_returns_total__mismatch_when_only_totals_differ":0.015,"P\\Tests\\Unit\\Invoicing\\EconomicV2CompareEngineTest::__pest_evaluable_it_detects_line_level_mismatches_for_quantity_and_price":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2CompareEngineTest::__pest_evaluable_it_detects_departmental_distribution_mismatches":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2CompareEngineTest::__pest_evaluable_it_returns_missing__target_when_draft_or_booked_target_is_unavailable":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2LineNormalizerTest::__pest_evaluable_it_normalizes_draft_invoice_lines_including_departmental_distributions":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2LineNormalizerTest::__pest_evaluable_it_marks_text_only_zero_value_lines_as_non_billable_and_keeps_deterministic_key":0.008,"P\\Tests\\Unit\\Invoicing\\EconomicV2LineNormalizerTest::__pest_evaluable_it_normalizes_booked_invoices_and_computes_net_total_delta_from_lines":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2LineNormalizerTest::__pest_evaluable_it_contains_internal_normalization_path_with_departmental_metadata_support":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_documents_economic_v2_invoice_paths_in_openapi":0.009,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_documents_v2_historical_distribution_and_pricing_history_paths_in_openapi":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_aligns_legacy_compare_schema_with_runtime_payload_by_removing_stale_required_order__ids":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicV2OpenApiSpecTest::__pest_evaluable_it_defines_new_reusable_v2_schemas_for_normalization_comparison_versioning_and_distribution":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicV2RevenueAndBarredSupportTest::__pest_evaluable_it_supports_barred_filtering_when_listing_e_conomic_customers":0.006,"P\\Tests\\Unit\\Invoicing\\EconomicV2RevenueAndBarredSupportTest::__pest_evaluable_it_implements_a_dedicated_v2_e_conomic_revenue_statistics_service_and_route":0.006,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_registers_all_collected_invoice_economic_v2_routes_with_explicit_permissions":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_registers_version_aware_distribution_and_pricing_history_v2_routes":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_caches_v2_distribution_responses_with_a_configurable_redis_ttl":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_writes_fixed_pricing_versions_from_create_and_delete_flows":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_writes_vehicle_subscription_versions_for_create_update_delete_flows":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_writes_discount_override_versions_from_superuser_discounts_route":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicV2RouteAndVersioningHooksTest::__pest_evaluable_it_keeps_legacy_compare_endpoint_path_for_backward_compatibility":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicV2VersioningServiceStructureTest::__pest_evaluable_it_implements_effective_range_lifecycle_methods_for_all_versioned_entities":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicV2VersioningServiceStructureTest::__pest_evaluable_it_closes_previous_active_interval_before_inserting_a_new_version":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicV2VersioningServiceStructureTest::__pest_evaluable_it_includes_best_effort_backfill_with_provenance_and_confidence_metadata":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicV2VersioningServiceStructureTest::__pest_evaluable_it_anchors_historical_resolution_on_order_created__at_timestamps_in_distribution_service":0.004,"P\\Tests\\Unit\\Invoicing\\InvoicingOrdersCalculationsHardeningTest::__pest_evaluable_it_guards_against_empty_order_ids_in_net_amount_calculation":0.007,"P\\Tests\\Unit\\Invoicing\\InvoicingOrdersCalculationsHardeningTest::__pest_evaluable_it_guards_against_empty_customer_arrays_in_date_range_transaction_fetches":0.003,"P\\Tests\\Unit\\Invoicing\\InvoicingOrdersCalculationsHardeningTest::__pest_evaluable_it_uses_centralized_duplicate_filtering_for_possible_duplicate_detection":0.003,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodRouteCacheHelpersTest::__pest_evaluable_it_uses_sane_ttl_defaults_and_clamps_negative_ttl_to_zero":0,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodRouteCacheHelpersTest::__pest_evaluable_it_builds_deterministic_cache_keys_per_scope_and_date_range":0.007,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodRouteCacheHelpersTest::__pest_evaluable_it_falls_back_to_resolver_directly_when_cache_ttl_is_disabled":0,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodRouteGuardsTest::__pest_evaluable_it_requires_superuser_permission_for_invoicing_period_distribution_all_endpoint":0.017,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodRouteGuardsTest::__pest_evaluable_it_uses_shared_date_range_normalization_across_invoicing_period_endpoints":0.032,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodUtilsTest::__pest_evaluable_it_normalizes_a_valid_invoicing_date_range_to_full_day_timestamps":0,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodUtilsTest::__pest_evaluable_it_rejects_invalid_date_formats":0.012,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodUtilsTest::__pest_evaluable_it_rejects_descending_date_ranges":0,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodUtilsTest::__pest_evaluable_it_finds_duplicate_orders_regardless_of_original_input_order":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2DistributionServiceFallbackTest::__pest_evaluable_it_runs_best_effort_backfill_when_fixed_pricing_version_history_is_empty":0,"P\\Tests\\Unit\\Selfserve\\SelfserveConditionEvaluatorTest::__pest_evaluable_it_evaluates_self_serve_conditions_with_combined_AND_and_OR_semantics":0,"P\\Tests\\Unit\\Selfserve\\SelfserveConditionEvaluatorTest::__pest_evaluable_it_prefers_condition_results_over_question_answers_when_evaluating_task_gates":0,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_self_serve_machine_type__eligibility__summary__and_webhook_endpoints":0.01,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_defines_reusable_self_serve_wash_and_machine_type_schemas":0.007,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_self_serve_machine_types__eligibility__summaries__and_machine_start_webhook_routes":0.014,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_keeps_machine_type_support_wired_into_lanes__tasks__and_conditions_routes":0.018,"P\\Tests\\Unit\\N8n\\N8nRouteHelpersTest::__pest_evaluable_it_normalizes_webhook_methods_and_falls_back_to_POST_for_unsupported_verbs":0,"P\\Tests\\Unit\\N8n\\N8nRouteHelpersTest::__pest_evaluable_it_filters_request_parameters_down_to_the_allowed_n8n_query_keys":0,"P\\Tests\\Unit\\N8n\\N8nRouteWiringTest::__pest_evaluable_it_registers_workflow_lifecycle_endpoints_for_the_n8n_module":0.002,"P\\Tests\\Unit\\N8n\\N8nRouteWiringTest::__pest_evaluable_it_registers_execution_and_webhook_trigger_endpoints_for_the_n8n_module":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_redistributes_booked_department_75_net_amounts_using_fixed_pricing_and_wash_subscription_weights":0.008,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_keeps_classified_booked_department_75_amounts_undistributed_when_no_monthly_basis_exists":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_keeps_unclassified_booked_department_75_lines_undistributed_with_warnings":0,"P\\Tests\\Unit\\Invoicing\\EconomicEndpointUrlEncodingTest::__pest_evaluable_it_encodes_raw_filter_query_values_that_include_timestamps":0,"P\\Tests\\Unit\\Invoicing\\EconomicEndpointUrlEncodingTest::__pest_evaluable_it_avoids_double_encoding_query_values_that_are_already_escaped":0,"P\\Tests\\Unit\\Invoicing\\EconomicV2BookedDepartment75DistributionTest::__pest_evaluable_it_supports_bulk_booked_invoice_line_payloads_with_top_level_department_numbers":0,"P\\Tests\\Unit\\Redis\\RedisAtomicReservationTest::__pest_evaluable_it_claims_a_slot_atomically_with_nx_and_expiration":0,"P\\Tests\\Unit\\Redis\\RedisAtomicReservationTest::__pest_evaluable_it_returns_false_when_the_slot_is_already_claimed_and_clamps_ttl_to_one_second":0,"P\\Tests\\Unit\\Bookings\\OrderBookingRouteIdempotencyGuardTest::__pest_evaluable_it_guards_order_booking_creation_with_redis_backed_idempotency":0.24,"P\\Tests\\Unit\\DynamicImages\\DynamicImagePreRenderCronWiringTest::__pest_evaluable_it_registers_dynamic_image_pre_render_cron_task_and_related_helpers":0.005,"P\\Tests\\Integration\\Database\\DbConnectionTest::__pest_evaluable_it_can_connect_to_a_configured_MySQL_instance_in_integration_mode":0.226,"P\\Tests\\Integration\\Invoicing\\EconomicV2BackfillAndDistributionIntegrationTest::__pest_evaluable_it_runs_best_effort_backfill_repeatedly_without_introducing_duplicate_same_start_rows":0.017,"P\\Tests\\Integration\\Invoicing\\EconomicV2BackfillAndDistributionIntegrationTest::__pest_evaluable_it_resolves_version_aware_distribution_payload_shapes_over_a_real_date_range":0.014,"P\\Tests\\Integration\\Invoicing\\EconomicV2VersioningServiceIntegrationTest::__pest_evaluable_it_creates_closes_and_rotates_fixed_pricing_versions_without_overlap":0.011,"P\\Tests\\Integration\\Invoicing\\EconomicV2VersioningServiceIntegrationTest::__pest_evaluable_it_tracks_vehicle_and_discount_version_lifecycles_with_closure_semantics":0.016,"P\\Tests\\Integration\\Permissions\\LegacyPermissionRedisCacheScriptTest::__pest_evaluable_it_keeps_legacy_route_permission_redis_cache_script_green":0.013,"P\\Tests\\Unit\\Permissions\\AllowOwnOrDepartmentAccessForbiddenTest::__pest_evaluable_it_returns_both_own_and_elevated_permissions_when_both_are_missing":0,"P\\Tests\\Unit\\Permissions\\AllowOwnOrDepartmentAccessForbiddenTest::__pest_evaluable_it_returns_only_elevated_permission_when_own_permission_exists_but_own_context_fails":0,"P\\Tests\\Unit\\Permissions\\AllowOwnOrDepartmentAccessForbiddenTest::__pest_evaluable_it_allows_elevated_permission_path_without_forbidden_and_validates_department_access":0,"P\\Tests\\Unit\\Permissions\\ForbiddenResponseWiringTest::__pest_evaluable_it_routes_and_trait_use_forbidden_responses_for_permission_and_ownership_denials":0.018,"P\\Tests\\Unit\\Permissions\\GroupPermissionSessionInvalidationWiringTest::__pest_evaluable_it_invalidates_related_user_permission_and_auth_session_caches_when_group_permissions_change":0.004,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetDurationTest::__pest_evaluable_it_parses_advanced_target_duration_from_snake__case_fields":0.066,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetDurationTest::__pest_evaluable_it_parses_advanced_target_duration_from_camelCase_aliases":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetDurationTest::__pest_evaluable_it_normalizes_ENTIRE__DURATION_to_ignore_target__duration__every":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetDurationTest::__pest_evaluable_it_defaults_recurring_target__duration__every_to_1_when_omitted":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetDurationTest::__pest_evaluable_it_keeps_strict_legacy_mode_when_target__duration_is_invalid":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetMathTest::__pest_evaluable_it_computes_weekly_target_using_touched_ISO_weeks_for_March_2026":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetMathTest::__pest_evaluable_it_applies_target__duration__every_for_weekly_cadence":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetMathTest::__pest_evaluable_it_prorates_ENTIRE__DURATION_target_by_overlap_days":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetMathTest::__pest_evaluable_it_splits_advanced_target_by_override_weights_per_department":0,"P\\Tests\\Unit\\Goals\\GoalsCriteriaTargetMathTest::__pest_evaluable_it_preserves_legacy_target_behavior_when_target__duration_is_missing":0,"P\\Tests\\Unit\\Goals\\GoalsLegacyRendererScriptsTest::__pest_evaluable_it_keeps_legacy_monthly_renderer_script_green":0.147,"P\\Tests\\Unit\\Goals\\GoalsLegacyRendererScriptsTest::__pest_evaluable_it_keeps_legacy_department_daily_renderer_script_green":0.147,"P\\Tests\\Unit\\Goals\\GoalsOpenApiSpecTest::__pest_evaluable_it_documents_advanced_goals_target_duration_fields_in_openapi":0.007,"P\\Tests\\Unit\\Workfeed\\WorkfeedClientConformanceTest::__pest_evaluable_it_uses_company_scoped_workfeed_endpoints_and_raw_Authorization_header":0.003,"P\\Tests\\Unit\\Workfeed\\WorkfeedClientConformanceTest::__pest_evaluable_it_normalizes_documented_shift_query_parameters":0.001,"P\\Tests\\Unit\\Workfeed\\WorkfeedConfigRouteWiringTest::__pest_evaluable_it_registers_module_config_endpoints_for_workfeed":0.004,"P\\Tests\\Unit\\Workfeed\\WorkfeedRouteHelpersTest::__pest_evaluable_it_filters_request_parameters_down_to_the_allowed_workfeed_query_keys":0,"P\\Tests\\Unit\\Workfeed\\WorkfeedRouteWiringTest::__pest_evaluable_it_registers_employee_and_department_endpoints_for_the_workfeed_module":0.004,"P\\Tests\\Unit\\Workfeed\\WorkfeedRouteWiringTest::__pest_evaluable_it_registers_shift_endpoints_for_the_workfeed_module":0.002,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_calculates_workfeed_employee_hours_for_the_hour_slot_based_on_overlap":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_extracts_department_id_from_supported_workfeed_shift_shapes":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_normalizes_wrapped_workfeed_collections_from_common_response_keys":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineSchemaConformanceTest::__pest_evaluable_it_includes_a_date_key_in_department_weather_timeline_entries":0.003,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineSchemaConformanceTest::__pest_evaluable_it_documents_the_date_key_in_the_openapi_department_weather_timeline_schema":0.005,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineRangeTest::__pest_evaluable_it_builds_a_timeline_range_from_start_of_yesterday_to_end_of_today":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineRangeTest::__pest_evaluable_it_builds_a_timeline_range_relative_to_an_explicit_selected_date_override":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_calculates_workfeed_employee_hours_across_multiple_departments_for_one_hour_slot":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_normalizes_department_id_input_from_scalar_csv_and_nested_array_values":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineRangeTest::__pest_evaluable_it_builds_a_timeline_range_relative_to_explicit_date__from_and_date__to_override":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineRangeTest::__pest_evaluable_it_resolves_forecast_day_count_for_a_given_timeline_range_with_sane_limits":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_resolves_weather_coordinates_from_valid_coordinates_only":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_returns_null_weather_coordinates_when_all_department_locations_are_invalid":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_classifies_weatherapi_no_matching_location_errors_as_location_lookup_failures":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_does_not_classify_non_location_weatherapi_errors_as_location_lookup_failures":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_returns_an_empty_forecast_fallback_when_coordinates_are_unavailable":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_builds_timeline_entries_with_mostly__clear_weather_when_forecast_payload_is_empty":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherFallbackBehaviorTest::__pest_evaluable_it_wires_departments_weather_route_through_the_forecast_fallback_path":0.005,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_uses_sane_ttl_defaults_and_clamps_negative_ttl_to_zero":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_builds_deterministic_cache_keys_for_department_sets_and_timeline_ranges":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_builds_order_insensitive_cache_keys_for_equivalent_department_id_sets":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_falls_back_to_resolver_directly_when_cache_ttl_is_disabled":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTimelineRangeTest::__pest_evaluable_it_marks_non_started_slots_as_unknown_regardless_of_wash_hour_ratio":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherPreloadCronWiringTest::__pest_evaluable_it_registers_and_implements_cron_preloading_for_department_weather_cache":0.007,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_uses_sane_stale_ttl_defaults_and_clamps_stale_ttl_below_fresh_ttl":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_uses_sane_hot_activity_ttl_defaults_and_clamps_to_a_positive_value":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheRuntimeBehaviorTest::__pest_evaluable_it_serves_fresh_cached_payloads_without_invoking_the_resolver":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheRuntimeBehaviorTest::__pest_evaluable_it_serves_stale_cached_payloads_and_enqueues_a_refresh_signal":0.01,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheRuntimeBehaviorTest::__pest_evaluable_it_recomputes_and_rewrites_cache_payloads_when_stale_window_is_exceeded":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheRuntimeBehaviorTest::__pest_evaluable_it_records_hot_keys_order_insensitively_and_applies_preload_target_caps":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_normalizes_batched_wash_count_rows_into_hourly_slot_totals":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_wires_department_weather_route_to_use_batched_wash_aggregation":0.007,"P\\Tests\\Unit\\Orders\\AttachmentsListManyTest::__pest_evaluable_it_groups_attachments_by_object_id_and_preserves_empty_object_groups":0,"P\\Tests\\Unit\\Orders\\AttachmentsListManyTest::__pest_evaluable_it_keeps_listMany_payload_parity_with_list_for_one_object_id":0,"P\\Tests\\Unit\\Orders\\EconomicModuleOrdersBatchHelpersTest::__pest_evaluable_it_ensures_economic_module_rows_in_one_sanitized_batch":0,"P\\Tests\\Unit\\Orders\\EconomicModuleOrdersBatchHelpersTest::__pest_evaluable_it_returns_id_keyed_economic_module_payload_with_null_defaults":0,"P\\Tests\\Unit\\Orders\\OrdersRouteListBatchingWiringTest::__pest_evaluable_it_wires_GET__orders_through_batched_enrichment_and_stripe_snapshot_caching":0.006,"P\\Tests\\Unit\\Orders\\UsersCashierNamesBatchTest::__pest_evaluable_it_returns_cashier_names_from_cache_and_fetches_missing_ones_in_batch":0,"P\\Tests\\Unit\\Orders\\UsersCashierNamesBatchTest::__pest_evaluable_it_returns_deterministic_map_for_duplicate_and_unsorted_cashier_ids":0,"P\\Tests\\Unit\\Orders\\UsersCashierNamesBatchWiringTest::__pest_evaluable_it_implements_batched_cashier_name_lookup_with_cache_first_fallback_behavior":0.006,"P\\Tests\\Unit\\Orders\\UsersCashierNamesBatchWiringTest::__pest_evaluable_it_sanitizes_and_deduplicates_cashier_ids_before_batch_lookup":0.003,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_counts_overtime_minutes_when_a_shift_carries_an_extended_approval_end_timestamp":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_counts_unapproved_overtime_from_a_bounded_shift_updateTime_fallback":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherWorkfeedHoursTest::__pest_evaluable_it_does_not_treat_late_unapproved_administrative_edits_as_overtime":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherCacheHelpersTest::__pest_evaluable_it_changes_weather_timeline_cache_key_when_department_weather_targets_change":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherStatusTargetsTest::__pest_evaluable_it_evaluates_healthy_degraded_and_unhealthy_statuses_from_configured_department_targets":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherStatusTargetsTest::__pest_evaluable_it_returns_unknown_when_configured_targets_are_missing_for_department_status_evaluation":0,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherStatusTargetsTest::__pest_evaluable_it_aggregates_multi_department_slot_statuses_using_worst_severity_and_unknown_fallback_rules":0.046,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTargetsOpenApiSpecTest::__pest_evaluable_it_documents_department_weather_target_endpoints_and_reusable_schemas_in_openapi":0.006,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTargetsOpenApiSpecTest::__pest_evaluable_it_documents_unknown_weather_status_behavior_when_targets_are_missing":0.005,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTargetsRouteWiringTest::__pest_evaluable_it_registers_department_weather_target_routes_with_explicit_read_and_manage_permissions":0.004,"P\\Tests\\Unit\\Workfeed\\DepartmentWeatherTargetsRouteWiringTest::__pest_evaluable_it_persists_department_weather_targets_using_canonical_department_variable_keys":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_machine_relay_status_get_and_set_endpoints":0.004,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_batches_sequential_machine_relay_status_requests_into_a_single_Shelly_get_call":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_respects_the_1_request_per_second_Shelly_gate_for_back_to_back_requests":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_retries_missing_Shelly_status_payloads_until_relay_status_becomes_ready":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_times_out_with_a_clear_Shelly_readiness_error_when_payload_remains_missing":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_uses_direct_set_switch_and_seeds_cache_so_immediate_status_read_does_not_call_Shelly_get":0,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_self_serve_lane_gate_open_endpoint":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_in_progress_self_serve_wash_details_endpoint":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_adds_vehicle_type_product_on_STOP_when_program_selector_is_on__then_turns_off_cleaner_machine_and_selector_relays":0.132,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_skips_vehicle_type_product_add_when_program_selector_is_off_and_only_disables_configured_relays":0.001,"P\\Tests\\Unit\\Selfserve\\DepartmentSelfServeEnabledRelaySyncWiringTest::__pest_evaluable_it_syncs_lane_relay_states_when_department_self_serve_enabled_flag_changes":0.005,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_supports_hard_relay_set_even_when_lane_status_is_CLOSED":0,"P\\Tests\\Unit\\Selfserve\\SelfserveStartCleanerRelayWiringTest::__pest_evaluable_it_enables_cleaner_relay_on_wash_start_command_and_webhook_flow":0.012,"P\\Tests\\Unit\\Selfserve\\SelfserveWashCompletionRelayWiringTest::__pest_evaluable_it_forces_machine_relay_off_when_a_self_serve_wash_session_is_completed":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePortControllerTest::__pest_evaluable_it_opens_exit_port_by_switching_relay__in__id_on":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePortControllerTest::__pest_evaluable_it_opens_entrance_port_by_switching_relay__out__id_on":0,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_in_progress_self_serve_wash_start_and_machine_relay_fields":0.007,"P\\Tests\\Unit\\Selfserve\\SelfserveConditionEvaluatorTest::__pest_evaluable_it_evaluates_typed_task_gates_with_strict_semantics":0,"P\\Tests\\Unit\\Selfserve\\SelfserveConfigVersioningTest::__pest_evaluable_it_validates_typed_task_gates_for_known_references":0,"P\\Tests\\Unit\\Selfserve\\SelfserveConfigVersioningTest::__pest_evaluable_it_fails_validation_when_typed_task_gates_reference_unknown_entities":0,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_self_serve_config_draft_publish_rollback_lifecycle_endpoints":0.005,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_keeps_legacy_self_serve_CRUD_routes_syncing_canonical_drafts":0.013,"P\\Tests\\Unit\\Selfserve\\SelfserveStartCleanerRelayWiringTest::__pest_evaluable_it_keeps_cleaner_relay_enable_wired_into_machine_relay_start_paths":0.007,"P\\Tests\\Unit\\Selfserve\\SelfserveConfigVersioningTest::__pest_evaluable_it_stores_config_json_with_apostrophes_without_violating_json_constraints":0.277,"P\\Tests\\Unit\\Selfserve\\SelfserveConfigVersioningTest::__pest_evaluable_it_encodes_config_json_payloads_with_apostrophes_before_persistence":0,"P\\Tests\\Unit\\Selfserve\\SelfserveWashCompletionRelayWiringTest::__pest_evaluable_it_forces_machine_and_cleaner_relays_off_when_a_self_serve_wash_session_is_completed":0.005,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_vehicle_type_override_into_self_serve_preview_and_synchronization_routes":0.006,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_uses_local_demo_responses_and_skips_Shelly_cloud_calls_for_demo_relay_ids":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_returns_default_OFF_status_for_demo_relay_ids_without_Shelly_lookups":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_excludes_demo_relay_ids_from_Shelly_status_batch_payloads":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePortControllerTest::__pest_evaluable_it_keeps_demo_relay_gate_open_queued_but_skips_Shelly_switch_calls":0,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_allowed_services_route_through_machine_relay_visibility_sync":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_enables_MACHINE_relay_when_MACHINE_is_visible_in_allowed_services":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_turns_MACHINE_relay_off_immediately_when_MACHINE_is_not_visible":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_does_not_enable_MACHINE_relay_when_allowEnable_is_false_even_if_MACHINE_is_visible":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_is_a_safe_no_op_when_MACHINE_relay_is_not_configured":0,"P\\Tests\\Unit\\Selfserve\\SelfserveRelayVisibilitySyncWiringTest::__pest_evaluable_it_synchronizes_machine_relay_from_visible_services_during_session_synchronization":0.006,"P\\Tests\\Unit\\Selfserve\\SelfserveRelayVisibilitySyncWiringTest::__pest_evaluable_it_adds_explicit_relay_disable_session_helper_for_visibility_driven_OFF_transitions":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_self_serve_property_gate_command_permissions":0.008,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneCommandEnumTest::__pest_evaluable_it_parses_property_gate_lane_commands":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePropertyGateCommandTest::__pest_evaluable_it_opens_entrance_gate_for_OPEN__PROPERTY__ACCESS__GATE_command":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePropertyGateCommandTest::__pest_evaluable_it_opens_exit_gate_for_OPEN__PROPERTY__EXIT__GATE_command":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePropertyGateCommandTest::__pest_evaluable_it_blocks_property_gate_commands_when_department_self_serve_is_disabled":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePropertyGateCommandTest::__pest_evaluable_it_throws_a_clear_error_when_entrance_gate_is_missing_for_property_access_command":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePropertyGateCommandTest::__pest_evaluable_it_wraps_low_level_gate_errors_for_property_access_command_failures":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveWashFlowMachineAllowedWiringTest::__pest_evaluable_it_reads_machine_allowed_state_from_session_summary_payload_safely":0.005,"P\\Tests\\Unit\\Selfserve\\DbObjectPaginationLegacyColumnCompatibilityTest::__pest_evaluable_it_guards_pagination_against_searchable_fields_missing_from_legacy_schemas":0.004,"P\\Tests\\Unit\\Selfserve\\SelfserveSchemaBootstrapCompatibilityTest::__pest_evaluable_it_adds_dynamic__images__vehicle__type_column_for_legacy_selfserve_wash_session_task_schemas":0.004,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneInvoiceIncludedMinutesTest::__pest_evaluable_it_computes_billable_minutes_when_elapsed_minutes_exceed_included_minutes":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneInvoiceIncludedMinutesTest::__pest_evaluable_it_computes_zero_billable_minutes_when_elapsed_minutes_equal_included_minutes":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneInvoiceIncludedMinutesTest::__pest_evaluable_it_computes_zero_billable_minutes_when_included_minutes_exceed_elapsed_minutes":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneInvoiceIncludedMinutesTest::__pest_evaluable_it_handles_zero_and_low_elapsed_wash_time_edge_cases":0,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_self_serve_machine_wash_included_minutes_in_config_schemas":0.004,"P\\Tests\\Unit\\Selfserve\\SelfserveRouteWiringTest::__pest_evaluable_it_wires_machine_wash_included_minutes_into_self_serve_module_config":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_blocks_explicit_relay_writes_when_department_self_serve_is_disabled":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_returns_disabled_no_op_from_machine_relay_visibility_sync_when_department_self_serve_is_disabled":0,"P\\Tests\\Unit\\Selfserve\\SelfserveOpenApiSpecTest::__pest_evaluable_it_documents_property_gate_lane_commands_and_sanitized_gate_failure_responses":0.007,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_fast_when_workspace_id_is_missing_for_gate_calls":0.019,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_fast_when_channel_id_is_missing_for_gate_calls":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_with_terminal_status_details_when_call_never_reaches_accepted_state":0.072,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_hangs_up_when_call_reaches_accepted_state":0.013,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_hangs_up_immediately_when_status_transitions_to_accepted":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_maps_legacy_timeout_option_to_documented_ringTimeout_payload_field":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_clamps_derived_ringTimeout_to_Bird_documented_max_when_gate_timeout_is_high":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_passes_documented_hangup_cause_when_provided":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_drops_unsupported_hangup_cause_values_from_request_payload":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_fast_when_workspace_id_is_missing_for_gate_flash_calls":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_creates_gate_flash_call_with_documented_ringTimeout_payload":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_polls_flash_call_and_succeeds_once_accepted":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_fails_flash_gate_flow_when_terminal_failure_status_is_returned":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_does_not_fallback_to_regular_gate_call_when_flash_succeeds":0,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_falls_back_to_regular_gate_call_when_flash_fails":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePropertyGateCommandTest::__pest_evaluable_it_wraps_low_level_gate_errors_for_property_exit_command_failures":0.002,"P\\Tests\\Unit\\Bird\\BirdGateCallFlowTest::__pest_evaluable_it_falls_back_to_regular_gate_call_when_flash_caller_id_is_not_confirmed":0,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_maps_voice_call_facade_methods_to_documented_endpoints_and_methods":0.039,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_maps_recordings_insights_log_and_flash_methods_to_documented_resources":0,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_encodes_path_segments_before_building_outbound_endpoints":0,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_serializes_query_parameters_for_GET_transport_requests":0,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_maps_4xx_and_5xx_transport_failures_into_informative_exceptions":0,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_returns_raw_payload_for_malformed_JSON_responses_and_null_for_empty_responses":0.007,"P\\Tests\\Unit\\Bird\\BirdOpenApiSpecTest::__pest_evaluable_it_documents_all_Bird_voice_call_parity_endpoints_including_gather_recordings_insights_and_log":0.007,"P\\Tests\\Unit\\Bird\\BirdOpenApiSpecTest::__pest_evaluable_it_documents_flash_hangup_endpoint_and_marks_end_alias_as_deprecated":0.005,"P\\Tests\\Unit\\Bird\\BirdOpenApiSpecTest::__pest_evaluable_it_defines_request_and_response_schemas_for_Bird_call_command_recording_insight_log_and_flash_payloads":0.004,"P\\Tests\\Unit\\Bird\\BirdRequestValidationTest::__pest_evaluable_it_accepts_valid_payloads_for_create_and_nested_call_flow_commands":0,"P\\Tests\\Unit\\Bird\\BirdRequestValidationTest::__pest_evaluable_it_rejects_unknown_fields_and_invalid_enum_values_in_strict_schemas":0.014,"P\\Tests\\Unit\\Bird\\BirdRequestValidationTest::__pest_evaluable_it_supports_CSV_normalization_in_schema_validation_for_log_filters":0,"P\\Tests\\Unit\\Bird\\BirdRequestValidationTest::__pest_evaluable_it_validates_flash_hangup_payloads_for_both_supported_request_shapes":0,"P\\Tests\\Unit\\Bird\\BirdRequestValidationTest::__pest_evaluable_it_fails_before_forward_step_when_strict_validation_fails_and_forwards_when_valid":0,"P\\Tests\\Unit\\Bird\\BirdRouteWiringTest::__pest_evaluable_it_registers_full_Bird_voice_call_route_surface_with_strict_validation_and_permissions":0.005,"P\\Tests\\Unit\\Bird\\BirdRouteWiringTest::__pest_evaluable_it_registers_flash_hangup_endpoint_and_keeps_end_alias_wired_as_compatibility_path":0.004,"P\\Tests\\Unit\\Bird\\BirdRouteWiringTest::__pest_evaluable_it_keeps_Bird_number_and_webhook_routes_intact":0.006,"P\\Tests\\Unit\\Bird\\BirdModuleClientMappingTest::__pest_evaluable_it_handles_malformed_and_empty_response_bodies_without_throwing_transport_errors":0.005,"P\\Tests\\Unit\\Selfserve\\DepartmentGatesRelaysRouteWiringTest::__pest_evaluable_it_validates_department_gate_config_on_both_create_and_update_routes":0.004,"P\\Tests\\Unit\\Selfserve\\SelfserveSchemaBootstrapCompatibilityTest::__pest_evaluable_it_adds_wash__started__at_column_for_legacy_selfserve_wash_session_schemas":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveWashStartedAtWiringTest::__pest_evaluable_it_stores_wash__started__at_in_self_serve_wash_sessions_when_machine_start_is_triggered":0.003,"P\\Tests\\Unit\\Selfserve\\SelfserveWashStartedAtWiringTest::__pest_evaluable_it_passes_lane_wash_start_time_into_the_session_machine_start_marker":0.004,"P\\Tests\\Unit\\Selfserve\\SelfserveWashStartedAtWiringTest::__pest_evaluable_it_resolves_in_progress_wash__started__at_from_session_with_compatibility_fallbacks":0.003,"P\\Tests\\Unit\\Bird\\BirdPayloadClassesTest::__pest_evaluable_it_normalizes_say_payload_and_applies_hangup_default":0.019,"P\\Tests\\Unit\\Bird\\BirdPayloadClassesTest::__pest_evaluable_it_normalizes_log_query_csv_and_integer_fields":0,"P\\Tests\\Unit\\Bird\\BirdPayloadClassesTest::__pest_evaluable_it_normalizes_nested_create_call_payload_sections_and_drops_unknown_keys":0,"P\\Tests\\Unit\\Bird\\BirdPayloadClassesTest::__pest_evaluable_it_keeps_gather_nested_say_payload_unchanged_when_hangup_is_omitted":0,"P\\Tests\\Unit\\Bird\\BirdPayloadClassesTest::__pest_evaluable_it_supports_no_body_and_flash_hangup_payload_normalization":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_respects_the_2_second_Shelly_gate_for_back_to_back_requests":0.023,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_enforces_a_2_second_gap_between_sequential_switch_requests_used_by_wash_start_and_stop_flows":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_keeps_back_to_back_get_switch_requests_ordered_through_the_relay_controller":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneRelayShellyBatchingTest::__pest_evaluable_it_executes_sequential_switch_requests_used_by_wash_start_and_stop_flows":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLanePortControllerTest::__pest_evaluable_it_does_not_print_Shelly_switch_responses_to_output":0,"P\\Tests\\Unit\\Selfserve\\ShellyGlobalRateLimitWiringTest::__pest_evaluable_it_enforces_a_global_2_second_Shelly_gate_in_sendPostRequest":0.004,"P\\Tests\\Unit\\Selfserve\\ShellyGlobalRateLimitWiringTest::__pest_evaluable_it_uses_Redis_NX_PX_semantics_for_cross_request_Shelly_rate_limiting":0.003,"P\\Tests\\Unit\\Selfserve\\ShellyGlobalRateLimitBehaviorTest::__pest_evaluable_it_enforces_the_2_second_Shelly_gate_across_separate_request_contexts":0,"P\\Tests\\Unit\\Selfserve\\ShellyGlobalRateLimitBehaviorTest::__pest_evaluable_it_does_not_delay_when_the_Shelly_gate_is_already_expired":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_adds_vehicle_type_product_on_STOP_when_program_selector_relay_is_online__then_turns_off_cleaner_and_machine_relays":0.201,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_skips_vehicle_type_product_add_when_program_selector_relay_is_offline_and_only_disables_configured_relays":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneStopFlowTest::__pest_evaluable_it_bills_primary_product_when_selector_relay_is_online_even_if_relay_output_is_off":0.004,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneInvoiceModeBillingTest::__pest_evaluable_it_bills_manual_self_serve_stop_using_full_elapsed_minutes_without_included_minute_reduction":0,"P\\Tests\\Unit\\Selfserve\\SelfserveLaneInvoiceModeBillingTest::__pest_evaluable_it_keeps_included_minute_reduction_for_automatic_mode":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_sends_Slack_on_first_dtmf_input_captured_within_the_300_second_window":0.005,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_does_not_send_Slack_when_dtmf_input_repeats_without_change":0.008,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_sends_Slack_again_when_dtmf_input_changes_within_the_window":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_keeps_gathering_and_does_not_hang_up_before_300_seconds_have_elapsed":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_announces_timeout_waits_10_seconds_and_sends_hangup_once_at_or_after_300_seconds":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_keeps_polling_until_terminal_status_and_only_then_finalizes":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_finalizes_immediately_when_webhook_payload_is_already_terminal_before_timeout_logic":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_does_not_duplicate_timeout_or_hangup_actions_across_retries_and_lock_contention":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_keeps_state_when_polling_fails_so_completion_is_not_falsely_reported":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_sends_Slack_when_dtmf_input_repeats_without_change":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_answers_immediately_once_and_does_not_re_answer_on_subsequent_webhook_retries":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_configures_gather_with_retry_loop_semantics_until_input_is_entered":0.004,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_sends_Slack_when_gather_result_provides_keys_field_instead_of_dtmf":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_sends_Slack_when_gather_conditions_variable_keys_carries_the_entered_value":0,"P\\Tests\\Unit\\Bird\\BirdVoiceWebhooksRouteCallLifecycleTest::__pest_evaluable_it_configures_gather_to_check_every_2_seconds_with_retry_loop_semantics_until_input_is_entered":0,"P\\Tests\\Unit\\Users\\UsersAutomaticGetTargetUserFromRequestTest::__pest_evaluable_it_ignores_non_numeric_customer__number_values_from_request":0.042,"P\\Tests\\Unit\\Users\\UsersAutomaticGetTargetUserFromRequestTest::__pest_evaluable_it_casts_numeric_customer__number_strings_to_int_before_lookup":0,"P\\Tests\\Unit\\Users\\UsersAutomaticGetTargetUserFromRequestTest::__pest_evaluable_it_prefers_user__id_when_both_user__id_and_customer__number_are_valid":0,"P\\Tests\\Unit\\Users\\UsersAutomaticGetTargetUserFromRequestTest::__pest_evaluable_it_falls_back_to_customer__number_when_user__id_is_invalid":0,"P\\Tests\\Unit\\Users\\UsersAutomaticGetTargetUserFromRequestTest::__pest_evaluable_it_normalizes_request_identifiers_before_user_lookups":0.004,"P\\Tests\\Unit\\Users\\UsersAutomaticGetTargetUserFromRequestTest::__pest_evaluable_it_rejects_non_positive_and_non_digit_request_values":0.003,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_uses_pagination_results_when_present":0.006,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_falls_back_to_collection_count_when_pagination_metadata_is_missing":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_returns_zero_when_neither_pagination_nor_collection_is_available":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_counts_object_based_collections_when_pagination_metadata_is_missing":0,"P\\Tests\\Unit\\Invoicing\\EconomicPaymentTermsRouteCollectionExtractionTest::__pest_evaluable_it_uses_collection_when_present":0,"P\\Tests\\Unit\\Invoicing\\EconomicPaymentTermsRouteCollectionExtractionTest::__pest_evaluable_it_falls_back_to_paymentTerms_when_collection_is_missing":0,"P\\Tests\\Unit\\Invoicing\\EconomicPaymentTermsRouteCollectionExtractionTest::__pest_evaluable_it_supports_top_level_array_payloads":0,"P\\Tests\\Unit\\Invoicing\\EconomicPaymentTermsRouteCollectionExtractionTest::__pest_evaluable_it_returns_an_empty_array_when_no_known_collection_shape_exists":0,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_uses_grant__2_in_economic__endpoint__t_when_explicitly_requested_and_configured":0.009,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_falls_back_to_grant__1_in_economic__endpoint__t_when_grant__2_is_missing":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_throws_for_missing_required_primary_grant_in_economic__endpoint__t":0.014,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_throws_for_missing_app_secret_in_economic__endpoint__t":0,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_uses_grant__2_in_legacy_economic__m_when_available":0,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_falls_back_to_grant__1_in_legacy_economic__m_when_grant__2_is_missing":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_throws_for_missing_required_primary_grant_in_legacy_economic__m":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_throws_for_missing_app_secret_in_legacy_economic__m":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_uses_unfiltered_total_when_no_customer_filters_are_active":0.004,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_uses_filtered_total_when_search_is_active":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_uses_filtered_total_when_barred_filter_is_active":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_treats__null__search_and_barred_values_as_no_filter":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRoutePaginationFallbackTest::__pest_evaluable_it_falls_back_to_filtered_total_when_unfiltered_total_is_missing":0,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRouteEconomicFailureHandlingTest::__pest_evaluable_it_maps_e_conomic_integration_failures_in_customer_listing_to_explicit_502_responses":0.014,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRouteEconomicFailureHandlingTest::__pest_evaluable_it_logs_LIST__CUSTOMERS_success_only_after_pagination_and_customer_mapping_are_completed":0.009,"P\\Tests\\Unit\\Invoicing\\CustomerSearchRouteEconomicFailureHandlingTest::__pest_evaluable_it_guards_against_malformed_customer_list_payloads_before_calling_paginate":0.011,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_throws_deterministic_upstream_exceptions_for_non_2xx_endpoint_trait_responses":0,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_throws_deterministic_upstream_exceptions_for_non_2xx_legacy_economic__m_responses":0,"P\\Tests\\Unit\\Invoicing\\EconomicAuthTokenFallbackTest::__pest_evaluable_it_returns_raw_payload_unchanged_on_successful_HTTP_statuses":0,"P\\Tests\\Unit\\Invoicing\\EconomicCustomersListResponseValidationTest::__pest_evaluable_it_accepts_valid_list_payloads_that_include_collection_and_pagination_results":0.02,"P\\Tests\\Unit\\Invoicing\\EconomicCustomersListResponseValidationTest::__pest_evaluable_it_throws_when_pagination_is_missing_from_the_response_payload":0,"P\\Tests\\Unit\\Invoicing\\EconomicCustomersListResponseValidationTest::__pest_evaluable_it_throws_when_upstream_responds_with_an_error_shaped_payload":0,"P\\Tests\\Unit\\Router\\RouterThrowableHandlingTest::__pest_evaluable_it_catches_throwables_during_auto_route_loading_and_maps_them_to_internal_server_errors":0.003,"P\\Tests\\Unit\\Users\\EconomicCustomerZeroHandlingTest::__pest_evaluable_it_does_not_request_e_conomic_customer_data_when_customer_number_is_zero":0.101,"P\\Tests\\Unit\\Users\\EconomicCustomerZeroHandlingTest::__pest_evaluable_it_short_circuits_e_conomic_customer_lookup_for_non_positive_customer_numbers":0.005,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueRouteRegistrationTest::__pest_evaluable_it_registers_queued_economic_invoice_endpoints_in_economicInvoiceRoute":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueRouteRegistrationTest::__pest_evaluable_it_registers_collected_invoice_queue_endpoints_in_orderInvoicesRoute":0.005,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueSchemaBootstrapTest::__pest_evaluable_it_defines_economic_transfer_queue_jobs_schema_bootstrap_table_and_tracking_columns":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueSchemaBootstrapTest::__pest_evaluable_it_provides_queue_processor_class_constants_and_processing_entrypoint":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueCronIntegrationTest::__pest_evaluable_it_registers_economic_transfer_queue_cron_task_and_handler":0.005,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueOpenApiSpecTest::__pest_evaluable_it_documents_economic_transfer_queue_paths_in_openapi":0.01,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueOpenApiSpecTest::__pest_evaluable_it_documents_economic_transfer_queue_schemas_in_openapi":0.007,"P\\Tests\\Unit\\Invoicing\\EconomicTransferOrderItemSkipTest::__pest_evaluable_it_skips_order_items_with_zero_quantity_for_e_conomic_export":0.008,"P\\Tests\\Unit\\Invoicing\\EconomicTransferOrderItemSkipTest::__pest_evaluable_it_skips_order_items_with_zero_price_for_e_conomic_export":0,"P\\Tests\\Unit\\Invoicing\\EconomicTransferOrderItemSkipTest::__pest_evaluable_it_keeps_positive_quantity_and_price_items_billable_for_e_conomic_export":0,"P\\Tests\\Unit\\Invoicing\\EconomicTransferOrderItemSkipTest::__pest_evaluable_it_wires_zero_cost_and_zero_quantity_skip_guard_into_transfer_line_builder":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicInvoiceDraftZeroItemSkipWiringTest::__pest_evaluable_it_skips_zero_cost_and_zero_quantity_items_in_legacy_economic_draft_helper":0.008,"P\\Tests\\Unit\\Invoicing\\EconomicTransferOrderItemSkipTest::__pest_evaluable_it_skips_malformed_order_item_payloads_for_e_conomic_export_safety":0,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueHardeningTest::__pest_evaluable_it_hardens_transfer_queue_with_type_validation_retry_caps_and_stale_lock_recovery":0.004,"P\\Tests\\Unit\\Router\\AutoloadRedisCacheValidationTest::__pest_evaluable_it_validates_cached_autoload_paths_before_returning_and_clears_stale_cache_entries":0.003,"P\\Tests\\Unit\\Orders\\OrdersRegistrationDateRangeQueryTest::__pest_evaluable_it_applies_created__at_bounds_directly_in_SQL_when_filtering_orders_by_registration_number":0,"P\\Tests\\Unit\\Orders\\OrdersRegistrationDateRangeQueryTest::__pest_evaluable_it_rejects_an_inverted_date_range_for_registration_lookups":0,"P\\Tests\\Unit\\Orders\\OrdersRegistrationDateRangeQueryTest::__pest_evaluable_it_returns_early_when_registration_number_is_blank":0,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueCronIntegrationTest::__pest_evaluable_it_wires_queue_worker_class_loading_for_CLI_queue_action":0.003,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_guards_all_economic_invoice_queue_endpoints_before_constructing_queue_service":0.012,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_guards_all_collected_invoice_queue_endpoints_before_constructing_queue_service":0.011,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_uses_a_consistent_unavailable_service_contract_for_missing_queue_dependencies":0.009,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_gracefully_handles_unavailable_queue_dependencies_on_collected_invoice_queue_endpoints":0.005,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceEconomicQueueResponseContractTest::__pest_evaluable_it_returns_the_queue_job_id_in_POST__collected_invoices_economic_enqueue_response":0.204,"P\\Tests\\Unit\\Invoicing\\UserCollectedInvoiceUpdateRouteValidationTest::__pest_evaluable_it_requires_id_and_at_least_one_mutable_field_for_PUT__collected_invoices_in_user_route":0.004,"P\\Tests\\Unit\\Invoicing\\UserCollectedInvoiceUpdateRouteValidationTest::__pest_evaluable_it_supports_independent_po__number_and_closed__at_updates_for_PUT__collected_invoices_in_user_route":0.175,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceEconomicQueueResponseContractTest::__pest_evaluable_it_returns_queued_contract_for_POST__collected_invoices_stripe_book":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicDraftQueueOnlyRouteBehaviorTest::__pest_evaluable_it_keeps_order_draft_export_route_queue_only":0.006,"P\\Tests\\Unit\\Invoicing\\EconomicDraftQueueOnlyRouteBehaviorTest::__pest_evaluable_it_keeps_collected_invoice_draft_producing_routes_queue_only_in_orderInvoicesRoute":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_guards_collected_invoice_and_stripe_draft_producing_endpoints_before_constructing_queue_service":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueOpenApiSpecTest::__pest_evaluable_it_documents_unavailable_queue_dependency_responses_for_async_economic_transfer_endpoints":0.007,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueuePayloadValidationTest::__pest_evaluable_it_normalizes_order_payloads_to_strict_positive_integer_ids":0,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueuePayloadValidationTest::__pest_evaluable_it_normalizes_collected_invoice_payload_booleans_to_strict_bool_values":0.007,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueuePayloadValidationTest::__pest_evaluable_it_rejects_invalid_transfer_payloads_before_enqueue_write_attempts":0.036,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_processes_queued_transfer_jobs_to_completion":0.284,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_deduplicates_active_jobs_per_transfer_target":0.012,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_rejects_invalid_payloads_without_inserting_queue_rows":0.011,"P\\Tests\\Integration\\Invoicing\\EconomicTransferQueueIntegrationTest::__pest_evaluable_it_supports_fail_retry_and_reprocess_lifecycle_transitions":0.006,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_keeps_queue_guards_on_economic_invoice_queue_status_and_retry_routes":0.012,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_supports_synchronous_fallback_branches_before_queue_enqueue_on_economic_invoice_export_routes":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_keeps_queue_status_endpoints_guarded_while_collected_invoice_export_routes_support_synchronous_fallback":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueAvailabilityGuardTest::__pest_evaluable_it_uses_a_consistent_unavailable_service_contract_for_queue_status_lifecycle_endpoints":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicDraftQueueOnlyRouteBehaviorTest::__pest_evaluable_it_supports_synchronous_fallback_and_queued_processing_for_order_draft_export_route":0.005,"P\\Tests\\Unit\\Invoicing\\EconomicDraftQueueOnlyRouteBehaviorTest::__pest_evaluable_it_supports_synchronous_fallback_and_queued_processing_for_collected_invoice_draft_producing_routes":0.004,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceEconomicQueueResponseContractTest::__pest_evaluable_it_documents_both_synchronous_fallback_and_queued_response_contracts_for_POST__collected_invoices_economic":0.236,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceEconomicQueueResponseContractTest::__pest_evaluable_it_documents_both_synchronous_fallback_and_queued_response_contracts_for_POST__collected_invoices_stripe_book":0.004,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueOpenApiSpecTest::__pest_evaluable_it_documents_200_fallback_plus_202_queue_contracts_for_export_endpoints_and_keeps_503_on_queue_lifecycle_endpoints":0.009,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueRouteHardeningTest::__pest_evaluable_it_returns_pagination_metadata_and_strict_status_handling_for_collected_invoice_queue_list":0.014,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueRouteHardeningTest::__pest_evaluable_it_enforces_retry_constraints_for_collected_invoice_queue_jobs_before_retry_execution":0.005,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueOpenApiSpecTest::__pest_evaluable_it_documents_collected_queue_list_metadata_and_strict_status_filter_enum_in_openapi":0,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_builds_a_completed_collected_invoice_queue_summary_from_payload_and_result":0.195,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_prefers_queue_error_messages_for_failed_jobs_and_keeps_null_safe_outcome_fields":0.003,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_uses_deterministic_status_message_fallback_when_no_explicit_message_exists":0.003,"P\\Tests\\Unit\\Invoicing\\CollectedInvoiceQueueDetailsSummaryBuilderTest::__pest_evaluable_it_handles_object_payload_result_values_and_malformed_fields_without_throwing":0.002,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueWorkerTickRouteTest::__pest_evaluable_it_ticks_collected_invoice_transfer_queue_from_queue_list_and_status_routes":0.008,"P\\Tests\\Unit\\Invoicing\\EconomicTransferQueueWorkerTickRouteTest::__pest_evaluable_it_ticks_order_transfer_queue_from_draft_and_invoice_status_routes":0.009,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodQueueOverlayTest::__pest_evaluable_it_marks_queued_transactions_and_clears_requires__action_when_all_actionable_work_is_already_queued":0.826,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodQueueOverlayTest::__pest_evaluable_it_keeps_requires__action_true_when_a_customer_still_has_unqueued_actionable_transactions":0.045,"P\\Tests\\Unit\\Invoicing\\InvoicingPeriodQueueOverlayTest::__pest_evaluable_it_blocks_fixed_pricing_or_subscription_customers_with_no_transactions_when_a_relevant_queue_job_exists":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerCommandQueueTest::__pest_evaluable_it_queues_admin_commands__exposes_agent_poll_result_handlers__and_keeps_heartbeats_from_mutating_discovery_state":0.003,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerCommandQueueTest::__pest_evaluable_it_defines_the_dispatchable_gateway_guard_on_the_loaded_manager_class":0.009,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_effective_gateway_status_from_heartbeat_freshness with data set \"dataset \"recent heartbeat stays online\"\"":0.008,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_effective_gateway_status_from_heartbeat_freshness with data set \"dataset \"late heartbeat degrades after one minute\"\"":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_effective_gateway_status_from_heartbeat_freshness with data set \"dataset \"stale heartbeat goes offline after five minutes\"\"":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_effective_gateway_status_from_heartbeat_freshness with data set \"dataset \"explicit offline reports stay offline even when heartbeat is fresh\"\"":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_effective_gateway_status_from_heartbeat_freshness with data set \"dataset \"missing heartbeat is treated as offline\"\"":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_marks_ready_discovery_as_stale_when_the_gateway_heartbeat_has_expired":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayRouteWiringTest::__pest_evaluable_it_registers_the_edge_gateway_management_REST_endpoints":0.021,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayRouteWiringTest::__pest_evaluable_it_registers_public_installer__claim__heartbeat__command_polling__and_internal_broker_auth_endpoints":0.004,"P\\Tests\\Unit\\Selfserve\\EdgeBrokerClientConfigTest::__pest_evaluable_it_uses_the_same_default_broker_url_and_shared_secret_fallback_as_the_docker_stack":0.267,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_builds_install_script_urls_with_forwarded_https_scheme_when_proxied":0.336,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_appends_forwarded_ports_when_the_forwarded_host_omits_them":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_infers_https_for_the_staging_api_host_when_only_the_https_port_is_present":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_prefers_EDGE__PUBLIC__API__URL_when_explicitly_configured":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_normalizes_public_broker_overrides_to_https_and_browser_shell_urls_to_wss":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_keeps_localhost_broker_overrides_on_plain_http_and_ws_for_local_development":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_includes_node_pty_build_prerequisites_in_the_generated_install_script":0.007,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_prefers_EDGE__PUBLIC__API__URL_when_explicitly_configured_and_derives_the_broker_from_the_api_origin":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_normalizes_public_broker_overrides_to_https__strips_paths__and_browser_shell_urls_to_wss":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_merges_incoming_heartbeat_metadata_with_existing_gateway_metadata":0.003,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayRouteWiringTest::__pest_evaluable_it_registers_the_v2_operator_facing_edge_gateway_routes":0.006,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayRouteWiringTest::__pest_evaluable_it_registers_PHP_edge_agent_routes_for_operations_and_legacy_relay_command_polling":0.003,"P\\Tests\\Unit\\Selfserve\\EdgeGatewaySchemaBootstrapTest::__pest_evaluable_it_defines_the_v2_edge_gateway_schema_bootstrap_tables":0.1,"P\\Tests\\Unit\\Selfserve\\EdgeGatewaySchemaBootstrapTest::__pest_evaluable_it_stores_operation_metadata_and_event_timelines_for_management_workflows":0.002,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_builds_the_installer_around_the_PHP_agent_artifacts_and_management_polling_config":0.011,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_exposes_update_payload__credential_rotation__and_operation_endpoints_without_shell_transport_wiring":0.015,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerCommandQueueTest::__pest_evaluable_it_keeps_relay_dispatch_and_discovery_queueing_on_the_edge_gateway_manager":0.009,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerCommandQueueTest::__pest_evaluable_it_loads_relay_command_helpers_on_the_manager_and_gateway_operations_on_the_dedicated_service":0.015,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_relay_fallback_and_transport_health_details_without_shell_or_update_runtime_state":0.015,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_resolves_edge_agent_artifacts_from_the_source_tree_layout":0.445,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_includes_the_container_mounted_artifact_directory_as_a_candidate":0.021,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_reads_install_artifacts_through_the_shared_locator":0.006,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_builds_update_payloads_with_checksums_from_resolved_artifact_paths":0.017,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_resolves_edge_agent_artifacts_from_a_supported_runtime_layout":0.014,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_builds_install_script_urls_with_the_PHP_agent_artifacts_and_forwarded_https_scheme":0.001,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_explains_the_legacy_dist_mount_mismatch_when_php_artifacts_are_unavailable":0.001,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_prioritizes_mounted_and_baked_in_artifact_directories_before_repo_fallbacks":0.022,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_falls_back_to_baked_in_artifacts_when_the_mount_path_is_absent":0.002,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_prioritizes_router_resources_before_mounted_and_baked_in_artifact_directories":0.024,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_lists_orders_for_an_admin_scoped_user_and_limits_the_results_to_the_permitted_departments":1.109,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_lists_only_the_targeted_customer_orders_for_subuser_sessions":2.935,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_returns_the_current_auth_and_permission_failures_when_order_listing_is_not_allowed":1.375,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_creates_orders_through_the_real_endpoint":0.982,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_order_creation_requests":3.423,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_updates_orders_through_the_primary_endpoint":1.13,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_reassigns_invoice_collections_when_changing_an_order_across_the_draft_customer_boundary":1.615,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_supports_legacy_field_value_metadata_updates_through_the_primary_endpoint":5.592,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_updates_through_the_primary_order_endpoint":1.29,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_updates_orders_through_the_legacy_alias_endpoint":1.517,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_supports_legacy_field_value_metadata_updates_through_the_alias_endpoint":4.136,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_updates_through_the_legacy_alias_endpoint":0.512,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_unsupported_legacy_field_value_updates_on_both_update_endpoints":0.965,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_deletes_orders_through_the_real_endpoint":1.045,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_rejects_invalid_order_delete_requests":1.64,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_regenerates_attached_wash_certificates_when_certificate_metadata_changes_through_the_primary_endpoint":3.683,"P\\Tests\\Api\\OrdersApiTest::__pest_evaluable_it_regenerates_attached_wash_certificates_for_legacy_field_value_updates_through_the_alias_endpoint":3.194,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayOperationLegacySchemaCompatibilityTest::__pest_evaluable_it_keeps_the_legacy_operation__type_column_compatible_with_v2_operation_queueing":0.027,"P\\Tests\\Unit\\Bookings\\OrderBookingsListCacheTest::__pest_evaluable_it_uses_sane_ttl_defaults_and_clamps_negative_ttl_to_zero":0,"P\\Tests\\Unit\\Bookings\\OrderBookingsListCacheTest::__pest_evaluable_it_builds_deterministic_keys_for_equivalent_contexts":0,"P\\Tests\\Unit\\Bookings\\OrderBookingsListCacheTest::__pest_evaluable_it_stores_and_retrieves_full_order_bookings_payloads":0,"P\\Tests\\Unit\\Bookings\\OrderBookingsListCacheTest::__pest_evaluable_it_ignores_malformed_cached_payloads_and_clears_order_bookings_list_caches":0,"P\\Tests\\Unit\\Bookings\\OrderBookingsRouteCacheWiringTest::__pest_evaluable_it_caches_the_paginated_order_bookings_list_response_and_invalidates_it_on_booking_changes":0.008,"P\\Tests\\Unit\\Bookings\\VehicleSearchBookedMetadataRouteContractTest::__pest_evaluable_it_keeps_booked_vehicle_search_metadata_aligned_with_filtered_pending_order_bookings":0.005,"P\\Tests\\Unit\\Bookings\\OrderBookingsCountsCacheTest::__pest_evaluable_it_uses_sane_ttl_defaults_and_clamps_negative_ttl_to_zero":0.012,"P\\Tests\\Unit\\Bookings\\OrderBookingsCountsCacheTest::__pest_evaluable_it_builds_deterministic_keys_for_equivalent_count_contexts":0.012,"P\\Tests\\Unit\\Bookings\\OrderBookingsCountsCacheTest::__pest_evaluable_it_stores_and_retrieves_normalized_booking_counts":0.014,"P\\Tests\\Unit\\Bookings\\OrderBookingsCountsCacheTest::__pest_evaluable_it_ignores_malformed_payloads_and_clears_order_bookings_count_caches":0.078,"P\\Tests\\Unit\\Bookings\\OrderBookingsCountsRouteWiringTest::__pest_evaluable_it_adds_a_cached_order_bookings_counts_endpoint_and_invalidates_it_on_booking_changes":0.008,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayFleetUsageStatisticsTest::__pest_evaluable_it_summarizes_fleet_usage_statistics_for_the_dashboard_landing_view":0.218,"P\\Tests\\Api\\ApiCoverageManifestTest::__pest_evaluable_it_keeps_every_selected_API_operation_covered_by_happy_path_and_failure_tests":3.168,"P\\Tests\\Api\\ApiCoverageManifestTest::__pest_evaluable_it_keeps_the_OpenAPI_manifest_entries_aligned_with_the_API_spec":0.753,"P\\Tests\\Api\\ApiFixturesCleanupTest::__pest_evaluable_it_removes_generated_customer_traces_during_fixture_cleanup":8.099,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_logs_in_with_valid_customer_credentials":4.515,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_rejects_invalid_login_payloads_and_credentials":1.271,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_returns_the_cached_auth_session_payload_for_a_valid_token":0.888,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_includes_economic_runtime_config_for_uncached_auth_sessions":1.349,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_rejects_invalid_auth_session_tokens":0.698,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_logs_out_and_invalidates_the_token_for_future_session_calls":1.365,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_logs_out_and_invalidates_cached_subuser_sessions":1.932,"P\\Tests\\Api\\AuthApiTest::__pest_evaluable_it_rejects_invalid_logout_tokens":0.393,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_returns_the_raw_202_gather_webhook_contract_over_HTTP":0.788,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_still_prompts_for_department_selection_when_only_one_department_is_eligible":0.732,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_accepts_the_legacy_initial_webhook_body_with_top_level_call_identifiers":0.692,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_returns_native_flow_gather_data_after_a_top_level_department_selection_when_distinct_gate_choices_exist":1.515,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_opens_the_gate_immediately_after_a_top_level_department_selection_when_entrance_and_exit_share_the_same_gate":1.719,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_uses_a_multi_digit_gather_contract_when_10_departments_are_eligible":0.587,"P\\Tests\\Api\\BirdVoiceWebhookApiTest::__pest_evaluable_it_returns_a_raw_400_transport_error_for_malformed_webhook_payloads":0.419,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_lists_only_visible_departments_and_can_return_a_single_department_with_the_slack_webhook":1.048,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_department_listing_when_the_permission_is_missing":0.378,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_creates_departments_through_the_real_endpoint":0.59,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_invalid_department_create_requests":1.554,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_updates_departments_through_the_real_endpoint":0.788,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_invalid_department_update_requests":1.552,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_lists_department_categories_for_a_department":1.006,"P\\Tests\\Api\\DepartmentsApiTest::__pest_evaluable_it_rejects_invalid_department_category_requests":1.191,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_lists_the_draft_customer_config_entry_in_economic_config_responses":0.947,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_round_trips_the_draft_customer_config_value_through_economic_config_updates":3.28,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_rejects_order_draft_exports_for_the_configured_draft_customer":0.86,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_rejects_booked_invoice_exports_for_the_configured_draft_customer":1.127,"P\\Tests\\Api\\EconomicDraftCustomerApiTest::__pest_evaluable_it_rejects_collected_invoice_exports_for_the_configured_draft_customer":0.888,"P\\Tests\\Api\\PingApiTest::__pest_evaluable_it_returns_the_ping_contract":0.877,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_returns_a_setup_required_error_when_department_terminal_readers_are_requested_without_terminal_setup":1.281,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_sends_a_Stripe_invoice_by_email_and_persists_the_hosted_invoice_association":5.434,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_returns_a_conflict_when_a_Stripe_hosted_invoice_is_already_active_for_the_order":4.756,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_allows_sending_a_new_Stripe_hosted_invoice_when_the_existing_association_is_already_terminal":3.943,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_voids_an_unpaid_Stripe_hosted_invoice_and_clears_the_local_order_association":3.746,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_refuses_to_cancel_a_paid_Stripe_hosted_invoice":4.131,"P\\Tests\\Api\\StripeApiTest::__pest_evaluable_it_returns_a_setup_required_error_when_creating_a_payment_intent_for_a_department_without_terminal_setup":0.726,"P\\Tests\\Api\\VehiclesApiTest::__pest_evaluable_it_returns_the_newest_vehicle_last__order__id_that_still_has_order_items":1.326,"P\\Tests\\Api\\VehiclesApiTest::__pest_evaluable_it_returns_a_null_vehicle_last__order__id_when_no_order_with_items_exists":1.159,"P\\Tests\\Unit\\Users\\EconomicCustomerModelParsingTest::__pest_evaluable_it_parses_cached_economic_customer_payloads_that_use_snake__case_customer__number":0.246,"P\\Tests\\Unit\\Users\\EconomicCustomerModelParsingTest::__pest_evaluable_it_leaves_the_economic_customer_model_empty_when_no_valid_customer_number_is_present":0}} \ No newline at end of file diff --git a/services/nginx/app/classes/edge_gateway_install_service.php b/services/nginx/app/classes/edge_gateway_install_service.php index 608ffed4..2289b3a0 100644 --- a/services/nginx/app/classes/edge_gateway_install_service.php +++ b/services/nginx/app/classes/edge_gateway_install_service.php @@ -8,6 +8,12 @@ class edge_gateway_install_service { private const ARTIFACTS = [ 'agent.php' => 'application/x-httpd-php; charset=utf-8', + 'lan-worker.php' => 'application/x-httpd-php; charset=utf-8', + 'docker-compose.gateway.yml' => 'text/yaml; charset=utf-8', + 'Dockerfile.edge-agent' => 'text/plain; charset=utf-8', + 'Dockerfile.lan-worker' => 'text/plain; charset=utf-8', + 'gateway-launcher.sh' => 'text/x-shellscript; charset=utf-8', + 'truckwash-edge-gateway-stack.service' => 'text/plain; charset=utf-8', 'truckwash-edge-agent.service' => 'text/plain; charset=utf-8', ]; diff --git a/services/nginx/app/classes/edge_gateway_manager.php b/services/nginx/app/classes/edge_gateway_manager.php index 97724907..35f54366 100644 --- a/services/nginx/app/classes/edge_gateway_manager.php +++ b/services/nginx/app/classes/edge_gateway_manager.php @@ -29,6 +29,20 @@ class edge_gateway_manager public const STATUS_OFFLINE = 'OFFLINE'; public const DEFAULT_RELEASE_CHANNEL = 'stable'; public const DEFAULT_AGENT_SERVICE_NAME = 'truckwash-edge-agent.service'; + public const DEFAULT_STACK_SERVICE_NAME = 'truckwash-edge-gateway-stack.service'; + public const DEFAULT_COMPOSE_STACK_FILE = 'docker-compose.gateway.yml'; + public const DEFAULT_LAUNCHER_SCRIPT_NAME = 'gateway-launcher.sh'; + public const DEFAULT_LAN_WORKER_ARTIFACT = 'lan-worker.php'; + public const DEFAULT_EDGE_AGENT_DOCKERFILE = 'Dockerfile.edge-agent'; + public const DEFAULT_LAN_WORKER_DOCKERFILE = 'Dockerfile.lan-worker'; + public const DEFAULT_INSTALL_DIR = '/opt/truckwash-edge-agent'; + public const DEFAULT_RUNTIME_DIR = '/opt/truckwash-edge-agent/runtime'; + public const DEFAULT_STATE_DATABASE_PATH = '/opt/truckwash-edge-agent/runtime/gateway-state.sqlite'; + public const DEFAULT_UPDATE_WINDOW = '02:00-04:00'; + public const DEFAULT_COMPOSE_PROJECT_NAME = 'truckwash-edge-gateway'; + public const DEFAULT_EDGE_AGENT_BASE_IMAGE = 'php:8.2-cli-bookworm'; + public const DEFAULT_LAN_WORKER_BASE_IMAGE = 'php:8.2-cli-bookworm'; + public const DEFAULT_WORKER_BASE_URL = 'http://lan-worker:8090'; public const INSTALL_TOKEN_TTL_SECONDS = 1800; public const HEARTBEAT_DEGRADED_AFTER_SECONDS = 60; public const HEARTBEAT_OFFLINE_AFTER_SECONDS = 300; @@ -113,7 +127,34 @@ class edge_gateway_manager 'is_primary' => 1, 'metadata_json' => array_merge($metadata, [ 'credentials_rotated_at' => $this->now(), - 'agent_runtime' => 'php-cli', + 'agent_runtime' => 'compose-php', + 'runtime_mode' => 'compose', + 'update_window' => self::DEFAULT_UPDATE_WINDOW, + 'container_health' => [ + 'overall_status' => self::STATUS_PENDING, + 'services' => [ + [ + 'name' => 'edge-agent', + 'status' => self::STATUS_PENDING, + ], + [ + 'name' => 'lan-worker', + 'status' => self::STATUS_PENDING, + ], + ], + ], + 'outbox_status' => [ + 'depth' => 0, + 'oldest_age_seconds' => 0, + 'last_flushed_at' => null, + 'pending_types' => [], + ], + 'rollback_status' => [ + 'state' => 'NONE', + 'reason' => null, + 'at' => null, + ], + 'last_sync_at' => null, ]), ]); @@ -755,8 +796,22 @@ class edge_gateway_manager 'installToken' => $plainToken, 'gatewayId' => null, 'agentToken' => null, - 'installDir' => '/opt/truckwash-edge-agent', + 'installDir' => self::DEFAULT_INSTALL_DIR, + 'runtimeDir' => self::DEFAULT_RUNTIME_DIR, + 'runtimeMode' => 'compose', 'serviceName' => self::DEFAULT_AGENT_SERVICE_NAME, + 'stackServiceName' => self::DEFAULT_STACK_SERVICE_NAME, + 'composeFileName' => self::DEFAULT_COMPOSE_STACK_FILE, + 'composeProjectName' => self::DEFAULT_COMPOSE_PROJECT_NAME, + 'launcherScriptName' => self::DEFAULT_LAUNCHER_SCRIPT_NAME, + 'lanWorkerArtifactName' => self::DEFAULT_LAN_WORKER_ARTIFACT, + 'edgeAgentDockerfileName' => self::DEFAULT_EDGE_AGENT_DOCKERFILE, + 'lanWorkerDockerfileName' => self::DEFAULT_LAN_WORKER_DOCKERFILE, + 'stateDatabasePath' => self::DEFAULT_STATE_DATABASE_PATH, + 'workerBaseUrl' => self::DEFAULT_WORKER_BASE_URL, + 'updateWindow' => self::DEFAULT_UPDATE_WINDOW, + 'edgeAgentBaseImage' => self::DEFAULT_EDGE_AGENT_BASE_IMAGE, + 'lanWorkerBaseImage' => self::DEFAULT_LAN_WORKER_BASE_IMAGE, 'heartbeatIntervalSeconds' => 15, 'operationPollTimeoutSeconds' => self::COMMAND_POLL_TIMEOUT_SECONDS, ], JSON_UNESCAPED_SLASHES); @@ -935,11 +990,24 @@ heartbeat_marker_is_fresh() { [ "${modified_epoch:-0}" -ge "$minimum_epoch" ] } print_service_diagnostics() { - log_error "truckwash-edge-agent.service did not complete installation verification." - log_error "systemctl status --no-pager truckwash-edge-agent.service" - systemctl status --no-pager truckwash-edge-agent.service || true - log_error "journalctl -u truckwash-edge-agent.service -n 40 --no-pager" - journalctl -u truckwash-edge-agent.service -n 40 --no-pager || true + log_error "truckwash-edge-gateway-stack.service did not complete installation verification." + log_error "systemctl status --no-pager truckwash-edge-gateway-stack.service" + systemctl status --no-pager truckwash-edge-gateway-stack.service || true + log_error "journalctl -u truckwash-edge-gateway-stack.service -n 60 --no-pager" + journalctl -u truckwash-edge-gateway-stack.service -n 60 --no-pager || true + log_error "docker ps --format '{{.Names}} {{.Status}}'" + docker ps --format '{{.Names}} {{.Status}}' || true +} +resolve_compose_command() { + if docker compose version >/dev/null 2>&1; then + echo "docker compose" + return 0 + fi + if command -v docker-compose >/dev/null 2>&1; then + echo "docker-compose" + return 0 + fi + return 1 } wait_for_gateway_claim() { local config_path="$1" @@ -977,16 +1045,24 @@ wait_for_post_restart_heartbeat() { fetch_http "Verify install token" "__VERIFY_URL__" INSTALL_DIR=/opt/truckwash-edge-agent +RUNTIME_DIR="$INSTALL_DIR/runtime" CONFIG_PATH="$INSTALL_DIR/config.json" CONFIG_TEMPLATE_PATH="$INSTALL_DIR/config.template.json" -HEARTBEAT_MARKER_PATH="$INSTALL_DIR/runtime/last-heartbeat-ok.txt" +HEARTBEAT_MARKER_PATH="$RUNTIME_DIR/last-heartbeat-ok.txt" +STACK_SERVICE_PATH="/etc/systemd/system/truckwash-edge-gateway-stack.service" REUSE_EXISTING_CREDENTIALS=0 -run_step "Creating install directory" mkdir -p "$INSTALL_DIR" +run_step "Creating install directory" mkdir -p "$INSTALL_DIR" "$RUNTIME_DIR" "$RUNTIME_DIR/backups" export DEBIAN_FRONTEND=noninteractive run_step "Updating package lists" apt-get update -run_step "Installing required packages" apt-get install -y curl ca-certificates php-cli php-curl php-mbstring +run_step "Installing required packages" apt-get install -y curl ca-certificates docker.io docker-compose-plugin php-cli php-curl php-mbstring php-sqlite3 fetch_http "Download PHP edge agent" "__AGENT_URL__" "$INSTALL_DIR/agent.php" -fetch_http "Download systemd service unit" "__SERVICE_URL__" "$INSTALL_DIR/truckwash-edge-agent.service" +fetch_http "Download LAN worker" "__WORKER_URL__" "$INSTALL_DIR/lan-worker.php" +fetch_http "Download compose stack" "__COMPOSE_URL__" "$INSTALL_DIR/docker-compose.gateway.yml" +fetch_http "Download edge-agent Dockerfile" "__EDGE_DOCKERFILE_URL__" "$INSTALL_DIR/Dockerfile.edge-agent" +fetch_http "Download lan-worker Dockerfile" "__WORKER_DOCKERFILE_URL__" "$INSTALL_DIR/Dockerfile.lan-worker" +fetch_http "Download gateway launcher" "__LAUNCHER_URL__" "$INSTALL_DIR/gateway-launcher.sh" +fetch_http "Download compose stack service unit" "__STACK_SERVICE_URL__" "$INSTALL_DIR/truckwash-edge-gateway-stack.service" +fetch_http "Download compatibility service unit" "__LEGACY_SERVICE_URL__" "$INSTALL_DIR/truckwash-edge-agent.service" if config_has_claimed_gateway "$CONFIG_PATH"; then REUSE_EXISTING_CREDENTIALS=1 log_info "Existing claimed gateway detected; reinstall will reuse saved gateway credentials." @@ -996,12 +1072,15 @@ __CONFIG_JSON__ EOF_JSON run_step "Writing agent config" merge_agent_config "$CONFIG_TEMPLATE_PATH" "$CONFIG_PATH" rm -f "$CONFIG_TEMPLATE_PATH" -run_step "Installing systemd service definition" install -m 0644 "$INSTALL_DIR/truckwash-edge-agent.service" "/etc/systemd/system/truckwash-edge-agent.service" -run_step "Setting PHP agent permissions" chmod 0755 "$INSTALL_DIR/agent.php" +run_step "Installing systemd stack definition" install -m 0644 "$INSTALL_DIR/truckwash-edge-gateway-stack.service" "$STACK_SERVICE_PATH" +run_step "Setting executable permissions" chmod 0755 "$INSTALL_DIR/agent.php" "$INSTALL_DIR/lan-worker.php" "$INSTALL_DIR/gateway-launcher.sh" +run_step "Ensuring Docker is enabled" systemctl enable docker +run_step "Starting Docker" systemctl restart docker +run_step "Checking Docker Compose availability" resolve_compose_command >/dev/null run_step "Reloading systemd" systemctl daemon-reload -run_step "Enabling truckwash-edge-agent.service" systemctl enable truckwash-edge-agent.service -run_step "Restarting truckwash-edge-agent.service" systemctl restart truckwash-edge-agent.service -run_step "Verifying truckwash-edge-agent.service is active" systemctl is-active --quiet truckwash-edge-agent.service +run_step "Enabling truckwash-edge-gateway-stack.service" systemctl enable truckwash-edge-gateway-stack.service +run_step "Restarting truckwash-edge-gateway-stack.service" systemctl restart truckwash-edge-gateway-stack.service +run_step "Verifying truckwash-edge-gateway-stack.service is active" systemctl is-active --quiet truckwash-edge-gateway-stack.service if [ "$REUSE_EXISTING_CREDENTIALS" -eq 1 ]; then run_step "Waiting for post-reinstall heartbeat" wait_for_post_restart_heartbeat "$HEARTBEAT_MARKER_PATH" "$INSTALL_STARTED_AT" 30 log_info "Reinstall reused gateway $(read_config_value "$CONFIG_PATH" gatewayId)." @@ -1009,13 +1088,19 @@ else run_step "Waiting for gateway claim" wait_for_gateway_claim "$CONFIG_PATH" "$HEARTBEAT_MARKER_PATH" "$INSTALL_STARTED_AT" 30 log_info "Gateway claim completed for gateway $(read_config_value "$CONFIG_PATH" gatewayId)." fi -echo 'TruckWash edge agent installed.' +echo 'TruckWash edge gateway stack installed.' BASH; return strtr($script, [ '__VERIFY_URL__' => $this->buildInstallTokenVerifyUrl($plainToken), '__AGENT_URL__' => $this->buildAgentArtifactUrl('agent.php'), - '__SERVICE_URL__' => $this->buildAgentArtifactUrl(self::DEFAULT_AGENT_SERVICE_NAME), + '__WORKER_URL__' => $this->buildAgentArtifactUrl(self::DEFAULT_LAN_WORKER_ARTIFACT), + '__COMPOSE_URL__' => $this->buildAgentArtifactUrl(self::DEFAULT_COMPOSE_STACK_FILE), + '__EDGE_DOCKERFILE_URL__' => $this->buildAgentArtifactUrl(self::DEFAULT_EDGE_AGENT_DOCKERFILE), + '__WORKER_DOCKERFILE_URL__' => $this->buildAgentArtifactUrl(self::DEFAULT_LAN_WORKER_DOCKERFILE), + '__LAUNCHER_URL__' => $this->buildAgentArtifactUrl(self::DEFAULT_LAUNCHER_SCRIPT_NAME), + '__STACK_SERVICE_URL__' => $this->buildAgentArtifactUrl(self::DEFAULT_STACK_SERVICE_NAME), + '__LEGACY_SERVICE_URL__' => $this->buildAgentArtifactUrl(self::DEFAULT_AGENT_SERVICE_NAME), '__CONFIG_JSON__' => (string)$configJson, ]); } @@ -1052,8 +1137,17 @@ BASH; 'apiUrl' => $this->getApiBaseUrl(), 'gatewayId' => (int)$gateway->id, 'agentToken' => $newToken, - 'installDir' => '/opt/truckwash-edge-agent', + 'installDir' => self::DEFAULT_INSTALL_DIR, + 'runtimeDir' => self::DEFAULT_RUNTIME_DIR, 'serviceName' => self::DEFAULT_AGENT_SERVICE_NAME, + 'stackServiceName' => self::DEFAULT_STACK_SERVICE_NAME, + 'composeFileName' => self::DEFAULT_COMPOSE_STACK_FILE, + 'composeProjectName' => self::DEFAULT_COMPOSE_PROJECT_NAME, + 'launcherScriptName' => self::DEFAULT_LAUNCHER_SCRIPT_NAME, + 'stateDatabasePath' => self::DEFAULT_STATE_DATABASE_PATH, + 'workerBaseUrl' => self::DEFAULT_WORKER_BASE_URL, + 'updateWindow' => (string)($metadata['update_window'] ?? self::DEFAULT_UPDATE_WINDOW), + 'runtimeMode' => (string)($metadata['runtime_mode'] ?? 'compose'), 'heartbeatIntervalSeconds' => 15, 'operationPollTimeoutSeconds' => self::COMMAND_POLL_TIMEOUT_SECONDS, 'installedVersion' => $gateway->installed_version->value() === null ? null : (string)$gateway->installed_version->value(), @@ -1064,7 +1158,10 @@ BASH; (int)$gateway->department_id->value(), 'GATEWAY_CREDENTIALS_ROTATED', $userId, - ['service_name' => self::DEFAULT_AGENT_SERVICE_NAME] + [ + 'service_name' => self::DEFAULT_AGENT_SERVICE_NAME, + 'stack_service_name' => self::DEFAULT_STACK_SERVICE_NAME, + ] ); return [ @@ -1074,8 +1171,9 @@ BASH; 'config' => $payload, 'config_json' => json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), 'restart_instructions' => [ - 'sudo systemctl restart ' . self::DEFAULT_AGENT_SERVICE_NAME, - 'sudo systemctl status ' . self::DEFAULT_AGENT_SERVICE_NAME . ' --no-pager', + 'sudo systemctl restart ' . self::DEFAULT_STACK_SERVICE_NAME, + 'sudo systemctl status ' . self::DEFAULT_STACK_SERVICE_NAME . ' --no-pager', + 'cd ' . self::DEFAULT_INSTALL_DIR . ' && sudo ./gateway-launcher.sh reconcile', ], ]; } @@ -1100,6 +1198,30 @@ BASH; 'serviceUnitUrl' => $this->buildAgentArtifactUrl(self::DEFAULT_AGENT_SERVICE_NAME), 'serviceUnitSha256' => $this->buildAgentArtifactSha256(self::DEFAULT_AGENT_SERVICE_NAME), 'serviceName' => self::DEFAULT_AGENT_SERVICE_NAME, + 'runtimeMode' => 'compose', + 'installDir' => self::DEFAULT_INSTALL_DIR, + 'runtimeDir' => self::DEFAULT_RUNTIME_DIR, + 'stackServiceName' => self::DEFAULT_STACK_SERVICE_NAME, + 'stackServiceUnitUrl' => $this->buildAgentArtifactUrl(self::DEFAULT_STACK_SERVICE_NAME), + 'stackServiceUnitSha256' => $this->buildAgentArtifactSha256(self::DEFAULT_STACK_SERVICE_NAME), + 'composeFileName' => self::DEFAULT_COMPOSE_STACK_FILE, + 'composeFileUrl' => $this->buildAgentArtifactUrl(self::DEFAULT_COMPOSE_STACK_FILE), + 'composeFileSha256' => $this->buildAgentArtifactSha256(self::DEFAULT_COMPOSE_STACK_FILE), + 'composeProjectName' => self::DEFAULT_COMPOSE_PROJECT_NAME, + 'launcherScriptName' => self::DEFAULT_LAUNCHER_SCRIPT_NAME, + 'launcherScriptUrl' => $this->buildAgentArtifactUrl(self::DEFAULT_LAUNCHER_SCRIPT_NAME), + 'launcherScriptSha256' => $this->buildAgentArtifactSha256(self::DEFAULT_LAUNCHER_SCRIPT_NAME), + 'lanWorkerArtifactUrl' => $this->buildAgentArtifactUrl(self::DEFAULT_LAN_WORKER_ARTIFACT), + 'lanWorkerArtifactSha256' => $this->buildAgentArtifactSha256(self::DEFAULT_LAN_WORKER_ARTIFACT), + 'edgeAgentDockerfileUrl' => $this->buildAgentArtifactUrl(self::DEFAULT_EDGE_AGENT_DOCKERFILE), + 'edgeAgentDockerfileSha256' => $this->buildAgentArtifactSha256(self::DEFAULT_EDGE_AGENT_DOCKERFILE), + 'lanWorkerDockerfileUrl' => $this->buildAgentArtifactUrl(self::DEFAULT_LAN_WORKER_DOCKERFILE), + 'lanWorkerDockerfileSha256' => $this->buildAgentArtifactSha256(self::DEFAULT_LAN_WORKER_DOCKERFILE), + 'stateDatabasePath' => self::DEFAULT_STATE_DATABASE_PATH, + 'workerBaseUrl' => self::DEFAULT_WORKER_BASE_URL, + 'updateWindow' => self::DEFAULT_UPDATE_WINDOW, + 'edgeAgentBaseImage' => self::DEFAULT_EDGE_AGENT_BASE_IMAGE, + 'lanWorkerBaseImage' => self::DEFAULT_LAN_WORKER_BASE_IMAGE, ]; } @@ -2500,6 +2622,12 @@ BASH; ]; $gateway['version_drift'] = self::buildVersionDriftSummary($gateway); $gateway['credential_freshness'] = self::buildCredentialFreshnessSummary($gateway, $now); + $gateway['container_health'] = self::buildContainerHealthSummary($gateway, $effectiveStatus); + $gateway['outbox_status'] = self::buildOutboxStatusSummary($gateway, $now); + $gateway['last_sync_at'] = self::resolveLastSyncAt($gateway); + $gateway['update_window'] = self::buildUpdateWindowSummary($gateway); + $gateway['staged_version'] = self::buildStagedVersionSummary($gateway); + $gateway['rollback_status'] = self::buildRollbackStatusSummary($gateway); $gateway['diagnostics'] = self::buildGatewayDiagnostics($gateway, $effectiveStatus, $now); $gateway['error_state'] = self::primaryGatewayErrorState($gateway['diagnostics'], $gateway); @@ -2722,6 +2850,147 @@ BASH; ]; } + private static function buildContainerHealthSummary(array $gateway, string $effectiveStatus): array + { + $metadata = isset($gateway['metadata']) && is_array($gateway['metadata']) ? (array)$gateway['metadata'] : []; + $raw = isset($metadata['container_health']) && is_array($metadata['container_health']) + ? (array)$metadata['container_health'] + : []; + $rawServices = isset($raw['services']) && is_array($raw['services']) ? (array)$raw['services'] : []; + $defaultServices = [ + ['name' => 'edge-agent', 'status' => $effectiveStatus === self::STATUS_OFFLINE ? 'offline' : 'healthy'], + ['name' => 'lan-worker', 'status' => $effectiveStatus === self::STATUS_OFFLINE ? 'offline' : 'healthy'], + ]; + $services = $rawServices !== [] ? $rawServices : $defaultServices; + $healthyCount = 0; + $degradedCount = 0; + foreach ($services as $index => $service) { + if (!is_array($service)) { + $services[$index] = ['name' => 'service-' . $index, 'status' => 'unknown']; + continue; + } + + $status = strtolower(trim((string)($service['status'] ?? 'unknown'))); + $name = trim((string)($service['name'] ?? 'service-' . $index)); + if (in_array($status, ['healthy', 'running', 'online'], true)) { + $status = 'healthy'; + $healthyCount += 1; + } elseif (in_array($status, ['degraded', 'starting', 'unknown'], true)) { + $status = 'degraded'; + $degradedCount += 1; + } else { + $status = $status === 'offline' ? 'offline' : 'degraded'; + $degradedCount += 1; + } + + $services[$index] = array_merge($service, [ + 'name' => $name, + 'status' => $status, + ]); + } + + $state = $effectiveStatus === self::STATUS_OFFLINE + ? self::STATUS_OFFLINE + : ($degradedCount > 0 ? self::STATUS_DEGRADED : self::STATUS_ONLINE); + + return [ + 'state' => strtoupper((string)($raw['state'] ?? $state)), + 'summary' => (string)($raw['summary'] ?? sprintf('%d/%d containers healthy', $healthyCount, count($services))), + 'services' => $services, + 'healthy_count' => $healthyCount, + 'total' => count($services), + ]; + } + + private static function buildOutboxStatusSummary(array $gateway, ?int $now = null): array + { + $metadata = isset($gateway['metadata']) && is_array($gateway['metadata']) ? (array)$gateway['metadata'] : []; + $raw = isset($metadata['outbox_status']) && is_array($metadata['outbox_status']) + ? (array)$metadata['outbox_status'] + : []; + $queued = max(0, (int)($raw['queued'] ?? $raw['queue_depth'] ?? 0)); + $oldestQueuedAt = isset($raw['oldest_queued_at']) ? (string)$raw['oldest_queued_at'] : null; + $oldestAgeSeconds = self::heartbeatAgeSeconds($oldestQueuedAt, $now); + $state = $queued === 0 + ? 'IN_SYNC' + : (($oldestAgeSeconds !== null && $oldestAgeSeconds >= self::HEARTBEAT_OFFLINE_AFTER_SECONDS) ? 'DEGRADED' : 'QUEUED'); + + return [ + 'state' => (string)($raw['state'] ?? $state), + 'queued' => $queued, + 'oldest_queued_at' => $oldestQueuedAt, + 'oldest_age_seconds' => $oldestAgeSeconds, + 'last_replayed_at' => isset($raw['last_replayed_at']) ? (string)$raw['last_replayed_at'] : null, + 'summary' => (string)($raw['summary'] ?? ($queued === 0 ? 'Outbox is empty' : sprintf('%d outbound items queued', $queued))), + ]; + } + + private static function resolveLastSyncAt(array $gateway): ?string + { + $metadata = isset($gateway['metadata']) && is_array($gateway['metadata']) ? (array)$gateway['metadata'] : []; + if (!empty($metadata['last_sync_at'])) { + return (string)$metadata['last_sync_at']; + } + + $outbox = isset($gateway['outbox_status']) && is_array($gateway['outbox_status']) + ? (array)$gateway['outbox_status'] + : []; + + return isset($outbox['last_replayed_at']) && $outbox['last_replayed_at'] !== null + ? (string)$outbox['last_replayed_at'] + : null; + } + + private static function buildUpdateWindowSummary(array $gateway): array + { + $metadata = isset($gateway['metadata']) && is_array($gateway['metadata']) ? (array)$gateway['metadata'] : []; + $window = trim((string)($metadata['update_window'] ?? self::DEFAULT_UPDATE_WINDOW)); + if ($window === '') { + $window = self::DEFAULT_UPDATE_WINDOW; + } + + return [ + 'window' => $window, + 'timezone' => isset($metadata['timezone']) ? (string)$metadata['timezone'] : null, + 'strategy' => 'nightly', + ]; + } + + private static function buildStagedVersionSummary(array $gateway): ?array + { + $metadata = isset($gateway['metadata']) && is_array($gateway['metadata']) ? (array)$gateway['metadata'] : []; + $raw = isset($metadata['staged_version']) && is_array($metadata['staged_version']) + ? (array)$metadata['staged_version'] + : []; + $targetVersion = trim((string)($raw['target_version'] ?? $raw['version'] ?? '')); + if ($targetVersion === '') { + return null; + } + + return [ + 'target_version' => $targetVersion, + 'staged_at' => isset($raw['staged_at']) ? (string)$raw['staged_at'] : null, + 'apply_after' => isset($raw['apply_after']) ? (string)$raw['apply_after'] : null, + 'status' => isset($raw['status']) ? (string)$raw['status'] : 'STAGED', + ]; + } + + private static function buildRollbackStatusSummary(array $gateway): array + { + $metadata = isset($gateway['metadata']) && is_array($gateway['metadata']) ? (array)$gateway['metadata'] : []; + $raw = isset($metadata['rollback_status']) && is_array($metadata['rollback_status']) + ? (array)$metadata['rollback_status'] + : []; + $state = trim((string)($raw['state'] ?? 'IDLE')); + + return [ + 'state' => $state !== '' ? $state : 'IDLE', + 'reason' => isset($raw['reason']) ? (string)$raw['reason'] : null, + 'rolled_back_to' => isset($raw['rolled_back_to']) ? (string)$raw['rolled_back_to'] : null, + 'at' => isset($raw['at']) ? (string)$raw['at'] : null, + ]; + } + private static function buildGatewayDiagnostics(array $gateway, string $effectiveStatus, ?int $now = null): array { $diagnostics = []; @@ -2779,6 +3048,51 @@ BASH; ]; } + $containerHealth = isset($gateway['container_health']) && is_array($gateway['container_health']) + ? (array)$gateway['container_health'] + : []; + $containerState = strtoupper((string)($containerHealth['state'] ?? self::STATUS_ONLINE)); + if (in_array($containerState, [self::STATUS_DEGRADED, self::STATUS_OFFLINE], true)) { + $diagnostics[] = [ + 'code' => 'EDGE_GATEWAY_CONTAINER_DEGRADED', + 'severity' => $containerState === self::STATUS_OFFLINE ? 'danger' : 'warning', + 'message' => 'One or more compose services are not healthy on the gateway.', + 'recommended_action' => 'restart_agent', + ]; + } + + $outboxStatus = isset($gateway['outbox_status']) && is_array($gateway['outbox_status']) + ? (array)$gateway['outbox_status'] + : []; + if ((int)($outboxStatus['queued'] ?? 0) > 0) { + $diagnostics[] = [ + 'code' => 'EDGE_GATEWAY_OUTBOX_BACKLOG', + 'severity' => 'warning', + 'message' => 'The gateway has queued outbound control-plane items waiting for replay.', + 'recommended_action' => 'inspect_connectivity', + ]; + } + + $rollbackStatus = isset($gateway['rollback_status']) && is_array($gateway['rollback_status']) + ? (array)$gateway['rollback_status'] + : []; + $rollbackState = strtoupper((string)($rollbackStatus['state'] ?? 'IDLE')); + if ($rollbackState === 'ROLLED_BACK') { + $diagnostics[] = [ + 'code' => 'EDGE_GATEWAY_UPDATE_ROLLED_BACK', + 'severity' => 'warning', + 'message' => 'The last container rollout was rolled back automatically.', + 'recommended_action' => 'review_diagnostics', + ]; + } elseif ($rollbackState === 'FAILED') { + $diagnostics[] = [ + 'code' => 'EDGE_GATEWAY_ROLLBACK_FAILED', + 'severity' => 'danger', + 'message' => 'Gateway rollback failed and manual intervention is required.', + 'recommended_action' => 'review_diagnostics', + ]; + } + return $diagnostics; } diff --git a/services/nginx/app/classes/edge_gateway_operation_service.php b/services/nginx/app/classes/edge_gateway_operation_service.php index 428a3fbe..7144e014 100644 --- a/services/nginx/app/classes/edge_gateway_operation_service.php +++ b/services/nginx/app/classes/edge_gateway_operation_service.php @@ -428,8 +428,8 @@ class edge_gateway_operation_service if ($type === self::TYPE_UNINSTALL) { return array_merge([ - 'service_name' => edge_gateway_manager::DEFAULT_AGENT_SERVICE_NAME, - 'install_dir' => '/opt/truckwash-edge-agent', + 'service_name' => edge_gateway_manager::DEFAULT_STACK_SERVICE_NAME, + 'install_dir' => edge_gateway_manager::DEFAULT_INSTALL_DIR, ], $request); } @@ -594,14 +594,33 @@ class edge_gateway_operation_service ]; } } elseif ($type === self::TYPE_UPDATE) { + $requestedUpdate = is_array($operation->request_json->value()) ? (array)$operation->request_json->value() : []; + $stagedVersion = trim((string)($result['staged_version'] ?? $result['target_version'] ?? $requestedUpdate['target_version'] ?? '')); + $applied = array_key_exists('applied', $result) + ? (bool)$result['applied'] + : trim((string)($result['installed_version'] ?? '')) !== ''; if ($ok) { - $installedVersion = trim((string)($result['installed_version'] ?? $result['target_version'] ?? $operation->request_json->value()['target_version'] ?? '')); - if ($installedVersion !== '') { + $installedVersion = trim((string)($result['installed_version'] ?? ($applied ? $stagedVersion : ''))); + if ($applied && $installedVersion !== '') { $gateway->installed_version->set($installedVersion); $gateway->target_version->set($installedVersion); } $metadata['last_update_completed_at'] = $now; $metadata['last_update_error'] = null; + if ($stagedVersion !== '') { + $metadata['staged_version'] = [ + 'target_version' => $stagedVersion, + 'staged_at' => $result['staged_at'] ?? $now, + 'apply_after' => $result['apply_after'] ?? null, + 'status' => $applied ? 'APPLIED' : 'STAGED', + ]; + } + if (isset($result['update_window'])) { + $metadata['update_window'] = (string)$result['update_window']; + } + if (isset($result['rollback_status']) && is_array($result['rollback_status'])) { + $metadata['rollback_status'] = (array)$result['rollback_status']; + } } else { $metadata['last_update_error'] = [ 'code' => $errorCode, diff --git a/services/nginx/app/objects/users_o.php b/services/nginx/app/objects/users_o.php index 265f88a9..3237e3d3 100644 --- a/services/nginx/app/objects/users_o.php +++ b/services/nginx/app/objects/users_o.php @@ -500,6 +500,27 @@ class users_o extends db return $fallbackName; } + /** + * @return array{name:string}|null + */ + private static function buildCustomerNameCachePayload(mixed $cached_name, ?string $fallback_name): ?array + { + if ( + is_object($cached_name) + && isset($cached_name->name) + && is_string($cached_name->name) + && trim($cached_name->name) !== '' + ) { + return ['name' => $cached_name->name]; + } + + if ($fallback_name !== null && trim($fallback_name) !== '') { + return ['name' => $fallback_name]; + } + + return null; + } + public function getCustomerEcocomicData(int $customer_number = null): users_o { // Check if the customer number is set @@ -1547,25 +1568,32 @@ class users_o extends db if (count($customer_numbers_to_fetch) > 0) { foreach ( $customer_numbers_to_fetch as $customer_number ) { // Get the customer name from the external source + $fallback_name = null; try { // Try to get the economic customer data cached in the user $tmp_user = new users_o(); $tmp_user->getUserByCustomerNumber($customer_number); + if ($tmp_user->exists()) { + $display_name = $tmp_user->display_name->value(); + if (is_string($display_name) && trim($display_name) !== '') { + $fallback_name = $display_name; + } + } $cached_name = $tmp_user->getCached('economic_customer'); // If not cached, fetch from E-conomic if (!$cached_name) { $tmp_user->getCustomerEcocomicData($customer_number); $cached_name = $tmp_user->getCached('economic_customer'); } - if ($cached_name) { - $customer_names[(string)$customer_number] = $cached_name->name; + $cache_payload = self::buildCustomerNameCachePayload($cached_name, $fallback_name); + if ($cache_payload !== null) { + $customer_names[(string)$customer_number] = $cache_payload['name']; + $this->cache('economic_customer_name', $cache_payload, $customer_number); + $this->setCachedExpiration('economic_customer_name', self::$economicCustomerNameCacheExpiration, $customer_number); } - // Cache the name - $this->cache('economic_customer_name', $cached_name, $customer_number); - $this->setCachedExpiration('economic_customer_name', self::$economicCustomerNameCacheExpiration, $customer_number); } catch ( Exception $e ) { // Ignore exceptions - $customer_names[(string)$customer_number] = 'Unable to fetch name'; + $customer_names[(string)$customer_number] = $fallback_name ?? 'Unable to fetch name'; } } } diff --git a/services/nginx/app/resources/edge-gateway-agent/Dockerfile.edge-agent b/services/nginx/app/resources/edge-gateway-agent/Dockerfile.edge-agent new file mode 100644 index 00000000..0de2179b --- /dev/null +++ b/services/nginx/app/resources/edge-gateway-agent/Dockerfile.edge-agent @@ -0,0 +1,19 @@ +ARG BASE_IMAGE=php:8.2-cli-bookworm +FROM ${BASE_IMAGE} + +WORKDIR /opt/truckwash-edge-agent + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + curl \ + ca-certificates \ + libcurl4-openssl-dev \ + libsqlite3-dev; \ + docker-php-ext-install curl sqlite3; \ + rm -rf /var/lib/apt/lists/* + +COPY agent.php /opt/truckwash-edge-agent/agent.php + +ENTRYPOINT ["php", "/opt/truckwash-edge-agent/agent.php"] +CMD ["--config", "/config/config.json"] diff --git a/services/nginx/app/resources/edge-gateway-agent/Dockerfile.lan-worker b/services/nginx/app/resources/edge-gateway-agent/Dockerfile.lan-worker new file mode 100644 index 00000000..242a7c92 --- /dev/null +++ b/services/nginx/app/resources/edge-gateway-agent/Dockerfile.lan-worker @@ -0,0 +1,17 @@ +ARG BASE_IMAGE=php:8.2-cli-bookworm +FROM ${BASE_IMAGE} + +WORKDIR /opt/truckwash-edge-agent + +RUN set -eux; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + curl \ + ca-certificates \ + libcurl4-openssl-dev; \ + docker-php-ext-install curl; \ + rm -rf /var/lib/apt/lists/* + +COPY lan-worker.php /opt/truckwash-edge-agent/lan-worker.php + +CMD ["php", "-S", "0.0.0.0:8090", "/opt/truckwash-edge-agent/lan-worker.php"] diff --git a/services/nginx/app/resources/edge-gateway-agent/agent.php b/services/nginx/app/resources/edge-gateway-agent/agent.php index fac5e6a7..42415ae7 100644 --- a/services/nginx/app/resources/edge-gateway-agent/agent.php +++ b/services/nginx/app/resources/edge-gateway-agent/agent.php @@ -177,46 +177,193 @@ final class Logger } } +final class LocalStateStore +{ + private SQLite3 $db; + + public function __construct(string $path) + { + if (!class_exists('SQLite3')) { + throw new RuntimeException('The edge-agent container requires sqlite3 support.'); + } + + $directory = dirname($path); + if (!is_dir($directory)) { + @mkdir($directory, 0777, true); + } + + $this->db = new SQLite3($path); + $this->db->busyTimeout(5000); + $this->db->exec('PRAGMA journal_mode = WAL;'); + $this->db->exec('PRAGMA synchronous = NORMAL;'); + $this->db->exec( + 'CREATE TABLE IF NOT EXISTS kv ( + key TEXT PRIMARY KEY, + value_json TEXT NOT NULL, + updated_at TEXT NOT NULL + )' + ); + $this->db->exec( + 'CREATE TABLE IF NOT EXISTS outbox ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + item_type TEXT NOT NULL, + endpoint TEXT NOT NULL, + payload_json TEXT NOT NULL, + created_at TEXT NOT NULL + )' + ); + } + + public function getJson(string $key, mixed $default = null): mixed + { + $statement = $this->db->prepare('SELECT value_json FROM kv WHERE key = :key LIMIT 1'); + $statement->bindValue(':key', $key, SQLITE3_TEXT); + $result = $statement->execute(); + $row = $result instanceof SQLite3Result ? $result->fetchArray(SQLITE3_ASSOC) : false; + if (!is_array($row) || !array_key_exists('value_json', $row)) { + return $default; + } + + $decoded = json_decode((string)$row['value_json'], true); + return json_last_error() === JSON_ERROR_NONE ? $decoded : $default; + } + + public function setJson(string $key, mixed $value): void + { + $statement = $this->db->prepare( + 'INSERT INTO kv (key, value_json, updated_at) + VALUES (:key, :value_json, :updated_at) + ON CONFLICT(key) DO UPDATE SET value_json = excluded.value_json, updated_at = excluded.updated_at' + ); + $statement->bindValue(':key', $key, SQLITE3_TEXT); + $statement->bindValue(':value_json', json_encode($value, JSON_UNESCAPED_SLASHES), SQLITE3_TEXT); + $statement->bindValue(':updated_at', date('c'), SQLITE3_TEXT); + $statement->execute(); + } + + public function enqueue(string $type, string $endpoint, array $payload): void + { + $statement = $this->db->prepare( + 'INSERT INTO outbox (item_type, endpoint, payload_json, created_at) + VALUES (:item_type, :endpoint, :payload_json, :created_at)' + ); + $statement->bindValue(':item_type', $type, SQLITE3_TEXT); + $statement->bindValue(':endpoint', $endpoint, SQLITE3_TEXT); + $statement->bindValue(':payload_json', json_encode($payload, JSON_UNESCAPED_SLASHES), SQLITE3_TEXT); + $statement->bindValue(':created_at', date('c'), SQLITE3_TEXT); + $statement->execute(); + } + + /** + * @return array> + */ + public function queuedItems(int $limit = 25): array + { + $statement = $this->db->prepare( + 'SELECT id, item_type, endpoint, payload_json, created_at + FROM outbox + ORDER BY id ASC + LIMIT :limit' + ); + $statement->bindValue(':limit', max(1, $limit), SQLITE3_INTEGER); + $result = $statement->execute(); + $items = []; + if (!$result instanceof SQLite3Result) { + return $items; + } + + while (($row = $result->fetchArray(SQLITE3_ASSOC)) !== false) { + $items[] = [ + 'id' => (int)$row['id'], + 'type' => (string)$row['item_type'], + 'endpoint' => (string)$row['endpoint'], + 'payload' => json_decode((string)$row['payload_json'], true) ?: [], + 'created_at' => (string)$row['created_at'], + ]; + } + + return $items; + } + + public function removeOutboxItem(int $id): void + { + $statement = $this->db->prepare('DELETE FROM outbox WHERE id = :id'); + $statement->bindValue(':id', $id, SQLITE3_INTEGER); + $statement->execute(); + } + + public function outboxSummary(): array + { + $countResult = $this->db->querySingle('SELECT COUNT(*) FROM outbox'); + $oldestResult = $this->db->querySingle('SELECT created_at FROM outbox ORDER BY id ASC LIMIT 1'); + $lastSyncAt = $this->getJson('last_sync_at'); + + return [ + 'queued' => (int)$countResult, + 'oldest_queued_at' => is_string($oldestResult) && $oldestResult !== '' ? $oldestResult : null, + 'last_replayed_at' => is_string($lastSyncAt) && $lastSyncAt !== '' ? $lastSyncAt : null, + ]; + } +} + final class TruckwashEdgeAgent { + private const DEFAULT_INSTALL_DIR = '/opt/truckwash-edge-agent'; + private const DEFAULT_RUNTIME_DIR = '/opt/truckwash-edge-agent/runtime'; + private const DEFAULT_STATE_DATABASE = '/opt/truckwash-edge-agent/runtime/gateway-state.sqlite'; + private const DEFAULT_WORKER_BASE_URL = 'http://lan-worker:8090'; + private const DEFAULT_UPDATE_WINDOW = '02:00-04:00'; + private AgentConfig $config; private HttpJsonClient $http; + private HttpJsonClient $workerHttp; private Logger $logger; + private LocalStateStore $stateStore; private string $installDir; private string $runtimeDir; private string $statePath; private string $lastOperationSnapshotPath; private string $lastHeartbeatMarkerPath; + private string $stagedUpdatePath; private int $lastHeartbeatAt = 0; private string $agentInstanceId; public function __construct(string $configPath) { $this->config = AgentConfig::load($configPath); + $this->installDir = rtrim((string)$this->config->get('installDir', self::DEFAULT_INSTALL_DIR), DIRECTORY_SEPARATOR); + $this->runtimeDir = rtrim((string)$this->config->get('runtimeDir', self::DEFAULT_RUNTIME_DIR), DIRECTORY_SEPARATOR); + if (!is_dir($this->runtimeDir)) { + @mkdir($this->runtimeDir, 0777, true); + } + $this->http = new HttpJsonClient((string)$this->config->get('apiUrl')); - $this->installDir = rtrim((string)$this->config->get('installDir', dirname($configPath)), DIRECTORY_SEPARATOR); - $this->runtimeDir = $this->installDir . DIRECTORY_SEPARATOR . 'runtime'; - @mkdir($this->runtimeDir, 0777, true); + $this->workerHttp = new HttpJsonClient((string)$this->config->get('workerBaseUrl', self::DEFAULT_WORKER_BASE_URL)); $this->logger = new Logger($this->runtimeDir . DIRECTORY_SEPARATOR . 'agent.log'); + $this->stateStore = new LocalStateStore((string)$this->config->get('stateDatabasePath', self::DEFAULT_STATE_DATABASE)); $this->statePath = $this->runtimeDir . DIRECTORY_SEPARATOR . 'current-operation.json'; $this->lastOperationSnapshotPath = $this->runtimeDir . DIRECTORY_SEPARATOR . 'last-operation.json'; $this->lastHeartbeatMarkerPath = $this->runtimeDir . DIRECTORY_SEPARATOR . 'last-heartbeat-ok.txt'; + $this->stagedUpdatePath = $this->runtimeDir . DIRECTORY_SEPARATOR . 'staged-update.json'; $this->agentInstanceId = $this->ensureAgentInstanceId(); } public function run(): void { - $this->logger->info('Truckwash PHP edge agent starting.'); + $this->logger->info('Truckwash compose edge-agent starting.'); $this->recoverPreviousOperationState(); while (true) { try { + $this->reloadConfigFromDisk(); $this->ensureClaimed(); + $this->flushOutbox(); $this->heartbeat(); $processedManagementOperation = $this->processManagementOperation(); if (!$processedManagementOperation) { $this->processCommandQueue(); } + $this->flushOutbox(); } catch (Throwable $throwable) { $this->logger->error($throwable->getMessage()); sleep(2); @@ -232,26 +379,34 @@ final class TruckwashEdgeAgent return; } + $installedVersion = (string)$this->config->get('installedVersion', 'compose-php-agent-v2'); $response = $this->http->post('/edge-agent/claim', [ 'token' => (string)$this->config->get('installToken'), 'hostname' => gethostname() ?: 'truckwash-edge', - 'installed_version' => (string)$this->config->get('installedVersion', 'php-agent-v1'), + 'installed_version' => $installedVersion, 'metadata' => [ - 'runtime' => 'php-cli', + 'runtime' => 'compose-php', + 'runtime_mode' => 'compose', 'php_version' => PHP_VERSION, 'agent_instance_id' => $this->agentInstanceId, + 'update_window' => (string)$this->config->get('updateWindow', self::DEFAULT_UPDATE_WINDOW), + 'container_health' => $this->buildContainerHealth(), + 'outbox_status' => $this->buildOutboxStatus(), + 'last_sync_at' => $this->stateStore->getJson('last_sync_at'), + 'rollback_status' => $this->readRollbackStatus(), + 'staged_version' => $this->currentStagedUpdate(), ], ]); $payload = $response['data'] ?? []; - $gateway = $payload['gateway'] ?? []; - $installedVersion = (string)$this->config->get('installedVersion', 'php-agent-v1'); + $gateway = is_array($payload['gateway'] ?? null) ? (array)$payload['gateway'] : []; $this->config->set('gatewayId', $gateway['id'] ?? null); $this->config->set('agentToken', $payload['agent_token'] ?? null); $this->config->set('installedVersion', $installedVersion); $this->config->set('targetVersion', (string)($gateway['target_version'] ?? $installedVersion)); $this->config->set('agentInstanceId', $this->agentInstanceId); $this->config->save(); + $this->stateStore->setJson('last_sync_at', date('c')); $this->logger->info('Claimed gateway ' . (string)($gateway['id'] ?? 'unknown') . '.'); } @@ -268,22 +423,38 @@ final class TruckwashEdgeAgent } $operationState = $this->readOperationState(); - $this->http->post('/edge-agent/gateways/' . $gatewayId . '/heartbeat', [ + $payload = [ 'agent_token' => (string)$this->config->get('agentToken'), 'status' => 'ONLINE', 'hostname' => gethostname() ?: 'truckwash-edge', - 'installed_version' => (string)$this->config->get('installedVersion', 'php-agent-v1'), - 'target_version' => (string)$this->config->get('targetVersion', $this->config->get('installedVersion', 'php-agent-v1')), + 'installed_version' => (string)$this->config->get('installedVersion', 'compose-php-agent-v2'), + 'target_version' => (string)$this->config->get( + 'targetVersion', + $this->config->get('installedVersion', 'compose-php-agent-v2') + ), 'metadata' => array_merge([ 'agent_instance_id' => $this->agentInstanceId, + 'runtime' => 'compose-php', + 'runtime_mode' => 'compose', 'current_operation' => $operationState, - 'system_metrics' => [ - 'memory_usage_bytes' => memory_get_usage(true), - 'memory_peak_bytes' => memory_get_peak_usage(true), - 'load_average' => function_exists('sys_getloadavg') ? sys_getloadavg() : [], - ], + 'system_metrics' => $this->buildSystemMetrics(), + 'container_health' => $this->buildContainerHealth(), + 'outbox_status' => $this->buildOutboxStatus(), + 'last_sync_at' => $this->stateStore->getJson('last_sync_at'), + 'update_window' => (string)$this->config->get('updateWindow', self::DEFAULT_UPDATE_WINDOW), + 'staged_version' => $this->currentStagedUpdate(), + 'rollback_status' => $this->readRollbackStatus(), ], $metadata), - ]); + ]; + + $posted = $this->sendControlPlaneEvent( + '/edge-agent/gateways/' . $gatewayId . '/heartbeat', + $payload, + 'heartbeat' + ); + if (!$posted) { + return; + } $this->lastHeartbeatAt = time(); file_put_contents($this->lastHeartbeatMarkerPath, json_encode([ @@ -296,10 +467,20 @@ final class TruckwashEdgeAgent private function processCommandQueue(): void { $gatewayId = (int)$this->config->get('gatewayId'); - $response = $this->http->post('/edge-agent/gateways/' . $gatewayId . '/commands/poll', [ - 'agent_token' => (string)$this->config->get('agentToken'), - 'wait_seconds' => (int)$this->config->get('operationPollTimeoutSeconds', 20), - ]); + if ($gatewayId <= 0) { + return; + } + + try { + $response = $this->http->post('/edge-agent/gateways/' . $gatewayId . '/commands/poll', [ + 'agent_token' => (string)$this->config->get('agentToken'), + 'wait_seconds' => (int)$this->config->get('operationPollTimeoutSeconds', 20), + ]); + } catch (Throwable $throwable) { + $this->logger->warning('Command polling failed: ' . $throwable->getMessage()); + return; + } + $command = $response['data'] ?? null; if (!is_array($command) || empty($command['id'])) { return; @@ -317,28 +498,46 @@ final class TruckwashEdgeAgent default => throw new RuntimeException('Unsupported command type: ' . $type), }; - $this->http->post('/edge-agent/gateways/' . $gatewayId . '/commands/' . $jobId . '/result', [ - 'agent_token' => (string)$this->config->get('agentToken'), - 'ok' => true, - 'result' => $result, - ]); + $this->sendControlPlaneEvent( + '/edge-agent/gateways/' . $gatewayId . '/commands/' . $jobId . '/result', + [ + 'agent_token' => (string)$this->config->get('agentToken'), + 'ok' => true, + 'result' => $result, + ], + 'command_result' + ); } catch (Throwable $throwable) { - $this->http->post('/edge-agent/gateways/' . $gatewayId . '/commands/' . $jobId . '/result', [ - 'agent_token' => (string)$this->config->get('agentToken'), - 'ok' => false, - 'error' => $throwable->getMessage(), - ]); + $this->sendControlPlaneEvent( + '/edge-agent/gateways/' . $gatewayId . '/commands/' . $jobId . '/result', + [ + 'agent_token' => (string)$this->config->get('agentToken'), + 'ok' => false, + 'error' => $throwable->getMessage(), + ], + 'command_result' + ); } } private function processManagementOperation(): bool { $gatewayId = (int)$this->config->get('gatewayId'); - $response = $this->http->post('/edge-agent/gateways/' . $gatewayId . '/operations/next', [ - 'agent_token' => (string)$this->config->get('agentToken'), - 'wait_seconds' => (int)$this->config->get('operationPollTimeoutSeconds', 20), - 'agent_instance_id' => $this->agentInstanceId, - ]); + if ($gatewayId <= 0) { + return false; + } + + try { + $response = $this->http->post('/edge-agent/gateways/' . $gatewayId . '/operations/next', [ + 'agent_token' => (string)$this->config->get('agentToken'), + 'wait_seconds' => (int)$this->config->get('operationPollTimeoutSeconds', 20), + 'agent_instance_id' => $this->agentInstanceId, + ]); + } catch (Throwable $throwable) { + $this->logger->warning('Operation polling failed: ' . $throwable->getMessage()); + return false; + } + $operation = $response['data'] ?? null; if (!is_array($operation) || empty($operation['id'])) { return false; @@ -363,7 +562,7 @@ final class TruckwashEdgeAgent $gatewayId, $operationId, 'OPERATION_AGENT_STARTED', - 'PHP edge agent started processing the operation', + 'Compose edge-agent started processing the operation', 10, ['type' => $type, 'agent_instance_id' => $this->agentInstanceId], 'starting' @@ -377,11 +576,15 @@ final class TruckwashEdgeAgent default => throw new RuntimeException('Unsupported operation type: ' . $type), }; - $this->http->post('/edge-agent/gateways/' . $gatewayId . '/operations/' . $operationId . '/complete', [ - 'agent_token' => (string)$this->config->get('agentToken'), - 'ok' => true, - 'result' => $result, - ]); + $this->sendControlPlaneEvent( + '/edge-agent/gateways/' . $gatewayId . '/operations/' . $operationId . '/complete', + [ + 'agent_token' => (string)$this->config->get('agentToken'), + 'ok' => true, + 'result' => $result, + ], + 'operation_complete' + ); $this->snapshotOperationState('COMPLETED', ['result' => $result]); $this->clearOperationState(); } catch (Throwable $throwable) { @@ -393,12 +596,16 @@ final class TruckwashEdgeAgent 'context' => ['type' => $type, 'progress' => 100, 'agent_instance_id' => $this->agentInstanceId], ]); - $this->http->post('/edge-agent/gateways/' . $gatewayId . '/operations/' . $operationId . '/complete', [ - 'agent_token' => (string)$this->config->get('agentToken'), - 'ok' => false, - 'error_code' => $errorCode, - 'error_message' => $throwable->getMessage(), - ]); + $this->sendControlPlaneEvent( + '/edge-agent/gateways/' . $gatewayId . '/operations/' . $operationId . '/complete', + [ + 'agent_token' => (string)$this->config->get('agentToken'), + 'ok' => false, + 'error_code' => $errorCode, + 'error_message' => $throwable->getMessage(), + ], + 'operation_complete' + ); $this->snapshotOperationState('FAILED', [ 'error_code' => $errorCode, 'error_message' => $throwable->getMessage(), @@ -411,13 +618,17 @@ final class TruckwashEdgeAgent private function postOperationEvent(int $gatewayId, int $operationId, array $payload): void { - $this->http->post('/edge-agent/gateways/' . $gatewayId . '/operations/' . $operationId . '/events', [ - 'agent_token' => (string)$this->config->get('agentToken'), - 'level' => (string)($payload['level'] ?? 'INFO'), - 'code' => $payload['code'] ?? null, - 'message' => (string)($payload['message'] ?? 'Operation event'), - 'context' => is_array($payload['context'] ?? null) ? (array)$payload['context'] : [], - ]); + $this->sendControlPlaneEvent( + '/edge-agent/gateways/' . $gatewayId . '/operations/' . $operationId . '/events', + [ + 'agent_token' => (string)$this->config->get('agentToken'), + 'level' => (string)($payload['level'] ?? 'INFO'), + 'code' => $payload['code'] ?? null, + 'message' => (string)($payload['message'] ?? 'Operation event'), + 'context' => is_array($payload['context'] ?? null) ? (array)$payload['context'] : [], + ], + 'operation_event' + ); } private function emitOperationProgress( @@ -454,34 +665,50 @@ final class TruckwashEdgeAgent private function runDiscovery(): array { - return ['inventory' => []]; + try { + $response = $this->workerHttp->post('/discover', [ + 'hostname' => gethostname() ?: 'truckwash-edge', + 'gateway_id' => (int)$this->config->get('gatewayId', 0), + ], 20); + $inventory = isset($response['inventory']) && is_array($response['inventory']) ? (array)$response['inventory'] : []; + return ['inventory' => $inventory]; + } catch (Throwable) { + $hostname = gethostname() ?: 'truckwash-edge'; + return ['inventory' => [[ + 'device_id' => 'gateway-runtime-' . substr(sha1($hostname), 0, 10), + 'local_ip' => gethostbyname($hostname), + 'model' => 'TruckWash Edge Gateway', + 'channel_count' => 1, + 'online' => true, + 'capabilities' => [ + 'local_discovery' => true, + 'relay_commands' => true, + 'gateway_management_v2' => true, + ], + 'metadata' => [ + 'hostname' => $hostname, + 'runtime' => 'compose-php', + 'php_version' => PHP_VERSION, + ], + ]]]; + } } private function runManagementDiscovery(int $gatewayId, int $operationId, array $request): array { - $this->emitOperationProgress($gatewayId, $operationId, 'DISCOVERY_COLLECTING', 'Collecting local gateway inventory', 35, [], 'discovering'); - $hostname = gethostname() ?: 'truckwash-edge'; - $fallbackInventory = [[ - 'device_id' => 'gateway-runtime-' . substr(sha1($hostname), 0, 10), - 'local_ip' => gethostbyname($hostname), - 'model' => 'TruckWash Edge Gateway', - 'channel_count' => 1, - 'online' => true, - 'capabilities' => [ - 'local_discovery' => true, - 'relay_commands' => true, - 'gateway_management_v2' => true, - ], - 'metadata' => [ - 'hostname' => $hostname, - 'runtime' => 'php-cli', - 'php_version' => PHP_VERSION, - ], - ]]; - + $this->emitOperationProgress( + $gatewayId, + $operationId, + 'DISCOVERY_COLLECTING', + 'Collecting local gateway inventory from lan-worker', + 35, + [], + 'discovering' + ); $inventory = isset($request['inventory']) && is_array($request['inventory']) && $request['inventory'] !== [] ? (array)$request['inventory'] - : $fallbackInventory; + : (array)($this->runDiscovery()['inventory'] ?? []); + $this->emitOperationProgress($gatewayId, $operationId, 'DISCOVERY_COMPLETED', 'Inventory collected', 90, [ 'device_count' => count($inventory), ], 'finishing'); @@ -496,59 +723,158 @@ final class TruckwashEdgeAgent throw new RuntimeException('Update request is missing target version'); } - $agentArtifactUrl = trim((string)($request['artifactUrl'] ?? '')); - $serviceUnitUrl = trim((string)($request['serviceUnitUrl'] ?? '')); - $agentArtifactSha = trim((string)($request['artifactSha256'] ?? '')); - $serviceUnitSha = trim((string)($request['serviceUnitSha256'] ?? '')); + $updateWindow = trim((string)($request['updateWindow'] ?? $this->config->get('updateWindow', self::DEFAULT_UPDATE_WINDOW))); + if ($updateWindow === '') { + $updateWindow = self::DEFAULT_UPDATE_WINDOW; + } - $this->emitOperationProgress($gatewayId, $operationId, 'UPDATE_VALIDATED', 'Validated update request', 20, [ + $requiredArtifacts = [ + [ + 'url' => (string)($request['artifactUrl'] ?? ''), + 'sha256' => (string)($request['artifactSha256'] ?? ''), + 'path' => $this->installDir . DIRECTORY_SEPARATOR . 'agent.php', + 'label' => 'edge-agent runtime', + 'progress' => 35, + 'code' => 'UPDATE_DOWNLOAD_AGENT', + ], + [ + 'url' => (string)($request['lanWorkerArtifactUrl'] ?? ''), + 'sha256' => (string)($request['lanWorkerArtifactSha256'] ?? ''), + 'path' => $this->installDir . DIRECTORY_SEPARATOR . 'lan-worker.php', + 'label' => 'lan-worker runtime', + 'progress' => 45, + 'code' => 'UPDATE_DOWNLOAD_WORKER', + ], + [ + 'url' => (string)($request['composeFileUrl'] ?? ''), + 'sha256' => (string)($request['composeFileSha256'] ?? ''), + 'path' => $this->installDir . DIRECTORY_SEPARATOR . 'docker-compose.gateway.yml', + 'label' => 'compose stack', + 'progress' => 55, + 'code' => 'UPDATE_DOWNLOAD_COMPOSE', + ], + [ + 'url' => (string)($request['edgeAgentDockerfileUrl'] ?? ''), + 'sha256' => (string)($request['edgeAgentDockerfileSha256'] ?? ''), + 'path' => $this->installDir . DIRECTORY_SEPARATOR . 'Dockerfile.edge-agent', + 'label' => 'edge-agent Dockerfile', + 'progress' => 60, + 'code' => 'UPDATE_DOWNLOAD_EDGE_DOCKERFILE', + ], + [ + 'url' => (string)($request['lanWorkerDockerfileUrl'] ?? ''), + 'sha256' => (string)($request['lanWorkerDockerfileSha256'] ?? ''), + 'path' => $this->installDir . DIRECTORY_SEPARATOR . 'Dockerfile.lan-worker', + 'label' => 'lan-worker Dockerfile', + 'progress' => 65, + 'code' => 'UPDATE_DOWNLOAD_WORKER_DOCKERFILE', + ], + [ + 'url' => (string)($request['launcherScriptUrl'] ?? ''), + 'sha256' => (string)($request['launcherScriptSha256'] ?? ''), + 'path' => $this->installDir . DIRECTORY_SEPARATOR . 'gateway-launcher.sh', + 'label' => 'gateway launcher', + 'progress' => 72, + 'code' => 'UPDATE_DOWNLOAD_LAUNCHER', + ], + [ + 'url' => (string)($request['stackServiceUnitUrl'] ?? ''), + 'sha256' => (string)($request['stackServiceUnitSha256'] ?? ''), + 'path' => $this->installDir . DIRECTORY_SEPARATOR . 'truckwash-edge-gateway-stack.service', + 'label' => 'compose systemd unit', + 'progress' => 80, + 'code' => 'UPDATE_DOWNLOAD_STACK_SERVICE', + ], + [ + 'url' => (string)($request['serviceUnitUrl'] ?? ''), + 'sha256' => (string)($request['serviceUnitSha256'] ?? ''), + 'path' => $this->installDir . DIRECTORY_SEPARATOR . 'truckwash-edge-agent.service', + 'label' => 'compatibility systemd unit', + 'progress' => 84, + 'code' => 'UPDATE_DOWNLOAD_LEGACY_SERVICE', + ], + ]; + + foreach (['composeFileUrl', 'launcherScriptUrl', 'stackServiceUnitUrl'] as $requiredKey) { + if (trim((string)($request[$requiredKey] ?? '')) === '') { + throw new RuntimeException('Update request is missing compose artifact ' . $requiredKey); + } + } + + $this->emitOperationProgress($gatewayId, $operationId, 'UPDATE_VALIDATED', 'Validated compose update request', 20, [ 'target_version' => $targetVersion, + 'update_window' => $updateWindow, ], 'validating'); $changedFiles = []; - if ($agentArtifactUrl !== '') { - $this->emitOperationProgress($gatewayId, $operationId, 'UPDATE_DOWNLOAD_AGENT', 'Downloading PHP edge agent artifact', 40, [ - 'url' => $agentArtifactUrl, - ], 'downloading-agent'); + foreach ($requiredArtifacts as $artifact) { + if ($artifact['url'] === '') { + continue; + } + + $this->emitOperationProgress( + $gatewayId, + $operationId, + (string)$artifact['code'], + 'Downloading ' . (string)$artifact['label'], + (int)$artifact['progress'], + ['url' => (string)$artifact['url']], + 'downloading' + ); $changedFiles[] = $this->downloadToFileAtomic( - $agentArtifactUrl, - $this->installDir . DIRECTORY_SEPARATOR . 'agent.php', - $agentArtifactSha + (string)$artifact['url'], + (string)$artifact['path'], + (string)$artifact['sha256'] ); } - if ($serviceUnitUrl !== '') { - $this->emitOperationProgress($gatewayId, $operationId, 'UPDATE_DOWNLOAD_SERVICE', 'Downloading service unit', 65, [ - 'url' => $serviceUnitUrl, - ], 'downloading-service'); - $changedFiles[] = $this->downloadToFileAtomic( - $serviceUnitUrl, - $this->installDir . DIRECTORY_SEPARATOR . 'truckwash-edge-agent.service', - $serviceUnitSha - ); - } + $this->ensureExecutable($this->installDir . DIRECTORY_SEPARATOR . 'agent.php'); + $this->ensureExecutable($this->installDir . DIRECTORY_SEPARATOR . 'lan-worker.php'); + $this->ensureExecutable($this->installDir . DIRECTORY_SEPARATOR . 'gateway-launcher.sh'); - $this->emitOperationProgress($gatewayId, $operationId, 'UPDATE_WRITE_CONFIG', 'Writing staged update metadata', 85, [], 'writing-config'); - $this->config->set('installedVersion', $targetVersion); - $this->config->set('targetVersion', $targetVersion); - $this->config->set('lastStagedUpdate', [ + $stagedAt = date('c'); + $stagedUpdate = [ 'target_version' => $targetVersion, - 'staged_at' => date('c'), + 'staged_at' => $stagedAt, + 'apply_after' => $this->nextUpdateWindowStartIso($updateWindow), + 'status' => 'STAGED', + 'update_window' => $updateWindow, + 'compose_project_name' => (string)($request['composeProjectName'] ?? $this->config->get('composeProjectName', 'truckwash-edge-gateway')), + 'stack_service_name' => (string)($request['stackServiceName'] ?? $this->config->get('stackServiceName', 'truckwash-edge-gateway-stack.service')), 'changed_files' => $changedFiles, - ]); - $this->config->save(); + ]; - $this->emitOperationProgress($gatewayId, $operationId, 'UPDATE_STAGED', 'Artifacts staged successfully', 95, [ - 'changed_files' => $changedFiles, + $this->emitOperationProgress($gatewayId, $operationId, 'UPDATE_STAGE_METADATA', 'Writing staged update metadata', 90, [], 'writing-config'); + $this->config->set('targetVersion', $targetVersion); + $this->config->set('lastStagedUpdate', $stagedUpdate); + $this->config->save(); + $this->stateStore->setJson('staged_update', $stagedUpdate); + $this->stateStore->setJson('rollback_status', [ + 'state' => 'IDLE', + 'reason' => null, + 'rolled_back_to' => null, + 'at' => null, + ]); + file_put_contents($this->stagedUpdatePath, json_encode($stagedUpdate, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL); + + $this->emitOperationProgress($gatewayId, $operationId, 'UPDATE_STAGED', 'Compose rollout staged for the maintenance window', 96, [ + 'target_version' => $targetVersion, + 'apply_after' => $stagedUpdate['apply_after'], ], 'staged'); return [ - 'installed_version' => $targetVersion, + 'applied' => false, + 'installed_version' => (string)$this->config->get('installedVersion', 'compose-php-agent-v2'), + 'staged_version' => $targetVersion, 'target_version' => $targetVersion, + 'staged_at' => $stagedAt, + 'apply_after' => $stagedUpdate['apply_after'], + 'update_window' => $updateWindow, 'restart_required' => true, - 'service_name' => (string)$this->config->get('serviceName', 'truckwash-edge-agent.service'), + 'service_name' => (string)($request['stackServiceName'] ?? $this->config->get('stackServiceName', 'truckwash-edge-gateway-stack.service')), 'changed_files' => $changedFiles, 'agent_instance_id' => $this->agentInstanceId, + 'rollback_status' => $this->readRollbackStatus(), ]; } @@ -557,7 +883,7 @@ final class TruckwashEdgeAgent $this->emitOperationProgress($gatewayId, $operationId, 'UNINSTALL_PREPARE', 'Preparing uninstall manifest', 40, [], 'preparing-uninstall'); $manifestPath = $this->runtimeDir . DIRECTORY_SEPARATOR . 'uninstall-plan.json'; $manifest = [ - 'service_name' => (string)($request['service_name'] ?? $this->config->get('serviceName', 'truckwash-edge-agent.service')), + 'service_name' => (string)($request['service_name'] ?? $this->config->get('stackServiceName', 'truckwash-edge-gateway-stack.service')), 'install_dir' => (string)($request['install_dir'] ?? $this->installDir), 'generated_at' => date('c'), 'agent_instance_id' => $this->agentInstanceId, @@ -584,7 +910,15 @@ final class TruckwashEdgeAgent { $localIp = (string)($request['localIp'] ?? $request['local_ip'] ?? ''); $channel = (int)($request['channel'] ?? 0); - return $this->fetchShellyState($localIp, $channel); + + try { + return $this->workerHttp->post('/relay/status', [ + 'local_ip' => $localIp, + 'channel' => $channel, + ], 8) ?? []; + } catch (Throwable) { + return $this->fetchShellyState($localIp, $channel); + } } private function switchRelay(array $request): array @@ -593,15 +927,23 @@ final class TruckwashEdgeAgent $channel = (int)($request['channel'] ?? 0); $on = (bool)($request['on'] ?? false); - $rpcUrl = sprintf('http://%s/rpc/Switch.Set?id=%d&on=%s', $localIp, $channel, $on ? 'true' : 'false'); try { - $this->http->getJson($rpcUrl, 8); + return $this->workerHttp->post('/relay/switch', [ + 'local_ip' => $localIp, + 'channel' => $channel, + 'on' => $on, + ], 8) ?? []; } catch (Throwable) { - $legacyUrl = sprintf('http://%s/relay/%d?turn=%s', $localIp, $channel, $on ? 'on' : 'off'); - $this->http->getJson($legacyUrl, 8); - } + $rpcUrl = sprintf('http://%s/rpc/Switch.Set?id=%d&on=%s', $localIp, $channel, $on ? 'true' : 'false'); + try { + $this->http->getJson($rpcUrl, 8); + } catch (Throwable) { + $legacyUrl = sprintf('http://%s/relay/%d?turn=%s', $localIp, $channel, $on ? 'on' : 'off'); + $this->http->getJson($legacyUrl, 8); + } - return $this->fetchShellyState($localIp, $channel); + return $this->fetchShellyState($localIp, $channel); + } } private function downloadToFileAtomic(string $url, string $path, string $expectedSha256 = ''): array @@ -690,6 +1032,153 @@ final class TruckwashEdgeAgent } } + private function buildSystemMetrics(): array + { + $diskTotal = @disk_total_space($this->installDir); + $diskFree = @disk_free_space($this->installDir); + $diskUsed = (is_numeric($diskTotal) && is_numeric($diskFree)) ? ((float)$diskTotal - (float)$diskFree) : null; + $diskUsagePct = ($diskTotal && $diskUsed !== null && $diskTotal > 0) + ? (int)round(($diskUsed / (float)$diskTotal) * 100) + : null; + $memoryLimitBytes = $this->iniBytes((string)ini_get('memory_limit')); + $memoryUsage = memory_get_usage(true); + $memoryUsagePct = ($memoryLimitBytes !== null && $memoryLimitBytes > 0) + ? (int)round(($memoryUsage / $memoryLimitBytes) * 100) + : null; + $loadAverage = function_exists('sys_getloadavg') ? sys_getloadavg() : []; + + return [ + 'memory_usage_bytes' => $memoryUsage, + 'memory_peak_bytes' => memory_get_peak_usage(true), + 'memory_usage_pct' => $memoryUsagePct, + 'cpu_usage_pct' => is_array($loadAverage) && isset($loadAverage[0]) ? max(0, (int)round((float)$loadAverage[0] * 100)) : null, + 'load_average' => $loadAverage, + 'disk_usage_pct' => $diskUsagePct, + 'disk_used_bytes' => $diskUsed, + 'disk_total_bytes' => is_numeric($diskTotal) ? (int)$diskTotal : null, + 'disk_mount' => $this->installDir, + 'latency_ms' => null, + ]; + } + + private function buildContainerHealth(): array + { + $services = [[ + 'name' => 'edge-agent', + 'status' => 'healthy', + 'updated_at' => date('c'), + ]]; + + try { + $workerHealth = $this->workerHttp->getJson(rtrim((string)$this->config->get('workerBaseUrl', self::DEFAULT_WORKER_BASE_URL), '/') . '/health', 5); + $services[] = [ + 'name' => 'lan-worker', + 'status' => (string)($workerHealth['status'] ?? 'healthy'), + 'updated_at' => (string)($workerHealth['timestamp'] ?? date('c')), + ]; + } catch (Throwable $throwable) { + $services[] = [ + 'name' => 'lan-worker', + 'status' => 'degraded', + 'updated_at' => date('c'), + 'error' => $throwable->getMessage(), + ]; + } + + $healthyCount = count(array_filter($services, static fn(array $service): bool => (string)($service['status'] ?? '') === 'healthy')); + $state = $healthyCount === count($services) ? 'ONLINE' : 'DEGRADED'; + + return [ + 'state' => $state, + 'summary' => sprintf('%d/%d containers healthy', $healthyCount, count($services)), + 'services' => $services, + ]; + } + + private function buildOutboxStatus(): array + { + $summary = $this->stateStore->outboxSummary(); + $queued = (int)($summary['queued'] ?? 0); + return [ + 'state' => $queued > 0 ? 'QUEUED' : 'IN_SYNC', + 'queued' => $queued, + 'oldest_queued_at' => $summary['oldest_queued_at'] ?? null, + 'last_replayed_at' => $summary['last_replayed_at'] ?? null, + 'summary' => $queued > 0 ? sprintf('%d outbound items queued', $queued) : 'Outbox is empty', + ]; + } + + private function flushOutbox(): void + { + $items = $this->stateStore->queuedItems(25); + foreach ($items as $item) { + try { + $this->http->post((string)$item['endpoint'], is_array($item['payload'] ?? null) ? (array)$item['payload'] : [], 20); + $this->stateStore->removeOutboxItem((int)$item['id']); + $this->stateStore->setJson('last_sync_at', date('c')); + } catch (Throwable $throwable) { + $this->logger->warning('Outbox replay blocked on ' . (string)$item['type'] . ': ' . $throwable->getMessage()); + break; + } + } + } + + private function sendControlPlaneEvent(string $endpoint, array $payload, string $type): bool + { + try { + $this->http->post($endpoint, $payload, 20); + $this->stateStore->setJson('last_sync_at', date('c')); + return true; + } catch (Throwable $throwable) { + $this->stateStore->enqueue($type, $endpoint, $payload); + $this->logger->warning('Queued ' . $type . ' to local outbox after transport failure: ' . $throwable->getMessage()); + return false; + } + } + + private function readRollbackStatus(): array + { + $rollbackPath = $this->runtimeDir . DIRECTORY_SEPARATOR . 'rollback-status.json'; + if (is_file($rollbackPath)) { + $decoded = json_decode((string)file_get_contents($rollbackPath), true); + if (is_array($decoded)) { + return $decoded; + } + } + + $rollback = $this->stateStore->getJson('rollback_status', null); + if (is_array($rollback)) { + return $rollback; + } + + return [ + 'state' => 'IDLE', + 'reason' => null, + 'rolled_back_to' => null, + 'at' => null, + ]; + } + + private function currentStagedUpdate(): ?array + { + if (is_file($this->stagedUpdatePath)) { + $decoded = json_decode((string)file_get_contents($this->stagedUpdatePath), true); + if (is_array($decoded)) { + return $decoded; + } + } + + $staged = $this->stateStore->getJson('staged_update', null); + return is_array($staged) ? $staged : null; + } + + private function ensureExecutable(string $path): void + { + if (is_file($path)) { + @chmod($path, 0755); + } + } + private function ensureAgentInstanceId(): string { $configured = trim((string)$this->config->get('agentInstanceId', '')); @@ -752,19 +1241,61 @@ final class TruckwashEdgeAgent $decoded = json_decode((string)file_get_contents($this->statePath), true); return is_array($decoded) ? $decoded : null; } + + private function nextUpdateWindowStartIso(string $window): string + { + $parts = explode('-', $window, 2); + $start = trim((string)($parts[0] ?? '02:00')); + if (!preg_match('/^\d{2}:\d{2}$/', $start)) { + $start = '02:00'; + } + + $now = new DateTimeImmutable('now'); + [$hour, $minute] = array_map('intval', explode(':', $start)); + $candidate = $now->setTime($hour, $minute, 0); + if ($candidate <= $now) { + $candidate = $candidate->modify('+1 day'); + } + + return $candidate->format(DateTimeInterface::ATOM); + } + + private function reloadConfigFromDisk(): void + { + $reloaded = AgentConfig::load($this->config->path); + $this->config = $reloaded; + $this->workerHttp = new HttpJsonClient((string)$this->config->get('workerBaseUrl', self::DEFAULT_WORKER_BASE_URL)); + } + + private function iniBytes(string $value): ?int + { + $normalized = trim(strtolower($value)); + if ($normalized === '' || $normalized === '-1') { + return null; + } + + $unit = substr($normalized, -1); + $number = (float)$normalized; + return match ($unit) { + 'g' => (int)round($number * 1024 * 1024 * 1024), + 'm' => (int)round($number * 1024 * 1024), + 'k' => (int)round($number * 1024), + default => is_numeric($normalized) ? (int)$normalized : null, + }; + } } $configPath = null; foreach ($argv as $index => $argument) { if ($argument === '--config' && isset($argv[$index + 1])) { $configPath = $argv[$index + 1]; - break; } } -if (!is_string($configPath) || trim($configPath) === '') { - fwrite(STDERR, "Usage: agent.php --config /path/to/config.json\n"); +if ($configPath === null) { + fwrite(STDERR, "Usage: php agent.php --config /path/to/config.json\n"); exit(1); } -(new TruckwashEdgeAgent($configPath))->run(); +$agent = new TruckwashEdgeAgent($configPath); +$agent->run(); diff --git a/services/nginx/app/resources/edge-gateway-agent/docker-compose.gateway.yml b/services/nginx/app/resources/edge-gateway-agent/docker-compose.gateway.yml new file mode 100644 index 00000000..c6f27cb2 --- /dev/null +++ b/services/nginx/app/resources/edge-gateway-agent/docker-compose.gateway.yml @@ -0,0 +1,38 @@ +services: + lan-worker: + build: + context: . + dockerfile: Dockerfile.lan-worker + args: + BASE_IMAGE: ${LAN_WORKER_BASE_IMAGE:-php:8.2-cli-bookworm} + container_name: truckwash-lan-worker + restart: unless-stopped + ports: + - "127.0.0.1:8090:8090" + volumes: + - ./runtime:/opt/truckwash-edge-agent/runtime + healthcheck: + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8090/health"] + interval: 30s + timeout: 5s + retries: 3 + + edge-agent: + build: + context: . + dockerfile: Dockerfile.edge-agent + args: + BASE_IMAGE: ${EDGE_AGENT_BASE_IMAGE:-php:8.2-cli-bookworm} + container_name: truckwash-edge-agent + restart: unless-stopped + depends_on: + lan-worker: + condition: service_healthy + volumes: + - ./config.json:/config/config.json + - ./runtime:/opt/truckwash-edge-agent/runtime + healthcheck: + test: ["CMD-SHELL", "test -f /opt/truckwash-edge-agent/runtime/last-heartbeat-ok.txt"] + interval: 30s + timeout: 5s + retries: 3 diff --git a/services/nginx/app/resources/edge-gateway-agent/gateway-launcher.sh b/services/nginx/app/resources/edge-gateway-agent/gateway-launcher.sh new file mode 100644 index 00000000..2108186d --- /dev/null +++ b/services/nginx/app/resources/edge-gateway-agent/gateway-launcher.sh @@ -0,0 +1,194 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +ACTION="${1:-up}" +INSTALL_DIR="${TRUCKWASH_INSTALL_DIR:-/opt/truckwash-edge-agent}" +CONFIG_PATH="$INSTALL_DIR/config.json" +COMPOSE_FILE="$INSTALL_DIR/docker-compose.gateway.yml" +RUNTIME_DIR="$INSTALL_DIR/runtime" +ROLLBACK_STATUS_PATH="$RUNTIME_DIR/rollback-status.json" +STAGED_UPDATE_PATH="$RUNTIME_DIR/staged-update.json" + +log() { + printf '[gateway-launcher] %s\n' "$1" +} + +compose_cmd() { + if docker compose version >/dev/null 2>&1; then + docker compose "$@" + return + fi + + if command -v docker-compose >/dev/null 2>&1; then + docker-compose "$@" + return + fi + + echo "Docker Compose is required." >&2 + return 1 +} + +config_value() { + local key="$1" + local fallback="${2:-}" + php -r ' + $path = $argv[1]; + $key = $argv[2]; + $fallback = $argv[3]; + if (!is_file($path)) { + echo $fallback; + exit(0); + } + $decoded = json_decode((string)file_get_contents($path), true); + if (!is_array($decoded) || !array_key_exists($key, $decoded) || $decoded[$key] === null || $decoded[$key] === "") { + echo $fallback; + exit(0); + } + echo is_scalar($decoded[$key]) ? (string)$decoded[$key] : json_encode($decoded[$key], JSON_UNESCAPED_SLASHES); + ' "$CONFIG_PATH" "$key" "$fallback" +} + +ensure_dirs() { + mkdir -p "$RUNTIME_DIR" "$RUNTIME_DIR/backups" +} + +within_update_window() { + local window + window="$(config_value updateWindow '02:00-04:00')" + php -r ' + $window = $argv[1]; + [$start, $end] = array_pad(explode("-", $window, 2), 2, ""); + $parse = static function (string $value): ?int { + if (!preg_match("/^(\d{2}):(\d{2})$/", trim($value), $matches)) { + return null; + } + return ((int)$matches[1] * 60) + (int)$matches[2]; + }; + $startMinutes = $parse($start); + $endMinutes = $parse($end); + if ($startMinutes === null || $endMinutes === null) { + exit(1); + } + $nowMinutes = ((int)date("G") * 60) + (int)date("i"); + if ($startMinutes <= $endMinutes) { + exit(($nowMinutes >= $startMinutes && $nowMinutes <= $endMinutes) ? 0 : 1); + } + exit(($nowMinutes >= $startMinutes || $nowMinutes <= $endMinutes) ? 0 : 1); + ' "$window" +} + +write_rollback_status() { + local state="$1" + local reason="${2:-}" + local rolled_back_to="${3:-}" + cat > "$ROLLBACK_STATUS_PATH" </dev/null +} + +reconcile_stack() { + ensure_dirs + log "Reconciling compose stack" + apply_stack + sleep 5 + + if ! healthcheck_stack; then + log "Healthcheck failed after compose rollout; reverting to previous artifacts" + rollback_stack + return 1 + fi + + write_rollback_status "IDLE" "" "" + if [ -f "$STAGED_UPDATE_PATH" ] && within_update_window; then + php -r ' + $configPath = $argv[1]; + $stagedPath = $argv[2]; + $config = is_file($configPath) ? json_decode((string)file_get_contents($configPath), true) : []; + $staged = is_file($stagedPath) ? json_decode((string)file_get_contents($stagedPath), true) : []; + if (!is_array($config) || !is_array($staged)) { + exit(0); + } + $targetVersion = trim((string)($staged["target_version"] ?? "")); + if ($targetVersion !== "") { + $config["installedVersion"] = $targetVersion; + $config["targetVersion"] = $targetVersion; + file_put_contents($configPath, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL); + $staged["status"] = "APPLIED"; + $staged["applied_at"] = date(DATE_ATOM); + file_put_contents($stagedPath, json_encode($staged, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL); + } + ' "$CONFIG_PATH" "$STAGED_UPDATE_PATH" + fi +} + +case "$ACTION" in + up) + reconcile_stack + ;; + reconcile) + if [ -f "$STAGED_UPDATE_PATH" ] && ! within_update_window; then + log "Update is staged but outside the maintenance window; keeping current stack running" + exit 0 + fi + reconcile_stack + ;; + down) + cd "$INSTALL_DIR" + compose_cmd -f "$COMPOSE_FILE" down + ;; + *) + echo "Usage: gateway-launcher.sh [up|reconcile|down]" >&2 + exit 1 + ;; +esac diff --git a/services/nginx/app/resources/edge-gateway-agent/lan-worker.php b/services/nginx/app/resources/edge-gateway-agent/lan-worker.php new file mode 100644 index 00000000..e3e7ed9e --- /dev/null +++ b/services/nginx/app/resources/edge-gateway-agent/lan-worker.php @@ -0,0 +1,146 @@ + true, + CURLOPT_TIMEOUT => $timeoutSeconds, + CURLOPT_CONNECTTIMEOUT => min(5, $timeoutSeconds), + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_HTTPHEADER => ['Accept: application/json'], + ]); + + $raw = curl_exec($ch); + $status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE); + $error = curl_error($ch); + curl_close($ch); + + if ($raw === false || $status >= 400) { + throw new RuntimeException($error !== '' ? $error : 'HTTP ' . $status); + } + + $decoded = json_decode((string)$raw, true); + if (!is_array($decoded)) { + throw new RuntimeException('Invalid JSON response from device'); + } + + return $decoded; +} + +function worker_fetch_shelly_state(string $localIp, int $channel): array +{ + if ($localIp === '') { + throw new RuntimeException('Missing Shelly IP address'); + } + + try { + $payload = worker_http_get_json(sprintf('http://%s/rpc/Switch.GetStatus?id=%d', $localIp, $channel)); + return [ + 'online' => true, + 'on' => (bool)($payload['output'] ?? false), + 'output' => (bool)($payload['output'] ?? false), + 'raw' => $payload, + ]; + } catch (Throwable) { + $payload = worker_http_get_json(sprintf('http://%s/relay/%d', $localIp, $channel)); + return [ + 'online' => true, + 'on' => (bool)($payload['ison'] ?? false), + 'output' => (bool)($payload['ison'] ?? false), + 'raw' => $payload, + ]; + } +} + +function worker_switch_shelly_state(string $localIp, int $channel, bool $on): array +{ + try { + worker_http_get_json(sprintf('http://%s/rpc/Switch.Set?id=%d&on=%s', $localIp, $channel, $on ? 'true' : 'false')); + } catch (Throwable) { + worker_http_get_json(sprintf('http://%s/relay/%d?turn=%s', $localIp, $channel, $on ? 'on' : 'off')); + } + + return worker_fetch_shelly_state($localIp, $channel); +} + +$method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')); +$path = (string)(parse_url((string)($_SERVER['REQUEST_URI'] ?? '/'), PHP_URL_PATH) ?? '/'); +$body = worker_read_json_body(); +$hostname = gethostname() ?: 'truckwash-edge'; + +try { + if ($method === 'GET' && $path === '/health') { + worker_json_response(200, [ + 'status' => 'healthy', + 'service' => 'lan-worker', + 'timestamp' => date(DateTimeInterface::ATOM), + ]); + return; + } + + if ($method === 'POST' && $path === '/discover') { + worker_json_response(200, [ + 'inventory' => [[ + 'device_id' => 'gateway-runtime-' . substr(sha1($hostname), 0, 10), + 'local_ip' => gethostbyname($hostname), + 'model' => 'TruckWash Edge Gateway', + 'channel_count' => 1, + 'online' => true, + 'capabilities' => [ + 'local_discovery' => true, + 'relay_commands' => true, + 'gateway_management_v2' => true, + ], + 'metadata' => [ + 'hostname' => $hostname, + 'runtime' => 'compose-lan-worker', + 'php_version' => PHP_VERSION, + ], + ]], + ]); + return; + } + + if ($method === 'POST' && $path === '/relay/status') { + $localIp = trim((string)($body['local_ip'] ?? $body['localIp'] ?? '')); + $channel = (int)($body['channel'] ?? 0); + worker_json_response(200, worker_fetch_shelly_state($localIp, $channel)); + return; + } + + if ($method === 'POST' && $path === '/relay/switch') { + $localIp = trim((string)($body['local_ip'] ?? $body['localIp'] ?? '')); + $channel = (int)($body['channel'] ?? 0); + $on = (bool)($body['on'] ?? false); + worker_json_response(200, worker_switch_shelly_state($localIp, $channel, $on)); + return; + } + + worker_json_response(404, ['message' => 'Not found']); +} catch (Throwable $throwable) { + worker_json_response(422, [ + 'message' => $throwable->getMessage(), + 'error_code' => 'EDGE_GATEWAY_WORKER_FAILED', + ]); +} diff --git a/services/nginx/app/resources/edge-gateway-agent/truckwash-edge-gateway-stack.service b/services/nginx/app/resources/edge-gateway-agent/truckwash-edge-gateway-stack.service new file mode 100644 index 00000000..77012ad1 --- /dev/null +++ b/services/nginx/app/resources/edge-gateway-agent/truckwash-edge-gateway-stack.service @@ -0,0 +1,16 @@ +[Unit] +Description=TruckWash Edge Gateway Compose Stack +After=network-online.target docker.service +Wants=network-online.target docker.service + +[Service] +Type=oneshot +RemainAfterExit=yes +WorkingDirectory=/opt/truckwash-edge-agent +ExecStart=/opt/truckwash-edge-agent/gateway-launcher.sh up +ExecReload=/opt/truckwash-edge-agent/gateway-launcher.sh reconcile +ExecStop=/opt/truckwash-edge-agent/gateway-launcher.sh down +TimeoutStartSec=300 + +[Install] +WantedBy=multi-user.target diff --git a/services/nginx/app/routes/edgeGatewaysRoute.php b/services/nginx/app/routes/edgeGatewaysRoute.php index 084345df..075073ac 100644 --- a/services/nginx/app/routes/edgeGatewaysRoute.php +++ b/services/nginx/app/routes/edgeGatewaysRoute.php @@ -59,6 +59,12 @@ class edgeGatewaysRoute $this->get('/edge-agent/install-token/verify', fn() => $this->handleInstallTokenVerify()); $this->get('/edge-agent/install.sh', fn() => $this->renderInstallScript()); $this->get('/edge-agent/artifacts/agent.php', fn() => $this->renderArtifact('agent.php')); + $this->get('/edge-agent/artifacts/lan-worker.php', fn() => $this->renderArtifact('lan-worker.php')); + $this->get('/edge-agent/artifacts/docker-compose.gateway.yml', fn() => $this->renderArtifact('docker-compose.gateway.yml')); + $this->get('/edge-agent/artifacts/Dockerfile.edge-agent', fn() => $this->renderArtifact('Dockerfile.edge-agent')); + $this->get('/edge-agent/artifacts/Dockerfile.lan-worker', fn() => $this->renderArtifact('Dockerfile.lan-worker')); + $this->get('/edge-agent/artifacts/gateway-launcher.sh', fn() => $this->renderArtifact('gateway-launcher.sh')); + $this->get('/edge-agent/artifacts/truckwash-edge-gateway-stack.service', fn() => $this->renderArtifact('truckwash-edge-gateway-stack.service')); $this->get('/edge-agent/artifacts/truckwash-edge-agent.service', fn() => $this->renderArtifact('truckwash-edge-agent.service')); $this->post('/edge-agent/claim', fn() => $this->handleAgentClaim()); $this->post('/edge-agent/gateways/{id}/heartbeat', fn() => $this->handleAgentHeartbeat()); diff --git a/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayArtifactLocatorTest.php b/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayArtifactLocatorTest.php index f3596ea7..fdabbb71 100644 --- a/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayArtifactLocatorTest.php +++ b/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayArtifactLocatorTest.php @@ -94,9 +94,9 @@ it('prioritizes router resources before mounted and baked-in artifact directorie it('reads install artifacts through the shared locator', function (): void { $service = new EdgeGatewayInstallServiceHarness(); - $contents = $service->readArtifact('truckwash-edge-agent.service'); + $contents = $service->readArtifact('truckwash-edge-gateway-stack.service'); - expect($contents)->toContain('ExecStart=/usr/bin/php /opt/truckwash-edge-agent/agent.php'); + expect($contents)->toContain('ExecStart=/opt/truckwash-edge-agent/gateway-launcher.sh up'); }); it('builds update payloads with checksums from resolved artifact paths', function (): void { @@ -105,6 +105,9 @@ it('builds update payloads with checksums from resolved artifact paths', functio expect($payload['artifactSha256'])->toBe(hash_file('sha256', app_path('resources/edge-gateway-agent/agent.php'))); expect($payload['serviceUnitSha256'])->toBe(hash_file('sha256', app_path('resources/edge-gateway-agent/truckwash-edge-agent.service'))); + expect($payload['stackServiceUnitSha256'])->toBe(hash_file('sha256', app_path('resources/edge-gateway-agent/truckwash-edge-gateway-stack.service'))); + expect($payload['composeFileSha256'])->toBe(hash_file('sha256', app_path('resources/edge-gateway-agent/docker-compose.gateway.yml'))); + expect($payload['launcherScriptSha256'])->toBe(hash_file('sha256', app_path('resources/edge-gateway-agent/gateway-launcher.sh'))); }); it('falls back to baked-in artifacts when the mount path is absent', function (): void { diff --git a/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayManagerUrlTest.php b/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayManagerUrlTest.php index 23f8b9b8..394ab817 100644 --- a/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayManagerUrlTest.php +++ b/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayManagerUrlTest.php @@ -30,7 +30,7 @@ function with_edge_gateway_server_state(array $server, callable $callback): void } } -it('builds install script urls with the PHP agent artifacts and forwarded https scheme', function (): void { +it('builds install script urls with the compose edge gateway artifacts and forwarded https scheme', function (): void { with_edge_gateway_server_state([ 'HTTP_HOST' => 'api.truckwash.io:4433', 'HTTP_X_FORWARDED_PROTO' => 'https', @@ -44,12 +44,17 @@ it('builds install script urls with the PHP agent artifacts and forwarded https expect($manager->buildInstallCommand('abc123'))->toContain("https://api.truckwash.io:4433/edge-agent/install.sh?token=abc123"); expect($script)->toContain('fetch_http "Verify install token" "https://api.truckwash.io:4433/edge-agent/install-token/verify?token=abc123"'); expect($script)->toContain('fetch_http "Download PHP edge agent" "https://api.truckwash.io:4433/edge-agent/artifacts/agent.php" "$INSTALL_DIR/agent.php"'); - expect($script)->toContain('fetch_http "Download systemd service unit" "https://api.truckwash.io:4433/edge-agent/artifacts/truckwash-edge-agent.service" "$INSTALL_DIR/truckwash-edge-agent.service"'); + expect($script)->toContain('fetch_http "Download LAN worker" "https://api.truckwash.io:4433/edge-agent/artifacts/lan-worker.php" "$INSTALL_DIR/lan-worker.php"'); + expect($script)->toContain('fetch_http "Download compose stack" "https://api.truckwash.io:4433/edge-agent/artifacts/docker-compose.gateway.yml" "$INSTALL_DIR/docker-compose.gateway.yml"'); + expect($script)->toContain('fetch_http "Download compose stack service unit" "https://api.truckwash.io:4433/edge-agent/artifacts/truckwash-edge-gateway-stack.service" "$INSTALL_DIR/truckwash-edge-gateway-stack.service"'); expect($script)->toContain('Installer failed during step: ${CURRENT_STEP:-unknown}'); expect($script)->toContain('Last request: ${CURRENT_METHOD} ${CURRENT_URL}'); expect($script)->toContain('Response body preview (first 400 bytes):'); expect($script)->toContain('"apiUrl":"https://api.truckwash.io:4433"'); expect($script)->toContain('"serviceName":"truckwash-edge-agent.service"'); + expect($script)->toContain('"stackServiceName":"truckwash-edge-gateway-stack.service"'); + expect($script)->toContain('"composeFileName":"docker-compose.gateway.yml"'); + expect($script)->toContain('"stateDatabasePath":"/opt/truckwash-edge-agent/runtime/gateway-state.sqlite"'); expect($script)->toContain('"operationPollTimeoutSeconds":20'); expect($script)->not->toContain('"shellActionPollTimeoutSeconds"'); expect($script)->not->toContain('"brokerUrl"'); diff --git a/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayRouteWiringTest.php b/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayRouteWiringTest.php index ff6cd6cd..2e2c406f 100644 --- a/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayRouteWiringTest.php +++ b/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayRouteWiringTest.php @@ -25,6 +25,12 @@ it('registers PHP edge agent routes for operations and legacy relay command poll expect($route)->toContain("'/edge-agent/install-token/verify'"); expect($route)->toContain("'/edge-agent/install.sh'"); expect($route)->toContain("'/edge-agent/artifacts/agent.php'"); + expect($route)->toContain("'/edge-agent/artifacts/lan-worker.php'"); + expect($route)->toContain("'/edge-agent/artifacts/docker-compose.gateway.yml'"); + expect($route)->toContain("'/edge-agent/artifacts/Dockerfile.edge-agent'"); + expect($route)->toContain("'/edge-agent/artifacts/Dockerfile.lan-worker'"); + expect($route)->toContain("'/edge-agent/artifacts/gateway-launcher.sh'"); + expect($route)->toContain("'/edge-agent/artifacts/truckwash-edge-gateway-stack.service'"); expect($route)->toContain("'/edge-agent/artifacts/truckwash-edge-agent.service'"); expect($route)->toContain("'/edge-agent/claim'"); expect($route)->toContain("'/edge-agent/gateways/{id}/heartbeat'"); diff --git a/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayUpdateLifecycleTest.php b/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayUpdateLifecycleTest.php index d47e4ab9..bfc0a2c6 100644 --- a/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayUpdateLifecycleTest.php +++ b/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayUpdateLifecycleTest.php @@ -1,28 +1,32 @@ 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 systemd service unit" "__SERVICE_URL__" "$INSTALL_DIR/truckwash-edge-agent.service"'); + expect($managerSource)->toContain('fetch_http "Download LAN worker" "__WORKER_URL__" "$INSTALL_DIR/lan-worker.php"'); + expect($managerSource)->toContain('fetch_http "Download compose stack" "__COMPOSE_URL__" "$INSTALL_DIR/docker-compose.gateway.yml"'); + 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('apt-get install -y curl ca-certificates php-cli php-curl php-mbstring'); + expect($managerSource)->toContain('apt-get install -y curl ca-certificates docker.io docker-compose-plugin php-cli php-curl php-mbstring php-sqlite3'); expect($managerSource)->toContain('Existing claimed gateway detected; reinstall will reuse saved gateway credentials.'); expect($managerSource)->toContain('run_step "Writing agent config" merge_agent_config "$CONFIG_TEMPLATE_PATH" "$CONFIG_PATH"'); - expect($managerSource)->toContain('systemctl enable truckwash-edge-agent.service'); - expect($managerSource)->toContain('systemctl restart truckwash-edge-agent.service'); - expect($managerSource)->toContain('systemctl is-active --quiet truckwash-edge-agent.service'); + 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" 30'); expect($managerSource)->toContain('run_step "Waiting for post-reinstall heartbeat" wait_for_post_restart_heartbeat "$HEARTBEAT_MARKER_PATH" "$INSTALL_STARTED_AT" 30'); - expect($managerSource)->toContain('journalctl -u truckwash-edge-agent.service -n 40 --no-pager || true'); + expect($managerSource)->toContain('journalctl -u truckwash-edge-gateway-stack.service -n 60 --no-pager || true'); expect($managerSource)->not->toContain('agent.mjs'); expect($managerSource)->not->toContain('"brokerUrl"'); expect($serviceSource)->toContain('ExecStart=/usr/bin/php /opt/truckwash-edge-agent/agent.php --config /opt/truckwash-edge-agent/config.json'); + expect($stackServiceSource)->toContain('ExecStart=/opt/truckwash-edge-agent/gateway-launcher.sh up'); expect($serviceSource)->not->toContain('node /opt/truckwash-edge-agent/agent.mjs'); }); diff --git a/services/nginx/app/tests/Unit/Users/UsersCustomerNamesCacheTest.php b/services/nginx/app/tests/Unit/Users/UsersCustomerNamesCacheTest.php new file mode 100644 index 00000000..91bb2cb5 --- /dev/null +++ b/services/nginx/app/tests/Unit/Users/UsersCustomerNamesCacheTest.php @@ -0,0 +1,38 @@ +getMethod($method); + $target->setAccessible(true); + return $target->invokeArgs(null, $args); +} + +it('builds customer name cache payloads from economic data or display-name fallbacks', function (): void { + expect(invoke_users_private_static('buildCustomerNameCachePayload', [ + (object)['name' => 'Truckwash ApS'], + 'Fallback Name', + ]))->toBe(['name' => 'Truckwash ApS']); + + expect(invoke_users_private_static('buildCustomerNameCachePayload', [ + null, + 'Fallback Name', + ]))->toBe(['name' => 'Fallback Name']); + + expect(invoke_users_private_static('buildCustomerNameCachePayload', [ + (object)['name' => ' '], + null, + ]))->toBeNull(); +}); + +it('guards bulk customer-name cache writes behind a resolved payload check', function (): void { + $usersFile = app_path('objects/users_o.php'); + $content = file_get_contents($usersFile); + + expect($content)->not->toBeFalse(); + expect($content)->toContain('$cache_payload = self::buildCustomerNameCachePayload($cached_name, $fallback_name);'); + expect($content)->toContain('if ($cache_payload !== null) {'); + expect($content)->toContain("\$this->cache('economic_customer_name', \$cache_payload, \$customer_number);"); +}); diff --git a/services/php/Dockerfile b/services/php/Dockerfile index 9d02d90c..2ad44be7 100644 --- a/services/php/Dockerfile +++ b/services/php/Dockerfile @@ -43,8 +43,8 @@ RUN set -eux; \ pdo_mysql \ mysqli \ zip; \ - pecl install imagick-3.7.0 redis xdebug; \ - docker-php-ext-enable imagick redis xdebug; \ + pecl install imagick-3.7.0; \ + docker-php-ext-enable imagick; \ apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false $PHPIZE_DEPS; \ rm -rf /var/lib/apt/lists/*