From 378aa21418874463186857eab8b75a25e09a73da Mon Sep 17 00:00:00 2001 From: Miley Chandonnet Date: Mon, 1 Jun 2026 02:37:10 -0500 Subject: [PATCH 1/2] parallelize CI: split jvm-tests into lint, jvm-tests, and android-tests jobs Previously ktlint ran sequentially before all JVM + Android tests in a single job, causing 15+ minute CI times. This splits the work into four parallel jobs (lint, jvm-tests, android-tests, ios-tests) so total CI time is bounded by the slowest individual job. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yml | 62 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd536546..27fdabcc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,8 +7,8 @@ on: branches: [ main ] jobs: - jvm-tests: - name: JVM + Android Tests (ubuntu, JDK 21) + lint: + name: Lint (ubuntu, JDK 21) runs-on: ubuntu-latest steps: @@ -36,9 +36,63 @@ jobs: timeout-minutes: 10 run: ./gradlew ktlintCheck - - name: Run JVM + Android tests + jvm-tests: + name: JVM Tests (ubuntu, JDK 21) + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Create local.properties + run: | + cat > local.properties << 'EOF' + anthropic_api_key=placeholder + google_api_key=placeholder + openai_api_key=placeholder + EOF + + - name: Run JVM tests + timeout-minutes: 20 + run: ./gradlew :ampere-core:jvmTest :ampere-cli:jvmTest :ampere-compose:jvmTest + + android-tests: + name: Android Tests (ubuntu, JDK 21) + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Create local.properties + run: | + cat > local.properties << 'EOF' + anthropic_api_key=placeholder + google_api_key=placeholder + openai_api_key=placeholder + EOF + + - name: Run Android unit tests timeout-minutes: 20 - run: ./gradlew :ampere-core:jvmTest :ampere-core:testDebugUnitTest :ampere-cli:jvmTest :ampere-compose:jvmTest :ampere-compose:testDebugUnitTest + run: ./gradlew :ampere-core:testDebugUnitTest :ampere-compose:testDebugUnitTest ios-tests: name: iOS Simulator Tests (macos, JDK 21) From 845c92fce9b945cd26fe83081983de5e64053418 Mon Sep 17 00:00:00 2001 From: Miley Chandonnet Date: Mon, 1 Jun 2026 03:11:05 -0500 Subject: [PATCH 2/2] replace runBlocking with runTest to virtualize delay() calls in JVM tests 84 delay() calls across 17 test files were executing as real wall-clock time inside runBlocking, contributing to 10+ minute JVM test runs. This converts them to runTest with virtual time so delays complete instantly. Files with a class-level TestScope use runTest(scope.testScheduler) to share the scheduler with the event bus scope; files with independent coroutine scopes use runTest(UnconfinedTestDispatcher()). Co-Authored-By: Claude Sonnet 4.6 --- .../agents/data/TicketRepositoryTest.kt | 65 ++++++++++--------- .../domain/MinimalAutonomousAgentTest.kt | 12 ++-- .../cognition/sparks/PhaseSparkManagerTest.kt | 17 ++--- .../environment/WorkspaceStateStoreTest.kt | 6 +- .../ampere/agents/events/AgentEventApiTest.kt | 22 +++---- .../events/AgentEventBusIntegrationTest.kt | 4 +- .../agents/events/EventBusIntegrationTest.kt | 8 +-- .../events/EventBusLoggingAndErrorsTest.kt | 8 +-- .../ampere/agents/events/EventBusTest.kt | 8 +-- .../ampere/agents/events/EventRouterTest.kt | 4 +- .../meetings/MeetingOrchestratorTest.kt | 35 +++++----- .../events/messages/AgentMessageApiTest.kt | 10 +-- .../events/messages/MessageRouterTest.kt | 4 +- .../escalation/EscalationEventHandlerTest.kt | 8 +-- .../events/relay/EventStreamServiceTest.kt | 20 +++--- .../tickets/TicketMeetingIntegrationTest.kt | 15 +++-- .../events/tickets/TicketOrchestratorTest.kt | 37 ++++++----- 17 files changed, 144 insertions(+), 139 deletions(-) diff --git a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/data/TicketRepositoryTest.kt b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/data/TicketRepositoryTest.kt index 9e81a5e5..8b73cb59 100644 --- a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/data/TicketRepositoryTest.kt +++ b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/data/TicketRepositoryTest.kt @@ -9,7 +9,8 @@ import kotlin.test.assertIs import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.datetime.Clock import kotlinx.datetime.Instant import link.socket.ampere.agents.domain.status.TicketStatus @@ -76,7 +77,7 @@ class TicketRepositoryTest { @Test fun `createTicket successfully inserts and returns ticket`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val ticket = createTicket() val result = repo.createTicket(ticket) @@ -98,7 +99,7 @@ class TicketRepositoryTest { @Test fun `createTicket preserves all ticket fields`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val dueDate = Clock.System.now() val ticket = createTicket( id = "ticket-full", @@ -127,7 +128,7 @@ class TicketRepositoryTest { @Test fun `createTicket returns error for duplicate id`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val ticket = createTicket() repo.createTicket(ticket) @@ -146,7 +147,7 @@ class TicketRepositoryTest { @Test fun `getTicket returns null for non-existent id without throwing`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val result = repo.getTicket("nonexistent-id") assertTrue(result.isSuccess) @@ -156,7 +157,7 @@ class TicketRepositoryTest { @Test fun `getTicket returns ticket for existing id`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val ticket = createTicket() repo.createTicket(ticket) @@ -174,7 +175,7 @@ class TicketRepositoryTest { @Test fun `updateStatus accepts valid transition BACKLOG to READY`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val ticket = createTicket(status = TicketStatus.Backlog) repo.createTicket(ticket) @@ -190,7 +191,7 @@ class TicketRepositoryTest { @Test fun `updateStatus accepts valid transition READY to IN_PROGRESS`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val ticket = createTicket(status = TicketStatus.Ready) repo.createTicket(ticket) @@ -204,7 +205,7 @@ class TicketRepositoryTest { @Test fun `updateStatus accepts valid transition IN_PROGRESS to DONE`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val ticket = createTicket(status = TicketStatus.InProgress) repo.createTicket(ticket) @@ -218,7 +219,7 @@ class TicketRepositoryTest { @Test fun `updateStatus rejects invalid transition BACKLOG to IN_PROGRESS`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val ticket = createTicket(status = TicketStatus.Backlog) repo.createTicket(ticket) @@ -234,7 +235,7 @@ class TicketRepositoryTest { @Test fun `updateStatus rejects invalid transition READY to DONE`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val ticket = createTicket(status = TicketStatus.Ready) repo.createTicket(ticket) @@ -250,7 +251,7 @@ class TicketRepositoryTest { @Test fun `updateStatus rejects invalid transition DONE to any status`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val ticket = createTicket(status = TicketStatus.Done) repo.createTicket(ticket) @@ -264,7 +265,7 @@ class TicketRepositoryTest { @Test fun `updateStatus returns TicketNotFound for nonexistent ticket`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val result = repo.updateStatus("nonexistent", TicketStatus.Ready) assertTrue(result.isFailure) @@ -276,7 +277,7 @@ class TicketRepositoryTest { @Test fun `updateStatus updates the updatedAt timestamp`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val ticket = createTicket(status = TicketStatus.Backlog) repo.createTicket(ticket) @@ -297,7 +298,7 @@ class TicketRepositoryTest { @Test fun `assignTicket assigns agent to ticket`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val ticket = createTicket() repo.createTicket(ticket) @@ -311,7 +312,7 @@ class TicketRepositoryTest { @Test fun `assignTicket can unassign ticket with null`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val ticket = createTicket(assignedAgentId = assigneeAgentId) repo.createTicket(ticket) @@ -325,7 +326,7 @@ class TicketRepositoryTest { @Test fun `assignTicket returns TicketNotFound for nonexistent ticket`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val result = repo.assignTicket("nonexistent", assigneeAgentId) assertTrue(result.isFailure) @@ -340,7 +341,7 @@ class TicketRepositoryTest { @Test fun `getTicketsByStatus returns tickets with matching status`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { repo.createTicket(createTicket(id = "t1", status = TicketStatus.Backlog)) repo.createTicket(createTicket(id = "t2", status = TicketStatus.Backlog)) repo.createTicket(createTicket(id = "t3", status = TicketStatus.Ready)) @@ -356,7 +357,7 @@ class TicketRepositoryTest { @Test fun `getTicketsByStatus returns empty list when no matches`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { repo.createTicket(createTicket(status = TicketStatus.Backlog)) val result = repo.getTicketsByStatus(TicketStatus.Done) @@ -368,7 +369,7 @@ class TicketRepositoryTest { @Test fun `getTicketsByStatus orders by priority DESC then createdAt ASC`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val baseTime = Clock.System.now() val older = baseTime val newer = baseTime + kotlin.time.Duration.parse("1h") @@ -393,7 +394,7 @@ class TicketRepositoryTest { @Test fun `getTicketsByAgent returns tickets assigned to agent`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { repo.createTicket(createTicket(id = "t1", assignedAgentId = "agent-1")) repo.createTicket(createTicket(id = "t2", assignedAgentId = "agent-1")) repo.createTicket(createTicket(id = "t3", assignedAgentId = "agent-2")) @@ -409,7 +410,7 @@ class TicketRepositoryTest { @Test fun `getTicketsByAgent returns empty list for unassigned agent`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { repo.createTicket(createTicket(assignedAgentId = "agent-1")) val result = repo.getTicketsByAgent("agent-unknown") @@ -425,7 +426,7 @@ class TicketRepositoryTest { @Test fun `getAllTickets returns all tickets`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { repo.createTicket(createTicket(id = "t1")) repo.createTicket(createTicket(id = "t2")) repo.createTicket(createTicket(id = "t3")) @@ -439,7 +440,7 @@ class TicketRepositoryTest { @Test fun `getAllTickets returns empty list when no tickets exist`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val result = repo.getAllTickets() assertTrue(result.isSuccess) @@ -453,7 +454,7 @@ class TicketRepositoryTest { @Test fun `getTicketsByPriority returns tickets with matching priority`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { repo.createTicket(createTicket(id = "t1", priority = TicketPriority.CRITICAL)) repo.createTicket(createTicket(id = "t2", priority = TicketPriority.CRITICAL)) repo.createTicket(createTicket(id = "t3", priority = TicketPriority.LOW)) @@ -473,7 +474,7 @@ class TicketRepositoryTest { @Test fun `getTicketsByType returns tickets with matching type`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { repo.createTicket(createTicket(id = "t1", type = TicketType.BUG)) repo.createTicket(createTicket(id = "t2", type = TicketType.BUG)) repo.createTicket(createTicket(id = "t3", type = TicketType.FEATURE)) @@ -493,7 +494,7 @@ class TicketRepositoryTest { @Test fun `getTicketsByCreator returns tickets created by agent`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { repo.createTicket(createTicket(id = "t1", createdByAgentId = "creator-1")) repo.createTicket(createTicket(id = "t2", createdByAgentId = "creator-1")) repo.createTicket(createTicket(id = "t3", createdByAgentId = "creator-2")) @@ -513,7 +514,7 @@ class TicketRepositoryTest { @Test fun `updateTicketDetails updates specified fields only`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val ticket = createTicket( title = "Original Title", description = "Original Description", @@ -537,7 +538,7 @@ class TicketRepositoryTest { @Test fun `updateTicketDetails returns TicketNotFound for nonexistent ticket`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val result = repo.updateTicketDetails( ticketId = "nonexistent", title = "New Title", @@ -555,7 +556,7 @@ class TicketRepositoryTest { @Test fun `deleteTicket removes ticket from database`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val ticket = createTicket() repo.createTicket(ticket) @@ -568,7 +569,7 @@ class TicketRepositoryTest { @Test fun `deleteTicket succeeds even for nonexistent ticket`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val result = repo.deleteTicket("nonexistent") assertTrue(result.isSuccess) @@ -581,7 +582,7 @@ class TicketRepositoryTest { @Test fun `InvalidStateTransition error message contains states`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val ticket = createTicket(status = TicketStatus.Backlog) repo.createTicket(ticket) diff --git a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/domain/MinimalAutonomousAgentTest.kt b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/domain/MinimalAutonomousAgentTest.kt index 473e308b..8c55d47a 100644 --- a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/domain/MinimalAutonomousAgentTest.kt +++ b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/domain/MinimalAutonomousAgentTest.kt @@ -10,9 +10,9 @@ import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import kotlinx.datetime.Clock import link.socket.ampere.agents.config.AgentConfiguration import link.socket.ampere.agents.definition.AgentId @@ -300,7 +300,7 @@ class MinimalAutonomousAgentTest { } @Test - fun `task spark removed after task completes`() = runBlocking { + fun `task spark removed after task completes`() = runTest(UnconfinedTestDispatcher()) { val initialDepth = agent.sparkDepth agent.testRememberTask(stubTask) @@ -389,7 +389,7 @@ class MinimalAutonomousAgentTest { @Test fun `initialize starts agent without error`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { agent.initialize(testScope) // Wait for loop to start executing @@ -402,7 +402,7 @@ class MinimalAutonomousAgentTest { @Test fun `pauseAgent stops the runtime loop and resets working memory`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // Add some state before initializing agent.testRememberIdea(stubIdea) @@ -424,7 +424,7 @@ class MinimalAutonomousAgentTest { @Test fun `resumeAgent can be called after pause`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { agent.initialize(testScope) delay(20) @@ -440,7 +440,7 @@ class MinimalAutonomousAgentTest { @Test fun `shutdownAgent stops loop and clears all memory`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { agent.testRememberIdea(stubIdea) val idea1 = stubIdea.copy(name = "Idea 1") diff --git a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/domain/cognition/sparks/PhaseSparkManagerTest.kt b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/domain/cognition/sparks/PhaseSparkManagerTest.kt index 88e1da6d..509dd1e4 100644 --- a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/domain/cognition/sparks/PhaseSparkManagerTest.kt +++ b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/domain/cognition/sparks/PhaseSparkManagerTest.kt @@ -5,8 +5,9 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.datetime.Clock import link.socket.ampere.agents.config.AgentConfiguration import link.socket.ampere.agents.config.CognitiveConfig @@ -105,7 +106,7 @@ class PhaseSparkManagerTest { } @Test - fun `enterPhase publishes PhaseEntered after current phase is assigned`() = runBlocking { + fun `enterPhase publishes PhaseEntered after current phase is assigned`() = runTest(UnconfinedTestDispatcher()) { val agent = TestAgent() val bus = EventSerialBus(this) val events = mutableListOf() @@ -130,7 +131,7 @@ class PhaseSparkManagerTest { } @Test - fun `sequential transitions publish exit before enter`() = runBlocking { + fun `sequential transitions publish exit before enter`() = runTest(UnconfinedTestDispatcher()) { val agent = TestAgent() val bus = EventSerialBus(this) val events = mutableListOf() @@ -152,7 +153,7 @@ class PhaseSparkManagerTest { } @Test - fun `withPhase restores previous phase spark`() = runBlocking { + fun `withPhase restores previous phase spark`() = runTest(UnconfinedTestDispatcher()) { val agent = TestAgent() val manager = PhaseSparkManager(agent, enabled = true) @@ -165,7 +166,7 @@ class PhaseSparkManagerTest { } @Test - fun `nested withPhase publishes depth-aware bracket events`() = runBlocking { + fun `nested withPhase publishes depth-aware bracket events`() = runTest(UnconfinedTestDispatcher()) { val agent = TestAgent() val bus = EventSerialBus(this) val events = mutableListOf() @@ -235,7 +236,7 @@ class PhaseSparkManagerTest { } @Test - fun `disabled manager does not publish phase events`() = runBlocking { + fun `disabled manager does not publish phase events`() = runTest(UnconfinedTestDispatcher()) { val agent = TestAgent() val bus = EventSerialBus(this) val events = mutableListOf() @@ -271,7 +272,7 @@ class PhaseSparkManagerTest { } @Test - fun `library augments built-in phase spark when spike flag is on and selection matches`() = runBlocking { + fun `library augments built-in phase spark when spike flag is on and selection matches`() = runTest(UnconfinedTestDispatcher()) { val sources = listOf( DeclarativePhaseSparkSource( id = "cooking-domain", @@ -303,7 +304,7 @@ class PhaseSparkManagerTest { } @Test - fun `multi-spark library push and pop preserves order on exit`() = runBlocking { + fun `multi-spark library push and pop preserves order on exit`() = runTest(UnconfinedTestDispatcher()) { val extraSources = listOf( DeclarativePhaseSparkSource( id = "extra-a", diff --git a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/environment/WorkspaceStateStoreTest.kt b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/environment/WorkspaceStateStoreTest.kt index 588451e6..f8a98845 100644 --- a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/environment/WorkspaceStateStoreTest.kt +++ b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/environment/WorkspaceStateStoreTest.kt @@ -8,9 +8,9 @@ import kotlin.test.assertNull import kotlin.test.assertTrue import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import kotlinx.datetime.Clock import kotlinx.datetime.Instant import link.socket.ampere.agents.domain.Urgency @@ -515,7 +515,7 @@ class WorkspaceStateStoreTest { @Test fun `store reacts to events published on the bus`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val scope = TestScope(UnconfinedTestDispatcher()) val bus = EventSerialBus(scope) val store = WorkspaceStateStore( @@ -566,7 +566,7 @@ class WorkspaceStateStoreTest { @Test fun `full task lifecycle through bus`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val scope = TestScope(UnconfinedTestDispatcher()) val bus = EventSerialBus(scope) val store = WorkspaceStateStore( diff --git a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/AgentEventApiTest.kt b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/AgentEventApiTest.kt index f6707aac..8c2e5714 100644 --- a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/AgentEventApiTest.kt +++ b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/AgentEventApiTest.kt @@ -10,9 +10,9 @@ import kotlin.test.assertNotEquals import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import kotlinx.datetime.Clock import link.socket.ampere.agents.domain.Urgency import link.socket.ampere.agents.domain.event.Event @@ -58,7 +58,7 @@ class AgentEventApiTest { @Test fun `agent can publish and subscribe to TaskCreated`() { - runBlocking { + runTest(scope.testScheduler) { val api = agentEventApiFactory.create(stubAgentId) val received = CompletableDeferred() @@ -84,7 +84,7 @@ class AgentEventApiTest { @Test fun `multiple subscribers receive same event`() { - runBlocking { + runTest(scope.testScheduler) { val api1 = agentEventApiFactory.create(stubAgentId) val api2 = agentEventApiFactory.create(stubAgentId2) @@ -107,7 +107,7 @@ class AgentEventApiTest { @Test fun `events persist and can be queried historically`() { - runBlocking { + runTest(scope.testScheduler) { val api = agentEventApiFactory.create(stubAgentId) val since = Clock.System.now() @@ -128,7 +128,7 @@ class AgentEventApiTest { @Test fun `first task completion for new task type publishes FIRST_SUCCESS milestone`() { - runBlocking { + runTest(scope.testScheduler) { val api = agentEventApiFactory.create(stubAgentId) val received = CompletableDeferred() @@ -153,7 +153,7 @@ class AgentEventApiTest { @Test fun `second completion for same task type does not publish another FIRST_SUCCESS milestone`() { - runBlocking { + runTest(scope.testScheduler) { val api = agentEventApiFactory.create(stubAgentId) val milestones = mutableListOf() @@ -181,7 +181,7 @@ class AgentEventApiTest { @Test fun `task failure followed by successful retry publishes RECOVERY milestone`() { - runBlocking { + runTest(scope.testScheduler) { val api = agentEventApiFactory.create(stubAgentId) val received = CompletableDeferred() @@ -212,7 +212,7 @@ class AgentEventApiTest { @Test fun `successful task without prior failure does not publish RECOVERY milestone`() { - runBlocking { + runTest(scope.testScheduler) { val api = agentEventApiFactory.create(stubAgentId) val milestones = mutableListOf() @@ -236,7 +236,7 @@ class AgentEventApiTest { @Test fun `explicit reachMilestone API publishes milestone`() { - runBlocking { + runTest(scope.testScheduler) { val api = agentEventApiFactory.create(stubAgentId) val received = CompletableDeferred() @@ -263,7 +263,7 @@ class AgentEventApiTest { @Test fun `multiple AgentEventApi instances can coexist and observe their own agentId's events`() { - runBlocking { + runTest(scope.testScheduler) { val api1 = agentEventApiFactory.create(stubAgentId) val receivedA = CompletableDeferred() @@ -298,7 +298,7 @@ class AgentEventApiTest { @Test fun `code submitted event can be published and subscribed`() { - runBlocking { + runTest(scope.testScheduler) { val api = agentEventApiFactory.create(stubAgentId) val received = CompletableDeferred() diff --git a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/AgentEventBusIntegrationTest.kt b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/AgentEventBusIntegrationTest.kt index 834a0969..072e51d8 100644 --- a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/AgentEventBusIntegrationTest.kt +++ b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/AgentEventBusIntegrationTest.kt @@ -8,9 +8,9 @@ import kotlin.test.assertEquals import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import link.socket.ampere.agents.domain.Urgency import link.socket.ampere.agents.domain.event.Event import link.socket.ampere.agents.events.api.AgentEventApiFactory @@ -49,7 +49,7 @@ class AgentEventBusIntegrationTest { @Test fun `complete agent communication flow`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // Setup: Three agents with one shared event bus val api1 = agentEventApiFactory.create("code-writer") val api2 = agentEventApiFactory.create("code-reviewer") diff --git a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/EventBusIntegrationTest.kt b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/EventBusIntegrationTest.kt index 77a4603f..2875e256 100644 --- a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/EventBusIntegrationTest.kt +++ b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/EventBusIntegrationTest.kt @@ -15,10 +15,10 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.test.runTest import kotlinx.datetime.Clock import kotlinx.datetime.Instant import link.socket.ampere.agents.domain.Urgency @@ -80,7 +80,7 @@ class EventBusIntegrationTest { @Test fun `events persist and can be replayed`() { - runBlocking { + runTest(scope.testScheduler) { val dir = createTempDirectory(prefix = "events-bus-db") val dbFile = dir / "bus.sqlite" @@ -137,7 +137,7 @@ class EventBusIntegrationTest { @Test fun `publish failures do not crash bus`() { - runBlocking { + runTest(scope.testScheduler) { val bus = eventSerialBusFactory.create() var goodHandlerCalled = false @@ -163,7 +163,7 @@ class EventBusIntegrationTest { @Test fun `concurrent publishing is safe and persists all events`() { - runBlocking { + runTest(scope.testScheduler) { val bus = eventSerialBusFactory.create() val n = 25 diff --git a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/EventBusLoggingAndErrorsTest.kt b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/EventBusLoggingAndErrorsTest.kt index 6ddd857b..0177fdfa 100644 --- a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/EventBusLoggingAndErrorsTest.kt +++ b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/EventBusLoggingAndErrorsTest.kt @@ -8,9 +8,9 @@ import kotlin.test.assertEquals import kotlin.test.assertNull import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import kotlinx.datetime.Clock import link.socket.ampere.agents.domain.Urgency import link.socket.ampere.agents.domain.event.Event @@ -88,7 +88,7 @@ class EventBusLoggingAndErrorsTest { @Test fun `subscriber exceptions do not affect others and are logged`() { - runBlocking { + runTest(scope.testScheduler) { val logger = TestLogger() val repo = EventRepository(json, scope, db) val bus = EventSerialBus(scope, logger) @@ -119,7 +119,7 @@ class EventBusLoggingAndErrorsTest { @Test fun `database write failures are logged but do not crash`() { - runBlocking { + runTest(scope.testScheduler) { val logger = TestLogger() val repo = EventRepository(json, scope, db) val bus = EventSerialBus(scope, logger) @@ -150,7 +150,7 @@ class EventBusLoggingAndErrorsTest { @Test fun `malformed JSON in database is handled gracefully`() { - runBlocking { + runTest(scope.testScheduler) { val logger = TestLogger() val repo = EventRepository(json, scope, db) val bus = EventSerialBus(scope, logger) diff --git a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/EventBusTest.kt b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/EventBusTest.kt index 69f9ce5a..9490f6d6 100644 --- a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/EventBusTest.kt +++ b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/EventBusTest.kt @@ -8,10 +8,10 @@ import kotlin.test.assertEquals import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.test.runTest import kotlinx.datetime.Clock import link.socket.ampere.agents.domain.Urgency import link.socket.ampere.agents.domain.event.Event @@ -64,7 +64,7 @@ class EventBusTest { @Test fun `subscriber receives only matching events`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val bus = EventSerialBus(scope) val receivedTask = CompletableDeferred() var nonMatchingCalled: Boolean @@ -95,7 +95,7 @@ class EventBusTest { @Test fun `multiple subscribers receive event`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val bus = EventSerialBus(scope) val s1 = CompletableDeferred() val s2 = CompletableDeferred() @@ -123,7 +123,7 @@ class EventBusTest { @Test fun `unsubscribe prevents further delivery`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { var count = 0 val bus = EventSerialBus(scope) diff --git a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/EventRouterTest.kt b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/EventRouterTest.kt index 64a6332e..5c9e5298 100644 --- a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/EventRouterTest.kt +++ b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/EventRouterTest.kt @@ -8,9 +8,9 @@ import kotlin.test.assertEquals import kotlin.test.assertIs import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import link.socket.ampere.agents.domain.Urgency import link.socket.ampere.agents.domain.event.Event import link.socket.ampere.agents.domain.event.EventSource @@ -49,7 +49,7 @@ class EventRouterTest { } @Test - fun `routes TaskCreated to subscribed agents as NotificationEvent`() = runBlocking { + fun `routes TaskCreated to subscribed agents as NotificationEvent`() = runTest(scope.testScheduler) { val routerApi = agentEventApiFactory.create("router-agent") val router = EventRouter(routerApi, eventSerialBus) diff --git a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/meetings/MeetingOrchestratorTest.kt b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/meetings/MeetingOrchestratorTest.kt index e0c56033..80a7d1ff 100644 --- a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/meetings/MeetingOrchestratorTest.kt +++ b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/meetings/MeetingOrchestratorTest.kt @@ -12,7 +12,8 @@ import kotlin.time.Duration.Companion.hours import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlinx.serialization.json.Json @@ -149,7 +150,7 @@ class MeetingOrchestratorTest { @Test fun `scheduleMeeting creates meeting and publishes event`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val meeting = createTestMeeting() val result = orchestrator.scheduleMeeting(meeting, stubScheduledBy) @@ -176,7 +177,7 @@ class MeetingOrchestratorTest { @Test fun `scheduleMeeting fails for past scheduled time`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val pastTime = Clock.System.now() - 1.hours val meeting = createTestMeeting(scheduledFor = pastTime) @@ -191,7 +192,7 @@ class MeetingOrchestratorTest { @Test fun `scheduleMeeting fails for meeting with no participants`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val meeting = createTestMeeting(requiredParticipants = emptyList()) val result = orchestrator.scheduleMeeting(meeting, stubScheduledBy) @@ -205,7 +206,7 @@ class MeetingOrchestratorTest { @Test fun `scheduleMeeting fails for non-scheduled status`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val meeting = Meeting( id = randomUUID(), type = MeetingType.AdHoc("Test"), @@ -227,7 +228,7 @@ class MeetingOrchestratorTest { @Test fun `startMeeting transitions meeting to in progress`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // First schedule a meeting val meeting = createTestMeeting() orchestrator.scheduleMeeting(meeting, stubScheduledBy) @@ -255,7 +256,7 @@ class MeetingOrchestratorTest { @Test fun `startMeeting fails for non-existent meeting`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val result = orchestrator.startMeeting("non-existent-id") assertTrue(result.isFailure) @@ -267,7 +268,7 @@ class MeetingOrchestratorTest { @Test fun `startMeeting fails for meeting not in scheduled status`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // Create and start a meeting val meeting = createTestMeeting() orchestrator.scheduleMeeting(meeting, stubScheduledBy) @@ -287,7 +288,7 @@ class MeetingOrchestratorTest { @Test fun `advanceAgenda returns next pending agenda item`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // Schedule and start meeting val agendaItems = listOf( AgendaItem( @@ -328,7 +329,7 @@ class MeetingOrchestratorTest { @Test fun `advanceAgenda returns null when all items complete`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // Create meeting with no agenda items val meeting = createTestMeeting(agendaItems = emptyList()) orchestrator.scheduleMeeting(meeting, stubScheduledBy) @@ -343,7 +344,7 @@ class MeetingOrchestratorTest { @Test fun `advanceAgenda fails for meeting not in progress`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val meeting = createTestMeeting() orchestrator.scheduleMeeting(meeting, stubScheduledBy) // Don't start the meeting @@ -361,7 +362,7 @@ class MeetingOrchestratorTest { @Test fun `completeMeeting transitions meeting to completed with outcomes`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // Schedule and start meeting val meeting = createTestMeeting() orchestrator.scheduleMeeting(meeting, stubScheduledBy) @@ -406,7 +407,7 @@ class MeetingOrchestratorTest { @Test fun `completeMeeting works with empty outcomes`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val meeting = createTestMeeting() orchestrator.scheduleMeeting(meeting, stubScheduledBy) orchestrator.startMeeting(meeting.id) @@ -423,7 +424,7 @@ class MeetingOrchestratorTest { @Test fun `completeMeeting fails for meeting not in progress`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val meeting = createTestMeeting() orchestrator.scheduleMeeting(meeting, stubScheduledBy) // Don't start the meeting @@ -439,7 +440,7 @@ class MeetingOrchestratorTest { @Test fun `completeMeeting fails for non-existent meeting`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val result = orchestrator.completeMeeting("non-existent-id", emptyList()) assertTrue(result.isFailure) @@ -450,7 +451,7 @@ class MeetingOrchestratorTest { @Test fun `full meeting lifecycle from schedule to complete`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // Create meeting val agendaItems = listOf( AgendaItem( @@ -520,7 +521,7 @@ class MeetingOrchestratorTest { @Test fun `state transitions are validated correctly`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val meeting = createTestMeeting() orchestrator.scheduleMeeting(meeting, stubScheduledBy) diff --git a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/messages/AgentMessageApiTest.kt b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/messages/AgentMessageApiTest.kt index dd352f6c..77d5e923 100644 --- a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/messages/AgentMessageApiTest.kt +++ b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/messages/AgentMessageApiTest.kt @@ -9,9 +9,9 @@ import kotlin.test.assertNotNull import kotlin.test.assertTrue import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import link.socket.ampere.agents.domain.event.MessageEvent import link.socket.ampere.agents.domain.status.EventStatus import link.socket.ampere.agents.events.EventRepository @@ -54,7 +54,7 @@ class AgentMessageApiTest { @Test fun `create thread, escalate status, then resolve`() { - runBlocking { + runTest(scope.testScheduler) { val api = agentMessageApiFactory.create(stubAgentId) val received = mutableListOf() @@ -145,7 +145,7 @@ class AgentMessageApiTest { @Test fun `reopen thread allows posting after escalation`() { - runBlocking { + runTest(scope.testScheduler) { val api = agentMessageApiFactory.create(stubAgentId) val received = mutableListOf() @@ -213,7 +213,7 @@ class AgentMessageApiTest { @Test fun `discretionary escalateToHuman still publishes EscalationRequested`() { - runBlocking { + runTest(scope.testScheduler) { val api = agentMessageApiFactory.create(stubAgentId) val received = mutableListOf() @@ -245,7 +245,7 @@ class AgentMessageApiTest { @Test fun `reopen thread fails when not in WAITING_FOR_HUMAN state`() { - runBlocking { + runTest(scope.testScheduler) { val api = agentMessageApiFactory.create(stubAgentId) // Create thread (starts in OPEN state) diff --git a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/messages/MessageRouterTest.kt b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/messages/MessageRouterTest.kt index e1a4029b..6a593d65 100644 --- a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/messages/MessageRouterTest.kt +++ b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/messages/MessageRouterTest.kt @@ -7,9 +7,9 @@ import kotlin.test.Test import kotlin.test.assertTrue import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import link.socket.ampere.agents.definition.AgentId import link.socket.ampere.agents.domain.event.EventSource import link.socket.ampere.agents.domain.event.MessageEvent @@ -72,7 +72,7 @@ class MessageRouterTest { @Test fun `routes thread and channel events to subscribed agents`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val routerApi = agentMessageApiFactory.create("router-agent") val router = MessageRouter(routerApi, escalationEventHandler, eventSerialBus) diff --git a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/messages/escalation/EscalationEventHandlerTest.kt b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/messages/escalation/EscalationEventHandlerTest.kt index 1a38f148..988101e9 100644 --- a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/messages/escalation/EscalationEventHandlerTest.kt +++ b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/messages/escalation/EscalationEventHandlerTest.kt @@ -9,9 +9,9 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import link.socket.ampere.agents.domain.event.MessageEvent import link.socket.ampere.agents.events.EventRepository import link.socket.ampere.agents.events.bus.EventSerialBus @@ -95,7 +95,7 @@ class EscalationEventHandlerTest { @Test fun `notifier reacts to escalation event and forwards to human`() { - runBlocking { + runTest(scope.testScheduler) { val agentId = "notifier-agent" val api: AgentMessageApi = apiFactory.create(agentId) val messageRouter = getMessageRouter(api) @@ -135,7 +135,7 @@ class EscalationEventHandlerTest { @Test fun `no thread found leads to no human notification`() { - runBlocking { + runTest(scope.testScheduler) { val agentId = "notifier-agent" val api: AgentMessageApi = apiFactory.create(agentId) val messageRouter = getMessageRouter(api) @@ -164,7 +164,7 @@ class EscalationEventHandlerTest { @Test fun `escalation handler with start method self-subscribes to events`() { - runBlocking { + runTest(scope.testScheduler) { val agentId = "standalone-agent" val api: AgentMessageApi = apiFactory.create(agentId) diff --git a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/relay/EventStreamServiceTest.kt b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/relay/EventStreamServiceTest.kt index 143c14a4..d5c29b0a 100644 --- a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/relay/EventStreamServiceTest.kt +++ b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/relay/EventStreamServiceTest.kt @@ -12,9 +12,9 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import kotlinx.datetime.Clock import kotlinx.datetime.Instant import link.socket.ampere.agents.domain.Urgency @@ -86,7 +86,7 @@ class EventStreamServiceTest { @Test fun `subscribeToLiveEvents emits events published to EventBus`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val events = mutableListOf() // Start collecting events @@ -114,7 +114,7 @@ class EventStreamServiceTest { @Test fun `subscribeToLiveEvents with event type filter only emits matching events`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val events = mutableListOf() val filter = EventRelayFilters( eventTypes = setOf(Event.TaskCreated.EVENT_TYPE), @@ -142,7 +142,7 @@ class EventStreamServiceTest { @Test fun `subscribeToLiveEvents with source filter only emits matching events`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val events = mutableListOf() val filter = EventRelayFilters(eventSources = setOf(stubSourceA)) @@ -167,7 +167,7 @@ class EventStreamServiceTest { @Test fun `subscribeToLiveEvents with urgency filter only emits matching events`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val events = mutableListOf() val filter = EventRelayFilters(urgencies = setOf(Urgency.HIGH)) @@ -191,7 +191,7 @@ class EventStreamServiceTest { @Test fun `replayEvents returns events within time range`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val now = Clock.System.now() val past = now - 1000.milliseconds val future = now + 1000.milliseconds @@ -221,7 +221,7 @@ class EventStreamServiceTest { @Test fun `replayEvents returns empty flow when no events in range`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val now = Clock.System.now() val past = now - 2000.milliseconds val wayPast = now - 3000.milliseconds @@ -243,7 +243,7 @@ class EventStreamServiceTest { @Test fun `replayEvents with filter only returns matching events`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val now = Clock.System.now() // Save events of different types @@ -273,7 +273,7 @@ class EventStreamServiceTest { @Test fun `replayEvents returns events in chronological order`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val now = Clock.System.now() val t1 = now - 500.milliseconds val t2 = now @@ -305,7 +305,7 @@ class EventStreamServiceTest { @Test fun `replayEvents with combined filters uses AND logic`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val now = Clock.System.now() // Save various events diff --git a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/tickets/TicketMeetingIntegrationTest.kt b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/tickets/TicketMeetingIntegrationTest.kt index 43c1f2f2..b4785545 100644 --- a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/tickets/TicketMeetingIntegrationTest.kt +++ b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/tickets/TicketMeetingIntegrationTest.kt @@ -11,7 +11,8 @@ import kotlin.time.Duration.Companion.hours import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.datetime.Clock import link.socket.ampere.agents.definition.AgentId import link.socket.ampere.agents.domain.event.Event @@ -120,7 +121,7 @@ class TicketMeetingIntegrationTest { @Test fun `scheduleTicketMeeting creates meeting and links to ticket`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // Create a ticket first val createResult = ticketOrchestrator.createTicket( title = "Meeting Test Ticket", @@ -187,7 +188,7 @@ class TicketMeetingIntegrationTest { @Test fun `blockTicket with decision keyword automatically schedules meeting`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // Create a ticket and assign it val createTicketResult = ticketOrchestrator.createTicket( title = "Auto Meeting Test", @@ -248,7 +249,7 @@ class TicketMeetingIntegrationTest { @Test fun `blockTicket with human keyword includes human in meeting participants`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // Create a ticket and assign it val createTicketResult = ticketOrchestrator.createTicket( title = "Human Approval Test", @@ -299,7 +300,7 @@ class TicketMeetingIntegrationTest { @Test fun `blockTicket without decision keywords does not schedule meeting`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // Create a ticket and assign it val createTicketResult = ticketOrchestrator.createTicket( title = "No Meeting Test", @@ -356,7 +357,7 @@ class TicketMeetingIntegrationTest { @Test fun `getMeetingsForTicket returns all associated meetings`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // Create a ticket val createTicketResult = ticketOrchestrator.createTicket( title = "Multiple Meetings Test", @@ -406,7 +407,7 @@ class TicketMeetingIntegrationTest { @Test fun `getTicketsForMeeting returns associated ticket`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // Create a ticket val createTicketResult = ticketOrchestrator.createTicket( title = "Reverse Lookup Test", diff --git a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/tickets/TicketOrchestratorTest.kt b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/tickets/TicketOrchestratorTest.kt index ffacda13..5e1268a6 100644 --- a/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/tickets/TicketOrchestratorTest.kt +++ b/ampere-core/src/jvmTest/kotlin/link/socket/ampere/agents/events/tickets/TicketOrchestratorTest.kt @@ -11,7 +11,8 @@ import kotlin.test.assertTrue import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.UnconfinedTestDispatcher import link.socket.ampere.agents.definition.AgentId import link.socket.ampere.agents.domain.Urgency import link.socket.ampere.agents.domain.event.Event @@ -123,7 +124,7 @@ class TicketOrchestratorTest { @Test fun `createTicket creates ticket and thread and publishes event`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val result = ticketOrchestrator.createTicket( title = "Test Ticket", description = "Test description", @@ -166,7 +167,7 @@ class TicketOrchestratorTest { @Test fun `createTicket fails with blank title`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val result = ticketOrchestrator.createTicket( title = "", description = "Test description", @@ -184,7 +185,7 @@ class TicketOrchestratorTest { @Test fun `createTicket sets correct urgency based on priority`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // Create a CRITICAL priority ticket ticketOrchestrator.createTicket( title = "Critical Ticket", @@ -206,7 +207,7 @@ class TicketOrchestratorTest { @Test fun `transitionTicketStatus with valid transition publishes event and updates thread`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // Create a ticket first val createResult = ticketOrchestrator.createTicket( title = "Transition Test", @@ -243,7 +244,7 @@ class TicketOrchestratorTest { @Test fun `transitionTicketStatus fails with invalid state transition`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // Create a ticket val createResult = ticketOrchestrator.createTicket( title = "Invalid Transition", @@ -269,7 +270,7 @@ class TicketOrchestratorTest { @Test fun `transitionTicketStatus fails for non-existent ticket`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { val result = ticketOrchestrator.transitionTicketStatus( ticketId = "non-existent-id", newStatus = TicketStatus.Ready, @@ -284,7 +285,7 @@ class TicketOrchestratorTest { @Test fun `transitionTicketStatus rejects unauthorized agent`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // Create a ticket val createResult = ticketOrchestrator.createTicket( title = "Unauthorized Test", @@ -311,7 +312,7 @@ class TicketOrchestratorTest { @Test fun `transitionTicketStatus allows assigned agent to modify`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // Create and assign a ticket val createResult = ticketOrchestrator.createTicket( title = "Assigned Agent Test", @@ -345,7 +346,7 @@ class TicketOrchestratorTest { @Test fun `assignTicket assigns ticket and publishes event`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // Create a ticket val createResult = ticketOrchestrator.createTicket( title = "Assignment Test", @@ -382,7 +383,7 @@ class TicketOrchestratorTest { @Test fun `assignTicket rejects assignment by unauthorized agent`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // Create a ticket val createResult = ticketOrchestrator.createTicket( title = "Unauthorized Assignment", @@ -409,7 +410,7 @@ class TicketOrchestratorTest { @Test fun `assignTicket allows unassignment`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // Create and assign a ticket val createResult = ticketOrchestrator.createTicket( title = "Unassignment Test", @@ -450,7 +451,7 @@ class TicketOrchestratorTest { @Test fun `blockTicket creates thread message requesting human intervention`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // Create a ticket and transition to IN_PROGRESS val createResult = ticketOrchestrator.createTicket( title = "Block Test", @@ -493,7 +494,7 @@ class TicketOrchestratorTest { @Test fun `blockTicket fails for invalid state transition`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // Create a ticket in BACKLOG status val createResult = ticketOrchestrator.createTicket( title = "Invalid Block", @@ -522,7 +523,7 @@ class TicketOrchestratorTest { @Test fun `full ticket lifecycle from creation to completion`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // Create ticket val createResult = ticketOrchestrator.createTicket( title = "Full Lifecycle Test", @@ -568,7 +569,7 @@ class TicketOrchestratorTest { @Test fun `ticket lifecycle with blocking and unblocking`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // Create and progress ticket to IN_PROGRESS val createResult = ticketOrchestrator.createTicket( title = "Block Lifecycle", @@ -607,7 +608,7 @@ class TicketOrchestratorTest { @Test fun `multiple events are published for complex workflows`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { publishedEvents.clear() // Create ticket @@ -643,7 +644,7 @@ class TicketOrchestratorTest { @Test fun `permission validation works correctly across different agents`() { - runBlocking { + runTest(UnconfinedTestDispatcher()) { // Create ticket as creator val createResult = ticketOrchestrator.createTicket( title = "Permission Test",