test: share agent acp turn helpers

This commit is contained in:
Peter Steinberger 2026-03-13 23:47:23 +00:00
parent 2d1134be23
commit 26578a18c8
1 changed files with 83 additions and 117 deletions

View File

@ -171,6 +171,61 @@ function subscribeAssistantEvents() {
return { assistantEvents, stop };
}
async function runAcpTurnWithAssistantEvents(chunks: string[]) {
const { assistantEvents, stop } = subscribeAssistantEvents();
const runTurn = createRunTurnFromTextDeltas(chunks);
mockAcpManager({
runTurn: (params: unknown) => runTurn(params),
});
try {
await agentCommand({ message: "ping", sessionKey: "agent:codex:acp:test" }, runtime);
} finally {
stop();
}
const logLines = vi.mocked(runtime.log).mock.calls.map(([first]) => String(first));
return { assistantEvents, logLines };
}
async function runAcpTurnWithTextDeltas(params: { message?: string; chunks: string[] }) {
const runTurn = createRunTurnFromTextDeltas(params.chunks);
mockAcpManager({
runTurn: (input: unknown) => runTurn(input),
});
await agentCommand(
{
message: params.message ?? "ping",
sessionKey: "agent:codex:acp:test",
},
runtime,
);
return { runTurn };
}
function expectPersistedAcpTranscript(params: {
storePath: string;
userContent: string;
assistantText: string;
}) {
const persistedStore = JSON.parse(fs.readFileSync(params.storePath, "utf-8")) as Record<
string,
{ sessionFile?: string }
>;
const sessionFile = persistedStore["agent:codex:acp:test"]?.sessionFile;
const messages = readSessionMessages("acp-session-1", params.storePath, sessionFile);
expect(messages).toHaveLength(2);
expect(messages[0]).toMatchObject({
role: "user",
content: params.userContent,
});
expect(messages[1]).toMatchObject({
role: "assistant",
content: [{ type: "text", text: params.assistantText }],
});
}
async function runAcpSessionWithPolicyOverrides(params: {
acpOverrides: Partial<NonNullable<OpenClawConfig["acp"]>>;
resolveSession?: Parameters<typeof mockAcpManager>[0]["resolveSession"];
@ -209,13 +264,7 @@ describe("agentCommand ACP runtime routing", () => {
it("routes ACP sessions through AcpSessionManager instead of embedded agent", async () => {
await withAcpSessionEnv(async () => {
const runTurn = createRunTurnFromTextDeltas(["ACP_", "OK"]);
mockAcpManager({
runTurn: (params: unknown) => runTurn(params),
});
await agentCommand({ message: "ping", sessionKey: "agent:codex:acp:test" }, runtime);
const { runTurn } = await runAcpTurnWithTextDeltas({ chunks: ["ACP_", "OK"] });
expect(runTurn).toHaveBeenCalledWith(
expect.objectContaining({
@ -234,64 +283,32 @@ describe("agentCommand ACP runtime routing", () => {
it("persists ACP child session history to the transcript store", async () => {
await withAcpSessionEnvInfo(async ({ storePath }) => {
const runTurn = createRunTurnFromTextDeltas(["ACP_", "OK"]);
mockAcpManager({
runTurn: (params: unknown) => runTurn(params),
});
await agentCommand({ message: "ping", sessionKey: "agent:codex:acp:test" }, runtime);
const persistedStore = JSON.parse(fs.readFileSync(storePath, "utf-8")) as Record<
string,
{ sessionFile?: string }
>;
const sessionFile = persistedStore["agent:codex:acp:test"]?.sessionFile;
const messages = readSessionMessages("acp-session-1", storePath, sessionFile);
expect(messages).toHaveLength(2);
expect(messages[0]).toMatchObject({
role: "user",
content: "ping",
});
expect(messages[1]).toMatchObject({
role: "assistant",
content: [{ type: "text", text: "ACP_OK" }],
await runAcpTurnWithTextDeltas({ chunks: ["ACP_", "OK"] });
expectPersistedAcpTranscript({
storePath,
userContent: "ping",
assistantText: "ACP_OK",
});
});
});
it("preserves exact ACP transcript text without trimming whitespace", async () => {
await withAcpSessionEnvInfo(async ({ storePath }) => {
const runTurn = createRunTurnFromTextDeltas([" ACP_OK\n"]);
mockAcpManager({
runTurn: (params: unknown) => runTurn(params),
await runAcpTurnWithTextDeltas({
message: " ping\n",
chunks: [" ACP_OK\n"],
});
await agentCommand({ message: " ping\n", sessionKey: "agent:codex:acp:test" }, runtime);
const persistedStore = JSON.parse(fs.readFileSync(storePath, "utf-8")) as Record<
string,
{ sessionFile?: string }
>;
const sessionFile = persistedStore["agent:codex:acp:test"]?.sessionFile;
const messages = readSessionMessages("acp-session-1", storePath, sessionFile);
expect(messages).toHaveLength(2);
expect(messages[0]).toMatchObject({
role: "user",
content: " ping\n",
});
expect(messages[1]).toMatchObject({
role: "assistant",
content: [{ type: "text", text: " ACP_OK\n" }],
expectPersistedAcpTranscript({
storePath,
userContent: " ping\n",
assistantText: " ACP_OK\n",
});
});
});
it("suppresses ACP NO_REPLY lead fragments before emitting assistant text", async () => {
await withAcpSessionEnv(async () => {
const { assistantEvents, stop } = subscribeAssistantEvents();
const runTurn = createRunTurnFromTextDeltas([
const { assistantEvents, logLines } = await runAcpTurnWithAssistantEvents([
"NO",
"NO_",
"NO_RE",
@ -299,19 +316,7 @@ describe("agentCommand ACP runtime routing", () => {
"Actual answer",
]);
mockAcpManager({
runTurn: (params: unknown) => runTurn(params),
});
try {
await agentCommand({ message: "ping", sessionKey: "agent:codex:acp:test" }, runtime);
} finally {
stop();
}
expect(assistantEvents).toEqual([{ text: "Actual answer", delta: "Actual answer" }]);
const logLines = vi.mocked(runtime.log).mock.calls.map(([first]) => String(first));
expect(logLines.some((line) => line.includes("NO_REPLY"))).toBe(false);
expect(logLines.some((line) => line.includes("Actual answer"))).toBe(true);
});
@ -319,31 +324,13 @@ describe("agentCommand ACP runtime routing", () => {
it("keeps silent-only ACP turns out of assistant output", async () => {
await withAcpSessionEnv(async () => {
const assistantEvents: string[] = [];
const stop = onAgentEvent((evt) => {
if (evt.stream !== "assistant") {
return;
}
if (typeof evt.data?.text === "string") {
assistantEvents.push(evt.data.text);
}
});
const runTurn = createRunTurnFromTextDeltas(["NO", "NO_", "NO_RE", "NO_REPLY"]);
mockAcpManager({
runTurn: (params: unknown) => runTurn(params),
});
try {
await agentCommand({ message: "ping", sessionKey: "agent:codex:acp:test" }, runtime);
} finally {
stop();
}
expect(assistantEvents).toEqual([]);
const logLines = vi.mocked(runtime.log).mock.calls.map(([first]) => String(first));
const { assistantEvents, logLines } = await runAcpTurnWithAssistantEvents([
"NO",
"NO_",
"NO_RE",
"NO_REPLY",
]);
expect(assistantEvents.map((event) => event.text).filter(Boolean)).toEqual([]);
expect(logLines.some((line) => line.includes("NO_REPLY"))).toBe(false);
expect(logLines.some((line) => line.includes("No reply from agent."))).toBe(true);
});
@ -351,18 +338,12 @@ describe("agentCommand ACP runtime routing", () => {
it("preserves repeated identical ACP delta chunks", async () => {
await withAcpSessionEnv(async () => {
const { assistantEvents, stop } = subscribeAssistantEvents();
const runTurn = createRunTurnFromTextDeltas(["b", "o", "o", "k"]);
mockAcpManager({
runTurn: (params: unknown) => runTurn(params),
});
try {
await agentCommand({ message: "ping", sessionKey: "agent:codex:acp:test" }, runtime);
} finally {
stop();
}
const { assistantEvents, logLines } = await runAcpTurnWithAssistantEvents([
"b",
"o",
"o",
"k",
]);
expect(assistantEvents).toEqual([
{ text: "b", delta: "b" },
@ -370,30 +351,15 @@ describe("agentCommand ACP runtime routing", () => {
{ text: "boo", delta: "o" },
{ text: "book", delta: "k" },
]);
const logLines = vi.mocked(runtime.log).mock.calls.map(([first]) => String(first));
expect(logLines.some((line) => line.includes("book"))).toBe(true);
});
});
it("re-emits buffered NO prefix when ACP text becomes visible content", async () => {
await withAcpSessionEnv(async () => {
const { assistantEvents, stop } = subscribeAssistantEvents();
const runTurn = createRunTurnFromTextDeltas(["NO", "W"]);
mockAcpManager({
runTurn: (params: unknown) => runTurn(params),
});
try {
await agentCommand({ message: "ping", sessionKey: "agent:codex:acp:test" }, runtime);
} finally {
stop();
}
const { assistantEvents, logLines } = await runAcpTurnWithAssistantEvents(["NO", "W"]);
expect(assistantEvents).toEqual([{ text: "NOW", delta: "NOW" }]);
const logLines = vi.mocked(runtime.log).mock.calls.map(([first]) => String(first));
expect(logLines.some((line) => line.includes("NOW"))).toBe(true);
});
});