refactor(agent): dedupe harness and command workflows
This commit is contained in:
@@ -7,6 +7,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterAll, describe, expect, it } from "vitest";
|
||||
import { GatewayClient } from "../src/gateway/client.js";
|
||||
import { connectGatewayClient } from "../src/gateway/test-helpers.e2e.js";
|
||||
import { loadOrCreateDeviceIdentity } from "../src/infra/device-identity.js";
|
||||
import { sleep } from "../src/utils.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../src/utils/message-channel.js";
|
||||
@@ -243,17 +244,8 @@ const connectNode = async (
|
||||
const identityPath = path.join(inst.homeDir, `${label}-device.json`);
|
||||
const deviceIdentity = loadOrCreateDeviceIdentity(identityPath);
|
||||
const nodeId = deviceIdentity.deviceId;
|
||||
let settled = false;
|
||||
let resolveReady: (() => void) | null = null;
|
||||
let rejectReady: ((err: Error) => void) | null = null;
|
||||
const ready = new Promise<void>((resolve, reject) => {
|
||||
resolveReady = resolve;
|
||||
rejectReady = reject;
|
||||
});
|
||||
|
||||
const client = new GatewayClient({
|
||||
const client = await connectGatewayClient({
|
||||
url: `ws://127.0.0.1:${inst.port}`,
|
||||
connectDelayMs: 0,
|
||||
token: inst.gatewayToken,
|
||||
clientName: GATEWAY_CLIENT_NAMES.NODE_HOST,
|
||||
clientDisplayName: label,
|
||||
@@ -265,41 +257,8 @@ const connectNode = async (
|
||||
caps: ["system"],
|
||||
commands: ["system.run"],
|
||||
deviceIdentity,
|
||||
onHelloOk: () => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
resolveReady?.();
|
||||
},
|
||||
onConnectError: (err) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
rejectReady?.(err);
|
||||
},
|
||||
onClose: (code, reason) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
rejectReady?.(new Error(`gateway closed (${code}): ${reason}`));
|
||||
},
|
||||
timeoutMessage: `timeout waiting for ${label} to connect`,
|
||||
});
|
||||
|
||||
client.start();
|
||||
try {
|
||||
await Promise.race([
|
||||
ready,
|
||||
sleep(10_000).then(() => {
|
||||
throw new Error(`timeout waiting for ${label} to connect`);
|
||||
}),
|
||||
]);
|
||||
} catch (err) {
|
||||
client.stop();
|
||||
throw err;
|
||||
}
|
||||
return { client, nodeId };
|
||||
};
|
||||
|
||||
|
||||
18
test/helpers/dispatch-inbound-capture.ts
Normal file
18
test/helpers/dispatch-inbound-capture.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { vi } from "vitest";
|
||||
|
||||
export function buildDispatchInboundCaptureMock<T extends Record<string, unknown>>(
|
||||
actual: T,
|
||||
setCtx: (ctx: unknown) => void,
|
||||
) {
|
||||
const dispatchInboundMessage = vi.fn(async (params: { ctx: unknown }) => {
|
||||
setCtx(params.ctx);
|
||||
return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } };
|
||||
});
|
||||
|
||||
return {
|
||||
...actual,
|
||||
dispatchInboundMessage,
|
||||
dispatchInboundMessageWithDispatcher: dispatchInboundMessage,
|
||||
dispatchInboundMessageWithBufferedDispatcher: dispatchInboundMessage,
|
||||
};
|
||||
}
|
||||
23
test/helpers/mock-incoming-request.ts
Normal file
23
test/helpers/mock-incoming-request.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { IncomingMessage } from "node:http";
|
||||
import { EventEmitter } from "node:events";
|
||||
|
||||
export function createMockIncomingRequest(chunks: string[]): IncomingMessage {
|
||||
const req = new EventEmitter() as IncomingMessage & { destroyed?: boolean; destroy: () => void };
|
||||
req.destroyed = false;
|
||||
req.headers = {};
|
||||
req.destroy = () => {
|
||||
req.destroyed = true;
|
||||
};
|
||||
|
||||
void Promise.resolve().then(() => {
|
||||
for (const chunk of chunks) {
|
||||
req.emit("data", Buffer.from(chunk, "utf-8"));
|
||||
if (req.destroyed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
req.emit("end");
|
||||
});
|
||||
|
||||
return req;
|
||||
}
|
||||
@@ -3,10 +3,8 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { GatewayClient } from "../src/gateway/client.js";
|
||||
import { startGatewayServer } from "../src/gateway/server.js";
|
||||
import { getDeterministicFreePortBlock } from "../src/test-utils/ports.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../src/utils/message-channel.js";
|
||||
import { startGatewayWithClient } from "../src/gateway/test-helpers.e2e.js";
|
||||
import { buildOpenAiResponsesProviderConfig } from "../src/gateway/test-openai-responses-model.js";
|
||||
|
||||
type OpenAIResponseStreamEvent =
|
||||
| { type: "response.output_item.added"; item: Record<string, unknown> }
|
||||
@@ -77,44 +75,6 @@ function extractPayloadText(result: unknown): string {
|
||||
return texts.join("\n").trim();
|
||||
}
|
||||
|
||||
async function connectClient(params: { url: string; token: string }) {
|
||||
return await new Promise<InstanceType<typeof GatewayClient>>((resolve, reject) => {
|
||||
let settled = false;
|
||||
const stop = (err?: Error, client?: InstanceType<typeof GatewayClient>) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(client as InstanceType<typeof GatewayClient>);
|
||||
}
|
||||
};
|
||||
const client = new GatewayClient({
|
||||
url: params.url,
|
||||
connectDelayMs: 0,
|
||||
token: params.token,
|
||||
clientName: GATEWAY_CLIENT_NAMES.TEST,
|
||||
clientDisplayName: "vitest-timeout-fallback",
|
||||
clientVersion: "dev",
|
||||
mode: GATEWAY_CLIENT_MODES.TEST,
|
||||
onHelloOk: () => stop(undefined, client),
|
||||
onConnectError: (err) => stop(err),
|
||||
onClose: (code, reason) =>
|
||||
stop(new Error(`gateway closed during connect (${code}): ${reason}`)),
|
||||
});
|
||||
const timer = setTimeout(() => stop(new Error("gateway connect timeout")), 10_000);
|
||||
timer.unref();
|
||||
client.start();
|
||||
});
|
||||
}
|
||||
|
||||
async function getFreeGatewayPort(): Promise<number> {
|
||||
return await getDeterministicFreePortBlock({ offsets: [0, 1, 2, 3, 4] });
|
||||
}
|
||||
|
||||
describe("provider timeouts (e2e)", () => {
|
||||
it(
|
||||
"falls back when the primary provider aborts with a timeout-like AbortError",
|
||||
@@ -183,58 +143,18 @@ describe("provider timeouts (e2e)", () => {
|
||||
models: {
|
||||
mode: "replace",
|
||||
providers: {
|
||||
primary: {
|
||||
baseUrl: primaryBaseUrl,
|
||||
apiKey: "test",
|
||||
api: "openai-responses",
|
||||
models: [
|
||||
{
|
||||
id: "gpt-5.2",
|
||||
name: "gpt-5.2",
|
||||
api: "openai-responses",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128_000,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
],
|
||||
},
|
||||
fallback: {
|
||||
baseUrl: fallbackBaseUrl,
|
||||
apiKey: "test",
|
||||
api: "openai-responses",
|
||||
models: [
|
||||
{
|
||||
id: "gpt-5.2",
|
||||
name: "gpt-5.2",
|
||||
api: "openai-responses",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128_000,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
],
|
||||
},
|
||||
primary: buildOpenAiResponsesProviderConfig(primaryBaseUrl),
|
||||
fallback: buildOpenAiResponsesProviderConfig(fallbackBaseUrl),
|
||||
},
|
||||
},
|
||||
gateway: { auth: { token } },
|
||||
};
|
||||
|
||||
await fs.writeFile(configPath, `${JSON.stringify(cfg, null, 2)}\n`);
|
||||
process.env.OPENCLAW_CONFIG_PATH = configPath;
|
||||
|
||||
const port = await getFreeGatewayPort();
|
||||
const server = await startGatewayServer(port, {
|
||||
bind: "loopback",
|
||||
auth: { mode: "token", token },
|
||||
controlUiEnabled: false,
|
||||
});
|
||||
|
||||
const client = await connectClient({
|
||||
url: `ws://127.0.0.1:${port}`,
|
||||
const { server, client } = await startGatewayWithClient({
|
||||
cfg,
|
||||
configPath,
|
||||
token,
|
||||
clientDisplayName: "vitest-timeout-fallback",
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user