From d86c5775b8a62800e00c286311b5cb54e9739118 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 30 Apr 2026 01:39:49 +0100 Subject: [PATCH] test: cover sdk gateway integration --- packages/sdk/src/index.e2e.test.ts | 82 +++++++++++++++++++- packages/sdk/src/index.test.ts | 2 +- packages/sdk/src/package.e2e.test.ts | 111 +++++++++++++++++++++++++++ 3 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 packages/sdk/src/package.e2e.test.ts diff --git a/packages/sdk/src/index.e2e.test.ts b/packages/sdk/src/index.e2e.test.ts index 534cd6627f5..16a2e4bdda9 100644 --- a/packages/sdk/src/index.e2e.test.ts +++ b/packages/sdk/src/index.e2e.test.ts @@ -1,7 +1,10 @@ import type { AddressInfo } from "node:net"; import net from "node:net"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { WebSocketServer, type RawData, type WebSocket } from "ws"; +import { __testing as agentJobTesting } from "../../../src/gateway/server-methods/agent-job.js"; +import { installGatewayTestHooks, startServer } from "../../../src/gateway/test-helpers.js"; +import { emitAgentEvent, registerAgentRunContext } from "../../../src/infra/agent-events.js"; import { GatewayClientTransport, OpenClaw } from "./index.js"; type JsonObject = Record; @@ -276,3 +279,80 @@ describe("OpenClaw SDK websocket e2e", () => { } }); }); + +describe("OpenClaw SDK real Gateway e2e", () => { + installGatewayTestHooks({ scope: "test" }); + + it("consumes real Gateway events, waits, and calls current model status RPCs", 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(); + + await expect(oc.models.status({ probe: false })).resolves.toMatchObject({ + providers: expect.any(Array), + }); + + registerAgentRunContext(runId, { + sessionKey: "agent:main:dashboard:sdk-real-gateway", + verboseLevel: "off", + }); + + const run = await oc.runs.get(runId); + const waitPromise = run.wait({ timeoutMs: 1_000 }); + await vi.waitFor(() => expect(agentJobTesting.getWaiterCount(runId)).toBeGreaterThan(0)); + + 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 }, + }); + + await expect(waitPromise).resolves.toMatchObject({ + runId, + status: "completed", + startedAt: 111, + endedAt: 222, + }); + + const seen: string[] = []; + const sessionKeys: Array = []; + for await (const event of run.events()) { + seen.push(event.type); + sessionKeys.push(event.sessionKey); + if (event.type === "run.completed") { + break; + } + } + 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(); + } + }); +}); diff --git a/packages/sdk/src/index.test.ts b/packages/sdk/src/index.test.ts index f23b7400327..62aa33480d7 100644 --- a/packages/sdk/src/index.test.ts +++ b/packages/sdk/src/index.test.ts @@ -30,7 +30,7 @@ class FakeTransport implements OpenClawTransport { this.calls.push({ method, params, options }); const response = this.responses[method]; if (typeof response === "function") { - return (await response(params, options, this)) as T; + return (await (response as FakeResponseHandler)(params, options, this)) as T; } return response as T; } diff --git a/packages/sdk/src/package.e2e.test.ts b/packages/sdk/src/package.e2e.test.ts new file mode 100644 index 00000000000..d039e351040 --- /dev/null +++ b/packages/sdk/src/package.e2e.test.ts @@ -0,0 +1,111 @@ +import { spawn } from "node:child_process"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; + +type CommandResult = { + stdout: string; + stderr: string; +}; + +const COMMAND_TIMEOUT_MS = 120_000; +const tempDirs: string[] = []; + +function runCommand( + command: string, + args: string[], + options: { cwd: string; timeoutMs?: number }, +): Promise { + return new Promise((resolve, reject) => { + const stdout: string[] = []; + const stderr: string[] = []; + const child = spawn(command, args, { + cwd: options.cwd, + env: { ...process.env, npm_config_audit: "false", npm_config_fund: "false" }, + stdio: ["ignore", "pipe", "pipe"], + }); + const timer = setTimeout(() => { + child.kill("SIGKILL"); + reject( + new Error( + `command timed out after ${options.timeoutMs ?? COMMAND_TIMEOUT_MS}ms: ${[ + command, + ...args, + ].join(" ")}`, + ), + ); + }, options.timeoutMs ?? COMMAND_TIMEOUT_MS); + child.stdout?.setEncoding("utf8"); + child.stderr?.setEncoding("utf8"); + child.stdout?.on("data", (chunk) => stdout.push(String(chunk))); + child.stderr?.on("data", (chunk) => stderr.push(String(chunk))); + child.once("error", (error) => { + clearTimeout(timer); + reject(error); + }); + child.once("exit", (code, signal) => { + clearTimeout(timer); + const result = { stdout: stdout.join(""), stderr: stderr.join("") }; + if (code === 0) { + resolve(result); + return; + } + reject( + new Error( + `command failed (${String(code ?? signal)}): ${[command, ...args].join(" ")}\n` + + `--- stdout ---\n${result.stdout}\n--- stderr ---\n${result.stderr}`, + ), + ); + }); + }); +} + +describe("OpenClaw SDK package e2e", () => { + afterEach(async () => { + await Promise.all( + tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })), + ); + }); + + it("packs and imports from an external temp consumer", async () => { + const repoRoot = process.cwd(); + const packageRoot = path.join(repoRoot, "packages", "sdk"); + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-sdk-consumer-")); + tempDirs.push(tempDir); + + await runCommand("pnpm", ["--filter", "@openclaw/sdk", "build"], { + cwd: repoRoot, + timeoutMs: 180_000, + }); + await runCommand("pnpm", ["pack", "--pack-destination", tempDir], { + cwd: packageRoot, + }); + + const packedFiles = (await fs.readdir(tempDir)).filter((file) => file.endsWith(".tgz")); + expect(packedFiles).toHaveLength(1); + const tarball = path.join(tempDir, packedFiles[0] ?? ""); + + await fs.writeFile( + path.join(tempDir, "package.json"), + JSON.stringify({ private: true, type: "module" }), + ); + await runCommand("npm", ["install", "--ignore-scripts", "--no-audit", "--no-fund", tarball], { + cwd: tempDir, + }); + + const importScript = ` + import { GatewayClientTransport, OpenClaw, normalizeGatewayEvent } from "@openclaw/sdk"; + if (typeof GatewayClientTransport !== "function") throw new Error("missing transport export"); + if (typeof OpenClaw !== "function") throw new Error("missing client export"); + const event = normalizeGatewayEvent({ + event: "agent", + payload: { runId: "pack-smoke", stream: "lifecycle", data: { phase: "start" } } + }); + if (event.type !== "run.started") throw new Error("unexpected event normalization"); + `; + await runCommand(process.execPath, ["--input-type=module", "-e", importScript], { + cwd: tempDir, + }); + }); +});