test: harden qa parity runtime staging

This commit is contained in:
Peter Steinberger
2026-04-22 07:16:03 +01:00
parent 137f64d0c0
commit dd9adc57c2
6 changed files with 61 additions and 12 deletions

View File

@@ -9,7 +9,7 @@ describe("QA provider image generation config", () => {
requiredPluginIds: ["qa-channel"],
});
expect(patch.plugins.allow).toEqual(["memory-core", "qa-channel"]);
expect(patch.plugins.allow).toEqual(["acpx", "memory-core", "qa-channel"]);
expect(patch.agents.defaults.imageGenerationModel.primary).toBe("mock-openai/gpt-image-1");
expect(patch.models?.providers["mock-openai"]?.baseUrl).toBe("http://127.0.0.1:44080/v1");
});
@@ -33,7 +33,7 @@ describe("QA provider image generation config", () => {
});
expect(patch.plugins).toEqual({
allow: ["memory-core", "openai", "qa-channel"],
allow: ["acpx", "memory-core", "openai", "qa-channel"],
entries: {
openai: {
enabled: true,

View File

@@ -1,3 +1,4 @@
import { QA_BASE_RUNTIME_PLUGIN_IDS } from "../qa-gateway-config.js";
import type { QaProviderMode } from "./index.js";
import { getQaProvider } from "./index.js";
@@ -45,7 +46,11 @@ export function buildQaImageGenerationConfigPatch(input: QaImageGenerationPatchI
return {
plugins: {
allow: uniqueNonEmpty(["memory-core", ...enabledPluginIds, ...input.requiredPluginIds]),
allow: uniqueNonEmpty([
...QA_BASE_RUNTIME_PLUGIN_IDS,
...enabledPluginIds,
...input.requiredPluginIds,
]),
...(enabledPluginIds.length > 0
? {
entries: Object.fromEntries(

View File

@@ -58,12 +58,14 @@ describe("buildQaGatewayConfig", () => {
expect(cfg.models?.providers?.openai?.request).toEqual({ allowPrivateNetwork: true });
expect(cfg.models?.providers?.anthropic?.baseUrl).toBe("http://127.0.0.1:44080");
expect(cfg.models?.providers?.anthropic?.request).toEqual({ allowPrivateNetwork: true });
expect(cfg.plugins?.allow).toEqual(["memory-core", "qa-channel"]);
expect(cfg.plugins?.allow).toEqual(["acpx", "memory-core", "qa-channel"]);
expect(cfg.plugins?.entries?.acpx).toEqual({ enabled: false });
expect(cfg.plugins?.entries?.["memory-core"]).toEqual({ enabled: true });
expect(cfg.plugins?.entries?.["qa-channel"]).toEqual({ enabled: true });
expect(cfg.plugins?.entries?.openai).toBeUndefined();
expect(cfg.gateway?.reload?.deferralTimeoutMs).toBe(1_000);
expect(cfg.tools?.profile).toBe("coding");
expect(cfg.agents?.list?.[0]?.tools?.profile).toBe("coding");
expect(cfg.channels?.["qa-channel"]).toMatchObject({
enabled: true,
baseUrl: "http://127.0.0.1:43124",
@@ -94,7 +96,7 @@ describe("buildQaGatewayConfig", () => {
expect(cfg.models?.providers?.anthropic?.models.map((model) => model.id)).toContain(
"claude-opus-4-6",
);
expect(cfg.plugins?.allow).toEqual(["memory-core"]);
expect(cfg.plugins?.allow).toEqual(["acpx", "memory-core"]);
});
it("can wire AIMock as a separate mock provider lane", () => {
@@ -131,7 +133,7 @@ describe("buildQaGatewayConfig", () => {
transportConfig: {},
});
expect(cfg.plugins?.allow).toEqual(["memory-core"]);
expect(cfg.plugins?.allow).toEqual(["acpx", "memory-core"]);
expect(cfg.plugins?.entries?.["qa-channel"]).toBeUndefined();
expect(cfg.channels?.["qa-channel"]).toBeUndefined();
});
@@ -147,7 +149,7 @@ describe("buildQaGatewayConfig", () => {
...createQaChannelTransportParams(),
});
expect(cfg.plugins?.allow).toEqual(["memory-core", "active-memory", "qa-channel"]);
expect(cfg.plugins?.allow).toEqual(["acpx", "memory-core", "active-memory", "qa-channel"]);
expect(cfg.plugins?.entries?.["active-memory"]).toEqual({ enabled: true });
});
@@ -167,7 +169,7 @@ describe("buildQaGatewayConfig", () => {
expect(getPrimaryModel(cfg.agents?.defaults?.model)).toBe("openai/gpt-5.4");
expect(getPrimaryModel(cfg.agents?.list?.[0]?.model)).toBe("openai/gpt-5.4");
expect(cfg.models).toBeUndefined();
expect(cfg.plugins?.allow).toEqual(["memory-core", "openai", "qa-channel"]);
expect(cfg.plugins?.allow).toEqual(["acpx", "memory-core", "openai", "qa-channel"]);
expect(cfg.plugins?.entries?.openai).toEqual({ enabled: true });
expect(cfg.agents?.defaults?.models?.["openai/gpt-5.4"]).toEqual({
params: { transport: "sse", openaiWsWarmup: false, fastMode: true },
@@ -187,7 +189,13 @@ describe("buildQaGatewayConfig", () => {
...createQaChannelTransportParams(),
});
expect(cfg.plugins?.allow).toEqual(["memory-core", "anthropic", "google", "qa-channel"]);
expect(cfg.plugins?.allow).toEqual([
"acpx",
"memory-core",
"anthropic",
"google",
"qa-channel",
]);
expect(cfg.plugins?.entries?.anthropic).toEqual({ enabled: true });
expect(cfg.plugins?.entries?.google).toEqual({ enabled: true });
expect(cfg.plugins?.entries?.openai).toBeUndefined();
@@ -209,7 +217,7 @@ describe("buildQaGatewayConfig", () => {
});
expect(getPrimaryModel(cfg.agents?.defaults?.model)).toBe("codex-cli/test-model");
expect(cfg.plugins?.allow).toEqual(["memory-core", "openai", "qa-channel"]);
expect(cfg.plugins?.allow).toEqual(["acpx", "memory-core", "openai", "qa-channel"]);
expect(cfg.plugins?.entries?.openai).toEqual({ enabled: true });
expect(cfg.plugins?.entries?.["codex-cli"]).toBeUndefined();
});
@@ -249,7 +257,7 @@ describe("buildQaGatewayConfig", () => {
expect(cfg.models?.mode).toBe("merge");
expect(cfg.models?.providers?.["custom-openai"]?.api).toBe("openai-responses");
expect(cfg.plugins?.allow).toEqual(["memory-core", "openai", "qa-channel"]);
expect(cfg.plugins?.allow).toEqual(["acpx", "memory-core", "openai", "qa-channel"]);
});
it("can set a QA default thinking level for judge turns", () => {

View File

@@ -20,6 +20,8 @@ export const DEFAULT_QA_CONTROL_UI_ALLOWED_ORIGINS = Object.freeze([
"http://localhost:43124",
]);
export const QA_BASE_RUNTIME_PLUGIN_IDS = Object.freeze(["acpx", "memory-core"]);
export function mergeQaControlUiAllowedOrigins(extraOrigins?: string[]) {
const normalizedExtra = (extraOrigins ?? [])
.map((origin) => origin.trim())
@@ -96,7 +98,9 @@ export function buildQaGatewayConfig(params: {
const transportPluginEntries = Object.fromEntries(
transportPluginIds.map((pluginId) => [pluginId, { enabled: true }]),
);
const allowedPlugins = [...new Set(["memory-core", ...selectedPluginIds, ...transportPluginIds])];
const allowedPlugins = [
...new Set([...QA_BASE_RUNTIME_PLUGIN_IDS, ...selectedPluginIds, ...transportPluginIds]),
];
const resolveModelParams = (modelRef: string) =>
provider.resolveModelParams({
modelRef,
@@ -114,6 +118,9 @@ export function buildQaGatewayConfig(params: {
allow: allowedPlugins,
entries: {
acpx: {
// The parity gateway stages a clean bundled-plugin tree. Keep the
// runtime backend plugin in the allowlist so this disabled entry is
// not mistaken for stale config when optional bundles are pruned.
enabled: false,
},
"memory-core": {
@@ -174,6 +181,9 @@ export function buildQaGatewayConfig(params: {
subagents: {
allowAgents: ["*"],
},
tools: {
profile: "coding",
},
},
],
},

View File

@@ -23,6 +23,11 @@ const VISIBLE_REPLY_LEAK_PATTERNS = [
/\bnot inventing status\b/i,
];
const TOOL_BACKED_FAILURE_PATTERNS = [
/\btool\s+[a-z0-9_.-]+\s+not found\b/i,
/^status:\s*blocked\b/im,
];
export function extractQaVisibleReplyLeakText(text: string): string | undefined {
const trimmed = text.trim();
if (!trimmed) {
@@ -47,5 +52,8 @@ export function extractQaFailureReplyText(text: string): string | undefined {
if (visibleReplyLeak) {
return visibleReplyLeak;
}
if (TOOL_BACKED_FAILURE_PATTERNS.some((pattern) => pattern.test(trimmed))) {
return trimmed;
}
return undefined;
}

View File

@@ -79,6 +79,24 @@ describe("qa suite transport helpers", () => {
await expect(pending).rejects.toThrow("checking thread context");
});
it("fails success-only waitForOutboundMessage calls when a tool-backed scenario reports missing tools", async () => {
const state = createQaBusState();
const pending = waitForOutboundMessage(
state,
(candidate) => candidate.text.includes("Status: complete"),
5_000,
);
state.addOutboundMessage({
to: "dm:qa-operator",
text: "Read: AGENT.md\nEvidence snippet: Tool read not found\nStatus: blocked",
senderId: "openclaw",
senderName: "OpenClaw QA",
});
await expect(pending).rejects.toThrow("Tool read not found");
});
it("fails raw scenario waitForCondition calls when a classified failure reply arrives", async () => {
const state = createQaBusState();
const waitForCondition = createScenarioWaitForCondition(state);