mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 05:52:45 +00:00
test: optimize redundant suites for faster runtime
This commit is contained in:
@@ -2,6 +2,10 @@ import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { setMSTeamsRuntime } from "./runtime.js";
|
import { setMSTeamsRuntime } from "./runtime.js";
|
||||||
|
|
||||||
|
vi.mock("openclaw/plugin-sdk", () => ({
|
||||||
|
isPrivateIpAddress: () => false,
|
||||||
|
}));
|
||||||
|
|
||||||
/** Mock DNS resolver that always returns a public IP (for anti-SSRF validation in tests). */
|
/** Mock DNS resolver that always returns a public IP (for anti-SSRF validation in tests). */
|
||||||
const publicResolveFn = async () => ({ address: "13.107.136.10" });
|
const publicResolveFn = async () => ({ address: "13.107.136.10" });
|
||||||
|
|
||||||
|
|||||||
@@ -234,16 +234,11 @@ describe("trigger handling", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("drops top-level restricted commands for unauthorized senders", async () => {
|
it("enforces top-level command auth but keeps inline text for unauthorized senders", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
for (const command of ["/status", "/whoami"] as const) {
|
for (const command of ["/status", "/whoami"] as const) {
|
||||||
await expectUnauthorizedCommandDropped(home, command);
|
await expectUnauthorizedCommandDropped(home, command);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps inline commands for unauthorized senders", async () => {
|
|
||||||
await withTempHome(async (home) => {
|
|
||||||
for (const command of ["/status", "/help"] as const) {
|
for (const command of ["/status", "/help"] as const) {
|
||||||
const runEmbeddedPiAgentMock = mockEmbeddedOk();
|
const runEmbeddedPiAgentMock = mockEmbeddedOk();
|
||||||
const res = await runInlineUnauthorizedCommand({
|
const res = await runInlineUnauthorizedCommand({
|
||||||
@@ -305,109 +300,115 @@ describe("trigger handling", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects elevated toggles when disabled", async () => {
|
it("enforces elevated toggles across enabled and mention scenarios", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const cfg = makeWhatsAppElevatedCfg(home, { elevatedEnabled: false });
|
const isolateStore = (cfg: ReturnType<typeof makeWhatsAppElevatedCfg>, label: string) => {
|
||||||
|
cfg.session = { ...cfg.session, store: join(home, `${label}.sessions.json`) };
|
||||||
|
return cfg;
|
||||||
|
};
|
||||||
|
|
||||||
const res = await getReplyFromConfig(
|
{
|
||||||
{
|
const cfg = isolateStore(makeWhatsAppElevatedCfg(home, { elevatedEnabled: false }), "off");
|
||||||
Body: "/elevated on",
|
const res = await getReplyFromConfig(
|
||||||
From: "+1000",
|
{
|
||||||
To: "+2000",
|
Body: "/elevated on",
|
||||||
Provider: "whatsapp",
|
From: "+1000",
|
||||||
SenderE164: "+1000",
|
To: "+2000",
|
||||||
},
|
Provider: "whatsapp",
|
||||||
{},
|
SenderE164: "+1000",
|
||||||
cfg,
|
},
|
||||||
);
|
{},
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
cfg,
|
||||||
expect(text).toContain("tools.elevated.enabled");
|
);
|
||||||
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
|
expect(text).toContain("tools.elevated.enabled");
|
||||||
|
|
||||||
const storeRaw = await fs.readFile(requireSessionStorePath(cfg), "utf-8");
|
const storeRaw = await fs.readFile(requireSessionStorePath(cfg), "utf-8");
|
||||||
const store = JSON.parse(storeRaw) as Record<string, { elevatedLevel?: string }>;
|
const store = JSON.parse(storeRaw) as Record<string, { elevatedLevel?: string }>;
|
||||||
expect(store[MAIN_SESSION_KEY]?.elevatedLevel).toBeUndefined();
|
expect(store[MAIN_SESSION_KEY]?.elevatedLevel).toBeUndefined();
|
||||||
});
|
}
|
||||||
});
|
|
||||||
|
|
||||||
it("allows elevated off in groups without mention", async () => {
|
{
|
||||||
await withTempHome(async (home) => {
|
const cfg = isolateStore(
|
||||||
const cfg = makeWhatsAppElevatedCfg(home, { requireMentionInGroups: false });
|
makeWhatsAppElevatedCfg(home, { requireMentionInGroups: false }),
|
||||||
|
"group-off",
|
||||||
|
);
|
||||||
|
const res = await getReplyFromConfig(
|
||||||
|
{
|
||||||
|
Body: "/elevated off",
|
||||||
|
From: "whatsapp:group:123@g.us",
|
||||||
|
To: "whatsapp:+2000",
|
||||||
|
Provider: "whatsapp",
|
||||||
|
SenderE164: "+1000",
|
||||||
|
CommandAuthorized: true,
|
||||||
|
ChatType: "group",
|
||||||
|
WasMentioned: false,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
|
expect(text).toContain("Elevated mode disabled.");
|
||||||
|
const store = await readSessionStore(cfg);
|
||||||
|
expect(store["agent:main:whatsapp:group:123@g.us"]?.elevatedLevel).toBe("off");
|
||||||
|
}
|
||||||
|
|
||||||
const res = await getReplyFromConfig(
|
{
|
||||||
{
|
const cfg = isolateStore(
|
||||||
Body: "/elevated off",
|
makeWhatsAppElevatedCfg(home, { requireMentionInGroups: true }),
|
||||||
From: "whatsapp:group:123@g.us",
|
"group-on",
|
||||||
To: "whatsapp:+2000",
|
);
|
||||||
Provider: "whatsapp",
|
const res = await getReplyFromConfig(
|
||||||
SenderE164: "+1000",
|
{
|
||||||
CommandAuthorized: true,
|
Body: "/elevated on",
|
||||||
ChatType: "group",
|
From: "whatsapp:group:123@g.us",
|
||||||
WasMentioned: false,
|
To: "whatsapp:+2000",
|
||||||
},
|
Provider: "whatsapp",
|
||||||
{},
|
SenderE164: "+1000",
|
||||||
cfg,
|
CommandAuthorized: true,
|
||||||
);
|
ChatType: "group",
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
WasMentioned: true,
|
||||||
expect(text).toContain("Elevated mode disabled.");
|
},
|
||||||
|
{},
|
||||||
|
cfg,
|
||||||
|
);
|
||||||
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
|
expect(text).toContain("Elevated mode set to ask");
|
||||||
|
const store = await readSessionStore(cfg);
|
||||||
|
expect(store["agent:main:whatsapp:group:123@g.us"]?.elevatedLevel).toBe("on");
|
||||||
|
}
|
||||||
|
|
||||||
const store = await readSessionStore(cfg);
|
{
|
||||||
expect(store["agent:main:whatsapp:group:123@g.us"]?.elevatedLevel).toBe("off");
|
const cfg = isolateStore(
|
||||||
});
|
makeWhatsAppElevatedCfg(home, { requireMentionInGroups: false }),
|
||||||
});
|
"group-ignore",
|
||||||
|
);
|
||||||
it("allows elevated directive in groups when mentioned", async () => {
|
const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock();
|
||||||
await withTempHome(async (home) => {
|
runEmbeddedPiAgentMock.mockClear();
|
||||||
const cfg = makeWhatsAppElevatedCfg(home, { requireMentionInGroups: true });
|
runEmbeddedPiAgentMock.mockResolvedValue({
|
||||||
|
payloads: [{ text: "ok" }],
|
||||||
const res = await getReplyFromConfig(
|
meta: {
|
||||||
{
|
durationMs: 1,
|
||||||
Body: "/elevated on",
|
agentMeta: { sessionId: "s", provider: "p", model: "m" },
|
||||||
From: "whatsapp:group:123@g.us",
|
},
|
||||||
To: "whatsapp:+2000",
|
});
|
||||||
Provider: "whatsapp",
|
const res = await getReplyFromConfig(
|
||||||
SenderE164: "+1000",
|
{
|
||||||
CommandAuthorized: true,
|
Body: "/elevated on",
|
||||||
ChatType: "group",
|
From: "whatsapp:group:123@g.us",
|
||||||
WasMentioned: true,
|
To: "whatsapp:+2000",
|
||||||
},
|
Provider: "whatsapp",
|
||||||
{},
|
SenderE164: "+1000",
|
||||||
cfg,
|
ChatType: "group",
|
||||||
);
|
WasMentioned: false,
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
},
|
||||||
expect(text).toContain("Elevated mode set to ask");
|
{},
|
||||||
|
cfg,
|
||||||
const store = await readSessionStore(cfg);
|
);
|
||||||
expect(store["agent:main:whatsapp:group:123@g.us"]?.elevatedLevel).toBe("on");
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
});
|
expect(text).toBeUndefined();
|
||||||
});
|
expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled();
|
||||||
|
}
|
||||||
it("ignores elevated directive in groups when not mentioned", async () => {
|
|
||||||
await withTempHome(async (home) => {
|
|
||||||
getRunEmbeddedPiAgentMock().mockResolvedValue({
|
|
||||||
payloads: [{ text: "ok" }],
|
|
||||||
meta: {
|
|
||||||
durationMs: 1,
|
|
||||||
agentMeta: { sessionId: "s", provider: "p", model: "m" },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const cfg = makeWhatsAppElevatedCfg(home, { requireMentionInGroups: false });
|
|
||||||
|
|
||||||
const res = await getReplyFromConfig(
|
|
||||||
{
|
|
||||||
Body: "/elevated on",
|
|
||||||
From: "whatsapp:group:123@g.us",
|
|
||||||
To: "whatsapp:+2000",
|
|
||||||
Provider: "whatsapp",
|
|
||||||
SenderE164: "+1000",
|
|
||||||
ChatType: "group",
|
|
||||||
WasMentioned: false,
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
cfg,
|
|
||||||
);
|
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
|
||||||
expect(text).toBeUndefined();
|
|
||||||
expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -439,56 +440,57 @@ describe("trigger handling", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses tools.elevated.allowFrom.discord for elevated approval", async () => {
|
it("handles discord elevated allowlist and override behavior", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const cfg = makeCfg(home);
|
{
|
||||||
cfg.tools = { elevated: { allowFrom: { discord: ["123"] } } };
|
const cfg = makeCfg(home);
|
||||||
|
cfg.session = { ...cfg.session, store: join(home, "discord-allow.sessions.json") };
|
||||||
|
cfg.tools = { elevated: { allowFrom: { discord: ["123"] } } };
|
||||||
|
|
||||||
const res = await getReplyFromConfig(
|
const res = await getReplyFromConfig(
|
||||||
{
|
{
|
||||||
Body: "/elevated on",
|
Body: "/elevated on",
|
||||||
From: "discord:123",
|
From: "discord:123",
|
||||||
To: "user:123",
|
To: "user:123",
|
||||||
Provider: "discord",
|
Provider: "discord",
|
||||||
SenderName: "Peter Steinberger",
|
SenderName: "Peter Steinberger",
|
||||||
SenderUsername: "steipete",
|
SenderUsername: "steipete",
|
||||||
SenderTag: "steipete",
|
SenderTag: "steipete",
|
||||||
CommandAuthorized: true,
|
CommandAuthorized: true,
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
cfg,
|
cfg,
|
||||||
);
|
);
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
expect(text).toContain("Elevated mode set to ask");
|
expect(text).toContain("Elevated mode set to ask");
|
||||||
|
const store = await readSessionStore(cfg);
|
||||||
|
expect(store[MAIN_SESSION_KEY]?.elevatedLevel).toBe("on");
|
||||||
|
}
|
||||||
|
|
||||||
const store = await readSessionStore(cfg);
|
{
|
||||||
expect(store[MAIN_SESSION_KEY]?.elevatedLevel).toBe("on");
|
const cfg = makeCfg(home);
|
||||||
});
|
cfg.session = { ...cfg.session, store: join(home, "discord-deny.sessions.json") };
|
||||||
});
|
cfg.tools = {
|
||||||
|
elevated: {
|
||||||
|
allowFrom: { discord: [] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
it("treats explicit discord elevated allowlist as override", async () => {
|
const res = await getReplyFromConfig(
|
||||||
await withTempHome(async (home) => {
|
{
|
||||||
const cfg = makeCfg(home);
|
Body: "/elevated on",
|
||||||
cfg.tools = {
|
From: "discord:123",
|
||||||
elevated: {
|
To: "user:123",
|
||||||
allowFrom: { discord: [] },
|
Provider: "discord",
|
||||||
},
|
SenderName: "steipete",
|
||||||
};
|
},
|
||||||
|
{},
|
||||||
const res = await getReplyFromConfig(
|
cfg,
|
||||||
{
|
);
|
||||||
Body: "/elevated on",
|
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
||||||
From: "discord:123",
|
expect(text).toContain("tools.elevated.allowFrom.discord");
|
||||||
To: "user:123",
|
expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled();
|
||||||
Provider: "discord",
|
}
|
||||||
SenderName: "steipete",
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
cfg,
|
|
||||||
);
|
|
||||||
const text = Array.isArray(res) ? res[0]?.text : res?.text;
|
|
||||||
expect(text).toContain("tools.elevated.allowFrom.discord");
|
|
||||||
expect(getRunEmbeddedPiAgentMock()).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -30,10 +30,6 @@ import {
|
|||||||
} from "../media/input-files.js";
|
} from "../media/input-files.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
import { resolveAssistantStreamDeltaText } from "./agent-event-assistant-text.js";
|
import { resolveAssistantStreamDeltaText } from "./agent-event-assistant-text.js";
|
||||||
import {
|
|
||||||
buildAgentMessageFromConversationEntries,
|
|
||||||
type ConversationEntry,
|
|
||||||
} from "./agent-prompt.js";
|
|
||||||
import type { AuthRateLimiter } from "./auth-rate-limit.js";
|
import type { AuthRateLimiter } from "./auth-rate-limit.js";
|
||||||
import type { ResolvedGatewayAuth } from "./auth.js";
|
import type { ResolvedGatewayAuth } from "./auth.js";
|
||||||
import { sendJson, setSseHeaders, writeDone } from "./http-common.js";
|
import { sendJson, setSseHeaders, writeDone } from "./http-common.js";
|
||||||
@@ -41,14 +37,13 @@ import { handleGatewayPostJsonEndpoint } from "./http-endpoint-helpers.js";
|
|||||||
import { resolveAgentIdForRequest, resolveSessionKey } from "./http-utils.js";
|
import { resolveAgentIdForRequest, resolveSessionKey } from "./http-utils.js";
|
||||||
import {
|
import {
|
||||||
CreateResponseBodySchema,
|
CreateResponseBodySchema,
|
||||||
type ContentPart,
|
|
||||||
type CreateResponseBody,
|
type CreateResponseBody,
|
||||||
type ItemParam,
|
|
||||||
type OutputItem,
|
type OutputItem,
|
||||||
type ResponseResource,
|
type ResponseResource,
|
||||||
type StreamingEvent,
|
type StreamingEvent,
|
||||||
type Usage,
|
type Usage,
|
||||||
} from "./open-responses.schema.js";
|
} from "./open-responses.schema.js";
|
||||||
|
import { buildAgentPrompt } from "./openresponses-prompt.js";
|
||||||
|
|
||||||
type OpenResponsesHttpOptions = {
|
type OpenResponsesHttpOptions = {
|
||||||
auth: ResolvedGatewayAuth;
|
auth: ResolvedGatewayAuth;
|
||||||
@@ -67,24 +62,6 @@ function writeSseEvent(res: ServerResponse, event: StreamingEvent) {
|
|||||||
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractTextContent(content: string | ContentPart[]): string {
|
|
||||||
if (typeof content === "string") {
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
return content
|
|
||||||
.map((part) => {
|
|
||||||
if (part.type === "input_text") {
|
|
||||||
return part.text;
|
|
||||||
}
|
|
||||||
if (part.type === "output_text") {
|
|
||||||
return part.text;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
})
|
|
||||||
.filter(Boolean)
|
|
||||||
.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResolvedResponsesLimits = {
|
type ResolvedResponsesLimits = {
|
||||||
maxBodyBytes: number;
|
maxBodyBytes: number;
|
||||||
maxUrlParts: number;
|
maxUrlParts: number;
|
||||||
@@ -172,52 +149,7 @@ function applyToolChoice(params: {
|
|||||||
return { tools };
|
return { tools };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildAgentPrompt(input: string | ItemParam[]): {
|
export { buildAgentPrompt } from "./openresponses-prompt.js";
|
||||||
message: string;
|
|
||||||
extraSystemPrompt?: string;
|
|
||||||
} {
|
|
||||||
if (typeof input === "string") {
|
|
||||||
return { message: input };
|
|
||||||
}
|
|
||||||
|
|
||||||
const systemParts: string[] = [];
|
|
||||||
const conversationEntries: ConversationEntry[] = [];
|
|
||||||
|
|
||||||
for (const item of input) {
|
|
||||||
if (item.type === "message") {
|
|
||||||
const content = extractTextContent(item.content).trim();
|
|
||||||
if (!content) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.role === "system" || item.role === "developer") {
|
|
||||||
systemParts.push(content);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedRole = item.role === "assistant" ? "assistant" : "user";
|
|
||||||
const sender = normalizedRole === "assistant" ? "Assistant" : "User";
|
|
||||||
|
|
||||||
conversationEntries.push({
|
|
||||||
role: normalizedRole,
|
|
||||||
entry: { sender, body: content },
|
|
||||||
});
|
|
||||||
} else if (item.type === "function_call_output") {
|
|
||||||
conversationEntries.push({
|
|
||||||
role: "tool",
|
|
||||||
entry: { sender: `Tool:${item.call_id}`, body: item.output },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Skip reasoning and item_reference for prompt building (Phase 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = buildAgentMessageFromConversationEntries(conversationEntries);
|
|
||||||
|
|
||||||
return {
|
|
||||||
message,
|
|
||||||
extraSystemPrompt: systemParts.length > 0 ? systemParts.join("\n\n") : undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveOpenResponsesSessionKey(params: {
|
function resolveOpenResponsesSessionKey(params: {
|
||||||
req: IncomingMessage;
|
req: IncomingMessage;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ let InputFileContentPartSchema: typeof import("./open-responses.schema.js").Inpu
|
|||||||
let ToolDefinitionSchema: typeof import("./open-responses.schema.js").ToolDefinitionSchema;
|
let ToolDefinitionSchema: typeof import("./open-responses.schema.js").ToolDefinitionSchema;
|
||||||
let CreateResponseBodySchema: typeof import("./open-responses.schema.js").CreateResponseBodySchema;
|
let CreateResponseBodySchema: typeof import("./open-responses.schema.js").CreateResponseBodySchema;
|
||||||
let OutputItemSchema: typeof import("./open-responses.schema.js").OutputItemSchema;
|
let OutputItemSchema: typeof import("./open-responses.schema.js").OutputItemSchema;
|
||||||
let buildAgentPrompt: typeof import("./openresponses-http.js").buildAgentPrompt;
|
let buildAgentPrompt: typeof import("./openresponses-prompt.js").buildAgentPrompt;
|
||||||
|
|
||||||
describe("OpenResponses Feature Parity", () => {
|
describe("OpenResponses Feature Parity", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
@@ -23,7 +23,7 @@ describe("OpenResponses Feature Parity", () => {
|
|||||||
CreateResponseBodySchema,
|
CreateResponseBodySchema,
|
||||||
OutputItemSchema,
|
OutputItemSchema,
|
||||||
} = await import("./open-responses.schema.js"));
|
} = await import("./open-responses.schema.js"));
|
||||||
({ buildAgentPrompt } = await import("./openresponses-http.js"));
|
({ buildAgentPrompt } = await import("./openresponses-prompt.js"));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Schema Validation", () => {
|
describe("Schema Validation", () => {
|
||||||
|
|||||||
70
src/gateway/openresponses-prompt.ts
Normal file
70
src/gateway/openresponses-prompt.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import {
|
||||||
|
buildAgentMessageFromConversationEntries,
|
||||||
|
type ConversationEntry,
|
||||||
|
} from "./agent-prompt.js";
|
||||||
|
import type { ContentPart, ItemParam } from "./open-responses.schema.js";
|
||||||
|
|
||||||
|
function extractTextContent(content: string | ContentPart[]): string {
|
||||||
|
if (typeof content === "string") {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
return content
|
||||||
|
.map((part) => {
|
||||||
|
if (part.type === "input_text") {
|
||||||
|
return part.text;
|
||||||
|
}
|
||||||
|
if (part.type === "output_text") {
|
||||||
|
return part.text;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAgentPrompt(input: string | ItemParam[]): {
|
||||||
|
message: string;
|
||||||
|
extraSystemPrompt?: string;
|
||||||
|
} {
|
||||||
|
if (typeof input === "string") {
|
||||||
|
return { message: input };
|
||||||
|
}
|
||||||
|
|
||||||
|
const systemParts: string[] = [];
|
||||||
|
const conversationEntries: ConversationEntry[] = [];
|
||||||
|
|
||||||
|
for (const item of input) {
|
||||||
|
if (item.type === "message") {
|
||||||
|
const content = extractTextContent(item.content).trim();
|
||||||
|
if (!content) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.role === "system" || item.role === "developer") {
|
||||||
|
systemParts.push(content);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedRole = item.role === "assistant" ? "assistant" : "user";
|
||||||
|
const sender = normalizedRole === "assistant" ? "Assistant" : "User";
|
||||||
|
|
||||||
|
conversationEntries.push({
|
||||||
|
role: normalizedRole,
|
||||||
|
entry: { sender, body: content },
|
||||||
|
});
|
||||||
|
} else if (item.type === "function_call_output") {
|
||||||
|
conversationEntries.push({
|
||||||
|
role: "tool",
|
||||||
|
entry: { sender: `Tool:${item.call_id}`, body: item.output },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Skip reasoning and item_reference for prompt building (Phase 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = buildAgentMessageFromConversationEntries(conversationEntries);
|
||||||
|
|
||||||
|
return {
|
||||||
|
message,
|
||||||
|
extraSystemPrompt: systemParts.length > 0 ? systemParts.join("\n\n") : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -209,6 +209,65 @@ describe("telegram inbound media", () => {
|
|||||||
|
|
||||||
fetchSpy.mockRestore();
|
fetchSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("captures pin and venue location payload fields", async () => {
|
||||||
|
const { handler, replySpy } = await createBotHandler();
|
||||||
|
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
message: {
|
||||||
|
chat: { id: 42, type: "private" as const },
|
||||||
|
message_id: 5,
|
||||||
|
caption: "Meet here",
|
||||||
|
date: 1736380800,
|
||||||
|
location: {
|
||||||
|
latitude: 48.858844,
|
||||||
|
longitude: 2.294351,
|
||||||
|
horizontal_accuracy: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
assert: (payload: Record<string, unknown>) => {
|
||||||
|
expect(payload.Body).toContain("Meet here");
|
||||||
|
expect(payload.Body).toContain("48.858844");
|
||||||
|
expect(payload.LocationLat).toBe(48.858844);
|
||||||
|
expect(payload.LocationLon).toBe(2.294351);
|
||||||
|
expect(payload.LocationSource).toBe("pin");
|
||||||
|
expect(payload.LocationIsLive).toBe(false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: {
|
||||||
|
chat: { id: 42, type: "private" as const },
|
||||||
|
message_id: 6,
|
||||||
|
date: 1736380800,
|
||||||
|
venue: {
|
||||||
|
title: "Eiffel Tower",
|
||||||
|
address: "Champ de Mars, Paris",
|
||||||
|
location: { latitude: 48.858844, longitude: 2.294351 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
assert: (payload: Record<string, unknown>) => {
|
||||||
|
expect(payload.Body).toContain("Eiffel Tower");
|
||||||
|
expect(payload.LocationName).toBe("Eiffel Tower");
|
||||||
|
expect(payload.LocationAddress).toBe("Champ de Mars, Paris");
|
||||||
|
expect(payload.LocationSource).toBe("place");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
for (const testCase of cases) {
|
||||||
|
replySpy.mockClear();
|
||||||
|
await handler({
|
||||||
|
message: testCase.message,
|
||||||
|
me: { username: "openclaw_bot" },
|
||||||
|
getFile: async () => ({ file_path: "unused" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||||
|
const payload = replySpy.mock.calls[0][0] as Record<string, unknown>;
|
||||||
|
testCase.assert(payload);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("telegram media groups", () => {
|
describe("telegram media groups", () => {
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
import { onSpy } from "./bot.media.e2e-harness.js";
|
|
||||||
|
|
||||||
let handler: (ctx: Record<string, unknown>) => Promise<void>;
|
|
||||||
let replySpy: ReturnType<typeof vi.fn>;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const { createTelegramBot } = await import("./bot.js");
|
|
||||||
const replyModule = await import("../auto-reply/reply.js");
|
|
||||||
replySpy = (replyModule as unknown as { __replySpy: ReturnType<typeof vi.fn> }).__replySpy;
|
|
||||||
|
|
||||||
onSpy.mockClear();
|
|
||||||
createTelegramBot({ token: "tok" });
|
|
||||||
const registeredHandler = onSpy.mock.calls.find((call) => call[0] === "message")?.[1] as (
|
|
||||||
ctx: Record<string, unknown>,
|
|
||||||
) => Promise<void>;
|
|
||||||
expect(registeredHandler).toBeDefined();
|
|
||||||
handler = registeredHandler;
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
replySpy.mockClear();
|
|
||||||
});
|
|
||||||
|
|
||||||
function expectSingleReplyPayload(replySpy: ReturnType<typeof vi.fn>) {
|
|
||||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
|
||||||
return replySpy.mock.calls[0][0] as Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("telegram inbound media", () => {
|
|
||||||
const _INBOUND_MEDIA_TEST_TIMEOUT_MS = process.platform === "win32" ? 30_000 : 20_000;
|
|
||||||
it(
|
|
||||||
"includes location text and ctx fields for pins",
|
|
||||||
async () => {
|
|
||||||
await handler({
|
|
||||||
message: {
|
|
||||||
chat: { id: 42, type: "private" },
|
|
||||||
message_id: 5,
|
|
||||||
caption: "Meet here",
|
|
||||||
date: 1736380800,
|
|
||||||
location: {
|
|
||||||
latitude: 48.858844,
|
|
||||||
longitude: 2.294351,
|
|
||||||
horizontal_accuracy: 12,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
me: { username: "openclaw_bot" },
|
|
||||||
getFile: async () => ({ file_path: "unused" }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const payload = expectSingleReplyPayload(replySpy);
|
|
||||||
expect(payload.Body).toContain("Meet here");
|
|
||||||
expect(payload.Body).toContain("48.858844");
|
|
||||||
expect(payload.LocationLat).toBe(48.858844);
|
|
||||||
expect(payload.LocationLon).toBe(2.294351);
|
|
||||||
expect(payload.LocationSource).toBe("pin");
|
|
||||||
expect(payload.LocationIsLive).toBe(false);
|
|
||||||
},
|
|
||||||
_INBOUND_MEDIA_TEST_TIMEOUT_MS,
|
|
||||||
);
|
|
||||||
|
|
||||||
it(
|
|
||||||
"captures venue fields for named places",
|
|
||||||
async () => {
|
|
||||||
await handler({
|
|
||||||
message: {
|
|
||||||
chat: { id: 42, type: "private" },
|
|
||||||
message_id: 6,
|
|
||||||
date: 1736380800,
|
|
||||||
venue: {
|
|
||||||
title: "Eiffel Tower",
|
|
||||||
address: "Champ de Mars, Paris",
|
|
||||||
location: { latitude: 48.858844, longitude: 2.294351 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
me: { username: "openclaw_bot" },
|
|
||||||
getFile: async () => ({ file_path: "unused" }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const payload = expectSingleReplyPayload(replySpy);
|
|
||||||
expect(payload.Body).toContain("Eiffel Tower");
|
|
||||||
expect(payload.LocationName).toBe("Eiffel Tower");
|
|
||||||
expect(payload.LocationAddress).toBe("Champ de Mars, Paris");
|
|
||||||
expect(payload.LocationSource).toBe("place");
|
|
||||||
},
|
|
||||||
_INBOUND_MEDIA_TEST_TIMEOUT_MS,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user