diff --git a/tests/unit/chatcore-plugin-onrequest.test.ts b/tests/unit/chatcore-plugin-onrequest.test.ts index c278e4c75a..bcc80dd628 100644 --- a/tests/unit/chatcore-plugin-onrequest.test.ts +++ b/tests/unit/chatcore-plugin-onrequest.test.ts @@ -56,6 +56,17 @@ test("no headers arg → backward compatible (undefined in ctx)", async () => { assert.equal(capturedCtx!.headers, undefined); }); +test("transcript sensitivity is propagated as server-owned plugin context", async () => { + let capturedCtx: Record | undefined; + registerHook("onRequest", PLUGIN, async (ctx: Record) => { + capturedCtx = ctx; + return {}; + }); + const gate = await runPluginOnRequestHook(baseArgs({ videoTranscriptSensitive: true })); + assert.equal(gate.blocked, false); + assert.equal(capturedCtx?.videoTranscriptSensitive, true); +}); + test("a blocking hook → blocked:true with a 403 JSON Response", async () => { registerHook("onRequest", PLUGIN, async () => ({ blocked: true, diff --git a/tests/unit/chatcore-plugin-onresponse.test.ts b/tests/unit/chatcore-plugin-onresponse.test.ts index 5125a07647..57bae0b7e5 100644 --- a/tests/unit/chatcore-plugin-onresponse.test.ts +++ b/tests/unit/chatcore-plugin-onresponse.test.ts @@ -6,7 +6,9 @@ import { test, after } from "node:test"; import assert from "node:assert/strict"; -const { registerHook, unregisterHook } = await import("../../src/lib/plugins/hooks.ts");const { runPluginOnResponseHook, runPluginOnStreamCompleteHook } = await import("../../open-sse/handlers/chatCore/pluginOnResponse.ts"); +const { registerHook, unregisterHook } = await import("../../src/lib/plugins/hooks.ts"); +const { runPluginOnResponseHook, runPluginOnStreamCompleteHook } = + await import("../../open-sse/handlers/chatCore/pluginOnResponse.ts"); async function waitFor(pred: () => boolean, timeoutMs = 2000): Promise { const deadline = Date.now() + timeoutMs; @@ -126,6 +128,27 @@ test("no headers arg → backward compatible (undefined in ctx)", async () => { assert.equal(captured!.headers, undefined); }); +test("transcript sensitivity is propagated as server-owned plugin context", async () => { + let captured: Record | undefined; + registerHook("onResponse", "test-onresponse-plugin", async (ctx: Record) => { + captured = ctx; + return {}; + }); + + await runPluginOnResponseHook({ + requestId: "req-transcript-sensitive", + body: { messages: [{ role: "user", content: "processed video request" }] }, + model: "gpt-4o", + provider: "openai", + apiKeyInfo: null, + response: { status: 200, data: { ok: true } }, + videoTranscriptSensitive: true, + }); + + await waitFor(() => captured !== undefined); + assert.equal(captured?.videoTranscriptSensitive, true); +}); + test("a throwing hook never rejects the caller (fail-open)", async () => { registerHook("onResponse", "test-onresponse-plugin", async () => { throw new Error("boom"); diff --git a/tests/unit/combo-10597-error-body-logging.test.ts b/tests/unit/combo-10597-error-body-logging.test.ts index f6924c64cc..92b8029dd2 100644 --- a/tests/unit/combo-10597-error-body-logging.test.ts +++ b/tests/unit/combo-10597-error-body-logging.test.ts @@ -264,3 +264,39 @@ test("transcript-sensitive round-robin masked-200 failures omit quality echoes f assert.equal(retainedLogs.includes(transcriptSentinel), false); assert.match(retainedLogs, /omitted: video transcript/); }); + +test("transcript-sensitive terminal logs omit the echo while the functional client error stays intact", async () => { + const transcriptSentinel = "PRIVATE_COMBO_TERMINAL_TRANSCRIPT_SENTINEL"; + const localWarnCalls: WarnCall[] = []; + const result = await handleComboChat({ + body: { model: "test", messages: [{ role: "user", content: "processed video request" }] }, + combo: { + name: "test-combo-10597-private-terminal", + strategy: "priority", + models: [{ model: "claude/private-video-terminal" }], + config: { maxRetries: 0 }, + }, + handleSingleModel: async () => + new Response(JSON.stringify({ error: { message: transcriptSentinel } }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }), + log: { + info: () => {}, + debug: () => {}, + error: () => {}, + warn: (tag: string, msg: string, meta?: unknown) => { + localWarnCalls.push({ tag, msg, meta }); + }, + }, + settings: {}, + allCombos: [], + videoTranscriptSensitive: true, + }); + + assert.equal(result.ok, false); + assert.match(await result.text(), new RegExp(transcriptSentinel)); + const retainedLogs = JSON.stringify(localWarnCalls); + assert.equal(retainedLogs.includes(transcriptSentinel), false); + assert.match(retainedLogs, /omitted: video transcript/); +}); diff --git a/tests/unit/combo-dispatch-prelude.test.ts b/tests/unit/combo-dispatch-prelude.test.ts index 283de6a8c7..150a413394 100644 --- a/tests/unit/combo-dispatch-prelude.test.ts +++ b/tests/unit/combo-dispatch-prelude.test.ts @@ -223,6 +223,36 @@ test("tryFusionDispatch: owns the request and synthesizes for the fusion strateg assert.ok(dispatched.includes("p/panelA") && dispatched.includes("p/panelB")); }); +test("tryFusionDispatch: propagates transcript sensitivity into native fusion logs", async () => { + const transcriptSentinel = "PRIVATE_FUSION_PRELUDE_TRANSCRIPT_SENTINEL"; + const ctx = setup({ + name: "private-fusion", + strategy: "fusion", + models: [{ model: "p/failing" }, { model: "p/healthy" }], + config: { minPanel: 1 }, + }); + const res = await tryFusionDispatch({ + body: ctx.body, + combo: ctx.combo, + cfg: ctx.config as unknown as Record, + config: ctx.config, + strategy: "fusion", + allCombos: [], + handleSingleModel: async () => okResponse("unused"), + handleSingleModelWithTimeout: async (_body, modelStr) => { + if (modelStr === "p/failing") throw new Error(transcriptSentinel); + return okResponse("safe answer"); + }, + log: ctx.log, + runCombo: async () => okResponse("recursed"), + videoTranscriptSensitive: true, + }); + assert.ok(res); + const retainedLogs = JSON.stringify(ctx.records); + assert.equal(retainedLogs.includes(transcriptSentinel), false); + assert.match(retainedLogs, /omitted: video transcript/); +}); + test("tryRuntimeUnitDispatch: falls through when the combo has no executable combo-ref", async () => { const ctx = setup({ name: "flat",