fix(athena): add partial result tracking to session-waiter

Return CouncilSessionWaitResult with timedOut/aborted flags instead of raw array, so callers know when results are partial. Add 5 tests covering normal flow, abort, partial results, and edge cases.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
ismeth
2026-02-18 20:55:48 +01:00
committed by YeonGyu-Kim
parent 4da77be93f
commit 87487d8d25
2 changed files with 126 additions and 3 deletions

View File

@@ -0,0 +1,108 @@
/// <reference types="bun-types" />
import { describe, expect, test } from "bun:test"
import type { BackgroundManager } from "../../features/background-agent"
import { waitForCouncilSessions } from "./session-waiter"
describe("waitForCouncilSessions", () => {
test("resolves all sessions when tasks have sessionIDs immediately", async () => {
//#given
const launched = [
{ member: { model: "openai/gpt-5.3-codex", name: "GPT" }, taskId: "task-1" },
{ member: { model: "anthropic/claude-opus-4-6" }, taskId: "task-2" },
]
const manager = {
getTask: (id: string) => ({ sessionID: `ses-${id}` }),
} as unknown as BackgroundManager
//#when
const result = await waitForCouncilSessions(launched, manager)
//#then
expect(result.sessions).toHaveLength(2)
expect(result.timedOut).toBe(false)
expect(result.aborted).toBe(false)
expect(result.sessions[0].taskId).toBe("task-1")
expect(result.sessions[0].memberName).toBe("GPT")
expect(result.sessions[1].taskId).toBe("task-2")
expect(result.sessions[1].memberName).toBe("anthropic/claude-opus-4-6")
})
test("returns empty sessions for empty launched list", async () => {
//#given
const manager = { getTask: () => undefined } as unknown as BackgroundManager
//#when
const result = await waitForCouncilSessions([], manager)
//#then
expect(result.sessions).toHaveLength(0)
expect(result.timedOut).toBe(false)
expect(result.aborted).toBe(false)
})
test("sets aborted flag when abort signal fires", async () => {
//#given
const launched = [
{ member: { model: "openai/gpt-5.3-codex" }, taskId: "task-1" },
]
const manager = { getTask: () => undefined } as unknown as BackgroundManager
const controller = new AbortController()
// Abort immediately
controller.abort()
//#when
const result = await waitForCouncilSessions(launched, manager, controller.signal)
//#then
expect(result.sessions).toHaveLength(0)
expect(result.aborted).toBe(true)
expect(result.timedOut).toBe(false)
})
test("resolves partial sessions when some tasks get sessionIDs", async () => {
//#given
const launched = [
{ member: { model: "openai/gpt-5.3-codex", name: "GPT" }, taskId: "task-1" },
{ member: { model: "anthropic/claude-opus-4-6" }, taskId: "task-2" },
]
const controller = new AbortController()
let callCount = 0
const manager = {
getTask: (id: string) => {
callCount++
// Only task-1 gets a session, task-2 never does
if (id === "task-1") return { sessionID: "ses-task-1" }
return undefined
},
} as unknown as BackgroundManager
// Abort after a short delay to avoid waiting full 30s
setTimeout(() => controller.abort(), 200)
//#when
const result = await waitForCouncilSessions(launched, manager, controller.signal)
//#then
expect(result.sessions).toHaveLength(1)
expect(result.sessions[0].taskId).toBe("task-1")
expect(result.aborted).toBe(true)
})
test("uses member model as memberName when name is not provided", async () => {
//#given
const launched = [
{ member: { model: "google/gemini-3-pro" }, taskId: "task-1" },
]
const manager = {
getTask: () => ({ sessionID: "ses-1" }),
} as unknown as BackgroundManager
//#when
const result = await waitForCouncilSessions(launched, manager)
//#then
expect(result.sessions[0].memberName).toBe("google/gemini-3-pro")
expect(result.sessions[0].model).toBe("google/gemini-3-pro")
})
})

View File

@@ -11,6 +11,12 @@ interface CouncilSessionInfo {
sessionId: string
}
export interface CouncilSessionWaitResult {
sessions: CouncilSessionInfo[]
timedOut: boolean
aborted: boolean
}
/**
* Waits for background sessions to be created for launched council members.
* Returns session info for each member whose session became available within the timeout.
@@ -19,16 +25,21 @@ export async function waitForCouncilSessions(
launched: CouncilLaunchedMember[],
manager: BackgroundManager,
abort?: AbortSignal
): Promise<CouncilSessionInfo[]> {
): Promise<CouncilSessionWaitResult> {
const results: CouncilSessionInfo[] = []
const pending = new Map(
launched.map((entry) => [entry.taskId, entry])
)
const deadline = Date.now() + WAIT_TIMEOUT_MS
let timedOut = false
let aborted = false
while (pending.size > 0 && Date.now() < deadline) {
if (abort?.aborted) break
if (abort?.aborted) {
aborted = true
break
}
for (const [taskId, entry] of pending) {
const task = manager.getTask(taskId)
@@ -48,5 +59,9 @@ export async function waitForCouncilSessions(
}
}
return results
if (pending.size > 0 && !aborted) {
timedOut = true
}
return { sessions: results, timedOut, aborted }
}