refactor(chatCore): extrai runPluginOnResponseHook (#3501) (#4782)

chatCore #3501: extract runPluginOnResponseHook. Integrated into release/v3.8.35.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-23 10:12:57 -03:00
committed by GitHub
parent 8a28a3ae5b
commit 2566f44f60
3 changed files with 109 additions and 9 deletions

View File

@@ -170,6 +170,7 @@ import { recordCompressionCacheStats } from "./chatCore/compressionCacheStats.ts
import { writeCavemanOutputAnalytics } from "./chatCore/cavemanOutputAnalytics.ts";
import { scheduleQuotaShareConsumption } from "./chatCore/quotaShareConsumption.ts";
import { emitRequestGamificationEvent } from "./chatCore/gamificationEvent.ts";
import { runPluginOnResponseHook } from "./chatCore/pluginOnResponse.ts";
import {
appendNonStreamingSseTerminalSignal,
type NonStreamingSseTerminalState,
@@ -4205,15 +4206,7 @@ export async function handleChatCore({
await emitRequestGamificationEvent({ apiKeyId: apiKeyInfo?.id, model, provider });
// ── Plugin onResponse hook (fire-and-forget) ──
try {
const { runOnResponse } = await import("@/lib/plugins/hooks");
runOnResponse(
{ requestId: traceId, body, model, provider, apiKeyInfo, metadata: {} },
{ status: 200 }
).catch(() => {});
} catch (_) {
/* plugin onResponse optional */
}
await runPluginOnResponseHook({ requestId: traceId, body, model, provider, apiKeyInfo });
return {
success: true,

View File

@@ -0,0 +1,34 @@
/**
* chatCore plugin onResponse hook (Quality Gate v2 / Fase 9 — chatCore god-file decomposition,
* #3501).
*
* Extracted from handleChatCore's streaming finalization: runs the registered plugin `onResponse`
* hooks for a completed (status 200) response. Fire-and-forget and fail-open — the inner run is
* not awaited and both the dynamic import and the run swallow their own errors, so a misbehaving
* plugin never affects the response. Behaviour is byte-identical to the previous inline block.
*/
export async function runPluginOnResponseHook(args: {
requestId: string;
body: unknown;
model: string | null | undefined;
provider: string | null | undefined;
apiKeyInfo: unknown;
}): Promise<void> {
try {
const { runOnResponse } = await import("@/lib/plugins/hooks");
runOnResponse(
{
requestId: args.requestId,
body: args.body,
model: args.model,
provider: args.provider,
apiKeyInfo: args.apiKeyInfo,
metadata: {},
},
{ status: 200 }
).catch(() => {});
} catch (_) {
/* plugin onResponse optional */
}
}

View File

@@ -0,0 +1,73 @@
// Characterization of runPluginOnResponseHook — the plugin onResponse hook extracted from
// handleChatCore's streaming finalization (chatCore god-file decomposition, #3501). Hooks are
// in-memory (no DB). Locks: a registered onResponse hook receives the request context + status-200
// response, and the helper is fire-and-forget / fail-open (no registered hooks → no-op).
import { test, after } from "node:test";
import assert from "node:assert/strict";
const { registerHook, unregisterHook } = await import("../../src/lib/plugins/hooks.ts");
const { runPluginOnResponseHook } = await import(
"../../open-sse/handlers/chatCore/pluginOnResponse.ts"
);
async function waitFor(pred: () => boolean, timeoutMs = 2000): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline && !pred()) {
await new Promise((r) => setTimeout(r, 10));
}
}
after(() => {
unregisterHook("onResponse", "test-onresponse-plugin");
});
test("no registered hooks → resolves without throwing (no-op)", async () => {
await assert.doesNotReject(
runPluginOnResponseHook({
requestId: "req-noop",
body: { messages: [] },
model: "gpt-x",
provider: "openai",
apiKeyInfo: null,
})
);
});
test("registered onResponse hook receives the request context and status-200 response", async () => {
let captured: Record<string, unknown> | undefined;
registerHook("onResponse", "test-onresponse-plugin", async (ctx: Record<string, unknown>) => {
captured = ctx;
return {};
});
await runPluginOnResponseHook({
requestId: "req-42",
body: { messages: [{ role: "user", content: "hi" }] },
model: "gpt-4o",
provider: "openai",
apiKeyInfo: { id: "key-1" },
});
await waitFor(() => captured !== undefined);
assert.ok(captured, "expected the onResponse hook to be invoked");
assert.equal(captured!.requestId, "req-42");
assert.equal(captured!.model, "gpt-4o");
assert.equal(captured!.provider, "openai");
assert.deepEqual(captured!.response, { status: 200 });
});
test("a throwing hook never rejects the caller (fail-open)", async () => {
registerHook("onResponse", "test-onresponse-plugin", async () => {
throw new Error("boom");
});
await assert.doesNotReject(
runPluginOnResponseHook({
requestId: "req-throw",
body: {},
model: "m",
provider: "p",
apiKeyInfo: null,
})
);
await new Promise((r) => setTimeout(r, 30));
});