diff --git a/scripts/pre-commit/pnpm-audit-prod.mjs b/scripts/pre-commit/pnpm-audit-prod.mjs index 66503c6c4069..bbe06be257be 100644 --- a/scripts/pre-commit/pnpm-audit-prod.mjs +++ b/scripts/pre-commit/pnpm-audit-prod.mjs @@ -5,6 +5,8 @@ import { readFile } from "node:fs/promises"; import path from "node:path"; import process from "node:process"; import { pathToFileURL } from "node:url"; +// This zero-install hook runs on Node 22.22.3+, where native TypeScript stripping is enabled. +import { truncateUtf16Safe } from "../../packages/normalization-core/src/utf16-slice.ts"; import { readBoundedResponseText as readBoundedResponseTextWithLimit } from "../lib/bounded-response.mjs"; const DEFAULT_REGISTRY = "https://registry.npmjs.org"; @@ -783,7 +785,7 @@ export async function readBoundedBulkAdvisoryErrorText( text += decoder.decode(value, { stream: true }); if (text.length > maxChars) { - text = text.slice(0, maxChars); + text = truncateUtf16Safe(text, maxChars); truncated = true; break; } diff --git a/test/scripts/pnpm-audit-prod.test.ts b/test/scripts/pnpm-audit-prod.test.ts index 6338880c4896..da1c7f5899b0 100644 --- a/test/scripts/pnpm-audit-prod.test.ts +++ b/test/scripts/pnpm-audit-prod.test.ts @@ -248,6 +248,26 @@ snapshots: expect(text.length).toBeLessThan(4200); }); + it.each([ + { + caseName: "drops a split surrogate pair", + responseBody: `abc\u{1f600}tail`, + expectedText: "abc\n[truncated]", + }, + { + caseName: "preserves a complete surrogate pair", + responseBody: `ab\u{1f600}tail`, + expectedText: `ab\u{1f600}\n[truncated]`, + }, + ])( + "keeps bulk advisory error truncation UTF-16 safe: $caseName", + async ({ responseBody, expectedText }) => { + const response = new Response(responseBody, { status: 500 }); + + await expect(readBoundedBulkAdvisoryErrorText(response, 4)).resolves.toBe(expectedText); + }, + ); + it("aborts stalled bulk advisory requests", async () => { let signal: AbortSignal | undefined; const request = fetchBulkAdvisories({