From 0c7f07818f0eec0f4c527233019fd0d504d09804 Mon Sep 17 00:00:00 2001 From: Mariano <132747814+mbelinky@users.noreply.github.com> Date: Mon, 9 Mar 2026 22:40:14 +0100 Subject: [PATCH] acp: add regression coverage and smoke-test docs (#41456) Merged via squash. Prepared head SHA: 514d5873520683efcca1542cbca1ee6ec645582b Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com> Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com> Reviewed-by: @mbelinky --- CHANGELOG.md | 1 + docs/tools/acp-agents.md | 40 +++++++++++++++++++ extensions/acpx/src/runtime.test.ts | 33 +++++++++++++++ ...sessions.gateway-server-sessions-a.test.ts | 12 ++++++ 4 files changed, 86 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d0a735cb18..dfa23b105af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai - ACP/session UX: replay stored user and assistant text on `loadSession`, expose Gateway-backed session controls and metadata, and emit approximate session usage updates so IDE clients restore context more faithfully. (#41425) Thanks @mbelinky. - ACP/tool streaming: enrich `tool_call` and `tool_call_update` events with best-effort text content and file-location hints so IDE clients can follow bridge tool activity more naturally. (#41442) Thanks @mbelinky. - ACP/runtime attachments: forward normalized inbound image attachments into ACP runtime turns so ACPX sessions can preserve image prompt content on the runtime path. (#41427) Thanks @mbelinky. +- ACP/regressions: add gateway RPC coverage for ACP lineage patching, ACPX runtime coverage for image prompt serialization, and an operator smoke-test procedure for live ACP spawn verification. (#41456) Thanks @mbelinky. ## 2026.3.8 diff --git a/docs/tools/acp-agents.md b/docs/tools/acp-agents.md index 74ed73248f1..e41a96248ae 100644 --- a/docs/tools/acp-agents.md +++ b/docs/tools/acp-agents.md @@ -246,6 +246,46 @@ Interface details: - `streamTo` (optional): `"parent"` streams initial ACP run progress summaries back to the requester session as system events. - When available, accepted responses include `streamLogPath` pointing to a session-scoped JSONL log (`.acp-stream.jsonl`) you can tail for full relay history. +### Operator smoke test + +Use this after a gateway deploy when you want a quick live check that ACP spawn +is actually working end-to-end, not just passing unit tests. + +Recommended gate: + +1. Verify the deployed gateway version/commit on the target host. +2. Confirm the deployed source includes the ACP lineage acceptance in + `src/gateway/sessions-patch.ts` (`subagent:* or acp:* sessions`). +3. Open a temporary ACPX bridge session to a live agent (for example + `razor(main)` on `jpclawhq`). +4. Ask that agent to call `sessions_spawn` with: + - `runtime: "acp"` + - `agentId: "codex"` + - `mode: "run"` + - task: `Reply with exactly LIVE-ACP-SPAWN-OK` +5. Verify the agent reports: + - `accepted=yes` + - a real `childSessionKey` + - no validator error +6. Clean up the temporary ACPX bridge session. + +Example prompt to the live agent: + +```text +Use the sessions_spawn tool now with runtime: "acp", agentId: "codex", and mode: "run". +Set the task to: "Reply with exactly LIVE-ACP-SPAWN-OK". +Then report only: accepted=; childSessionKey=; error=. +``` + +Notes: + +- Keep this smoke test on `mode: "run"` unless you are intentionally testing + thread-bound persistent ACP sessions. +- Do not require `streamTo: "parent"` for the basic gate. That path depends on + requester/session capabilities and is a separate integration check. +- Treat thread-bound `mode: "session"` testing as a second, richer integration + pass from a real Discord thread or Telegram topic. + ## Sandbox compatibility ACP sessions currently run on the host runtime, not inside the OpenClaw sandbox. diff --git a/extensions/acpx/src/runtime.test.ts b/extensions/acpx/src/runtime.test.ts index bb3b94cec9e..38137b3f581 100644 --- a/extensions/acpx/src/runtime.test.ts +++ b/extensions/acpx/src/runtime.test.ts @@ -127,6 +127,39 @@ describe("AcpxRuntime", () => { expect(promptArgs).toContain("--approve-all"); }); + it("serializes text plus image attachments into ACP prompt blocks", async () => { + const { runtime, logPath } = await createMockRuntimeFixture(); + + const handle = await runtime.ensureSession({ + sessionKey: "agent:codex:acp:with-image", + agent: "codex", + mode: "persistent", + }); + + for await (const _event of runtime.runTurn({ + handle, + text: "describe this image", + attachments: [{ mediaType: "image/png", data: "aW1hZ2UtYnl0ZXM=" }], + mode: "prompt", + requestId: "req-image", + })) { + // Consume stream to completion so prompt logging is finalized. + } + + const logs = await readMockRuntimeLogEntries(logPath); + const prompt = logs.find( + (entry) => + entry.kind === "prompt" && String(entry.sessionName ?? "") === "agent:codex:acp:with-image", + ); + expect(prompt).toBeDefined(); + + const stdinBlocks = JSON.parse(String(prompt?.stdinText ?? "")); + expect(stdinBlocks).toEqual([ + { type: "text", text: "describe this image" }, + { type: "image", mimeType: "image/png", data: "aW1hZ2UtYnl0ZXM=" }, + ]); + }); + it("preserves leading spaces across streamed text deltas", async () => { const runtime = sharedFixture?.runtime; expect(runtime).toBeDefined(); diff --git a/src/gateway/server.sessions.gateway-server-sessions-a.test.ts b/src/gateway/server.sessions.gateway-server-sessions-a.test.ts index 3837247c9bc..f986d49c648 100644 --- a/src/gateway/server.sessions.gateway-server-sessions-a.test.ts +++ b/src/gateway/server.sessions.gateway-server-sessions-a.test.ts @@ -463,6 +463,18 @@ describe("gateway server sessions", () => { expect(spawnedPatched.ok).toBe(true); expect(spawnedPatched.payload?.entry.spawnedBy).toBe("agent:main:main"); + const acpPatched = await rpcReq<{ + ok: true; + entry: { spawnedBy?: string; spawnDepth?: number }; + }>(ws, "sessions.patch", { + key: "agent:main:acp:child", + spawnedBy: "agent:main:main", + spawnDepth: 1, + }); + expect(acpPatched.ok).toBe(true); + expect(acpPatched.payload?.entry.spawnedBy).toBe("agent:main:main"); + expect(acpPatched.payload?.entry.spawnDepth).toBe(1); + const spawnedPatchedInvalidKey = await rpcReq(ws, "sessions.patch", { key: "agent:main:main", spawnedBy: "agent:main:main",