import fs from "node:fs/promises" ;
import path from "node:path" ;
import { afterAll, beforeAll, beforeEach, describe, expect, it, type Mock } from "vitest" ;
import { resolveSessionTranscriptPath } from "../config/sessions.js" ;
import { emitAgentEvent } from "../infra/agent-events.js" ;
import { captureEnv } from "../test-utils/env.js" ;
import {
agentCommand,
getFreePort,
installGatewayTestHooks,
startGatewayServer,
testState,
} from "./test-helpers.js" ;
const { createOpenClawTools } = await import ("../agents/openclaw-tools.js" );
installGatewayTestHooks({ scope: "suite" });
let server: Awaited<ReturnType<typeof startGatewayServer>>;
let gatewayPort: number;
const gatewayToken = "test-gateway-token-1234567890" ;
let envSnapshot: ReturnType<typeof captureEnv>;
type SessionSendTool = ReturnType<typeof createOpenClawTools>[number];
const SESSION_SEND_E2E_TIMEOUT_MS = 10 _000 ;
let cachedSessionsSendTool: SessionSendTool | null = null ;
function getSessionsSendTool(): SessionSendTool {
if (cachedSessionsSendTool) {
return cachedSessionsSendTool;
}
const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_send" );
if (!tool) {
throw new Error("missing sessions_send tool" );
}
cachedSessionsSendTool = tool;
return cachedSessionsSendTool;
}
async function emitLifecycleAssistantReply(params: {
opts: unknown;
defaultSessionId: string;
includeTimestamp?: boolean ;
resolveText: (extraSystemPrompt?: string) => string;
}) {
const commandParams = params.opts as {
sessionId?: string;
runId?: string;
extraSystemPrompt?: string;
};
const sessionId = commandParams.sessionId ?? params.defaultSessionId;
const runId = commandParams.runId ?? sessionId;
const sessionFile = resolveSessionTranscriptPath(sessionId);
await fs.mkdir(path.dirname(sessionFile), { recursive: true });
const startedAt = Date.now();
emitAgentEvent({
runId,
stream: "lifecycle" ,
data: { phase: "start" , startedAt },
});
const text = params.resolveText(commandParams.extraSystemPrompt);
const message = {
role: "assistant" ,
content: [{ type: "text" , text }],
...(params.includeTimestamp ? { timestamp: Date.now() } : {}),
};
await fs.appendFile(sessionFile, `${JSON.stringify({ message })}\n`, "utf8" );
emitAgentEvent({
runId,
stream: "lifecycle" ,
data: { phase: "end" , startedAt, endedAt: Date.now() },
});
}
beforeAll(async () => {
envSnapshot = captureEnv(["OPENCLAW_GATEWAY_PORT" , "OPENCLAW_GATEWAY_TOKEN" ]);
gatewayPort = await getFreePort();
const { approveDevicePairing, requestDevicePairing } = await import ("../infra/device-pairing.js" );
const { loadOrCreateDeviceIdentity, publicKeyRawBase64UrlFromPem } =
await import ("../infra/device-identity.js" );
const identity = loadOrCreateDeviceIdentity();
const pending = await requestDevicePairing({
deviceId: identity.deviceId,
publicKey: publicKeyRawBase64UrlFromPem(identity.publicKeyPem),
clientId: "openclaw-cli" ,
clientMode: "cli" ,
role: "operator" ,
scopes: ["operator.admin" , "operator.read" , "operator.write" , "operator.approvals" ],
silent: false ,
});
await approveDevicePairing(pending.request.requestId, {
callerScopes: pending.request.scopes ?? ["operator.admin" ],
});
testState.gatewayAuth = { mode: "token" , token: gatewayToken };
process.env.OPENCLAW_GATEWAY_PORT = String(gatewayPort);
process.env.OPENCLAW_GATEWAY_TOKEN = gatewayToken;
server = await startGatewayServer(gatewayPort);
});
beforeEach(() => {
testState.gatewayAuth = { mode: "token" , token: gatewayToken };
process.env.OPENCLAW_GATEWAY_PORT = String(gatewayPort);
process.env.OPENCLAW_GATEWAY_TOKEN = gatewayToken;
});
afterAll(async () => {
await server.close();
envSnapshot.restore();
});
describe("sessions_send gateway loopback" , () => {
it("returns reply when lifecycle ends before agent.wait" , async () => {
const spy = agentCommand as unknown as Mock<(opts: unknown) => Promise<void >>;
spy.mockImplementation(async (opts: unknown) =>
emitLifecycleAssistantReply({
opts,
defaultSessionId: "main" ,
includeTimestamp: true ,
resolveText: (extraSystemPrompt) => {
if (extraSystemPrompt?.includes("Agent-to-agent reply step" )) {
return "REPLY_SKIP" ;
}
if (extraSystemPrompt?.includes("Agent-to-agent announce step" )) {
return "ANNOUNCE_SKIP" ;
}
return "pong" ;
},
}),
);
const tool = getSessionsSendTool();
const result = await tool.execute("call-loopback" , {
sessionKey: "main" ,
message: "ping" ,
timeoutSeconds: 5 ,
});
const details = result.details as {
status?: string;
reply?: string;
sessionKey?: string;
};
expect(details.status).toBe("ok" );
expect(details.reply).toBe("pong" );
expect(details.sessionKey).toBe("main" );
const firstCall = spy.mock.calls[0 ]?.[0 ] as
| { lane?: string; inputProvenance?: { kind?: string; sourceTool?: string } }
| undefined;
expect(firstCall?.lane).toMatch(/^nested(?::|$)/);
expect(firstCall?.inputProvenance).toMatchObject({
kind: "inter_session" ,
sourceTool: "sessions_send" ,
});
});
});
describe("sessions_send label lookup" , () => {
it(
"finds session by label and sends message" ,
{ timeout: SESSION_SEND_E2E_TIMEOUT_MS },
async () => {
// This is an operator feature; enable broader session tool targeting for this test.
const configPath = process.env.OPENCLAW_CONFIG_PATH;
if (!configPath) {
throw new Error("OPENCLAW_CONFIG_PATH missing in gateway test environment" );
}
await fs.mkdir(path.dirname(configPath), { recursive: true });
await fs.writeFile(
configPath,
JSON.stringify({ tools: { sessions: { visibility: "all" } } }, null , 2 ) + "\n" ,
"utf-8" ,
);
const spy = agentCommand as unknown as Mock<(opts: unknown) => Promise<void >>;
spy.mockImplementation(async (opts: unknown) =>
emitLifecycleAssistantReply({
opts,
defaultSessionId: "test-labeled" ,
resolveText: () => "labeled response" ,
}),
);
// First, create a session with a label via sessions.patch
const { callGateway } = await import ("./call.js" );
await callGateway({
method: "sessions.patch" ,
params: { key: "test-labeled-session" , label: "my-test-worker" },
timeoutMs: 5000 ,
});
const tool = createOpenClawTools({
config: {
tools: {
sessions: {
visibility: "all" ,
},
},
},
}).find((candidate) => candidate.name === "sessions_send" );
if (!tool) {
throw new Error("missing sessions_send tool" );
}
// Send using label instead of sessionKey
const result = await tool.execute("call-by-label" , {
label: "my-test-worker" ,
message: "hello labeled session" ,
timeoutSeconds: 5 ,
});
const details = result.details as {
status?: string;
reply?: string;
sessionKey?: string;
};
expect(details.status).toBe("ok" );
expect(details.reply).toBe("labeled response" );
expect(details.sessionKey).toBe("agent:main:test-labeled-session" );
},
);
});
Messung V0.5 in Prozent C=99 H=100 G=99
¤ Dauer der Verarbeitung: 0.16 Sekunden
(vorverarbeitet am 2026-06-07)
¤
*© Formatika GbR, Deutschland