mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-29 04:33:36 +00:00
251 lines
7.3 KiB
TypeScript
251 lines
7.3 KiB
TypeScript
// Codex tests cover user input bridge plugin behavior.
|
|
import type { EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { createCodexUserInputBridge } from "./user-input-bridge.js";
|
|
|
|
function createParams(): EmbeddedRunAttemptParams {
|
|
return {
|
|
sessionId: "session-1",
|
|
sessionKey: "agent:main:session-1",
|
|
onBlockReply: vi.fn(),
|
|
} as unknown as EmbeddedRunAttemptParams;
|
|
}
|
|
|
|
function expectFirstBlockReplyText(params: EmbeddedRunAttemptParams): string {
|
|
const onBlockReply = params.onBlockReply;
|
|
if (onBlockReply === undefined) {
|
|
throw new Error("Expected onBlockReply callback");
|
|
}
|
|
const payload = vi.mocked(onBlockReply).mock.calls[0]?.[0];
|
|
if (typeof payload?.text !== "string") {
|
|
throw new Error("Expected first block reply text");
|
|
}
|
|
return payload.text;
|
|
}
|
|
|
|
describe("Codex app-server user input bridge", () => {
|
|
it("prompts the originating chat and resolves request_user_input from the next queued message", async () => {
|
|
const params = createParams();
|
|
const bridge = createCodexUserInputBridge({
|
|
paramsForRun: params,
|
|
threadId: "thread-1",
|
|
turnId: "turn-1",
|
|
});
|
|
|
|
const response = bridge.handleRequest({
|
|
id: "input-1",
|
|
params: {
|
|
threadId: "thread-1",
|
|
turnId: "turn-1",
|
|
itemId: "tool-1",
|
|
questions: [
|
|
{
|
|
id: "choice",
|
|
header: "Mode",
|
|
question: "Pick a mode",
|
|
isOther: false,
|
|
isSecret: false,
|
|
options: [
|
|
{ label: "Fast", description: "Use less reasoning" },
|
|
{ label: "Deep", description: "Use more reasoning" },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
await vi.waitFor(() => expect(params.onBlockReply).toHaveBeenCalledTimes(1));
|
|
expect(expectFirstBlockReplyText(params)).toContain("Pick a mode");
|
|
expect(bridge.handleQueuedMessage("2")).toBe(true);
|
|
|
|
await expect(response).resolves.toEqual({
|
|
answers: { choice: { answers: ["Deep"] } },
|
|
});
|
|
});
|
|
|
|
it("maps keyed multi-question replies to Codex answer ids", async () => {
|
|
const params = createParams();
|
|
const bridge = createCodexUserInputBridge({
|
|
paramsForRun: params,
|
|
threadId: "thread-1",
|
|
turnId: "turn-1",
|
|
});
|
|
|
|
const response = bridge.handleRequest({
|
|
id: "input-2",
|
|
params: {
|
|
threadId: "thread-1",
|
|
turnId: "turn-1",
|
|
itemId: "tool-1",
|
|
questions: [
|
|
{
|
|
id: "repo",
|
|
header: "Repository",
|
|
question: "Which repo?",
|
|
isOther: true,
|
|
isSecret: false,
|
|
options: null,
|
|
},
|
|
{
|
|
id: "scope",
|
|
header: "Scope",
|
|
question: "Which scope?",
|
|
isOther: false,
|
|
isSecret: false,
|
|
options: [{ label: "Tests", description: "Only tests" }],
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
await vi.waitFor(() => expect(params.onBlockReply).toHaveBeenCalledTimes(1));
|
|
expect(bridge.handleQueuedMessage("repo: openclaw\nscope: Tests")).toBe(true);
|
|
|
|
await expect(response).resolves.toEqual({
|
|
answers: {
|
|
repo: { answers: ["openclaw"] },
|
|
scope: { answers: ["Tests"] },
|
|
},
|
|
});
|
|
});
|
|
|
|
it("rejects free-form option replies when Other is disabled", async () => {
|
|
const params = createParams();
|
|
const bridge = createCodexUserInputBridge({
|
|
paramsForRun: params,
|
|
threadId: "thread-1",
|
|
turnId: "turn-1",
|
|
});
|
|
|
|
const response = bridge.handleRequest({
|
|
id: "input-options",
|
|
params: {
|
|
threadId: "thread-1",
|
|
turnId: "turn-1",
|
|
itemId: "tool-1",
|
|
questions: [
|
|
{
|
|
id: "mode",
|
|
header: "Mode",
|
|
question: "Pick a mode",
|
|
isOther: false,
|
|
isSecret: false,
|
|
options: [{ label: "Fast", description: "Use less reasoning" }],
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
await vi.waitFor(() => expect(params.onBlockReply).toHaveBeenCalledTimes(1));
|
|
expect(bridge.handleQueuedMessage("banana")).toBe(true);
|
|
|
|
await expect(response).resolves.toEqual({
|
|
answers: { mode: { answers: [] } },
|
|
});
|
|
});
|
|
|
|
it("escapes prompt question and option text before chat display", async () => {
|
|
const params = createParams();
|
|
const bridge = createCodexUserInputBridge({
|
|
paramsForRun: params,
|
|
threadId: "thread-1",
|
|
turnId: "turn-1",
|
|
});
|
|
|
|
const response = bridge.handleRequest({
|
|
id: "input-escaped",
|
|
params: {
|
|
threadId: "thread-1",
|
|
turnId: "turn-1",
|
|
itemId: "tool-1",
|
|
questions: [
|
|
{
|
|
id: "mode",
|
|
header: "Mode <@U123>",
|
|
question: "Pick [trusted](https://evil) @here",
|
|
isOther: false,
|
|
isSecret: false,
|
|
options: [{ label: "Fast <@U123>", description: "Use [less](https://evil)" }],
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
await vi.waitFor(() => expect(params.onBlockReply).toHaveBeenCalledTimes(1));
|
|
const text = expectFirstBlockReplyText(params);
|
|
expect(text).toContain("Mode <\uff20U123>");
|
|
expect(text).toContain("Pick \uff3btrusted\uff3d\uff08https://evil\uff09 \uff20here");
|
|
expect(text).toContain(
|
|
"Fast <\uff20U123> - Use \uff3bless\uff3d\uff08https://evil\uff09",
|
|
);
|
|
expect(text).not.toContain("<@U123>");
|
|
expect(text).not.toContain("[trusted](https://evil)");
|
|
expect(text).not.toContain("@here");
|
|
|
|
expect(bridge.handleQueuedMessage("1")).toBe(true);
|
|
await expect(response).resolves.toEqual({
|
|
answers: { mode: { answers: ["Fast <@U123>"] } },
|
|
});
|
|
});
|
|
|
|
it("clears pending prompts when Codex resolves the server request itself", async () => {
|
|
const params = createParams();
|
|
const bridge = createCodexUserInputBridge({
|
|
paramsForRun: params,
|
|
threadId: "thread-1",
|
|
turnId: "turn-1",
|
|
});
|
|
|
|
const response = bridge.handleRequest({
|
|
id: "input-3",
|
|
params: {
|
|
threadId: "thread-1",
|
|
turnId: "turn-1",
|
|
itemId: "tool-1",
|
|
questions: [
|
|
{
|
|
id: "answer",
|
|
header: "Answer",
|
|
question: "Continue?",
|
|
isOther: true,
|
|
isSecret: false,
|
|
options: null,
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
await vi.waitFor(() => expect(params.onBlockReply).toHaveBeenCalledTimes(1));
|
|
bridge.handleNotification({
|
|
method: "serverRequest/resolved",
|
|
params: { threadId: "thread-1", requestId: "input-3" },
|
|
});
|
|
|
|
await expect(response).resolves.toEqual({ answers: {} });
|
|
expect(bridge.handleQueuedMessage("too late")).toBe(false);
|
|
});
|
|
|
|
it("resolves malformed empty question prompts without waiting for chat input", async () => {
|
|
const params = createParams();
|
|
const bridge = createCodexUserInputBridge({
|
|
paramsForRun: params,
|
|
threadId: "thread-1",
|
|
turnId: "turn-1",
|
|
});
|
|
|
|
await expect(
|
|
bridge.handleRequest({
|
|
id: "input-empty",
|
|
params: {
|
|
threadId: "thread-1",
|
|
turnId: "turn-1",
|
|
itemId: "tool-1",
|
|
questions: [],
|
|
},
|
|
}),
|
|
).resolves.toEqual({ answers: {} });
|
|
expect(params.onBlockReply).not.toHaveBeenCalled();
|
|
expect(bridge.handleQueuedMessage("late answer")).toBe(false);
|
|
});
|
|
});
|