mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-03 09:41:37 +00:00
709 lines
22 KiB
TypeScript
709 lines
22 KiB
TypeScript
// OpenClaw SDK tests cover index behavior.
|
|
import type { AddressInfo } from "node:net";
|
|
import net from "node:net";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { WebSocketServer, type WebSocket } from "ws";
|
|
import { installGatewayTestHooks, startServer } from "../../../src/gateway/test-helpers.js";
|
|
import { emitAgentEvent, registerAgentRunContext } from "../../../src/infra/agent-events.js";
|
|
import { rawDataToString } from "../../../src/infra/ws.js";
|
|
import { withTimeout } from "../../../src/utils/with-timeout.js";
|
|
import { GatewayClientTransport, OpenClaw } from "./index.js";
|
|
|
|
type JsonObject = Record<string, unknown>;
|
|
type FakeGatewayRequest = {
|
|
id: string;
|
|
method: string;
|
|
params?: unknown;
|
|
};
|
|
type FakeGateway = {
|
|
url: string;
|
|
requests: FakeGatewayRequest[];
|
|
close: () => Promise<void>;
|
|
};
|
|
|
|
const servers: WebSocketServer[] = [];
|
|
|
|
function expectJsonObject(value: unknown): JsonObject {
|
|
expect(value && typeof value).toBe("object");
|
|
return value as JsonObject;
|
|
}
|
|
|
|
function sendJson(socket: WebSocket, payload: JsonObject): void {
|
|
socket.send(JSON.stringify(payload));
|
|
}
|
|
|
|
async function reservePort(): Promise<number> {
|
|
const server = net.createServer();
|
|
await new Promise<void>((resolve) => {
|
|
server.listen(0, "127.0.0.1", resolve);
|
|
});
|
|
const { port } = server.address() as AddressInfo;
|
|
await new Promise<void>((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
return port;
|
|
}
|
|
|
|
async function createFakeGateway(port = 0): Promise<FakeGateway> {
|
|
const server = new WebSocketServer({ host: "127.0.0.1", port });
|
|
servers.push(server);
|
|
await new Promise<void>((resolve) => {
|
|
server.once("listening", resolve);
|
|
});
|
|
let seq = 1;
|
|
const requests: FakeGatewayRequest[] = [];
|
|
server.on("connection", (socket) => {
|
|
socket.binaryType = "nodebuffer";
|
|
sendJson(socket, {
|
|
type: "event",
|
|
event: "connect.challenge",
|
|
seq: seq++,
|
|
payload: { nonce: "sdk-e2e-nonce", ts: Date.now() },
|
|
});
|
|
|
|
socket.on("message", (raw) => {
|
|
const frame = JSON.parse(rawDataToString(raw)) as FakeGatewayRequest;
|
|
requests.push(frame);
|
|
const reply = (payload: JsonObject): void => {
|
|
sendJson(socket, { type: "res", id: frame.id, ok: true, payload });
|
|
};
|
|
|
|
if (frame.method === "connect") {
|
|
reply({
|
|
type: "hello-ok",
|
|
protocol: 1,
|
|
server: { version: "sdk-e2e", connId: "conn-sdk-e2e" },
|
|
features: {
|
|
methods: [
|
|
"agent",
|
|
"agent.wait",
|
|
"agent.identity.get",
|
|
"agents.create",
|
|
"agents.delete",
|
|
"agents.list",
|
|
"agents.update",
|
|
"connect",
|
|
"exec.approval.list",
|
|
"exec.approval.resolve",
|
|
"models.authStatus",
|
|
"models.list",
|
|
"sessions.abort",
|
|
"sessions.create",
|
|
"sessions.compact",
|
|
"sessions.list",
|
|
"sessions.patch",
|
|
"sessions.resolve",
|
|
"sessions.send",
|
|
"tasks.cancel",
|
|
"tasks.get",
|
|
"tasks.list",
|
|
"tools.catalog",
|
|
"tools.effective",
|
|
"tools.invoke",
|
|
],
|
|
events: ["agent", "sessions.changed"],
|
|
},
|
|
snapshot: {
|
|
presence: [],
|
|
health: {},
|
|
stateVersion: { presence: 0, health: 0 },
|
|
uptimeMs: 1,
|
|
},
|
|
auth: { role: "operator", scopes: [] },
|
|
policy: {
|
|
maxPayload: 262144,
|
|
maxBufferedBytes: 262144,
|
|
tickIntervalMs: 30000,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (frame.method === "agents.list") {
|
|
reply({ agents: [{ id: "main" }] });
|
|
return;
|
|
}
|
|
|
|
if (frame.method === "agent.identity.get") {
|
|
reply({ agentId: "main", ...(frame.params as JsonObject | undefined) });
|
|
return;
|
|
}
|
|
|
|
if (
|
|
frame.method === "agents.create" ||
|
|
frame.method === "agents.update" ||
|
|
frame.method === "agents.delete"
|
|
) {
|
|
reply({ ok: true, method: frame.method, params: frame.params as JsonObject | undefined });
|
|
return;
|
|
}
|
|
|
|
if (frame.method === "agent") {
|
|
const params = frame.params as { sessionKey?: string } | undefined;
|
|
reply({
|
|
status: "accepted",
|
|
runId: "run-sdk-e2e",
|
|
sessionKey: params?.sessionKey,
|
|
});
|
|
setTimeout(() => {
|
|
sendJson(socket, {
|
|
type: "event",
|
|
event: "agent",
|
|
seq: seq++,
|
|
payload: {
|
|
runId: "run-sdk-e2e",
|
|
sessionKey: params?.sessionKey,
|
|
stream: "lifecycle",
|
|
ts: Date.now(),
|
|
data: { phase: "start" },
|
|
},
|
|
});
|
|
sendJson(socket, {
|
|
type: "event",
|
|
event: "agent",
|
|
seq: seq++,
|
|
payload: {
|
|
runId: "run-sdk-e2e",
|
|
sessionKey: params?.sessionKey,
|
|
stream: "assistant",
|
|
ts: Date.now(),
|
|
data: { delta: "hello from fake gateway" },
|
|
},
|
|
});
|
|
sendJson(socket, {
|
|
type: "event",
|
|
event: "agent",
|
|
seq: seq++,
|
|
payload: {
|
|
runId: "run-sdk-e2e",
|
|
sessionKey: params?.sessionKey,
|
|
stream: "lifecycle",
|
|
ts: Date.now(),
|
|
data: { phase: "end" },
|
|
},
|
|
});
|
|
}, 50);
|
|
return;
|
|
}
|
|
|
|
if (frame.method === "agent.wait") {
|
|
reply({
|
|
status: "ok",
|
|
runId: "run-sdk-e2e",
|
|
sessionKey: "main",
|
|
startedAt: 123,
|
|
endedAt: 456,
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (frame.method === "sessions.list") {
|
|
reply({ sessions: [{ key: "sdk-session" }] });
|
|
return;
|
|
}
|
|
|
|
if (frame.method === "sessions.create") {
|
|
const params = frame.params as { key?: string } | undefined;
|
|
reply({ key: params?.key ?? "sdk-session" });
|
|
return;
|
|
}
|
|
|
|
if (frame.method === "sessions.resolve") {
|
|
reply({ key: "sdk-session", params: frame.params as JsonObject | undefined });
|
|
return;
|
|
}
|
|
|
|
if (frame.method === "sessions.send") {
|
|
const params = frame.params as { key?: string } | undefined;
|
|
reply({ status: "ok", runId: "run-session-e2e", sessionKey: params?.key });
|
|
return;
|
|
}
|
|
|
|
if (frame.method === "sessions.abort") {
|
|
reply({
|
|
ok: true,
|
|
abortedRunId: (frame.params as { runId?: string } | undefined)?.runId ?? "run-sdk-e2e",
|
|
status: "aborted",
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (frame.method === "sessions.patch" || frame.method === "sessions.compact") {
|
|
reply({ ok: true, method: frame.method, params: frame.params as JsonObject | undefined });
|
|
return;
|
|
}
|
|
|
|
if (frame.method === "tasks.list") {
|
|
reply({
|
|
tasks: [
|
|
{
|
|
id: "task-sdk-e2e",
|
|
status: "running",
|
|
title: "SDK task",
|
|
runId: "run-sdk-e2e",
|
|
sessionKey: "sdk-session",
|
|
},
|
|
],
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (frame.method === "tasks.get") {
|
|
reply({
|
|
task: {
|
|
id: (frame.params as { taskId?: string } | undefined)?.taskId ?? "task-sdk-e2e",
|
|
status: "running",
|
|
title: "SDK task",
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (frame.method === "tasks.cancel") {
|
|
reply({
|
|
found: true,
|
|
cancelled: true,
|
|
task: {
|
|
id: (frame.params as { taskId?: string } | undefined)?.taskId ?? "task-sdk-e2e",
|
|
status: "cancelled",
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (frame.method === "models.list") {
|
|
reply({ models: [{ id: "gpt-5.4" }] });
|
|
return;
|
|
}
|
|
|
|
if (frame.method === "models.authStatus") {
|
|
reply({ providers: [] });
|
|
return;
|
|
}
|
|
|
|
if (frame.method === "tools.catalog") {
|
|
reply({ tools: [{ name: "shell" }] });
|
|
return;
|
|
}
|
|
|
|
if (frame.method === "tools.effective") {
|
|
reply({ tools: [{ name: "shell", enabled: true }] });
|
|
return;
|
|
}
|
|
|
|
if (frame.method === "tools.invoke") {
|
|
reply({ ok: true, toolName: "shell", output: { ok: true } });
|
|
return;
|
|
}
|
|
|
|
if (frame.method === "exec.approval.list") {
|
|
reply({ approvals: [] });
|
|
return;
|
|
}
|
|
|
|
if (frame.method === "exec.approval.resolve") {
|
|
expect(frame.params).toMatchObject({ id: "approval-1", decision: "allow-once" });
|
|
reply({ ok: true, params: frame.params as JsonObject | undefined });
|
|
return;
|
|
}
|
|
|
|
sendJson(socket, {
|
|
type: "res",
|
|
id: frame.id,
|
|
ok: false,
|
|
error: { code: "UNKNOWN_METHOD", message: `unhandled fake Gateway method ${frame.method}` },
|
|
});
|
|
});
|
|
});
|
|
|
|
const { port: boundPort } = server.address() as AddressInfo;
|
|
return {
|
|
url: `ws://127.0.0.1:${boundPort}`,
|
|
requests,
|
|
close: () => {
|
|
const index = servers.indexOf(server);
|
|
if (index >= 0) {
|
|
servers.splice(index, 1);
|
|
}
|
|
for (const socket of server.clients) {
|
|
socket.terminate();
|
|
}
|
|
return new Promise<void>((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("OpenClaw SDK websocket e2e", () => {
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
servers.splice(0).map(
|
|
(server) =>
|
|
new Promise<void>((resolve) => {
|
|
for (const client of server.clients) {
|
|
client.terminate();
|
|
}
|
|
server.close(() => resolve());
|
|
}),
|
|
),
|
|
);
|
|
});
|
|
|
|
it("runs an agent and streams normalized events over a Gateway websocket", async () => {
|
|
const gateway = await createFakeGateway();
|
|
const transport = new GatewayClientTransport({
|
|
url: gateway.url,
|
|
deviceIdentity: null,
|
|
requestTimeoutMs: 2_000,
|
|
});
|
|
const oc = new OpenClaw({ transport });
|
|
try {
|
|
const agent = await oc.agents.get("main");
|
|
const run = await agent.run({
|
|
input: "say hello",
|
|
sessionKey: "main",
|
|
idempotencyKey: "sdk-e2e",
|
|
});
|
|
const seenPromise = (async () => {
|
|
const seen: string[] = [];
|
|
|
|
for await (const event of run.events()) {
|
|
seen.push(event.type);
|
|
if (event.type === "run.completed") {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return seen;
|
|
})();
|
|
|
|
const [seen, result] = await Promise.all([
|
|
withTimeout(seenPromise, 2_000, { message: "timed out waiting for SDK run events" }),
|
|
run.wait({ timeoutMs: 2_000 }),
|
|
]);
|
|
|
|
expect(run.id).toBe("run-sdk-e2e");
|
|
expect(seen).toEqual(["run.started", "assistant.delta", "run.completed"]);
|
|
expect(result.runId).toBe("run-sdk-e2e");
|
|
expect(result.sessionKey).toBe("main");
|
|
expect(result.status).toBe("completed");
|
|
expect(result.startedAt).toBe(123);
|
|
expect(result.endedAt).toBe(456);
|
|
const cancelResult = expectJsonObject(await run.cancel());
|
|
expect(cancelResult.abortedRunId).toBe("run-sdk-e2e");
|
|
expect(cancelResult.status).toBe("aborted");
|
|
} finally {
|
|
await oc.close();
|
|
await gateway.close();
|
|
}
|
|
});
|
|
|
|
it("covers documented namespace helpers over a Gateway websocket", async () => {
|
|
const gateway = await createFakeGateway();
|
|
const transport = new GatewayClientTransport({
|
|
url: gateway.url,
|
|
deviceIdentity: null,
|
|
requestTimeoutMs: 2_000,
|
|
});
|
|
const oc = new OpenClaw({ transport });
|
|
|
|
try {
|
|
const agents = expectJsonObject(await oc.agents.list());
|
|
expect(agents.agents).toEqual([{ id: "main" }]);
|
|
const agent = await oc.agents.get("main");
|
|
const identity = expectJsonObject(await agent.identity({ sessionKey: "sdk-session" }));
|
|
expect(identity.agentId).toBe("main");
|
|
expect(identity.sessionKey).toBe("sdk-session");
|
|
const createAgent = expectJsonObject(
|
|
await oc.agents.create({ name: "SDK Agent", workspace: "/tmp/sdk-agent" }),
|
|
);
|
|
expect(createAgent.method).toBe("agents.create");
|
|
expect(createAgent.params).toEqual({ name: "SDK Agent", workspace: "/tmp/sdk-agent" });
|
|
const updateAgent = expectJsonObject(
|
|
await oc.agents.update({ agentId: "sdk-agent", name: "Renamed SDK Agent" }),
|
|
);
|
|
expect(updateAgent.method).toBe("agents.update");
|
|
expect(updateAgent.params).toEqual({ agentId: "sdk-agent", name: "Renamed SDK Agent" });
|
|
const clearAgentModel = expectJsonObject(
|
|
await oc.agents.update({ agentId: "sdk-agent", model: null }),
|
|
);
|
|
expect(clearAgentModel.params).toEqual({ agentId: "sdk-agent", model: null });
|
|
const deleteAgent = expectJsonObject(await oc.agents.delete({ agentId: "sdk-agent" }));
|
|
expect(deleteAgent.method).toBe("agents.delete");
|
|
expect(deleteAgent.params).toEqual({ agentId: "sdk-agent" });
|
|
|
|
const sessions = expectJsonObject(await oc.sessions.list());
|
|
expect(sessions.sessions).toEqual([{ key: "sdk-session" }]);
|
|
const session = await oc.sessions.create({ key: "sdk-session", agentId: "main" });
|
|
expect(session.key).toBe("sdk-session");
|
|
const resolvedSession = expectJsonObject(await oc.sessions.resolve({ key: "sdk-session" }));
|
|
expect(resolvedSession.key).toBe("sdk-session");
|
|
const sessionRun = await session.send("continue");
|
|
expect(sessionRun.id).toBe("run-session-e2e");
|
|
const abortSession = expectJsonObject(await session.abort(sessionRun.id));
|
|
expect(abortSession.abortedRunId).toBe("run-session-e2e");
|
|
const patchSession = expectJsonObject(await session.patch({ label: "Renamed" }));
|
|
expect(patchSession.method).toBe("sessions.patch");
|
|
const compactSession = expectJsonObject(await session.compact({ maxLines: 200 }));
|
|
expect(compactSession.method).toBe("sessions.compact");
|
|
|
|
const tasks = await oc.tasks.list({ status: "running" });
|
|
expect(tasks.tasks).toEqual([
|
|
{
|
|
id: "task-sdk-e2e",
|
|
status: "running",
|
|
title: "SDK task",
|
|
runId: "run-sdk-e2e",
|
|
sessionKey: "sdk-session",
|
|
},
|
|
]);
|
|
const task = await oc.tasks.get("task-sdk-e2e");
|
|
expect(task.task).toEqual({
|
|
id: "task-sdk-e2e",
|
|
status: "running",
|
|
title: "SDK task",
|
|
});
|
|
const cancelledTask = await oc.tasks.cancel("task-sdk-e2e");
|
|
expect(cancelledTask.cancelled).toBe(true);
|
|
|
|
const models = expectJsonObject(await oc.models.list());
|
|
expect(models.models).toEqual([{ id: "gpt-5.4" }]);
|
|
const modelStatus = expectJsonObject(await oc.models.status({ probe: false }));
|
|
expect(modelStatus.providers).toEqual([]);
|
|
const tools = expectJsonObject(await oc.tools.list());
|
|
expect(tools.tools).toEqual([{ name: "shell" }]);
|
|
const effectiveTools = expectJsonObject(
|
|
await oc.tools.effective({ sessionKey: "sdk-session" }),
|
|
);
|
|
expect(effectiveTools.tools).toEqual([{ name: "shell", enabled: true }]);
|
|
const toolResult = await oc.tools.invoke("shell", {
|
|
args: { command: "pwd" },
|
|
sessionKey: "sdk-session",
|
|
});
|
|
expect(toolResult.ok).toBe(true);
|
|
expect(toolResult.toolName).toBe("shell");
|
|
expect(toolResult.output).toEqual({ ok: true });
|
|
const approvals = expectJsonObject(await oc.approvals.list());
|
|
expect(approvals.approvals).toEqual([]);
|
|
const approvalResult = expectJsonObject(
|
|
await oc.approvals.respond("approval-1", { decision: "allow-once" }),
|
|
);
|
|
expect(approvalResult.ok).toBe(true);
|
|
|
|
expect(gateway.requests.map((request) => request.method)).toEqual([
|
|
"connect",
|
|
"agents.list",
|
|
"agent.identity.get",
|
|
"agents.create",
|
|
"agents.update",
|
|
"agents.update",
|
|
"agents.delete",
|
|
"sessions.list",
|
|
"sessions.create",
|
|
"sessions.resolve",
|
|
"sessions.send",
|
|
"sessions.abort",
|
|
"sessions.patch",
|
|
"sessions.compact",
|
|
"tasks.list",
|
|
"tasks.get",
|
|
"tasks.cancel",
|
|
"models.list",
|
|
"models.authStatus",
|
|
"tools.catalog",
|
|
"tools.effective",
|
|
"tools.invoke",
|
|
"exec.approval.list",
|
|
"exec.approval.resolve",
|
|
]);
|
|
const requestParams = new Map(
|
|
gateway.requests.map((request) => [request.method, request.params]),
|
|
);
|
|
expect(requestParams.get("agents.list")).toEqual({});
|
|
expect(requestParams.get("sessions.list")).toEqual({});
|
|
expect(requestParams.get("models.list")).toEqual({});
|
|
expect(requestParams.get("tools.catalog")).toEqual({});
|
|
expect(requestParams.get("exec.approval.list")).toEqual({});
|
|
} finally {
|
|
await oc.close();
|
|
await gateway.close();
|
|
}
|
|
}, 10_000);
|
|
|
|
it("retries after an initial websocket connection failure", async () => {
|
|
const port = await reservePort();
|
|
const url = `ws://127.0.0.1:${port}`;
|
|
const transport = new GatewayClientTransport({
|
|
url,
|
|
deviceIdentity: null,
|
|
connectChallengeTimeoutMs: 200,
|
|
preauthHandshakeTimeoutMs: 200,
|
|
requestTimeoutMs: 500,
|
|
});
|
|
|
|
const initialConnectError = await transport.connect().catch((error: unknown) => error);
|
|
expect(initialConnectError).toBeInstanceOf(Error);
|
|
expect(String(initialConnectError)).toMatch(/ECONNREFUSED/);
|
|
|
|
const gateway = await createFakeGateway(port);
|
|
try {
|
|
await expect(transport.connect()).resolves.toBeUndefined();
|
|
} finally {
|
|
await transport.close();
|
|
await gateway.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("OpenClaw SDK real Gateway e2e", () => {
|
|
installGatewayTestHooks({ scope: "test" });
|
|
|
|
it("streams real Gateway agent events", async () => {
|
|
const token = "sdk-real-gateway-token";
|
|
const started = await startServer(token, { controlUiEnabled: false });
|
|
const transport = new GatewayClientTransport({
|
|
url: `ws://127.0.0.1:${started.port}`,
|
|
token,
|
|
deviceIdentity: null,
|
|
requestTimeoutMs: 2_000,
|
|
});
|
|
const oc = new OpenClaw({ transport });
|
|
const runId = "sdk-real-gateway-run";
|
|
|
|
try {
|
|
await oc.connect();
|
|
|
|
registerAgentRunContext(runId, {
|
|
sessionKey: "agent:main:dashboard:sdk-real-gateway",
|
|
verboseLevel: "off",
|
|
});
|
|
|
|
const run = await oc.runs.get(runId);
|
|
const eventsPromise = (async () => {
|
|
const seen: string[] = [];
|
|
const sessionKeys: Array<string | undefined> = [];
|
|
for await (const event of run.events()) {
|
|
seen.push(event.type);
|
|
sessionKeys.push(event.sessionKey);
|
|
if (event.type === "run.completed") {
|
|
break;
|
|
}
|
|
}
|
|
return { seen, sessionKeys };
|
|
})();
|
|
|
|
emitAgentEvent({
|
|
runId,
|
|
stream: "lifecycle",
|
|
data: { phase: "start", startedAt: 111 },
|
|
});
|
|
emitAgentEvent({
|
|
runId,
|
|
stream: "assistant",
|
|
data: { delta: "hello from real gateway" },
|
|
});
|
|
emitAgentEvent({
|
|
runId,
|
|
stream: "lifecycle",
|
|
data: { phase: "end", endedAt: 222 },
|
|
});
|
|
|
|
const { seen, sessionKeys } = await withTimeout(eventsPromise, 2_000, {
|
|
message: "timed out waiting for real Gateway SDK events",
|
|
});
|
|
expect(seen).toEqual(["run.started", "assistant.delta", "run.completed"]);
|
|
expect(sessionKeys).toEqual([
|
|
"agent:main:dashboard:sdk-real-gateway",
|
|
"agent:main:dashboard:sdk-real-gateway",
|
|
"agent:main:dashboard:sdk-real-gateway",
|
|
]);
|
|
} finally {
|
|
await oc.close();
|
|
await started.server.close();
|
|
started.envSnapshot.restore();
|
|
}
|
|
});
|
|
});
|
|
|
|
const liveGatewayUrl = process.env.OPENCLAW_SDK_LIVE_GATEWAY_URL;
|
|
const liveGatewayToken = process.env.OPENCLAW_SDK_LIVE_GATEWAY_TOKEN;
|
|
const liveGatewayDescribe = liveGatewayUrl && liveGatewayToken ? describe : describe.skip;
|
|
|
|
function readLiveTextDelta(data: unknown): string {
|
|
if (!data || typeof data !== "object") {
|
|
return "";
|
|
}
|
|
const record = data as Record<string, unknown>;
|
|
for (const key of ["delta", "text", "content"]) {
|
|
const value = record[key];
|
|
if (typeof value === "string") {
|
|
return value;
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function expectArrayProperty(value: unknown, property: string): void {
|
|
expect(value && typeof value).toBe("object");
|
|
const record = value as Record<string, unknown>;
|
|
expect(Array.isArray(record[property])).toBe(true);
|
|
}
|
|
|
|
liveGatewayDescribe("OpenClaw SDK live Gateway e2e", () => {
|
|
it("connects to a configured Gateway, streams a real run, and waits for completion", async () => {
|
|
const oc = new OpenClaw({
|
|
url: liveGatewayUrl,
|
|
token: liveGatewayToken,
|
|
requestTimeoutMs: 20_000,
|
|
});
|
|
|
|
try {
|
|
await oc.connect();
|
|
expectArrayProperty(await oc.agents.list(), "agents");
|
|
expectArrayProperty(await oc.models.status({ probe: false }), "providers");
|
|
|
|
const agent = await oc.agents.get(process.env.OPENCLAW_SDK_LIVE_AGENT_ID ?? "main");
|
|
const run = await agent.run({
|
|
input: "Reply with exactly: OPENCLAW_SDK_LIVE_OK",
|
|
sessionKey: `sdk-live-e2e-${Date.now()}`,
|
|
deliver: false,
|
|
timeoutMs: 120_000,
|
|
label: "SDK live E2E",
|
|
});
|
|
|
|
const eventsPromise = (async () => {
|
|
const eventTypes: string[] = [];
|
|
let text = "";
|
|
for await (const event of run.events()) {
|
|
eventTypes.push(event.type);
|
|
if (event.type === "assistant.delta" || event.type === "assistant.message") {
|
|
text += readLiveTextDelta(event.data);
|
|
}
|
|
if (
|
|
event.type === "run.completed" ||
|
|
event.type === "run.failed" ||
|
|
event.type === "run.cancelled" ||
|
|
event.type === "run.timed_out"
|
|
) {
|
|
return { eventTypes, terminal: event.type, text };
|
|
}
|
|
}
|
|
return { eventTypes, terminal: undefined, text };
|
|
})();
|
|
|
|
const result = await run.wait({ timeoutMs: 180_000 });
|
|
const events = await withTimeout(eventsPromise, 5_000, {
|
|
message: "timed out waiting for live SDK run events",
|
|
});
|
|
|
|
expect(result.status).toBe("completed");
|
|
expect(events.terminal).toBe("run.completed");
|
|
expect(events.eventTypes).toContain("run.started");
|
|
expect(events.text).toContain("OPENCLAW_SDK_LIVE_OK");
|
|
} finally {
|
|
await oc.close();
|
|
}
|
|
}, 240_000);
|
|
});
|