createSelfServeScenario([ 'department' => [ 'name' => 'Edge Expected State Department', ], 'relay_ids' => [ 'entry' => 'expected-entry', 'exit' => 'expected-exit', 'machine' => 'expected-machine', 'program_picker' => 'expected-program', 'cleaner' => 'expected-cleaner', ], ]); $departmentId = (int)$scenario['department']['id']; $laneId = (int)$scenario['lane']['id']; api_fixtures()->setDepartmentShellyTransportMode($departmentId, 'gateway'); $session = api_fixtures()->createEdgeOperatorSession($departmentId, [ 'modules_selfserve_lane_relay_machine_status_set', 'modules_selfserve_lane_relay_machine_cleaner_status_set', ]); $gateway = api_fixtures()->createClaimedEdgeGateway([ 'department_id' => $departmentId, 'label' => 'Expected State Gateway', ]); $relayIps = [ 'expected-entry' => '10.41.0.11', 'expected-exit' => '10.41.0.12', 'expected-machine' => '10.41.0.13', 'expected-program' => '10.41.0.14', 'expected-cleaner' => '10.41.0.15', ]; foreach ($relayIps as $relayId => $ip) { api_fixtures()->createEdgeRelayBinding([ 'gateway_id' => (int)$gateway['id'], 'department_id' => $departmentId, 'relay_id' => $relayId, 'device_id' => 'device-' . $relayId, 'local_ip' => $ip, 'metadata' => [ 'device_type' => 'SHELLY_SWITCH', ], ]); } $queueResponse = api_client()->post('/modules/self-serve/lane/hardware/batch/set', [ 'lane_id' => $laneId, 'transport' => 'gateway', 'commands' => [ ['target' => 'MACHINE', 'on' => true], ['target' => 'CLEANER', 'on' => false], ], ], $session['headers']); $queueResponse ->assertStatus(202) ->assertEnvelope() ->assertSuccess(); $expectedResponse = api_client()->post('/edge-agent/gateways/' . (int)$gateway['id'] . '/expected-relay-states', [ 'agent_token' => (string)$gateway['agent_token'], 'wait_seconds' => 0, ]); $expectedResponse ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); $lanes = $expectedResponse->data()['lanes'] ?? []; expect($lanes)->toHaveKey((string)$laneId); $laneStates = $lanes[(string)$laneId]; expect(array_keys($laneStates))->toBe([ 'ENTRANCE_GATE', 'EXIT_GATE', 'MACHINE', 'PROGRAM_PICKER', 'CLEANER', ]) ->and($laneStates['MACHINE']['device'])->toBe([ 'ip' => '10.41.0.13', 'id' => 'device-expected-machine', 'type' => 'SHELLY_SWITCH', ]) ->and($laneStates['MACHINE']['state'])->toBeTrue() ->and($laneStates['MACHINE']['completed'])->toBeNull() ->and($laneStates['CLEANER']['state'])->toBeFalse() ->and($laneStates['CLEANER']['completed'])->toBeNull() ->and($laneStates['ENTRANCE_GATE']['completed'])->not->toBeNull(); expect($expectedResponse->data())->toHaveKey('CLEANER') ->and($expectedResponse->data()['MACHINE']['device'] ?? null)->toBe($laneStates['MACHINE']['device']) ->and($expectedResponse->data()['CLEANER']['completed'])->toBeNull(); $machineUpdated = (string)$laneStates['MACHINE']['updated']; $cleanerUpdated = (string)$laneStates['CLEANER']['updated']; $resultResponse = api_client()->post('/edge-agent/gateways/' . (int)$gateway['id'] . '/relay-state-results', [ 'agent_token' => (string)$gateway['agent_token'], 'results' => [ [ 'lane_id' => $laneId, 'role' => 'MACHINE', 'state' => true, 'updated' => $machineUpdated, 'ok' => true, 'completed' => '2026-07-01T09:10:00+00:00', ], [ 'lane_id' => $laneId, 'role' => 'CLEANER', 'state' => false, 'updated' => $cleanerUpdated, 'ok' => false, 'skipped' => true, 'error' => 'Relay offline', ], ], ]); $resultResponse ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); expect($resultResponse->data()['summary'] ?? []) ->toHaveKey('completed', 1) ->toHaveKey('skipped', 1) ->toHaveKey('failed', 0); $afterResult = api_client()->post('/edge-agent/gateways/' . (int)$gateway['id'] . '/expected-relay-states', [ 'agent_token' => (string)$gateway['agent_token'], 'wait_seconds' => 0, ]); $afterResult ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); $afterLaneStates = $afterResult->data()['lanes'][(string)$laneId] ?? []; expect($afterLaneStates['MACHINE']['completed'] ?? null)->not->toBeNull() ->and($afterLaneStates['CLEANER']['completed'] ?? null)->toBeNull() ->and($afterLaneStates['CLEANER']['last_error'] ?? null)->toBe('Relay offline'); $staleResponse = api_client()->post('/edge-agent/gateways/' . (int)$gateway['id'] . '/relay-state-results', [ 'agent_token' => (string)$gateway['agent_token'], 'results' => [[ 'lane_id' => $laneId, 'role' => 'CLEANER', 'state' => false, 'updated' => '2000-01-01 00:00:00', 'ok' => true, 'completed' => '2026-07-01T09:11:00+00:00', ]], ]); $staleResponse ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); expect($staleResponse->data()['summary']['stale'] ?? null)->toBe(1) ->and($staleResponse->data()['rejected'][0]['reason'] ?? null)->toBe('STALE_STATE'); });