Files
Moltbot/src/gateway/server.sessions-send.e2e.test.ts
2026-02-17 14:31:02 +09:00

220 lines
7.2 KiB
TypeScript

import fs from "node:fs/promises";
import path from "node:path";
import { afterAll, beforeAll, 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-token";
let envSnapshot: ReturnType<typeof captureEnv>;
beforeAll(async () => {
envSnapshot = captureEnv(["OPENCLAW_GATEWAY_PORT", "OPENCLAW_GATEWAY_TOKEN"]);
gatewayPort = await getFreePort();
testState.gatewayAuth = { mode: "token", token: gatewayToken };
process.env.OPENCLAW_GATEWAY_PORT = String(gatewayPort);
process.env.OPENCLAW_GATEWAY_TOKEN = gatewayToken;
server = await startGatewayServer(gatewayPort);
});
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) => {
const params = opts as {
sessionId?: string;
runId?: string;
extraSystemPrompt?: string;
};
const sessionId = params.sessionId ?? "main";
const runId = params.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 },
});
let text = "pong";
if (params.extraSystemPrompt?.includes("Agent-to-agent reply step")) {
text = "REPLY_SKIP";
} else if (params.extraSystemPrompt?.includes("Agent-to-agent announce step")) {
text = "ANNOUNCE_SKIP";
}
const message = {
role: "assistant",
content: [{ type: "text", text }],
timestamp: Date.now(),
};
await fs.appendFile(sessionFile, `${JSON.stringify({ message })}\n`, "utf8");
emitAgentEvent({
runId,
stream: "lifecycle",
data: {
phase: "end",
startedAt,
endedAt: Date.now(),
},
});
});
const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_send");
if (!tool) {
throw new Error("missing sessions_send tool");
}
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).toBe("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: 60_000 }, 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) => {
const params = opts as {
sessionId?: string;
runId?: string;
extraSystemPrompt?: string;
};
const sessionId = params.sessionId ?? "test-labeled";
const runId = params.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 = "labeled response";
const message = {
role: "assistant",
content: [{ type: "text", text }],
};
await fs.appendFile(sessionFile, `${JSON.stringify({ message })}\n`, "utf8");
emitAgentEvent({
runId,
stream: "lifecycle",
data: { phase: "end", startedAt, endedAt: Date.now() },
});
});
// 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().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");
});
it("returns error when label not found", { timeout: 60_000 }, async () => {
const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_send");
if (!tool) {
throw new Error("missing sessions_send tool");
}
const result = await tool.execute("call-missing-label", {
label: "nonexistent-label",
message: "hello",
timeoutSeconds: 5,
});
const details = result.details as { status?: string; error?: string };
expect(details.status).toBe("error");
expect(details.error).toContain("No session found with label");
});
it("returns error when neither sessionKey nor label provided", { timeout: 60_000 }, async () => {
const tool = createOpenClawTools().find((candidate) => candidate.name === "sessions_send");
if (!tool) {
throw new Error("missing sessions_send tool");
}
const result = await tool.execute("call-no-key", {
message: "hello",
timeoutSeconds: 5,
});
const details = result.details as { status?: string; error?: string };
expect(details.status).toBe("error");
expect(details.error).toContain("Either sessionKey or label is required");
});
});