diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index dc16e91e..8722bca8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -120,6 +120,52 @@ jobs: working-directory: services/edge-broker run: npm test + edge-gateway-backend: + name: Edge Gateway Backend (required) + runs-on: [self-hosted, Linux, X64, default] + + 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 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Boot local stack + run: docker compose up -d traefik redis mysql-debug edge-broker php1 caddy + + - name: Run edge gateway API tests + run: docker compose exec -T php1 sh -lc "cd /var/www/html && composer test:api:edge" + + - name: Run edge gateway integration tests + run: docker compose exec -T php1 sh -lc "cd /var/www/html && composer test:integration:edge" + + - name: Run edge gateway E2E smoke + run: node scripts/edge-gateway-e2e.mjs + + - name: Tear down local stack + if: always() + run: docker compose down -v + integration: name: Integration (advisory) runs-on: [self-hosted, Linux, X64, default] diff --git a/.gitignore b/.gitignore index a2e8bc12..5b026400 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ .env /services/caddy/logs* /.tmp/ +/.env.staging diff --git a/README.md b/README.md index db36d3d2..52128bca 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,8 @@ For local Docker development, run the PHP suites inside `php1`: docker exec php1 sh -lc "cd /var/www/html && composer test:unit" docker exec php1 sh -lc "cd /var/www/html && composer test:integration" docker exec php1 sh -lc "cd /var/www/html && composer test:api" +docker exec php1 sh -lc "cd /var/www/html && composer test:api:edge" +docker exec php1 sh -lc "cd /var/www/html && composer test:integration:edge" ``` Integration tests are opt-in and should be run with required services available: @@ -100,6 +102,28 @@ $env:RUN_INTEGRATION_TESTS='1' composer test:integration ``` +### Edge Gateway Regression Coverage +The dedicated backend regression lane for the PHP edge gateway stack is split into: + +- API contract tests for operator, agent, and broker-facing routes +- DB-backed integration tests for install sessions, heartbeats, tasks, logs, statistics, and shell persistence +- a local dockerized smoke that runs the real PHP edge agent against the local backend and broker + +Run the targeted PHP suites inside `php1`: + +```powershell +docker exec php1 sh -lc "cd /var/www/html && composer test:api:edge" +docker exec php1 sh -lc "cd /var/www/html && composer test:integration:edge" +``` + +Run the full local smoke from `backend-php` on the host: + +```powershell +node .\scripts\edge-gateway-e2e.mjs +``` + +The E2E smoke expects the local compose stack and Docker daemon to be available. It boots a disposable gateway container, waits for a real heartbeat, validates live operations and telemetry, and verifies browser shell transcript persistence. + ### Public Staging Edge-Gateway Smoke `api.truckwash.io:4433` is the public staging ingress. For Edge Gateways v2, the router must serve the canonical artifacts from `services/nginx/app/resources/edge-gateway-agent`, not from the legacy `dist/agent.mjs` output or a separate runtime mount. diff --git a/scripts/edge-gateway-e2e.mjs b/scripts/edge-gateway-e2e.mjs new file mode 100644 index 00000000..99a60316 --- /dev/null +++ b/scripts/edge-gateway-e2e.mjs @@ -0,0 +1,561 @@ +import assert from "node:assert/strict"; +import { execFile as execFileCallback, spawn as spawnCallback } from "node:child_process"; +import { randomUUID } from "node:crypto"; +import { promises as fs } from "node:fs"; +import path from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; + +import { DEFAULT_CONFIG_FILE_NAME, DEFAULT_HOST_API_URL } from "./test-gateway.mjs"; + +const execFile = promisify(execFileCallback); +const COMPOSE_SERVICES = ["traefik", "redis", "mysql-debug", "edge-broker", "php1", "caddy"]; + +async function runCommand(command, args, { cwd, allowFailure = false, stdio = "pipe" } = {}) { + if (stdio === "inherit") { + await new Promise((resolve, reject) => { + const child = spawnCallback(command, args, { + cwd, + stdio: "inherit", + windowsHide: true, + }); + + child.on("exit", (code) => { + if (code === 0 || allowFailure) { + resolve(); + return; + } + + reject(new Error(`${command} ${args.join(" ")} failed with exit code ${code}`)); + }); + child.on("error", reject); + }); + + return { stdout: "", stderr: "", code: 0 }; + } + + try { + const result = await execFile(command, args, { + cwd, + windowsHide: true, + encoding: "utf8", + }); + + return { stdout: result.stdout, stderr: result.stderr, code: 0 }; + } catch (error) { + if (!allowFailure) { + throw error; + } + + return { + stdout: error.stdout || "", + stderr: error.stderr || "", + code: typeof error.code === "number" ? error.code : 1, + }; + } +} + +function normalizeBaseUrl(url) { + return String(url || "").replace(/\/+$/, ""); +} + +async function waitForCondition(predicate, { timeoutMs = 30_000, intervalMs = 500, message = "Timed out" } = {}) { + const deadline = Date.now() + timeoutMs; + + while (Date.now() < deadline) { + if (await predicate()) { + return; + } + + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + } + + throw new Error(message); +} + +async function ensureComposeServices(rootDir) { + await runCommand("docker", ["compose", "up", "-d", ...COMPOSE_SERVICES], { + cwd: rootDir, + stdio: "inherit", + }); +} + +async function waitForApiReady(baseUrl, attempts = 60) { + const root = normalizeBaseUrl(baseUrl); + let lastError = "API never responded"; + + for (let attempt = 0; attempt < attempts; attempt += 1) { + try { + const response = await fetch(`${root}/ping`); + if (response.ok) { + return; + } + + lastError = `Unexpected ping status ${response.status}`; + } catch (error) { + lastError = error instanceof Error ? error.message : String(error); + } + + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + + throw new Error(`API did not become ready at ${root}/ping: ${lastError}`); +} + +function parseLastJsonLine(output) { + const lines = String(output || "") + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean); + + for (let index = lines.length - 1; index >= 0; index -= 1) { + try { + return JSON.parse(lines[index]); + } catch { + // Continue scanning backwards for the JSON payload. + } + } + + throw new Error(`Unable to parse JSON from command output:\n${output}`); +} + +async function runPhpFixture(rootDir, action, payload = null) { + const encodedPayload = payload === null + ? "" + : ` ${Buffer.from(JSON.stringify(payload), "utf8").toString("base64url")}`; + const command = `cd /var/www/html && CONFIG_DB_TARGET=debug php tests/Support/EdgeGatewayE2eFixture.php ${action}${encodedPayload}`; + + const result = await runCommand("docker", [ + "compose", + "exec", + "-T", + "php1", + "sh", + "-lc", + command, + ], { + cwd: rootDir, + }); + + return parseLastJsonLine(result.stdout); +} + +async function apiRequest(baseUrl, method, endpoint, { token = null, body = null, headers = {} } = {}) { + const response = await fetch(`${normalizeBaseUrl(baseUrl)}${endpoint}`, { + method, + headers: { + ...(body === null ? {} : { "content-type": "application/json" }), + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...headers, + }, + body: body === null ? undefined : JSON.stringify(body), + }); + + const rawBody = await response.text(); + let json = null; + + if (rawBody !== "") { + try { + json = JSON.parse(rawBody); + } catch { + json = null; + } + } + + if (!response.ok) { + const message = + json?.data?.message || + json?.error || + rawBody || + `HTTP ${response.status}`; + throw new Error(`${method} ${endpoint} failed: ${message}`); + } + + return json; +} + +async function loadWebSocketImplementation() { + if (typeof WebSocket !== "undefined") { + return WebSocket; + } + + const module = await import("ws"); + return module.default; +} + +function onSocket(socket, eventName, handler) { + if (typeof socket.addEventListener === "function") { + socket.addEventListener(eventName, (event) => { + if (eventName === "message") { + handler(event.data); + return; + } + + handler(event); + }); + return; + } + + socket.on(eventName, handler); +} + +function collectSocketMessages(socket) { + const messages = []; + + onSocket(socket, "message", (payload) => { + const text = typeof payload === "string" + ? payload + : Buffer.isBuffer(payload) + ? payload.toString("utf8") + : typeof payload?.toString === "function" + ? payload.toString() + : ""; + + if (text === "") { + return; + } + + try { + messages.push(JSON.parse(text)); + } catch { + // Ignore non-JSON frames. + } + }); + + return messages; +} + +async function waitForSocketOpen(socket) { + if (socket.readyState === 1) { + return; + } + + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error("Timed out waiting for websocket open.")), 10_000); + + const onOpen = () => { + clearTimeout(timeout); + resolve(); + }; + + const onError = (error) => { + clearTimeout(timeout); + reject(error instanceof Error ? error : new Error(String(error))); + }; + + onSocket(socket, "open", onOpen); + onSocket(socket, "error", onError); + }); +} + +async function waitForSocketMessage(messages, predicate, options) { + await waitForCondition(() => messages.some(predicate), options); +} + +function buildSocketUrl(wsUrl, token) { + const url = new URL(String(wsUrl)); + url.searchParams.set("token", token); + return url.toString(); +} + +function closeSocket(socket) { + if (!socket || typeof socket.close !== "function") { + return; + } + + const readyState = typeof socket.readyState === "number" ? socket.readyState : null; + if (readyState !== null && readyState >= 2) { + return; + } + + socket.close(); +} + +function collectMessages(rows) { + return Array.isArray(rows) + ? rows + .map((row) => (row && typeof row === "object" ? row.message : null)) + .filter((value) => typeof value === "string") + : []; +} + +async function main() { + const scriptPath = fileURLToPath(import.meta.url); + const rootDir = path.resolve(path.dirname(scriptPath), ".."); + const runId = randomUUID().slice(0, 8); + const containerName = `truckwash-edge-e2e-${runId}`; + const configDir = path.join(rootDir, ".tmp", "edge-gateway-e2e", runId); + const configFilePath = path.join(configDir, DEFAULT_CONFIG_FILE_NAME); + const baseUrl = process.env.EDGE_GATEWAY_E2E_BASE_URL || DEFAULT_HOST_API_URL; + + let fixture = null; + let gatewayId = null; + let streamSocket = null; + let shellSocket = null; + + try { + await ensureComposeServices(rootDir); + await waitForApiReady(baseUrl); + + fixture = await runPhpFixture(rootDir, "create"); + const authToken = String(fixture.auth_token || ""); + const departmentId = Number(fixture.department_id || 0); + + assert.ok(authToken !== "", "Fixture helper did not return an auth token."); + assert.ok(departmentId > 0, "Fixture helper did not return a department id."); + + const installTokenResponse = await apiRequest(baseUrl, "POST", "/edge-gateways/install-token", { + token: authToken, + body: { + department_id: departmentId, + label: `Edge Gateway E2E ${runId}`, + }, + }); + const installToken = String(installTokenResponse?.data?.token || ""); + assert.ok(installToken !== "", "Install token creation did not return a token."); + + await runCommand(process.execPath, [ + "scripts/test-gateway.mjs", + "start", + "--install-token", + installToken, + "--container-name", + containerName, + "--config-dir", + configDir, + "--heartbeat-seconds", + "3", + "--skip-compose-up", + ], { + cwd: rootDir, + stdio: "inherit", + }); + + await waitForCondition( + async () => { + try { + await fs.access(configFilePath); + return true; + } catch { + return false; + } + }, + { message: `Gateway config file was not created at ${configFilePath}` } + ); + + const config = JSON.parse(await fs.readFile(configFilePath, "utf8")); + gatewayId = Number(config.gatewayId || 0); + assert.ok(gatewayId > 0, "Gateway config did not include a gateway id."); + + await waitForCondition( + async () => { + const result = await runCommand("docker", [ + "exec", + containerName, + "test", + "-f", + "/opt/truckwash-edge-agent/runtime/last-heartbeat-ok.txt", + ], { + allowFailure: true, + }); + + return result.code === 0; + }, + { + timeoutMs: 60_000, + message: "Gateway never wrote the successful heartbeat marker.", + } + ); + + await waitForCondition( + async () => { + const detail = await apiRequest(baseUrl, "GET", `/edge-gateways/${gatewayId}`, { + token: authToken, + }); + + return detail?.data?.status === "ONLINE" + && Object.keys(detail?.data?.metadata?.system_metrics || {}).length > 0; + }, + { + timeoutMs: 60_000, + message: "Gateway detail never transitioned to ONLINE with fresh system metrics after install.", + } + ); + + const WebSocketImpl = await loadWebSocketImplementation(); + const streamSession = await apiRequest(baseUrl, "POST", `/edge-gateways/${gatewayId}/stream-session`, { + token: authToken, + body: { + scopes: ["overview", "tasks", "logs", "statistics"], + }, + }); + const streamWsUrl = buildSocketUrl(String(streamSession?.data?.ws_url || ""), String(streamSession?.data?.token || "")); + streamSocket = new WebSocketImpl(streamWsUrl); + const streamMessages = collectSocketMessages(streamSocket); + + await waitForSocketOpen(streamSocket); + await waitForSocketMessage( + streamMessages, + (message) => message?.type === "gateway.stream.ready", + { timeoutMs: 15_000, message: "Gateway stream never became ready." } + ); + + const operationResponse = await apiRequest(baseUrl, "POST", `/edge-gateways/${gatewayId}/operations`, { + token: authToken, + body: { + type: "DISCOVERY", + request: { + inventory: [{ + device_id: `edge-e2e-${runId}`, + local_ip: "10.70.80.90", + model: "TruckWash Edge E2E", + channel_count: 1, + online: true, + capabilities: { + gateway_management_v2: true, + }, + metadata: { + hostname: `edge-e2e-${runId}`, + }, + }], + }, + }, + }); + const operationId = Number(operationResponse?.data?.operation?.id || 0); + assert.ok(operationId > 0, "Operation creation did not return an operation id."); + + await waitForSocketMessage( + streamMessages, + (message) => message?.type === "task.updated" && Number(message?.operationId || 0) === operationId, + { timeoutMs: 30_000, message: "Live gateway stream never emitted task.updated for the queued operation." } + ); + + await waitForCondition( + async () => { + const operations = await apiRequest(baseUrl, "GET", `/edge-gateways/${gatewayId}/operations`, { + token: authToken, + }); + + const operation = Array.isArray(operations?.data) + ? operations.data.find((item) => Number(item?.id || 0) === operationId) + : null; + + return operation?.status === "COMPLETED"; + }, + { timeoutMs: 30_000, message: "Gateway operation never completed through the live agent." } + ); + + await waitForSocketMessage( + streamMessages, + (message) => message?.type === "gateway.telemetry" || message?.type === "stats.updated", + { timeoutMs: 15_000, message: "Live gateway stream never emitted telemetry or statistics updates." } + ); + + await waitForCondition( + async () => { + const logs = await apiRequest(baseUrl, "GET", `/edge-gateways/${gatewayId}/logs`, { + token: authToken, + }); + + return Array.isArray(logs?.data?.log_entries) && logs.data.log_entries.length > 0; + }, + { timeoutMs: 20_000, message: "Gateway logs page never received live log entries from the running agent." } + ); + + const statistics = await apiRequest(baseUrl, "GET", `/edge-gateways/${gatewayId}/statistics`, { + token: authToken, + }); + assert.ok( + Object.keys(statistics?.data?.system_metrics || {}).length > 0, + "Gateway statistics page did not expose system metrics after live telemetry." + ); + + const shellSession = await apiRequest(baseUrl, "POST", `/edge-gateways/${gatewayId}/shell-sessions`, { + token: authToken, + body: { + reason: "Edge gateway E2E shell validation", + cwd: "/opt/truckwash-edge-agent", + cols: 120, + rows: 40, + }, + }); + const shellWsUrl = buildSocketUrl(String(shellSession?.data?.ws_url || ""), String(shellSession?.data?.token || "")); + shellSocket = new WebSocketImpl(shellWsUrl); + const shellMessages = collectSocketMessages(shellSocket); + + await waitForSocketOpen(shellSocket); + await waitForSocketMessage( + shellMessages, + (message) => message?.type === "opened", + { timeoutMs: 20_000, message: "Browser shell never opened against the live gateway." } + ); + + shellSocket.send(JSON.stringify({ + type: "input", + data: "printf 'edge-e2e-shell\\n'; exit\r", + })); + + await waitForSocketMessage( + shellMessages, + (message) => message?.type === "output" && String(message?.data || "").includes("edge-e2e-shell"), + { timeoutMs: 20_000, message: "Browser shell never returned the expected command output." } + ); + + await waitForSocketMessage( + shellMessages, + (message) => message?.type === "closed", + { timeoutMs: 20_000, message: "Browser shell never closed cleanly." } + ); + + const logsAfterShell = await apiRequest(baseUrl, "GET", `/edge-gateways/${gatewayId}/logs`, { + token: authToken, + }); + const shellTranscripts = Array.isArray(logsAfterShell?.data?.shell_sessions) + ? logsAfterShell.data.shell_sessions.map((session) => String(session?.transcript || "")) + : []; + + assert.ok( + shellTranscripts.some((transcript) => transcript.includes("edge-e2e-shell")), + "Gateway logs page did not persist the shell transcript." + ); + + const timelineMessages = collectMessages(logsAfterShell?.data?.timeline || []); + assert.ok( + timelineMessages.includes("GATEWAY_SHELL_SESSION_CLOSED"), + "Gateway logs page did not include the shell close audit event." + ); + + process.stdout.write("Edge gateway E2E smoke completed successfully.\n"); + } finally { + closeSocket(shellSocket); + closeSocket(streamSocket); + + if (gatewayId !== null && fixture?.auth_token) { + await apiRequest(baseUrl, "DELETE", `/edge-gateways/${gatewayId}`, { + token: String(fixture.auth_token), + }).catch(() => {}); + } + + await runCommand(process.execPath, [ + "scripts/test-gateway.mjs", + "stop", + "--container-name", + containerName, + ], { + cwd: rootDir, + allowFailure: true, + }).catch(() => {}); + + if (fixture !== null) { + await runPhpFixture(rootDir, "cleanup", fixture).catch(() => {}); + } + + await fs.rm(configDir, { recursive: true, force: true }).catch(() => {}); + } +} + +main().catch((error) => { + process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + process.exitCode = 1; +}); diff --git a/services/nginx/app/.phpunit.cache/test-results b/services/nginx/app/.phpunit.cache/test-results index 63b6610c..b6b5c9e1 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":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,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayViewCacheTest::__pest_evaluable_it_syncs_gateway_snapshots_into_cached_list_payloads_and_refreshes_fleet_usage":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayModuleRouteWiringTest::__pest_evaluable_it_keeps_only_the_module_facade_in_the_global_classes_directory_and_conditionally_loads_module_routes":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_builds_install_script_urls_with_the_compose_edge_gateway_artifacts_and_forwarded_https_scheme":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_builds_the_installer_around_the_compose_stack_artifacts_and_management_polling_config":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_exposes_update_payload__credential_rotation__cancel_endpoints__and_operation_endpoints_without_shell_transport_wiring":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayRouteWiringTest::__pest_evaluable_it_registers_the_v2_operator_facing_edge_gateway_routes":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewaySchemaBootstrapTest::__pest_evaluable_it_defines_the_v2_edge_gateway_schema_bootstrap_tables":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewaySchemaBootstrapTest::__pest_evaluable_it_stores_operation_metadata_and_event_timelines_for_management_workflows":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_prefers_EDGE__PUBLIC__API__URL_when_explicitly_configured":7,"P\\Tests\\Api\\PlateScannersApiTest::__pest_evaluable_it_returns_the_updated_lane_id_after_editing_a_scanner_whose_null_lane_was_already_cached":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,"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.254,"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.204,"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.005,"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.006,"P\\Tests\\Unit\\Selfserve\\EdgeGatewaySchemaBootstrapTest::__pest_evaluable_it_defines_the_v2_edge_gateway_schema_bootstrap_tables":0.005,"P\\Tests\\Unit\\Selfserve\\EdgeGatewaySchemaBootstrapTest::__pest_evaluable_it_stores_operation_metadata_and_event_timelines_for_management_workflows":0.004,"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.013,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerCommandQueueTest::__pest_evaluable_it_keeps_relay_dispatch_and_discovery_queueing_on_the_edge_gateway_manager":0.08,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerCommandQueueTest::__pest_evaluable_it_loads_relay_command_helpers_on_the_manager_and_gateway_operations_on_the_dedicated_service":0.008,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_relay_fallback_and_transport_health_details_without_shell_or_update_runtime_state":0.168,"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.016,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_builds_update_payloads_with_checksums_from_resolved_artifact_paths":0.232,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_resolves_edge_agent_artifacts_from_a_supported_runtime_layout":0.225,"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.001,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_prioritizes_router_resources_before_mounted_and_baked_in_artifact_directories":0.026,"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.017,"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,"P\\Tests\\Api\\ApiCoverageManifestTest::__pest_evaluable_it_keeps_every_selected_API_operation_covered_by_happy_path_and_failure_tests":0.271,"P\\Tests\\Api\\ApiCoverageManifestTest::__pest_evaluable_it_keeps_the_OpenAPI_manifest_entries_aligned_with_the_API_spec":0.097,"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,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayFleetUsageStatisticsTest::__pest_evaluable_it_derives_fleet_usage_directly_from_cached_gateway_row_summaries":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_builds_install_script_urls_with_the_compose_edge_gateway_artifacts_and_forwarded_https_scheme":0.619,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayMetadataUpdateContractTest::__pest_evaluable_it_adds_a_gateway_metadata_update_endpoint_with_label_and_primary_assignment_fields":0.009,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayMetadataUpdateContractTest::__pest_evaluable_it_reassigns_department_primary_gateways_through_dedicated_manager_helpers":0.004,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayShellPollingTransportTest::__pest_evaluable_it_removes_shell_access_from_the_edge_gateway_HTTP_contracts":0.006,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_builds_the_installer_around_the_compose_stack_artifacts_and_management_polling_config":0.047,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayViewCacheTest::__pest_evaluable_it_uses_sane_ttl_defaults_and_deterministic_cache_keys":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayViewCacheTest::__pest_evaluable_it_stores_and_retrieves_cached_list_and_detail_payloads":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayViewCacheTest::__pest_evaluable_it_syncs_gateway_snapshots_into_cached_list_payloads_and_refreshes_fleet_usage":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayViewCacheTest::__pest_evaluable_it_removes_deleted_gateways_from_cached_list_and_detail_payloads":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_exposes_update_payload__credential_rotation__cancel_endpoints__and_operation_endpoints_without_shell_transport_wiring":0.013,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayModuleRouteWiringTest::__pest_evaluable_it_registers_module_scoped_edge_gateway_operator_routes":0.016,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayModuleRouteWiringTest::__pest_evaluable_it_registers_edge_gateway_module_config_endpoints":0.006,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayModuleRouteWiringTest::__pest_evaluable_it_registers_edge_gateway_config_endpoints_from_the_module_route_directory":0.008,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayModuleRouteWiringTest::__pest_evaluable_it_keeps_only_the_module_facade_in_the_global_classes_directory_and_conditionally_loads_module_routes":0.014,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayInstallSessionLifecycleTest::__pest_evaluable_it_caps_install_session_diagnostics_and_events_while_preserving_the_first_start_timestamp":0.021,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayInstallSessionLifecycleTest::__pest_evaluable_it_clears_terminal_failure_details_after_a_successful_claim_update":0.013,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayInstallSessionLifecycleTest::__pest_evaluable_it_marks_expired_non_terminal_install_sessions_as_terminal_when_read_back":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_builds_websocket_broker_urls_on_the_traefik_broker_path":0,"P\\Tests\\Unit\\Selfserve\\DepartmentGateConfigRelayTest::__pest_evaluable_it_validates_relay_gate_configs_and_keeps_relay_specific_fields_in_the_payload":0.016,"P\\Tests\\Unit\\Selfserve\\DepartmentGateConfigRelayTest::__pest_evaluable_it_rejects_relay_gate_configs_without_a_logical_relay_id":0.011,"P\\Tests\\Unit\\Bird\\DepartmentGatesRelayOpenTest::__pest_evaluable_it_dispatches_relay_backed_gates_through_the_edge_gateway_relay_manager":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayDepartmentWorkspaceContractTest::__pest_evaluable_it_defines_the_department_hardware_workspace_service_payload_surface":0.005,"P\\Tests\\Unit\\Selfserve\\PlateScannerWorkspaceContractTest::__pest_evaluable_it_adds_lane_aware_scanner_management_and_a_dedicated_rotate_key_action":0.005,"P\\Tests\\Unit\\Selfserve\\PlateScannerWorkspaceContractTest::__pest_evaluable_it_uses_the_scanner_default_lane_before_requiring_an_explicit_lane__id_in_machine_button_webhooks":0.003,"P\\Tests\\Unit\\Selfserve\\GatewayShellyTransportTest::__pest_evaluable_it_maps_gateway_relay_status_responses_into_the_Shelly_cloud_payload_shape":0.015,"P\\Tests\\Unit\\Selfserve\\GatewayShellyTransportTest::__pest_evaluable_it_maps_gateway_relay_switch_responses_into_the_Shelly_cloud_payload_shape":0,"P\\Tests\\Unit\\Selfserve\\GatewayShellyTransportTest::__pest_evaluable_it_requires_a_valid_department_id_for_gateway_transport_requests":0,"P\\Tests\\Unit\\Selfserve\\GatewayShellyTransportTest::__pest_evaluable_it_rejects_unsupported_gateway_transport_endpoints":0,"P\\Tests\\Unit\\Selfserve\\GatewayShellyTransportTest::__pest_evaluable_it_surfaces_binding_lookup_failures_while_resolving_relay_state_through_the_gateway_transport":0,"P\\Tests\\Unit\\Selfserve\\GatewayShellyTransportTest::__pest_evaluable_it_surfaces_offline_gateway_failures_while_dispatching_relay_switch_commands":0,"P\\Tests\\Api\\PlateScannersApiTest::__pest_evaluable_it_returns_the_updated_lane_id_after_editing_a_scanner_whose_null_lane_was_already_cached":21.835,"P\\Tests\\Api\\PlateScannersApiTest::__pest_evaluable_it_rejects_plate_scanner_edits_when_the_permission_is_missing":7.837,"P\\Tests\\Unit\\Selfserve\\ShellyRelayInventoryTest::__pest_evaluable_it_normalizes_owned_Shelly_devices_into_relay_select_options":0.277,"P\\Tests\\Unit\\Selfserve\\ShellyRelayInventoryTest::__pest_evaluable_it_fails_fast_when_Shelly_inventory_does_not_include_owned_devices_status":0.015,"P\\Tests\\Unit\\Selfserve\\DepartmentLaneRelayOptionsRouteWiringTest::__pest_evaluable_it_registers_a_department_lane_relay_options_endpoint_backed_by_Shelly_inventory":0.081}} \ 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,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayViewCacheTest::__pest_evaluable_it_syncs_gateway_snapshots_into_cached_list_payloads_and_refreshes_fleet_usage":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayModuleRouteWiringTest::__pest_evaluable_it_keeps_only_the_module_facade_in_the_global_classes_directory_and_conditionally_loads_module_routes":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_builds_install_script_urls_with_the_compose_edge_gateway_artifacts_and_forwarded_https_scheme":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_builds_the_installer_around_the_compose_stack_artifacts_and_management_polling_config":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_exposes_update_payload__credential_rotation__cancel_endpoints__and_operation_endpoints_without_shell_transport_wiring":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayRouteWiringTest::__pest_evaluable_it_registers_the_v2_operator_facing_edge_gateway_routes":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewaySchemaBootstrapTest::__pest_evaluable_it_defines_the_v2_edge_gateway_schema_bootstrap_tables":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewaySchemaBootstrapTest::__pest_evaluable_it_stores_operation_metadata_and_event_timelines_for_management_workflows":7,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_prefers_EDGE__PUBLIC__API__URL_when_explicitly_configured":7,"P\\Tests\\Api\\PlateScannersApiTest::__pest_evaluable_it_returns_the_updated_lane_id_after_editing_a_scanner_whose_null_lane_was_already_cached":7,"P\\Tests\\Api\\EdgeGatewayAgentApiTest::__pest_evaluable_it_serves_installer_artifacts_and_recovers_gateway_runtime_status_after_fresh_heartbeats":7,"P\\Tests\\Api\\EdgeGatewayAgentApiTest::__pest_evaluable_it_polls_operations_and_commands__submits_results__and_records_broker_presence_for_task_pages":7,"P\\Tests\\Api\\EdgeGatewayBrokerApiTest::__pest_evaluable_it_builds_broker_backlog_and_completes_gateway_operations_through_broker_endpoints":7,"P\\Tests\\Api\\EdgeGatewayOperatorApiTest::__pest_evaluable_it_creates_install_tokens__tracks_installer_status__and_exposes_claimed_gateway_detail_to_authorized_operators":7,"P\\Tests\\Api\\EdgeGatewayOperatorApiTest::__pest_evaluable_it_manages_edge_gateway_metadata__bindings__operations__sessions__rotation__cutover__and_deletion":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.189,"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.579,"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.204,"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.028,"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.006,"P\\Tests\\Unit\\Selfserve\\EdgeGatewaySchemaBootstrapTest::__pest_evaluable_it_defines_the_v2_edge_gateway_schema_bootstrap_tables":0.005,"P\\Tests\\Unit\\Selfserve\\EdgeGatewaySchemaBootstrapTest::__pest_evaluable_it_stores_operation_metadata_and_event_timelines_for_management_workflows":0.004,"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.013,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerCommandQueueTest::__pest_evaluable_it_keeps_relay_dispatch_and_discovery_queueing_on_the_edge_gateway_manager":0.08,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerCommandQueueTest::__pest_evaluable_it_loads_relay_command_helpers_on_the_manager_and_gateway_operations_on_the_dedicated_service":0.008,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerHeartbeatStatusTest::__pest_evaluable_it_derives_relay_fallback_and_transport_health_details_without_shell_or_update_runtime_state":0.417,"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.016,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_builds_update_payloads_with_checksums_from_resolved_artifact_paths":0.232,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_resolves_edge_agent_artifacts_from_a_supported_runtime_layout":0.225,"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.001,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayArtifactLocatorTest::__pest_evaluable_it_prioritizes_router_resources_before_mounted_and_baked_in_artifact_directories":0.026,"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.017,"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,"P\\Tests\\Api\\ApiCoverageManifestTest::__pest_evaluable_it_keeps_every_selected_API_operation_covered_by_happy_path_and_failure_tests":0.271,"P\\Tests\\Api\\ApiCoverageManifestTest::__pest_evaluable_it_keeps_the_OpenAPI_manifest_entries_aligned_with_the_API_spec":0.097,"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,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayFleetUsageStatisticsTest::__pest_evaluable_it_derives_fleet_usage_directly_from_cached_gateway_row_summaries":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_builds_install_script_urls_with_the_compose_edge_gateway_artifacts_and_forwarded_https_scheme":0.619,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayMetadataUpdateContractTest::__pest_evaluable_it_adds_a_gateway_metadata_update_endpoint_with_label_and_primary_assignment_fields":0.009,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayMetadataUpdateContractTest::__pest_evaluable_it_reassigns_department_primary_gateways_through_dedicated_manager_helpers":0.004,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayShellPollingTransportTest::__pest_evaluable_it_removes_shell_access_from_the_edge_gateway_HTTP_contracts":0.006,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_builds_the_installer_around_the_compose_stack_artifacts_and_management_polling_config":0.052,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayViewCacheTest::__pest_evaluable_it_uses_sane_ttl_defaults_and_deterministic_cache_keys":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayViewCacheTest::__pest_evaluable_it_stores_and_retrieves_cached_list_and_detail_payloads":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayViewCacheTest::__pest_evaluable_it_syncs_gateway_snapshots_into_cached_list_payloads_and_refreshes_fleet_usage":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayViewCacheTest::__pest_evaluable_it_removes_deleted_gateways_from_cached_list_and_detail_payloads":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayUpdateLifecycleTest::__pest_evaluable_it_exposes_update_payload__credential_rotation__cancel_endpoints__and_operation_endpoints_without_shell_transport_wiring":0.014,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayModuleRouteWiringTest::__pest_evaluable_it_registers_module_scoped_edge_gateway_operator_routes":0.016,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayModuleRouteWiringTest::__pest_evaluable_it_registers_edge_gateway_module_config_endpoints":0.006,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayModuleRouteWiringTest::__pest_evaluable_it_registers_edge_gateway_config_endpoints_from_the_module_route_directory":0.008,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayModuleRouteWiringTest::__pest_evaluable_it_keeps_only_the_module_facade_in_the_global_classes_directory_and_conditionally_loads_module_routes":0.014,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayInstallSessionLifecycleTest::__pest_evaluable_it_caps_install_session_diagnostics_and_events_while_preserving_the_first_start_timestamp":0.021,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayInstallSessionLifecycleTest::__pest_evaluable_it_clears_terminal_failure_details_after_a_successful_claim_update":0.013,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayInstallSessionLifecycleTest::__pest_evaluable_it_marks_expired_non_terminal_install_sessions_as_terminal_when_read_back":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayManagerUrlTest::__pest_evaluable_it_builds_websocket_broker_urls_on_the_traefik_broker_path":0,"P\\Tests\\Unit\\Selfserve\\DepartmentGateConfigRelayTest::__pest_evaluable_it_validates_relay_gate_configs_and_keeps_relay_specific_fields_in_the_payload":0.016,"P\\Tests\\Unit\\Selfserve\\DepartmentGateConfigRelayTest::__pest_evaluable_it_rejects_relay_gate_configs_without_a_logical_relay_id":0.011,"P\\Tests\\Unit\\Bird\\DepartmentGatesRelayOpenTest::__pest_evaluable_it_dispatches_relay_backed_gates_through_the_edge_gateway_relay_manager":0,"P\\Tests\\Unit\\Selfserve\\EdgeGatewayDepartmentWorkspaceContractTest::__pest_evaluable_it_defines_the_department_hardware_workspace_service_payload_surface":0.005,"P\\Tests\\Unit\\Selfserve\\PlateScannerWorkspaceContractTest::__pest_evaluable_it_adds_lane_aware_scanner_management_and_a_dedicated_rotate_key_action":0.005,"P\\Tests\\Unit\\Selfserve\\PlateScannerWorkspaceContractTest::__pest_evaluable_it_uses_the_scanner_default_lane_before_requiring_an_explicit_lane__id_in_machine_button_webhooks":0.003,"P\\Tests\\Unit\\Selfserve\\GatewayShellyTransportTest::__pest_evaluable_it_maps_gateway_relay_status_responses_into_the_Shelly_cloud_payload_shape":0.015,"P\\Tests\\Unit\\Selfserve\\GatewayShellyTransportTest::__pest_evaluable_it_maps_gateway_relay_switch_responses_into_the_Shelly_cloud_payload_shape":0,"P\\Tests\\Unit\\Selfserve\\GatewayShellyTransportTest::__pest_evaluable_it_requires_a_valid_department_id_for_gateway_transport_requests":0,"P\\Tests\\Unit\\Selfserve\\GatewayShellyTransportTest::__pest_evaluable_it_rejects_unsupported_gateway_transport_endpoints":0,"P\\Tests\\Unit\\Selfserve\\GatewayShellyTransportTest::__pest_evaluable_it_surfaces_binding_lookup_failures_while_resolving_relay_state_through_the_gateway_transport":0,"P\\Tests\\Unit\\Selfserve\\GatewayShellyTransportTest::__pest_evaluable_it_surfaces_offline_gateway_failures_while_dispatching_relay_switch_commands":0,"P\\Tests\\Api\\PlateScannersApiTest::__pest_evaluable_it_returns_the_updated_lane_id_after_editing_a_scanner_whose_null_lane_was_already_cached":21.835,"P\\Tests\\Api\\PlateScannersApiTest::__pest_evaluable_it_rejects_plate_scanner_edits_when_the_permission_is_missing":7.837,"P\\Tests\\Unit\\Selfserve\\ShellyRelayInventoryTest::__pest_evaluable_it_normalizes_owned_Shelly_devices_into_relay_select_options":0.179,"P\\Tests\\Unit\\Selfserve\\ShellyRelayInventoryTest::__pest_evaluable_it_fails_fast_when_Shelly_inventory_does_not_include_owned_devices_status":0.018,"P\\Tests\\Unit\\Selfserve\\DepartmentLaneRelayOptionsRouteWiringTest::__pest_evaluable_it_registers_a_department_lane_relay_options_endpoint_backed_by_Shelly_inventory":0.094,"P\\Tests\\Unit\\Selfserve\\DepartmentLaneRelayNullificationRouteWiringTest::__pest_evaluable_it_nullifies_department_lane_relay_fields_when_blank_select_values_are_submitted":0.005,"P\\Tests\\Api\\EdgeGatewayAgentApiTest::__pest_evaluable_it_serves_installer_artifacts_and_recovers_gateway_runtime_status_after_fresh_heartbeats":27.902,"P\\Tests\\Api\\EdgeGatewayAgentApiTest::__pest_evaluable_it_polls_operations_and_commands__submits_results__and_records_broker_presence_for_task_pages":25.265,"P\\Tests\\Api\\EdgeGatewayAgentApiTest::__pest_evaluable_it_rejects_missing_and_invalid_edge_agent_tokens":13.716,"P\\Tests\\Api\\EdgeGatewayBrokerApiTest::__pest_evaluable_it_validates_broker_sessions_and_ingests_presence__telemetry__logs__and_shell_lifecycle_data":197.978,"P\\Tests\\Api\\EdgeGatewayBrokerApiTest::__pest_evaluable_it_builds_broker_backlog_and_completes_gateway_operations_through_broker_endpoints":27.165,"P\\Tests\\Api\\EdgeGatewayBrokerApiTest::__pest_evaluable_it_rejects_invalid_edge_broker_shared_secrets":6.737,"P\\Tests\\Api\\EdgeGatewayOperatorApiTest::__pest_evaluable_it_creates_install_tokens__tracks_installer_status__and_exposes_claimed_gateway_detail_to_authorized_operators":49.198,"P\\Tests\\Api\\EdgeGatewayOperatorApiTest::__pest_evaluable_it_manages_edge_gateway_metadata__bindings__operations__sessions__rotation__cutover__and_deletion":33.998,"P\\Tests\\Api\\EdgeGatewayOperatorApiTest::__pest_evaluable_it_rejects_operator_edge_routes_when_module_permission_or_department_access_is_missing":43.949}} \ No newline at end of file diff --git a/services/nginx/app/classes/shelly_relay_inventory.php b/services/nginx/app/classes/shelly_relay_inventory.php index 162335bc..4e9add12 100644 --- a/services/nginx/app/classes/shelly_relay_inventory.php +++ b/services/nginx/app/classes/shelly_relay_inventory.php @@ -9,6 +9,8 @@ class shelly_relay_inventory private ?shelly $client; /** @var callable|null */ private $inventory_fetcher = null; + /** @var callable|null */ + private $device_list_fetcher = null; public function __construct(?shelly $client = null) { @@ -21,6 +23,12 @@ class shelly_relay_inventory return $this; } + public function setDeviceListFetcher(callable $fetcher): self + { + $this->device_list_fetcher = $fetcher; + return $this; + } + /** * @return array> * @throws Exception @@ -28,10 +36,21 @@ class shelly_relay_inventory public function listRelayOptions(): array { $devices_status = $this->fetchOwnedDevicesStatus(); + $device_catalog = $this->fetchOwnedDeviceCatalog(); $options_by_id = []; foreach ($devices_status as $device) { - $option = $this->buildRelayOption($this->normalizeToArray($device)); + $normalized_device = $this->normalizeToArray($device); + $device_id = $this->extractFirstString([ + $normalized_device['_dev_info']['id'] ?? null, + $normalized_device['id'] ?? null, + ]); + $catalog_entry = $device_id !== '' ? ($device_catalog[$device_id] ?? null) : null; + + $option = $this->buildRelayOption( + $normalized_device, + is_array($catalog_entry) ? $catalog_entry : null + ); if ($option === null) { continue; } @@ -41,9 +60,10 @@ class shelly_relay_inventory $options = array_values($options_by_id); usort($options, static function (array $left, array $right): int { - $online_compare = ((int)!($left['online'] ?? false)) <=> ((int)!($right['online'] ?? false)); - if ($online_compare !== 0) { - return $online_compare; + $status_compare = self::statusSortWeight((string)($left['status_color'] ?? '')) + <=> self::statusSortWeight((string)($right['status_color'] ?? '')); + if ($status_compare !== 0) { + return $status_compare; } $name_compare = strcasecmp((string)($left['name'] ?? ''), (string)($right['name'] ?? '')); @@ -57,6 +77,48 @@ class shelly_relay_inventory return $options; } + /** + * @return array> + */ + private function fetchOwnedDeviceCatalog(): array + { + try { + $payload = is_callable($this->device_list_fetcher) + ? ($this->device_list_fetcher)() + : $this->getClient()->sendGetRequest('/interface/device/list', [ + 'no_shared' => 'true', + ]); + } catch (\Throwable) { + return []; + } + + $normalized = $this->normalizeToArray($payload); + if (($normalized['isok'] ?? true) === false) { + return []; + } + + $devices = $normalized['data']['devices'] ?? null; + if (!is_array($devices)) { + return []; + } + + $catalog_by_id = []; + foreach ($devices as $device_key => $device) { + $normalized_device = $this->normalizeToArray($device); + $device_id = $this->extractFirstString([ + $normalized_device['id'] ?? null, + is_string($device_key) ? $device_key : null, + ]); + if ($device_id === '') { + continue; + } + + $catalog_by_id[$device_id] = $normalized_device; + } + + return $catalog_by_id; + } + /** * @return array * @throws Exception @@ -98,7 +160,7 @@ class shelly_relay_inventory * @param array $device * @return array|null */ - private function buildRelayOption(array $device): ?array + private function buildRelayOption(array $device, ?array $catalog_entry = null): ?array { if ($device === [] || !$this->isRelayCapableDevice($device)) { return null; @@ -112,7 +174,10 @@ class shelly_relay_inventory return null; } - $device_name = $this->extractFirstString([ + $cloud_name = $this->extractFirstString([ + $catalog_entry['name'] ?? null, + ]); + $local_device_name = $this->extractFirstString([ $device['name'] ?? null, $device['_dev_info']['name'] ?? null, $device['settings']['name'] ?? null, @@ -120,18 +185,38 @@ class shelly_relay_inventory $device['status']['name'] ?? null, $device['status']['sys']['device']['name'] ?? null, ]); + $device_name = $cloud_name !== '' ? $cloud_name : $local_device_name; $device_code = $this->extractFirstString([ $device['_dev_info']['code'] ?? null, $device['code'] ?? null, ]); + $device_type = $this->extractDeviceType($device, $device_code, $catalog_entry); + $control_type = $this->extractControlType($device); + $control_name = $this->extractControlName($device); + $online = $this->extractOnlineState($device); + if ($online === null) { + $online = $this->normalizeBoolean($catalog_entry['cloud_online'] ?? null); + } + $status_color = $this->extractStatusColor($online); return [ 'id' => $device_id, - 'name' => $this->buildRelayLabel($device_id, $device_name, $device_code), + 'name' => $this->buildRelayLabel( + $device_type, + $cloud_name, + $local_device_name, + $control_name, + $device_id + ), 'device_id' => $device_id, 'device_name' => $device_name !== '' ? $device_name : null, + 'cloud_name' => $cloud_name !== '' ? $cloud_name : null, + 'device_type' => $device_type, 'code' => $device_code !== '' ? $device_code : null, - 'online' => $this->extractOnlineState($device), + 'control_type' => $control_type, + 'control_name' => $control_name !== '' ? $control_name : null, + 'status_color' => $status_color, + 'online' => $online, ]; } @@ -176,7 +261,7 @@ class shelly_relay_inventory /** * @param array $device */ - private function extractOnlineState(array $device): bool + private function extractOnlineState(array $device): ?bool { $online = $device['_dev_info']['online'] ?? $device['online'] ?? null; if (is_bool($online)) { @@ -187,21 +272,106 @@ class shelly_relay_inventory return (int)$online === 1; } - return false; + return null; } - private function buildRelayLabel(string $device_id, string $device_name, string $device_code): string + private function extractStatusColor(?bool $online): string { - $label = $device_id; - if ($device_name !== '' && strcasecmp($device_name, $device_id) !== 0) { - $label = $device_name . ' (' . $device_id . ')'; + if ($online === true) { + return 'Green'; } - if ($device_code !== '') { - return $label . ' · ' . $device_code; + if ($online === false) { + return 'Red'; } - return $label; + return 'Yellow'; + } + + /** + * @param array $device + */ + private function extractDeviceType(array $device, string $device_code, ?array $catalog_entry = null): ?string + { + $device_type = $this->extractFirstString([ + $device['_dev_info']['model'] ?? null, + $device['_dev_info']['type'] ?? null, + $device['model'] ?? null, + $device['type'] ?? null, + $device['settings']['device']['type'] ?? null, + $catalog_entry['type'] ?? null, + $device_code, + ]); + + return $device_type !== '' ? $device_type : null; + } + + /** + * @param array $device + */ + private function extractControlType(array $device): string + { + $status = $this->normalizeToArray($device['status'] ?? null); + $settings = $this->normalizeToArray($device['settings'] ?? null); + + foreach (['switch:0', 'switch_0', 'switch0'] as $switch_key) { + if (array_key_exists($switch_key, $device) || array_key_exists($switch_key, $status)) { + return 'Switch'; + } + } + + if ((isset($device['switches']) && is_array($device['switches']) && $device['switches'] !== []) + || (isset($settings['switches']) && is_array($settings['switches']) && $settings['switches'] !== [])) { + return 'Switch'; + } + + if ((isset($device['relays']) && is_array($device['relays']) && $device['relays'] !== []) + || (isset($settings['relays']) && is_array($settings['relays']) && $settings['relays'] !== [])) { + return 'Relay'; + } + + return 'Device'; + } + + /** + * @param array $device + */ + private function extractControlName(array $device): string + { + $status = $this->normalizeToArray($device['status'] ?? null); + $settings = $this->normalizeToArray($device['settings'] ?? null); + + return $this->extractFirstString([ + $status['switch:0']['name'] ?? null, + $status['switch_0']['name'] ?? null, + $status['switch0']['name'] ?? null, + $device['switches'][0]['name'] ?? null, + $settings['switches'][0]['name'] ?? null, + $device['relays'][0]['name'] ?? null, + $settings['relays'][0]['name'] ?? null, + ]); + } + + private function buildRelayLabel( + ?string $device_type, + string $cloud_name, + string $local_device_name, + string $control_name, + string $device_id + ): string { + if ($cloud_name !== '') { + $display_name = $cloud_name; + } elseif ($local_device_name !== '' && $control_name !== '' && strcasecmp($local_device_name, $control_name) !== 0) { + $display_name = $local_device_name . ' / ' . $control_name; + } else { + $display_name = $control_name !== '' ? $control_name : ($local_device_name !== '' ? $local_device_name : $device_id); + } + + if ($device_type !== null && $device_type !== '') { + return $display_name . ' (' . $device_type . ')'; + } + + return $display_name; } /** @@ -219,6 +389,29 @@ class shelly_relay_inventory return ''; } + private function normalizeBoolean(mixed $value): ?bool + { + if (is_bool($value)) { + return $value; + } + + if (is_numeric($value)) { + return (int)$value === 1; + } + + return null; + } + + private static function statusSortWeight(string $status_color): int + { + return match (strtolower($status_color)) { + 'green' => 0, + 'yellow' => 1, + 'red' => 2, + default => 3, + }; + } + /** * @return array */ diff --git a/services/nginx/app/composer.json b/services/nginx/app/composer.json index ce38ddfc..dfaa72f9 100644 --- a/services/nginx/app/composer.json +++ b/services/nginx/app/composer.json @@ -7,6 +7,14 @@ "Composer\\Config::disableProcessTimeout", "@php -r \"putenv('RUN_API_TESTS=1'); passthru('vendor/bin/pest --testsuite=Api --colors=always', $exitCode); exit($exitCode);\"" ], + "test:api:edge": [ + "Composer\\Config::disableProcessTimeout", + "@php -r \"putenv('RUN_API_TESTS=1'); putenv('API_TEST_BOOTSTRAP_SCHEMA=1'); putenv('CONFIG_DB_TARGET=debug'); passthru('vendor/bin/pest tests/Api/EdgeGateway*ApiTest.php --colors=always', $exitCode); exit($exitCode);\"" + ], + "test:integration:edge": [ + "Composer\\Config::disableProcessTimeout", + "@php -r \"putenv('RUN_INTEGRATION_TESTS=1'); putenv('CONFIG_DB_TARGET=debug'); passthru('vendor/bin/pest tests/Integration/EdgeGateway --colors=always', $exitCode); exit($exitCode);\"" + ], "test:coverage": [ "@php -r \"is_dir('build/logs') || mkdir('build/logs', 0777, true);\"", "@php -r \"if (extension_loaded('pcov')) { passthru('php -d pcov.enabled=1 vendor/bin/pest --testsuite=Unit --coverage --coverage-clover build/logs/clover.xml --colors=always', $exitCode); exit($exitCode); } if (extension_loaded('xdebug')) { passthru('php -d xdebug.mode=coverage vendor/bin/pest --testsuite=Unit --coverage --coverage-clover build/logs/clover.xml --colors=always', $exitCode); exit($exitCode); } fwrite(STDERR, 'No coverage driver available. Enable pcov or xdebug for test:coverage.' . PHP_EOL); exit(1);\"" diff --git a/services/nginx/app/modules/edgegateway/classes/edge_gateway_manager.php b/services/nginx/app/modules/edgegateway/classes/edge_gateway_manager.php index 85967b2e..6b71f5fe 100644 --- a/services/nginx/app/modules/edgegateway/classes/edge_gateway_manager.php +++ b/services/nginx/app/modules/edgegateway/classes/edge_gateway_manager.php @@ -3959,11 +3959,19 @@ BASH; $channelStatus = self::deriveChannelStatus($gateway, $effectiveStatus, $now); $relayHealth = self::buildRelayHealth($gateway, $effectiveStatus, $now); $fallbackSummary = self::buildFallbackSummary($relayHealth); + $gateway['outbox_status'] = self::buildOutboxStatusSummary($gateway, $now); + $lastSyncAt = self::resolveLastSyncAt($gateway); $gateway['channel_status'] = $channelStatus; $gateway['relay_health'] = $relayHealth; $gateway['fallback_summary'] = $fallbackSummary; - $gateway['transport_health'] = self::deriveTransportHealth($effectiveStatus, $channelStatus, $fallbackSummary); + $gateway['transport_health'] = self::deriveTransportHealth( + $gateway, + $effectiveStatus, + $channelStatus, + $fallbackSummary, + $lastSyncAt + ); $gateway['last_successful_command_at'] = $gateway['operational_snapshot']['last_successful_command_at'] ?? self::findLatestCompletionTimestamp((array)($gateway['recent_commands'] ?? []), null); $gateway['last_successful_discovery_at'] = $gateway['operational_snapshot']['last_successful_discovery_at'] @@ -3976,8 +3984,7 @@ 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['last_sync_at'] = $lastSyncAt; $gateway['update_window'] = self::buildUpdateWindowSummary($gateway); $gateway['staged_version'] = self::buildStagedVersionSummary($gateway); $gateway['rollback_status'] = self::buildRollbackStatusSummary($gateway); @@ -4150,8 +4157,18 @@ BASH; return $summary; } - private static function deriveTransportHealth(string $effectiveStatus, array $channelStatus, array $fallbackSummary): array + private static function deriveTransportHealth( + array $gateway, + string $effectiveStatus, + array $channelStatus, + array $fallbackSummary, + ?string $lastSyncAt + ): array { + $metadata = isset($gateway['metadata']) && is_array($gateway['metadata']) ? (array)$gateway['metadata'] : []; + $controlPlaneStatus = isset($metadata['control_plane_status']) && is_array($metadata['control_plane_status']) + ? (array)$metadata['control_plane_status'] + : []; $brokerState = (string)($channelStatus['broker']['state'] ?? self::STATUS_OFFLINE); $affectedRelayCount = count((array)($fallbackSummary['affected_relays'] ?? [])); $transportState = $effectiveStatus; @@ -4169,6 +4186,13 @@ BASH; ? 'Broker fast path er aktiv med API polling som fallback' : 'API polling er aktiv som primær kontrolkanal'), 'recommended_action' => $fallbackSummary['recommended_action'] ?? ($channelStatus['broker']['last_error'] ?? null), + 'last_successful_sync_at' => $lastSyncAt, + 'last_transport_failure_at' => isset($controlPlaneStatus['last_transport_failure_at']) + ? (string)$controlPlaneStatus['last_transport_failure_at'] + : null, + 'last_transport_error' => isset($controlPlaneStatus['last_transport_error']) + ? (string)$controlPlaneStatus['last_transport_error'] + : null, ]; } @@ -4320,17 +4344,23 @@ BASH; 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']; - } - + $controlPlaneStatus = isset($metadata['control_plane_status']) && is_array($metadata['control_plane_status']) + ? (array)$metadata['control_plane_status'] + : []; $outbox = isset($gateway['outbox_status']) && is_array($gateway['outbox_status']) ? (array)$gateway['outbox_status'] : []; + $outboxMetadata = isset($metadata['outbox_status']) && is_array($metadata['outbox_status']) + ? (array)$metadata['outbox_status'] + : []; - return isset($outbox['last_replayed_at']) && $outbox['last_replayed_at'] !== null - ? (string)$outbox['last_replayed_at'] - : null; + return self::latestTimestamp([ + $controlPlaneStatus['last_successful_sync_at'] ?? null, + $metadata['last_sync_at'] ?? null, + $outbox['last_replayed_at'] ?? null, + $outboxMetadata['last_replayed_at'] ?? null, + $gateway['last_heartbeat_at'] ?? null, + ]); } private static function buildUpdateWindowSummary(array $gateway): array @@ -4551,6 +4581,33 @@ BASH; return null; } + /** + * @param array $timestamps + */ + private static function latestTimestamp(array $timestamps): ?string + { + $latestValue = null; + $latestEpoch = 0; + + foreach ($timestamps as $timestamp) { + if (!is_string($timestamp) || trim($timestamp) === '') { + continue; + } + + $epoch = strtotime($timestamp); + if ($epoch === false) { + continue; + } + + if ($latestValue === null || $epoch >= $latestEpoch) { + $latestValue = $timestamp; + $latestEpoch = $epoch; + } + } + + return $latestValue; + } + private static function normalizeFallbackMode(?string $fallbackMode): string { $normalized = strtoupper(trim((string)$fallbackMode)); diff --git a/services/nginx/app/resources/edge-gateway-agent/agent.php b/services/nginx/app/resources/edge-gateway-agent/agent.php index 7d29b5e5..979232a3 100644 --- a/services/nginx/app/resources/edge-gateway-agent/agent.php +++ b/services/nginx/app/resources/edge-gateway-agent/agent.php @@ -998,6 +998,8 @@ final class TruckwashEdgeAgent private const DEFAULT_UPDATE_WINDOW = '02:00-04:00'; private const OPERATION_COMPLETE_TIMEOUT_SECONDS = 120; private const BROKER_MESSAGE_PUMP_LIMIT = 12; + private const LOOP_STALE_AFTER_SECONDS = 30; + private const CONTROL_PLANE_SYNC_STALE_AFTER_SECONDS = 90; private AgentConfig $config; private HttpJsonClient $http; @@ -1011,6 +1013,7 @@ final class TruckwashEdgeAgent private string $statePath; private string $lastOperationSnapshotPath; private string $lastHeartbeatMarkerPath; + private string $controlPlaneStatusPath; private string $stagedUpdatePath; private int $lastHeartbeatAt = 0; private string $agentInstanceId; @@ -1031,12 +1034,14 @@ final class TruckwashEdgeAgent $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->controlPlaneStatusPath = $this->runtimeDir . DIRECTORY_SEPARATOR . 'control-plane-status.json'; $this->stagedUpdatePath = $this->runtimeDir . DIRECTORY_SEPARATOR . 'staged-update.json'; $this->agentInstanceId = $this->ensureAgentInstanceId(); $this->brokerClient = new BrokerWebSocketClient($this->logger); $this->shellBridge = new AgentShellBridge($this->installDir); $this->logger->setSink(fn(string $level, string $message): bool => $this->emitLogFrame($level, $message)); $this->configureBrokerClient(); + $this->initializeControlPlaneStatus(); } public function run(): void @@ -1046,6 +1051,7 @@ final class TruckwashEdgeAgent while (true) { try { + $this->touchLoopHeartbeat(); $this->reloadConfigFromDisk(); $this->ensureClaimed(); $this->configureBrokerClient(); @@ -1081,23 +1087,29 @@ final class TruckwashEdgeAgent } $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' => $installedVersion, - 'metadata' => [ - '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(), - ], - ]); + try { + $response = $this->http->post('/edge-agent/claim', [ + 'token' => (string)$this->config->get('installToken'), + 'hostname' => gethostname() ?: 'truckwash-edge', + 'installed_version' => $installedVersion, + 'metadata' => [ + '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->currentLastSuccessfulSyncAt(), + 'control_plane_status' => $this->buildControlPlaneStatusPayload(), + 'rollback_status' => $this->readRollbackStatus(), + 'staged_version' => $this->currentStagedUpdate(), + ], + ]); + } catch (Throwable $throwable) { + $this->recordTransportFailure('Gateway claim failed', $throwable); + throw $throwable; + } $payload = $response['data'] ?? []; $gateway = is_array($payload['gateway'] ?? null) ? (array)$payload['gateway'] : []; @@ -1110,7 +1122,7 @@ final class TruckwashEdgeAgent $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->recordSuccessfulSync(); $this->logger->info('Claimed gateway ' . (string)($gateway['id'] ?? 'unknown') . '.'); } @@ -1145,7 +1157,7 @@ final class TruckwashEdgeAgent { $sent = $this->brokerClient->send($message); if ($sent) { - $this->stateStore->setJson('last_sync_at', date('c')); + $this->recordSuccessfulSync(); } return $sent; } @@ -1224,6 +1236,7 @@ final class TruckwashEdgeAgent return; } + $this->recordHeartbeatAttempt(); $operationState = $this->readOperationState(); $payload = [ 'agent_token' => (string)$this->config->get('agentToken'), @@ -1242,7 +1255,8 @@ final class TruckwashEdgeAgent 'system_metrics' => $this->buildSystemMetrics(), 'container_health' => $this->buildContainerHealth(), 'outbox_status' => $this->buildOutboxStatus(), - 'last_sync_at' => $this->stateStore->getJson('last_sync_at'), + 'last_sync_at' => $this->currentLastSuccessfulSyncAt(), + 'control_plane_status' => $this->buildControlPlaneStatusPayload(), 'update_window' => (string)$this->config->get('updateWindow', self::DEFAULT_UPDATE_WINDOW), 'staged_version' => $this->currentStagedUpdate(), 'rollback_status' => $this->readRollbackStatus(), @@ -1265,10 +1279,14 @@ final class TruckwashEdgeAgent return; } + $heartbeatSucceededAt = date('c'); $this->lastHeartbeatAt = time(); + $this->recordSuccessfulSync($heartbeatSucceededAt, [ + 'last_heartbeat_success_at' => $heartbeatSucceededAt, + ]); file_put_contents($this->lastHeartbeatMarkerPath, json_encode([ 'gateway_id' => $gatewayId, - 'at' => date('c'), + 'at' => $heartbeatSucceededAt, 'agent_instance_id' => $this->agentInstanceId, ], JSON_UNESCAPED_SLASHES) . PHP_EOL); } @@ -1991,6 +2009,143 @@ final class TruckwashEdgeAgent ]; } + private function initializeControlPlaneStatus(): void + { + $this->writeControlPlaneStatus(); + } + + private function touchLoopHeartbeat(): void + { + $this->writeControlPlaneStatus([ + 'last_loop_at' => date('c'), + ]); + } + + private function currentLastSuccessfulSyncAt(): ?string + { + $current = $this->readControlPlaneStatus()['last_successful_sync_at'] ?? $this->stateStore->getJson('last_sync_at'); + return $this->normalizeControlPlaneStatusTimestamp($current); + } + + private function recordHeartbeatAttempt(): string + { + $attemptedAt = date('c'); + $this->writeControlPlaneStatus([ + 'last_heartbeat_attempt_at' => $attemptedAt, + ]); + return $attemptedAt; + } + + private function recordSuccessfulSync(?string $at = null, array $statusOverrides = []): void + { + $syncedAt = $this->normalizeControlPlaneStatusTimestamp($at ?? date('c')) ?? date('c'); + $this->stateStore->setJson('last_sync_at', $syncedAt); + $statusOverrides['last_successful_sync_at'] = $syncedAt; + $this->writeControlPlaneStatus($statusOverrides); + } + + private function recordTransportFailure(string $context, Throwable $throwable): void + { + $message = $this->normalizeControlPlaneStatusString($throwable->getMessage()) ?? $throwable::class; + $this->writeControlPlaneStatus([ + 'last_transport_failure_at' => date('c'), + 'last_transport_error' => $message, + ]); + $this->logger->warning($context . ': ' . $message); + } + + /** + * @return array + */ + private function buildControlPlaneStatusPayload(): array + { + return $this->writeControlPlaneStatus(); + } + + /** + * @return array + */ + private function readControlPlaneStatus(): array + { + if (!is_file($this->controlPlaneStatusPath)) { + return []; + } + + $decoded = json_decode((string)file_get_contents($this->controlPlaneStatusPath), true); + return is_array($decoded) ? $decoded : []; + } + + /** + * @param array $overrides + * @return array + */ + private function writeControlPlaneStatus(array $overrides = []): array + { + $current = $this->readControlPlaneStatus(); + $outboxSummary = $this->stateStore->outboxSummary(); + $lastSuccessfulSyncAt = array_key_exists('last_successful_sync_at', $overrides) + ? $overrides['last_successful_sync_at'] + : ($current['last_successful_sync_at'] ?? $this->stateStore->getJson('last_sync_at')); + $lastTransportError = array_key_exists('last_transport_error', $overrides) + ? $overrides['last_transport_error'] + : ($current['last_transport_error'] ?? null); + $lastTransportFailureAt = array_key_exists('last_transport_failure_at', $overrides) + ? $overrides['last_transport_failure_at'] + : ($current['last_transport_failure_at'] ?? null); + $lastHeartbeatAttemptAt = array_key_exists('last_heartbeat_attempt_at', $overrides) + ? $overrides['last_heartbeat_attempt_at'] + : ($current['last_heartbeat_attempt_at'] ?? null); + $lastHeartbeatSuccessAt = array_key_exists('last_heartbeat_success_at', $overrides) + ? $overrides['last_heartbeat_success_at'] + : ($current['last_heartbeat_success_at'] ?? null); + $lastLoopAt = array_key_exists('last_loop_at', $overrides) + ? $overrides['last_loop_at'] + : ($current['last_loop_at'] ?? null); + + $status = [ + 'started_at' => $this->normalizeControlPlaneStatusTimestamp($current['started_at'] ?? null) ?? date('c'), + 'agent_instance_id' => $this->agentInstanceId, + 'last_loop_at' => $this->normalizeControlPlaneStatusTimestamp($lastLoopAt), + 'last_heartbeat_attempt_at' => $this->normalizeControlPlaneStatusTimestamp($lastHeartbeatAttemptAt), + 'last_heartbeat_success_at' => $this->normalizeControlPlaneStatusTimestamp($lastHeartbeatSuccessAt), + 'last_successful_sync_at' => $this->normalizeControlPlaneStatusTimestamp($lastSuccessfulSyncAt), + 'outbox_queued' => array_key_exists('outbox_queued', $overrides) + ? max(0, (int)$overrides['outbox_queued']) + : max(0, (int)($outboxSummary['queued'] ?? 0)), + 'broker_connected' => array_key_exists('broker_connected', $overrides) + ? (bool)$overrides['broker_connected'] + : $this->isBrokerConnected(), + 'last_transport_error' => $this->normalizeControlPlaneStatusString($lastTransportError), + 'last_transport_failure_at' => $this->normalizeControlPlaneStatusTimestamp($lastTransportFailureAt), + ]; + + file_put_contents( + $this->controlPlaneStatusPath, + json_encode($status, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL + ); + + return $status; + } + + private function normalizeControlPlaneStatusTimestamp(mixed $value): ?string + { + return is_string($value) && trim($value) !== '' ? trim($value) : null; + } + + private function normalizeControlPlaneStatusString(mixed $value): ?string + { + if (is_string($value)) { + $normalized = trim($value); + return $normalized !== '' ? $normalized : null; + } + if (is_scalar($value)) { + $normalized = trim((string)$value); + return $normalized !== '' ? $normalized : null; + } + + return null; + } + private function flushOutbox(): void { $items = $this->stateStore->queuedItems(25); @@ -2006,9 +2161,12 @@ final class TruckwashEdgeAgent $this->http->post($endpoint, $payload, $timeoutSeconds); } $this->stateStore->removeOutboxItem((int)$item['id']); - $this->stateStore->setJson('last_sync_at', date('c')); + $this->recordSuccessfulSync(); } catch (Throwable $throwable) { - $this->logger->warning('Outbox replay blocked on ' . (string)$item['type'] . ': ' . $throwable->getMessage()); + $this->recordTransportFailure( + 'Outbox replay blocked on ' . (string)$item['type'] . ' for ' . (string)$item['endpoint'], + $throwable + ); break; } } @@ -2106,17 +2264,19 @@ final class TruckwashEdgeAgent { $brokerDispatch = $this->dispatchBrokerControlPlaneEvent($endpoint, $payload); if ($brokerDispatch === true) { - $this->stateStore->setJson('last_sync_at', date('c')); return true; } try { $this->http->post($endpoint, $payload, 20); - $this->stateStore->setJson('last_sync_at', date('c')); + $this->recordSuccessfulSync(); return true; } catch (Throwable $throwable) { $this->stateStore->enqueue($type, $endpoint, $payload); - $this->logger->warning('Queued ' . $type . ' to local outbox after transport failure: ' . $throwable->getMessage()); + $this->recordTransportFailure( + 'Queued ' . $type . ' to local outbox after transport failure on ' . $endpoint, + $throwable + ); return false; } } @@ -2125,17 +2285,19 @@ final class TruckwashEdgeAgent { $brokerDispatch = $this->dispatchBrokerControlPlaneEvent($endpoint, $payload); if ($brokerDispatch === true) { - $this->stateStore->setJson('last_sync_at', date('c')); return ['data' => ['status' => 'ACKNOWLEDGED']]; } try { $response = $this->http->post($endpoint, $payload, $timeoutSeconds); - $this->stateStore->setJson('last_sync_at', date('c')); + $this->recordSuccessfulSync(); return is_array($response) ? $response : null; } catch (Throwable $throwable) { $this->stateStore->enqueue($type, $endpoint, $payload); - $this->logger->warning('Queued ' . $type . ' to local outbox after transport failure: ' . $throwable->getMessage()); + $this->recordTransportFailure( + 'Queued ' . $type . ' to local outbox after transport failure on ' . $endpoint, + $throwable + ); return null; } } @@ -2356,22 +2518,27 @@ final class TruckwashEdgeAgent $brokerDispatch = $this->dispatchBrokerControlPlaneEvent($endpoint, $payload); if ($brokerDispatch === true) { - $this->stateStore->setJson('last_sync_at', date('c')); return true; } try { $this->http->post($endpoint, $payload, self::OPERATION_COMPLETE_TIMEOUT_SECONDS); - $this->stateStore->setJson('last_sync_at', date('c')); + $this->recordSuccessfulSync(); return true; } catch (Throwable $throwable) { if ($queueOnFailure) { $this->stateStore->enqueue('operation_complete', $endpoint, $payload); - $this->logger->warning('Queued operation_complete to local outbox after completion acknowledgement failure: ' . $throwable->getMessage()); + $this->recordTransportFailure( + 'Queued operation_complete to local outbox after completion acknowledgement failure on ' . $endpoint, + $throwable + ); return false; } - $this->logger->warning('Retrying backend completion acknowledgement later: ' . $throwable->getMessage()); + $this->recordTransportFailure( + 'Retrying backend completion acknowledgement later for ' . $endpoint, + $throwable + ); return false; } } 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 index ccd68a6b..67caedb4 100644 --- a/services/nginx/app/resources/edge-gateway-agent/docker-compose.gateway.yml +++ b/services/nginx/app/resources/edge-gateway-agent/docker-compose.gateway.yml @@ -89,7 +89,11 @@ services: - ./config.json:/config/config.json - ./runtime:/opt/truckwash-edge-agent/runtime healthcheck: - test: ["CMD-SHELL", "kill -0 1"] + test: + [ + "CMD-SHELL", + "php -r '$$path=\"/opt/truckwash-edge-agent/runtime/control-plane-status.json\"; if (!is_file($$path)) { exit(1); } $$data=json_decode((string)file_get_contents($$path), true); if (!is_array($$data)) { exit(1); } $$loopAt=strtotime((string)($$data[\"last_loop_at\"] ?? \"\")); $$syncAt=strtotime((string)($$data[\"last_successful_sync_at\"] ?? $$data[\"started_at\"] ?? \"\")); if ($$loopAt === false || $$syncAt === false) { exit(1); } $$now=time(); exit((($$now - $$loopAt) <= 30 && ($$now - $$syncAt) <= 90) ? 0 : 1);'", + ] interval: 30s timeout: 5s retries: 3 diff --git a/services/nginx/app/routes/departmentLanesRoute.php b/services/nginx/app/routes/departmentLanesRoute.php index 6b0cfa9f..96497cc2 100644 --- a/services/nginx/app/routes/departmentLanesRoute.php +++ b/services/nginx/app/routes/departmentLanesRoute.php @@ -302,11 +302,11 @@ class departmentLanesRoute // Get the request data $name = $response->getRequestParameter('name') ?? null; $department = $response->getRequestParameter('department') ?? null; - $relay_in_id = $response->getRequestParameter('relay_in_id') ?? null; - $relay_out_id = $response->getRequestParameter('relay_out_id') ?? null; - $relay_machine_id = $response->getRequestParameter('relay_machine_id') ?? null; - $relay_machine_program_picker_id = $response->getRequestParameter('relay_machine_program_picker_id') ?? null; - $relay_machine_cleaner_id = $response->getRequestParameter('relay_machine_cleaner_id') ?? null; + $relay_in_id = self::normalizeRelayRequestParameter($response->getRequestParameter('relay_in_id') ?? null); + $relay_out_id = self::normalizeRelayRequestParameter($response->getRequestParameter('relay_out_id') ?? null); + $relay_machine_id = self::normalizeRelayRequestParameter($response->getRequestParameter('relay_machine_id') ?? null); + $relay_machine_program_picker_id = self::normalizeRelayRequestParameter($response->getRequestParameter('relay_machine_program_picker_id') ?? null); + $relay_machine_cleaner_id = self::normalizeRelayRequestParameter($response->getRequestParameter('relay_machine_cleaner_id') ?? null); $dynamic_image_id = $response->getRequestParameter('dynamic_image_id') ?? null; $machine_type_id = $response->getRequestParameter('machine_type_id') ?? null; @@ -323,7 +323,6 @@ class departmentLanesRoute } else { $machine_type_id = null; } - // Remove spaces from the relay_in_id and relay_out_id // Check if the required fields are set if ($name && $department) { // Add the department lane @@ -362,11 +361,11 @@ class departmentLanesRoute $id = $response->getRequestParameter('id') ?? null; $name = $response->getRequestParameter('name') ?? null; $department = $response->getRequestParameter('department') ?? null; - $relay_in_id = $response->getRequestParameter('relay_in_id') ?? null; - $relay_out_id = $response->getRequestParameter('relay_out_id') ?? null; - $relay_machine_id = $response->getRequestParameter('relay_machine_id') ?? null; - $relay_machine_program_picker_id = $response->getRequestParameter('relay_machine_program_picker_id') ?? null; - $relay_machine_cleaner_id = $response->getRequestParameter('relay_machine_cleaner_id') ?? null; + $relay_in_id = self::normalizeRelayRequestParameter($response->getRequestParameter('relay_in_id') ?? null); + $relay_out_id = self::normalizeRelayRequestParameter($response->getRequestParameter('relay_out_id') ?? null); + $relay_machine_id = self::normalizeRelayRequestParameter($response->getRequestParameter('relay_machine_id') ?? null); + $relay_machine_program_picker_id = self::normalizeRelayRequestParameter($response->getRequestParameter('relay_machine_program_picker_id') ?? null); + $relay_machine_cleaner_id = self::normalizeRelayRequestParameter($response->getRequestParameter('relay_machine_cleaner_id') ?? null); $dynamic_image_id = $response->getRequestParameter('dynamic_image_id') ?? null; $machine_type_id = $response->getRequestParameter('machine_type_id') ?? null; @@ -387,19 +386,19 @@ class departmentLanesRoute $department_lane->department->set((int)$department); } if (self::isParametersSet(['relay_in_id'])) { - $department_lane->relay_in_id->set((string)$relay_in_id); + self::syncDepartmentLaneRelayValue($department_lane->relay_in_id, $relay_in_id); } if (self::isParametersSet(['relay_out_id'])) { - $department_lane->relay_out_id->set((string)$relay_out_id); + self::syncDepartmentLaneRelayValue($department_lane->relay_out_id, $relay_out_id); } if (self::isParametersSet(['relay_machine_id'])) { - $department_lane->relay_machine_id->set((string)$relay_machine_id); + self::syncDepartmentLaneRelayValue($department_lane->relay_machine_id, $relay_machine_id); } if (self::isParametersSet(['relay_machine_program_picker_id'])) { - $department_lane->relay_machine_program_picker_id->set((string)$relay_machine_program_picker_id); + self::syncDepartmentLaneRelayValue($department_lane->relay_machine_program_picker_id, $relay_machine_program_picker_id); } if (self::isParametersSet(['relay_machine_cleaner_id'])) { - $department_lane->relay_machine_cleaner_id->set((string)$relay_machine_cleaner_id); + self::syncDepartmentLaneRelayValue($department_lane->relay_machine_cleaner_id, $relay_machine_cleaner_id); } if (self::isParametersSet(['dynamic_image_id'])) { $param = $response->getRequestParameter('dynamic_image_id'); @@ -437,4 +436,25 @@ class departmentLanesRoute ] ); } + + private static function normalizeRelayRequestParameter(mixed $value): ?string + { + $normalized = trim((string)($value ?? '')); + if ($normalized === '' || strtolower($normalized) === 'null') { + return null; + } + + return $normalized; + } + + private static function syncDepartmentLaneRelayValue(mixed $field, mixed $value): void + { + $normalized = self::normalizeRelayRequestParameter($value); + if ($normalized === null) { + $field->nullify(); + return; + } + + $field->set($normalized); + } } diff --git a/services/nginx/app/tests/Api/EdgeGatewayAgentApiTest.php b/services/nginx/app/tests/Api/EdgeGatewayAgentApiTest.php new file mode 100644 index 00000000..61a13609 --- /dev/null +++ b/services/nginx/app/tests/Api/EdgeGatewayAgentApiTest.php @@ -0,0 +1,334 @@ +createDepartment([ + 'name' => 'Edge Agent Department', + ]); + $session = api_fixtures()->createEdgeOperatorSession((int)$department['id']); + $installTokenResponse = api_client()->post('/edge-gateways/install-token', [ + 'department_id' => (int)$department['id'], + 'label' => 'Edge Agent Install', + ], $session['headers']); + + $installTokenResponse + ->assertStatus(201) + ->assertEnvelope() + ->assertSuccess(); + + $installToken = $installTokenResponse->data(); + + $installScript = api_client()->get('/edge-agent/install.sh?token=' . urlencode((string)$installToken['token'])); + + expect($installScript->status)->toBe(200) + ->and($installScript->body) + ->toContain('/edge-agent/install-token/status') + ->toContain('agent.php') + ->toContain((string)$installToken['token']); + + $artifact = api_client()->get('/edge-agent/artifacts/agent.php'); + expect($artifact->status)->toBe(200) + ->and($artifact->body) + ->toContain('post('/edge-agent/claim', [ + 'token' => (string)$installToken['token'], + 'hostname' => 'edge-agent-api', + 'installed_version' => 'php-agent-v1', + ]); + + $claimResponse + ->assertStatus(201) + ->assertEnvelope() + ->assertSuccess(); + + $gatewayId = (int)($claimResponse->data()['gateway']['id'] ?? 0); + $agentToken = (string)($claimResponse->data()['agent_token'] ?? ''); + + expect($gatewayId)->toBeGreaterThan(0) + ->and($agentToken)->not->toBe(''); + + edge_agent_test_set_heartbeat_age($gatewayId, edge_gateway_manager::HEARTBEAT_DEGRADED_AFTER_SECONDS + 1); + + $degradedDetail = api_client()->get('/edge-gateways/' . $gatewayId, $session['headers']); + + $degradedDetail + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($degradedDetail->data()) + ->toHaveKey('status', 'DEGRADED'); + + edge_agent_test_set_heartbeat_age($gatewayId, edge_gateway_manager::HEARTBEAT_OFFLINE_AFTER_SECONDS + 1); + + $offlineDetail = api_client()->get('/edge-gateways/' . $gatewayId, $session['headers']); + + $offlineDetail + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($offlineDetail->data()) + ->toHaveKey('status', 'OFFLINE'); + + $heartbeatResponse = api_client()->post('/edge-agent/gateways/' . $gatewayId . '/heartbeat', [ + 'agent_token' => $agentToken, + 'status' => 'ONLINE', + 'hostname' => 'edge-agent-api', + 'metadata' => [ + 'system_metrics' => [ + 'cpu_percent' => 21, + 'memory_mb' => 128, + ], + ], + 'inventory' => edge_agent_test_inventory('heartbeat'), + ]); + + $heartbeatResponse + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($heartbeatResponse->data()) + ->toHaveKey('status', 'ONLINE'); + + $recoveredDetail = api_client()->get('/edge-gateways/' . $gatewayId, $session['headers']); + + $recoveredDetail + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($recoveredDetail->data()) + ->toHaveKey('status', 'ONLINE') + ->and($recoveredDetail->data()['metadata']['system_metrics']['cpu_percent'] ?? null) + ->toBe(21); +}); + +it('polls operations and commands, submits results, and records broker presence for task pages', function (): void { + $department = api_fixtures()->createDepartment([ + 'name' => 'Edge Agent Operations Department', + ]); + $session = api_fixtures()->createEdgeOperatorSession((int)$department['id']); + $gateway = api_fixtures()->createClaimedEdgeGateway([ + 'department_id' => (int)$department['id'], + 'label' => 'Edge Agent Runtime', + ]); + + $operationResponse = api_client()->post('/edge-gateways/' . (int)$gateway['id'] . '/operations', [ + 'type' => 'DISCOVERY', + 'request' => [ + 'inventory' => edge_agent_test_inventory('operation'), + ], + ], $session['headers']); + + $operationResponse + ->assertStatus(201) + ->assertEnvelope() + ->assertSuccess(); + + $operationId = (int)($operationResponse->data()['operation']['id'] ?? 0); + expect($operationId)->toBeGreaterThan(0); + + $operationLease = api_client()->post('/edge-agent/gateways/' . (int)$gateway['id'] . '/operations/next', [ + 'agent_token' => (string)$gateway['agent_token'], + 'wait_seconds' => 0, + 'agent_instance_id' => 'edge-agent-api-test', + ]); + + $operationLease + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($operationLease->data()) + ->toHaveKey('id', $operationId) + ->toHaveKey('status', 'IN_PROGRESS'); + + $eventResponse = api_client()->post( + '/edge-agent/gateways/' . (int)$gateway['id'] . '/operations/' . $operationId . '/events', + [ + 'agent_token' => (string)$gateway['agent_token'], + 'level' => 'INFO', + 'code' => 'DISCOVERY_RUNNING', + 'message' => 'Discovery is running through the agent API.', + 'context' => [ + 'progress' => 55, + 'label' => 'Discovery running', + ], + ] + ); + + $eventResponse + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + $completeResponse = api_client()->post( + '/edge-agent/gateways/' . (int)$gateway['id'] . '/operations/' . $operationId . '/complete', + [ + 'agent_token' => (string)$gateway['agent_token'], + 'ok' => true, + 'result' => [ + 'inventory' => edge_agent_test_inventory('completed'), + ], + ] + ); + + $completeResponse + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($completeResponse->data()) + ->toHaveKey('status', 'COMPLETED'); + + $job = api_fixtures()->createEdgeCommandJob([ + 'gateway_id' => (int)$gateway['id'], + 'command_type' => 'GET_RELAY_STATUS', + 'request' => [ + 'relayId' => 'relay-main', + 'localIp' => '10.0.0.18', + ], + ]); + + $commandPoll = api_client()->post('/edge-agent/gateways/' . (int)$gateway['id'] . '/commands/poll', [ + 'agent_token' => (string)$gateway['agent_token'], + 'wait_seconds' => 0, + ]); + + $commandPoll + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($commandPoll->data()) + ->toHaveKey('id', (int)$job['id']) + ->toHaveKey('command_type', 'GET_RELAY_STATUS'); + + $commandResult = api_client()->post( + '/edge-agent/gateways/' . (int)$gateway['id'] . '/commands/' . (int)$job['id'] . '/result', + [ + 'agent_token' => (string)$gateway['agent_token'], + 'ok' => true, + 'result' => [ + 'relayId' => 'relay-main', + 'online' => true, + ], + ] + ); + + $commandResult + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($commandResult->data()) + ->toHaveKey('acknowledged', true) + ->and($commandResult->data()['job']['status'] ?? null) + ->toBe('COMPLETED'); + + $presenceResponse = api_client()->post('/edge-agent/gateways/' . (int)$gateway['id'] . '/presence', [ + 'agent_token' => (string)$gateway['agent_token'], + 'status' => 'connected', + 'connection_id' => 'broker-connection-1', + 'metadata' => [ + 'transport' => 'ws', + ], + ]); + + $presenceResponse + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($presenceResponse->data()) + ->toHaveKey('connected', true); + + $tasksPage = api_client()->get('/edge-gateways/' . (int)$gateway['id'] . '/tasks', $session['headers']); + + $tasksPage + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($tasksPage->data()['operations'] ?? []) + ->toBeArray() + ->not->toBeEmpty() + ->and($tasksPage->data()['recent_commands'] ?? []) + ->toBeArray() + ->not->toBeEmpty(); + + expect($tasksPage->data()['recent_operations_summary']['completed'] ?? 0) + ->toBeGreaterThanOrEqual(1); +}); + +it('rejects missing and invalid edge agent tokens', function (): void { + $department = api_fixtures()->createDepartment([ + 'name' => 'Edge Agent Auth Department', + ]); + $gateway = api_fixtures()->createClaimedEdgeGateway([ + 'department_id' => (int)$department['id'], + ]); + + $missingToken = api_client()->get('/edge-agent/install-token/verify'); + + $missingToken + ->assertStatus(400) + ->assertEnvelope() + ->assertSuccess(false) + ->assertMessage('Missing token'); + + $missingAgentToken = api_client()->post('/edge-agent/gateways/' . (int)$gateway['id'] . '/heartbeat', [ + 'status' => 'ONLINE', + ]); + + $missingAgentToken + ->assertStatus(401) + ->assertEnvelope() + ->assertSuccess(false) + ->assertMessage('Missing edge gateway agent token'); + + $invalidAgentToken = api_client()->post('/edge-agent/gateways/' . (int)$gateway['id'] . '/operations/next', [ + 'agent_token' => 'invalid-token', + 'wait_seconds' => 0, + ]); + + $invalidAgentToken + ->assertStatus(401) + ->assertEnvelope() + ->assertSuccess(false) + ->assertMessage('Invalid edge gateway token'); +}); + +function edge_agent_test_set_heartbeat_age(int $gatewayId, int $secondsAgo): void +{ + $db = api_test_runtime()->db(); + $timestamp = date('Y-m-d H:i:s', time() - max(0, $secondsAgo)); + $escapedTimestamp = $db->real_escape_string($timestamp); + $db->query("UPDATE edge_gateways SET last_heartbeat_at = '{$escapedTimestamp}', status = 'ONLINE' WHERE id = " . (int)$gatewayId); + api_fixtures()->clearEdgeGatewayViewCache(); +} + +function edge_agent_test_inventory(string $suffix): array +{ + return [[ + 'device_id' => 'agent-' . $suffix, + 'local_ip' => '10.30.40.50', + 'model' => 'TruckWash Edge Agent', + 'channel_count' => 1, + 'online' => true, + 'capabilities' => [ + 'gateway_management_v2' => true, + ], + 'metadata' => [ + 'hostname' => 'edge-' . $suffix, + ], + ]]; +} diff --git a/services/nginx/app/tests/Api/EdgeGatewayBrokerApiTest.php b/services/nginx/app/tests/Api/EdgeGatewayBrokerApiTest.php new file mode 100644 index 00000000..dae8e90a --- /dev/null +++ b/services/nginx/app/tests/Api/EdgeGatewayBrokerApiTest.php @@ -0,0 +1,389 @@ +createDepartment([ + 'name' => 'Edge Broker Department', + ]); + $session = api_fixtures()->createEdgeOperatorSession((int)$department['id']); + $gateway = api_fixtures()->createClaimedEdgeGateway([ + 'department_id' => (int)$department['id'], + 'label' => 'Broker Gateway', + ]); + + $validateGateway = api_client()->post( + '/edge-agent/internal/gateways/' . (int)$gateway['id'] . '/validate', + ['token' => (string)$gateway['agent_token']], + edge_test_broker_headers() + ); + + $validateGateway + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($validateGateway->data()) + ->toHaveKey('gateway_id', (int)$gateway['id']) + ->toHaveKey('department_id', (int)$department['id']); + + $presence = api_client()->post( + '/edge-agent/internal/gateways/' . (int)$gateway['id'] . '/presence', + [ + 'status' => 'connected', + 'connection_id' => 'broker-presence-1', + 'metadata' => [ + 'transport' => 'ws', + ], + ], + edge_test_broker_headers() + ); + + $presence + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($presence->data()) + ->toHaveKey('connected', true) + ->toHaveKey('connection_id', 'broker-presence-1'); + + $telemetry = api_client()->post( + '/edge-agent/internal/gateways/' . (int)$gateway['id'] . '/telemetry', + [ + 'status' => 'ONLINE', + 'metadata' => [ + 'system_metrics' => [ + 'cpu_load' => 0.42, + 'memory_mb' => 512, + ], + ], + 'inventory' => edge_broker_test_inventory('telemetry'), + ], + edge_test_broker_headers() + ); + + $telemetry + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($telemetry->data()['metadata']['system_metrics']['cpu_load'] ?? null) + ->toBe(0.42); + + $logEntry = api_client()->post( + '/edge-agent/internal/gateways/' . (int)$gateway['id'] . '/logs', + [ + 'level' => 'INFO', + 'stream' => 'agent', + 'source' => 'BROKER', + 'message' => 'Broker forwarded a live gateway log.', + 'context' => [ + 'source' => 'broker-test', + ], + ], + edge_test_broker_headers() + ); + + $logEntry + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($logEntry->data()) + ->toHaveKey('message', 'Broker forwarded a live gateway log.'); + + $streamSession = api_client()->post( + '/edge-gateways/' . (int)$gateway['id'] . '/stream-session', + ['scopes' => ['logs', 'statistics', 'tasks']], + $session['headers'] + ); + + $streamSession + ->assertStatus(201) + ->assertEnvelope() + ->assertSuccess(); + + $streamValidation = api_client()->post( + '/edge-agent/internal/browser-streams/validate', + ['token' => (string)$streamSession->data()['token']], + edge_test_broker_headers() + ); + + $streamValidation + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($streamValidation->data()) + ->toHaveKey('session_type', 'gateway-stream') + ->toHaveKey('gateway_id', (int)$gateway['id']); + + $shellSession = api_client()->post( + '/edge-gateways/' . (int)$gateway['id'] . '/shell-sessions', + ['reason' => 'Broker shell validation'], + $session['headers'] + ); + + $shellSession + ->assertStatus(201) + ->assertEnvelope() + ->assertSuccess(); + + $shellToken = (string)$shellSession->data()['token']; + + $validateShell = api_client()->post( + '/edge-agent/internal/shell-sessions/validate', + ['token' => $shellToken], + edge_test_broker_headers() + ); + + $validateShell + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($validateShell->data()) + ->toHaveKey('status', 'PENDING'); + + $openedShell = api_client()->post( + '/edge-agent/internal/shell-sessions/opened', + [ + 'token' => $shellToken, + 'connection_id' => 'shell-connection-1', + ], + edge_test_broker_headers() + ); + + $openedShell + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($openedShell->data()) + ->toHaveKey('status', 'OPEN') + ->toHaveKey('connection_id', 'shell-connection-1'); + + $closedShell = api_client()->post( + '/edge-agent/internal/shell-sessions/close', + [ + 'token' => $shellToken, + 'transcript' => "edge-broker-shell\n", + 'reason' => 'agent_exit', + ], + edge_test_broker_headers() + ); + + $closedShell + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($closedShell->data()) + ->toHaveKey('status', 'COMPLETED') + ->and($closedShell->data()['transcript'] ?? null) + ->toBe("edge-broker-shell\n"); + + $logsPage = api_client()->get('/edge-gateways/' . (int)$gateway['id'] . '/logs', $session['headers']); + + $logsPage + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect(collect_gateway_messages($logsPage->data()['log_entries'] ?? [])) + ->toContain('Broker forwarded a live gateway log.'); + expect(collect_gateway_messages($logsPage->data()['timeline'] ?? [])) + ->toContain('GATEWAY_SHELL_SESSION_OPENED') + ->toContain('GATEWAY_SHELL_SESSION_CLOSED'); + expect($logsPage->data()['shell_sessions'][0]['transcript'] ?? null) + ->toBe("edge-broker-shell\n"); + + $statisticsPage = api_client()->get('/edge-gateways/' . (int)$gateway['id'] . '/statistics', $session['headers']); + + $statisticsPage + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($statisticsPage->data()) + ->toHaveKey('system_metrics') + ->and($statisticsPage->data()['system_metrics']['cpu_load'] ?? null) + ->toBe(0.42); +}); + +it('builds broker backlog and completes gateway operations through broker endpoints', function (): void { + $department = api_fixtures()->createDepartment([ + 'name' => 'Edge Broker Backlog Department', + ]); + $session = api_fixtures()->createEdgeOperatorSession((int)$department['id']); + $gateway = api_fixtures()->createClaimedEdgeGateway([ + 'department_id' => (int)$department['id'], + 'label' => 'Broker Backlog Gateway', + ]); + + $queuedOperation = api_client()->post('/edge-gateways/' . (int)$gateway['id'] . '/operations', [ + 'type' => 'DISCOVERY', + 'request' => [ + 'inventory' => edge_broker_test_inventory('backlog'), + ], + ], $session['headers']); + + $queuedOperation + ->assertStatus(201) + ->assertEnvelope() + ->assertSuccess(); + + $operationId = (int)($queuedOperation->data()['operation']['id'] ?? 0); + expect($operationId)->toBeGreaterThan(0); + + $backlog = api_client()->post( + '/edge-agent/internal/gateways/' . (int)$gateway['id'] . '/backlog', + ['agent_instance_id' => 'broker-agent-1'], + edge_test_broker_headers() + ); + + $backlog + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($backlog->data()['dispatch'] ?? []) + ->toBeArray() + ->not->toBeEmpty() + ->and($backlog->data()['dispatch'][0]['type'] ?? null) + ->toBe('TASK_DISPATCH') + ->and((int)($backlog->data()['dispatch'][0]['operation']['id'] ?? 0)) + ->toBe($operationId); + + $event = api_client()->post( + '/edge-agent/internal/gateways/' . (int)$gateway['id'] . '/operations/' . $operationId . '/events', + [ + 'level' => 'INFO', + 'code' => 'BROKER_EXECUTING', + 'message' => 'Broker is executing the operation.', + 'context' => [ + 'progress' => 50, + ], + ], + edge_test_broker_headers() + ); + + $event + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + $complete = api_client()->post( + '/edge-agent/internal/gateways/' . (int)$gateway['id'] . '/operations/' . $operationId . '/complete', + [ + 'ok' => true, + 'result' => [ + 'inventory' => edge_broker_test_inventory('completed'), + ], + ], + edge_test_broker_headers() + ); + + $complete + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($complete->data()) + ->toHaveKey('status', 'COMPLETED'); + + $operations = api_client()->get('/edge-gateways/' . (int)$gateway['id'] . '/operations', $session['headers']); + + $operations + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect(($operations->data()[0]['status'] ?? null)) + ->toBe('COMPLETED'); + + $events = api_client()->get( + '/edge-gateways/' . (int)$gateway['id'] . '/operations/' . $operationId . '/events', + $session['headers'] + ); + + $events + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect(collect_gateway_messages($events->data())) + ->toContain('Broker is executing the operation.') + ->toContain('Operation completed successfully'); + + $tasksPage = api_client()->get('/edge-gateways/' . (int)$gateway['id'] . '/tasks', $session['headers']); + + $tasksPage + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($tasksPage->data()['recent_operations_summary']['completed'] ?? 0) + ->toBeGreaterThanOrEqual(1); +}); + +it('rejects invalid edge broker shared secrets', function (): void { + $department = api_fixtures()->createDepartment([ + 'name' => 'Edge Broker Forbidden Department', + ]); + $gateway = api_fixtures()->createClaimedEdgeGateway([ + 'department_id' => (int)$department['id'], + ]); + + $response = api_client()->post( + '/edge-agent/internal/gateways/' . (int)$gateway['id'] . '/presence', + ['status' => 'connected'], + ['X-Edge-Broker-Secret' => 'wrong-secret'] + ); + + $response + ->assertStatus(403) + ->assertEnvelope() + ->assertSuccess(false) + ->assertMessage('Invalid edge broker secret'); +}); + +function edge_broker_test_inventory(string $suffix): array +{ + return [[ + 'device_id' => 'broker-' . $suffix, + 'local_ip' => '10.40.50.60', + 'model' => 'TruckWash Edge Broker', + 'channel_count' => 1, + 'online' => true, + 'capabilities' => [ + 'relay_commands' => true, + ], + 'metadata' => [ + 'hostname' => 'broker-' . $suffix, + ], + ]]; +} + +/** + * @param mixed $items + * @return array + */ +function collect_gateway_messages(mixed $items): array +{ + if (!is_array($items)) { + return []; + } + + $messages = []; + foreach ($items as $item) { + if (is_array($item) && isset($item['message']) && is_string($item['message'])) { + $messages[] = $item['message']; + } + } + + return $messages; +} diff --git a/services/nginx/app/tests/Api/EdgeGatewayOperatorApiTest.php b/services/nginx/app/tests/Api/EdgeGatewayOperatorApiTest.php new file mode 100644 index 00000000..e7e91e7f --- /dev/null +++ b/services/nginx/app/tests/Api/EdgeGatewayOperatorApiTest.php @@ -0,0 +1,418 @@ +createDepartment([ + 'name' => 'Edge Operator Department', + ]); + $session = api_fixtures()->createEdgeOperatorSession((int)$department['id']); + + $createResponse = api_client()->post('/edge-gateways/install-token', [ + 'department_id' => (int)$department['id'], + 'label' => 'Dock 7 Gateway', + ], $session['headers']); + + $createResponse + ->assertStatus(201) + ->assertEnvelope() + ->assertSuccess(); + + $installToken = $createResponse->data(); + expect($installToken) + ->toBeArray() + ->toHaveKeys(['claim_token_id', 'token', 'install_command']); + + $statusResponse = api_client()->get( + '/edge-gateways/install-token/' . (int)$installToken['claim_token_id'] . '/status', + $session['headers'] + ); + + $statusResponse + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($statusResponse->data()) + ->toHaveKey('status', 'PENDING') + ->toHaveKey('gateway_id', null); + + $verifyResponse = api_client()->get('/edge-agent/install-token/verify?token=' . urlencode((string)$installToken['token'])); + + $verifyResponse + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($verifyResponse->data()) + ->toHaveKey('valid', true) + ->toHaveKey('claim_token_id', (int)$installToken['claim_token_id']); + + $runningStatus = api_client()->post('/edge-agent/install-token/status', [ + 'token' => (string)$installToken['token'], + 'status' => 'RUNNING', + 'step' => 'BOOTSTRAP', + 'message' => 'Installer is downloading gateway artifacts.', + 'diagnostics' => [ + 'download-1', + 'download-2', + 'download-3', + 'download-4', + 'download-5', + 'download-6', + 'download-7', + ], + ]); + + $runningStatus + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + $runningStatusDetail = api_client()->get( + '/edge-gateways/install-token/' . (int)$installToken['claim_token_id'] . '/status', + $session['headers'] + ); + + $runningStatusDetail + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($runningStatusDetail->data()) + ->toHaveKey('status', 'RUNNING') + ->toHaveKey('step', 'BOOTSTRAP') + ->and($runningStatusDetail->data()['diagnostics'] ?? []) + ->toBeArray() + ->toHaveCount(6); + + $claimResponse = api_client()->post('/edge-agent/claim', [ + 'token' => (string)$installToken['token'], + 'hostname' => 'edge-operator-api', + 'installed_version' => 'php-agent-v1', + 'metadata' => [ + 'agent_runtime' => 'compose-php', + ], + ]); + + $claimResponse + ->assertStatus(201) + ->assertEnvelope() + ->assertSuccess(); + + $claimedPayload = $claimResponse->data(); + $gatewayId = (int)($claimedPayload['gateway']['id'] ?? 0); + + expect($claimedPayload) + ->toHaveKey('agent_token') + ->and($gatewayId) + ->toBeGreaterThan(0) + ->and($claimedPayload['gateway']['status'] ?? null) + ->toBe('ONLINE'); + + $claimedStatus = api_client()->get( + '/edge-gateways/install-token/' . (int)$installToken['claim_token_id'] . '/status', + $session['headers'] + ); + + $claimedStatus + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($claimedStatus->data()) + ->toHaveKey('status', 'CLAIMED') + ->toHaveKey('gateway_id', $gatewayId) + ->toHaveKey('last_error', null); + + $listResponse = api_client()->get('/edge-gateways?department_id=' . (int)$department['id'], $session['headers']); + + $listResponse + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect(collect_gateway_ids_from_api_response($listResponse->data())) + ->toContain($gatewayId); + + $detailResponse = api_client()->get('/edge-gateways/' . $gatewayId, $session['headers']); + + $detailResponse + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($detailResponse->data()) + ->toHaveKey('id', $gatewayId) + ->toHaveKey('department_id', (int)$department['id']) + ->toHaveKey('status', 'ONLINE'); +}); + +it('manages edge gateway metadata, bindings, operations, sessions, rotation, cutover, and deletion', function (): void { + $department = api_fixtures()->createDepartment([ + 'name' => 'Edge Operator Control Department', + ]); + $session = api_fixtures()->createEdgeOperatorSession((int)$department['id']); + $gateway = api_fixtures()->createClaimedEdgeGateway([ + 'department_id' => (int)$department['id'], + 'label' => 'Original Gateway Label', + ]); + api_fixtures()->createClaimedEdgeGateway([ + 'department_id' => (int)$department['id'], + 'label' => 'Alternate Gateway Label', + 'is_primary' => 0, + ]); + + $updateResponse = api_client()->put('/edge-gateways/' . (int)$gateway['id'], [ + 'label' => 'Renamed Gateway', + 'is_primary' => false, + ], $session['headers']); + + $updateResponse + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($updateResponse->data()) + ->toHaveKey('label', 'Renamed Gateway') + ->toHaveKey('is_primary', false); + + $bindingsResponse = api_client()->put('/edge-gateways/' . (int)$gateway['id'] . '/bindings', [ + 'bindings' => [[ + 'relay_id' => 'relay-main', + 'device_id' => 'device-main', + 'local_ip' => '10.0.0.18', + 'channel' => 0, + 'binding_source' => 'MANUAL', + ]], + ], $session['headers']); + + $bindingsResponse + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($bindingsResponse->data()['bindings'] ?? []) + ->toBeArray() + ->toHaveCount(1) + ->and(($bindingsResponse->data()['bindings'][0]['relay_id'] ?? null)) + ->toBe('relay-main'); + + $operationResponse = api_client()->post('/edge-gateways/' . (int)$gateway['id'] . '/operations', [ + 'type' => 'DISCOVERY', + 'request' => [ + 'inventory' => edge_operator_test_inventory('operator'), + ], + ], $session['headers']); + + $operationResponse + ->assertStatus(201) + ->assertEnvelope() + ->assertSuccess(); + + $operationId = (int)($operationResponse->data()['operation']['id'] ?? 0); + expect($operationId)->toBeGreaterThan(0); + + $operationsList = api_client()->get('/edge-gateways/' . (int)$gateway['id'] . '/operations', $session['headers']); + + $operationsList + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect(collect_gateway_ids_from_api_response($operationsList->data(), 'id')) + ->toContain($operationId); + + $eventsResponse = api_client()->get( + '/edge-gateways/' . (int)$gateway['id'] . '/operations/' . $operationId . '/events', + $session['headers'] + ); + + $eventsResponse + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($eventsResponse->data()) + ->toBeArray() + ->not->toBeEmpty() + ->and($eventsResponse->data()[0]['code'] ?? null) + ->toBe('OPERATION_QUEUED'); + + $cancelResponse = api_client()->post( + '/edge-gateways/' . (int)$gateway['id'] . '/operations/' . $operationId . '/cancel', + [], + $session['headers'] + ); + + $cancelResponse + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($cancelResponse->data()['operation']['status'] ?? null) + ->toBe('CANCELLED'); + + $streamSession = api_client()->post( + '/edge-gateways/' . (int)$gateway['id'] . '/stream-session', + ['scopes' => ['logs', 'tasks']], + $session['headers'] + ); + + $streamSession + ->assertStatus(201) + ->assertEnvelope() + ->assertSuccess(); + + expect($streamSession->data()) + ->toHaveKey('token') + ->toHaveKey('ws_url'); + + $shellSession = api_client()->post( + '/edge-gateways/' . (int)$gateway['id'] . '/shell-sessions', + [ + 'reason' => 'Operator smoke session', + 'cwd' => '/opt/truckwash-edge-agent', + 'cols' => 120, + 'rows' => 40, + ], + $session['headers'] + ); + + $shellSession + ->assertStatus(201) + ->assertEnvelope() + ->assertSuccess(); + + expect($shellSession->data()) + ->toHaveKey('token') + ->toHaveKey('session') + ->and($shellSession->data()['session']['reason'] ?? null) + ->toBe('Operator smoke session'); + + $rotateResponse = api_client()->post( + '/edge-gateways/' . (int)$gateway['id'] . '/rotate-credentials', + [], + $session['headers'] + ); + + $rotateResponse + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($rotateResponse->data()) + ->toHaveKey('gateway_id', (int)$gateway['id']) + ->toHaveKey('agent_token') + ->toHaveKey('config_json'); + + $cutoverResponse = api_client()->post( + '/departments/' . (int)$department['id'] . '/gateway-cutover', + ['transport_mode' => 'gateway'], + $session['headers'] + ); + + $cutoverResponse + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($cutoverResponse->data()) + ->toHaveKey('department_id', (int)$department['id']) + ->toHaveKey('transport_mode', 'gateway'); + + $deleteResponse = api_client()->delete('/edge-gateways/' . (int)$gateway['id'], null, $session['headers']); + + $deleteResponse + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect($deleteResponse->data()) + ->toHaveKey('deleted', true) + ->toHaveKey('gateway_id', (int)$gateway['id']); +}); + +it('rejects operator edge routes when module permission or department access is missing', function (): void { + $department = api_fixtures()->createDepartment([ + 'name' => 'Edge Operator Access Department', + ]); + $allowed = api_fixtures()->createEdgeOperatorSession((int)$department['id']); + $token = api_client()->post('/edge-gateways/install-token', [ + 'department_id' => (int)$department['id'], + 'label' => 'Restricted Gateway', + ], $allowed['headers']); + + $token + ->assertStatus(201) + ->assertEnvelope() + ->assertSuccess(); + + $missingModule = api_fixtures()->createUserSession([ + 'department_access_' . (int)$department['id'], + ]); + + $missingModuleResponse = api_client()->get( + '/edge-gateways?department_id=' . (int)$department['id'], + $missingModule['headers'] + ); + + $missingModuleResponse + ->assertStatus(403) + ->assertEnvelope() + ->assertSuccess(false) + ->assertMissingPermissions(['modules_shelly_config']); + + $missingDepartment = api_fixtures()->createUserSession(['modules_shelly_config']); + + $missingDepartmentResponse = api_client()->get( + '/edge-gateways/install-token/' . (int)$token->data()['claim_token_id'] . '/status', + $missingDepartment['headers'] + ); + + $missingDepartmentResponse + ->assertStatus(403) + ->assertEnvelope() + ->assertSuccess(false) + ->assertMissingPermissions(['department_access_' . (int)$department['id']]); +}); + +function edge_operator_test_inventory(string $suffix): array +{ + return [[ + 'device_id' => 'gateway-' . $suffix, + 'local_ip' => '10.20.30.40', + 'model' => 'TruckWash Edge Test', + 'channel_count' => 2, + 'online' => true, + 'capabilities' => [ + 'local_discovery' => true, + 'relay_commands' => true, + ], + 'metadata' => [ + 'hostname' => 'edge-' . $suffix, + ], + ]]; +} + +/** + * @param mixed $items + * @return array + */ +function collect_gateway_ids_from_api_response(mixed $items, string $key = 'id'): array +{ + if (!is_array($items)) { + return []; + } + + $ids = []; + foreach ($items as $item) { + if (is_array($item) && isset($item[$key]) && is_numeric($item[$key])) { + $ids[] = (int)$item[$key]; + } + } + + return $ids; +} diff --git a/services/nginx/app/tests/Integration/EdgeGateway/EdgeGatewayBackendIntegrationTest.php b/services/nginx/app/tests/Integration/EdgeGateway/EdgeGatewayBackendIntegrationTest.php new file mode 100644 index 00000000..25e7d074 --- /dev/null +++ b/services/nginx/app/tests/Integration/EdgeGateway/EdgeGatewayBackendIntegrationTest.php @@ -0,0 +1,421 @@ +createDepartment([ + 'name' => 'Edge Integration Install Department', + ]); + + $token = $context['manager']->createInstallToken((int)$department['id'], 'Integration Install Token'); + $claimTokenId = (int)($token['claim_token_id'] ?? 0); + + $context['manager']->reportInstallTokenStatus((string)$token['token'], [ + 'status' => 'FAILED', + 'step' => 'DOWNLOAD_FAILED', + 'message' => 'The installer could not download the runtime bundle.', + 'diagnostics' => [ + 'diag-1', + 'diag-2', + 'diag-3', + 'diag-4', + 'diag-5', + 'diag-6', + 'diag-7', + 'diag-8', + ], + ]); + + $failedStatus = $context['manager']->getInstallTokenStatus($claimTokenId); + + expect($failedStatus) + ->toHaveKey('status', 'FAILED') + ->toHaveKey('step', 'DOWNLOAD_FAILED') + ->toHaveKey('last_error', 'The installer could not download the runtime bundle.') + ->and($failedStatus['diagnostics'] ?? []) + ->toBeArray() + ->toHaveCount(6); + + $claimed = $context['manager']->claimGateway((string)$token['token'], 'edge-integration-host', 'php-agent-v1', [ + 'source' => 'integration-test', + ]); + + $gatewayId = (int)($claimed['gateway']['id'] ?? 0); + $agentToken = (string)($claimed['agent_token'] ?? ''); + expect($gatewayId)->toBeGreaterThan(0) + ->and($agentToken)->not->toBe(''); + + $claimedStatus = $context['manager']->getInstallTokenStatus($claimTokenId); + expect($claimedStatus) + ->toHaveKey('status', 'CLAIMED') + ->toHaveKey('gateway_id', $gatewayId) + ->toHaveKey('last_error', null); + + edge_gateway_integration_set_heartbeat_age($context['mysqli'], $gatewayId, edge_gateway_manager::HEARTBEAT_DEGRADED_AFTER_SECONDS + 1); + $degraded = $context['manager']->getGateway($gatewayId); + expect($degraded)->toHaveKey('status', 'DEGRADED'); + + edge_gateway_integration_set_heartbeat_age($context['mysqli'], $gatewayId, edge_gateway_manager::HEARTBEAT_OFFLINE_AFTER_SECONDS + 1); + $offline = $context['manager']->getGateway($gatewayId); + expect($offline)->toHaveKey('status', 'OFFLINE'); + + $context['manager']->recordHeartbeat($gatewayId, $agentToken, [ + 'status' => 'ONLINE', + 'hostname' => 'edge-integration-host', + 'metadata' => [ + 'system_metrics' => [ + 'cpu_percent' => 44, + ], + ], + ]); + + $online = $context['manager']->getGateway($gatewayId); + expect($online) + ->toHaveKey('status', 'ONLINE') + ->and($online['metadata']['system_metrics']['cpu_percent'] ?? null) + ->toBe(44); + } finally { + $context['cleanup']->run(); + } +}); + +it('assembles tasks, logs, statistics, operations, commands, and shell lifecycle state from persisted records', function (): void { + $context = edge_gateway_integration_context(); + + try { + $department = $context['fixtures']->createDepartment([ + 'name' => 'Edge Integration Runtime Department', + ]); + $user = $context['fixtures']->createUser([ + 'display_name' => 'Edge Integration User', + ]); + + $token = $context['manager']->createInstallToken((int)$department['id'], 'Integration Runtime Token', (int)$user['id']); + $claimed = $context['manager']->claimGateway((string)$token['token'], 'edge-runtime-host', 'php-agent-v1'); + $gatewayId = (int)($claimed['gateway']['id'] ?? 0); + $agentToken = (string)($claimed['agent_token'] ?? ''); + + $operation = $context['operations']->queueOperation( + $gatewayId, + edge_gateway_operation_service::TYPE_DISCOVERY, + ['inventory' => edge_gateway_integration_inventory('runtime')], + (int)$user['id'] + ); + $operationId = (int)($operation['id'] ?? 0); + expect($operationId)->toBeGreaterThan(0); + + $claimedOperation = $context['operations']->claimNextOperation($gatewayId, $agentToken, 0, 'integration-agent-1'); + expect($claimedOperation) + ->toBeArray() + ->toHaveKey('status', 'IN_PROGRESS'); + + $context['operations']->appendAgentOperationEvent($gatewayId, $operationId, $agentToken, [ + 'level' => 'INFO', + 'code' => 'DISCOVERY_RUNNING', + 'message' => 'Integration discovery is executing.', + 'context' => [ + 'progress' => 70, + ], + ]); + + $context['operations']->completeAgentOperation($gatewayId, $operationId, $agentToken, [ + 'ok' => true, + 'result' => [ + 'inventory' => edge_gateway_integration_inventory('completed'), + ], + ]); + + $job = $context['fixtures']->createEdgeCommandJob([ + 'gateway_id' => $gatewayId, + 'command_type' => 'GET_RELAY_STATUS', + 'request' => [ + 'relayId' => 'relay-main', + ], + 'requested_by' => (int)$user['id'], + ]); + $jobId = (int)($job['id'] ?? 0); + + $polledCommand = $context['manager']->pollCommand($gatewayId, $agentToken, 0); + expect($polledCommand) + ->toBeArray() + ->toHaveKey('id', $jobId) + ->toHaveKey('command_type', 'GET_RELAY_STATUS'); + + $commandResult = $context['manager']->submitCommandResult($gatewayId, $jobId, $agentToken, true, [ + 'relayId' => 'relay-main', + 'online' => true, + ]); + expect($commandResult) + ->toHaveKey('acknowledged', true) + ->and($commandResult['job']['status'] ?? null) + ->toBe('COMPLETED'); + + $shellSession = $context['manager']->createShellSession( + $gatewayId, + (int)$user['id'], + 'Integration shell session', + 120, + 40, + '/opt/truckwash-edge-agent' + ); + $shellToken = (string)($shellSession['token'] ?? ''); + expect($shellToken)->not->toBe(''); + + $validatedShell = $context['manager']->validateShellSessionToken($shellToken); + expect($validatedShell)->toHaveKey('status', 'PENDING'); + + $openedShell = $context['manager']->markShellSessionOpened($shellToken, 'shell-connection-1'); + expect($openedShell)->toHaveKey('status', 'OPEN'); + + $closedShell = $context['manager']->closeShellSessionByToken( + $shellToken, + "edge-shell-output\n", + 'agent_exit' + ); + expect($closedShell) + ->toHaveKey('status', 'COMPLETED') + ->and($closedShell['transcript'] ?? null) + ->toBe("edge-shell-output\n"); + + $context['manager']->appendGatewayLogEntry( + $gatewayId, + 'Integration log line', + 'INFO', + 'agent', + 'BROKER', + ['source' => 'integration'] + ); + + $context['manager']->recordBrokerPresence( + $gatewayId, + 'connected', + 'broker-connection-1', + null, + ['transport' => 'ws'] + ); + + $context['manager']->recordTelemetryFromBroker($gatewayId, [ + 'status' => 'ONLINE', + 'metadata' => [ + 'system_metrics' => [ + 'cpu_percent' => 17, + 'memory_mb' => 256, + ], + ], + 'inventory' => edge_gateway_integration_inventory('telemetry'), + ]); + + $tasks = $context['manager']->buildGatewayTasksPage($gatewayId); + $logs = $context['manager']->buildGatewayLogsPage($gatewayId); + $statistics = $context['manager']->buildGatewayStatisticsPage($gatewayId); + + expect($tasks['operations'] ?? []) + ->toBeArray() + ->not->toBeEmpty() + ->and(($tasks['operations'][0]['status'] ?? null)) + ->toBe('COMPLETED'); + expect($tasks['recent_commands'] ?? []) + ->toBeArray() + ->not->toBeEmpty() + ->and(($tasks['recent_commands'][0]['status'] ?? null)) + ->toBe('COMPLETED'); + + expect(edge_gateway_integration_messages($logs['log_entries'] ?? [])) + ->toContain('Integration log line'); + expect(edge_gateway_integration_messages($logs['timeline'] ?? [])) + ->toContain('Integration discovery is executing.'); + expect($logs['shell_sessions'] ?? []) + ->toBeArray() + ->not->toBeEmpty() + ->and(($logs['shell_sessions'][0]['transcript'] ?? null)) + ->toBe("edge-shell-output\n"); + + expect($statistics['system_metrics']['cpu_percent'] ?? null) + ->toBe(17); + expect($statistics['gateway']['inventory'] ?? []) + ->toBeArray() + ->not->toBeEmpty(); + } finally { + $context['cleanup']->run(); + } +}); + +/** + * @return array{cleanup:ApiCleanup,fixtures:ApiFixtures,manager:edge_gateway_manager,operations:edge_gateway_operation_service,mysqli:mysqli} + */ +function edge_gateway_integration_context(): array +{ + if (!integration_enabled()) { + test()->markTestSkipped('Set RUN_INTEGRATION_TESTS=1 to run edge gateway integration tests.'); + } + + static $bootstrapped = null; + + if ($bootstrapped === null) { + $dbConfig = edge_gateway_integration_db_config(); + $GLOBALS['CONFIG_DB'] = $dbConfig; + $GLOBALS['response'] = new class { + public function internal_server_error(string $message): void + { + throw new RuntimeException($message); + } + }; + + $db = new db($dbConfig); + $db->connect(); + $GLOBALS['db'] = $db; + + $mysqli = $db->conn(); + mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); + $mysqli->set_charset('utf8mb4'); + + (new ApiSchemaBootstrap($mysqli))->ensureSchema(); + + $bootstrapped = [ + 'mysqli' => $mysqli, + 'redis' => edge_gateway_integration_redis_client(), + ]; + } + + $cleanup = new ApiCleanup(); + + return [ + 'cleanup' => $cleanup, + 'fixtures' => new ApiFixtures($bootstrapped['mysqli'], $bootstrapped['redis'], $cleanup), + 'manager' => new edge_gateway_manager(), + 'operations' => new edge_gateway_operation_service(), + 'mysqli' => $bootstrapped['mysqli'], + ]; +} + +/** + * @return array{host:string,user:string,password:string,database:string,port:int} + */ +function edge_gateway_integration_db_config(): array +{ + $target = strtolower(trim((string)(getenv('CONFIG_DB_TARGET') ?: 'live'))); + if ($target !== 'debug') { + $target = 'live'; + } + + $host = edge_gateway_integration_config_value('CONFIG_DB_HOST', 'CONFIG_DB_DEBUG_HOST', $target); + $user = edge_gateway_integration_config_value('CONFIG_DB_USER', 'CONFIG_DB_DEBUG_USER', $target); + $password = edge_gateway_integration_config_value('CONFIG_DB_PASSWORD', 'CONFIG_DB_DEBUG_PASSWORD', $target); + $database = edge_gateway_integration_config_value('CONFIG_DB_DATABASE', 'CONFIG_DB_DEBUG_DATABASE', $target); + $port = (int)(edge_gateway_integration_config_value('CONFIG_DB_PORT', 'CONFIG_DB_DEBUG_PORT', $target) ?: '3306'); + + if ($host === '' || $user === '' || $database === '') { + test()->markTestSkipped('Edge gateway integration tests require configured database environment variables.'); + } + + return [ + 'host' => $host, + 'user' => $user, + 'password' => $password, + 'database' => $database, + 'port' => $port > 0 ? $port : 3306, + ]; +} + +function edge_gateway_integration_redis_client(): ?PredisClient +{ + $target = strtolower(trim((string)(getenv('CONFIG_DB_TARGET') ?: 'live'))); + if ($target !== 'debug') { + $target = 'live'; + } + + $host = edge_gateway_integration_config_value('REDIS_CONFIG_HOST', 'REDIS_CONFIG_DEBUG_HOST', $target); + if ($host === '') { + return null; + } + + $parameters = [ + 'scheme' => 'tcp', + 'host' => $host, + 'port' => (int)(edge_gateway_integration_config_value('REDIS_CONFIG_PORT', 'REDIS_CONFIG_DEBUG_PORT', $target) ?: '6379'), + 'database' => (int)(edge_gateway_integration_config_value('REDIS_CONFIG_DATABASE', 'REDIS_CONFIG_DEBUG_DATABASE', $target) ?: '0'), + 'password' => edge_gateway_integration_config_value('REDIS_CONFIG_PASSWORD', 'REDIS_CONFIG_DEBUG_PASSWORD', $target), + ]; + + $user = edge_gateway_integration_config_value('REDIS_CONFIG_USER', 'REDIS_CONFIG_DEBUG_USER', $target); + if ($user !== '') { + $parameters['username'] = $user; + } + + return new PredisClient($parameters); +} + +function edge_gateway_integration_config_value(string $liveKey, string $debugKey, string $target): string +{ + $liveValue = trim((string)(getenv($liveKey) ?: '')); + $debugValue = trim((string)(getenv($debugKey) ?: '')); + + if ($target === 'debug' && $debugValue !== '') { + return $debugValue; + } + + return $liveValue; +} + +function edge_gateway_integration_set_heartbeat_age(mysqli $mysqli, int $gatewayId, int $secondsAgo): void +{ + $timestamp = date('Y-m-d H:i:s', time() - max(0, $secondsAgo)); + $escaped = $mysqli->real_escape_string($timestamp); + $mysqli->query("UPDATE edge_gateways SET last_heartbeat_at = '{$escaped}', status = 'ONLINE' WHERE id = " . (int)$gatewayId); +} + +function edge_gateway_integration_inventory(string $suffix): array +{ + return [[ + 'device_id' => 'integration-' . $suffix, + 'local_ip' => '10.50.60.70', + 'model' => 'TruckWash Integration Gateway', + 'channel_count' => 1, + 'online' => true, + 'capabilities' => [ + 'gateway_management_v2' => true, + ], + 'metadata' => [ + 'hostname' => 'integration-' . $suffix, + ], + ]]; +} + +/** + * @param mixed $rows + * @return array + */ +function edge_gateway_integration_messages(mixed $rows): array +{ + if (!is_array($rows)) { + return []; + } + + $messages = []; + foreach ($rows as $row) { + if (is_array($row) && isset($row['message']) && is_string($row['message'])) { + $messages[] = $row['message']; + } + } + + return $messages; +} diff --git a/services/nginx/app/tests/Support/Api/ApiFixtures.php b/services/nginx/app/tests/Support/Api/ApiFixtures.php index dbcdfd68..0277dc65 100644 --- a/services/nginx/app/tests/Support/Api/ApiFixtures.php +++ b/services/nginx/app/tests/Support/Api/ApiFixtures.php @@ -445,6 +445,25 @@ final class ApiFixtures ]; } + /** + * @param array $permissions + * @param array $userAttributes + * @return array{user:array,token:string,headers:array} + */ + public function createEdgeOperatorSession(int $departmentId, array $permissions = [], array $userAttributes = []): array + { + if ($departmentId <= 0) { + throw new RuntimeException('Edge operator sessions require a positive department id.'); + } + + $permissions = array_values(array_unique(array_merge( + ['modules_shelly_config', 'department_access_' . $departmentId], + $permissions + ))); + + return $this->createUserSession($permissions, $userAttributes); + } + /** * @param array $permissions * @return array{user:array,subuser:array,token:string,headers:array} @@ -593,6 +612,11 @@ final class ApiFixtures ], $extraHeaders); } + public function clearEdgeGatewayViewCache(): void + { + $this->deleteRedisPattern('edge_gateway:view:v1:*'); + } + public function fetchRowById(string $table, int $id): ?array { $table = $this->sanitizeIdentifier($table); @@ -600,6 +624,395 @@ final class ApiFixtures return $this->queryOneBySql("SELECT * FROM `{$table}` WHERE id = {$id} LIMIT 1"); } + /** + * @param array $attributes + * @return array + */ + public function createEdgeInstallToken(array $attributes): array + { + $departmentId = (int)($attributes['department_id'] ?? 0); + if ($departmentId <= 0) { + throw new RuntimeException('Edge install tokens require department_id.'); + } + + $token = (string)($attributes['token'] ?? (bin2hex(random_bytes(18)) . $this->uniqueSuffix())); + $createdAt = (string)($attributes['created_at'] ?? $this->now()); + $expiresAt = (string)($attributes['expires_at'] ?? date('Y-m-d H:i:s', strtotime($createdAt) + 1800)); + $installSession = [ + 'status' => (string)($attributes['status'] ?? 'PENDING'), + 'step' => (string)($attributes['step'] ?? 'PENDING'), + 'message' => (string)($attributes['message'] ?? 'Installer command generated. Run it on the gateway host.'), + 'started_at' => $createdAt, + 'updated_at' => $createdAt, + 'terminal' => false, + 'gateway_id' => $attributes['gateway_id'] ?? null, + 'last_error' => $attributes['last_error'] ?? null, + 'diagnostics' => isset($attributes['diagnostics']) && is_array($attributes['diagnostics']) + ? (array)$attributes['diagnostics'] + : [], + 'events' => isset($attributes['events']) && is_array($attributes['events']) + ? (array)$attributes['events'] + : [ + [ + 'status' => (string)($attributes['status'] ?? 'PENDING'), + 'step' => (string)($attributes['step'] ?? 'PENDING'), + 'message' => (string)($attributes['message'] ?? 'Installer command generated. Run it on the gateway host.'), + 'at' => $createdAt, + ], + ], + ]; + + $claimTokenId = $this->insertRow('edge_gateway_claim_tokens', [ + 'department_id' => $departmentId, + 'label' => $attributes['label'] ?? ('Edge Install ' . $this->uniqueSuffix()), + 'token_hash' => hash('sha256', $token), + 'created_by' => $attributes['created_by'] ?? null, + 'expires_at' => $expiresAt, + 'used_at' => $attributes['used_at'] ?? null, + 'metadata_json' => array_merge( + isset($attributes['metadata']) && is_array($attributes['metadata']) ? (array)$attributes['metadata'] : [], + ['install_session' => $installSession] + ), + 'created_at' => $createdAt, + 'updated_at' => $attributes['updated_at'] ?? $createdAt, + 'deleted_at' => $attributes['deleted_at'] ?? null, + ]); + + $this->cleanup->add(fn() => $this->deleteById('edge_gateway_claim_tokens', $claimTokenId)); + + return [ + 'claim_token_id' => $claimTokenId, + 'department_id' => $departmentId, + 'label' => $attributes['label'] ?? null, + 'token' => $token, + 'expires_at' => $expiresAt, + ]; + } + + /** + * @param array $attributes + * @return array + */ + public function createClaimedEdgeGateway(array $attributes): array + { + $departmentId = (int)($attributes['department_id'] ?? 0); + if ($departmentId <= 0) { + throw new RuntimeException('Claimed edge gateways require department_id.'); + } + + $agentToken = (string)($attributes['agent_token'] ?? (bin2hex(random_bytes(24)) . $this->uniqueSuffix())); + $createdAt = (string)($attributes['created_at'] ?? $this->now()); + $metadata = array_merge([ + 'credentials_rotated_at' => $createdAt, + 'agent_runtime' => 'compose-php', + 'runtime_mode' => 'compose', + 'update_window' => '02:00-04:00', + 'container_health' => [ + 'overall_status' => 'PENDING', + 'services' => [], + ], + '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, + ], isset($attributes['metadata']) && is_array($attributes['metadata']) ? (array)$attributes['metadata'] : []); + + $gatewayId = $this->insertRow('edge_gateways', [ + 'department_id' => $departmentId, + 'label' => (string)($attributes['label'] ?? ('Edge Gateway ' . $this->uniqueSuffix())), + 'hostname' => $attributes['hostname'] ?? ('edge-' . $this->uniqueSuffix()), + 'agent_token_hash' => hash('sha256', $agentToken), + 'status' => (string)($attributes['status'] ?? 'ONLINE'), + 'transport_mode' => (string)($attributes['transport_mode'] ?? 'gateway'), + 'release_channel' => (string)($attributes['release_channel'] ?? 'stable'), + 'installed_version' => $attributes['installed_version'] ?? 'php-agent-v1', + 'target_version' => $attributes['target_version'] ?? ($attributes['installed_version'] ?? 'php-agent-v1'), + 'last_heartbeat_at' => $attributes['last_heartbeat_at'] ?? $createdAt, + 'last_seen_ip' => $attributes['last_seen_ip'] ?? '127.0.0.1', + 'discovery_status' => (string)($attributes['discovery_status'] ?? 'PENDING'), + 'is_primary' => $attributes['is_primary'] ?? 1, + 'metadata_json' => $metadata, + 'created_at' => $createdAt, + 'updated_at' => $attributes['updated_at'] ?? $createdAt, + 'deleted_at' => $attributes['deleted_at'] ?? null, + ]); + + $this->cleanup->add(function () use ($gatewayId): void { + $this->deleteWhere('edge_gateway_operation_events', ['gateway_id' => $gatewayId]); + $this->deleteWhere('edge_gateway_operations', ['gateway_id' => $gatewayId]); + $this->deleteWhere('edge_gateway_command_jobs', ['gateway_id' => $gatewayId]); + $this->deleteWhere('edge_gateway_device_inventory', ['gateway_id' => $gatewayId]); + $this->deleteWhere('edge_gateway_relay_bindings', ['gateway_id' => $gatewayId]); + $this->deleteWhere('edge_gateway_log_entries', ['gateway_id' => $gatewayId]); + $this->deleteWhere('edge_gateway_shell_sessions', ['gateway_id' => $gatewayId]); + $this->deleteWhere('edge_gateway_audit_logs', ['gateway_id' => $gatewayId]); + $this->deleteById('edge_gateways', $gatewayId); + $this->clearEdgeGatewayViewCache(); + }); + + return [ + 'id' => $gatewayId, + 'department_id' => $departmentId, + 'label' => (string)($attributes['label'] ?? ''), + 'agent_token' => $agentToken, + ]; + } + + /** + * @param array $attributes + * @return array + */ + public function createEdgeCommandJob(array $attributes): array + { + $gatewayId = (int)($attributes['gateway_id'] ?? 0); + if ($gatewayId <= 0) { + throw new RuntimeException('Edge command jobs require gateway_id.'); + } + + $jobId = $this->insertRow('edge_gateway_command_jobs', [ + 'gateway_id' => $gatewayId, + 'command_type' => (string)($attributes['command_type'] ?? 'DISCOVER_SHELLY'), + 'status' => (string)($attributes['status'] ?? 'PENDING'), + 'request_json' => isset($attributes['request']) && is_array($attributes['request']) ? (array)$attributes['request'] : [], + 'response_json' => isset($attributes['response']) && is_array($attributes['response']) ? (array)$attributes['response'] : [], + 'delivery_json' => isset($attributes['delivery']) && is_array($attributes['delivery']) ? (array)$attributes['delivery'] : [], + 'correlation_id' => (string)($attributes['correlation_id'] ?? ('edge-command-' . $this->uniqueSuffix())), + 'requested_by' => $attributes['requested_by'] ?? null, + 'requested_at' => $attributes['requested_at'] ?? $this->now(), + 'completed_at' => $attributes['completed_at'] ?? null, + 'error_message' => $attributes['error_message'] ?? null, + 'created_at' => $attributes['created_at'] ?? $this->now(), + 'updated_at' => $attributes['updated_at'] ?? $this->now(), + 'deleted_at' => $attributes['deleted_at'] ?? null, + ]); + + $this->cleanup->add(fn() => $this->deleteById('edge_gateway_command_jobs', $jobId)); + + return [ + 'id' => $jobId, + 'gateway_id' => $gatewayId, + ]; + } + + /** + * @param array $attributes + * @return array + */ + public function createEdgeOperation(array $attributes): array + { + $gatewayId = (int)($attributes['gateway_id'] ?? 0); + if ($gatewayId <= 0) { + throw new RuntimeException('Edge operations require gateway_id.'); + } + + $operationId = $this->insertRow('edge_gateway_operations', [ + 'gateway_id' => $gatewayId, + 'type' => (string)($attributes['type'] ?? 'DISCOVERY'), + 'operation_type' => (string)($attributes['operation_type'] ?? ($attributes['type'] ?? 'DISCOVERY')), + 'status' => (string)($attributes['status'] ?? 'PENDING'), + 'request_json' => isset($attributes['request']) && is_array($attributes['request']) ? (array)$attributes['request'] : [], + 'summary_json' => isset($attributes['summary']) && is_array($attributes['summary']) ? (array)$attributes['summary'] : [ + 'label' => 'Queued', + 'progress' => 0, + 'retryable' => true, + ], + 'result_json' => isset($attributes['result']) && is_array($attributes['result']) ? (array)$attributes['result'] : [], + 'error_code' => $attributes['error_code'] ?? null, + 'error_message' => $attributes['error_message'] ?? null, + 'correlation_id' => (string)($attributes['correlation_id'] ?? ('edge-operation-' . $this->uniqueSuffix())), + 'agent_instance_id' => $attributes['agent_instance_id'] ?? null, + 'lease_expires_at' => $attributes['lease_expires_at'] ?? null, + 'last_progress_at' => $attributes['last_progress_at'] ?? null, + 'attempt_count' => $attributes['attempt_count'] ?? 0, + 'requested_by' => $attributes['requested_by'] ?? null, + 'requested_at' => $attributes['requested_at'] ?? $this->now(), + 'started_at' => $attributes['started_at'] ?? null, + 'completed_at' => $attributes['completed_at'] ?? null, + 'created_at' => $attributes['created_at'] ?? $this->now(), + 'updated_at' => $attributes['updated_at'] ?? $this->now(), + 'deleted_at' => $attributes['deleted_at'] ?? null, + ]); + + $this->cleanup->add(function () use ($gatewayId, $operationId): void { + $this->deleteWhere('edge_gateway_operation_events', [ + 'gateway_id' => $gatewayId, + 'operation_id' => $operationId, + ]); + $this->deleteById('edge_gateway_operations', $operationId); + }); + + return [ + 'id' => $operationId, + 'gateway_id' => $gatewayId, + ]; + } + + /** + * @param array $attributes + * @return array + */ + public function createEdgeOperationEvent(array $attributes): array + { + $gatewayId = (int)($attributes['gateway_id'] ?? 0); + $operationId = (int)($attributes['operation_id'] ?? 0); + if ($gatewayId <= 0 || $operationId <= 0) { + throw new RuntimeException('Edge operation events require gateway_id and operation_id.'); + } + + $eventId = $this->insertRow('edge_gateway_operation_events', [ + 'gateway_id' => $gatewayId, + 'operation_id' => $operationId, + 'stage' => (string)($attributes['stage'] ?? 'RECORDED'), + 'level' => (string)($attributes['level'] ?? 'INFO'), + 'code' => $attributes['code'] ?? null, + 'message' => (string)($attributes['message'] ?? 'Edge operation event'), + 'context_json' => isset($attributes['context']) && is_array($attributes['context']) ? (array)$attributes['context'] : [], + 'counts_json' => isset($attributes['counts']) && is_array($attributes['counts']) ? (array)$attributes['counts'] : [], + 'payload_json' => isset($attributes['payload']) && is_array($attributes['payload']) ? (array)$attributes['payload'] : [], + 'created_at' => $attributes['created_at'] ?? $this->now(), + 'deleted_at' => $attributes['deleted_at'] ?? null, + ]); + + $this->cleanup->add(fn() => $this->deleteById('edge_gateway_operation_events', $eventId)); + + return [ + 'id' => $eventId, + 'gateway_id' => $gatewayId, + 'operation_id' => $operationId, + ]; + } + + /** + * @param array $attributes + * @return array + */ + public function createEdgeAuditLog(array $attributes): array + { + $gatewayId = (int)($attributes['gateway_id'] ?? 0); + $departmentId = (int)($attributes['department_id'] ?? 0); + if ($gatewayId <= 0 || $departmentId <= 0) { + throw new RuntimeException('Edge audit logs require gateway_id and department_id.'); + } + + $auditLogId = $this->insertRow('edge_gateway_audit_logs', [ + 'gateway_id' => $gatewayId, + 'department_id' => $departmentId, + 'action' => (string)($attributes['action'] ?? 'EDGE_AUDIT'), + 'actor_user_id' => $attributes['actor_user_id'] ?? null, + 'actor_type' => (string)($attributes['actor_type'] ?? 'USER'), + 'severity' => (string)($attributes['severity'] ?? 'INFO'), + 'context_json' => isset($attributes['context']) && is_array($attributes['context']) ? (array)$attributes['context'] : [], + 'created_at' => $attributes['created_at'] ?? $this->now(), + 'updated_at' => $attributes['updated_at'] ?? $this->now(), + ]); + + $this->cleanup->add(fn() => $this->deleteById('edge_gateway_audit_logs', $auditLogId)); + + return [ + 'id' => $auditLogId, + 'gateway_id' => $gatewayId, + 'department_id' => $departmentId, + ]; + } + + /** + * @param array $attributes + * @return array + */ + public function createEdgeLogEntry(array $attributes): array + { + $gatewayId = (int)($attributes['gateway_id'] ?? 0); + if ($gatewayId <= 0) { + throw new RuntimeException('Edge log entries require gateway_id.'); + } + + $gatewayRow = $this->fetchRowById('edge_gateways', $gatewayId); + $departmentId = (int)($attributes['department_id'] ?? ($gatewayRow['department_id'] ?? 0)); + + $logEntryId = $this->insertRow('edge_gateway_log_entries', [ + 'gateway_id' => $gatewayId, + 'department_id' => $departmentId > 0 ? $departmentId : null, + 'level' => (string)($attributes['level'] ?? 'INFO'), + 'stream' => (string)($attributes['stream'] ?? 'agent'), + 'source' => (string)($attributes['source'] ?? 'BROKER'), + 'message' => (string)($attributes['message'] ?? 'Edge gateway log entry'), + 'context_json' => isset($attributes['context']) && is_array($attributes['context']) ? (array)$attributes['context'] : [], + 'created_at' => $attributes['created_at'] ?? $this->now(), + 'updated_at' => $attributes['updated_at'] ?? $this->now(), + ]); + + $this->cleanup->add(fn() => $this->deleteById('edge_gateway_log_entries', $logEntryId)); + + return [ + 'id' => $logEntryId, + 'gateway_id' => $gatewayId, + ]; + } + + /** + * @param array $attributes + * @return array + */ + public function createEdgeShellSession(array $attributes): array + { + $gatewayId = (int)($attributes['gateway_id'] ?? 0); + if ($gatewayId <= 0) { + throw new RuntimeException('Edge shell sessions require gateway_id.'); + } + + $gatewayRow = $this->fetchRowById('edge_gateways', $gatewayId); + $departmentId = (int)($attributes['department_id'] ?? ($gatewayRow['department_id'] ?? 0)); + if ($departmentId <= 0) { + throw new RuntimeException('Edge shell sessions require department_id or a valid gateway row.'); + } + + $sessionToken = (string)($attributes['token'] ?? bin2hex(random_bytes(24))); + $createdAt = (string)($attributes['created_at'] ?? $this->now()); + $expiresAt = (string)($attributes['expires_at'] ?? date('Y-m-d H:i:s', strtotime($createdAt) + 900)); + + $sessionId = $this->insertRow('edge_gateway_shell_sessions', [ + 'gateway_id' => $gatewayId, + 'department_id' => $departmentId, + 'actor_user_id' => $attributes['actor_user_id'] ?? null, + 'session_token_hash' => hash('sha256', $sessionToken), + 'status' => (string)($attributes['status'] ?? 'PENDING'), + 'reason' => (string)($attributes['reason'] ?? 'Diagnostic shell session'), + 'connection_id' => $attributes['connection_id'] ?? null, + 'cwd' => $attributes['cwd'] ?? '/opt/truckwash-edge-agent', + 'shell_command' => $attributes['shell_command'] ?? null, + 'shell_args_json' => isset($attributes['shell_args']) && is_array($attributes['shell_args']) ? (array)$attributes['shell_args'] : [], + 'cols' => $attributes['cols'] ?? 120, + 'terminal_rows' => $attributes['rows'] ?? 32, + 'transcript' => $attributes['transcript'] ?? null, + 'metadata_json' => isset($attributes['metadata']) && is_array($attributes['metadata']) ? (array)$attributes['metadata'] : [], + 'expires_at' => $expiresAt, + 'approved_at' => $attributes['approved_at'] ?? $createdAt, + 'opened_at' => $attributes['opened_at'] ?? null, + 'closed_at' => $attributes['closed_at'] ?? null, + 'created_at' => $createdAt, + 'updated_at' => $attributes['updated_at'] ?? $createdAt, + 'deleted_at' => $attributes['deleted_at'] ?? null, + ]); + + $this->cleanup->add(fn() => $this->deleteById('edge_gateway_shell_sessions', $sessionId)); + + return [ + 'id' => $sessionId, + 'gateway_id' => $gatewayId, + 'department_id' => $departmentId, + 'token' => $sessionToken, + 'expires_at' => $expiresAt, + ]; + } + public function cleanupDeleteById(string $table, int $id): void { $this->cleanup->add(fn() => $this->deleteById($table, $id)); diff --git a/services/nginx/app/tests/Support/ApiTestSupport.php b/services/nginx/app/tests/Support/ApiTestSupport.php index 516c4cf8..6023373b 100644 --- a/services/nginx/app/tests/Support/ApiTestSupport.php +++ b/services/nginx/app/tests/Support/ApiTestSupport.php @@ -49,3 +49,21 @@ function assert_api_envelope(ApiResponse $response): ApiResponse { return $response->assertEnvelope(); } + +function edge_test_broker_secret(): string +{ + $secret = trim((string)(getenv('EDGE_BROKER_SHARED_SECRET') ?: '')); + + return $secret !== '' ? $secret : 'truckwash-edge-test-secret'; +} + +/** + * @param array $extraHeaders + * @return array + */ +function edge_test_broker_headers(array $extraHeaders = []): array +{ + return array_merge([ + 'X-Edge-Broker-Secret' => edge_test_broker_secret(), + ], $extraHeaders); +} diff --git a/services/nginx/app/tests/Support/EdgeGatewayE2eFixture.php b/services/nginx/app/tests/Support/EdgeGatewayE2eFixture.php new file mode 100644 index 00000000..d9461554 --- /dev/null +++ b/services/nginx/app/tests/Support/EdgeGatewayE2eFixture.php @@ -0,0 +1,226 @@ +createDepartment([ + 'name' => 'Edge Gateway E2E Department', + ]); + $session = $fixtures->createEdgeOperatorSession((int)$department['id'], [], [ + 'display_name' => 'Edge Gateway E2E Operator', + ]); + + edge_gateway_e2e_fixture_output([ + 'department_id' => (int)$department['id'], + 'user_id' => (int)$session['user']['id'], + 'group_id' => (int)$session['user']['group_id'], + 'customer_number' => (int)$session['user']['customer_number'], + 'auth_token' => (string)$session['token'], + 'broker_secret' => edge_test_broker_secret(), + ]); + } + + if ($action === 'cleanup') { + $encoded = trim((string)($argv[2] ?? '')); + if ($encoded === '') { + edge_gateway_e2e_fixture_fail('Cleanup requires a base64url payload argument.'); + } + + $payload = json_decode(base64_decode(strtr($encoded, '-_', '+/')) ?: '', true); + if (!is_array($payload)) { + edge_gateway_e2e_fixture_fail('Invalid cleanup payload.'); + } + + $context = edge_gateway_e2e_fixture_context(); + $db = $context['mysqli']; + $departmentId = (int)($payload['department_id'] ?? 0); + $userId = (int)($payload['user_id'] ?? 0); + $groupId = (int)($payload['group_id'] ?? 0); + + if ($departmentId > 0) { + $gatewayIds = edge_gateway_e2e_gateway_ids($db, $departmentId); + edge_gateway_e2e_delete_by_gateway_ids($db, 'edge_gateway_operation_events', $gatewayIds); + edge_gateway_e2e_delete_by_gateway_ids($db, 'edge_gateway_operations', $gatewayIds); + edge_gateway_e2e_delete_by_gateway_ids($db, 'edge_gateway_command_jobs', $gatewayIds); + edge_gateway_e2e_delete_by_gateway_ids($db, 'edge_gateway_log_entries', $gatewayIds); + edge_gateway_e2e_delete_by_gateway_ids($db, 'edge_gateway_shell_sessions', $gatewayIds); + edge_gateway_e2e_delete_by_gateway_ids($db, 'edge_gateway_device_inventory', $gatewayIds); + edge_gateway_e2e_delete_by_gateway_ids($db, 'edge_gateway_relay_bindings', $gatewayIds); + edge_gateway_e2e_delete_by_gateway_ids($db, 'edge_gateway_audit_logs', $gatewayIds); + + $db->query('DELETE FROM edge_gateway_claim_tokens WHERE department_id = ' . $departmentId); + $db->query('DELETE FROM edge_gateways WHERE department_id = ' . $departmentId); + $db->query('DELETE FROM department_variables WHERE department_id = ' . $departmentId); + } + + if ($userId > 0) { + $db->query('DELETE FROM tokens WHERE user_id = ' . $userId); + $db->query('DELETE FROM users WHERE id = ' . $userId); + } + + if ($groupId > 0) { + $db->query('DELETE FROM groups_permissions WHERE group_id = ' . $groupId); + $db->query('DELETE FROM groups WHERE id = ' . $groupId); + } + + if ($departmentId > 0) { + $db->query('DELETE FROM departments WHERE id = ' . $departmentId); + } + + edge_gateway_e2e_fixture_output(['ok' => true]); + } + + edge_gateway_e2e_fixture_fail('Unsupported action: ' . $action); +} catch (Throwable $throwable) { + edge_gateway_e2e_fixture_fail($throwable->getMessage()); +} + +/** + * @return array{mysqli:mysqli,fixtures:ApiFixtures} + */ +function edge_gateway_e2e_fixture_context(): array +{ + static $context = null; + + if ($context !== null) { + return $context; + } + + $dbConfig = edge_gateway_e2e_db_config(); + $GLOBALS['CONFIG_DB'] = $dbConfig; + $GLOBALS['response'] = new class { + public function internal_server_error(string $message): void + { + throw new RuntimeException($message); + } + }; + + $db = new db($dbConfig); + $db->connect(); + $GLOBALS['db'] = $db; + + $mysqli = $db->conn(); + mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); + $mysqli->set_charset('utf8mb4'); + + (new ApiSchemaBootstrap($mysqli))->ensureSchema(); + + $context = [ + 'mysqli' => $mysqli, + 'fixtures' => new ApiFixtures($mysqli, null, new ApiCleanup()), + ]; + + return $context; +} + +/** + * @return array{host:string,user:string,password:string,database:string,port:int} + */ +function edge_gateway_e2e_db_config(): array +{ + $target = strtolower(trim((string)(getenv('CONFIG_DB_TARGET') ?: 'live'))); + if ($target !== 'debug') { + $target = 'live'; + } + + $host = edge_gateway_e2e_config_value('CONFIG_DB_HOST', 'CONFIG_DB_DEBUG_HOST', $target); + $user = edge_gateway_e2e_config_value('CONFIG_DB_USER', 'CONFIG_DB_DEBUG_USER', $target); + $password = edge_gateway_e2e_config_value('CONFIG_DB_PASSWORD', 'CONFIG_DB_DEBUG_PASSWORD', $target); + $database = edge_gateway_e2e_config_value('CONFIG_DB_DATABASE', 'CONFIG_DB_DEBUG_DATABASE', $target); + $port = (int)(edge_gateway_e2e_config_value('CONFIG_DB_PORT', 'CONFIG_DB_DEBUG_PORT', $target) ?: '3306'); + + if ($host === '' || $user === '' || $database === '') { + throw new RuntimeException('Missing database configuration for edge gateway E2E fixtures.'); + } + + return [ + 'host' => $host, + 'user' => $user, + 'password' => $password, + 'database' => $database, + 'port' => $port > 0 ? $port : 3306, + ]; +} + +function edge_gateway_e2e_config_value(string $liveKey, string $debugKey, string $target): string +{ + $liveValue = trim((string)(getenv($liveKey) ?: '')); + $debugValue = trim((string)(getenv($debugKey) ?: '')); + + if ($target === 'debug' && $debugValue !== '') { + return $debugValue; + } + + return $liveValue; +} + +/** + * @return array + */ +function edge_gateway_e2e_gateway_ids(mysqli $db, int $departmentId): array +{ + $ids = []; + $result = $db->query('SELECT id FROM edge_gateways WHERE department_id = ' . $departmentId); + if ($result === false) { + return []; + } + + while ($row = $result->fetch_assoc()) { + if (isset($row['id']) && is_numeric($row['id'])) { + $ids[] = (int)$row['id']; + } + } + + $result->free(); + + return $ids; +} + +/** + * @param array $gatewayIds + */ +function edge_gateway_e2e_delete_by_gateway_ids(mysqli $db, string $table, array $gatewayIds): void +{ + $gatewayIds = array_values(array_unique(array_filter(array_map('intval', $gatewayIds), static fn(int $id): bool => $id > 0))); + if ($gatewayIds === []) { + return; + } + + $db->query('DELETE FROM `' . $table . '` WHERE gateway_id IN (' . implode(', ', $gatewayIds) . ')'); +} + +/** + * @param array $payload + */ +function edge_gateway_e2e_fixture_output(array $payload): void +{ + echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL; + exit(0); +} + +function edge_gateway_e2e_fixture_fail(string $message): never +{ + fwrite(STDERR, $message . PHP_EOL); + exit(1); +} diff --git a/services/nginx/app/tests/Support/bootstrap.php b/services/nginx/app/tests/Support/bootstrap.php index 9f126921..3d8a52c4 100644 --- a/services/nginx/app/tests/Support/bootstrap.php +++ b/services/nginx/app/tests/Support/bootstrap.php @@ -95,6 +95,14 @@ function integration_enabled(): bool return getenv('RUN_INTEGRATION_TESTS') === '1'; } +$edgeBrokerSharedSecret = trim((string)(getenv('EDGE_BROKER_SHARED_SECRET') ?: '')); +if ($edgeBrokerSharedSecret === '') { + $edgeBrokerSharedSecret = 'truckwash-edge-test-secret'; + putenv('EDGE_BROKER_SHARED_SECRET=' . $edgeBrokerSharedSecret); + $_ENV['EDGE_BROKER_SHARED_SECRET'] = $edgeBrokerSharedSecret; + $_SERVER['EDGE_BROKER_SHARED_SECRET'] = $edgeBrokerSharedSecret; +} + function run_legacy_script(string $relativeScriptPath): array { $script = app_path($relativeScriptPath); diff --git a/services/nginx/app/tests/Unit/Selfserve/DepartmentLaneRelayNullificationRouteWiringTest.php b/services/nginx/app/tests/Unit/Selfserve/DepartmentLaneRelayNullificationRouteWiringTest.php new file mode 100644 index 00000000..29e60fe8 --- /dev/null +++ b/services/nginx/app/tests/Unit/Selfserve/DepartmentLaneRelayNullificationRouteWiringTest.php @@ -0,0 +1,17 @@ +not->toBeFalse(); + expect($route)->toContain('private static function normalizeRelayRequestParameter'); + expect($route)->toContain('private static function syncDepartmentLaneRelayValue'); + expect($route)->toContain('$field->nullify();'); + expect($route)->toContain('self::syncDepartmentLaneRelayValue($department_lane->relay_in_id, $relay_in_id);'); + expect($route)->toContain('self::syncDepartmentLaneRelayValue($department_lane->relay_out_id, $relay_out_id);'); + expect($route)->toContain('self::syncDepartmentLaneRelayValue($department_lane->relay_machine_id, $relay_machine_id);'); + expect($route)->toContain( + 'self::syncDepartmentLaneRelayValue($department_lane->relay_machine_program_picker_id, $relay_machine_program_picker_id);' + ); + expect($route)->toContain('self::syncDepartmentLaneRelayValue($department_lane->relay_machine_cleaner_id, $relay_machine_cleaner_id);'); +}); diff --git a/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayManagerHeartbeatStatusTest.php b/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayManagerHeartbeatStatusTest.php index f8757055..59782a29 100644 --- a/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayManagerHeartbeatStatusTest.php +++ b/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayManagerHeartbeatStatusTest.php @@ -74,11 +74,17 @@ it('derives relay fallback and transport health details without shell or update 'last_heartbeat_at' => '2026-04-08 10:04:30', 'department_transport_mode' => edge_gateway_manager::TRANSPORT_MODE_GATEWAY, 'metadata' => [ + 'last_sync_at' => '2026-04-08 10:04:20', 'broker_presence' => [ 'connected' => false, 'last_seen_at' => '2026-04-08 10:03:00', 'last_error' => 'broker timeout', ], + 'control_plane_status' => [ + 'last_successful_sync_at' => '2026-04-08 10:04:10', + 'last_transport_failure_at' => '2026-04-08 10:04:12', + 'last_transport_error' => 'POST https://api.truckwash.io/edge-agent/gateways/17/heartbeat returned HTTP 502', + ], ], 'operational_snapshot' => [ 'command_backlog' => 2, @@ -132,6 +138,10 @@ it('derives relay fallback and transport health details without shell or update expect($gateway['fallback_summary']['local_only_relays'])->toBe(1); expect($gateway['transport_health']['status'])->toBe(edge_gateway_manager::STATUS_DEGRADED); expect($gateway['transport_health']['recommended_action'])->toBe('retry_discovery'); + expect($gateway['transport_health']['last_successful_sync_at'])->toBe('2026-04-08 10:04:30'); + expect($gateway['transport_health']['last_transport_failure_at'])->toBe('2026-04-08 10:04:12'); + expect($gateway['transport_health']['last_transport_error'])->toContain('returned HTTP 502'); + expect($gateway['last_sync_at'])->toBe('2026-04-08 10:04:30'); expect($gateway['last_successful_discovery_at'])->toBe('2026-04-08 10:02:00'); expect($gateway['diagnostics'])->not->toBeEmpty(); expect($gateway['error_state']['code'])->toBe('EDGE_GATEWAY_OPERATION_TIMEOUT'); diff --git a/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayUpdateLifecycleTest.php b/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayUpdateLifecycleTest.php index 713dcc63..fcd094b5 100644 --- a/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayUpdateLifecycleTest.php +++ b/services/nginx/app/tests/Unit/Selfserve/EdgeGatewayUpdateLifecycleTest.php @@ -6,6 +6,7 @@ it('builds the installer around the compose stack artifacts and management polli $stackServiceSource = file_get_contents(app_path('resources/edge-gateway-agent/truckwash-edge-gateway-stack.service')); $launcherSource = file_get_contents(app_path('resources/edge-gateway-agent/gateway-launcher.sh')); $composeSource = file_get_contents(app_path('resources/edge-gateway-agent/docker-compose.gateway.yml')); + $agentSource = file_get_contents(app_path('resources/edge-gateway-agent/agent.php')); $edgeDockerfileSource = file_get_contents(app_path('resources/edge-gateway-agent/Dockerfile.edge-agent')); $workerDockerfileSource = file_get_contents(app_path('resources/edge-gateway-agent/Dockerfile.lan-worker')); $autoUpdaterSource = file_get_contents(app_path('resources/edge-gateway-agent/auto-updater.php')); @@ -14,6 +15,7 @@ it('builds the installer around the compose stack artifacts and management polli expect($managerSource)->not->toBeFalse(); expect($launcherSource)->not->toBeFalse(); expect($composeSource)->not->toBeFalse(); + expect($agentSource)->not->toBeFalse(); expect($edgeDockerfileSource)->not->toBeFalse(); expect($workerDockerfileSource)->not->toBeFalse(); expect($autoUpdaterSource)->not->toBeFalse(); @@ -66,7 +68,11 @@ it('builds the installer around the compose stack artifacts and management polli expect($composeSource)->toContain('condition: service_healthy'); expect($composeSource)->toContain("minio:\n condition: service_started"); expect($composeSource)->toContain("mariadb:\n condition: service_started"); - expect($composeSource)->toContain("test: [\"CMD-SHELL\", \"kill -0 1\"]"); + expect($composeSource)->toContain('/opt/truckwash-edge-agent/runtime/control-plane-status.json'); + expect($composeSource)->toContain('$$data[\\"last_loop_at\\"]'); + expect($composeSource)->toContain('$$data[\\"last_successful_sync_at\\"]'); + expect($composeSource)->toContain('<= 30'); + expect($composeSource)->toContain('<= 90'); expect($composeSource)->toContain("http://127.0.0.1:8090/health"); expect($composeSource)->toContain('$$json=@file_get_contents(\'http://127.0.0.1:8090/health\');'); expect($composeSource)->toContain('$$path=\"/opt/truckwash-edge-agent/runtime/auto-updater-heartbeat.json\"'); @@ -76,6 +82,15 @@ it('builds the installer around the compose stack artifacts and management polli expect($composeSource)->toContain('container_name: truckwash-mariadb'); expect($composeSource)->toContain('container_name: truckwash-minio'); expect($composeSource)->toContain('container_name: truckwash-auto-updater'); + expect($agentSource)->toContain('private string $controlPlaneStatusPath;'); + expect($agentSource)->toContain('control-plane-status.json'); + expect($agentSource)->toContain("'control_plane_status' => \$this->buildControlPlaneStatusPayload()"); + expect($agentSource)->toContain("'last_heartbeat_attempt_at'"); + expect($agentSource)->toContain("'last_heartbeat_success_at'"); + expect($agentSource)->toContain("'last_successful_sync_at'"); + expect($agentSource)->toContain("'last_transport_failure_at'"); + expect($agentSource)->toContain("'last_transport_error'"); + expect($agentSource)->toContain('private function recordTransportFailure(string $context, Throwable $throwable): void'); expect($edgeDockerfileSource)->toContain('FROM ${BASE_IMAGE}'); expect($edgeDockerfileSource)->toContain('COPY agent.php /opt/truckwash-edge-agent/agent.php'); expect($edgeDockerfileSource)->not->toContain('docker-php-ext-install'); diff --git a/services/nginx/app/tests/Unit/Selfserve/ShellyRelayInventoryTest.php b/services/nginx/app/tests/Unit/Selfserve/ShellyRelayInventoryTest.php index a11af8f7..6cb7be00 100644 --- a/services/nginx/app/tests/Unit/Selfserve/ShellyRelayInventoryTest.php +++ b/services/nginx/app/tests/Unit/Selfserve/ShellyRelayInventoryTest.php @@ -5,69 +5,181 @@ app_require('classes/shelly_relay_inventory.php'); use classes\shelly_relay_inventory; it('normalizes owned Shelly devices into relay select options', function (): void { - $inventory = (new shelly_relay_inventory())->setInventoryFetcher(static function (): array { - return [ - 'isok' => true, - 'data' => [ - 'devices_status' => [ - 'device-key-1' => [ - '_dev_info' => [ - 'id' => 'shelly-plus-01', - 'code' => 'SPSW-001PE16EU', - 'online' => 1, + $inventory = (new shelly_relay_inventory()) + ->setInventoryFetcher(static function (): array { + return [ + 'isok' => true, + 'data' => [ + 'devices_status' => [ + 'device-key-1' => [ + '_dev_info' => [ + 'id' => 'shelly-plus-01', + 'code' => 'SPSW-001PE16EU', + 'model' => 'Shelly Plus 1PM', + 'online' => 1, + ], + 'name' => 'Entry Gate', + 'status' => [ + 'switch:0' => [ + 'output' => false, + 'name' => 'Entrance Relay', + ], + ], ], - 'name' => 'Entry Gate', - 'status' => [ - 'switch:0' => [ - 'output' => false, + 'device-key-2' => [ + '_dev_info' => [ + 'id' => 'shelly-pro-03', + 'code' => 'SPSW-201XE16EU', + 'model' => 'Shelly Pro 2PM', + ], + 'name' => 'Machine Cabinet', + 'relays' => [ + ['ison' => false, 'name' => 'Program Picker'], + ], + ], + 'device-key-3' => [ + '_dev_info' => [ + 'id' => 'shelly-legacy-02', + 'code' => 'SHSW-1', + 'model' => 'Shelly 1', + 'online' => 0, + ], + 'relays' => [ + ['ison' => false], + ], + ], + 'device-key-4' => [ + '_dev_info' => [ + 'id' => 'shelly-sensor-01', + 'code' => 'SHHT-1', + 'model' => 'Shelly H&T', + 'online' => 1, + ], + 'sensor' => [ + 'temperature' => 21.5, ], ], ], - 'device-key-2' => [ - '_dev_info' => [ - 'id' => 'shelly-legacy-02', - 'code' => 'SHSW-1', - 'online' => 0, + ], + ]; + }) + ->setDeviceListFetcher(static function (): array { + return [ + 'isok' => true, + 'data' => [ + 'devices' => [ + 'shelly-plus-01' => [ + 'id' => 'shelly-plus-01', + 'name' => 'Gate From Shelly Cloud', + 'type' => 'SPSW-001PE16EU', + 'cloud_online' => true, ], - 'relays' => [ - ['ison' => false], - ], - ], - 'device-key-3' => [ - '_dev_info' => [ - 'id' => 'shelly-sensor-01', - 'code' => 'SHHT-1', - 'online' => 1, - ], - 'sensor' => [ - 'temperature' => 21.5, + 'shelly-pro-03' => [ + 'id' => 'shelly-pro-03', + 'name' => 'Program Picker From Shelly Cloud', + 'type' => 'SPSW-201XE16EU', + 'cloud_online' => false, ], ], ], - ], - ]; - }); + ]; + }); expect($inventory->listRelayOptions())->toBe([ [ 'id' => 'shelly-plus-01', - 'name' => 'Entry Gate (shelly-plus-01) · SPSW-001PE16EU', + 'name' => 'Gate From Shelly Cloud (Shelly Plus 1PM)', 'device_id' => 'shelly-plus-01', - 'device_name' => 'Entry Gate', + 'device_name' => 'Gate From Shelly Cloud', + 'cloud_name' => 'Gate From Shelly Cloud', + 'device_type' => 'Shelly Plus 1PM', 'code' => 'SPSW-001PE16EU', + 'control_type' => 'Switch', + 'control_name' => 'Entrance Relay', + 'status_color' => 'Green', 'online' => true, ], + [ + 'id' => 'shelly-pro-03', + 'name' => 'Program Picker From Shelly Cloud (Shelly Pro 2PM)', + 'device_id' => 'shelly-pro-03', + 'device_name' => 'Program Picker From Shelly Cloud', + 'cloud_name' => 'Program Picker From Shelly Cloud', + 'device_type' => 'Shelly Pro 2PM', + 'code' => 'SPSW-201XE16EU', + 'control_type' => 'Relay', + 'control_name' => 'Program Picker', + 'status_color' => 'Red', + 'online' => false, + ], [ 'id' => 'shelly-legacy-02', - 'name' => 'shelly-legacy-02 · SHSW-1', + 'name' => 'shelly-legacy-02 (Shelly 1)', 'device_id' => 'shelly-legacy-02', 'device_name' => null, + 'cloud_name' => null, + 'device_type' => 'Shelly 1', 'code' => 'SHSW-1', + 'control_type' => 'Relay', + 'control_name' => null, + 'status_color' => 'Red', 'online' => false, ], ]); }); +it('falls back to local device and control names when Shelly cloud list metadata is unavailable', function (): void { + $inventory = (new shelly_relay_inventory()) + ->setInventoryFetcher(static function (): array { + return [ + 'isok' => true, + 'data' => [ + 'devices_status' => [ + 'device-key-1' => [ + '_dev_info' => [ + 'id' => 'shelly-plus-01', + 'code' => 'SPSW-001PE16EU', + 'model' => 'Shelly Plus 1PM', + 'online' => 1, + ], + 'name' => 'Entry Gate', + 'status' => [ + 'switch:0' => [ + 'output' => false, + 'name' => 'Entrance Relay', + ], + ], + ], + ], + ], + ]; + }) + ->setDeviceListFetcher(static function (): array { + return [ + 'isok' => false, + 'errors' => [ + '404' => 'Requested method was not found', + ], + ]; + }); + + expect($inventory->listRelayOptions())->toBe([ + [ + 'id' => 'shelly-plus-01', + 'name' => 'Entry Gate / Entrance Relay (Shelly Plus 1PM)', + 'device_id' => 'shelly-plus-01', + 'device_name' => 'Entry Gate', + 'cloud_name' => null, + 'device_type' => 'Shelly Plus 1PM', + 'code' => 'SPSW-001PE16EU', + 'control_type' => 'Switch', + 'control_name' => 'Entrance Relay', + 'status_color' => 'Green', + 'online' => true, + ], + ]); +}); + it('fails fast when Shelly inventory does not include owned devices status', function (): void { $inventory = (new shelly_relay_inventory())->setInventoryFetcher(static function (): array { return [