mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 04:02:18 +01:00
Fixes #5997, fixes #4081, fixes #4723, fixes #6625, fixes #3923 Refs #6606 — this PR removes the rejected root `paperclip` field, but #6606 also requires the protocol v3→v4 bump, which is out of scope here; referencing rather than closing it. ## Thinking Path > - Paperclip is the control plane that wakes and coordinates agent workers across company-scoped execution flows. > - The `openclaw_gateway` adapter is part of that wake path, so its outbound payload contract has to match the gateway's validated `agent` schema. > - `master` currently reintroduces a previously fixed regression by sending a top-level `paperclip` property in `agentParams` (see #3923, which reverts the original fix in #626). > - The gateway rejects unknown root params, which means OpenClaw wakes fail before the remote agent can start work. > - The actual wake context already rides in the generated `message`, so the extra root property is both redundant and harmful. > - This pull request removes that leaked root property, adds a focused regression test around param construction, and updates affected server expectations/docs to the supported contract. > - The benefit is that OpenClaw Gateway agents wake successfully again without losing inline wake context. ## What Changed - Removed the top-level `paperclip` field from OpenClaw Gateway `agentParams` and extracted `buildAgentParams()` so the contract is easy to test. - Added a package-level regression test that proves `payloadTemplate.paperclip` is stripped while explicit `agentId`/`timeout` behavior stays intact. - Updated server tests that inspect OpenClaw Gateway payloads to assert wake data is delivered in `message` instead of a rejected root field. - Updated the adapter configuration docs to state that wake context is embedded in the generated message text, not sent as a top-level param. ### Rebase onto current `master` (conflict resolution) This branch was opened against an older `master`; re-merging current `master` required: - Resolving conflicts in `execute.ts` — `master` hoisted `configuredAgentId` and moved the agentId/timeout precedence inline; this PR keeps the `buildAgentParams()` extraction that strips the gateway-rejected root `paperclip`. - Updating tests `master` added **after** this branch's base that assert the old root-`paperclip` contract. These suites use the OpenClaw gateway adapter purely as a delivery harness (`adapterType: "openclaw_gateway"` + a mock gateway) and observe wake content via the gateway payload, so dropping the root field requires them to read wake context from `message` instead: - `server/src/__tests__/heartbeat-comment-wake-batching.test.ts` - `server/src/__tests__/low-trust-red-team-routes.test.ts` (redaction guarantees preserved — sanitized body + `expectNoCanary` on the raw canary) - Replaced brittle JSON-substring assertions (flagged by Greptile) with a shared `parseWakePayloadFromMessage()` helper + `toMatchObject`, robust to serialization/key-order changes. The strict contract is confirmed upstream: OpenClaw's `AgentParamsSchema` is `Type.Object(..., { additionalProperties: false })` with no `paperclip` field, so a root `paperclip` is rejected (`invalid agent params: at root: unexpected property 'paperclip'`). ## Verification - `pnpm --filter @paperclipai/adapter-openclaw-gateway typecheck` — clean - `pnpm --filter @paperclipai/server typecheck` — clean - `pnpm exec vitest run --project @paperclipai/server server/src/__tests__/openclaw-gateway-adapter.test.ts server/src/__tests__/heartbeat-comment-wake-batching.test.ts server/src/__tests__/low-trust-red-team-routes.test.ts` — 26 passed (7 + 11 + 8) - `packages/adapters/openclaw-gateway/src/server/execute.test.ts` — 6 passed (run via a local temp vitest config because the root `vitest.config.ts` does not include this package) ## Risks - Low risk: this narrows the outbound payload to the gateway-supported contract and keeps wake context in the already-supported `message` channel. - Any downstream consumer that incorrectly depended on a top-level `paperclip` field from the gateway mock payloads would need to follow the supported `message` contract instead. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex CLI coding agent via API authored the original change; exact underlying model ID and context window were not exposed in that environment. - Rebase/conflict resolution and the test-assertion migration were done with Claude Code (Claude Opus 4.8, 1M context). ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked the related issues above - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [ ] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: serenakeyitan via breeze-runner <serenakeyitan@users.noreply.github.com> Co-authored-by: Andrew Aymeloglu <aaymeloglu@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
674 lines
19 KiB
TypeScript
674 lines
19 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 {
|
|
buildOpenClawGatewayConfig,
|
|
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(options?: {
|
|
waitPayload?: Record<string, unknown>;
|
|
}) {
|
|
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: options?.waitPayload ?? {
|
|
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()));
|
|
},
|
|
};
|
|
}
|
|
|
|
async function createMockGatewayServerWithPairing() {
|
|
const server = createServer();
|
|
const wss = new WebSocketServer({ server });
|
|
|
|
let agentPayload: Record<string, unknown> | null = null;
|
|
let approved = false;
|
|
let pendingRequestId = "req-1";
|
|
let lastSeenDeviceId: string | 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") {
|
|
const device = frame.params?.device as Record<string, unknown> | undefined;
|
|
const deviceId = typeof device?.id === "string" ? device.id : null;
|
|
if (deviceId) {
|
|
lastSeenDeviceId = deviceId;
|
|
}
|
|
|
|
if (deviceId && !approved) {
|
|
socket.send(
|
|
JSON.stringify({
|
|
type: "res",
|
|
id: frame.id,
|
|
ok: false,
|
|
error: {
|
|
code: "NOT_PAIRED",
|
|
message: "pairing required",
|
|
details: {
|
|
code: "PAIRING_REQUIRED",
|
|
requestId: pendingRequestId,
|
|
reason: "not-paired",
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
socket.close(1008, "pairing required");
|
|
return;
|
|
}
|
|
|
|
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", "device.pair.list", "device.pair.approve"],
|
|
events: ["agent"],
|
|
},
|
|
snapshot: { version: 1, ts: Date.now() },
|
|
policy: { maxPayload: 1_000_000, maxBufferedBytes: 1_000_000, tickIntervalMs: 30_000 },
|
|
},
|
|
}),
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (frame.method === "device.pair.list") {
|
|
socket.send(
|
|
JSON.stringify({
|
|
type: "res",
|
|
id: frame.id,
|
|
ok: true,
|
|
payload: {
|
|
pending: approved
|
|
? []
|
|
: [
|
|
{
|
|
requestId: pendingRequestId,
|
|
deviceId: lastSeenDeviceId ?? "device-unknown",
|
|
},
|
|
],
|
|
paired: approved && lastSeenDeviceId ? [{ deviceId: lastSeenDeviceId }] : [],
|
|
},
|
|
}),
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (frame.method === "device.pair.approve") {
|
|
const requestId = frame.params?.requestId;
|
|
if (requestId !== pendingRequestId) {
|
|
socket.send(
|
|
JSON.stringify({
|
|
type: "res",
|
|
id: frame.id,
|
|
ok: false,
|
|
error: { code: "INVALID_REQUEST", message: "unknown requestId" },
|
|
}),
|
|
);
|
|
return;
|
|
}
|
|
approved = true;
|
|
socket.send(
|
|
JSON.stringify({
|
|
type: "res",
|
|
id: frame.id,
|
|
ok: true,
|
|
payload: {
|
|
requestId: pendingRequestId,
|
|
device: {
|
|
deviceId: lastSeenDeviceId ?? "device-unknown",
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
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: "ok" },
|
|
},
|
|
}),
|
|
);
|
|
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);
|
|
},
|
|
context: {
|
|
taskId: "task-123",
|
|
issueId: "issue-123",
|
|
wakeReason: "issue_assigned",
|
|
issueIds: ["issue-123"],
|
|
paperclipWorkspace: {
|
|
cwd: "/tmp/worktrees/pap-123",
|
|
strategy: "git_worktree",
|
|
branchName: "pap-123-test",
|
|
},
|
|
paperclipWorkspaces: [
|
|
{
|
|
id: "workspace-1",
|
|
cwd: "/tmp/project",
|
|
},
|
|
],
|
|
paperclipRuntimeServiceIntents: [
|
|
{
|
|
name: "preview",
|
|
lifecycle: "ephemeral",
|
|
},
|
|
],
|
|
paperclipWake: {
|
|
reason: "issue_commented",
|
|
issue: {
|
|
id: "issue-123",
|
|
identifier: "PAP-874",
|
|
title: "chat-speed issues",
|
|
status: "in_progress",
|
|
priority: "medium",
|
|
},
|
|
commentIds: ["comment-1", "comment-2"],
|
|
latestCommentId: "comment-2",
|
|
comments: [
|
|
{
|
|
id: "comment-1",
|
|
issueId: "issue-123",
|
|
body: "First comment",
|
|
bodyTruncated: false,
|
|
createdAt: "2026-03-28T14:35:00.000Z",
|
|
author: { type: "user", id: "user-1" },
|
|
},
|
|
{
|
|
id: "comment-2",
|
|
issueId: "issue-123",
|
|
body: "Second comment",
|
|
bodyTruncated: false,
|
|
createdAt: "2026-03-28T14:35:10.000Z",
|
|
author: { type: "user", id: "user-1" },
|
|
},
|
|
],
|
|
commentWindow: {
|
|
requestedCount: 2,
|
|
includedCount: 2,
|
|
missingCount: 0,
|
|
},
|
|
truncated: false,
|
|
fallbackFetchNeeded: false,
|
|
},
|
|
},
|
|
},
|
|
),
|
|
);
|
|
|
|
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:issue:issue-123");
|
|
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(String(payload?.message ?? "")).toContain("## Paperclip Wake Payload");
|
|
expect(String(payload?.message ?? "")).toContain(
|
|
"Treat this wake payload as the highest-priority change for the current heartbeat.",
|
|
);
|
|
expect(String(payload?.message ?? "")).toContain(
|
|
"Do not switch to another issue until you have handled this wake.",
|
|
);
|
|
expect(String(payload?.message ?? "")).toContain("First comment");
|
|
expect(String(payload?.message ?? "")).toContain("\"commentIds\":[\"comment-1\",\"comment-2\"]");
|
|
expect(payload?.paperclip).toBeUndefined();
|
|
expect(String(payload?.message ?? "")).toContain("\"latestCommentId\":\"comment-2\"");
|
|
|
|
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");
|
|
});
|
|
|
|
it("returns adapter-managed runtime services from gateway result meta", async () => {
|
|
const gateway = await createMockGatewayServer({
|
|
waitPayload: {
|
|
runId: "run-123",
|
|
status: "ok",
|
|
startedAt: 1,
|
|
endedAt: 2,
|
|
meta: {
|
|
runtimeServices: [
|
|
{
|
|
name: "preview",
|
|
scopeType: "run",
|
|
url: "https://preview.example/run-123",
|
|
providerRef: "sandbox-123",
|
|
lifecycle: "ephemeral",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
});
|
|
|
|
try {
|
|
const result = await execute(
|
|
buildContext({
|
|
url: gateway.url,
|
|
headers: {
|
|
"x-openclaw-token": "gateway-token",
|
|
},
|
|
waitTimeoutMs: 2000,
|
|
}),
|
|
);
|
|
|
|
expect(result.exitCode).toBe(0);
|
|
expect(result.runtimeServices).toEqual([
|
|
expect.objectContaining({
|
|
serviceName: "preview",
|
|
scopeType: "run",
|
|
url: "https://preview.example/run-123",
|
|
providerRef: "sandbox-123",
|
|
lifecycle: "ephemeral",
|
|
status: "running",
|
|
}),
|
|
]);
|
|
} finally {
|
|
await gateway.close();
|
|
}
|
|
});
|
|
|
|
it("auto-approves pairing once and retries the run", async () => {
|
|
const gateway = await createMockGatewayServerWithPairing();
|
|
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.summary).toContain("ok");
|
|
expect(logs.some((entry) => entry.includes("pairing required; attempting automatic pairing approval"))).toBe(
|
|
true,
|
|
);
|
|
expect(logs.some((entry) => entry.includes("auto-approved pairing request"))).toBe(true);
|
|
expect(gateway.getAgentPayload()).toBeTruthy();
|
|
} finally {
|
|
await gateway.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("openclaw gateway ui build config", () => {
|
|
it("parses payload template and runtime services json", () => {
|
|
const config = buildOpenClawGatewayConfig({
|
|
adapterType: "openclaw_gateway",
|
|
cwd: "",
|
|
promptTemplate: "",
|
|
model: "",
|
|
thinkingEffort: "",
|
|
chrome: false,
|
|
dangerouslySkipPermissions: false,
|
|
search: false,
|
|
dangerouslyBypassSandbox: false,
|
|
command: "",
|
|
args: "",
|
|
extraArgs: "",
|
|
envVars: "",
|
|
envBindings: {},
|
|
url: "wss://gateway.example/ws",
|
|
payloadTemplateJson: JSON.stringify({
|
|
agentId: "remote-agent-123",
|
|
metadata: { team: "platform" },
|
|
}),
|
|
runtimeServicesJson: JSON.stringify({
|
|
services: [
|
|
{
|
|
name: "preview",
|
|
lifecycle: "shared",
|
|
},
|
|
],
|
|
}),
|
|
bootstrapPrompt: "",
|
|
maxTurnsPerRun: 0,
|
|
heartbeatEnabled: true,
|
|
intervalSec: 300,
|
|
});
|
|
|
|
expect(config).toEqual(
|
|
expect.objectContaining({
|
|
url: "wss://gateway.example/ws",
|
|
payloadTemplate: {
|
|
agentId: "remote-agent-123",
|
|
metadata: { team: "platform" },
|
|
},
|
|
workspaceRuntime: {
|
|
services: [
|
|
{
|
|
name: "preview",
|
|
lifecycle: "shared",
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|