Files
Jarvis/packages/AgentCore/Tests/AgentCoreTests/BoundedAsyncChannelTests.swift
james.maes 0020c35fb6 feat(04-01): scaffold AgentCore package with LLMProvider protocol + LLMEvent + BoundedAsyncChannel (AGENT-01, AGENT-07, AGENT-10)
- packages/AgentCore SPM package with three library products: AgentCore, AnthropicProvider, OllamaProvider (latter two ship placeholders this wave)
- LLMProvider protocol — Sendable, AsyncThrowingStream<LLMEvent, Error>, mandatory toolChoice param
- LLMEvent enum: 10 cases (messageStart, textDelta, thinkingDelta, toolUseRequested, toolUseBuffering, partialToolUseAtDisconnect, stopReason, usage, providerError, messageStop)
- StopReason: 5 cases (endTurn, toolUse, maxTokens, refusal, streamTruncated)
- ToolChoice: 4 cases (auto, none, any, tool(name:))
- ModelID literals per RESEARCH-DELTAS D1 — opus47 = "claude-opus-4-7", qwen25coder32b = "qwen2.5-coder:32b"
- CacheHints / TurnID / TurnUsage / ToolSchema / LLMMessage / LLMProviderError supporting types
- BoundedAsyncChannel<T> actor with .suspend / .dropOldest / .dropNewest policies, AsyncSequence conformance, single-consumer iterator
- 21 AgentCoreTests passing (channel policies, exhaustive enum cases, equality, RawValue literals)
2026-04-24 17:13:40 -05:00

171 lines
5.5 KiB
Swift

import XCTest
@testable import AgentCore
final class BoundedAsyncChannelTests: XCTestCase {
// MARK: - Test 5: .suspend policy
/// `.suspend` capacity-1: first send succeeds without blocking, second
/// send suspends until consumer reads.
func testSuspendPolicyBlocksWhenFull() async {
let channel = BoundedAsyncChannel<Int>(capacity: 1, policy: .suspend)
// First send returns immediately.
await channel.send(1)
// Second send should suspend until the consumer drains slot 1.
let secondSendStarted = expectation(description: "second send started")
let secondSendCompleted = expectation(description: "second send completed")
let producer = Task {
secondSendStarted.fulfill()
await channel.send(2)
secondSendCompleted.fulfill()
}
await fulfillment(of: [secondSendStarted], timeout: 1.0)
// Give the producer a chance to actually call send().
try? await Task.sleep(nanoseconds: 50_000_000) // 50ms
// Consume one — this should unblock the second send.
var iterator = channel.makeAsyncIterator()
let first = await iterator.next()
XCTAssertEqual(first, 1)
let second = await iterator.next()
XCTAssertEqual(second, 2)
await fulfillment(of: [secondSendCompleted], timeout: 1.0)
producer.cancel()
}
// MARK: - Test 6: .dropOldest
/// Capacity-3, send 5 without consuming: iteration yields 3,4,5.
func testDropOldestPolicy() async {
let channel = BoundedAsyncChannel<Int>(capacity: 3, policy: .dropOldest)
for i in 1...5 {
await channel.send(i)
}
await channel.finish()
var collected: [Int] = []
for await item in channel {
collected.append(item)
}
XCTAssertEqual(collected, [3, 4, 5])
}
// MARK: - Test 7: .dropNewest
/// Capacity-3, send 5 without consuming: iteration yields 1,2,3.
func testDropNewestPolicy() async {
let channel = BoundedAsyncChannel<Int>(capacity: 3, policy: .dropNewest)
for i in 1...5 {
await channel.send(i)
}
await channel.finish()
var collected: [Int] = []
for await item in channel {
collected.append(item)
}
XCTAssertEqual(collected, [1, 2, 3])
}
// MARK: - Test 8: finish()
/// `finish()` terminates the AsyncSequence cleanly; consumer's
/// `for await` loop exits after draining the buffer.
func testFinishTerminatesIteration() async {
let channel = BoundedAsyncChannel<Int>(capacity: 4, policy: .suspend)
await channel.send(1)
await channel.send(2)
await channel.finish()
var collected: [Int] = []
for await item in channel {
collected.append(item)
}
XCTAssertEqual(collected, [1, 2])
}
/// finish() while a consumer is suspended waiting for an element resumes
/// the consumer with `nil`.
func testFinishWakesPendingReceiver() async {
let channel = BoundedAsyncChannel<Int>(capacity: 1, policy: .suspend)
let receiverStarted = expectation(description: "receiver started")
let receiverFinished = expectation(description: "receiver finished")
Task {
var iterator = channel.makeAsyncIterator()
receiverStarted.fulfill()
let result = await iterator.next()
XCTAssertNil(result)
receiverFinished.fulfill()
}
await fulfillment(of: [receiverStarted], timeout: 1.0)
try? await Task.sleep(nanoseconds: 50_000_000)
await channel.finish()
await fulfillment(of: [receiverFinished], timeout: 1.0)
}
// MARK: - Test 9: load test
/// 10K-item producer + slow consumer on `.dropOldest` capacity-128
/// completes without hangs.
func testDropOldestLoadTest() async {
let channel = BoundedAsyncChannel<Int>(capacity: 128, policy: .dropOldest)
let producer = Task {
for i in 1...10_000 {
await channel.send(i)
}
await channel.finish()
}
var receivedCount = 0
for await _ in channel {
receivedCount += 1
// Slow consumer — ~100us per item is enough to keep buffer pressured
// without making the test crawl.
try? await Task.sleep(nanoseconds: 100_000)
}
await producer.value
// Lossy: well below 10K. Capacity-loaded: at least the buffer-worth
// (we may receive less than capacity if the producer's last burst
// overflowed; the contract is "completes without hangs" — assert
// we got at least one item and at most all of them).
XCTAssertGreaterThanOrEqual(receivedCount, 1)
XCTAssertLessThanOrEqual(receivedCount, 10_000)
}
// MARK: - Bonus coverage
func testEmptyChannelFinishImmediately() async {
let channel = BoundedAsyncChannel<Int>(capacity: 4, policy: .suspend)
await channel.finish()
var iterator = channel.makeAsyncIterator()
let result = await iterator.next()
XCTAssertNil(result)
}
func testSendAfterFinishIsNoop() async {
let channel = BoundedAsyncChannel<Int>(capacity: 4, policy: .suspend)
await channel.send(1)
await channel.finish()
await channel.send(2) // should be silently dropped
var collected: [Int] = []
for await item in channel {
collected.append(item)
}
XCTAssertEqual(collected, [1])
}
}