From 80b0f3c73b6332ffda3f7af717b720ec11a50da5 Mon Sep 17 00:00:00 2001 From: Jayesh Betala Date: Tue, 26 May 2026 21:56:19 +0530 Subject: [PATCH] fix(timeline): validate read limit --- bin/gstack-timeline-read | 12 +++++++++++- test/timeline.test.ts | 28 +++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/bin/gstack-timeline-read b/bin/gstack-timeline-read index 5c1b6bb6f..30773dd4c 100755 --- a/bin/gstack-timeline-read +++ b/bin/gstack-timeline-read @@ -17,12 +17,22 @@ BRANCH="" while [[ $# -gt 0 ]]; do case "$1" in --since) SINCE="$2"; shift 2 ;; - --limit) LIMIT="$2"; shift 2 ;; + --limit) + if [[ $# -lt 2 ]]; then + echo "gstack-timeline-read: --limit requires a positive integer" >&2 + exit 1 + fi + LIMIT="$2"; shift 2 ;; --branch) BRANCH="$2"; shift 2 ;; *) shift ;; esac done +if ! [[ "$LIMIT" =~ ^0*[1-9][0-9]*$ ]]; then + echo "gstack-timeline-read: --limit requires a positive integer" >&2 + exit 1 +fi + TIMELINE_FILE="$GSTACK_HOME/projects/$SLUG/timeline.jsonl" if [ ! -f "$TIMELINE_FILE" ]; then diff --git a/test/timeline.test.ts b/test/timeline.test.ts index 6d30a939e..105b828ac 100644 --- a/test/timeline.test.ts +++ b/test/timeline.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; -import { execFileSync, execSync, ExecSyncOptionsWithStringEncoding } from 'child_process'; +import { execFileSync, execSync, ExecSyncOptionsWithStringEncoding, spawnSync } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; @@ -56,6 +56,20 @@ function runReadArgs(args: string[] = []): string { } } +function runReadResult(args: string[] = []): { stdout: string; stderr: string; exitCode: number } { + const result = spawnSync(path.join(BIN, 'gstack-timeline-read'), args, { + cwd: ROOT, + env: { ...process.env, GSTACK_HOME: tmpDir }, + encoding: 'utf-8', + timeout: 15000, + }); + return { + stdout: (result.stdout || '').trim(), + stderr: (result.stderr || '').trim(), + exitCode: result.status ?? 1, + }; +} + beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-timeline-')); slugDir = path.join(tmpDir, 'projects'); @@ -176,4 +190,16 @@ describe('gstack-timeline-read', () => { expect(unlimitedEvents).toBe(5); expect(limitedEvents).toBe(2); }); + + test('rejects malformed --limit values', () => { + runLog(JSON.stringify({ skill: 'review', event: 'completed', branch: 'main', outcome: 'approved', ts: '2026-03-28T10:00:00Z' })); + + for (const value of ['1abc', 'nope', '0', '-1', '1.5']) { + const result = runReadResult(['--limit', value]); + + expect(result.exitCode).toBe(1); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain('gstack-timeline-read: --limit requires a positive integer'); + } + }); });