diff --git a/extensions/qa-channel/src/bus-client.test.ts b/extensions/qa-channel/src/bus-client.test.ts index 2c7237c83cf..899cbb50e95 100644 --- a/extensions/qa-channel/src/bus-client.test.ts +++ b/extensions/qa-channel/src/bus-client.test.ts @@ -1,10 +1,12 @@ import { createServer } from "node:http"; import { afterEach, describe, expect, it } from "vitest"; -import { pollQaBus } from "./bus-client.js"; +import { getQaBusState, pollQaBus } from "./bus-client.js"; -async function startJsonServer(handler: () => { statusCode?: number; body: string }) { - const server = createServer((_req, res) => { - const response = handler(); +async function startJsonServer( + handler: (req: { url?: string | undefined }) => { statusCode?: number; body: string }, +) { + const server = createServer((req, res) => { + const response = handler({ url: req.url }); res.writeHead(response.statusCode ?? 200, { "content-type": "application/json; charset=utf-8", }); @@ -53,4 +55,26 @@ describe("qa-bus client", () => { }), ).rejects.toThrow(SyntaxError); }); + + it("preserves baseUrl path prefixes when composing bus URLs", async () => { + const server = await startJsonServer((req) => ({ + statusCode: req.url === "/qa-bus/v1/state" ? 200 : 404, + body: + req.url === "/qa-bus/v1/state" + ? JSON.stringify({ + cursor: 1, + conversations: [], + threads: [], + messages: [], + events: [], + }) + : JSON.stringify({ error: `unexpected path: ${req.url}` }), + })); + stops.push(server.stop); + + await expect(getQaBusState(`${server.baseUrl}/qa-bus`)).resolves.toMatchObject({ + cursor: 1, + events: [], + }); + }); }); diff --git a/extensions/qa-channel/src/bus-client.ts b/extensions/qa-channel/src/bus-client.ts index 3c7c29b2126..77bd143d09a 100644 --- a/extensions/qa-channel/src/bus-client.ts +++ b/extensions/qa-channel/src/bus-client.ts @@ -34,13 +34,18 @@ export type { type JsonResult = Promise; +function buildQaBusUrl(baseUrl: string, path: string): URL { + const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`; + return new URL(path.replace(/^\/+/, ""), normalizedBaseUrl); +} + async function postJson( baseUrl: string, path: string, body: unknown, signal?: AbortSignal, ): JsonResult { - const url = new URL(path, baseUrl); + const url = buildQaBusUrl(baseUrl, path); const payload = JSON.stringify(body); const client = url.protocol === "https:" ? https : http; @@ -266,7 +271,7 @@ export async function injectQaBusInboundMessage(params: { } export async function getQaBusState(baseUrl: string): Promise { - const response = await fetch(`${baseUrl}/v1/state`); + const response = await fetch(buildQaBusUrl(baseUrl, "/v1/state")); if (!response.ok) { throw new Error(`qa-bus request failed: ${response.status}`); } diff --git a/extensions/qa-lab/src/bus-server.ts b/extensions/qa-lab/src/bus-server.ts index f4fd7da3296..5db53d45206 100644 --- a/extensions/qa-lab/src/bus-server.ts +++ b/extensions/qa-lab/src/bus-server.ts @@ -1,5 +1,6 @@ import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +import { normalizeAccountId } from "./bus-queries.js"; import type { QaBusState } from "./bus-state.js"; import type { QaBusCreateThreadInput, @@ -134,6 +135,7 @@ export async function handleQaBusRequest(params: { case "/v1/poll": { const input = body as unknown as QaBusPollInput; const timeoutMs = Math.max(0, Math.min(input.timeoutMs ?? 0, 30_000)); + const accountId = normalizeAccountId(input.accountId); const initial = params.state.poll(input); if (initial.events.length > 0 || timeoutMs === 0) { writeJson(params.res, 200, initial); @@ -142,7 +144,7 @@ export async function handleQaBusRequest(params: { try { await params.state.waitForCursorAdvance(input.cursor ?? 0, timeoutMs, (snapshot) => { return snapshot.events.some( - (event) => event.accountId === input.accountId && event.cursor > (input.cursor ?? 0), + (event) => event.accountId === accountId && event.cursor > (input.cursor ?? 0), ); }); } catch { diff --git a/extensions/qa-lab/src/bus-state.test.ts b/extensions/qa-lab/src/bus-state.test.ts index e0c4a065a8b..9eb7d9549bb 100644 --- a/extensions/qa-lab/src/bus-state.test.ts +++ b/extensions/qa-lab/src/bus-state.test.ts @@ -121,6 +121,21 @@ describe("qa-bus state", () => { await expect(pending).resolves.toBeUndefined(); }); + it("wakes default-account cursor waits when accountId is omitted", async () => { + const state = createQaBusState(); + const pending = state.waitForCursorAdvance(0, 500, (snapshot) => { + return snapshot.events.some((event) => event.accountId === "default" && event.cursor > 0); + }); + + state.addInboundMessage({ + conversation: { id: "target", kind: "direct" }, + senderId: "default-user", + text: "matched", + }); + + await expect(pending).resolves.toBeUndefined(); + }); + it("preserves inline attachments and lets search match attachment metadata", () => { const state = createQaBusState();