agentsclimarketplace

Simba testing

Skill Ahoo-Wang/skills/plugins/ahoo-simba-skills/skills/simba-testing

Guide for testing Simba distributed lock and leader-election code. Use when writing or reviewing tests for MutexContender, SimbaLocker, AbstractScheduler, backend TCK conformance via MutexContendServiceSpec, Redis/JDBC/Zookeeper integration tests, timing-sensitive lock behavior, or new Kotlin assertions in Simba-based code.From its SKILL.md

Install
npx -y skills add Ahoo-Wang/skills --skill simba-testing

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

7.0 KB, ~1.5k tokens by cl100k_base, as published. Nobody here has run it

Testing Simba-Based Code

Test Strategy Overview

Simba testing has three layers:

  1. Unit tests — mock the MutexContendServiceFactory, test your business logic in isolation
  2. TCK (Technology Compatibility Kit) — extend MutexContendServiceSpec to verify a backend implementation
  3. Integration tests — run against a real backend (Redis, MySQL, Zookeeper)

Choose the simplest layer that gives confidence. Most application code only needs unit tests with mocks. Backend implementors need TCK + integration tests.

Before writing a test, decide:

  • Application behavior: mock MutexContendServiceFactory, capture the contender, and trigger callbacks directly.
  • Backend implementation: extend MutexContendServiceSpec and run against the real backend.
  • Scheduler behavior: verify leadership gating separately from the business logic in work().

Unit Tests with MockK

For application code that injects MutexContendServiceFactory, mock it:

import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import me.ahoo.simba.core.MutexContendService
import me.ahoo.simba.core.MutexContendServiceFactory
import me.ahoo.simba.core.MutexContender
import me.ahoo.simba.core.MutexOwner
import me.ahoo.simba.core.MutexState

class MyServiceTest {
    private val mockFactory = mockk<MutexContendServiceFactory>()
    private val mockService = mockk<MutexContendService>(relaxed = true)

    @BeforeEach
    fun setup() {
        every { mockFactory.createMutexContendService(any()) } returns mockService
    }

    @Test
    fun `should start contend service`() {
        val contender = MyContender()
        val service = mockFactory.createMutexContendService(contender)
        service.start()

        verify { service.start() }
    }
}

Simulating Leadership Changes

To test code that reacts to onAcquired/onReleased, capture the contender and invoke callbacks directly:

@Test
fun `should react to leadership change`() {
    val contenderSlot = slot<MutexContender>()
    every { mockFactory.createMutexContendService(capture(contenderSlot)) } returns mockService

    // Create your service/component that uses Simba
    val myComponent = MyComponent(mockFactory)
    myComponent.start()

    // Simulate acquiring leadership
    val mutexState = MutexState(MutexOwner.NONE, MutexOwner("test-contender"))
    contenderSlot.captured.onAcquired(mutexState)

    // Assert your component's behavior
    myComponent.isLeader.assert().isTrue()

    // Simulate losing leadership
    val releasedState = MutexState(MutexOwner("test-contender"), MutexOwner.NONE)
    contenderSlot.captured.onReleased(releasedState)

    myComponent.isLeader.assert().isFalse()
}

SimbaLocker Unit Tests

Mock the factory and verify the locker lifecycle:

import me.ahoo.simba.locker.SimbaLocker

@Test
fun `locker should acquire and release`() {
    val mockService = mockk<MutexContendService>(relaxed = true)
    every { mockFactory.createMutexContendService(any()) } returns mockService

    val locker = SimbaLocker("test-lock", mockFactory)
    // acquire() will block, so in unit tests we typically don't call it directly
    // Instead test the code that uses the locker
    locker.close()

    verify { mockService.stop() }
}

TCK — Extending MutexContendServiceSpec

When implementing a new Simba backend, extend the TCK to verify correctness:

import me.ahoo.simba.test.MutexContendServiceSpec

class MyBackendMutexContendServiceTest : MutexContendServiceSpec() {
    override val mutexContendServiceFactory: MutexContendServiceFactory =
        MyBackendMutexContendServiceFactory(/* dependencies */)
}

This gives you five standard tests:

  1. start() — acquire, verify owner, stop, verify released
  2. restart() — stop and restart, verify full lifecycle repeats
  3. guard() — acquire, wait 3s, verify owner hasn't changed (TTL renewal works)
  4. multiContend() — 10 contenders compete, exactly one owner at any time
  5. schedule() — AbstractScheduler lifecycle, work executes on leader

Backend-Specific Test Requirements

BackendExternal dependencyNotes
RedisRunning Redis instanceCurrent repository tests use RedisStandaloneConfiguration defaults
JDBCRunning MySQL instanceCurrent repository tests use jdbc:mysql://localhost:3306/simba_db, root/root; init script: simba-jdbc/src/init-script/init-simba-mysql.sql
ZookeeperNoneUses Curator's TestingServer (embedded)

Do not silently add Testcontainers to this repository's tests. If CI isolation is required, add the dependency and Gradle wiring intentionally, then update the backend setup code and this skill together.

AbstractScheduler Tests

Test that scheduled work runs only on the leader:

import me.ahoo.simba.schedule.AbstractScheduler
import me.ahoo.simba.schedule.ScheduleConfig

@Test
fun `scheduler should run work only when leader`() {
    val workLatch = CountDownLatch(1)
    val scheduler = object : AbstractScheduler("test-scheduler", mockFactory) {
        override val config = ScheduleConfig.delay(Duration.ZERO, Duration.ofMillis(100))
        override val worker = "test"
        override fun work() {
            workLatch.countDown()
        }
    }

    scheduler.start()
    // Simulate acquiring leadership by triggering the contender
    // ...

    workLatch.await(5, TimeUnit.SECONDS).assert().isTrue()
    scheduler.stop()
}

Assertion Style

Use fluent-assert for new Kotlin assertions:

import me.ahoo.test.asserts.assert

value.assert().isEqualTo(expected)
collection.assert().hasSize(3)
bool.assert().isTrue()

Do not churn existing Hamcrest/AssertJ assertions solely for style. When adding or touching assertions, prefer .assert() and keep the local test readable.

Common Test Pitfalls

  1. Timing-dependent tests: Distributed lock tests are inherently timing-sensitive. Use generous timeouts (5-30s) and prefer CountDownLatch / CompletableFuture over new Thread.sleep calls.
  2. Shared mutex names: Each test should use a unique mutex name to avoid cross-test interference. Use "test-mutex-${UUID.randomUUID()}".
  3. Resource cleanup: Always stop/close services in @AfterEach to avoid leaked threads and held locks.
  4. Mocking MutexContendService vs MutexContendServiceFactory: Mock the factory (the DI seam), not the service directly. The factory is what application code injects.
  5. Testing AbstractScheduler without Simba: If you only need to test the work() method, call it directly. The scheduler pattern is about leadership gating, not the work itself.

What ships with it: 1 file

182 B alongside SKILL.md

agents/

Keep looking

Skills are one crate of 326,835. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.