Files
correx/testing/deterministic/src/test/kotlin/RouterContextBuilderTest.kt
T
kami e05532e7b2 feat: implement CreateGrant handler and grant-aware approval engine
- GlobalStreamHandler.handleCreateGrant validates scope (SESSION/STAGE)
  and sends ProtocolError to client on validation failure instead of
  silent log.warn + drop.
- Derive event stageId directly from GrantScope type, removing
  redundant stageIdForEvent tuple.
- SessionOrchestrator integrates ApprovalEngine to check active grants
  before requesting user approval for T2+ tool calls.
- ContextEntry gains EntryRole (SYSTEM/USER/ASSISTANT/TOOL) field for
  proper chat message role mapping.
- RouterContextBuilder.build is now suspend; uses Tokenizer for
  accurate token estimation with fallback to character-based estimate.
- LlamaCppInferenceProvider maps EntryRole to ChatMessage role instead
  of heuristic layer-based inference.
2026-05-28 14:06:58 +04:00

592 lines
25 KiB
Kotlin

import com.correx.core.context.model.ContextLayer
import com.correx.core.context.model.ContextPack
import com.correx.core.context.model.EntryRole
import com.correx.core.context.model.TokenBudget
import kotlinx.coroutines.runBlocking
import com.correx.core.events.types.SessionId
import com.correx.core.events.types.StageId
import com.correx.core.router.DefaultRouterContextBuilder
import com.correx.core.router.model.RouterConfig
import com.correx.core.router.model.RouterL2Entry
import com.correx.core.router.model.RouterState
import com.correx.core.router.model.RouterTurn
import com.correx.core.router.model.StageOutcomeKind
import com.correx.core.router.model.TurnRole
import com.correx.core.router.model.WorkflowStatus
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
class RouterContextBuilderTest {
private val config = RouterConfig(conversationKeepLast = 3, tokenBudget = TokenBudget(limit = 200))
private val builder = DefaultRouterContextBuilder(config)
private fun buildPack(state: RouterState, budget: TokenBudget): ContextPack = runBlocking {
buildPack(state, budget)
}
private val sessionId = SessionId("test-session")
private val stageId = StageId("stage-1")
private val clock = Clock.System
// --------------------------------------------------------------------------
// Budget enforcement
// --------------------------------------------------------------------------
@Test
fun `build fits within budget when all entries are small`() {
val state = RouterState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, "hi", clock.now()),
),
)
val pack = buildPack(state, TokenBudget(limit = 10000))
assertEquals(10000, pack.budgetLimit)
assertTrue(pack.budgetUsed <= pack.budgetLimit)
}
@Test
fun `build drops entries when budget is exceeded`() {
// Build a state where conversation + L2 entries together exceed the tight budget
val longContent = "x".repeat(800) // ~400 tokens each
val state = RouterState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, longContent, clock.now()),
RouterTurn(TurnRole.ROUTER, longContent, clock.now()),
RouterTurn(TurnRole.USER, longContent, clock.now()),
),
l2Memory = listOf(
RouterL2Entry(StageId("s1"), "summary", StageOutcomeKind.SUCCESS, clock.now()),
),
)
val pack = buildPack(state, TokenBudget(limit = 100))
// L0 entries (system prompt + workflow status) always fit; L1/L2 should be dropped
assertTrue(pack.compressionMetadata.entriesDropped > 0)
}
@Test
fun `build drops entries oldest-first for L2 memory`() {
val short = "ok"
val state = RouterState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
l2Memory = listOf(
RouterL2Entry(StageId("s1"), short, StageOutcomeKind.SUCCESS, Instant.parse("2026-01-01T00:00:00Z")),
RouterL2Entry(StageId("s2"), short, StageOutcomeKind.SUCCESS, Instant.parse("2026-01-02T00:00:00Z")),
RouterL2Entry(StageId("s3"), short, StageOutcomeKind.SUCCESS, Instant.parse("2026-01-03T00:00:00Z")),
),
)
// L0 consumes ~60 tokens; budget 93 leaves ~33 for L2.
// Each L2 entry is ~12 tokens; 2 fit (s1, s2), s3 is dropped
val pack = buildPack(state, TokenBudget(limit = 93))
val l2Entries = pack.layers[ContextLayer.L2] ?: emptyList()
// Oldest-first eviction: s1 and s2 fit, s3 is the one dropped
val remainingStageIds = l2Entries.map { it.sourceId }.toSet()
assertEquals(2, l2Entries.size)
assertTrue(remainingStageIds.contains("s1"))
assertTrue(remainingStageIds.contains("s2"))
assertEquals(0, l2Entries.count { it.sourceId == "s3" })
}
@Test
fun `build drops conversation turns oldest-first when conversationKeepLast exceeded`() {
val configWide = RouterConfig(conversationKeepLast = 2, tokenBudget = TokenBudget(limit = 10000))
val builderWide = DefaultRouterContextBuilder(configWide)
val longContent = "x".repeat(400)
val state = RouterState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, longContent, Instant.parse("2026-01-01T00:00:00Z")),
RouterTurn(TurnRole.ROUTER, longContent, Instant.parse("2026-01-02T00:00:00Z")),
RouterTurn(TurnRole.USER, longContent, Instant.parse("2026-01-03T00:00:00Z")),
RouterTurn(TurnRole.ROUTER, longContent, Instant.parse("2026-01-04T00:00:00Z")),
),
)
// Budget allows L0 + 2 long conversation turns; oldest 2 turns are dropped
val pack = builderWide.build(state, TokenBudget(limit = 500))
val l1Entries = pack.layers[ContextLayer.L1] ?: emptyList()
// Only the last 2 turns (oldest-first drop) should remain
assertEquals(2, l1Entries.size)
}
// --------------------------------------------------------------------------
// L0 immutability
// --------------------------------------------------------------------------
@Test
fun `L0 system prompt is always present`() {
val state = RouterState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.IDLE,
currentStageId = null,
)
val pack = buildPack(state, TokenBudget(limit = 0))
val l0Entries = pack.layers[ContextLayer.L0] ?: emptyList()
assertTrue(l0Entries.any { it.sourceType == "systemPrompt" })
}
@Test
fun `L0 workflow status is always present`() {
val state = RouterState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.FAILED,
currentStageId = StageId("failed-stage"),
)
val pack = buildPack(state, TokenBudget(limit = 0))
val l0Entries = pack.layers[ContextLayer.L0] ?: emptyList()
assertTrue(l0Entries.any { it.sourceType == "workflowStatus" })
val workflowEntry = l0Entries.find { it.sourceType == "workflowStatus" }
assertNotNull(workflowEntry)
assertTrue(workflowEntry!!.content.contains("failed"))
}
@Test
fun `L0 entries survive zero budget`() {
val state = RouterState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.COMPLETED,
currentStageId = null,
conversationHistory = emptyList(),
l2Memory = emptyList(),
)
val pack = buildPack(state, TokenBudget(limit = 0))
val l0Entries = pack.layers[ContextLayer.L0] ?: emptyList()
assertEquals(2, l0Entries.size) // systemPrompt + workflowStatus
}
@Test
fun `L0 entries survive budget that cannot cover L1 or L2`() {
val state = RouterState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, "x".repeat(500), clock.now()),
),
l2Memory = listOf(
RouterL2Entry(StageId("s1"), "x".repeat(500), StageOutcomeKind.SUCCESS, clock.now()),
),
)
val pack = buildPack(state, TokenBudget(limit = 50))
val l0Entries = pack.layers[ContextLayer.L0] ?: emptyList()
val l1Entries = pack.layers[ContextLayer.L1] ?: emptyList()
val l2Entries = pack.layers[ContextLayer.L2] ?: emptyList()
assertEquals(2, l0Entries.size)
assertEquals(0, l1Entries.size)
assertEquals(0, l2Entries.size)
assertTrue(pack.compressionMetadata.entriesDropped > 0)
}
// --------------------------------------------------------------------------
// L2 oldest-first eviction
// --------------------------------------------------------------------------
@Test
fun `L2 entries are evicted in insertion order (oldest first)`() {
val configTight = RouterConfig(conversationKeepLast = 0, tokenBudget = TokenBudget(limit = 10))
val builderTight = DefaultRouterContextBuilder(configTight)
val baseTime = Instant.parse("2026-01-01T00:00:00Z")
val state = RouterState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
l2Memory = listOf(
RouterL2Entry(StageId("s1"), "old", StageOutcomeKind.SUCCESS, baseTime),
RouterL2Entry(StageId("s2"), "mid", StageOutcomeKind.SUCCESS, Instant.parse("2026-01-01T01:00:00Z")),
RouterL2Entry(StageId("s3"), "new", StageOutcomeKind.SUCCESS, Instant.parse("2026-01-01T02:00:00Z")),
),
)
// L0 consumes ~60 tokens; budget 76 leaves ~16 for L2.
// Each L2 entry is ~12 tokens; only 1 fits — oldest (s1) survives
val pack = builderTight.build(state, TokenBudget(limit = 76))
val l2Entries = pack.layers[ContextLayer.L2] ?: emptyList()
// Oldest-first eviction: s1 fits, s2 and s3 are dropped
assertEquals(1, l2Entries.size)
assertEquals("s1", l2Entries[0].sourceId)
}
@Test
fun `L2 entries fit within budget are all retained`() {
val config = RouterConfig(conversationKeepLast = 0, tokenBudget = TokenBudget(limit = 10000))
val builder = DefaultRouterContextBuilder(config)
val state = RouterState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
l2Memory = listOf(
RouterL2Entry(StageId("s1"), "summary", StageOutcomeKind.SUCCESS, clock.now()),
RouterL2Entry(StageId("s2"), "summary", StageOutcomeKind.FAILURE, clock.now()),
),
)
val pack = buildPack(state, TokenBudget(limit = 10000))
val l2Entries = pack.layers[ContextLayer.L2] ?: emptyList()
assertEquals(2, l2Entries.size)
assertEquals(0, pack.compressionMetadata.entriesDropped)
}
// --------------------------------------------------------------------------
// Layer classification
// --------------------------------------------------------------------------
@Test
fun `system prompt is classified as L0`() {
val state = RouterState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.IDLE,
)
val pack = buildPack(state, TokenBudget(limit = 10000))
val l0Entries = pack.layers[ContextLayer.L0]
assertNotNull(l0Entries)
assertTrue(l0Entries!!.any { it.sourceType == "systemPrompt" && it.layer == ContextLayer.L0 })
}
@Test
fun `workflow status is classified as L0`() {
val state = RouterState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
)
val pack = buildPack(state, TokenBudget(limit = 10000))
val l0Entries = pack.layers[ContextLayer.L0]
assertTrue(l0Entries!!.any { it.sourceType == "workflowStatus" && it.layer == ContextLayer.L0 })
}
@Test
fun `conversation turns are classified as L1`() {
val state = RouterState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, "hello", clock.now()),
RouterTurn(TurnRole.ROUTER, "hi", clock.now()),
),
)
val pack = buildPack(state, TokenBudget(limit = 10000))
val l1Entries = pack.layers[ContextLayer.L1]
assertNotNull(l1Entries)
assertEquals(2, l1Entries!!.size)
assertTrue(l1Entries.all { it.layer == ContextLayer.L1 })
assertTrue(l1Entries.all { it.sourceType == "conversation" })
}
@Test
fun `l2 memory entries are classified as L2`() {
val state = RouterState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
l2Memory = listOf(
RouterL2Entry(StageId("s1"), "summary", StageOutcomeKind.SUCCESS, clock.now()),
),
)
val pack = buildPack(state, TokenBudget(limit = 10000))
val l2Entries = pack.layers[ContextLayer.L2]
assertNotNull(l2Entries)
assertEquals(1, l2Entries!!.size)
assertTrue(l2Entries.all { it.layer == ContextLayer.L2 })
assertTrue(l2Entries.all { it.sourceType == "stageSummary" })
}
@Test
fun `no entries other than L0 L1 L2 appear in pack`() {
val state = RouterState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, "hello", clock.now()),
),
l2Memory = listOf(
RouterL2Entry(StageId("s1"), "summary", StageOutcomeKind.SUCCESS, clock.now()),
),
)
val pack = buildPack(state, TokenBudget(limit = 10000))
val presentLayers = pack.layers.keys
assertTrue(presentLayers.containsAll(listOf(ContextLayer.L0, ContextLayer.L1, ContextLayer.L2)))
assertFalse(presentLayers.contains(ContextLayer.L3))
assertFalse(presentLayers.contains(ContextLayer.L4))
}
// --------------------------------------------------------------------------
// Conversation history capping (conversationKeepLast)
// --------------------------------------------------------------------------
@Test
fun `conversationHistory respects conversationKeepLast from config`() {
val config = RouterConfig(conversationKeepLast = 2, tokenBudget = TokenBudget(limit = 10000))
val builder = DefaultRouterContextBuilder(config)
val state = RouterState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, "one", Instant.parse("2026-01-01T00:00:00Z")),
RouterTurn(TurnRole.ROUTER, "two", Instant.parse("2026-01-02T00:00:00Z")),
RouterTurn(TurnRole.USER, "three", Instant.parse("2026-01-03T00:00:00Z")),
RouterTurn(TurnRole.ROUTER, "four", Instant.parse("2026-01-04T00:00:00Z")),
RouterTurn(TurnRole.USER, "five", Instant.parse("2026-01-05T00:00:00Z")),
),
)
val pack = buildPack(state, TokenBudget(limit = 10000))
val l1Entries = pack.layers[ContextLayer.L1] ?: emptyList()
// conversationKeepLast=2, so only the last 2 turns should appear
assertEquals(2, l1Entries.size)
}
@Test
fun `conversationHistory takeLast preserves ordering`() {
val config = RouterConfig(conversationKeepLast = 3, tokenBudget = TokenBudget(limit = 10000))
val builder = DefaultRouterContextBuilder(config)
val state = RouterState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, "first", Instant.parse("2026-01-01T00:00:00Z")),
RouterTurn(TurnRole.ROUTER, "second", Instant.parse("2026-01-02T00:00:00Z")),
RouterTurn(TurnRole.USER, "third", Instant.parse("2026-01-03T00:00:00Z")),
RouterTurn(TurnRole.ROUTER, "fourth", Instant.parse("2026-01-04T00:00:00Z")),
),
)
val pack = buildPack(state, TokenBudget(limit = 10000))
val l1Entries = pack.layers[ContextLayer.L1] ?: emptyList()
assertEquals(3, l1Entries.size)
// The last 3: second, third, fourth
assertTrue(l1Entries.any { it.content.contains("second") })
assertTrue(l1Entries.any { it.content.contains("third") })
assertTrue(l1Entries.any { it.content.contains("fourth") })
}
// --------------------------------------------------------------------------
// Empty state
// --------------------------------------------------------------------------
@Test
fun `build with empty state produces L0 only`() {
val state = RouterState()
val pack = buildPack(state, TokenBudget(limit = 10000))
val l0Entries = pack.layers[ContextLayer.L0]
assertNotNull(l0Entries)
assertEquals(2, l0Entries!!.size)
assertTrue(pack.layers[ContextLayer.L1].isNullOrEmpty())
assertTrue(pack.layers[ContextLayer.L2].isNullOrEmpty())
}
@Test
fun `pack contains correct metadata on empty state`() {
val state = RouterState()
val pack = buildPack(state, TokenBudget(limit = 5000))
assertEquals(5000, pack.budgetLimit)
assertEquals(0, pack.compressionMetadata.entriesDropped)
assertEquals(listOf("L0Immutable", "Conversation"), pack.compressionMetadata.appliedStrategies)
assertTrue(pack.compressionMetadata.truncatedLayers.isEmpty())
}
@Test
fun `pack has correct context pack id and session info`() {
val state = RouterState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
)
val pack = buildPack(state, TokenBudget(limit = 1000))
assertEquals("test-session-router-pack", pack.id.value)
assertEquals(sessionId, pack.sessionId)
assertEquals(stageId, pack.stageId)
}
// --------------------------------------------------------------------------
// Content formatting
// --------------------------------------------------------------------------
@Test
fun `system prompt has expected content`() {
val state = RouterState()
val pack = buildPack(state, TokenBudget(limit = 10000))
val systemEntry = pack.layers[ContextLayer.L0]?.find { it.sourceType == "systemPrompt" }
assertNotNull(systemEntry)
assertEquals(
"You are a routing assistant. Provide guidance based on workflow state and conversation context.",
systemEntry!!.content,
)
}
@Test
fun `workflow status entry contains status and stage`() {
val state = RouterState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.COMPLETED,
currentStageId = null,
)
val pack = buildPack(state, TokenBudget(limit = 10000))
val workflowEntry = pack.layers[ContextLayer.L0]?.find { it.sourceType == "workflowStatus" }
assertNotNull(workflowEntry)
assertTrue(workflowEntry!!.content.contains("Status: Completed"))
assertTrue(workflowEntry.content.contains("Stage: none"))
}
@Test
fun `conversation turn entry is formatted with role prefix`() {
val state = RouterState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, "user message", clock.now()),
),
)
val pack = buildPack(state, TokenBudget(limit = 10000))
val l1Entries = pack.layers[ContextLayer.L1] ?: emptyList()
assertEquals(1, l1Entries.size)
assertEquals("user message", l1Entries[0].content)
assertEquals(EntryRole.USER, l1Entries[0].role)
}
@Test
fun `l2 entry is formatted with stage outcome and summary`() {
val state = RouterState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
l2Memory = listOf(
RouterL2Entry(StageId("stage-x"), "completed with 3 items", StageOutcomeKind.SUCCESS, clock.now()),
),
)
val pack = buildPack(state, TokenBudget(limit = 10000))
val l2Entries = pack.layers[ContextLayer.L2] ?: emptyList()
assertEquals(1, l2Entries.size)
val entry = l2Entries[0]
assertEquals("stage-x", entry.sourceId)
assertTrue(entry.content.contains("Stage stage-x"))
assertTrue(entry.content.contains("SUCCESS"))
assertTrue(entry.content.contains("completed with 3 items"))
}
// --------------------------------------------------------------------------
// Token budget accounting
// --------------------------------------------------------------------------
@Test
fun `budgetUsed equals sum of tokenEstimate of all retained entries`() {
val state = RouterState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, "hello world", clock.now()),
),
l2Memory = listOf(
RouterL2Entry(StageId("s1"), "summary", StageOutcomeKind.SUCCESS, clock.now()),
),
)
val pack = buildPack(state, TokenBudget(limit = 10000))
val allEntries = pack.layers.values.flatten()
val computedSum = allEntries.sumOf { it.tokenEstimate }
assertEquals(computedSum, pack.budgetUsed)
}
@Test
fun `compressionMetadata entriesDropped reflects dropped count`() {
val config = RouterConfig(conversationKeepLast = 0, tokenBudget = TokenBudget(limit = 10))
val builder = DefaultRouterContextBuilder(config)
val state = RouterState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, "x".repeat(200), clock.now()),
),
l2Memory = listOf(
RouterL2Entry(StageId("s1"), "x".repeat(500), StageOutcomeKind.SUCCESS, clock.now()),
RouterL2Entry(StageId("s2"), "x".repeat(500), StageOutcomeKind.SUCCESS, clock.now()),
),
)
val pack = buildPack(state, TokenBudget(limit = 65))
// L0 consumes ~60 tokens, leaving 5 — both L2 entries (each ~259 tokens) dropped
// conversationKeepLast=0 means conversation entry is not included
assertEquals(2, pack.compressionMetadata.entriesDropped)
}
// --------------------------------------------------------------------------
// Integration: full lifecycle
// --------------------------------------------------------------------------
@Test
fun `build with full state including all layers`() {
val config = RouterConfig(conversationKeepLast = 2, tokenBudget = TokenBudget(limit = 10000))
val builder = DefaultRouterContextBuilder(config)
val state = RouterState(
sessionId = sessionId,
workflowStatus = WorkflowStatus.RUNNING,
currentStageId = stageId,
conversationHistory = listOf(
RouterTurn(TurnRole.USER, "first turn", Instant.parse("2026-01-01T00:00:00Z")),
RouterTurn(TurnRole.ROUTER, "first reply", Instant.parse("2026-01-02T00:00:00Z")),
RouterTurn(TurnRole.USER, "second turn", Instant.parse("2026-01-03T00:00:00Z")),
RouterTurn(TurnRole.ROUTER, "second reply", Instant.parse("2026-01-04T00:00:00Z")),
),
l2Memory = listOf(
RouterL2Entry(
StageId("s1"),
"stage one done",
StageOutcomeKind.SUCCESS,
Instant.parse("2026-01-01T10:00:00Z"),
),
RouterL2Entry(
StageId("s2"),
"stage two failed",
StageOutcomeKind.FAILURE,
Instant.parse("2026-01-02T10:00:00Z"),
),
),
)
val pack = buildPack(state, TokenBudget(limit = 10000))
// L0: system prompt + workflow status
val l0 = pack.layers[ContextLayer.L0]!!
assertEquals(2, l0.size)
assertEquals("systemPrompt", l0[0].sourceType)
assertEquals("workflowStatus", l0[1].sourceType)
// L1: last 2 conversation turns
val l1 = pack.layers[ContextLayer.L1]!!
assertEquals(2, l1.size)
assertEquals("conversation", l1[0].sourceType)
// L2: both stage summaries fit
val l2 = pack.layers[ContextLayer.L2]!!
assertEquals(2, l2.size)
assertEquals("stageSummary", l2[0].sourceType)
// Budget and metadata
assertTrue(pack.budgetUsed > 0)
assertTrue(pack.budgetUsed <= pack.budgetLimit)
assertEquals(0, pack.compressionMetadata.entriesDropped)
}
@Test
fun `build with null sessionId produces unknown pack id`() {
val state = RouterState(
workflowStatus = WorkflowStatus.IDLE,
)
val pack = buildPack(state, TokenBudget(limit = 1000))
assertTrue(pack.id.value.contains("unknown"))
}
}