Skip to content

[OPIK-8011] [QA] test: e2e coverage for online evaluation sampling rate - #7929

Open
AndreiCautisanu wants to merge 6 commits into
mainfrom
andreic/OPIK-8011-e2e-online-eval-sampling-rate
Open

[OPIK-8011] [QA] test: e2e coverage for online evaluation sampling rate#7929
AndreiCautisanu wants to merge 6 commits into
mainfrom
andreic/OPIK-8011-e2e-online-eval-sampling-rate

Conversation

@AndreiCautisanu

Copy link
Copy Markdown
Contributor

Details

Adds a @t2-cuj E2E test for @cap:online-evaluation.sampling-rate, previously uncovered in taxonomy.yaml. A wrong sampling rate is a silent failure mode — traces quietly go unscored, or get over-scored and burn LLM cost, for weeks before anyone notices — and the rate control was the one part of the rule dialog with no scripted coverage.

The test creates two deterministic Python-Equals rules over one batch of 30 traces: a 50% rule and a 100% rule.

  • The 100% rule is an in-test control. It is what turns "the partial rule skipped these traces" into a real negative rather than a timing guess — its own score landing on a trace proves the engine processed that trace, so a stalled pipeline can't masquerade as aggressive sampling.
  • The partial-rate assertion is a band, not an equality: the backend draws independently per trace (OnlineScoringSampler.shouldSampleTracesecureRandom.nextFloat() >= samplingRate) with no seed hook, so the count is a genuine binomial variable. n=30 with a 15–85% band gives a ~1-in-29,000 false-failure rate; a tighter 25–75% band would be ~1-in-300 while catching no additional real regression (every plausible break lands at 30, 0, or ~2).
  • Two data-testids were needed — the sampling control had no stable handle, as the number input's only accessible name was %, leaked from the suffix <label>.

One thing reviewers should know: SliderInputControl commits the typed value to the form in onBlur, not onChange. Verified against the live dialog — typing 25 and submitting without blurring persists sampling_rate: 1.0 while the UI still displays 25. That failure is invisible to a naive sampling assertion, because a rule left at 100% scores everything, which is exactly what "sampling ignored" looks like. So the POM forces the blur, and the spec gates on the persisted fraction before seeding any traces, turning a silent no-op into a loud early failure. This is arguably also a real UX bug (a user who types a rate and clicks Create gets 100%), but that's product behaviour and out of scope here.

Change checklist

  • User facing
  • Documentation update

Issues

  • Resolves #
  • OPIK-8011

AI-WATERMARK

AI-WATERMARK: yes

  • Tools: Claude Code
  • Model(s): Claude Opus 5
  • Scope: Wrote the spec, the POM additions, the backend-client field, the two FE data-testids, and the taxonomy update. Live-UI selector discovery and the blur-commit finding were done against a local OSS deployment via the Playwright MCP. The binomial band was computed rather than guessed.
  • Human verification: Author reviewed the full diff. The test was run 8 times locally (7 pre-rebase, 1 post-rebase onto a clean main base) — all green. The observed split was instrumented once with a temporary probe (partial=14/30 full=30/30, probe removed before commit) to confirm the test measures real sampling rather than passing vacuously. The blur-commit trap was verified empirically both ways against the running app: blur → persisted 0.5; no blur → persisted 1.0.

Testing

Commands run (from tests_end_to_end/e2e/, local OSS at http://localhost:5173, workspace default, Chromium):

npx playwright test tests/online-evaluation/online-evaluation-sampling-rate.spec.ts --reporter=list
npx playwright test tests/online-evaluation/ --reporter=list
npx tsc --noEmit
python3 tests_end_to_end/coverage/tag_lint.py --taxonomy tests_end_to_end/coverage/taxonomy.yaml --estate tests_end_to_end

Results:

Check Result
New spec, repeated runs 8/8 passed, ~29–37s each
Whole online-evaluation/ directory (shared POM regression) 4 passed, 1 skipped
tag_lint.py 39 specs checked, 1 exempt, 0 problem(s)
npx tsc --noEmit (e2e suite) clean
pre-commit on changed files eslint + typecheck passed, no reformatting

Scenarios validated:

  • Happy path — 50% rule scores a partial subset, 100% control scores all 30. Observed 14/30 vs 30/30.
  • The test isn't vacuous — instrumented the actual counts to confirm a genuine partial split, rather than trusting a green band that a 100%-behaving rule would also satisfy.
  • Persisted-rate gate — asserts the backend fraction (0.5 / 1.0) before seeding, so a rate that didn't take fails immediately instead of degrading into a silent no-op.
  • Blur-commit edge case — confirmed empirically that submitting without blurring persists 1.0; this is why the POM blurs explicitly.
  • Sibling-spec regression — ran the full feature directory because this PR touches the shared online-evaluation.page.ts.

Environment note: the FE change means the frontend container must be rebuilt for the testids to exist. Locally: docker compose --profile opik build frontend, then recreate with OPIK_FRONTEND_PULL_POLICY=never. Verified the attribute in the running container and in the live DOM before running the test.

Not run: the LLM-judge smoke test in the same directory skips in this environment — it's gated on an LLM provider key (pom/model-availability.ts), a pre-existing condition unrelated to this change. No video recording attached: this is a test-only change with no user-facing UI behaviour to demonstrate.

Documentation

No documentation change needed — this adds test coverage and two test hooks, with no user-facing behaviour change. Coverage bookkeeping lives in tests_end_to_end/coverage/taxonomy.yaml, updated in this PR: the spec is added to the area's specs: list and sampling-rate is flipped to covered: true, tier: t2-cuj.

Covers @cap:online-evaluation.sampling-rate, previously uncovered in
taxonomy.yaml. Two deterministic Python-Equals rules over one batch of 30
traces: a 50% rule and a 100% control. The control is what turns "the partial
rule skipped these traces" into a real negative rather than a timing guess.

The backend draws per trace (OnlineScoringSampler.shouldSampleTrace:
secureRandom.nextFloat() >= samplingRate) with no seed hook, so the assertion
is a 15-85% band: ~1-in-29,000 false failures at n=30, versus ~1-in-300 for a
25-75% band that catches no additional real regression.

SliderInputControl commits on blur, not on change — typing a rate and
submitting without blurring persists 1.0 while still displaying the typed
number. That is invisible to a sampling assertion (a rule left at 100% scores
everything, exactly like "sampling ignored"), so the POM forces the blur and
the spec gates on the persisted fraction before seeding any traces.

Adds two data-testids: the sampling control had no stable handle, as the
number input's only accessible name was "%", leaked from the suffix label.
The SliderInputControl testid derives from its existing id prop, so every
call site gains a handle with no call-site churn and no version-awareness.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AndreiCautisanu
AndreiCautisanu requested review from a team as code owners August 20, 2026 08:47
@github-actions github-actions Bot added Frontend tests Including test files, or tests related like configuration. typescript *.ts *.tsx 🟠 size/L labels Aug 20, 2026
@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

⏱️ pre-commit per-hook timing

Hook Description Result Duration
🌐 typecheck — frontend Whole-project tsc type check 31.51s
🌐 eslint — frontend Lint + autofix JS/TS 2.71s
Total (2 ran) 34.22s
⏭️ 41 skipped (no matching files changed)
Hook Description Result
🐍 trim trailing whitespace — python sdk Strip trailing whitespace ⏭️
🐍 fix end of files — python sdk Ensure files end in a newline ⏭️
🐍 ruff — python sdk Lint + autofix Python (ruff) ⏭️
🐍 ruff-format — python sdk Format Python code (ruff) ⏭️
🐍 mypy — python sdk Static type check ⏭️
🤖 trim trailing whitespace — optimizer Strip trailing whitespace ⏭️
🤖 fix end of files — optimizer Ensure files end in a newline ⏭️
🤖 check yaml — optimizer Validate YAML syntax ⏭️
🤖 check json — optimizer Validate JSON syntax ⏭️
🤖 check toml — optimizer Validate TOML syntax ⏭️
🤖 check for added large files — optimizer Block large files (>1MB) ⏭️
🔐 detect private key — optimizer Block committed private keys ⏭️
🤖 check for merge conflicts — optimizer Block merge-conflict markers ⏭️
🤖 check for case conflicts — optimizer Block case-only name clashes ⏭️
🤖 pyupgrade — optimizer Modernize Python syntax ⏭️
🤖 ruff — optimizer Lint + autofix Python (ruff) ⏭️
🤖 ruff-format — optimizer Format Python code (ruff) ⏭️
🤖 mypy — optimizer Static type check ⏭️
📓 nbstripout — optimizer notebooks Strip notebook output ⏭️
📝 markdownlint — optimizer Lint Markdown ⏭️
🔤 codespell — optimizer Fix common misspellings ⏭️
📊 radon cc — optimizer Cyclomatic-complexity gate ⏭️
📊 radon raw — optimizer Raw size metrics gate ⏭️
📊 xenon — optimizer Fail on complexity thresholds ⏭️
📊 lizard — optimizer Cyclomatic-complexity gate ⏭️
🧹 vulture — optimizer Find dead code ⏭️
🛡️ trim trailing whitespace — guardrails Strip trailing whitespace ⏭️
🛡️ fix end of files — guardrails Ensure files end in a newline ⏭️
🛡️ ruff — guardrails Lint + autofix Python (ruff) ⏭️
🛡️ ruff-format — guardrails Format Python code (ruff) ⏭️
🛡️ mypy — guardrails Static type check ⏭️
⚓ helm-docs Regenerate Helm chart README ⏭️
block non-public FE plugins Block non-public FE plugins ⏭️
☕ spotless — java backend Format Java code ⏭️
🧪 pre-commit wrapper smoke tests Self-test the wrapper scripts ⏭️
🧪 rebaseline script tests Self-test the changelog re-baseline script ⏭️
📘 eslint — typescript sdk Lint + autofix JS/TS ⏭️
📘 typecheck — typescript sdk Whole-project tsc type check ⏭️
⚙️ actionlint — github workflows Lint GitHub Actions workflows ⏭️
🐳 hadolint — dockerfiles Lint Dockerfiles ⏭️
🌈 zizmor — github workflows security Security-scan GitHub Actions workflows ⏭️

@CometActions

CometActions commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

No test needed here.

This PR is the sampling-rate e2e itself plus the hooks it needs: two data-testid attributes (SliderInputControl's number input, the Filtering & Sampling accordion trigger) and the id="topK" -> id="frequencyPenalty" fix in OpenRouterModelConfigs, which un-duplicates the DOM id so the Frequency penalty label and suffix stop pointing at the Top K slider. That fix is real but label-focus-only — the value binding was already correct — and the OpenRouter config panel needs a configured provider key to reach. Nothing here for a second test to cover; your spec is the coverage for online-evaluation.sampling-rate (taxonomy flipped to covered: true in this same diff).

Run

Advisory, from the QA test radar. Nothing here blocks this PR, and anything it proposes is a draft for review.

Re-checked after a push on 20 Aug 10:07 UTC — nothing the verdict depends on changed.

…rd cleanup

waitForTraceScoresSettled decides "settled" from a stable score-set
fingerprint, so with minScores: 0 a not-yet-scored trace looked stable from its
first poll: the helper returned after one quiet period holding an empty score
set while the control's score was still in flight, and that trace was then
tallied as "the control never scored it". timeoutMs could not rescue it — the
helper returns early rather than timing out. Now polls each trace for the 100%
rule first (a guaranteed arrival), then settles with minScores: 1.

Cleanup moves into try/finally: as a trailing test.step it was skipped whenever
an earlier assertion failed, and the project fixture's teardown does not reach
automation rules, so red runs leaked both rules. Verified by injecting a
mid-test failure — zero rules left behind.

Also fixes a pre-existing duplicate id in OpenRouterModelConfigs, where
Frequency penalty passed id="topK". That already broke its label association,
and with testids derived from the id it would have produced two
data-testid="topK-input" nodes and a Playwright strict-mode violation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…up masking failures

The band comments were wrong in three ways: bounds 5..25 are 16.7%-83.3% of 30
rather than the stated 15%-85%; 5 is 15% rounded UP, not down; and the quoted
~1-in-29,000 flake rate belonged to 4..25, so the real budget for the
configured bounds was ~1-in-16,800. Maintainers reading those comments would
have had a materially better flake budget in mind than the test delivered.
Bounds now derive from the fractions (4..26, ~1-in-118,000), so the numbers and
the prose cannot drift apart again.

Cleanup in `finally` awaited two throwing calls: a failed list skipped every
deletion, and deleteAutomationRule rethrows anything that is not a 404, so a
cleanup error replaced the assertion or scoring error that actually explained
the run. This was a regression from moving cleanup into `finally` — as a
trailing step it only ran on success and could not mask anything. Both calls
are now caught and warned per operation, and the loop continues past one
undeletable rule. Verified by injecting a test failure and a cleanup failure
together: the cleanup error logs as a warning and the original failure is what
gets reported.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rule deletion is workspace-global, not project-scoped: deleteEvaluatorsByIds,
deleteByRuleIds and deleteBaseRules all constrain on workspace_id plus the rule
id, so the project_id query parameter does not narrow the delete. Verified
against a running backend — a rule created against two projects and then
deleted "via project A" disappeared from project B as well.

listAutomationRulesForProject returns rules shared with other projects, so the
finally-block sweep could have destroyed a rule another project still used.
Rules this spec creates are single-project, so the filter changes nothing
today; it makes the blast radius a local property of the cleanup rather than
resting on "every rule in this project is ours".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The project fixture keeps its project when OPIK_LEAVE_FAILURES is set and the
test failed, but the finally-block rule cleanup deleted regardless — handing a
debugger a preserved project stripped of the rules that explain the failure.

Cleanup now skips when the body threw and the flag is set. Failure is tracked
with a catch-set flag rather than shouldLeaveArtifacts(testInfo): inside
`finally` Playwright has not finalised the status yet, so testInfo.status still
reads 'passed' on a failing test (verified with a probe spec) and the helper
would always return false — the guard would silently never fire.

Verified all three paths: failure + flag leaves the rules with a warning;
failure without the flag still cleans up; the success path is unchanged.

Not paginating the cleanup listing. listAutomationRulesForProject requests
size=500 and this spec creates two rules, so there is no second page to fetch;
adding pagination would be dead code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The retention guard added in 3c785bf used an early `return` inside `finally`.
A `return` there discards the in-flight exception and resolves the function, so
a failed run with OPIK_LEAVE_FAILURES set was reported as PASSED — a green tick
on a genuinely failing test, which is the worst failure mode test code can have.

Reproduced in a probe spec: a failing `expect` plus a `return` in `finally`
reports "1 passed". The previous round's verification missed it because the
injected failure and the retention flag were never exercised in the same run,
so the return branch was reachable but untested.

Cleanup is now guarded by a boolean instead, leaving no `return` in the
`finally`. Re-verified with both conditions together: the warning is emitted
AND the run reports "1 failed"; the success path still passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Frontend 🟠 size/L tests Including test files, or tests related like configuration. typescript *.ts *.tsx

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants