mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-17 11:20:37 +09:00
255 lines
6.9 KiB
TypeScript
255 lines
6.9 KiB
TypeScript
|
|
import { afterEach, describe, expect, it } from "vitest";
|
||
|
|
import { createServer } from "node:http";
|
||
|
|
import { WebSocketServer } from "ws";
|
||
|
|
import { execute, testEnvironment } from "@paperclipai/adapter-openclaw-gateway/server";
|
||
|
|
import { parseOpenClawGatewayStdoutLine } from "@paperclipai/adapter-openclaw-gateway/ui";
|
||
|
|
import type { AdapterExecutionContext } from "@paperclipai/adapter-utils";
|
||
|
|
|
||
|
|
function buildContext(
|
||
|
|
config: Record<string, unknown>,
|
||
|
|
overrides?: Partial<AdapterExecutionContext>,
|
||
|
|
): AdapterExecutionContext {
|
||
|
|
return {
|
||
|
|
runId: "run-123",
|
||
|
|
agent: {
|
||
|
|
id: "agent-123",
|
||
|
|
companyId: "company-123",
|
||
|
|
name: "OpenClaw Gateway Agent",
|
||
|
|
adapterType: "openclaw_gateway",
|
||
|
|
adapterConfig: {},
|
||
|
|
},
|
||
|
|
runtime: {
|
||
|
|
sessionId: null,
|
||
|
|
sessionParams: null,
|
||
|
|
sessionDisplayId: null,
|
||
|
|
taskKey: null,
|
||
|
|
},
|
||
|
|
config,
|
||
|
|
context: {
|
||
|
|
taskId: "task-123",
|
||
|
|
issueId: "issue-123",
|
||
|
|
wakeReason: "issue_assigned",
|
||
|
|
issueIds: ["issue-123"],
|
||
|
|
},
|
||
|
|
onLog: async () => {},
|
||
|
|
...overrides,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
async function createMockGatewayServer() {
|
||
|
|
const server = createServer();
|
||
|
|
const wss = new WebSocketServer({ server });
|
||
|
|
|
||
|
|
let agentPayload: Record<string, unknown> | null = null;
|
||
|
|
|
||
|
|
wss.on("connection", (socket) => {
|
||
|
|
socket.send(
|
||
|
|
JSON.stringify({
|
||
|
|
type: "event",
|
||
|
|
event: "connect.challenge",
|
||
|
|
payload: { nonce: "nonce-123" },
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
socket.on("message", (raw) => {
|
||
|
|
const text = Buffer.isBuffer(raw) ? raw.toString("utf8") : String(raw);
|
||
|
|
const frame = JSON.parse(text) as {
|
||
|
|
type: string;
|
||
|
|
id: string;
|
||
|
|
method: string;
|
||
|
|
params?: Record<string, unknown>;
|
||
|
|
};
|
||
|
|
|
||
|
|
if (frame.type !== "req") return;
|
||
|
|
|
||
|
|
if (frame.method === "connect") {
|
||
|
|
socket.send(
|
||
|
|
JSON.stringify({
|
||
|
|
type: "res",
|
||
|
|
id: frame.id,
|
||
|
|
ok: true,
|
||
|
|
payload: {
|
||
|
|
type: "hello-ok",
|
||
|
|
protocol: 3,
|
||
|
|
server: { version: "test", connId: "conn-1" },
|
||
|
|
features: { methods: ["connect", "agent", "agent.wait"], events: ["agent"] },
|
||
|
|
snapshot: { version: 1, ts: Date.now() },
|
||
|
|
policy: { maxPayload: 1_000_000, maxBufferedBytes: 1_000_000, tickIntervalMs: 30_000 },
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (frame.method === "agent") {
|
||
|
|
agentPayload = frame.params ?? null;
|
||
|
|
const runId =
|
||
|
|
typeof frame.params?.idempotencyKey === "string"
|
||
|
|
? frame.params.idempotencyKey
|
||
|
|
: "run-123";
|
||
|
|
|
||
|
|
socket.send(
|
||
|
|
JSON.stringify({
|
||
|
|
type: "res",
|
||
|
|
id: frame.id,
|
||
|
|
ok: true,
|
||
|
|
payload: {
|
||
|
|
runId,
|
||
|
|
status: "accepted",
|
||
|
|
acceptedAt: Date.now(),
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
socket.send(
|
||
|
|
JSON.stringify({
|
||
|
|
type: "event",
|
||
|
|
event: "agent",
|
||
|
|
payload: {
|
||
|
|
runId,
|
||
|
|
seq: 1,
|
||
|
|
stream: "assistant",
|
||
|
|
ts: Date.now(),
|
||
|
|
data: { delta: "cha" },
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
socket.send(
|
||
|
|
JSON.stringify({
|
||
|
|
type: "event",
|
||
|
|
event: "agent",
|
||
|
|
payload: {
|
||
|
|
runId,
|
||
|
|
seq: 2,
|
||
|
|
stream: "assistant",
|
||
|
|
ts: Date.now(),
|
||
|
|
data: { delta: "chacha" },
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (frame.method === "agent.wait") {
|
||
|
|
socket.send(
|
||
|
|
JSON.stringify({
|
||
|
|
type: "res",
|
||
|
|
id: frame.id,
|
||
|
|
ok: true,
|
||
|
|
payload: {
|
||
|
|
runId: frame.params?.runId,
|
||
|
|
status: "ok",
|
||
|
|
startedAt: 1,
|
||
|
|
endedAt: 2,
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
await new Promise<void>((resolve) => {
|
||
|
|
server.listen(0, "127.0.0.1", () => resolve());
|
||
|
|
});
|
||
|
|
|
||
|
|
const address = server.address();
|
||
|
|
if (!address || typeof address === "string") {
|
||
|
|
throw new Error("Failed to resolve test server address");
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
url: `ws://127.0.0.1:${address.port}`,
|
||
|
|
getAgentPayload: () => agentPayload,
|
||
|
|
close: async () => {
|
||
|
|
await new Promise<void>((resolve) => wss.close(() => resolve()));
|
||
|
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||
|
|
},
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
afterEach(() => {
|
||
|
|
// no global mocks
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("openclaw gateway ui stdout parser", () => {
|
||
|
|
it("parses assistant deltas from gateway event lines", () => {
|
||
|
|
const ts = "2026-03-06T15:00:00.000Z";
|
||
|
|
const line =
|
||
|
|
'[openclaw-gateway:event] run=run-1 stream=assistant data={"delta":"hello"}';
|
||
|
|
|
||
|
|
expect(parseOpenClawGatewayStdoutLine(line, ts)).toEqual([
|
||
|
|
{
|
||
|
|
kind: "assistant",
|
||
|
|
ts,
|
||
|
|
text: "hello",
|
||
|
|
delta: true,
|
||
|
|
},
|
||
|
|
]);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("openclaw gateway adapter execute", () => {
|
||
|
|
it("runs connect -> agent -> agent.wait and forwards wake payload", async () => {
|
||
|
|
const gateway = await createMockGatewayServer();
|
||
|
|
const logs: string[] = [];
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await execute(
|
||
|
|
buildContext(
|
||
|
|
{
|
||
|
|
url: gateway.url,
|
||
|
|
headers: {
|
||
|
|
"x-openclaw-token": "gateway-token",
|
||
|
|
},
|
||
|
|
payloadTemplate: {
|
||
|
|
message: "wake now",
|
||
|
|
},
|
||
|
|
waitTimeoutMs: 2000,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
onLog: async (_stream, chunk) => {
|
||
|
|
logs.push(chunk);
|
||
|
|
},
|
||
|
|
},
|
||
|
|
),
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(result.exitCode).toBe(0);
|
||
|
|
expect(result.timedOut).toBe(false);
|
||
|
|
expect(result.summary).toContain("chachacha");
|
||
|
|
expect(result.provider).toBe("openclaw");
|
||
|
|
|
||
|
|
const payload = gateway.getAgentPayload();
|
||
|
|
expect(payload).toBeTruthy();
|
||
|
|
expect(payload?.idempotencyKey).toBe("run-123");
|
||
|
|
expect(payload?.sessionKey).toBe("paperclip");
|
||
|
|
expect(String(payload?.message ?? "")).toContain("wake now");
|
||
|
|
expect(String(payload?.message ?? "")).toContain("PAPERCLIP_RUN_ID=run-123");
|
||
|
|
expect(String(payload?.message ?? "")).toContain("PAPERCLIP_TASK_ID=task-123");
|
||
|
|
|
||
|
|
expect(logs.some((entry) => entry.includes("[openclaw-gateway:event] run=run-123 stream=assistant"))).toBe(true);
|
||
|
|
} finally {
|
||
|
|
await gateway.close();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it("fails fast when url is missing", async () => {
|
||
|
|
const result = await execute(buildContext({}));
|
||
|
|
expect(result.exitCode).toBe(1);
|
||
|
|
expect(result.errorCode).toBe("openclaw_gateway_url_missing");
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("openclaw gateway testEnvironment", () => {
|
||
|
|
it("reports missing url as failure", async () => {
|
||
|
|
const result = await testEnvironment({
|
||
|
|
companyId: "company-123",
|
||
|
|
adapterType: "openclaw_gateway",
|
||
|
|
config: {},
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(result.status).toBe("fail");
|
||
|
|
expect(result.checks.some((check) => check.code === "openclaw_gateway_url_missing")).toBe(true);
|
||
|
|
});
|
||
|
|
});
|