diff --git a/extensions/qa-lab/src/providers/image-generation.test.ts b/extensions/qa-lab/src/providers/image-generation.test.ts index c25718ada21..928aa76ed68 100644 --- a/extensions/qa-lab/src/providers/image-generation.test.ts +++ b/extensions/qa-lab/src/providers/image-generation.test.ts @@ -14,6 +14,23 @@ describe("QA provider image generation config", () => { expect(patch.models?.providers["mock-openai"]?.baseUrl).toBe("http://127.0.0.1:44080/v1"); }); + it("preserves already-allowed plugins when configuring image generation", () => { + const patch = buildQaImageGenerationConfigPatch({ + providerMode: "mock-openai", + providerBaseUrl: "http://127.0.0.1:44080/v1", + requiredPluginIds: ["qa-channel"], + existingPluginIds: ["openai", "anthropic", "qa-channel"], + }); + + expect(patch.plugins.allow).toEqual([ + "acpx", + "memory-core", + "openai", + "anthropic", + "qa-channel", + ]); + }); + it("uses the selected mock provider for AIMock image generation", () => { const patch = buildQaImageGenerationConfigPatch({ providerMode: "aimock", diff --git a/extensions/qa-lab/src/providers/image-generation.ts b/extensions/qa-lab/src/providers/image-generation.ts index 1b3728fe4a6..5606db95e07 100644 --- a/extensions/qa-lab/src/providers/image-generation.ts +++ b/extensions/qa-lab/src/providers/image-generation.ts @@ -6,6 +6,7 @@ type QaImageGenerationPatchInput = { providerMode: QaProviderMode; providerBaseUrl?: string; requiredPluginIds: readonly string[]; + existingPluginIds?: readonly string[]; }; function splitModelProviderId(modelRef: string) { @@ -48,6 +49,7 @@ export function buildQaImageGenerationConfigPatch(input: QaImageGenerationPatchI plugins: { allow: uniqueNonEmpty([ ...QA_BASE_RUNTIME_PLUGIN_IDS, + ...(input.existingPluginIds ?? []), ...enabledPluginIds, ...input.requiredPluginIds, ]), diff --git a/extensions/qa-lab/src/suite-runtime-agent-media.test.ts b/extensions/qa-lab/src/suite-runtime-agent-media.test.ts index ca379e4ea92..da1e5fa6879 100644 --- a/extensions/qa-lab/src/suite-runtime-agent-media.test.ts +++ b/extensions/qa-lab/src/suite-runtime-agent-media.test.ts @@ -4,12 +4,16 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const fetchJsonMock = vi.hoisted(() => vi.fn()); const patchConfigMock = vi.hoisted(() => vi.fn(async () => undefined)); +const readConfigSnapshotMock = vi.hoisted(() => + vi.fn(async () => ({ hash: "hash", config: { plugins: { allow: [] as string[] } } })), +); const waitForGatewayHealthyMock = vi.hoisted(() => vi.fn(async () => undefined)); const waitForTransportReadyMock = vi.hoisted(() => vi.fn(async () => undefined)); vi.mock("./suite-runtime-gateway.js", () => ({ fetchJson: fetchJsonMock, patchConfig: patchConfigMock, + readConfigSnapshot: readConfigSnapshotMock, waitForGatewayHealthy: waitForGatewayHealthyMock, waitForTransportReady: waitForTransportReadyMock, })); @@ -29,6 +33,8 @@ describe("qa suite runtime agent media helpers", () => { beforeEach(() => { fetchJsonMock.mockReset(); patchConfigMock.mockClear(); + readConfigSnapshotMock.mockReset(); + readConfigSnapshotMock.mockResolvedValue({ hash: "hash", config: { plugins: { allow: [] } } }); waitForGatewayHealthyMock.mockClear(); waitForTransportReadyMock.mockClear(); }); @@ -102,4 +108,27 @@ describe("qa suite runtime agent media helpers", () => { expect(waitForGatewayHealthyMock).toHaveBeenCalled(); expect(waitForTransportReadyMock).toHaveBeenCalledWith(expect.anything(), 60_000); }); + + it("preserves plugins already allowed by the gateway when configuring media", async () => { + readConfigSnapshotMock.mockResolvedValue({ + hash: "hash", + config: { plugins: { allow: ["openai", "anthropic", "qa-channel"] } }, + }); + + await ensureImageGenerationConfigured({ + providerMode: "mock-openai", + mock: { baseUrl: "http://127.0.0.1:9999" }, + transport: { requiredPluginIds: ["qa-channel"] }, + } as never); + + expect(patchConfigMock).toHaveBeenCalledWith( + expect.objectContaining({ + patch: expect.objectContaining({ + plugins: expect.objectContaining({ + allow: ["acpx", "memory-core", "openai", "anthropic", "qa-channel"], + }), + }), + }), + ); + }); }); diff --git a/extensions/qa-lab/src/suite-runtime-agent-media.ts b/extensions/qa-lab/src/suite-runtime-agent-media.ts index e77ef222f2f..1adb4a2b242 100644 --- a/extensions/qa-lab/src/suite-runtime-agent-media.ts +++ b/extensions/qa-lab/src/suite-runtime-agent-media.ts @@ -4,6 +4,7 @@ import { buildQaImageGenerationConfigPatch } from "./providers/image-generation. import { fetchJson, patchConfig, + readConfigSnapshot, waitForGatewayHealthy, waitForTransportReady, } from "./suite-runtime-gateway.js"; @@ -13,6 +14,19 @@ function extractMediaPathFromText(text: string | undefined): string | undefined return /MEDIA:([^\n]+)/.exec(text ?? "")?.[1]?.trim(); } +function readPluginAllow(config: Record) { + const plugins = config.plugins; + if (typeof plugins !== "object" || plugins === null || Array.isArray(plugins)) { + return []; + } + const allow = (plugins as { allow?: unknown }).allow; + return Array.isArray(allow) + ? allow.filter( + (pluginId): pluginId is string => typeof pluginId === "string" && pluginId.length > 0, + ) + : []; +} + async function resolveGeneratedImagePath(params: { env: Pick; promptSnippet: string; @@ -71,12 +85,14 @@ async function resolveGeneratedImagePath(params: { } async function ensureImageGenerationConfigured(env: QaSuiteRuntimeEnv) { + const snapshot = await readConfigSnapshot(env); await patchConfig({ env, patch: buildQaImageGenerationConfigPatch({ providerMode: env.providerMode, providerBaseUrl: env.mock ? `${env.mock.baseUrl}/v1` : undefined, requiredPluginIds: env.transport.requiredPluginIds, + existingPluginIds: readPluginAllow(snapshot.config), }), }); await waitForGatewayHealthy(env);