mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:50:42 +00:00
test: cover app sdk gateway surfaces
This commit is contained in:
@@ -7,6 +7,16 @@ import { emitAgentEvent, registerAgentRunContext } from "../../../src/infra/agen
|
||||
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[] = [];
|
||||
|
||||
@@ -37,13 +47,17 @@ async function reservePort(): Promise<number> {
|
||||
return port;
|
||||
}
|
||||
|
||||
async function createFakeGateway(port = 0): Promise<{ url: string; close: () => Promise<void> }> {
|
||||
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[] = [];
|
||||
const sockets = new Set<WebSocket>();
|
||||
|
||||
server.on("connection", (socket) => {
|
||||
sockets.add(socket);
|
||||
socket.once("close", () => sockets.delete(socket));
|
||||
sendJson(socket, {
|
||||
type: "event",
|
||||
event: "connect.challenge",
|
||||
@@ -52,56 +66,84 @@ async function createFakeGateway(port = 0): Promise<{ url: string; close: () =>
|
||||
});
|
||||
|
||||
socket.on("message", (raw) => {
|
||||
const frame = JSON.parse(readRawMessage(raw)) as {
|
||||
id: string;
|
||||
method: string;
|
||||
params?: unknown;
|
||||
const frame = JSON.parse(readRawMessage(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") {
|
||||
sendJson(socket, {
|
||||
type: "res",
|
||||
id: frame.id,
|
||||
ok: true,
|
||||
payload: {
|
||||
type: "hello-ok",
|
||||
protocol: 1,
|
||||
server: { version: "sdk-e2e", connId: "conn-sdk-e2e" },
|
||||
features: {
|
||||
methods: [
|
||||
"agent",
|
||||
"agent.wait",
|
||||
"connect",
|
||||
"sessions.abort",
|
||||
"sessions.create",
|
||||
"sessions.send",
|
||||
],
|
||||
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,
|
||||
},
|
||||
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",
|
||||
"tools.catalog",
|
||||
"tools.effective",
|
||||
],
|
||||
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;
|
||||
sendJson(socket, {
|
||||
type: "res",
|
||||
id: frame.id,
|
||||
ok: true,
|
||||
payload: { status: "accepted", runId: "run-sdk-e2e", sessionKey: params?.sessionKey },
|
||||
reply({
|
||||
status: "accepted",
|
||||
runId: "run-sdk-e2e",
|
||||
sessionKey: params?.sessionKey,
|
||||
});
|
||||
setTimeout(() => {
|
||||
sendJson(socket, {
|
||||
@@ -145,43 +187,104 @@ async function createFakeGateway(port = 0): Promise<{ url: string; close: () =>
|
||||
}
|
||||
|
||||
if (frame.method === "agent.wait") {
|
||||
sendJson(socket, {
|
||||
type: "res",
|
||||
id: frame.id,
|
||||
ok: true,
|
||||
payload: {
|
||||
status: "ok",
|
||||
runId: "run-sdk-e2e",
|
||||
sessionKey: "main",
|
||||
startedAt: 123,
|
||||
endedAt: 456,
|
||||
},
|
||||
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") {
|
||||
sendJson(socket, {
|
||||
type: "res",
|
||||
id: frame.id,
|
||||
reply({
|
||||
ok: true,
|
||||
payload: {
|
||||
ok: true,
|
||||
abortedRunId: "run-sdk-e2e",
|
||||
status: "aborted",
|
||||
},
|
||||
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 === "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 === "exec.approval.list") {
|
||||
reply({ approvals: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (frame.method === "exec.approval.resolve") {
|
||||
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 sockets) {
|
||||
socket.terminate();
|
||||
}
|
||||
sockets.clear();
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
});
|
||||
@@ -195,6 +298,9 @@ describe("OpenClaw SDK websocket e2e", () => {
|
||||
servers.splice(0).map(
|
||||
(server) =>
|
||||
new Promise<void>((resolve) => {
|
||||
for (const client of server.clients) {
|
||||
client.terminate();
|
||||
}
|
||||
server.close(() => resolve());
|
||||
}),
|
||||
),
|
||||
@@ -256,6 +362,90 @@ describe("OpenClaw SDK websocket e2e", () => {
|
||||
}
|
||||
});
|
||||
|
||||
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 {
|
||||
await expect(oc.agents.list()).resolves.toMatchObject({ agents: [{ id: "main" }] });
|
||||
const agent = await oc.agents.get("main");
|
||||
await expect(agent.identity({ sessionKey: "sdk-session" })).resolves.toMatchObject({
|
||||
agentId: "main",
|
||||
sessionKey: "sdk-session",
|
||||
});
|
||||
await expect(oc.agents.create({ id: "sdk-agent" })).resolves.toMatchObject({
|
||||
method: "agents.create",
|
||||
});
|
||||
await expect(
|
||||
oc.agents.update({ id: "sdk-agent", label: "SDK Agent" }),
|
||||
).resolves.toMatchObject({ method: "agents.update" });
|
||||
await expect(oc.agents.delete({ id: "sdk-agent" })).resolves.toMatchObject({
|
||||
method: "agents.delete",
|
||||
});
|
||||
|
||||
await expect(oc.sessions.list()).resolves.toMatchObject({
|
||||
sessions: [{ key: "sdk-session" }],
|
||||
});
|
||||
const session = await oc.sessions.create({ key: "sdk-session", agentId: "main" });
|
||||
expect(session.key).toBe("sdk-session");
|
||||
await expect(oc.sessions.resolve({ key: "sdk-session" })).resolves.toMatchObject({
|
||||
key: "sdk-session",
|
||||
});
|
||||
const sessionRun = await session.send("continue");
|
||||
expect(sessionRun.id).toBe("run-session-e2e");
|
||||
await expect(session.abort(sessionRun.id)).resolves.toMatchObject({
|
||||
abortedRunId: "run-session-e2e",
|
||||
});
|
||||
await expect(session.patch({ label: "Renamed" })).resolves.toMatchObject({
|
||||
method: "sessions.patch",
|
||||
});
|
||||
await expect(session.compact({ maxLines: 200 })).resolves.toMatchObject({
|
||||
method: "sessions.compact",
|
||||
});
|
||||
|
||||
await expect(oc.models.list()).resolves.toMatchObject({ models: [{ id: "gpt-5.4" }] });
|
||||
await expect(oc.models.status({ probe: false })).resolves.toMatchObject({ providers: [] });
|
||||
await expect(oc.tools.list()).resolves.toMatchObject({ tools: [{ name: "shell" }] });
|
||||
await expect(oc.tools.effective({ sessionKey: "sdk-session" })).resolves.toMatchObject({
|
||||
tools: [{ name: "shell", enabled: true }],
|
||||
});
|
||||
await expect(oc.approvals.list()).resolves.toMatchObject({ approvals: [] });
|
||||
await expect(
|
||||
oc.approvals.respond("approval-1", { decision: "approve" }),
|
||||
).resolves.toMatchObject({ ok: true });
|
||||
|
||||
expect(gateway.requests.map((request) => request.method)).toEqual([
|
||||
"connect",
|
||||
"agents.list",
|
||||
"agent.identity.get",
|
||||
"agents.create",
|
||||
"agents.update",
|
||||
"agents.delete",
|
||||
"sessions.list",
|
||||
"sessions.create",
|
||||
"sessions.resolve",
|
||||
"sessions.send",
|
||||
"sessions.abort",
|
||||
"sessions.patch",
|
||||
"sessions.compact",
|
||||
"models.list",
|
||||
"models.authStatus",
|
||||
"tools.catalog",
|
||||
"tools.effective",
|
||||
"exec.approval.list",
|
||||
"exec.approval.resolve",
|
||||
]);
|
||||
} 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}`;
|
||||
@@ -349,3 +539,81 @@ describe("OpenClaw SDK real Gateway e2e", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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 "";
|
||||
}
|
||||
|
||||
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();
|
||||
await expect(oc.agents.list()).resolves.toBeDefined();
|
||||
await expect(oc.models.status({ probe: false })).resolves.toBeDefined();
|
||||
|
||||
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 Promise.race([
|
||||
eventsPromise,
|
||||
new Promise<never>((_resolve, reject) => {
|
||||
setTimeout(() => reject(new Error("timed out waiting for live SDK run events")), 5_000);
|
||||
}),
|
||||
]);
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -255,9 +255,24 @@ describe("OpenClaw SDK", () => {
|
||||
await expect(oc.artifacts.list()).rejects.toThrow(
|
||||
"oc.artifacts.list is not supported by the current OpenClaw Gateway yet",
|
||||
);
|
||||
await expect(oc.artifacts.get("artifact_123")).rejects.toThrow(
|
||||
"oc.artifacts.get is not supported by the current OpenClaw Gateway yet",
|
||||
);
|
||||
await expect(oc.artifacts.download("artifact_123")).rejects.toThrow(
|
||||
"oc.artifacts.download is not supported by the current OpenClaw Gateway yet",
|
||||
);
|
||||
await expect(oc.environments.list()).rejects.toThrow(
|
||||
"oc.environments.list is not supported by the current OpenClaw Gateway yet",
|
||||
);
|
||||
await expect(oc.environments.create({ provider: "testbox" })).rejects.toThrow(
|
||||
"oc.environments.create is not supported by the current OpenClaw Gateway yet",
|
||||
);
|
||||
await expect(oc.environments.status("environment_123")).rejects.toThrow(
|
||||
"oc.environments.status is not supported by the current OpenClaw Gateway yet",
|
||||
);
|
||||
await expect(oc.environments.delete("environment_123")).rejects.toThrow(
|
||||
"oc.environments.delete is not supported by the current OpenClaw Gateway yet",
|
||||
);
|
||||
expect(transport.calls).toEqual([]);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user