|
1 | 1 | import { describe, expect, spyOn, test } from "bun:test"; |
| 2 | +import { EventEmitter } from "node:events"; |
| 3 | +import { type Agent } from "@intx/agent"; |
2 | 4 | import { getLogger } from "@intx/log"; |
| 5 | +import type { InferenceSource } from "@intx/types/runtime"; |
3 | 6 |
|
| 7 | +import * as codexSession from "../../auth/codex/session.js"; |
4 | 8 | import { LOG_NAMESPACE_ROOT } from "../../branding.js"; |
5 | 9 | import { defined } from "../../../tests/helpers/defined.js"; |
6 | | -import { finalizeTUIRun } from "./exit.js"; |
| 10 | +import { createDeliveryGeneration } from "../queued-delivery.js"; |
| 11 | +import { createSessionOperationQueue } from "../session-operation-queue.js"; |
| 12 | +import { |
| 13 | + createRunLifecycle, |
| 14 | + finalizeTUIRun, |
| 15 | + resetSessionForRotation, |
| 16 | +} from "./exit.js"; |
7 | 17 | import type { RunnerServices, RunnerState } from "./state.js"; |
8 | 18 |
|
9 | 19 | function stubQuit(args: { |
@@ -114,3 +124,136 @@ describe("finalizeTUIRun quit order", () => { |
114 | 124 | await expect(pending).rejects.toThrow("stop"); |
115 | 125 | }); |
116 | 126 | }); |
| 127 | + |
| 128 | +const liveSource: InferenceSource = { |
| 129 | + id: "codex/work", |
| 130 | + provider: "openai", |
| 131 | + baseURL: "https://example.test", |
| 132 | + apiKey: "old-token", |
| 133 | + model: "m", |
| 134 | +}; |
| 135 | + |
| 136 | +function recordingAgent(sends: string[]): Agent { |
| 137 | + return { |
| 138 | + send: async (content) => { |
| 139 | + sends.push(typeof content === "string" ? content : String(content)); |
| 140 | + return { type: "reply", reply: "", turn: {} as never }; |
| 141 | + }, |
| 142 | + stream: async function* stream() { |
| 143 | + yield* []; |
| 144 | + }, |
| 145 | + deliver: () => undefined, |
| 146 | + close: async () => undefined, |
| 147 | + setSource: () => undefined, |
| 148 | + setSources: () => undefined, |
| 149 | + history: async () => [], |
| 150 | + checkpoints: async () => [], |
| 151 | + readAt: async () => [], |
| 152 | + get blobReader() { |
| 153 | + return {} as Agent["blobReader"]; |
| 154 | + }, |
| 155 | + }; |
| 156 | +} |
| 157 | + |
| 158 | +function stubSendLifecycle(agent: Agent): { |
| 159 | + state: RunnerState; |
| 160 | + services: RunnerServices; |
| 161 | +} { |
| 162 | + const state = { |
| 163 | + runTaskTitle: "keep-title", |
| 164 | + liveSource, |
| 165 | + connectedMcpServers: [], |
| 166 | + config: { cwd: "/tmp", task: "keep-title" }, |
| 167 | + sessionId: "s", |
| 168 | + startedAt: 1, |
| 169 | + inFlight: 0, |
| 170 | + fatalBuildError: null, |
| 171 | + sendAborted: false, |
| 172 | + initialCodexProfile: "work", |
| 173 | + initialXaiProfile: undefined, |
| 174 | + stampProvider: { fn: undefined }, |
| 175 | + } as unknown as RunnerState; |
| 176 | + const services = { |
| 177 | + crashGuard: { |
| 178 | + isFinalized: () => true, |
| 179 | + setPartialFlush: () => undefined, |
| 180 | + }, |
| 181 | + cycleRecorder: { |
| 182 | + dispose: async () => "", |
| 183 | + handleEvent: () => undefined, |
| 184 | + }, |
| 185 | + providerFailureAttempts: {}, |
| 186 | + correlationAcceptance: { observe: () => undefined }, |
| 187 | + runSink: { sink: () => undefined }, |
| 188 | + sessionCost: { addTurn: () => undefined }, |
| 189 | + buildAgent: async () => agent, |
| 190 | + emitter: new EventEmitter(), |
| 191 | + sessionOps: createSessionOperationQueue(), |
| 192 | + deliveryGeneration: createDeliveryGeneration(), |
| 193 | + toolset: { setToolPromoter: () => undefined }, |
| 194 | + subAgentSessions: { cancelAll: async () => [] }, |
| 195 | + activeRunHandle: { task: "", startedAt: 0, model: "" }, |
| 196 | + } as unknown as RunnerServices; |
| 197 | + return { state, services }; |
| 198 | +} |
| 199 | + |
| 200 | +function hangCodexRefresh(): { |
| 201 | + settle: (value: { access: string }) => void; |
| 202 | + spy: ReturnType<typeof spyOn>; |
| 203 | +} { |
| 204 | + let settle: ((value: { access: string }) => void) | undefined; |
| 205 | + const spy = spyOn(codexSession, "getValidCodexToken").mockImplementation( |
| 206 | + () => |
| 207 | + new Promise<{ access: string }>((resolve) => { |
| 208 | + settle = resolve; |
| 209 | + }), |
| 210 | + ); |
| 211 | + return { |
| 212 | + spy, |
| 213 | + settle: (value) => defined(settle, "settleRefresh")(value), |
| 214 | + }; |
| 215 | +} |
| 216 | + |
| 217 | +describe("agentProxy.send vs /clear", () => { |
| 218 | + test("a /clear during hung OAuth after awaitTail does not send into the rebuilt agent", async () => { |
| 219 | + const oldSends: string[] = []; |
| 220 | + const newSends: string[] = []; |
| 221 | + const oldAgent = recordingAgent(oldSends); |
| 222 | + const newAgent = recordingAgent(newSends); |
| 223 | + const { state, services } = stubSendLifecycle(oldAgent); |
| 224 | + const hung = hangCodexRefresh(); |
| 225 | + try { |
| 226 | + const { agentProxy } = await createRunLifecycle(state, services); |
| 227 | + const pending = agentProxy.send("keep me out of the new session"); |
| 228 | + await Promise.resolve(); |
| 229 | + await Promise.resolve(); |
| 230 | + |
| 231 | + resetSessionForRotation(state, services); |
| 232 | + state.currentAgent = newAgent; |
| 233 | + hung.settle({ access: "fresh-token" }); |
| 234 | + await Promise.allSettled([pending]); |
| 235 | + |
| 236 | + expect(newSends).toEqual([]); |
| 237 | + expect(oldSends).toEqual([]); |
| 238 | + } finally { |
| 239 | + hung.spy.mockRestore(); |
| 240 | + } |
| 241 | + }); |
| 242 | + |
| 243 | + test("hung OAuth after awaitTail still sends when the session is not rotated", async () => { |
| 244 | + const sends: string[] = []; |
| 245 | + const { state, services } = stubSendLifecycle(recordingAgent(sends)); |
| 246 | + const hung = hangCodexRefresh(); |
| 247 | + try { |
| 248 | + const { agentProxy } = await createRunLifecycle(state, services); |
| 249 | + const pending = agentProxy.send("deliver this"); |
| 250 | + await Promise.resolve(); |
| 251 | + await Promise.resolve(); |
| 252 | + hung.settle({ access: "fresh-token" }); |
| 253 | + await pending; |
| 254 | + expect(sends).toEqual(["deliver this"]); |
| 255 | + } finally { |
| 256 | + hung.spy.mockRestore(); |
| 257 | + } |
| 258 | + }); |
| 259 | +}); |
0 commit comments