Files
oh-my-openagent/src/features/background-agent/concurrency.test.ts
YeonGyu-Kim 8c2625cfb0 🏆 test: optimize test suite with FakeTimers and race condition fixes (#1284)
* fix: exclude prompt/permission from plan agent config

plan agent should only inherit model settings from prometheus,
not the prompt or permission. This ensures plan agent uses
OpenCode's default behavior while only overriding the model.

* test(todo-continuation-enforcer): use FakeTimers for 15x faster tests

- Add custom FakeTimers implementation (~100 lines)
- Replace all real setTimeout waits with fakeTimers.advanceBy()
- Test time: 104.6s → 7.01s

* test(callback-server): fix race conditions with Promise.all and Bun.fetch

- Use Bun.fetch.bind(Bun) to avoid globalThis.fetch mock interference
- Use Promise.all pattern for concurrent fetch/waitForCallback
- Add Bun.sleep(10) in afterEach for port release

* test(concurrency): replace placeholder assertions with getCount checks

Replace 6 meaningless expect(true).toBe(true) assertions with
actual getCount() verifications for test quality improvement

* refactor(config-handler): simplify planDemoteConfig creation

Remove unnecessary IIFE and destructuring, use direct spread instead

* test(executor): use FakeTimeouts for faster tests

- Add custom FakeTimeouts implementation
- Replace setTimeout waits with fakeTimeouts.advanceBy()
- Test time reduced from ~26s to ~6.8s

* test: fix gemini model mock for artistry unstable mode

* test: fix model list mock payload shape

* test: mock provider models for artistry category

---------

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
2026-01-30 22:10:52 +09:00

419 lines
12 KiB
TypeScript

import { describe, test, expect, beforeEach } from "bun:test"
import { ConcurrencyManager } from "./concurrency"
import type { BackgroundTaskConfig } from "../../config/schema"
describe("ConcurrencyManager.getConcurrencyLimit", () => {
test("should return model-specific limit when modelConcurrency is set", () => {
// #given
const config: BackgroundTaskConfig = {
modelConcurrency: { "anthropic/claude-sonnet-4-5": 5 }
}
const manager = new ConcurrencyManager(config)
// #when
const limit = manager.getConcurrencyLimit("anthropic/claude-sonnet-4-5")
// #then
expect(limit).toBe(5)
})
test("should return provider limit when providerConcurrency is set for model provider", () => {
// #given
const config: BackgroundTaskConfig = {
providerConcurrency: { anthropic: 3 }
}
const manager = new ConcurrencyManager(config)
// #when
const limit = manager.getConcurrencyLimit("anthropic/claude-sonnet-4-5")
// #then
expect(limit).toBe(3)
})
test("should return provider limit even when modelConcurrency exists but doesn't match", () => {
// #given
const config: BackgroundTaskConfig = {
modelConcurrency: { "google/gemini-3-pro": 5 },
providerConcurrency: { anthropic: 3 }
}
const manager = new ConcurrencyManager(config)
// #when
const limit = manager.getConcurrencyLimit("anthropic/claude-sonnet-4-5")
// #then
expect(limit).toBe(3)
})
test("should return default limit when defaultConcurrency is set", () => {
// #given
const config: BackgroundTaskConfig = {
defaultConcurrency: 2
}
const manager = new ConcurrencyManager(config)
// #when
const limit = manager.getConcurrencyLimit("anthropic/claude-sonnet-4-5")
// #then
expect(limit).toBe(2)
})
test("should return default 5 when no config provided", () => {
// #given
const manager = new ConcurrencyManager()
// #when
const limit = manager.getConcurrencyLimit("anthropic/claude-sonnet-4-5")
// #then
expect(limit).toBe(5)
})
test("should return default 5 when config exists but no concurrency settings", () => {
// #given
const config: BackgroundTaskConfig = {}
const manager = new ConcurrencyManager(config)
// #when
const limit = manager.getConcurrencyLimit("anthropic/claude-sonnet-4-5")
// #then
expect(limit).toBe(5)
})
test("should prioritize model-specific over provider-specific over default", () => {
// #given
const config: BackgroundTaskConfig = {
modelConcurrency: { "anthropic/claude-sonnet-4-5": 10 },
providerConcurrency: { anthropic: 5 },
defaultConcurrency: 2
}
const manager = new ConcurrencyManager(config)
// #when
const modelLimit = manager.getConcurrencyLimit("anthropic/claude-sonnet-4-5")
const providerLimit = manager.getConcurrencyLimit("anthropic/claude-opus-4-5")
const defaultLimit = manager.getConcurrencyLimit("google/gemini-3-pro")
// #then
expect(modelLimit).toBe(10)
expect(providerLimit).toBe(5)
expect(defaultLimit).toBe(2)
})
test("should handle models without provider part", () => {
// #given
const config: BackgroundTaskConfig = {
providerConcurrency: { "custom-model": 4 }
}
const manager = new ConcurrencyManager(config)
// #when
const limit = manager.getConcurrencyLimit("custom-model")
// #then
expect(limit).toBe(4)
})
test("should return Infinity when defaultConcurrency is 0", () => {
// #given
const config: BackgroundTaskConfig = { defaultConcurrency: 0 }
const manager = new ConcurrencyManager(config)
// #when
const limit = manager.getConcurrencyLimit("any-model")
// #then
expect(limit).toBe(Infinity)
})
test("should return Infinity when providerConcurrency is 0", () => {
// #given
const config: BackgroundTaskConfig = {
providerConcurrency: { anthropic: 0 }
}
const manager = new ConcurrencyManager(config)
// #when
const limit = manager.getConcurrencyLimit("anthropic/claude-sonnet-4-5")
// #then
expect(limit).toBe(Infinity)
})
test("should return Infinity when modelConcurrency is 0", () => {
// #given
const config: BackgroundTaskConfig = {
modelConcurrency: { "anthropic/claude-sonnet-4-5": 0 }
}
const manager = new ConcurrencyManager(config)
// #when
const limit = manager.getConcurrencyLimit("anthropic/claude-sonnet-4-5")
// #then
expect(limit).toBe(Infinity)
})
})
describe("ConcurrencyManager.acquire/release", () => {
let manager: ConcurrencyManager
beforeEach(() => {
// #given
const config: BackgroundTaskConfig = {}
manager = new ConcurrencyManager(config)
})
test("should allow acquiring up to limit", async () => {
// #given
const config: BackgroundTaskConfig = { defaultConcurrency: 2 }
manager = new ConcurrencyManager(config)
// #when
await manager.acquire("model-a")
await manager.acquire("model-a")
// #then - both resolved without waiting, count should be 2
expect(manager.getCount("model-a")).toBe(2)
})
test("should allow acquires up to default limit of 5", async () => {
// #given - no config = default limit of 5
// #when
await manager.acquire("model-a")
await manager.acquire("model-a")
await manager.acquire("model-a")
await manager.acquire("model-a")
await manager.acquire("model-a")
// #then - all 5 resolved, count should be 5
expect(manager.getCount("model-a")).toBe(5)
})
test("should queue when limit reached", async () => {
// #given
const config: BackgroundTaskConfig = { defaultConcurrency: 1 }
manager = new ConcurrencyManager(config)
await manager.acquire("model-a")
// #when
let resolved = false
const waitPromise = manager.acquire("model-a").then(() => { resolved = true })
// Give microtask queue a chance to run
await Promise.resolve()
// #then - should still be waiting
expect(resolved).toBe(false)
// #when - release
manager.release("model-a")
await waitPromise
// #then - now resolved
expect(resolved).toBe(true)
})
test("should queue multiple tasks and process in order", async () => {
// #given
const config: BackgroundTaskConfig = { defaultConcurrency: 1 }
manager = new ConcurrencyManager(config)
await manager.acquire("model-a")
// #when
const order: string[] = []
const task1 = manager.acquire("model-a").then(() => { order.push("1") })
const task2 = manager.acquire("model-a").then(() => { order.push("2") })
const task3 = manager.acquire("model-a").then(() => { order.push("3") })
// Give microtask queue a chance to run
await Promise.resolve()
// #then - none resolved yet
expect(order).toEqual([])
// #when - release one at a time
manager.release("model-a")
await task1
expect(order).toEqual(["1"])
manager.release("model-a")
await task2
expect(order).toEqual(["1", "2"])
manager.release("model-a")
await task3
expect(order).toEqual(["1", "2", "3"])
})
test("should handle independent models separately", async () => {
// #given
const config: BackgroundTaskConfig = { defaultConcurrency: 1 }
manager = new ConcurrencyManager(config)
await manager.acquire("model-a")
// #when - acquire different model
const resolved = await Promise.race([
manager.acquire("model-b").then(() => "resolved"),
Promise.resolve("timeout").then(() => "timeout")
])
// #then - different model should resolve immediately
expect(resolved).toBe("resolved")
})
test("should allow re-acquiring after release", async () => {
// #given
const config: BackgroundTaskConfig = { defaultConcurrency: 1 }
manager = new ConcurrencyManager(config)
// #when
await manager.acquire("model-a")
manager.release("model-a")
await manager.acquire("model-a")
// #then - count should be 1 after re-acquiring
expect(manager.getCount("model-a")).toBe(1)
})
test("should handle release when no acquire", () => {
// #given
const config: BackgroundTaskConfig = { defaultConcurrency: 2 }
manager = new ConcurrencyManager(config)
// #when - release without acquire
manager.release("model-a")
// #then - count should be 0 (no negative count)
expect(manager.getCount("model-a")).toBe(0)
})
test("should handle release when no prior acquire", () => {
// #given - default config
// #when - release without acquire
manager.release("model-a")
// #then - count should be 0 (no negative count)
expect(manager.getCount("model-a")).toBe(0)
})
test("should handle multiple acquires and releases correctly", async () => {
// #given
const config: BackgroundTaskConfig = { defaultConcurrency: 3 }
manager = new ConcurrencyManager(config)
// #when
await manager.acquire("model-a")
await manager.acquire("model-a")
await manager.acquire("model-a")
// Release all
manager.release("model-a")
manager.release("model-a")
manager.release("model-a")
// Should be able to acquire again
await manager.acquire("model-a")
// #then - count should be 1 after re-acquiring
expect(manager.getCount("model-a")).toBe(1)
})
test("should use model-specific limit for acquire", async () => {
// #given
const config: BackgroundTaskConfig = {
modelConcurrency: { "anthropic/claude-sonnet-4-5": 2 },
defaultConcurrency: 5
}
manager = new ConcurrencyManager(config)
await manager.acquire("anthropic/claude-sonnet-4-5")
await manager.acquire("anthropic/claude-sonnet-4-5")
// #when
let resolved = false
const waitPromise = manager.acquire("anthropic/claude-sonnet-4-5").then(() => { resolved = true })
// Give microtask queue a chance to run
await Promise.resolve()
// #then - should be waiting (model-specific limit is 2)
expect(resolved).toBe(false)
// Cleanup
manager.release("anthropic/claude-sonnet-4-5")
await waitPromise
})
})
describe("ConcurrencyManager.cleanup", () => {
test("cancelWaiters should reject all pending acquires", async () => {
// #given
const config: BackgroundTaskConfig = { defaultConcurrency: 1 }
const manager = new ConcurrencyManager(config)
await manager.acquire("model-a")
// Queue waiters
const errors: Error[] = []
const p1 = manager.acquire("model-a").catch(e => errors.push(e))
const p2 = manager.acquire("model-a").catch(e => errors.push(e))
// #when
manager.cancelWaiters("model-a")
await Promise.all([p1, p2])
// #then
expect(errors.length).toBe(2)
expect(errors[0].message).toContain("cancelled")
})
test("clear should cancel all models and reset state", async () => {
// #given
const config: BackgroundTaskConfig = { defaultConcurrency: 1 }
const manager = new ConcurrencyManager(config)
await manager.acquire("model-a")
await manager.acquire("model-b")
const errors: Error[] = []
const p1 = manager.acquire("model-a").catch(e => errors.push(e))
const p2 = manager.acquire("model-b").catch(e => errors.push(e))
// #when
manager.clear()
await Promise.all([p1, p2])
// #then
expect(errors.length).toBe(2)
expect(manager.getCount("model-a")).toBe(0)
expect(manager.getCount("model-b")).toBe(0)
})
test("getCount and getQueueLength should return correct values", async () => {
// #given
const config: BackgroundTaskConfig = { defaultConcurrency: 2 }
const manager = new ConcurrencyManager(config)
// #when
await manager.acquire("model-a")
expect(manager.getCount("model-a")).toBe(1)
expect(manager.getQueueLength("model-a")).toBe(0)
await manager.acquire("model-a")
expect(manager.getCount("model-a")).toBe(2)
// Queue one more
const p = manager.acquire("model-a").catch(() => {})
await Promise.resolve() // let it queue
expect(manager.getQueueLength("model-a")).toBe(1)
// Cleanup
manager.cancelWaiters("model-a")
await p
})
})