From 054e734e53b9a44833a80ec7c6d3c6c41637fe7e Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 2 Jun 2026 21:06:14 +0200 Subject: [PATCH] fix(lint): reject malformed oxlint shard env --- scripts/run-oxlint-shards.mjs | 53 +++++++++++++++++++++++---------- test/scripts/run-oxlint.test.ts | 35 +++++++++++++++++----- 2 files changed, 66 insertions(+), 22 deletions(-) diff --git a/scripts/run-oxlint-shards.mjs b/scripts/run-oxlint-shards.mjs index c682716c9ec8..4c92d21dd187 100644 --- a/scripts/run-oxlint-shards.mjs +++ b/scripts/run-oxlint-shards.mjs @@ -102,15 +102,11 @@ export function createWindowsExtensionShards({ } export function resolveWindowsExtensionChunkSize(env = process.env) { - const rawValue = env.OPENCLAW_OXLINT_WINDOWS_EXTENSION_CHUNK_SIZE; - if (rawValue === undefined) { - return DEFAULT_WINDOWS_EXTENSION_CHUNK_SIZE; - } - - const parsedValue = Number.parseInt(rawValue, 10); - return Number.isFinite(parsedValue) && parsedValue > 0 - ? parsedValue - : DEFAULT_WINDOWS_EXTENSION_CHUNK_SIZE; + return resolvePositiveEnvIntWithFallback( + env, + "OPENCLAW_OXLINT_WINDOWS_EXTENSION_CHUNK_SIZE", + DEFAULT_WINDOWS_EXTENSION_CHUNK_SIZE, + ); } export function shouldRunOxlintShardsSerial({ @@ -505,22 +501,49 @@ export function resolveShardKillGraceMs(env) { function resolveNonNegativeEnvInt(env, key, defaultValue) { const rawValue = env[key]; - if (rawValue === undefined) { + if (rawValue === undefined || rawValue === "") { return defaultValue; } - const parsedValue = Number.parseInt(rawValue, 10); - return Number.isFinite(parsedValue) && parsedValue >= 0 ? parsedValue : defaultValue; + const text = String(rawValue).trim(); + if (!/^\d+$/u.test(text)) { + throw new Error(`${key} must be a non-negative integer; got: ${rawValue}`); + } + const parsedValue = Number(text); + if (!Number.isSafeInteger(parsedValue)) { + throw new Error(`${key} must be a non-negative integer; got: ${rawValue}`); + } + return parsedValue; } function resolvePositiveEnvInt(env, key) { const rawValue = env[key]; - if (rawValue === undefined) { + if (rawValue === undefined || rawValue === "") { return null; } - const parsedValue = Number.parseInt(rawValue, 10); - return Number.isFinite(parsedValue) && parsedValue > 0 ? parsedValue : null; + return parsePositiveEnvInt(rawValue, key); +} + +function resolvePositiveEnvIntWithFallback(env, key, defaultValue) { + const rawValue = env[key]; + if (rawValue === undefined || rawValue === "") { + return defaultValue; + } + + return parsePositiveEnvInt(rawValue, key); +} + +function parsePositiveEnvInt(rawValue, key) { + const text = String(rawValue).trim(); + if (!/^\d+$/u.test(text)) { + throw new Error(`${key} must be a positive integer; got: ${rawValue}`); + } + const parsedValue = Number(text); + if (!Number.isSafeInteger(parsedValue) || parsedValue <= 0) { + throw new Error(`${key} must be a positive integer; got: ${rawValue}`); + } + return parsedValue; } function signalChildProcess({ child, signal, useProcessGroup }) { diff --git a/test/scripts/run-oxlint.test.ts b/test/scripts/run-oxlint.test.ts index b0c4f54c20d8..a3c5ead81415 100644 --- a/test/scripts/run-oxlint.test.ts +++ b/test/scripts/run-oxlint.test.ts @@ -197,22 +197,38 @@ describe("run-oxlint", () => { splitCore: true, }), ).toBe(2); + + expect(() => + resolveOxlintShardConcurrency({ + env: { CI: "true", OPENCLAW_OXLINT_SHARD_CONCURRENCY: "2x" }, + platform: "linux", + hostResources: roomyHost, + splitCore: true, + }), + ).toThrow("OPENCLAW_OXLINT_SHARD_CONCURRENCY must be a positive integer; got: 2x"); }); it("uses a bounded oxlint shard heartbeat by default", () => { expect(resolveShardHeartbeatMs({})).toBe(30_000); expect(resolveShardHeartbeatMs({ OPENCLAW_OXLINT_SHARD_HEARTBEAT_MS: "0" })).toBe(0); expect(resolveShardHeartbeatMs({ OPENCLAW_OXLINT_SHARD_HEARTBEAT_MS: "5000" })).toBe(5000); - expect(resolveShardHeartbeatMs({ OPENCLAW_OXLINT_SHARD_HEARTBEAT_MS: "bad" })).toBe(30_000); + expect(() => resolveShardHeartbeatMs({ OPENCLAW_OXLINT_SHARD_HEARTBEAT_MS: "5000ms" })).toThrow( + "OPENCLAW_OXLINT_SHARD_HEARTBEAT_MS must be a non-negative integer; got: 5000ms", + ); }); it("uses a bounded oxlint shard timeout by default", () => { expect(resolveShardTimeoutMs({})).toBe(900_000); expect(resolveShardTimeoutMs({ OPENCLAW_OXLINT_SHARD_TIMEOUT_MS: "0" })).toBe(0); expect(resolveShardTimeoutMs({ OPENCLAW_OXLINT_SHARD_TIMEOUT_MS: "5000" })).toBe(5000); - expect(resolveShardTimeoutMs({ OPENCLAW_OXLINT_SHARD_TIMEOUT_MS: "bad" })).toBe(900_000); + expect(() => resolveShardTimeoutMs({ OPENCLAW_OXLINT_SHARD_TIMEOUT_MS: "1e3" })).toThrow( + "OPENCLAW_OXLINT_SHARD_TIMEOUT_MS must be a non-negative integer; got: 1e3", + ); expect(resolveShardKillGraceMs({})).toBe(5_000); expect(resolveShardKillGraceMs({ OPENCLAW_OXLINT_SHARD_KILL_GRACE_MS: "0" })).toBe(0); + expect(() => resolveShardKillGraceMs({ OPENCLAW_OXLINT_SHARD_KILL_GRACE_MS: "-1" })).toThrow( + "OPENCLAW_OXLINT_SHARD_KILL_GRACE_MS must be a non-negative integer; got: -1", + ); }); it("fails a stuck oxlint shard instead of waiting forever", async () => { @@ -518,14 +534,19 @@ describe("run-oxlint", () => { ]); }); - it("keeps the default Windows oxlint extension chunk size for invalid overrides", () => { + it("rejects invalid Windows oxlint extension chunk size overrides", () => { expect(resolveWindowsExtensionChunkSize({})).toBe(8); expect( - resolveWindowsExtensionChunkSize({ OPENCLAW_OXLINT_WINDOWS_EXTENSION_CHUNK_SIZE: "0" }), - ).toBe(8); + () => resolveWindowsExtensionChunkSize({ OPENCLAW_OXLINT_WINDOWS_EXTENSION_CHUNK_SIZE: "0" }), + ).toThrow("OPENCLAW_OXLINT_WINDOWS_EXTENSION_CHUNK_SIZE must be a positive integer; got: 0"); expect( - resolveWindowsExtensionChunkSize({ OPENCLAW_OXLINT_WINDOWS_EXTENSION_CHUNK_SIZE: "abc" }), - ).toBe(8); + () => + resolveWindowsExtensionChunkSize({ + OPENCLAW_OXLINT_WINDOWS_EXTENSION_CHUNK_SIZE: "8 chunks", + }), + ).toThrow( + "OPENCLAW_OXLINT_WINDOWS_EXTENSION_CHUNK_SIZE must be a positive integer; got: 8 chunks", + ); }); it("filters tracked targets missing from sparse checkouts", () => {