package com.example.aiapp import android.content.Context import java.util.concurrent.CopyOnWriteArrayList /** * P0's benchmark gate (see docs/RUST.md and the 2026-09-05 decision): an in-process fake of the * backend, so the `bench` build type can drive a real session screen -- the real * [TranscriptSource], the real fold, the real paging -- with no server and no network permission. * * Only ever installed when [BuildConfig.FIXTURE_MODE] is true (see [MainActivity]); everything else * in this build compiles it in but never calls it, since Kotlin has no per-build-type source set * that both [MainActivity] (which every variant compiles) and this can share without one. * * The design: [requestFromServer] and [Sse] talk to `https://$FIXTURE_HOST:$FIXTURE_PORT` through * ordinary `java.net.URL`, exactly as they would talk to a real server. A * [java.net.URLStreamHandlerFactory] registered once for the whole process intercepts every * `https://` connection to that host and answers from this object's in-memory event log instead of * opening a socket -- see BenchNetwork.kt. Everything above that (TranscriptSource, SessionScreen, * the fold, uniqueItems) never learns the difference. */ object BenchFixture { const val FIXTURE_HOST = "bench.fixture.invalid" const val FIXTURE_PORT = 1 /** How many of the fixture's events are the opening backlog; see bench-fixture/README.md. */ private const val BACKLOG_COUNT = 3202 val settings = ServerSettings(FIXTURE_HOST, FIXTURE_PORT, "bench") /** The session id every bench run opens; nothing else in this build ever mints one. */ const val SESSION_ID = "bench-fixture-session" /** * The whole transcript, seq order, growing as [pushLive] is called during the streaming phase. * Read by both the REST page handler and the SSE handler, so a page requested mid- stream and a * live frame agree on what has "already happened" -- the same thing a real server's own * transcript file guarantees. */ private val log = CopyOnWriteArrayList>() /** The events not yet appended to [log] -- the streaming phase's own source. */ private var streamTail: List> = emptyList() private val images = mutableMapOf() @Volatile private var loaded = false /** * Parses the bundled fixture once. Safe to call more than once; only the first does anything. */ @Synchronized fun ensureLoaded(context: Context) { if (loaded) return val lines = context.assets.open("transcript.jsonl").bufferedReader().readLines().filter { it.isNotBlank() } val parsed = lines.map { it to parseSeqEvent(it) } log.addAll(parsed.take(BACKLOG_COUNT)) streamTail = parsed.drop(BACKLOG_COUNT) for (name in listOf("bench1.png", "bench2.png")) { images[name] = context.assets.open(name).readBytes() } loaded = true } /** The events the streaming phase has left to send. */ fun remainingStreamEvents(): Int = streamTail.size /** Sends the next fixture event onto the live log, as a real SSE frame would arrive. */ fun pushNextLiveEvent(): Boolean { val next = streamTail.firstOrNull() ?: return false streamTail = streamTail.drop(1) log.add(next) return true } /** Undoes [pushNextLiveEvent] and reloads the opening backlog, for running the bench twice. */ @Synchronized fun resetToBacklog(context: Context) { loaded = false log.clear() ensureLoaded(context) } fun fileBytes(name: String): ByteArray? = images[name] /** * Raw JSON lines with seq > [after], in order -- what an `/events?after=` connection replays. */ fun linesAfter(after: Long): List = log.filter { it.second.seq > after }.map { it.first } /** * One REST page: [fetchTranscript]'s `before`/`limit`/`after`, against the growing log. Ignores * `coalesce` -- the fixture's own deltas are already split the way a real reply streams, and * what the benchmark exercises is the fold and the paging, not the server's row-joining, which * client-core's own port tracks separately (CLIENT_CORE.md). */ fun page(before: Long?, limit: Int, after: Long?): List { val upper = before ?: (log.lastOrNull()?.second?.seq?.plus(1) ?: 1L) val candidates = log.filter { it.second.seq < upper && (after == null || it.second.seq > after) } return candidates.takeLast(limit).map { it.first } } }