Skip to content

feat(output): add csv and ndjson output modes for list commands#278

Open
ISTHA1607 wants to merge 1 commit into
TestSprite:mainfrom
ISTHA1607:feat/csv-ndjson-output
Open

feat(output): add csv and ndjson output modes for list commands#278
ISTHA1607 wants to merge 1 commit into
TestSprite:mainfrom
ISTHA1607:feat/csv-ndjson-output

Conversation

@ISTHA1607

@ISTHA1607 ISTHA1607 commented Jul 23, 2026

Copy link
Copy Markdown

Summary

  • Extends --output to accept csv/ndjson in addition to json/text, for project list, test list, and test result --history.
  • Lets users pipe CLI output directly into spreadsheets or log/JSON-lines pipelines without post-processing.

Closes #164

Test plan

  • npm test: 2019 passed, 3 skipped, 7 failed all 7 failures are in src/lib/ticker.spec.ts and reproduce identically on a clean main checkout (unrelated pre-existing flakiness, confirmed before opening this PR).
  • npx eslint on changed files: clean.

Extends --output to accept csv/ndjson in addition to json/text, for
project list, test list, and test result --history. Enables piping
CLI output directly into spreadsheets/log pipelines without custom
parsing.
@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown

✅ This PR is linked to an issue assigned to @ISTHA1607 — thanks! The needs-issue label has been removed.

@github-actions github-actions Bot added the needs-issue PR not linked to an issue yet — please open one first and claim it (see CONTRIBUTING) label Jul 23, 2026
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

CSV and NDJSON output modes are added for supported list commands. Shared renderers validate modes, escape CSV fields, emit newline-delimited JSON, and keep pagination metadata on stderr. Project and test commands now use command-specific column mappings and expanded validation tests.

Changes

CSV and NDJSON list output

Layer / File(s) Summary
Output mode and renderer contracts
src/lib/output.ts, src/index.ts, src/commands/doctor.test.ts
Output validation accepts json, text, csv, and ndjson; shared CSV/NDJSON renderers are added; non-list commands reject list-only modes. CLI help and validation expectations are updated.
Project list structured output
src/commands/project.ts, src/commands/project.test.ts
project list emits mapped CSV or NDJSON rows to stdout, routes nextToken to stderr, and tests quoting, empty pages, and scalar-command validation.
Test list and history structured output
src/commands/test.ts
test list and test result --history gain CSV/NDJSON row mappings and route cursors and history metadata to stderr.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: ruili-testsprite

Sequence Diagram(s)

sequenceDiagram
  participant Command as project list or test list
  participant Output
  participant stdout
  participant stderr
  Command->>Output: render CSV or NDJSON rows
  Output->>stdout: write parseable row data
  Command->>stderr: write pagination cursor and metadata
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding CSV and NDJSON output modes for list commands.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot removed the needs-issue PR not linked to an issue yet — please open one first and claim it (see CONTRIBUTING) label Jul 23, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/output.ts`:
- Around line 50-61: Update the NDJSON rendering flow around renderNdjson and
printNdjson to use the same selected ListColumn projections as CSV, emitting
only each column’s header/value fields per row. If the intended contract is
raw-row JSON instead, explicitly document that behavior in the relevant comments
and documentation rather than implying column alignment.
- Around line 69-76: Update csvEscapeField to neutralize spreadsheet
formula-triggering values by prefixing the required safe character when the
rendered field begins with =, +, -, @, tab, or CR, then apply CSV
quoting/escaping to the resulting literal value. Preserve null/undefined
handling and existing RFC 4180 behavior for all other fields.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4ac9c48e-c169-4bde-ad63-9a13d7055f67

📥 Commits

Reviewing files that changed from the base of the PR and between 26821e1 and cb8b8f5.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json, !package-lock.json
  • test/__snapshots__/help.snapshot.test.ts.snap is excluded by !**/*.snap, !**/*.snap
📒 Files selected for processing (6)
  • src/commands/doctor.test.ts
  • src/commands/project.test.ts
  • src/commands/project.ts
  • src/commands/test.ts
  • src/index.ts
  • src/lib/output.ts

Comment thread src/lib/output.ts
Comment on lines +50 to +61
/**
* One column of a CSV/NDJSON list rendering, derived straight from a
* source-of-truth wire type (e.g. `CliProject`, `CliTest`,
* `RunHistoryItem`) rather than the (possibly reordered/truncated) text
* table columns used for `--output text`.
*/
export interface ListColumn<T> {
/** CSV header cell / JSON-ish field name for this column. */
header: string;
/** Extract the raw value for this column from one row. */
value: (row: T) => unknown;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Compare CliTest's field set against TEST_CSV_COLUMNS headers.
ast-grep run --pattern 'interface CliTest { $$$ }' --lang typescript src/commands/test.ts
rg -n 'TEST_CSV_COLUMNS' -A 20 src/commands/test.ts | head -30

Repository: TestSprite/testsprite-cli

Length of output: 4298


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== output.ts outline =="
ast-grep outline src/lib/output.ts --view expanded || true

echo "== relevant output.ts sections =="
sed -n '1,80p;150,225p' src/lib/output.ts | cat -n

echo "== TEST_CSV_COLUMNS full =="
sed -n '10245,10275p' src/commands/test.ts | cat -n

echo "== CSV table constants =="
rg -n ': ReadonlyArray<ListColumn<.*>>(?:\n[ \t]+\$|=)' src -g '*.ts' | head -20 || true

echo "== print/select implementations =="
rg -n 'function print|const print|renderNdjson|printNdjson|printCsv|renderCsv' src/lib/output.ts src/commands -g '*.ts' -A 25 -B 5

echo "== tests around NDJSON =="
rg -n 'ndjson|printNdjson|renderNdjson|ndjson|PROJECT_CSV_COLUMNS|TEST_CSV_COLUMNS' src -g '*test*.ts' -A 8 -B 4

Repository: TestSprite/testsprite-cli

Length of output: 50382


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Ndjson list tests =="
rg -n 'output: '\''ndjson'\''|output: "\'ndjson\'"|ndjson|printNdjson|renderNdjson' src/commands src/lib -g '*.ts' -A 20 -B 8 | head -220

echo "== CSV constants and list fixtures =="
rg -n '(PROJECT|TEST|RUN_HISTORY)_CSV_COLUMNS' src/commands/src/lib -g '*.ts' -A 35
echo "== CliProject full =="
rg -n 'interface CliProject|export interface CliProject' src/commands -g '*.ts' -A 120 | head -180
echo "== RunHistoryItem =="
rg -n 'interface RunHistoryItem|export interface RunHistoryItem' src/commands src/lib -g '*.ts' -A 120 | head -160

Repository: TestSprite/testsprite-cli

Length of output: 248


Keep NDJSON output aligned with selected columns

renderNdjson writes JSON.stringify(row), and printNdjson receives no ListColumn selector, so list NDJSON renders every serializable field even though CSV uses selected column projections. If NDJSON is meant to mirror the documented CSV field set, pass/select columns into the NDJSON path; otherwise, make NDJSON raw-row output explicit in docs/comments.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/output.ts` around lines 50 - 61, Update the NDJSON rendering flow
around renderNdjson and printNdjson to use the same selected ListColumn
projections as CSV, emitting only each column’s header/value fields per row. If
the intended contract is raw-row JSON instead, explicitly document that behavior
in the relevant comments and documentation rather than implying column
alignment.

Comment thread src/lib/output.ts
Comment on lines +69 to +76
export function csvEscapeField(raw: unknown): string {
if (raw === null || raw === undefined) return '';
const s = String(raw);
if (/[",\r\n]/.test(s)) {
return `"${s.replace(/"/g, '""')}"`;
}
return s;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## locate output.ts and relevant files"
fd -a 'output\.ts$|samples\.ts$|DOCUMENTATION\.md$' . | sed 's#^\./##' | sort | head -80

echo
echo "## git diff stat"
git diff --stat || true

echo
echo "## inspect src/lib/output.ts around csvEscapeField"
if [ -f src/lib/output.ts ]; then
  wc -l src/lib/output.ts
  sed -n '1,180p' src/lib/output.ts | cat -n
fi

echo
echo "## inspect usage of csvEscapeField/printCsv"
rg -n "csvEscapeField|printCsv|output mode|csv|ndjson" src lib . --glob '!node_modules' --glob '!dist' --glob '!build' | head -200

echo
echo "## inspect relevant sample/dry-run list files"
if [ -f src/lib/dry-run/samples.ts ]; then
  wc -l src/lib/dry-run/samples.ts
  sed -n '1,220p' src/lib/dry-run/samples.ts | cat -n
fi

Repository: TestSprite/testsprite-cli

Length of output: 22427


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## inspect CSV column definitions in project.ts and test.ts"
sed -n '90,1125p' src/commands/project.ts | cat -n
echo
sed -n '4645,4820p' src/commands/test.ts | cat -n
echo
sed -n '10200,10340p' src/commands/test.ts | cat -n

echo
echo "## inspect dry-run sample CSV values for formula-like triggers"
python3 - <<'PY'
from pathlib import Path
p = Path('src/lib/dry-run/samples.ts')
s = p.read_text()
for i, line in enumerate(s.splitlines(), 1):
    if any(c in line for c in '=+-@') or 'PROJECT_CSV_COLUMNS' in line or 'TEST_CSV_COLUMNS' in line or 'RUN_HISTORY_CSV_COLUMNS' in line:
        print(f'{i}: {line}')
PY

echo
echo "## read-only behavioral probe of current csvEscapeField semantics"
python3 - <<'PY'
import re

def csv_escape_field(raw):
    if raw is None or raw is None:
        return ''
    s = str(raw)
    if re.search(r'["\r\n]', s):
        # This simple model doubles only existing double quotes and wraps.
        return '"' + s.replace('"', '""') + '"'
    return s

for raw in ['=cmd|\'/c calc\'!A1', '+123', '-123', '`@cell`', '\ttab', '\rcr', 'normal,quote"quote', 'cmd|cmd|A1']:
    print(repr(raw), '=>', repr(csv_escape_field(raw)))
PY

Repository: TestSprite/testsprite-cli

Length of output: 50381


🌐 Web query:

OWASP CSV Injection spreadsheet application formula characters = + - @ leading tab CR mitigation prefix apostrophe

💡 Result:

CSV Injection (or Formula Injection) occurs when untrusted user input is embedded in CSV files, causing spreadsheet applications (like Microsoft Excel, LibreOffice Calc, or Google Sheets) to interpret cell content as executable formulas [1][2]. Attackers can use this to perform actions ranging from data exfiltration to potential remote code execution via DDE or other "gadget" features [2][3]. Mitigation is difficult because there is no single universal standard for CSV parsing across different spreadsheet applications [1][4]. Security best practices involve the following approaches: Core Mitigation Techniques 1. Single Quote Prefix: Prepending a single quote (') to a field that begins with a formula-triggering character (=, +, -, @, |, \t, or \r) is a commonly cited mitigation intended to force the spreadsheet to treat the cell as literal text [2][5][6]. However, its effectiveness varies; some applications may display the quote or treat it inconsistently [7]. 2. Tab Character Prefix: To more reliably prevent formula execution in Microsoft Excel, it is recommended to prefix cells starting with formula-triggering characters with a tab character (0x09) inside a quoted field (e.g., "\t=1+1") [1][2]. Note that this makes the tab character part of the data, which may interfere with downstream programmatic processing [1][4]. 3. Structural Protection: Always encapsulate every cell field in double quotes and escape internal double quotes by doubling them (e.g., "") to prevent attackers from breaking out of the cell context [1][4][7]. Important Considerations - Microsoft Excel behavior: Excel may remove quotes or escape characters when a file is saved and re-opened, potentially re-activating formulas that were previously neutralized [1][2][3]. - Defense-in-depth: Relying solely on CSV output sanitization is insufficient. Implement input validation at the point of data entry to reject or sanitize inputs that begin with dangerous characters [8]. - Unicode Variants: Be aware that full-width or double-byte variants of formula characters (e.g., , , , ) may also be interpreted as formulas in some locales and should be included in sanitization logic [1][4]. There is no perfect, universal solution. Organizations should assess the specific spreadsheet software used by their users and consider the trade-offs between security and data integrity [1][2][6].

Citations:


Neutralize CSV formula-trigger values before rendering --output csv.

csvEscapeField only handles RFC 4180 delimiters/quotes, while project list, test list, and test result --history may render project/test names or metadata through CSV. Values starting with formula characters such as =, +, -, @, or leading tab/CR can be evaluated by spreadsheets; prefix neutralization should be paired with proper CSV quoting for that literal field.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/output.ts` around lines 69 - 76, Update csvEscapeField to neutralize
spreadsheet formula-triggering values by prefixing the required safe character
when the rendered field begins with =, +, -, @, tab, or CR, then apply CSV
quoting/escaping to the resulting literal value. Preserve null/undefined
handling and existing RFC 4180 behavior for all other fields.

@zeshi-du

Copy link
Copy Markdown
Contributor

Maintainer review: accepted — one nod pending before merge. Verified: no new dependency (the lockfile hunk is just the stale version-field sync), RFC 4180 escaping with doubled quotes and CR/LF handling, columns derived from the wire types rather than the text-table layout, non-list commands reject csv/ndjson with a typed VALIDATION_ERROR at print time, and CI is green on all three OSes — the local ticker.spec failures you flagged are a known pre-existing flake, not yours. This lands #164, which has been on our accept list since early July. Expect the merge shortly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Hackathon] feat(cli): add --output csv and --output ndjson for list commands

2 participants