fix: add SDK local readiness diagnostics for invalid ports - #145
fix: add SDK local readiness diagnostics for invalid ports#145gavin-at-pieces wants to merge 1 commit into
Conversation
|
Follow-up to the installer reliability work. This PR is intentionally narrower than the broader readiness contract:
Focused validation:
|
|
Validation update from my side:
Not claiming full CLI pytest or a full Neovim debug session — this is scoped vendored-wrapper + plugin import-surface validation. |
mark-at-pieces
left a comment
There was a problem hiding this comment.
Extensive PR Review — 6-Dimension Parallel Analysis
Verdict: Approve with suggestions — 1 high-severity issue, 11 medium, solid overall.
The PR's intent is sound: guarding against invalid ports and providing actionable diagnostics is a genuine improvement. The old host property fallback (return "http://127.0.0.1:39300") was verified to be dead code (the self.port getter always either returns a truthy string or raises ValueError from _port_scanning()), so removing it is a cleanup, not a regression. The port normalization actually fixes a pre-existing bug where invalid ports like "abc" were stored and produced malformed URLs like http://127.0.0.1:abc.
Should fix before merge
ARCH-2 (High) — Inconsistent tuple element ordering between _normalize_port and _probe_pieces_os_port
_normalize_port returns (port, code, message) but _probe_pieces_os_port returns (code, port, message) — the first two elements are swapped. Call sites destructure correctly today, but this is a maintenance trap: anyone copying a destructuring pattern from one call site to the other will silently swap code and port. Both return Tuple[Optional[str], ...] so no type error would catch the swap.
Suggestion: Align the ordering, or use NamedTuples to make field access unambiguous:
class NormResult(NamedTuple):
port: Optional[str]
code: str
message: str
class ProbeResult(NamedTuple):
code: Optional[str]
port: Optional[str]
error: Optional[str]SEC-1 (Medium) — isdigit() accepts Unicode superscript digits, causing unhandled crash
In _normalize_port, raw_port.isdigit() returns True for Unicode superscripts like '²' and '³', but int('²') raises ValueError — an unguarded crash path. Verified with Python:
'²'.isdigit()→Trueint('²')→ValueError'²'.isdecimal()→False(correct rejection)
Suggestion: Replace raw_port.isdigit() with raw_port.isdecimal(), or wrap the int() call in a try/except.
Suggested improvements
COR-1 (Medium) — Non-200 success codes fall through without explicit return in is_pieces_running
When the health probe returns a non-200 status, the diagnostic is set but no return False follows — execution falls through to the next loop iteration without sleeping. Note: urllib.request.urlopen raises HTTPError for 4xx/5xx codes, so this only triggers for non-200 success codes (201, 204) which health endpoints rarely return. Still, an explicit return False would make the control flow clearer.
BC-1 (Medium) — __str__/__repr__ can crash when PiecesOS isn't running (pre-existing)
Both methods call self.host, which can raise ValueError. This was already the case in the old code (the ValueError came from _port_scanning() propagating through the port getter). Good opportunity to add try/except guards since Python convention is that __repr__ should never raise:
def __repr__(self) -> str:
try:
return f"<PiecesClient(host={self.host})>"
except ValueError:
return f"<PiecesClient(port={self._port!r}, not connected)>"TEST-1 through TEST-4 (Medium) — Missing test coverage for several new code paths
- No direct tests for
_host_validation_error(6 branches, only indirectly tested through 4 malformed host cases) - No tests for
_normalize_portwith integer input (type hint acceptsUnion[str, int, None]) - No test for the valid port happy path (setting
"39300"→ verifyconnect_apiscalled correctly) - No test for
is_pieces_runningretry logic (maxium_retries > 1)
COR-2 (Medium) — Redundant _normalize_port call in host getter
The host getter calls _normalize_port(self.port), but the port setter already normalizes and stores only valid values. The getter's validation is redundant — it could simply check if self._port: and return _host_from_port(self._port).
Nice to have (follow-up PR)
- ARCH-1: Upgrade
_PORT_*string constants to astrEnum and the diagnostic dict to a dataclass for type safety and IDE autocomplete - ARCH-3: Extract readiness/diagnostic logic into a mixin or helper class — PiecesClient is growing large and this is a cohesive unit
Positive observations
- The new
urllib.parse.urlparse-based host validation in_host_validation_erroris a significant security improvement over the oldhost.startswith("http")check (which accepted malformed URLs like"httpevil.com") - Good parametrized test coverage for invalid port variants — 9 cases across 3 diagnostic codes including non-obvious inputs like
"39300/tcp" - Replacing the bare
except:inis_pieces_runningwith specificexcept ValueError/except Exceptionis a welcome fix (bare except swallowedKeyboardInterrupt)
Reviewed via 6-agent parallel analysis covering correctness, architecture, security, testing, breaking changes, and performance — with a 3-agent verification pass on the highest-impact findings.
Summary
None, or"null"ports do not produce REST or websocket URLs.is_pieces_running()still returnsbool, and startup failures still raiseValueErrorwith clearer diagnostic text.port = Nonereset safety.Tests
python -m pytest tests/test_client_readiness.py -vpython -m py_compile src/pieces_os_client/wrapper/client.py tests/test_client_readiness.pyScope
This PR is intentionally limited to Python SDK/CLI-facing local readiness diagnostics.
Out of scope:
Risks
.port.txtruntime contract remains upstream runtime evidence and is not implemented directly in this SDK PR.Rollback
src/pieces_os_client/wrapper/client.py.tests/test_client_readiness.py.