Skip to content

Type-validate probe inputs against the declared signature (#135) - #162

Merged
CoffeePoweredComputers merged 2 commits into
masterfrom
fix/135-probe-type-validation
Oct 15, 2026
Merged

CoffeePoweredComputers merged 2 commits into
masterfrom
fix/135-probe-type-validation

Conversation

@CoffeePoweredComputers

Copy link
Copy Markdown
Owner

Summary

Fixes #135. Probe inputs for probeable_code / probeable_spec were only checked by parameter name, so probing x="abc" against f(x: int) reached the oracle and either showed a misleading "successful" result ("abcabc") or leaked the raw Python TypeError text to the student.

Backend

  • validate_probe_input now checks int / float / bool / str values after the existing name checks. bool is never accepted for int or float; int is accepted for float; Any, containers and unknown hints are unchecked.
  • Hidden-signature mode (show_function_signature=False): the error is generic ("That value is not valid for parameter 'x'") so the hidden type is not revealed, and both oracle exception branches return a generic message instead of raw Python text. Visible mode says "Parameter 'x' must be an int".
  • ProbeOracleView calls a new ProbeService.validate_input(problem, probe_input) so the mode decision lives in the service. Response bodies are unchanged.
  • Refute shares the validator and always shows its signature, so refute inputs are now type-checked too (covered by a new integration test).
  • Rejected probes still do not consume quota.

Frontend

  • parseInputValue parses by declared type first (not JSON-first) and returns a per-field error; ProbePanel shows it inline, marks the input aria-invalid, and the Probe button is disabled while any field has an error.
  • Untyped fields (hidden mode, containers) keep JSON-first parsing and now accept Python-style True / False / None.
  • Ints outside the safe-integer range and non-Python float literals (hex, binary) are rejected client-side.
  • Hidden-mode placeholder hints that text should be quoted. New i18n keys in en and es.

Tests

  • tests/unit/test_probe_input_validation.py (75 tests, no DB): every primitive, the bool-for-int trap, aliases, defaults, skipped types, hidden-mode messages, oracle sanitisation.
  • Integration: probe round-trip 200, visible/hidden/bool mismatches 400, _record_probe not called on mismatch, refute mismatch 400.
  • Vitest matrix for parseInputValue (57 tests) plus composable tests through withSetup.
  • Playwright: new seeded double(n: int) problem in e2e-code and a strict inline-error test.

Known limitations

Related issues filed during this work

Test plan

  • pytest -m "unit or integration" — 2291 passed
  • pytest -m architecture — 51 passed
  • ruff check . clean; ruff format --check clean on changed files
  • npx vitest run src/components/activities src/features/editor — 90 passed
  • npm run lint:check — 0 errors
  • npx playwright test --project=student — 64 passed, 1 pre-existing self-skip

🤖 Generated with Claude Code

Probe inputs for probeable_code / probeable_spec problems were only
checked by parameter name, so x="abc" against f(x: int) reached the
oracle and either produced a misleading "successful" result or leaked
the raw Python TypeError text to the student.

Backend: validate_probe_input now checks int/float/bool/str values
after the existing name checks (bool never accepted for int/float;
int accepted for float; Any/containers unchecked). The message names
the type only when show_function_signature is on; hidden mode returns
a generic message, and both oracle exception branches are sanitised
in hidden mode so no path can reveal a hidden type. The view goes
through a new ProbeService.validate_input so the mode decision lives
in the service. Refute shares the validator and always shows its
signature, so its inputs are now type-checked too.

Frontend: parseInputValue parses by declared type first instead of
JSON-first, returning a per-field error that disables the Probe
button. Untyped fields keep JSON-first parsing and accept Python-style
True/False/None. Ints outside the safe-integer range and non-Python
float literals are rejected client-side.

Tests: new unit suite for the validator (no DB), probe round-trip and
type-mismatch integration tests in visible, hidden and refute modes,
a vitest matrix for parseInputValue, and a Playwright test against a
new seeded double(n: int) problem.

Closes #135

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@codecov-commenter

codecov-commenter commented Oct 15, 2026 •

Copy link
Copy Markdown

- Treat a `= None` default as an implicit Optional and skip the type
  check on both sides, matching `Optional[T]`. The fallback signature
  parser now keeps the default on the type so normalisation can see it.
- Give out-of-range ints their own message instead of the misleading
  "must be a whole number"; JSON cannot carry them losslessly, so they
  are still refused.
- Remove the dead `problems.probe.valuePlaceholder` key from en and es.

Pre-existing refute gaps (graded path unchecked, RefuteInput's own
parser) are tracked in #163.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@CoffeePoweredComputers

Copy link
Copy Markdown
Owner Author

Ultrareview triage

Finding Action
param: T = None now rejects null (normal) Fixed. A None default is treated as implicit Optional and left unchecked on both sides, same as Optional[T]. Tests added.
Big ints rejected with a misleading message (nit) Fixed. Distinct intRange message. Still refused: JSON cannot carry ints above 2^53 without corrupting them, and the server would then validate the wrong value.
Dead valuePlaceholder i18n key (nit) Fixed. Removed from en and es.
Hidden mode still acts as a type oracle (nit) Not changed, by design. The goal of reveal_types=False is to not name the type; a value kind being rejected is what a typed function does, and rejected probes have always been free of quota (validation and oracle failures never called _record_probe before this PR either). Moving the rejection to the 200 path would reveal exactly the same bit. Skipping the check in hidden mode would reinstate the #135 bug for those problems. If we want hidden-mode type discovery to cost something, that is a quota-policy change and deserves its own issue.
Graded refute path still unchecked; RefuteInput keeps its own parser (pre-existing) Out of scope, tracked in #163.

🤖 Generated with Claude Code

@CoffeePoweredComputers
CoffeePoweredComputers merged commit b0c2c86 into master Oct 15, 2026
18 checks passed
@CoffeePoweredComputers
CoffeePoweredComputers deleted the fix/135-probe-type-validation branch October 15, 2026 19:03
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.

Probeable: no type validation on probe inputs — wrong-type values silently produce confusing oracle results

2 participants