Skip to content

fix(dataset): harden inputs.json and raw-payload loader input validation#1120

Draft
ajcasagrande wants to merge 1 commit into
mainfrom
ajc/loader-input-hardening
Draft

fix(dataset): harden inputs.json and raw-payload loader input validation#1120
ajcasagrande wants to merge 1 commit into
mainfrom
ajc/loader-input-hardening

Conversation

@ajcasagrande

@ajcasagrande ajcasagrande commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Hardens input validation in two dataset loaders so malformed user inputs fail with actionable errors instead of bare tracebacks or silent misclassification. Carved from the ajc/agentx-on-main adversarial-hardening work; includes the two new adversarial test suites that pin the fixed behavior.

Crashes fixed (with pre-fix exceptions)

1. InputsJsonPayloadLoader.load_dataset — missing required keys (src/aiperf/dataset/loader/inputs_json.py)

An inputs.json entry missing session_id or payloads crashed with a bare, context-free exception:

KeyError: 'session_id'

Now raises a path- and index-rich error:

ValueError: <path>: data[0] missing required key 'session_id'

2. RawPayloadDatasetLoader.can_load — non-dict probe data (src/aiperf/dataset/loader/raw_payload.py)

Type-dispatch auto-detection feeds arbitrary first-record shapes into can_load. A non-dict record (list, string, scalar) crashed the detection chain mid-walk:

AttributeError: 'str' object has no attribute 'get'

Now guarded with isinstance(data, dict) so detection falls through cleanly (can_load returns False).

Deliberate behavior change: OSError now propagates from _dir_has_raw_payload_jsonl

Previously _dir_has_raw_payload_jsonl caught (orjson.JSONDecodeError, OSError) and skipped to the next file. That meant an unreadable .jsonl file (e.g. permission-denied) was silently swallowed and the directory was misclassified as not-raw-payload, sending the run down the wrong loader path with no signal.

The except clause is narrowed to (orjson.JSONDecodeError, ValueError): only malformed JSON is skipped; OSError/PermissionError from an unreadable file now propagates out of can_load so misconfigured inputs fail loudly at detection time. This is an intentional loud-failure change, pinned by test_can_load_directory_with_unreadable_jsonl_raises_post_fix.

Tests

  • tests/unit/dataset/loader/test_inputs_json_adversarial.py (new, 142 lines) — can_load boundary shapes, Pydantic min_length enforcement, session_id passthrough, one-turn-per-payload, duplicate/missing-key error pins.
  • tests/unit/dataset/loader/test_raw_payload_adversarial.py (new, 218 lines) — non-dict probe data, directory-peek discrimination (empty/null/array first lines, wrong extensions, malformed JSONL), JSONL line edge cases (blank lines, trailing newline, verbatim raw_payload), unreadable-file loud-failure pin.

Revert-verification (pin discipline)

Reverse-applied the two src fixes onto main and ran the adversarial suites — 5 tests failed, reproducing the exact pre-fix crashes:

FAILED test_raw_payload_adversarial.py::TestCanLoadDataShape::test_can_load_non_dict_data_returns_false[bad_data0]
FAILED test_raw_payload_adversarial.py::TestCanLoadDataShape::test_can_load_non_dict_data_returns_false[string]
FAILED test_raw_payload_adversarial.py::TestCanLoadDataShape::test_can_load_non_dict_data_returns_false[123]
       -> AttributeError: 'str' object has no attribute 'get'  (raw_payload.py:54)
FAILED test_inputs_json_adversarial.py::TestWave2FixForwardCompatibility::test_load_dataset_missing_required_key_raises_value_error_post_fix
       -> bare KeyError: 'session_id' instead of ValueError
FAILED test_raw_payload_adversarial.py::TestWave2BugCandidates::test_can_load_directory_with_unreadable_jsonl_raises_post_fix
       -> OSError swallowed; can_load silently returned False

Re-applied the fixes: all green.

Evidence

  • uv run pytest -n auto tests/unit/dataset/loader/test_inputs_json_adversarial.py tests/unit/dataset/loader/test_raw_payload_adversarial.py tests/unit/dataset/loader/test_inputs_json_payload.py tests/unit/dataset/loader/test_raw_payload.py47 passed
  • pytest --collect-only tests/unit/dataset/loader/ — clean (931 collected)
  • ruff format + ruff check on all four touched files — clean
  • make check-ruff-baselined — OK (118 total, 118 baselined, 0 new)
  • make check-ergonomics — OK (58 total, 58 baselined, 0 new)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved dataset loading to fail with clear errors when required session fields are missing.
    • Tightened file and format detection so invalid inputs are rejected earlier and unreadable files no longer get silently ignored.
    • Preserved original payload content more reliably when converting data into conversations.
    • Handled blank lines and trailing newlines more cleanly during JSONL ingestion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Anthony Casagrande <acasagrande@nvidia.com>
@ajcasagrande ajcasagrande added the AgentX Feature for AgentX label Jul 7, 2026
@github-actions github-actions Bot added the fix label Jul 7, 2026
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Try out this PR

Quick install:

pip install --upgrade --force-reinstall git+https://github.com/ai-dynamo/aiperf.git@63749f2c204c147935c703a4f6aab65e1455a452

Recommended with virtual environment (using uv):

uv venv --python 3.12 && source .venv/bin/activate
uv pip install --upgrade --force-reinstall git+https://github.com/ai-dynamo/aiperf.git@63749f2c204c147935c703a4f6aab65e1455a452

Last updated for commit: 63749f2Browse code

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR adds required-key validation to InputsJsonPayloadLoader.load_dataset, raising ValueError when session_id or payloads are missing, and tightens RawPayloadDatasetLoader.can_load to reject non-dict data and to stop silently swallowing OSError during directory probing. Two new adversarial test modules cover these behaviors.

Changes

Loader validation fixes

Layer / File(s) Summary
InputsJson required-key validation
src/aiperf/dataset/loader/inputs_json.py, tests/unit/dataset/loader/test_inputs_json_adversarial.py
load_dataset now raises ValueError with file/index context when session_id or payloads are missing from a data element; new tests pin can_load, load_dataset, and InputsJsonSession edge-case behavior including duplicate session IDs and empty payloads.
RawPayload type guard and error propagation
src/aiperf/dataset/loader/raw_payload.py, tests/unit/dataset/loader/test_raw_payload_adversarial.py
can_load returns False immediately for non-dict data and directory probing now only catches JSON parsing errors, letting OSError/PermissionError propagate; new tests cover data-shape rejection, directory peek edge cases, line-based dataset loading, raw_payload preservation, and unreadable-file handling.

Estimated code review effort: 2 (Simple) | ~12 minutes

Poem

A rabbit checked each key with care,
"session_id" and "payloads" must be there!
Non-dict data? Nope, turned away,
OSErrors now have their say.
Tests hop in, adversarial and bright,
Guarding burrows through the night. 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: hardening validation in the inputs.json and raw-payload dataset loaders.
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.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
tests/unit/dataset/loader/test_inputs_json_adversarial.py (1)

32-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Parametrize missing pytest.param / # fmt: skip.

As per path instructions, tests/**/*.py should "Use from pytest import param and put # fmt: skip on the ) line for @pytest.mark.parametrize."

🧹 Proposed fix
-import orjson
-import pytest
+import orjson
+import pytest
+from pytest import param
 from pydantic import ValidationError
...
-    `@pytest.mark.parametrize`("bad_data", [[], "s", 123])
+    `@pytest.mark.parametrize`(
+        "bad_data", [param([]), param("s"), param(123)]
+    )  # fmt: skip
🤖 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 `@tests/unit/dataset/loader/test_inputs_json_adversarial.py` at line 32, The
`@pytest.mark.parametrize` in the test module should follow the project’s pytest
formatting convention. Update the parametrization in the test function to use
pytest.param entries from from pytest import param, and add # fmt: skip on the
closing parenthesis line of the decorator so formatting is preserved. Use the
parametrized test definition around the bad_data cases as the target for this
change.

Source: Path instructions

src/aiperf/dataset/loader/raw_payload.py (1)

164-168: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant exception type in tuple.

orjson.JSONDecodeError is already a subclass of ValueError, so catching ValueError alone would produce identical behavior. Not incorrect, just redundant — the explicit inclusion of orjson.JSONDecodeError could be seen as documenting intent.

🤖 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/aiperf/dataset/loader/raw_payload.py` around lines 164 - 168, The
exception tuple in the raw payload loader is redundant because
`orjson.JSONDecodeError` is already covered by `ValueError`. Simplify the
`except` in the JSON parsing path of `raw_payload.py` by keeping only
`ValueError` in the `can_load`/payload-loading logic, or otherwise make the
choice consistent with the intended intent if you want the explicit type as
documentation.
tests/unit/dataset/loader/test_raw_payload_adversarial.py (1)

31-31: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Parametrize missing pytest.param / # fmt: skip.

As per path instructions, tests/**/*.py should "Use from pytest import param and put # fmt: skip on the ) line for @pytest.mark.parametrize."

🧹 Proposed fix
-import orjson
-import pytest
+import orjson
+import pytest
+from pytest import param
...
-    `@pytest.mark.parametrize`("bad_data", [[], "string", 123])
+    `@pytest.mark.parametrize`(
+        "bad_data", [param([]), param("string"), param(123)]
+    )  # fmt: skip
🤖 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 `@tests/unit/dataset/loader/test_raw_payload_adversarial.py` at line 31, The
parametrized test in the `test_raw_payload_adversarial` module should follow the
repository’s formatting rule for `tests/**/*.py`: import `param` from `pytest`,
use `param(...)` entries instead of bare literals in `pytest.mark.parametrize`,
and place `# fmt: skip` on the closing parenthesis line of the decorator. Update
the `@pytest.mark.parametrize` declaration accordingly so it matches the style
used by other tests and avoids formatter churn.

Source: Path instructions

🤖 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/aiperf/dataset/loader/inputs_json.py`:
- Around line 74-80: The validation loop in inputs_json loading should reject
non-dict entries before checking required keys. Update the entry validation in
the data_list iteration so the code first verifies each item is a mapping (for
example in the loop that checks session_id and payloads) and raises the same
ValueError path for invalid types, then perform the existing key presence checks
on valid dict entries only.

In `@tests/unit/dataset/loader/test_inputs_json_adversarial.py`:
- Around line 3-9: The module docstring in TestWave2FixForwardCompatibility is
stale because this file does not contain any xfail-strict tests. Update the
docstring to describe the actual coverage in test_inputs_json_adversarial.py:
the adversarial can_load/load_dataset behavior and the direct assertions for the
Wave 2 fixes, without mentioning xfail markers or tests that do not exist here.

---

Nitpick comments:
In `@src/aiperf/dataset/loader/raw_payload.py`:
- Around line 164-168: The exception tuple in the raw payload loader is
redundant because `orjson.JSONDecodeError` is already covered by `ValueError`.
Simplify the `except` in the JSON parsing path of `raw_payload.py` by keeping
only `ValueError` in the `can_load`/payload-loading logic, or otherwise make the
choice consistent with the intended intent if you want the explicit type as
documentation.

In `@tests/unit/dataset/loader/test_inputs_json_adversarial.py`:
- Line 32: The `@pytest.mark.parametrize` in the test module should follow the
project’s pytest formatting convention. Update the parametrization in the test
function to use pytest.param entries from from pytest import param, and add #
fmt: skip on the closing parenthesis line of the decorator so formatting is
preserved. Use the parametrized test definition around the bad_data cases as the
target for this change.

In `@tests/unit/dataset/loader/test_raw_payload_adversarial.py`:
- Line 31: The parametrized test in the `test_raw_payload_adversarial` module
should follow the repository’s formatting rule for `tests/**/*.py`: import
`param` from `pytest`, use `param(...)` entries instead of bare literals in
`pytest.mark.parametrize`, and place `# fmt: skip` on the closing parenthesis
line of the decorator. Update the `@pytest.mark.parametrize` declaration
accordingly so it matches the style used by other tests and avoids formatter
churn.
🪄 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: Enterprise

Run ID: 65751dfd-d6aa-4a0f-a083-c66d7ae8757b

📥 Commits

Reviewing files that changed from the base of the PR and between 0314074 and 63749f2.

📒 Files selected for processing (4)
  • src/aiperf/dataset/loader/inputs_json.py
  • src/aiperf/dataset/loader/raw_payload.py
  • tests/unit/dataset/loader/test_inputs_json_adversarial.py
  • tests/unit/dataset/loader/test_raw_payload_adversarial.py

Comment on lines 74 to +80
for idx, entry in enumerate(data_list):
if "session_id" not in entry:
raise ValueError(
f"{path}: data[{idx}] missing required key 'session_id'"
)
if "payloads" not in entry:
raise ValueError(f"{path}: data[{idx}] missing required key 'payloads'")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file around the reported lines.
sed -n '1,180p' src/aiperf/dataset/loader/inputs_json.py

# Find any surrounding validation for entry types or JSON shape in the loader.
rg -n "session_id|payloads|isinstance\\(entry|data_list|ValueError" src/aiperf/dataset/loader -n

Repository: ai-dynamo/aiperf

Length of output: 26717


Validation should reject non-object entries first.

"session_id" not in entry assumes each data[] item is a mapping. A null, number, or list will raise an unhandled TypeError here instead of the intended ValueError. Add an isinstance(entry, dict) guard before the key checks.

🤖 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/aiperf/dataset/loader/inputs_json.py` around lines 74 - 80, The
validation loop in inputs_json loading should reject non-dict entries before
checking required keys. Update the entry validation in the data_list iteration
so the code first verifies each item is a mapping (for example in the loop that
checks session_id and payloads) and raises the same ValueError path for invalid
types, then perform the existing key presence checks on valid dict entries only.

Comment on lines +3 to +9
"""Adversarial coverage for InputsJsonPayloadLoader.

Pins current behavior for edge cases in `can_load` and `load_dataset`,
and marks two xfail-strict tests that will flip to pass when the known
bugs (duplicate session_id overwrite, bare KeyError on missing keys) are
fixed in Wave 2.
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale docstring: no xfail-strict tests exist in this file.

The module docstring says it "marks two xfail-strict tests that will flip to pass," but TestWave2FixForwardCompatibility tests assert the fixed behavior directly with pytest.raises(...), with no @pytest.mark.xfail anywhere in the file. This looks like a copy-paste holdover from the raw_payload adversarial suite (which does still have such a marker) and will confuse future readers.

🤖 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 `@tests/unit/dataset/loader/test_inputs_json_adversarial.py` around lines 3 -
9, The module docstring in TestWave2FixForwardCompatibility is stale because
this file does not contain any xfail-strict tests. Update the docstring to
describe the actual coverage in test_inputs_json_adversarial.py: the adversarial
can_load/load_dataset behavior and the direct assertions for the Wave 2 fixes,
without mentioning xfail markers or tests that do not exist here.

@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 71.42857% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/aiperf/dataset/loader/inputs_json.py 50.00% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@datadog-official

datadog-official Bot commented Jul 7, 2026

Copy link
Copy Markdown

Pipelines

⚠️ Warnings

🚦 3 Pipeline jobs failed

Run Unit Tests | build (windows, 3.11)   View in Datadog   GitHub Actions

Run Unit Tests | build (windows, 3.12)   View in Datadog   GitHub Actions

Run Unit Tests | build (windows, 3.13)   View in Datadog   GitHub Actions

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 63749f2 | Docs | Give us feedback!

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

Labels

AgentX Feature for AgentX fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant