From f97dcf081a0ad6f2b2bc5f2fab63efdfdeb888de Mon Sep 17 00:00:00 2001 From: ttbombadil Date: Mon, 12 Oct 2026 11:55:12 +0200 Subject: [PATCH 1/2] fix: resolve findings from the new SonarQube 26.9 rules The Sonar way profiles gained about 50 rules on 2026-10-12; existing code violated 23 of them and blocked the pre-push gate. - sketch-facts: drop the redundant whitespace inside the array-size group, which caused super-linear backtracking; the group is trimmed anyway - scripts: startsWith, optional chains, explicit reduce seeds, safe stringification of unknown errors and ws RawData - tests: toHaveLength and getBy* presence assertions with the same expectations (approved by the owner) Co-Authored-By: Claude Opus 5.5 --- scripts/calibrate-capacity.ts | 2 +- scripts/capacity-calibration-policy.ts | 4 ++-- scripts/check-bundle-budget.mjs | 2 +- scripts/check-node-version.mjs | 2 +- scripts/deployment/deployment-support.ts | 10 ++++++++-- scripts/tutor-quality-real-provider-eval.ts | 4 ++-- scripts/validate-tutor-course-content.mjs | 2 +- server/services/tutor/curriculum/sketch-facts.ts | 2 +- tests/client/examples-menu.behavior.test.tsx | 2 +- tests/client/experimental-workspace-layout.test.tsx | 2 +- tests/client/tutor.test.tsx | 2 +- tests/integration/concurrent-50-clients.test.ts | 10 +++++----- tests/integration/runtime-output-storm.test.ts | 2 +- .../server/services/compiler-diagnostics-limit.test.ts | 2 +- .../evaluation/tutor-quality-real-provider-cli.test.ts | 2 +- tests/shared/source-project.test.ts | 2 +- 16 files changed, 29 insertions(+), 23 deletions(-) diff --git a/scripts/calibrate-capacity.ts b/scripts/calibrate-capacity.ts index afb40b69b..c6e643c65 100644 --- a/scripts/calibrate-capacity.ts +++ b/scripts/calibrate-capacity.ts @@ -709,7 +709,7 @@ function isClassroomComplete(classroom: ClassroomMeasurement | null, expectedUse } function buildQueueRecommendation(classroom: ClassroomMeasurement | null, complete: boolean, config: CalibrationCliConfig): Recommendation { - if (complete && classroom && classroom.queueWaitMs.length === config.expectedUsers) { + if (complete && classroom?.queueWaitMs.length === config.expectedUsers) { return recommendQueueTimeout(classroom.queueP95Ms, classroom.queueP99Ms, classroom.queueMaxMs, config.maxUserWaitSec); } return { diff --git a/scripts/capacity-calibration-policy.ts b/scripts/capacity-calibration-policy.ts index fa3b540a2..65e92f0b2 100644 --- a/scripts/capacity-calibration-policy.ts +++ b/scripts/capacity-calibration-policy.ts @@ -171,7 +171,7 @@ export function selectActiveRecommendation( const stable = measurements.filter((measurement) => activeIsStable(measurement, options)); const target = stable.filter((measurement) => (measurement.cpuP95Percent ?? Infinity) <= options.targetCpuPercent); if (target.length > 0) { - const selected = target.reduce((best, current) => current.requested > best.requested ? current : best); + const selected = target.reduce((best, current) => current.requested > best.requested ? current : best, target[0]); return { value: selected.requested, status: "recommended", @@ -181,7 +181,7 @@ export function selectActiveRecommendation( }; } if (stable.length > 0) { - const selected = stable.reduce((best, current) => current.requested > best.requested ? current : best); + const selected = stable.reduce((best, current) => current.requested > best.requested ? current : best, stable[0]); return { value: selected.requested, status: "recommended", diff --git a/scripts/check-bundle-budget.mjs b/scripts/check-bundle-budget.mjs index 2a349c95a..1c640fca0 100644 --- a/scripts/check-bundle-budget.mjs +++ b/scripts/check-bundle-budget.mjs @@ -16,7 +16,7 @@ const largest = Math.max(...sizes.map((file) => file.bytes), 0); // 700 kB limit reflected Vite 5 chunk placement. Recalibrated together with the initial-path check below. const limits = { total: 6_000_000, largest: 4_500_000, initial: 710_000 }; const initial = sizes - .filter((file) => /^index-/.test(file.name)) + .filter((file) => file.name.startsWith("index-")) .reduce((sum, file) => sum + file.bytes, 0); // Initial load path: static import closure of the HTML entry, from the Vite build manifest. diff --git a/scripts/check-node-version.mjs b/scripts/check-node-version.mjs index bbf5c50a3..0dd76df14 100644 --- a/scripts/check-node-version.mjs +++ b/scripts/check-node-version.mjs @@ -1,7 +1,7 @@ import { readFile } from "node:fs/promises"; const expected = (await readFile(".nvmrc", "utf8")).trim(); const declared = JSON.parse(await readFile("package.json", "utf8")).engines?.node; -if (!declared || !declared.includes(expected)) { +if (!declared?.includes(expected)) { console.error(`Node mismatch: expected ${expected}, engines=${declared}, runtime=${process.version}`); process.exit(1); } diff --git a/scripts/deployment/deployment-support.ts b/scripts/deployment/deployment-support.ts index 959c12417..4c1f9501c 100644 --- a/scripts/deployment/deployment-support.ts +++ b/scripts/deployment/deployment-support.ts @@ -154,7 +154,7 @@ export async function waitFor( } await sleep(500); } - const suffix = lastError ? `: ${String(lastError)}` : ""; + const suffix = lastError ? `: ${lastError instanceof Error ? String(lastError) : JSON.stringify(lastError)}` : ""; throw new Error(`Timed out waiting for ${description}${suffix}`); } @@ -236,6 +236,12 @@ export function openWebSocket( }); } +/** `ws` delivers text frames as Buffer, ArrayBuffer or fragment list depending on its options. */ +function rawDataToString(raw: WebSocket.RawData): string { + if (Array.isArray(raw)) return Buffer.concat(raw).toString("utf8"); + return Buffer.from(raw).toString("utf8"); +} + export function nextWebSocketMessage( socket: WebSocket, predicate: (message: Record) => boolean, @@ -248,7 +254,7 @@ export function nextWebSocketMessage( }, timeoutMs); const onMessage = (raw: WebSocket.RawData) => { try { - const message = JSON.parse(raw.toString()) as Record; + const message = JSON.parse(rawDataToString(raw)) as Record; if (!predicate(message)) return; clearTimeout(timer); socket.removeListener("message", onMessage); diff --git a/scripts/tutor-quality-real-provider-eval.ts b/scripts/tutor-quality-real-provider-eval.ts index f137721c9..702d367ea 100644 --- a/scripts/tutor-quality-real-provider-eval.ts +++ b/scripts/tutor-quality-real-provider-eval.ts @@ -251,10 +251,10 @@ export function isTutorQualityRelevantUntrackedPath(relativePath: string, output export function readTutorQualityGitState(cwd: string, outputDir: string): TutorQualityGitState { const sha = execFileSync(resolveGitExecutable(), ["rev-parse", "HEAD"], { cwd, encoding: "utf8" }).trim(); const lines = statusLines(cwd); - const trackedClean = lines.every((line) => line.slice(0, 2) === "??"); + const trackedClean = lines.every((line) => line.startsWith("??")); const outputRelative = path.relative(cwd, path.resolve(cwd, outputDir)); const relevantUntrackedClean = lines.every((line) => { - if (line.slice(0, 2) !== "??") return true; + if (!line.startsWith("??")) return true; return !isTutorQualityRelevantUntrackedPath(line.slice(3).trim(), outputRelative); }); return { sha, trackedClean, relevantUntrackedClean }; diff --git a/scripts/validate-tutor-course-content.mjs b/scripts/validate-tutor-course-content.mjs index 96986ecda..849f08101 100644 --- a/scripts/validate-tutor-course-content.mjs +++ b/scripts/validate-tutor-course-content.mjs @@ -2,7 +2,7 @@ import path from "node:path"; import { validateTutorCourseContentDirectory } from "../server/services/course-content/tutor-quality-directory-validator.ts"; const directory = process.argv[2]; -if (directory === undefined || directory.length === 0) { +if (!directory) { console.error("Usage: npm run validate:tutor-course-content -- "); process.exitCode = 2; } else { diff --git a/server/services/tutor/curriculum/sketch-facts.ts b/server/services/tutor/curriculum/sketch-facts.ts index 1eaa41c1f..c4baba0fc 100644 --- a/server/services/tutor/curriculum/sketch-facts.ts +++ b/server/services/tutor/curriculum/sketch-facts.ts @@ -25,7 +25,7 @@ const TYPE_ALIASES: Readonly> = { }; const TYPE_PATTERN = /\b(?:(?:unsigned|signed)\s+)?(?:char|byte|uint8_t|int|long|float)\b/g; -const ARRAY_PATTERN = /\b(char|byte|uint8_t|int|long|float)\s+([A-Za-z_]\w*)\s*\[\s*([^\]]*)\s*\]/g; +const ARRAY_PATTERN = /\b(char|byte|uint8_t|int|long|float)\s+([A-Za-z_]\w*)\s*\[([^\]]*)\]/g; const INITIALIZER_PATTERN = /^\s*=\s*\{([^}]*)\}/; const SERIAL_PATTERN = /\bSerial\s*\.\s*(print|println|write)\s*\(/gi; diff --git a/tests/client/examples-menu.behavior.test.tsx b/tests/client/examples-menu.behavior.test.tsx index 79023fc07..9098e15c9 100644 --- a/tests/client/examples-menu.behavior.test.tsx +++ b/tests/client/examples-menu.behavior.test.tsx @@ -98,7 +98,7 @@ describe("ExamplesMenu behavior", () => { expect(screen.getByText("Load Example")).toHaveClass("ui-type-menu-title"); fireEvent.click(screen.getByRole("button", { name: "Built-in" })); expect( - screen.queryByRole("button", { name: "Built-in", exact: true }), + screen.getByRole("button", { name: "Built-in", exact: true }), ).toBeInTheDocument(); expect( screen.queryByRole("button", { name: "Other" }), diff --git a/tests/client/experimental-workspace-layout.test.tsx b/tests/client/experimental-workspace-layout.test.tsx index b6f41f8c4..9a1f82b3e 100644 --- a/tests/client/experimental-workspace-layout.test.tsx +++ b/tests/client/experimental-workspace-layout.test.tsx @@ -398,6 +398,6 @@ describe("experimental workspace layout", () => { { credential: "volatile-key" }, { credential: "volatile-key" }, ]); - expect(localStorage.length).toBe(0); + expect(localStorage).toHaveLength(0); }); }); diff --git a/tests/client/tutor.test.tsx b/tests/client/tutor.test.tsx index 3179b2aa8..9ed229027 100644 --- a/tests/client/tutor.test.tsx +++ b/tests/client/tutor.test.tsx @@ -158,7 +158,7 @@ describe("useTutor", () => { difficulty: 30, }); expect(result.current.question?.question).toBe("Welche Ausgabe erwartest du?"); - expect(localStorage.length).toBe(0); + expect(localStorage).toHaveLength(0); }); it("resets a pinned dialog when the active Course Content revision changes", async () => { diff --git a/tests/integration/concurrent-50-clients.test.ts b/tests/integration/concurrent-50-clients.test.ts index 8c2cf10ca..37ae28c9b 100644 --- a/tests/integration/concurrent-50-clients.test.ts +++ b/tests/integration/concurrent-50-clients.test.ts @@ -421,10 +421,10 @@ void loop() { digitalWrite(13, HIGH); Serial.println("LED ON"); delay(500); digi const errored = results.filter((r) => r.errors.length > 0); // All must compile - expect(compiled.length).toBe(N); + expect(compiled).toHaveLength(N); // All must connect via WS - expect(connected.length).toBe(N); + expect(connected).toHaveLength(N); // All must receive serial output (the key user-visible requirement) if (gotOutput.length < N) { @@ -439,10 +439,10 @@ void loop() { digitalWrite(13, HIGH); Serial.println("LED ON"); delay(500); digi } // All must see simulation end - expect(gotStopped.length).toBe(N); + expect(gotStopped).toHaveLength(N); // No errors - expect(errored.length).toBe(0); + expect(errored).toHaveLength(0); }, 30_000); // ─── Per-client isolation: different code per client ─────────────────── @@ -464,7 +464,7 @@ void loop() { digitalWrite(13, HIGH); Serial.println("LED ON"); delay(500); digi const gotOutput = results.filter( (r) => r.compiled && r.receivedSerialOutput, ); - expect(gotOutput.length).toBe(N); + expect(gotOutput).toHaveLength(N); // Each client MUST see its own marker in the serial output. // This fails if the server uses a global shared lastCompiledCode diff --git a/tests/integration/runtime-output-storm.test.ts b/tests/integration/runtime-output-storm.test.ts index e433d9cca..1b227a998 100644 --- a/tests/integration/runtime-output-storm.test.ts +++ b/tests/integration/runtime-output-storm.test.ts @@ -74,7 +74,7 @@ maybeDescribe("runtime output storm", () => { expect(floodLines).toBeLessThan(1_000); expect(errors.filter((line) => line.includes("Runtime output rate limit"))).toHaveLength(1); // Without aggregation every line produced its own diagnostic log entry. - expect(logCalls.filter((message) => message.includes("STDERR")).length).toBe(0); + expect(logCalls.filter((message) => message.includes("STDERR"))).toHaveLength(0); expect(logCalls.length).toBeLessThan(200); }, 90_000); }); diff --git a/tests/server/services/compiler-diagnostics-limit.test.ts b/tests/server/services/compiler-diagnostics-limit.test.ts index dd08c349d..82c3295fc 100644 --- a/tests/server/services/compiler-diagnostics-limit.test.ts +++ b/tests/server/services/compiler-diagnostics-limit.test.ts @@ -27,7 +27,7 @@ describe("compiler diagnostics limits", () => { const chunk = "x".repeat(100_000); for (let index = 0; index < 10; index += 1) appendCompilerDiagnostics(buffer, chunk); - expect(buffer.value.length).toBe(MAX_COMPILER_DIAGNOSTIC_CHARS); + expect(buffer.value).toHaveLength(MAX_COMPILER_DIAGNOSTIC_CHARS); expect(buffer.omittedChars).toBe(1_000_000 - MAX_COMPILER_DIAGNOSTIC_CHARS); expect(limitCompilerDiagnostics(buffer.value, buffer.omittedChars)).toMatch(/\.\.\. 737856 more characters/); }); diff --git a/tests/server/services/tutor/evaluation/tutor-quality-real-provider-cli.test.ts b/tests/server/services/tutor/evaluation/tutor-quality-real-provider-cli.test.ts index 443658d2a..f3b247cd6 100644 --- a/tests/server/services/tutor/evaluation/tutor-quality-real-provider-cli.test.ts +++ b/tests/server/services/tutor/evaluation/tutor-quality-real-provider-cli.test.ts @@ -146,7 +146,7 @@ describe("Tutor Quality real-provider CLI contract", () => { expect(transcripts.find(({ scenario }) => scenario.id === "TQ-REG-001")?.stateAfter) .toEqual(transcripts.find(({ scenario }) => scenario.id === "TQ-REG-001")?.stateBefore); const strategyTranscripts = transcripts.filter(({ scenario }) => scenario.id.startsWith("strategy-")); - expect(strategyTranscripts.length).toBe(scenarios.filter(({ id }) => id.startsWith("strategy-")).length); + expect(strategyTranscripts).toHaveLength(scenarios.filter(({ id }) => id.startsWith("strategy-")).length); expect(strategyTranscripts.every(({ turns }) => turns.every(({ deterministicChecks }) => deterministicChecks.some( ({ name, passed }) => name === "expected-learning-phase" && passed, )))).toBe(true); diff --git a/tests/shared/source-project.test.ts b/tests/shared/source-project.test.ts index c21365fae..11aa24847 100644 --- a/tests/shared/source-project.test.ts +++ b/tests/shared/source-project.test.ts @@ -54,7 +54,7 @@ describe("source project validation and include resolution", () => { ].join("\n"), })); - expect(result.lineOrigins.length).toBe(result.source.split("\n").length); + expect(result.lineOrigins).toHaveLength(result.source.split("\n").length); expect(result.lineOrigins).toEqual([ { file: "a.h", line: 1 }, { file: "sub/b.h", line: 1 }, From 78575086037c54b473765569b5f54363b2b44e28 Mon Sep 17 00:00:00 2001 From: ttbombadil Date: Mon, 12 Oct 2026 11:58:27 +0200 Subject: [PATCH 2/2] refactor(scripts): replace a nested ternary in waitFor with a helper Co-Authored-By: Claude Opus 5.5 --- scripts/deployment/deployment-support.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scripts/deployment/deployment-support.ts b/scripts/deployment/deployment-support.ts index 4c1f9501c..c7d56312e 100644 --- a/scripts/deployment/deployment-support.ts +++ b/scripts/deployment/deployment-support.ts @@ -139,6 +139,10 @@ export async function httpsRequest( }); } +function describeError(error: unknown): string { + return error instanceof Error ? String(error) : JSON.stringify(error); +} + export async function waitFor( description: string, probe: () => Promise, @@ -154,7 +158,7 @@ export async function waitFor( } await sleep(500); } - const suffix = lastError ? `: ${lastError instanceof Error ? String(lastError) : JSON.stringify(lastError)}` : ""; + const suffix = lastError ? `: ${describeError(lastError)}` : ""; throw new Error(`Timed out waiting for ${description}${suffix}`); }