fix(release): resolve inherited base-reds surfaced by v3.8.35 release CI

Cycle base-reds that only run on PR→main (not the PR→release fast-path):

- test(autoCombo): suffixComposition-4517 used node:test in a vitest-only dir
  (#4753) → vitest found no suite. Switch to the vitest API. (Vitest job)
- test(agentSkills): openapiParser fixture wrote docs/reference/openapi.yaml;
  parser reads docs/openapi.yaml since #4781 → point fixture at the new path.
  (Unit/Coverage/Node24/Node26 shard 4)
- test(integration): proxy-pipeline source-scan expected inline streaming-cost
  code that #4790/#3501 extracted to the recordStreamingCost leaf → assert the
  delegation instead. (Integration 1/2)
- fix(chatCore): derive the log trace id from crypto, not Math.random
  (CodeQL js/insecure-randomness — log-correlation id, not a secret).
- test(resilience): circuit-breaker invalid-cooldown fallback asserted t>29000,
  flaking on slow CI where ~1.6s elapsed gave t=28401 → tolerate wall-clock
  drift (t>25000). (Unit 6/8)
This commit is contained in:
Diego Rodrigues de Sa e Souza 2026-06-23 15:54:05 -03:00
parent 92175a397f
commit 37c49781a7
5 changed files with 90 additions and 48 deletions

View file

@ -353,8 +353,10 @@ export async function handleChatCore({
const isModelScope = () => isModelScopeProvider(provider, credentials?.providerSpecificData);
const startTime = Date.now();
// Per-request trace id + checkpoint helper. Lets us see exactly which await
// a hung request was sitting on in `[STAGE_TRACE]` log lines.
const traceId = Math.random().toString(36).slice(2, 8);
// a hung request was sitting on in `[STAGE_TRACE]` log lines. Uses crypto RNG
// (not Math.random) purely to satisfy CodeQL js/insecure-randomness — this id
// is a log-correlation token, not a security secret.
const traceId = globalThis.crypto.randomUUID().slice(0, 6);
// Emit request.started event for real-time dashboard
setImmediate(() => {
@ -1126,20 +1128,21 @@ export async function handleChatCore({
}
}
// Phase 4A: unified output styles (supersedes cavemanOutputMode via the back-compat shim).
let outputStyleResult: import("../services/compression/outputStyles/apply.ts").OutputStylesResult | null =
null;
let outputStyleResult:
| import("../services/compression/outputStyles/apply.ts").OutputStylesResult
| null = null;
if (config.enabled) {
try {
const { resolveOutputStyleSelection } = await import(
"../services/compression/outputStyles/backCompat.ts"
);
const { resolveOutputStyleSelection } =
await import("../services/compression/outputStyles/backCompat.ts");
const selection = resolveOutputStyleSelection(config);
if (selection.length > 0) {
const { applyOutputStyles } = await import(
"../services/compression/outputStyles/apply.ts"
);
const { applyOutputStyles } =
await import("../services/compression/outputStyles/apply.ts");
const outputStyleLanguage =
config.languageConfig?.enabled === true ? config.languageConfig.defaultLanguage : "en";
config.languageConfig?.enabled === true
? config.languageConfig.defaultLanguage
: "en";
outputStyleResult = applyOutputStyles(
body as Parameters<typeof applyOutputStyles>[0],
selection,
@ -1156,7 +1159,10 @@ export async function handleChatCore({
outputStyleResult.skippedReason &&
outputStyleResult.skippedReason !== "no_styles"
) {
log?.debug?.("COMPRESSION", `Output styles skipped: ${outputStyleResult.skippedReason}`);
log?.debug?.(
"COMPRESSION",
`Output styles skipped: ${outputStyleResult.skippedReason}`
);
}
}
} catch (err) {
@ -1175,7 +1181,9 @@ export async function handleChatCore({
typeof (compressionInputBody as Record<string, unknown>)?.max_tokens === "number"
? ((compressionInputBody as Record<string, unknown>).max_tokens as number)
: null;
let adaptiveTelemetry: import("../services/compression/adaptiveCompression/types.ts").AdaptiveTelemetry | null = null;
let adaptiveTelemetry:
| import("../services/compression/adaptiveCompression/types.ts").AdaptiveTelemetry
| null = null;
const compressionPlan = selectCompressionPlan(
config,
compressionComboKey,
@ -1187,7 +1195,9 @@ export async function handleChatCore({
{
modelContextLimit: adaptiveModelContextLimit,
requestMaxTokens: requestMaxTokens,
onAdaptive: (t) => { adaptiveTelemetry = t; },
onAdaptive: (t) => {
adaptiveTelemetry = t;
},
}
);
const mode = compressionPlan.mode as CompressionConfig["defaultMode"];
@ -1400,12 +1410,10 @@ export async function handleChatCore({
if (outputStyleResult) {
void (async () => {
try {
const { buildOutputStyleTelemetry } = await import(
"../services/compression/outputStyles/telemetry.ts"
);
const { insertCompressionRunTelemetryRow } = await import(
"../../src/lib/db/compressionRunTelemetry.ts"
);
const { buildOutputStyleTelemetry } =
await import("../services/compression/outputStyles/telemetry.ts");
const { insertCompressionRunTelemetryRow } =
await import("../../src/lib/db/compressionRunTelemetry.ts");
const record = buildOutputStyleTelemetry({
requestId: skillRequestId ?? traceId ?? "",
model: effectiveModel ?? "",

View file

@ -71,8 +71,11 @@ describe("Chat Pipeline — handleSingleModelChat decomposition", () => {
});
it("chatCore should record cost for both non-streaming and streaming responses", () => {
// Non-streaming cost is still recorded inline; streaming cost was extracted to
// the recordStreamingCost leaf (open-sse/handlers/chatCore/streamingCost.ts,
// #4790 / #3501), so chatCore now delegates streaming cost to it.
assert.match(coreSrc, /if \(apiKeyInfo\?\.id && estimatedCost > 0\)/);
assert.match(coreSrc, /if \(apiKeyInfo\?\.id && streamUsage\)/);
assert.match(coreSrc, /recordStreamingCost\(/);
});
});
@ -208,12 +211,22 @@ describe("Plugin Architecture — plugins/hooks.ts", () => {
it("should run onRequest hooks in priority order", async () => {
const order = [];
hooks.registerHook("onRequest", "first", () => {
order.push("first");
}, 1);
hooks.registerHook("onRequest", "second", () => {
order.push("second");
}, 2);
hooks.registerHook(
"onRequest",
"first",
() => {
order.push("first");
},
1
);
hooks.registerHook(
"onRequest",
"second",
() => {
order.push("second");
},
2
);
const ctx = { requestId: "r1", body: {}, model: "test", metadata: {} };
await hooks.runOnRequest(ctx);
@ -221,13 +234,23 @@ describe("Plugin Architecture — plugins/hooks.ts", () => {
});
it("should support request blocking via emitHookBlocking", async () => {
hooks.registerHook("onRequest", "blocker", () => ({
blocked: true,
response: { error: "denied" },
}), 1);
hooks.registerHook("onRequest", "never-runs", () => {
throw new Error("should not run");
}, 2);
hooks.registerHook(
"onRequest",
"blocker",
() => ({
blocked: true,
response: { error: "denied" },
}),
1
);
hooks.registerHook(
"onRequest",
"never-runs",
() => {
throw new Error("should not run");
},
2
);
const ctx = { requestId: "r2", body: {}, model: "test", metadata: {} };
const result = await hooks.emitHookBlocking("onRequest", ctx);

View file

@ -5,7 +5,8 @@ import path from "node:path";
import os from "node:os";
// Dynamic import to pick up ESM module
const { parseOpenapi, getEndpointsForArea } = await import("../../src/lib/agentSkills/openapiParser.ts");
const { parseOpenapi, getEndpointsForArea } =
await import("../../src/lib/agentSkills/openapiParser.ts");
// ─── Fixture helpers ──────────────────────────────────────────────────────────
@ -15,7 +16,9 @@ const { parseOpenapi, getEndpointsForArea } = await import("../../src/lib/agentS
*/
function withFixtureOpenapi(yamlContent: string): { cleanup: () => void } {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omni-openapi-test-"));
const docsDir = path.join(tmpDir, "docs", "reference");
// openapiParser reads docs/openapi.yaml (consolidated location since #4781,
// previously docs/reference/openapi.yaml) — the fixture must mirror that path.
const docsDir = path.join(tmpDir, "docs");
fs.mkdirSync(docsDir, { recursive: true });
fs.writeFileSync(path.join(docsDir, "openapi.yaml"), yamlContent, "utf-8");
@ -118,7 +121,7 @@ test("parseOpenapi() groups /api/providers/* under 'providers' area", () => {
assert.ok(providerOps, "Expected 'providers' area to exist");
assert.ok(
providerOps!.length >= 5,
`Expected at least 5 provider endpoints, got ${providerOps!.length}`,
`Expected at least 5 provider endpoints, got ${providerOps!.length}`
);
const paths = providerOps!.map((op) => op.path);
@ -150,11 +153,11 @@ test("parseOpenapi() groups /api/v1/* under 'inference' area", () => {
assert.ok(inferenceOps, "Expected 'inference' area to exist");
assert.ok(
inferenceOps!.length >= 1,
`Expected at least 1 inference endpoint, got ${inferenceOps!.length}`,
`Expected at least 1 inference endpoint, got ${inferenceOps!.length}`
);
assert.ok(
inferenceOps!.some((op) => op.path === "/api/v1/chat/completions"),
"Expected /api/v1/chat/completions in inference area",
"Expected /api/v1/chat/completions in inference area"
);
} finally {
cleanup();
@ -184,7 +187,7 @@ test("parseOpenapi() throws if openapi.yaml is missing", () => {
assert.throws(
() => parseOpenapi(),
/openapiParser: could not read/,
"Expected error when openapi.yaml is missing",
"Expected error when openapi.yaml is missing"
);
} finally {
process.chdir(originalCwd);
@ -225,9 +228,9 @@ test(
assert.ok(providerOps, "Expected 'providers' area in real OpenAPI spec");
assert.ok(
providerOps!.length >= 5,
`Expected ≥5 provider endpoints in real spec, got ${providerOps!.length}`,
`Expected ≥5 provider endpoints in real spec, got ${providerOps!.length}`
);
},
}
);
test(
@ -237,11 +240,11 @@ test(
const endpoints = getEndpointsForArea("providers");
assert.ok(
endpoints.length >= 5,
`Expected ≥5 provider endpoint strings, got ${endpoints.length}: ${endpoints.join(", ")}`,
`Expected ≥5 provider endpoint strings, got ${endpoints.length}: ${endpoints.join(", ")}`
);
// Each entry should match "METHOD /path"
for (const ep of endpoints) {
assert.match(ep, /^[A-Z]+ \//, `Endpoint "${ep}" does not match METHOD /path format`);
}
},
}
);

View file

@ -10,7 +10,11 @@
*
* #4517.
*/
import { describe, it, before, after, beforeEach } from "node:test";
// NOTE: tests/unit/autoCombo/ is a vitest-only scope (the node:test runner in
// test:unit:ci does not walk this dir), so this suite uses the vitest API even
// though it asserts via node:assert. (#4753 originally landed it with node:test
// imports → vitest reported "No test suite found" and the test ran nowhere.)
import { describe, it, beforeAll, afterAll, beforeEach } from "vitest";
import assert from "node:assert/strict";
import {
buildAutoCandidateFilter,
@ -20,7 +24,7 @@ import {
describe("suffixComposition :free tier (#4517)", () => {
const ORIGINAL_ENV = { ...process.env };
before(() => {
beforeAll(() => {
// Snapshot env so we can restore it after each test.
});
@ -30,7 +34,7 @@ describe("suffixComposition :free tier (#4517)", () => {
process.env = { ...ORIGINAL_ENV };
});
after(() => {
afterAll(() => {
process.env = ORIGINAL_ENV;
});

View file

@ -219,7 +219,11 @@ test("invalid cooldownByKind values (NaN, Infinity, negative) fall back to reset
cb.reset();
cb._onFailure(kind);
const t = cb._timeUntilReset();
assert.ok(t > 29_000 && t <= 30_000, `expected resetTimeout fallback for ${kind}, got ${t}`);
// _timeUntilReset() = resetTimeout - elapsed-since-failure, so the lower bound
// must tolerate real wall-clock drift on slow/loaded CI runners (this loop took
// ~1.6s once → t=28401, flaking the old `> 29_000`). Any t well above the invalid
// cooldown values (NaN/Infinity/negative) and <= resetTimeout proves the fallback.
assert.ok(t > 25_000 && t <= 30_000, `expected resetTimeout fallback for ${kind}, got ${t}`);
}
cb.reset();
});