Skip to content

Add property-based testing: Hypothesis (Python) and Bombadil (TypeScript) - #1385

Open
raman325 wants to merge 16 commits into
mainfrom
property-based-testing
Open

Add property-based testing: Hypothesis (Python) and Bombadil (TypeScript)#1385
raman325 wants to merge 16 commits into
mainfrom
property-based-testing

Conversation

@raman325

Copy link
Copy Markdown
Owner

Proposed change

Adds a property-based testing layer on both stacks, complementing the example-based suites:

Python (Hypothesis)

  • tests/properties/ runs inside the normal pytest tests/ invocation: 18 pure properties covering the PIN generator (length/charset/never-unsafe), the provider slot-tag codec (round-trips across canonical/compact/slot-only/legacy formats, parser totality), and the SlotSyncManager.calculate_in_sync contract (all documented branches).
  • A stateful RuleBasedStateMachine drives the real BaseLock write orchestration (async_internal_set_usercode/async_internal_clear_usercode) and LockUsercodeUpdateCoordinator against MockLCMLock with a dict oracle — random interleavings of writes, deletes, external keypad changes, refreshes, and connection faults, asserting convergence, idempotency (WriteResult.NO_CHANGE), duplicate rejection, and fail-loud disconnected writes.
  • Settings profiles keep the suite fast: dev (default) runs 15 examples per property; CI sets HYPOTHESIS_PROFILE=ci for 200. Full-suite marginal cost is about one second.
  • The totality property immediately found a real edge: parse_slot_num(float('inf')) raised OverflowError through a helper documented to absorb conversion errors — fixed in this PR.

TypeScript (Bombadil)

  • pbt/ contains a static harness that mounts the built lcm-slot and lcm-lock-codes cards with a scripted mock hass and a light-DOM "chaos panel" (external PIN changes, availability/sync flips, slot add/clear), plus a Bombadil spec with temporal-logic properties: masked PINs never appear in the DOM, chip count always matches the model, pushed data eventually renders, suspended state eventually shows its banner — on top of Bombadil's defaults (no uncaught exceptions, console errors, or HTTP errors).
  • yarn test:pbt builds the bundle and runs a time-boxed headless exploration locally; a new nightly + manual-dispatch workflow (bombadil.yml) runs 5-minute explorations and uploads the trace on violation. Never runs on PRs.
  • Bombadil's default properties already earned their keep during development: they caught the harness failing to model the frontend's single-page-app routing (chip navigation + reload → 404), and building the spec surfaced a bug in Bombadil 0.6.1's shipped type declarations (filed upstream as 0.6.1 type declarations broken: actions.d.ts re-exports randomRange, which random.d.ts does not declare antithesishq/bombadil#253).

Counterexample triage policy is documented in AGENTS.md: a found counterexample is a deliverable — fix the code, or narrow a strategy only for documented contract edges, never silence by rerunning.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New feature (which adds functionality)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wvy9UfQtNuvz1qqbEFYoB7

raman325 and others added 16 commits July 31, 2026 14:15
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mption

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Installs @antithesishq/bombadil (pinned exact, 0.x) and adds a
dependency-free static harness (pbt/harness/) that mounts lcm-slot and
lcm-lock-codes against a scripted mock hass, plus a zero-dependency
static file server (pbt/serve.mjs) to serve it. This is the harness a
follow-up task will drive with Bombadil property specs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wvy9UfQtNuvz1qqbEFYoB7
Adds pbt/spec.ts (properties: no PIN leakage, chip count matches model,
pushed names eventually render, suspended state eventually shows its
banner, plus a shadow-DOM-aware click action generator) and pbt/run.mjs
(spawns the static server, drives `bombadil browser test`, propagates
its exit code). Wires yarn test:pbt, extends lint to ./pbt, and adds
pbt/spec.ts to the tsconfig include so type-aware eslint can check it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wvy9UfQtNuvz1qqbEFYoB7
Root tsconfig uses moduleResolution: node (node10), which cannot see
Bombadil's package.json exports subpaths, so pbt/spec.ts type-checked
as implicit-any everywhere. pbt/tsconfig.json extends the root config
with moduleResolution: bundler (contained to pbt/, root untouched) and
points eslint's type-aware parser at it for pbt/**/*.ts.

Switching resolution strategies surfaces a real bug in
@antithesishq/bombadil 0.6.1's own shipped types: actions.d.ts
re-exports randomRange from random.d.ts, which never declares it.
node10 resolution never reached these files at all, silently masking
it. skipLibCheck avoids failing our build on a declaration file we
don't own.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wvy9UfQtNuvz1qqbEFYoB7
- harness.js chaos-rename: digit-free labels (letters instead of the
  numeric revision) so a rename can never collide with a live secret
  PIN and falsely trip maskedPinNeverLeaks; harden the property itself
  with digit-boundary regex matching instead of raw substring includes
- deepText: join shadow-root chunks with a newline so adjacent text
  nodes can't fuse into a spurious digit run across a shadow boundary
- run.mjs: listen for the bombadil child process's 'error' event (e.g.
  ENOENT) so a failed launch resolves the exit promise instead of
  hanging forever and leaking the static server
- package.json: test:pbt now uses && so a failed build doesn't
  property-test a stale bundle
- namesEventuallyRendered: read modelNames inside the eventually thunk
  so the pending obligation tracks the current model, not a stale
  snapshot from when the formula was first evaluated
- harness.js chaos-add-slot: reuse the lowest free slot in 1..6 instead
  of max+1, which saturated permanently and let chaos coverage decay
- spec.ts extractors: optional-chain window.__lcmHarness with safe
  defaults so a sample taken before harness.js runs doesn't crash
- serve.mjs: resolve ROOT with fileURLToPath instead of raw pathname
  (percent-encoding breaks paths with spaces), and answer /favicon.ico
  with 204 before attempting a file read, since a 404 there could trip
  bombadil's default noHttpErrorCodes property in headed runs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wvy9UfQtNuvz1qqbEFYoB7
… frontend

Bombadil's noHttpErrorCodes default property caught a real harness
defect: clicking a lock-codes chip triggers history.pushState to a
virtual Home Assistant route (e.g.
/config/integrations/integration/lock_code_manager#config_entry=...),
and Bombadil's reload action then requests that route directly from
pbt/serve.mjs, which 404s. The real HA frontend is a single-page app
that serves its shell for every route; the harness server now mirrors
that by falling back to pbt/harness/index.html for any extensionless
path, while still 404ing honestly on missing assets (identified by
extension).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wvy9UfQtNuvz1qqbEFYoB7
Copilot AI review requested due to automatic review settings July 31, 2026 19:59

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added dependencies Pull requests that update a dependency file python Pull requests that update Python code javascript Pull requests that update javascript code documentation Documentation changes github-config Changes to .github/ configuration files code-quality Pull requests that improve code quality labels Jul 31, 2026
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.98%. Comparing base (30519e7) to head (951cfe9).
⚠️ Report is 8 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main    #1385   +/-   ##
=======================================
  Coverage   96.98%   96.98%           
=======================================
  Files          53       53           
  Lines        6495     6495           
  Branches      470      470           
=======================================
  Hits         6299     6299           
  Misses        196      196           
Flag Coverage Δ
python 97.51% <100.00%> (ø)
typescript 95.00% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...om_components/lock_code_manager/providers/_util.py 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

Labels

code-quality Pull requests that improve code quality dependencies Pull requests that update a dependency file documentation Documentation changes github-config Changes to .github/ configuration files javascript Pull requests that update javascript code python Pull requests that update Python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants