mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 03:32:43 +00:00
refactor(tests): consolidate repeated setup helpers
This commit is contained in:
@@ -185,6 +185,20 @@ async function connectManagerAndGetSocket(manager: OpenAIWebSocketManager) {
|
||||
return sock;
|
||||
}
|
||||
|
||||
async function createConnectedManager(
|
||||
opts?: ConstructorParameters<typeof OpenAIWebSocketManager>[0],
|
||||
): Promise<{ manager: OpenAIWebSocketManager; sock: MockWS }> {
|
||||
const manager = buildManager(opts);
|
||||
const sock = await connectManagerAndGetSocket(manager);
|
||||
return { manager, sock };
|
||||
}
|
||||
|
||||
function connectIgnoringFailure(manager: OpenAIWebSocketManager): Promise<void> {
|
||||
return manager.connect("sk-test").catch(() => {
|
||||
/* ignore rejection */
|
||||
});
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Tests
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -259,11 +273,7 @@ describe("OpenAIWebSocketManager", () => {
|
||||
|
||||
describe("send()", () => {
|
||||
it("sends a JSON-serialized event over the socket", async () => {
|
||||
const manager = buildManager();
|
||||
const connectPromise = manager.connect("sk-test");
|
||||
const sock = lastSocket();
|
||||
sock.simulateOpen();
|
||||
await connectPromise;
|
||||
const { manager, sock } = await createConnectedManager();
|
||||
|
||||
const event: ResponseCreateEvent = {
|
||||
type: "response.create",
|
||||
@@ -286,11 +296,7 @@ describe("OpenAIWebSocketManager", () => {
|
||||
});
|
||||
|
||||
it("includes previous_response_id when provided", async () => {
|
||||
const manager = buildManager();
|
||||
const connectPromise = manager.connect("sk-test");
|
||||
const sock = lastSocket();
|
||||
sock.simulateOpen();
|
||||
await connectPromise;
|
||||
const { manager, sock } = await createConnectedManager();
|
||||
|
||||
const event: ResponseCreateEvent = {
|
||||
type: "response.create",
|
||||
@@ -309,11 +315,7 @@ describe("OpenAIWebSocketManager", () => {
|
||||
|
||||
describe("onMessage()", () => {
|
||||
it("calls handler for each incoming message", async () => {
|
||||
const manager = buildManager();
|
||||
const connectPromise = manager.connect("sk-test");
|
||||
const sock = lastSocket();
|
||||
sock.simulateOpen();
|
||||
await connectPromise;
|
||||
const { manager, sock } = await createConnectedManager();
|
||||
|
||||
const received: OpenAIWebSocketEvent[] = [];
|
||||
manager.onMessage((e) => received.push(e));
|
||||
@@ -332,11 +334,7 @@ describe("OpenAIWebSocketManager", () => {
|
||||
});
|
||||
|
||||
it("returns an unsubscribe function that stops delivery", async () => {
|
||||
const manager = buildManager();
|
||||
const connectPromise = manager.connect("sk-test");
|
||||
const sock = lastSocket();
|
||||
sock.simulateOpen();
|
||||
await connectPromise;
|
||||
const { manager, sock } = await createConnectedManager();
|
||||
|
||||
const received: OpenAIWebSocketEvent[] = [];
|
||||
const unsubscribe = manager.onMessage((e) => received.push(e));
|
||||
@@ -349,11 +347,7 @@ describe("OpenAIWebSocketManager", () => {
|
||||
});
|
||||
|
||||
it("supports multiple simultaneous handlers", async () => {
|
||||
const manager = buildManager();
|
||||
const connectPromise = manager.connect("sk-test");
|
||||
const sock = lastSocket();
|
||||
sock.simulateOpen();
|
||||
await connectPromise;
|
||||
const { manager, sock } = await createConnectedManager();
|
||||
|
||||
const calls: number[] = [];
|
||||
manager.onMessage(() => calls.push(1));
|
||||
@@ -373,11 +367,7 @@ describe("OpenAIWebSocketManager", () => {
|
||||
});
|
||||
|
||||
it("is updated when a response.completed event is received", async () => {
|
||||
const manager = buildManager();
|
||||
const connectPromise = manager.connect("sk-test");
|
||||
const sock = lastSocket();
|
||||
sock.simulateOpen();
|
||||
await connectPromise;
|
||||
const { manager, sock } = await createConnectedManager();
|
||||
|
||||
const completedEvent: ResponseCompletedEvent = {
|
||||
type: "response.completed",
|
||||
@@ -389,11 +379,7 @@ describe("OpenAIWebSocketManager", () => {
|
||||
});
|
||||
|
||||
it("tracks the most recent completed response", async () => {
|
||||
const manager = buildManager();
|
||||
const connectPromise = manager.connect("sk-test");
|
||||
const sock = lastSocket();
|
||||
sock.simulateOpen();
|
||||
await connectPromise;
|
||||
const { manager, sock } = await createConnectedManager();
|
||||
|
||||
sock.simulateMessage({
|
||||
type: "response.completed",
|
||||
@@ -408,11 +394,7 @@ describe("OpenAIWebSocketManager", () => {
|
||||
});
|
||||
|
||||
it("is not updated for non-completed events", async () => {
|
||||
const manager = buildManager();
|
||||
const connectPromise = manager.connect("sk-test");
|
||||
const sock = lastSocket();
|
||||
sock.simulateOpen();
|
||||
await connectPromise;
|
||||
const { manager, sock } = await createConnectedManager();
|
||||
|
||||
sock.simulateMessage({ type: "response.in_progress", response: makeResponse("resp_x") });
|
||||
|
||||
@@ -549,11 +531,7 @@ describe("OpenAIWebSocketManager", () => {
|
||||
|
||||
describe("warmUp()", () => {
|
||||
it("sends a response.create event with generate: false", async () => {
|
||||
const manager = buildManager();
|
||||
const p = manager.connect("sk-test");
|
||||
const sock = lastSocket();
|
||||
sock.simulateOpen();
|
||||
await p;
|
||||
const { manager, sock } = await createConnectedManager();
|
||||
|
||||
manager.warmUp({ model: "gpt-5.2", instructions: "You are helpful." });
|
||||
|
||||
@@ -566,11 +544,7 @@ describe("OpenAIWebSocketManager", () => {
|
||||
});
|
||||
|
||||
it("includes tools when provided", async () => {
|
||||
const manager = buildManager();
|
||||
const p = manager.connect("sk-test");
|
||||
const sock = lastSocket();
|
||||
sock.simulateOpen();
|
||||
await p;
|
||||
const { manager, sock } = await createConnectedManager();
|
||||
|
||||
manager.warmUp({
|
||||
model: "gpt-5.2",
|
||||
@@ -612,9 +586,7 @@ describe("OpenAIWebSocketManager", () => {
|
||||
|
||||
it("emits error event on WebSocket socket error", async () => {
|
||||
const manager = buildManager({ maxRetries: 0 });
|
||||
const p = manager.connect("sk-test").catch(() => {
|
||||
/* ignore rejection */
|
||||
});
|
||||
const p = connectIgnoringFailure(manager);
|
||||
const errors = attachErrorCollector(manager);
|
||||
|
||||
lastSocket().simulateError(new Error("SSL handshake failed"));
|
||||
@@ -625,9 +597,7 @@ describe("OpenAIWebSocketManager", () => {
|
||||
|
||||
it("handles multiple successive socket errors without crashing", async () => {
|
||||
const manager = buildManager({ maxRetries: 0 });
|
||||
const p = manager.connect("sk-test").catch(() => {
|
||||
/* ignore rejection */
|
||||
});
|
||||
const p = connectIgnoringFailure(manager);
|
||||
const errors = attachErrorCollector(manager);
|
||||
|
||||
// Fire two errors in quick succession — previously the second would
|
||||
@@ -646,11 +616,7 @@ describe("OpenAIWebSocketManager", () => {
|
||||
|
||||
describe("full turn sequence", () => {
|
||||
it("tracks previous_response_id across turns and sends continuation correctly", async () => {
|
||||
const manager = buildManager();
|
||||
const p = manager.connect("sk-test");
|
||||
const sock = lastSocket();
|
||||
sock.simulateOpen();
|
||||
await p;
|
||||
const { manager, sock } = await createConnectedManager();
|
||||
|
||||
const received: OpenAIWebSocketEvent[] = [];
|
||||
manager.onMessage((e) => received.push(e));
|
||||
|
||||
@@ -87,6 +87,52 @@ async function runSessionThreadSpawnAndGetError(params: {
|
||||
return result.details as { error?: string; childSessionKey?: string };
|
||||
}
|
||||
|
||||
async function getDiscordThreadSessionTool() {
|
||||
return await getSessionsSpawnTool({
|
||||
agentSessionKey: "main",
|
||||
agentChannel: "discord",
|
||||
agentAccountId: "work",
|
||||
agentTo: "channel:123",
|
||||
agentThreadId: "456",
|
||||
});
|
||||
}
|
||||
|
||||
async function executeDiscordThreadSessionSpawn(toolCallId: string) {
|
||||
const tool = await getDiscordThreadSessionTool();
|
||||
return await tool.execute(toolCallId, {
|
||||
task: "do thing",
|
||||
thread: true,
|
||||
mode: "session",
|
||||
});
|
||||
}
|
||||
|
||||
function getSpawnedEventCall(): Record<string, unknown> {
|
||||
const [event] = (hookRunnerMocks.runSubagentSpawned.mock.calls[0] ?? []) as unknown as [
|
||||
Record<string, unknown>,
|
||||
];
|
||||
return event;
|
||||
}
|
||||
|
||||
function expectErrorResultMessage(result: { details: unknown }, pattern: RegExp): void {
|
||||
expect(result.details).toMatchObject({ status: "error" });
|
||||
const details = result.details as { error?: string };
|
||||
expect(details.error).toMatch(pattern);
|
||||
}
|
||||
|
||||
function expectThreadBindFailureCleanup(
|
||||
details: { childSessionKey?: string },
|
||||
pattern: RegExp,
|
||||
): void {
|
||||
expect(details.error).toMatch(pattern);
|
||||
expect(hookRunnerMocks.runSubagentSpawned).not.toHaveBeenCalled();
|
||||
expectSessionsDeleteWithoutAgentStart();
|
||||
const deleteCall = findGatewayRequest("sessions.delete");
|
||||
expect(deleteCall?.params).toMatchObject({
|
||||
key: details.childSessionKey,
|
||||
emitLifecycleHooks: false,
|
||||
});
|
||||
}
|
||||
|
||||
describe("sessions_spawn subagent lifecycle hooks", () => {
|
||||
beforeEach(() => {
|
||||
resetSubagentRegistryForTests();
|
||||
@@ -226,9 +272,7 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
|
||||
|
||||
expect(result.details).toMatchObject({ status: "accepted", runId: "run-1", mode: "run" });
|
||||
expect(hookRunnerMocks.runSubagentSpawning).toHaveBeenCalledTimes(1);
|
||||
const [event] = (hookRunnerMocks.runSubagentSpawned.mock.calls[0] ?? []) as unknown as [
|
||||
Record<string, unknown>,
|
||||
];
|
||||
const event = getSpawnedEventCall();
|
||||
expect(event).toMatchObject({
|
||||
mode: "run",
|
||||
threadRequested: true,
|
||||
@@ -243,14 +287,7 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
|
||||
error: "Unable to create or bind a Discord thread for this subagent session.",
|
||||
},
|
||||
});
|
||||
expect(details.error).toMatch(/thread/i);
|
||||
expect(hookRunnerMocks.runSubagentSpawned).not.toHaveBeenCalled();
|
||||
expectSessionsDeleteWithoutAgentStart();
|
||||
const deleteCall = findGatewayRequest("sessions.delete");
|
||||
expect(deleteCall?.params).toMatchObject({
|
||||
key: details.childSessionKey,
|
||||
emitLifecycleHooks: false,
|
||||
});
|
||||
expectThreadBindFailureCleanup(details, /thread/i);
|
||||
});
|
||||
|
||||
it("returns error when thread binding is not marked ready", async () => {
|
||||
@@ -261,14 +298,7 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
|
||||
threadBindingReady: false,
|
||||
},
|
||||
});
|
||||
expect(details.error).toMatch(/unable to create or bind a thread/i);
|
||||
expect(hookRunnerMocks.runSubagentSpawned).not.toHaveBeenCalled();
|
||||
expectSessionsDeleteWithoutAgentStart();
|
||||
const deleteCall = findGatewayRequest("sessions.delete");
|
||||
expect(deleteCall?.params).toMatchObject({
|
||||
key: details.childSessionKey,
|
||||
emitLifecycleHooks: false,
|
||||
});
|
||||
expectThreadBindFailureCleanup(details, /unable to create or bind a thread/i);
|
||||
});
|
||||
|
||||
it("rejects mode=session when thread=true is not requested", async () => {
|
||||
@@ -283,9 +313,7 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
|
||||
mode: "session",
|
||||
});
|
||||
|
||||
expect(result.details).toMatchObject({ status: "error" });
|
||||
const details = result.details as { error?: string };
|
||||
expect(details.error).toMatch(/requires thread=true/i);
|
||||
expectErrorResultMessage(result, /requires thread=true/i);
|
||||
expect(hookRunnerMocks.runSubagentSpawning).not.toHaveBeenCalled();
|
||||
expect(hookRunnerMocks.runSubagentSpawned).not.toHaveBeenCalled();
|
||||
const callGatewayMock = getCallGatewayMock();
|
||||
@@ -305,9 +333,7 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
|
||||
mode: "session",
|
||||
});
|
||||
|
||||
expect(result.details).toMatchObject({ status: "error" });
|
||||
const details = result.details as { error?: string };
|
||||
expect(details.error).toMatch(/only discord/i);
|
||||
expectErrorResultMessage(result, /only discord/i);
|
||||
expect(hookRunnerMocks.runSubagentSpawning).toHaveBeenCalledTimes(1);
|
||||
expect(hookRunnerMocks.runSubagentSpawned).not.toHaveBeenCalled();
|
||||
expectSessionsDeleteWithoutAgentStart();
|
||||
@@ -315,19 +341,7 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
|
||||
|
||||
it("runs subagent_ended cleanup hook when agent start fails after successful bind", async () => {
|
||||
mockAgentStartFailure();
|
||||
const tool = await getSessionsSpawnTool({
|
||||
agentSessionKey: "main",
|
||||
agentChannel: "discord",
|
||||
agentAccountId: "work",
|
||||
agentTo: "channel:123",
|
||||
agentThreadId: "456",
|
||||
});
|
||||
|
||||
const result = await tool.execute("call7", {
|
||||
task: "do thing",
|
||||
thread: true,
|
||||
mode: "session",
|
||||
});
|
||||
const result = await executeDiscordThreadSessionSpawn("call7");
|
||||
|
||||
expect(result.details).toMatchObject({ status: "error" });
|
||||
expect(hookRunnerMocks.runSubagentEnded).toHaveBeenCalledTimes(1);
|
||||
@@ -354,19 +368,7 @@ describe("sessions_spawn subagent lifecycle hooks", () => {
|
||||
it("falls back to sessions.delete cleanup when subagent_ended hook is unavailable", async () => {
|
||||
hookRunnerMocks.hasSubagentEndedHook = false;
|
||||
mockAgentStartFailure();
|
||||
const tool = await getSessionsSpawnTool({
|
||||
agentSessionKey: "main",
|
||||
agentChannel: "discord",
|
||||
agentAccountId: "work",
|
||||
agentTo: "channel:123",
|
||||
agentThreadId: "456",
|
||||
});
|
||||
|
||||
const result = await tool.execute("call8", {
|
||||
task: "do thing",
|
||||
thread: true,
|
||||
mode: "session",
|
||||
});
|
||||
const result = await executeDiscordThreadSessionSpawn("call8");
|
||||
|
||||
expect(result.details).toMatchObject({ status: "error" });
|
||||
expect(hookRunnerMocks.runSubagentEnded).not.toHaveBeenCalled();
|
||||
|
||||
Reference in New Issue
Block a user