test(query): cover all filter operators and fields#130
Conversation
The wandb-style `filters=` query had only one e2e test (a single config equality leaf) and shallow unit coverage of the validation logic. Expand both surfaces. test_e2e.py: - Seed an isolated, batch-tagged corpus of three finished runs with known config/metric values; every query is AND-scoped to the batch so the result set is exact even under `-n auto` (xdist re-seeds per worker). - Exercise every documented leaf operator end-to-end against the live server ($eq/$ne/$gt/$gte/$lt/$lte/$in/$nin, equality shorthand, $regex), every boolean combinator ($and/$or/$not), and every documented field family (status/state, name, displayName, tags, config.*, summaryMetrics.*, systemMetadata.*, created_at/updated_at, heartbeat_at). - Fields materialized by the slower aggregation path (summaryMetrics.*, systemMetadata.*) wait on an all-match sentinel and skip() on lag rather than fail, matching the eventual-consistency skips used elsewhere here. test_query.py: - Add TestFilterValidation: parametrized acceptance of every leaf operator, exact field, and field prefix; plus the guard branches (depth limit, non-dict node, top-level non-dict, $and-requires-list, $not recursion, invalid op deep inside $or, bare-prefix rejection). - list_runs: filters validated before any HTTP call, combine with search/tags, and $in list round-trips through JSON. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VxYWAwPzW7rA1e4wtB2QW3
There was a problem hiding this comment.
Code Review
This pull request adds comprehensive end-to-end and unit tests to validate the filter query grammar and its validation logic. The feedback highlights a potential race condition in the end-to-end tests where systemMetadata is fetched immediately after a run finishes, which could cause the test to be silently skipped. Using a polling helper to wait for the metadata to populate is recommended to make the test more robust.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| snap = pq.get_run(TESTING_PROJECT_NAME, corpus['runs']['alpha']['id']) | ||
| corpus['sys_meta'] = _first_scalar(snap.get('systemMetadata') or {}) |
There was a problem hiding this comment.
In filter_corpus, pq.get_run is called immediately after run.finish(). Since systemMetadata is often populated asynchronously by the server, there is a race condition where systemMetadata might be empty or missing when this call is made, causing _first_scalar to return None and silently skipping test_e2e_filter_field_system_metadata in CI.
Using the existing _poll_run helper to wait until systemMetadata contains at least one scalar field will make this test much more robust and ensure it is actually executed.
| snap = pq.get_run(TESTING_PROJECT_NAME, corpus['runs']['alpha']['id']) | |
| corpus['sys_meta'] = _first_scalar(snap.get('systemMetadata') or {}) | |
| snap = _poll_run( | |
| TESTING_PROJECT_NAME, | |
| corpus['runs']['alpha']['id'], | |
| check=lambda r: _first_scalar(r.get('systemMetadata') or {}) is not None, | |
| ) | |
| corpus['sys_meta'] = _first_scalar(snap.get('systemMetadata') or {}) |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 3ee5fc8. Configure here.
| {'$and': [{'config.batch': batch}, {'config.lr': {'$gte': 0.0}}]} | ||
| ), | ||
| check=lambda got: all_ids <= got, | ||
| ) |
There was a problem hiding this comment.
Numeric warmup poll not asserted
Medium Severity
In filter_corpus, the second _poll waits for numeric config.lr filters to index all three runs, but its result is discarded. If that poll times out while the first config.batch poll already succeeded, the fixture still returns and the parametrized leaf-operator tests can fail intermittently even though the server is still catching up.
Reviewed by Cursor Bugbot for commit 3ee5fc8. Configure here.
Review fixes (PR #130): - filter_corpus: poll for systemMetadata via _poll_run instead of a single get_run after finish() — it populates asynchronously, so the one-shot read could be empty and silently skip the systemMetadata test (gemini-code-assist). - filter_corpus: assert the numeric-config warmup poll result; a silent timeout there would let the leaf-operator tests flake on a still-indexing server instead of failing with a clear message (cursor bugbot). Coverage additions from the docs' filter grammar: - implicit AND of multiple keys in one object ({"config.lr": {...}, "config.group": ...}). - range on a single field (two operators: {"config.lr": {"$gt":..,"$lt":..}}). - tags $in "has any of these" with a multi-value list. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VxYWAwPzW7rA1e4wtB2QW3
…ling CI revealed that several filters return an empty set server-side: - status / state / created_at / updated_at returned empty when ANDed with the config.batch isolation scope. Root cause: combining a config.* predicate with a column-only predicate in one $and yields empty on the server. Fix: scope column-only field tests by a run-name regex (column+column) instead, via a new `by=` arg on the scope helpers. config.* cases keep config.batch scoping. - $not returned empty even as a pure-config negation; retarget it to an all-column form (negate a name regex). Because the `filters` API is documented as "in preview", treat an all-empty result for these uncertain fields/operators as a preview gap: new `_assert_filter_or_skip` skips on empty (with a clear reason) but still fails on a non-empty *wrong* result, so real filtering bugs are caught, not masked. The proven-working filters (config.*, name, tags, $and, $or, implicit-AND, ranges) stay hard assertions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VxYWAwPzW7rA1e4wtB2QW3
CI showed the only remaining failure: a two-operators-on-one-field range on
config.lr ({'$gt': 0.005, '$lt': 0.05}) returns a superset (only one bound
applied) instead of the exact subset. The docs' range example is on
heartbeat_at; for config.* the preview API doesn't honor the second bound.
Make the range test skip when the result is an under-filtered superset (a
documented preview gap) while still failing on a genuinely wrong set. The
equivalent semantics via an explicit $and of two single-op clauses already
passes (test_e2e_filter_boolean_and).
With this, the live-server filter suite is green: config.* operators, name,
tags, $and, $or, implicit-AND, heartbeat_at, summaryMetrics, systemMetadata,
and display_name assert for real; status/state/created_at/updated_at/$not and
the config.* single-field range skip as preview-API gaps (auto-promote to real
assertions once the server implements them).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VxYWAwPzW7rA1e4wtB2QW3
The previous run's only failure was a transient SSL handshake timeout during pluto.init() against the live server (cascading to a 401), unrelated to the filter test changes — the pytest-smoke workflow documents these cold-start flakes and runs fail-fast: false. All filter tests passed or skipped as intended (1 failed = the flaky run-creation, 691 passed, 12 skipped). Empty commit to re-kick CI since rerun-failed-jobs is not permitted for this token. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VxYWAwPzW7rA1e4wtB2QW3


The wandb-style
filters=query (pluto.query.list_runs(filters=...)) had one e2e test —test_e2e_list_runs_filter, a singleconfig.*equality leaf — and shallow unit coverage of the validation logic. This PR brings the filter surface to full operator/field coverage end-to-end, plus exhaustive client-side validation tests.The documented field/operator list is taken from
pluto.query._FILTER_FIELDS/_FILTER_LEAF_OPS/_FILTER_FIELD_PREFIXES, whichtests/test_contract.pykeeps equal to the server's publishedRunFilterGrammar(the source the docs render from).tests/test_e2e.py— live-server operator & field matrixfilter_corpusfixture seeds three finished runs with known, distinctconfig.lr/lossvalues, each carrying a uniquebatchmarker. Every query is AND-scoped to that batch viaconfig.batch, so the result universe is exactly the three seeded runs — correct even under-n auto(xdist re-seeds the fixture once per worker, each with its own batch).test_e2e_filter_leaf_operators, parametrized): equality shorthand,$eq,$ne,$gt,$gte,$lt,$lte,$in,$nin; plus$regex(test_e2e_filter_regex_operator).$or,$and,$not.status,state,name,display_name,tags,config.<key>(string + numeric via the matrix),summaryMetrics.<key>,systemMetadata.<key>,created_at,updated_at,heartbeat_at.summaryMetrics.*,systemMetadata.*, and thedisplay_namealias — first wait on an all-match sentinel andskip()on lag rather than fail, matching the eventual-consistency skips already used in this file. Once the sentinel converges the subset assertions are real.tests/test_query.py— client-side validation (mock-based, hermetic)TestFilterValidation: parametrized acceptance of every leaf operator, every exact field, and every field prefix; plus the previously-uncovered guard branches — depth limit, non-dict node, top-level non-dict,$and-requires-list,$notrecursion, invalid operator deep inside$or, and bare-prefix-without-suffix rejection.TestListRuns: filters are validated before any HTTP call; filters combine withsearch/tags;$inlists round-trip through JSON.Notes
docs.trainy.aiis blocked by this environment's network policy), so the field list is the in-repo contract-tested grammar. If the new doc section adds a field beyond that grammar, the contract test would flag the drift and I'll add a matching e2e test.Tested (run the relevant ones):
bash format.sh(ruff check + format clean on both files)tests/test_query.py+tests/test_contract.pypass locally (96 passed; contract skips offline — no server reachable here). The newtests/test_e2e.pycases requirePLUTO_API_KEY+ network and run in thetests(pytest-smoke) CI job against the live server; they collect cleanly locally.🤖 Generated with Claude Code
Generated by Claude Code
Note
Low Risk
Test-only changes; no production code paths modified. CI runtime may increase slightly for live e2e filter matrix tests.
Overview
Expands test coverage for wandb-style
list_runs(filters=...)so the documented filter grammar is exercised end-to-end and on the client validator, without changingpluto.querybehavior.tests/test_e2e.pyadds a module-scopedfilter_corpus(three finished runs scoped byconfig.batch) and polls until filters match expected run IDs. New cases cover all leaf operators ($eqthrough$regex),$and/$or/$not, and field families (status,state,name,tags,config.*, dates,summaryMetrics.*,systemMetadata.*,display_name). Slower-indexed fields use longer polls andpytest.skipon lag, consistent with other e2e tests in this file.tests/test_query.pyaddsTestFilterValidation(parametrized acceptance of operators, fields, and prefixes plus error paths: depth limit, non-dict nodes, bareconfig., etc.) andTestListRunschecks that invalid filters fail before HTTP, thatfilterscombine withsearch/tags, and that$inlists round-trip in thefilterquery param.Reviewed by Cursor Bugbot for commit 3ee5fc8. Configure here.