heartbeat() === null, 'no heartbeat precedes authenticated start'); heartbeatExpect(str_contains((string) $protocol->encode('start', []), '"seq":1'), 'start owns the first sequence'); $heartbeatNowNs += 9_999_999_999; heartbeatExpect($protocol->heartbeat() === null, 'idle heartbeat is throttled below ten seconds'); ++$heartbeatNowNs; heartbeatExpect($protocol->heartbeat() === ": keep-alive\n\n", 'ten idle seconds emit only an SSE comment'); heartbeatExpect($protocol->heartbeat() === null, 'poll bursts do not flood the stream'); $heartbeatNowNs += 9_000_000_000; heartbeatExpect(str_contains((string) $protocol->encode('delta', ['text' => 'synthetic']), '"seq":2'), 'heartbeat does not increment the application sequence or invent a delta'); $heartbeatNowNs += 9_000_000_000; heartbeatExpect($protocol->heartbeat() === null, 'real output resets the idle interval'); $heartbeatNowNs += 1_000_000_000; heartbeatExpect($protocol->heartbeat() !== null, 'heartbeat resumes after real output becomes idle'); heartbeatExpect(str_contains((string) $protocol->encode('done', []), '"seq":3'), 'done retains its original sequence'); $heartbeatNowNs += 100_000_000_000; heartbeatExpect($protocol->heartbeat() === null, 'no heartbeat follows successful terminal'); $failed = new AssistantSseProtocol(); $failed->encode('start', []); $failed->encode('error', ['code' => 'UPSTREAM_TIMEOUT']); $heartbeatNowNs += 100_000_000_000; heartbeatExpect($failed->heartbeat() === null, 'no heartbeat follows timeout or cancellation terminal'); $reflection = new ReflectionClass(DiagnosisController::class); $controller = $reflection->newInstanceWithoutConstructor(); [$emit, $opening] = heartbeatCapture(static fn () => $reflection->getMethod('openAssistantSse')->invoke($controller)); heartbeatExpect(is_callable($emit) && str_starts_with($opening, ':') && str_ends_with($opening, "\n\n"), 'real controller opens a flushed comment stream'); [$accepted, $wire] = heartbeatCapture(static fn () => $emit('heartbeat', [])); heartbeatExpect($accepted && $wire === '', 'suppressed heartbeat is not interpreted as a disconnected client'); heartbeatCapture(static fn () => $emit('start', [])); for ($second = 1; $second <= 90; ++$second) { $heartbeatNowNs += 1_000_000_000; [$accepted, $wire] = heartbeatCapture(static fn () => $emit('heartbeat', [])); heartbeatExpect($accepted && $wire === ($second % 10 === 0 ? ": keep-alive\n\n" : ''), 'silent upstream keeps the connection alive without progress or clinical data'); } [$accepted, $wire] = heartbeatCapture(static fn () => $emit('delta', ['text' => 'synthetic'])); heartbeatExpect($accepted && str_contains($wire, '"seq":2'), 'ninety seconds of waiting do not consume event numbers'); $heartbeatDisconnected = true; $heartbeatNowNs += 10_000_000_000; [$accepted, $wire] = heartbeatCapture(static fn () => $emit('heartbeat', [])); heartbeatExpect(!$accepted && $wire === '', 'disconnected transport receives no more output'); $heartbeatDisconnected = false; heartbeatCapture(static fn () => $emit('error', ['code' => 'UPSTREAM_TIMEOUT'])); [$accepted, $wire] = heartbeatCapture(static fn () => $emit('heartbeat', [])); heartbeatExpect(!$accepted && $wire === '', 'real emitter gates heartbeat on its shared terminal state'); echo "Assistant SSE idle heartbeat: {$checks} checks OK\n"; }