Skip to content

fix: clear the 10 open SonarCloud issues, incl. a quadratic regex on the TTS path (0.54.3)#157

Merged
OriNachum merged 5 commits into
mainfrom
fix/sonar-open-issues
Jul 25, 2026
Merged

fix: clear the 10 open SonarCloud issues, incl. a quadratic regex on the TTS path (0.54.3)#157
OriNachum merged 5 commits into
mainfrom
fix/sonar-open-issues

Conversation

@OriNachum

Copy link
Copy Markdown
Contributor

Clears all 10 open SonarCloud issues. The quality gate was already PASSED — every one of these is pre-existing debt in older code, so this is paydown, not a gate fix. Clearing them now removes the per-PR Sonar round-trip on any future PR that touches tts_client.py or app.py.

One of them was not a style nit

trailing_pause_ms matched !{3,}$ against sentence text. That pattern backtracks per start position when a long ! run is not at the end. Measured worst case:

40,000 "!" + "x":   regex 2649 ms   vs   rstrip 0.0055 ms   (~480,000x)

The text reaching this function is model output or caller-supplied /v1/audio/speech input, so the pathological case is reachable — this is a ReDoS-shaped stall on the TTS path, not a lint preference. Sonar rated it MAJOR; in context it is the most consequential item here.

Replaced with a linear rstrip("!") count. Because the function has no existing test (see below), I proved the rewrite behaviour-identical by differential-testing both implementations:

  • exhaustive over every string up to length 5 from the punctuation alphabet !?.…a<space>
  • plus 200,000 random strings up to length 40

209,331 strings, zero mismatches.

Cognitive complexity (S3776)

_synthesize_single carried complexity 30 against a limit of 15. Extracted:

  • _handle_tts_response(...) — the response-validation branch tree (status → empty body → truncation)
  • _is_truncated(clean, duration) and _min_plausible_duration(clean) — pure, and now directly testable

A _RETRY sentinel distinguishes "this attempt failed, retry within the same semaphore hold" from b"" ("give up"). Using empty bytes for both would have made a retry indistinguishable from "no audio" to the caller. Retry/reset semantics and the semaphore-hold guarantee are unchanged.

The mechanical eight

Rule Fix
S8513 ×2 chained endswith → single tuple-argument call
S8572 catch-all TTS retry arm uses log.exception so the traceback survives
S8410 ×3 FastAPI Annotated hints on /v1/audio/transcriptions; model also corrected from str = Form(None) to str | None
S5778 ×2 hoisted non-asserting setup out of pytest.raises so the tests assert what they claim

The S5778 pair is worth a word: in test_benchmark_all_lobes.py the block contained two calls, so the test would have passed had _make_all_lobes_args thrown — it now asserts only what it means to.

Testing gap, stated plainly

tts_client.py imports httpx at module top (realtime-container only), so the offline suite cannot import it — which is exactly why the file is in sonar.coverage.exclusions and why trailing_pause_ms had no test.

The new tests/test_tts_pause_and_truncation.py uses the repo's existing importorskip pattern (same as test_chatterbox_pcm16.py does for numpy): skips offline, 21 tests pass wherever the [realtime] extra is installed. Verified both ways. It includes a regression test asserting the 40k-character input completes in under 100 ms.

This is honest but not ideal — these helpers are pure and stdlib-only, and the httpx import is the only reason they can't be measured offline. Extracting them into a stdlib module so the offline suite covers them is a worthwhile follow-up, deliberately not done here to keep this PR to the Sonar findings.

Verification

2582 passed, 15 skipped
black + isort + flake8   clean
bandit                   0 issues (all confidence levels)
afi cli doctor --strict  PASS

Note on ordering

Versioned 0.54.3, sequenced deliberately behind #156 (which is open and takes 0.54.2). Both branch from main at 0.54.1, so if this merges first #156 will need a re-bump; merging #156 first means this needs no rebase.

No behaviour change intended

Every fix is refactor-or-equivalent. The one place behaviour could have drifted — the regex rewrite — is the one covered by the 209,331-string differential proof above.

  • lobes (Claude)

All ten were pre-existing; the quality gate was already PASSED, so this is
debt paydown rather than a gate fix. Clearing them now avoids the per-PR
Sonar round-trip on every future PR that touches these files.

The S8786 finding turned out to be more than a style nit. `trailing_pause_ms`
matched `!{3,}$` against sentence text, which backtracks per start position
when a long "!" run is NOT at the end. Measured worst case on 40k characters:

    regex  2649 ms   vs   rstrip  0.0055 ms   (~480,000x)

That text is model output or caller-supplied `/v1/audio/speech` input, so the
pathological case is reachable. Replaced with a linear rstrip count, and
proved the rewrite behaviour-identical by differential testing both
implementations over 209,331 strings (exhaustive to length 5 over the
punctuation alphabet, plus 200k random) — zero mismatches.

S3776: _synthesize_single carried cognitive complexity 30 against a limit of
15. Extracted _handle_tts_response (the response-validation branch tree) plus
the pure _is_truncated / _min_plausible_duration helpers. A _RETRY sentinel
distinguishes "retry within the same semaphore hold" from b"" ("give up") —
an empty-bytes retry would read as "no audio" to the caller.

Remaining eight are mechanical: chained endswith -> tuple form (S8513 x2),
log.exception in the catch-all arm (S8572), FastAPI Annotated hints with
`model` correctly typed `str | None` (S8410 x3), and hoisting non-asserting
setup out of pytest.raises so the tests assert what they claim (S5778 x2).

tts_client.py imports httpx at module top, so the offline suite cannot import
it and these helpers cannot be measured there. The new test file uses the
repo's existing importorskip pattern: it skips offline and passes (21 tests)
wherever the [realtime] extra is installed. Extracting the pure logic into a
stdlib module so the offline suite covers it is a worthwhile follow-up.

Verified: 2582 passed / 15 skipped, black + isort + flake8 clean, bandit 0
issues, afi cli doctor --strict PASS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JivjPuXSzBAwjDcEyURyWM
@OriNachum

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Clear SonarCloud findings; harden TTS pause detection and refactor synth response flow

🐞 Bug fix 🧪 Tests ⚙️ Configuration changes 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Replace ReDoS-prone TTS pause regex with linear trailing "!" counting.
• Refactor TTS synthesis response validation to reduce cognitive complexity and preserve retry
 semantics.
• Add realtime-only tests, fix remaining Sonar issues, and bump release to 0.54.3.
Diagram

graph TD
  A["FastAPI app"] --> B["TTS client"] --> C["Response validator"] --> D["httpx TTS request"] --> E{{"TTS backend"}}
  B --> F["Pause calc"]
  G["Realtime-only tests"] --> F --> B
  G --> C
  subgraph Legend
    direction LR
    _svc([Service/Module]) ~~~ _test[Tests] ~~~ _ext{{External}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Extract pure TTS helpers into a stdlib-only module
  • ➕ Offline CI can import and test pause/truncation logic without httpx
  • ➕ Clearer separation between pure logic and I/O concerns
  • ➕ Improves coverage without relying on realtime extras
  • ➖ New module surface area and potential API churn
  • ➖ Requires careful dependency/ownership boundaries for realtime code
2. Move httpx import behind a runtime-only boundary
  • ➕ Allows importing tts_client in offline environments
  • ➕ Keeps helper functions in-place without a new module
  • ➖ May complicate type checking and module initialization expectations
  • ➖ Can mask missing runtime deps until execution time

Recommendation: The current approach is a good incremental paydown: it removes a reachable ReDoS-shaped stall and reduces synthesis complexity without changing outward semantics, and it adds regression tests where feasible. As a follow-up, consider extracting the pure pause/truncation helpers (or deferring the httpx import) so the offline suite can execute these tests and provide broader CI coverage.

Files changed (8) +240 / -79

Bug fix (2) +118 / -75
app.pyFix /v1/audio/transcriptions parameter typing using Annotated +4/-3

Fix /v1/audio/transcriptions parameter typing using Annotated

• Introduces typing.Annotated for FastAPI parameters and corrects the optional model form field type to str | None, aligning with Sonar guidance and FastAPI conventions.

lobes/realtime/app.py

tts_client.pyRemove quadratic regex in trailing_pause_ms; extract TTS response helpers +114/-72

Remove quadratic regex in trailing_pause_ms; extract TTS response helpers

• Replaces the end-anchored !{3,}$ regex with a linear rstrip-based trailing-run count to avoid pathological backtracking. Extracts _handle_tts_response, _is_truncated, and _min_plausible_duration (plus a _RETRY sentinel) to reduce _synthesize_single complexity and keep retry-vs-give-up behavior unambiguous. Updates the catch-all retry logging to log.exception to preserve tracebacks.

lobes/realtime/tts_client.py

Tests (3) +105 / -2
test_benchmark_all_lobes.pyMake pytest.raises cover only the expected failing call +2/-1

Make pytest.raises cover only the expected failing call

• Hoists argument construction out of the pytest.raises block so the test fails if setup raises unexpectedly, ensuring it asserts only cmd_benchmark error behavior.

tests/test_benchmark_all_lobes.py

test_cli_runtime.pyHoist setup out of pytest.raises for explicit-missing path case +2/-1

Hoist setup out of pytest.raises for explicit-missing path case

• Creates the missing path string before entering pytest.raises so the test strictly validates resolve_deployment_dir raising the intended ModelGearError.

tests/test_cli_runtime.py

test_tts_pause_and_truncation.pyAdd realtime-only tests for pause mapping, truncation detection, and ReDoS regression +101/-0

Add realtime-only tests for pause mapping, truncation detection, and ReDoS regression

• Adds unit tests for trailing_pause_ms punctuation mapping, end-anchoring semantics, and a performance regression check for the former quadratic regex case. Adds direct tests for _min_plausible_duration and _is_truncated; the module is skipped when httpx is unavailable.

tests/test_tts_pause_and_truncation.py

Documentation (1) +15 / -0
CHANGELOG.mdDocument 0.54.3 Sonar fixes and new TTS regression tests +15/-0

Document 0.54.3 Sonar fixes and new TTS regression tests

• Adds a 0.54.3 entry describing the ReDoS-safe pause logic, the TTS refactor for cognitive complexity, FastAPI typing updates, pytest.raises tightening, and the new realtime-only test module.

CHANGELOG.md

Other (2) +2 / -2
pyproject.tomlBump project version to 0.54.3 +1/-1

Bump project version to 0.54.3

• Updates the package version to 0.54.3 to reflect the Sonar debt paydown release.

pyproject.toml

uv.lockSync lockfile version to 0.54.3 +1/-1

Sync lockfile version to 0.54.3

• Updates the editable package version in the lockfile to match the 0.54.3 release bump.

uv.lock

Bare _handle_tts_response / _is_truncated / _min_plausible_duration were
parsed as emphasis markers with spaces. Backticks are correct anyway — they
are code identifiers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JivjPuXSzBAwjDcEyURyWM
@qodo-code-review

qodo-code-review Bot commented Jul 24, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 80 rules

Grey Divider


Remediation recommended

1. Type-ignored TTS return ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
_handle_tts_response() is typed as returning object, and _synthesize_single() returns that value
with # type: ignore[return-value], which removes static guarantees that the function returns
bytes and makes future edits more likely to accidentally leak a non-bytes sentinel past the retry
check.
Code

lobes/realtime/tts_client.py[R383-386]

+                outcome = _handle_tts_response(resp, clean, tag, elapsed, attempt, lane)
+                if outcome is _RETRY:
+                    continue
+                return outcome  # type: ignore[return-value]
Relevance

⭐⭐⭐ High

Low-risk typing cleanup; team has history accepting maintainability improvements in realtime
modules.

PR-#54
PR-#24

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The helper is explicitly annotated to return object, and the caller explicitly suppresses the
return-type mismatch when returning its value, demonstrating the loss of static enforcement around
the bytes/sentinel contract.

lobes/realtime/tts_client.py[241-248]
lobes/realtime/tts_client.py[383-386]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`_handle_tts_response()` returns a retry sentinel (`_RETRY`) or PCM bytes, but it is currently annotated as returning `object`, and `_synthesize_single()` suppresses the resulting type error with `# type: ignore[return-value]`. This weakens the type contract and makes it easier for future changes to accidentally return a non-`bytes` value to callers without being caught by static checks.

### Issue Context
The current runtime behavior appears correct because `_synthesize_single()` checks `outcome is _RETRY` before returning, and all other current return paths are `bytes`. The problem is that the typing no longer enforces this invariant.

### Fix Focus Areas
- lobes/realtime/tts_client.py[241-248]
- lobes/realtime/tts_client.py[383-386]

### Suggested fix
- Introduce an explicit sentinel type (e.g., a private class `_Retry: ...` and a singleton `RETRY = _Retry()`), and annotate the helper as returning `bytes | _Retry`.
- In `_synthesize_single()`, narrow the type after the sentinel check so the final `return` is type-correct (and remove `# type: ignore[return-value]`).
- Optionally add a small `TypeAlias` for readability, e.g. `TTSOutcome: TypeAlias = bytes | _Retry`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread lobes/realtime/tts_client.py Outdated
Qodo review comment 3647941477 on PR #157.

The S3776 complexity fix in 0.54.3 split _handle_tts_response() out of
_synthesize_single(), but typed it as returning `object` and suppressed the
resulting mismatch at the call site with `# type: ignore[return-value]`. That
left "callers only ever see bytes" as an assertion rather than a checked
invariant, so a future edit could leak a non-bytes sentinel past the retry
check without a static error.

Replace the bare `_RETRY = object()` with a dedicated `_Retry` sentinel type,
annotate the helper `bytes | _Retry`, and narrow at the call site with
`isinstance` — identity against a module global does not narrow a union for a
type checker, so `is _RETRY` would still have needed the suppression.

Runtime behaviour is unchanged: the same singleton is returned and compared,
and both retry paths still continue the loop.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KK2FrZ6N7q2kdNTcQsqmzb
The conflict resolution joined the 0.54.3 list directly onto main's
0.54.2 heading (MD032/MD022).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F7m3KqJ5MyxnxUuqemYLTB
@sonarqubecloud

Copy link
Copy Markdown

@OriNachum
OriNachum merged commit b966b83 into main Jul 25, 2026
10 checks passed
@OriNachum
OriNachum deleted the fix/sonar-open-issues branch July 25, 2026 09:11
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.

1 participant