From 74cbcb38d2a4642194c883ad5d77049b4737bedf Mon Sep 17 00:00:00 2001 From: Nikhilesh Nanduri Date: Thu, 28 May 2026 02:59:17 +0530 Subject: [PATCH] fix(eval-list): reject malformed --limit values with exit 1 (#1683) parseInt("1abc") silently returned 1 (partial parse), and parseInt("nope") returned NaN causing slice(0, NaN) to show zero rows while the footer still reported the full count. Both are confusing silent failures. Fix: use Number.parseInt + Number.isSafeInteger + string round-trip check. Add 6 regression tests in test/eval-list-limit.test.ts. Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 22 +++++++++++++ VERSION | 2 +- package.json | 2 +- scripts/eval-list.ts | 10 +++++- test/eval-list-limit.test.ts | 60 ++++++++++++++++++++++++++++++++++++ 5 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 test/eval-list-limit.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dbc82f998..b1f915c06a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## [1.48.2.0] - 2026-05-28 + +**`eval:list --limit` now rejects malformed values instead of silently hiding runs.** + +`bun run eval:list -- --limit 1abc` used to silently show only 1 row; `--limit nope` would show zero rows while reporting the full count in the footer. Both cases now exit 1 with a clear error. Closes #1683. + +### The numbers that matter + +| Input | Before | After | +|-------|--------|-------| +| `--limit 1abc` | Shows 1 row, exit 0 | Error: must be positive integer, exit 1 | +| `--limit nope` | Shows 0 rows (NaN slice), exit 0 | Error: must be positive integer, exit 1 | +| `--limit 5` | Works | Works (unchanged) | + +### Itemized changes + +#### Fixed +- `scripts/eval-list.ts`: `--limit` parsing uses `Number.parseInt` + `Number.isSafeInteger` + string round-trip check. Malformed/negative/zero values print a clear stderr error and exit 1. + +#### For contributors +- `test/eval-list-limit.test.ts`: 6 gate-tier regression tests covering float, suffixed, non-numeric, zero, negative, and valid inputs. + ## [1.48.0.0] - 2026-05-26 ## **Agents stop dropping AskUserQuestion options when there are 5+.** A new canonical preamble rule + runtime gate makes Conductor's 4-option cap a split-or-batch decision, not a silent trim. diff --git a/VERSION b/VERSION index 01934fdf4c..b223e099d0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.48.0.0 +1.48.2.0 diff --git a/package.json b/package.json index eb77faa516..f42d16a724 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "1.48.0.0", + "version": "1.48.2.0", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "license": "MIT", "type": "module", diff --git a/scripts/eval-list.ts b/scripts/eval-list.ts index 12c5f0a943..76cc2f5ba2 100644 --- a/scripts/eval-list.ts +++ b/scripts/eval-list.ts @@ -21,7 +21,15 @@ let limit = 20; for (let i = 0; i < args.length; i++) { if (args[i] === '--branch' && args[i + 1]) { filterBranch = args[++i]; } else if (args[i] === '--tier' && args[i + 1]) { filterTier = args[++i]; } - else if (args[i] === '--limit' && args[i + 1]) { limit = parseInt(args[++i], 10); } + else if (args[i] === '--limit' && args[i + 1]) { + const raw = args[++i]; + const parsed = Number.parseInt(raw, 10); + if (!Number.isSafeInteger(parsed) || parsed < 1 || String(parsed) !== raw) { + console.error(`Error: --limit must be a positive integer, got: ${raw}`); + process.exit(1); + } + limit = parsed; + } } // Read eval files diff --git a/test/eval-list-limit.test.ts b/test/eval-list-limit.test.ts new file mode 100644 index 0000000000..f12ba818bc --- /dev/null +++ b/test/eval-list-limit.test.ts @@ -0,0 +1,60 @@ +import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; +import { spawnSync } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; + +const ROOT = path.resolve(import.meta.dir, '..'); +const SCRIPT = path.join(ROOT, 'scripts', 'eval-list.ts'); + +function run(args: string[], evalDir?: string): { stdout: string; stderr: string; exitCode: number } { + const result = spawnSync('bun', [SCRIPT, ...args], { + encoding: 'utf-8', + timeout: 15000, + env: evalDir ? { ...process.env, GSTACK_DEV_HOME: evalDir } : process.env, + }); + return { + stdout: result.stdout ?? '', + stderr: result.stderr ?? '', + exitCode: result.status ?? 1, + }; +} + +describe('eval-list --limit validation', () => { + test('rejects float (1.5) with exit 1', () => { + const { exitCode, stderr } = run(['--limit', '1.5']); + expect(exitCode).toBe(1); + expect(stderr).toContain('--limit'); + }); + + test('rejects suffix (1abc) with exit 1', () => { + const { exitCode, stderr } = run(['--limit', '1abc']); + expect(exitCode).toBe(1); + expect(stderr).toContain('--limit'); + }); + + test('rejects non-numeric (nope) with exit 1', () => { + const { exitCode, stderr } = run(['--limit', 'nope']); + expect(exitCode).toBe(1); + expect(stderr).toContain('--limit'); + }); + + test('rejects zero with exit 1', () => { + const { exitCode, stderr } = run(['--limit', '0']); + expect(exitCode).toBe(1); + expect(stderr).toContain('--limit'); + }); + + test('rejects negative with exit 1', () => { + const { exitCode, stderr } = run(['--limit', '-3']); + expect(exitCode).toBe(1); + expect(stderr).toContain('--limit'); + }); + + test('accepts valid positive integer without error', () => { + const { exitCode, stderr } = run(['--limit', '5']); + // May exit 0 (no eval dir) or print "No eval runs yet" — must NOT exit 1 + expect(exitCode).not.toBe(1); + expect(stderr).not.toContain('--limit must'); + }); +});