refactor(agent): dedupe harness and command workflows

This commit is contained in:
Peter Steinberger
2026-02-16 14:52:09 +00:00
parent 04892ee230
commit f717a13039
204 changed files with 7366 additions and 11540 deletions

View File

@@ -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 };
};

View 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,
};
}

View 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;
}

View File

@@ -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 {