Skip to content

Commit 5a6a19c

Browse files
authored
fix(qa): align runner inbound dispatch contract (#111507)
1 parent 163a6c6 commit 5a6a19c

2 files changed

Lines changed: 103 additions & 23 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Qa Lab integration tests cover the real QA Channel runtime contract.
2+
import { qaChannelPlugin, setQaChannelRuntime } from "@openclaw/qa-channel/api.js";
3+
import { describe, expect, it, vi } from "vitest";
4+
import { startQaBusServer } from "./bus-server.js";
5+
import { createQaBusState } from "./bus-state.js";
6+
import { createQaRunnerRuntime } from "./harness-runtime.js";
7+
import { createQaChannelGatewayConfig } from "./qa-channel-transport.js";
8+
9+
describe("QA runner runtime integration", () => {
10+
it("dispatches a QA Channel inbound turn through the embedded runner", async () => {
11+
const state = createQaBusState();
12+
const bus = await startQaBusServer({ state });
13+
const runtime = createQaRunnerRuntime();
14+
setQaChannelRuntime(runtime);
15+
const config = createQaChannelGatewayConfig({ baseUrl: bus.baseUrl });
16+
const account = qaChannelPlugin.config.resolveAccount(config, "default");
17+
const abort = new AbortController();
18+
const startAccount = qaChannelPlugin.gateway?.startAccount;
19+
if (!startAccount) {
20+
throw new Error("QA Channel gateway is unavailable");
21+
}
22+
const gatewayTask = startAccount({
23+
accountId: account.accountId,
24+
account,
25+
cfg: config,
26+
runtime: {
27+
log: () => undefined,
28+
error: () => undefined,
29+
exit: () => undefined,
30+
},
31+
abortSignal: abort.signal,
32+
log: {
33+
info: () => undefined,
34+
warn: () => undefined,
35+
error: () => undefined,
36+
debug: () => undefined,
37+
},
38+
getStatus: () => ({
39+
accountId: account.accountId,
40+
configured: true,
41+
enabled: true,
42+
running: true,
43+
}),
44+
setStatus: () => undefined,
45+
});
46+
47+
try {
48+
state.addInboundMessage({
49+
accountId: "default",
50+
conversation: { kind: "direct", id: "alice" },
51+
senderId: "alice",
52+
senderName: "Alice",
53+
text: "ping",
54+
});
55+
56+
await Promise.race([
57+
vi.waitFor(
58+
() => {
59+
expect(state.getSnapshot().messages).toContainEqual(
60+
expect.objectContaining({ direction: "outbound", text: "qa-echo: ping" }),
61+
);
62+
},
63+
{ interval: 25, timeout: 2_000 },
64+
),
65+
gatewayTask.then(() => {
66+
throw new Error("QA Channel gateway stopped before delivering the turn");
67+
}),
68+
]);
69+
} finally {
70+
abort.abort();
71+
try {
72+
await gatewayTask;
73+
} finally {
74+
await bus.stop();
75+
}
76+
}
77+
});
78+
});

extensions/qa-lab/src/harness-runtime.ts

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,19 @@ type SessionRecord = {
1515

1616
export function createQaRunnerRuntime(): PluginRuntime {
1717
const sessions = new Map<string, SessionRecord>();
18+
const dispatchReplyWithBufferedBlockDispatcher: PluginRuntime["channel"]["reply"]["dispatchReplyWithBufferedBlockDispatcher"] =
19+
async ({ ctx, dispatcherOptions }) => {
20+
await dispatcherOptions.deliver(
21+
{
22+
text: `qa-echo: ${ctx.BodyForAgent ?? ctx.Body ?? ""}`,
23+
},
24+
{ kind: "final" },
25+
);
26+
return {
27+
queuedFinal: false,
28+
counts: { tool: 0, block: 0, final: 1 },
29+
};
30+
};
1831
return {
1932
channel: {
2033
routing: {
@@ -73,39 +86,28 @@ export function createQaRunnerRuntime(): PluginRuntime {
7386
finalizeInboundContext(ctx: Record<string, unknown>) {
7487
return ctx as typeof ctx & { CommandAuthorized: boolean };
7588
},
76-
async dispatchReplyWithBufferedBlockDispatcher({
77-
ctx,
78-
dispatcherOptions,
79-
}: {
80-
ctx: { BodyForAgent?: string; Body?: string };
81-
dispatcherOptions: { deliver: (payload: { text: string }) => Promise<void> };
82-
}) {
83-
await dispatcherOptions.deliver({
84-
text: `qa-echo: ${ctx.BodyForAgent ?? ctx.Body ?? ""}`,
85-
});
86-
},
89+
dispatchReplyWithBufferedBlockDispatcher,
8790
},
8891
inbound: {
89-
async dispatchReply(
90-
params: Parameters<PluginRuntime["channel"]["inbound"]["dispatchReply"]>[0],
91-
) {
92+
async dispatch(params: Parameters<PluginRuntime["channel"]["inbound"]["dispatch"]>[0]) {
9293
const sessionKey =
9394
typeof params.ctxPayload.SessionKey === "string"
9495
? params.ctxPayload.SessionKey
95-
: params.routeSessionKey;
96-
await params.recordInboundSession({
97-
storePath: params.storePath,
96+
: params.route.sessionKey;
97+
sessions.set(sessionKey, {
9898
sessionKey,
99-
ctx: params.ctxPayload,
100-
onRecordError: params.record?.onRecordError ?? (() => undefined),
99+
body: params.ctxPayload.BodyForAgent ?? params.ctxPayload.Body ?? "",
101100
});
102-
const dispatchResult = await params.dispatchReplyWithBufferedBlockDispatcher({
101+
const delivery =
102+
params.admission?.kind === "observeOnly"
103+
? async () => ({ visibleReplySent: false })
104+
: params.delivery.deliver;
105+
const dispatchResult = await dispatchReplyWithBufferedBlockDispatcher({
103106
ctx: params.ctxPayload,
104107
cfg: params.cfg,
105108
dispatcherOptions: {
106-
...params.dispatcherOptions,
107109
deliver: async (payload, info) => {
108-
await params.delivery.deliver(payload, info);
110+
await delivery(payload, info);
109111
},
110112
onError: params.delivery.onError,
111113
},
@@ -116,7 +118,7 @@ export function createQaRunnerRuntime(): PluginRuntime {
116118
admission: params.admission ?? { kind: "dispatch" },
117119
dispatched: true,
118120
ctxPayload: params.ctxPayload,
119-
routeSessionKey: params.routeSessionKey,
121+
routeSessionKey: params.route.sessionKey,
120122
dispatchResult,
121123
};
122124
},

0 commit comments

Comments
 (0)