Skip to content

fix(runtime): reject a non-int api_port at construction - #89

Merged
kstonekuan merged 3 commits into
Hebbian-Robotics:mainfrom
chiruu12:reject-non-int-api-port
Aug 22, 2026
Merged

fix(runtime): reject a non-int api_port at construction#89
kstonekuan merged 3 commits into
Hebbian-Robotics:mainfrom
chiruu12:reject-non-int-api-port

Conversation

@chiruu12

Copy link
Copy Markdown
Contributor

Summary

RuntimeConfig(api_port=True) no longer builds. The range check added in #84 cannot catch a bool, because True == 1 satisfies it, and the value is only ever str()-ed after that, so it rendered API_PORT=True into the bundle and nothing downstream objected. Construction now refuses a non-int first, with the same ValueError the CLI already handles.

Closes #85.

Why

The type test is isinstance(self.api_port, int) and not isinstance(..., bool) rather than type(...) is int. The issue suggested the stricter form, so here is the reasoning for the narrower one, and the precedent it follows.

An IntEnum member passes. It is an int by every test Python has, ty cannot flag it because it really is an int subclass, and it stringifies to bare digits on 3.11+, so the .env it renders is byte-identical to the one a plain int renders. CONTRIBUTING asks for typed variants over bare literals, so rejecting one would refuse the style the project recommends.

A numpy integer is refused. This is the half worth arguing, because str(np.int64(8080)) is also '8080', so it too would have rendered a correct bundle. test_append_accepts_non_json_measurement_scalars sets the opposite precedent for measurements: numpy scalars are user data arriving mid-append, and catalog.py fingerprints them with default=repr rather than crashing the whole episode over one value in a dict of many.

api_port is not that kind of surface. It is set once by the caller, it is rendered into a .env that is never rewritten, and it is interpolated into api_base_url, which the health wait dials and started_summary prints. There is no partial-success path to protect: refusing costs the caller one line at the call site and happens before anything is written, whereas accommodating means carrying a non-int through every one of those uses on the strength of str() alone.

ValueError for both halves, including the cases where TypeError is the more classically correct type, because _command_up catches ValueError and a TypeError escapes as a traceback.

Two prior behaviors collapse into this one error. A float passed the range check and rendered API_PORT=8080.0, which Compose will not take. A str or None failed the range check as a TypeError from the comparison, which nothing catches.

Validation

uv run pytest tests/test_runtime_bundle.py tests/test_runtime_cli.py -q   78 passed
uv run ruff check .                                                      All checks passed!
uv run ruff format --check .                                             96 files already formatted
uv run ty check                                                          All checks passed!

Against unmodified main at f9ac687, 7 of the new tests fail:

FAILED test_api_port_true_is_rejected_even_though_it_passes_the_range
FAILED test_api_port_rejects_a_numpy_integer
FAILED test_api_port_of_the_wrong_type_is_rejected_at_construction[8080-str]
FAILED test_api_port_of_the_wrong_type_is_rejected_at_construction[8080.0-float]
FAILED test_api_port_of_the_wrong_type_is_rejected_at_construction[None-NoneType]
FAILED test_api_port_of_the_wrong_type_is_rejected_at_construction[True-bool]
FAILED test_api_port_of_the_wrong_type_is_rejected_at_construction[False-bool]
7 failed, 71 passed

test_api_port_accepts_an_int_enum_member passes on main too, since main has no type check at all. It is a guard rail against a later tightening, not a proof of this change.

False is in the wrong-type list rather than beside True on purpose. It is 0, so the range check rejects it either way, just with the wrong reason.

Checklist

  • I added or updated outcome-focused tests for changed business logic.
  • I updated documentation for changed behavior, flags, formats, or requirements. No doc change: the --help text and RUNTIME.md already state the 1-65535 range, and no documented behavior changes for a caller who was passing an int.
  • I ran uv run ruff check --fix, uv run ruff format, and uv run ty check.
  • I ran the relevant pytest suite.
  • I did not add recordings, generated media, credentials, private URLs, or runtime artifacts.
  • I preserved stored-data compatibility or documented an explicit version change.

Copilot AI lite review requested due to automatic review settings August 22, 2026 06:58

Copilot AI left a comment

Copy link
Copy Markdown

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.

@kstonekuan kstonekuan 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.

Thank you @chiruu12. Merging.

You made the call I asked you to make and argued it on the right axis. isinstance over type(...) is int is correct, and the reason it is correct is the one you gave: a config field set once and rendered into a .env that is never rewritten is not the same surface as a measurement value arriving mid-append. Reading test_append_accepts_non_json_measurement_scalars as a precedent that does not extend here, rather than one to either follow blindly or ignore, is the part that matters.

ValueError for both halves is right, for exactly the reason you gave. Classifying False with the wrong types rather than beside True is a nice distinction: the range check catches it either way, just for the wrong reason, and the test docstring says so.

What I validated on your branch against current main (f9ac687):

  • ruff check, ruff format --check, ty check clean; 388 passed, 3 skipped.
  • Reproduced your 7-failures-on-main result, so every new test is load-bearing.

Two notes, neither blocking:

The IntEnum test passes on main too and you said so in the PR body. Keeping a test that only guards against a later tightening is the right instinct, and putting that in the docstring instead of leaving me to work it out is what made the review fast.

Docs: agreed, nothing to change. The 1-65535 range was already stated and no int-passing caller sees a difference.

On #90: good split, and the issue is better for it. The design half about the bare FileNotFoundError(path) call sites belongs in Discussions under Ideas whenever you want to open it. Do not wait on me to fix #90 first, they are independent.

@kstonekuan
kstonekuan merged commit fd5c061 into Hebbian-Robotics:main Aug 22, 2026
5 checks passed
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.

RuntimeConfig accepts api_port=True (bool passes the range check)

3 participants