Skip to content

refactor: minor tweaks#47

Merged
aorumbayev merged 2 commits intomainfrom
refactor/zen-of-python-refinements
Mar 23, 2026
Merged

refactor: minor tweaks#47
aorumbayev merged 2 commits intomainfrom
refactor/zen-of-python-refinements

Conversation

@aorumbayev
Copy link
Copy Markdown
Member

@aorumbayev aorumbayev commented Mar 22, 2026

Summary

Four Zen of Python-guided refinements addressing the highest-impact code quality gaps found in a thorough codebase review:

  • Typed request models (server/requests.py): Pydantic models replace manual dict[str, Any] extraction and cast() chains in all mutating API routes. Missing fields now return 422 with structured validation errors instead of opaque KeyError 400s.
  • Exception handling audit: All contextlib.suppress(Exception) blocks converted to explicit try/except with logger.debug, narrowing to KaganError | OSError where possible. Zero silent suppressions remain.
  • Shared web utilities: asBool, normalizeLauncher, quoteShell extracted from 3 components to lib/utils.ts with 15 unit tests. Three duplicate definitions removed.
  • Test fixture consolidation: Shared board_with_task fixture replaces 7 duplicate board fixtures across TUI test files. ~90 lines of boilerplate removed.

Refinement #5 (chat SSE hook unification) is deferred to a follow-up PR due to higher complexity.

Test plan

  • 745 Python tests pass (all platforms)
  • 149 web tests pass (including 15 new utility tests)
  • Ruff lint + format clean
  • TypeScript build passes
  • Server integration test updated for 422 validation responses

1. Typed request models for server routes (explicit > implicit)
   - New `server/requests.py` with Pydantic models for all mutating endpoints
   - `parse_body()` helper validates JSON against typed models
   - Eliminates cast() chains and manual field extraction in _routes.py
   - ValidationError returns 422 (not silent 400 on missing fields)

2. Audit silent exception suppressions (errors should never pass silently)
   - Replace all `contextlib.suppress(Exception)` with try/except + logger.debug
   - Narrow exception types where possible (KaganError, OSError)
   - 4 files: mcp/toolsets/tasks.py, sessions.py, server/_routes.py, tui/task_editor_modal.py

3. Extract shared web utilities (one obvious way to do it)
   - Move asBool, normalizeLauncher, quoteShell to lib/utils.ts
   - Remove 3 duplicate definitions across agent-control, settings-panel, task-detail-page
   - Add 15 unit tests for extracted functions

4. Consolidate TUI test fixtures (readability counts)
   - Add board_with_task shared fixture to tests/helpers/fixtures.py
   - Replace 7 duplicate board fixtures across TUI test files
   - ~90 lines of test boilerplate removed

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Mar 22, 2026

Greptile Summary

This PR applies four focused Zen-of-Python refactors across the server, web, and TUI test layers: typed Pydantic request models replace manual dict extraction in all mutating API routes; contextlib.suppress(Exception) blocks are converted to explicit try/except with logger.debug; three duplicate utility functions (asBool, normalizeLauncher, quoteShell) are extracted to lib/utils.ts with 15 new unit tests; and a shared board_with_task pytest fixture removes ~90 lines of per-file boilerplate across TUI tests.

  • The parse_body + Pydantic model approach is clean and well-integrated into the existing handle_errors_error_response pipeline.
  • RunTaskRequest.agent_backend: str = "" is a subtle regression: the old code treated an explicit {"agent_backend": null} as falsy and fell back to the settings default; Pydantic now rejects null for a bare str field with a 422. Declaring it as str | None = None preserves the original semantics.
  • _validate_criteria is copy-pasted between CreateTaskRequest and UpdateTaskRequest; a shared mixin would keep the logic DRY.
  • FollowUpRequest._strip_text emits "text is required" even when the field is present but has the wrong type, which is a misleading error message.
  • The board_with_task fixture sets ui.tui_tutorial_seen: true — a flag not present in several of the original per-file fixtures — which subtly changes the TUI state for those tests. Tests pass per the author, but the semantic difference is worth noting.

Confidence Score: 4/5

  • Safe to merge after fixing the RunTaskRequest.agent_backend null-regression; remaining issues are style-level.
  • The overall direction is sound — typed models, explicit exception handling, and shared utilities are all improvements. Three issues are identified: one P1 behavioral regression (agent_backend: str rejecting null), and two P2 style concerns (duplicated validator, misleading error message). The P1 is a narrow edge-case that likely doesn't affect current clients (no tests failed), but it represents a contract change. Fixing it is a one-line change. Once addressed, the PR is ready to merge.
  • src/kagan/server/requests.py — specifically RunTaskRequest.agent_backend type and the duplicated _validate_criteria validator.

Important Files Changed

Filename Overview
src/kagan/server/requests.py New file: typed Pydantic request models for all mutating routes. RunTaskRequest.agent_backend: str = "" is a silent breaking change vs the old null-tolerant behavior; _validate_criteria is duplicated across two models; FollowUpRequest._strip_text emits a misleading error for wrong-type input.
src/kagan/server/_helpers.py Adds parse_body[T] generic helper and ValidationError → 422 branch in _error_response. Clean and consistent with existing error-handling patterns.
src/kagan/server/_routes.py All mutating routes migrated from manual dict extraction to typed parse_body calls. The set_settings endpoint intentionally retains inline request.json() parsing (freeform key-value map). The model_fields_set loop for partial task updates is correct. Contextlib suppressions replaced with explicit logger.debug except/try blocks.
packages/web/src/lib/utils.ts Extracts asBool, normalizeLauncher, quoteShell, and LAUNCHER_BACKENDS from three components into the shared utility module. Implementations are identical to the originals; export surface is clean.
packages/web/src/lib/tests/utils.test.ts 15 new unit tests covering all three extracted utilities across edge-case inputs (null/undefined, casing, whitespace, shell metacharacters). Comprehensive coverage.
tests/helpers/fixtures.py New board_with_task fixture consolidates repeated setup across 7 TUI test files. It includes settings_update({"ui.tui_tutorial_seen": "true"}) which was not present in all original per-file fixtures — a deliberate but behavior-changing addition worth verifying in tests that care about tutorial state.
src/kagan/mcp/toolsets/sessions.py Replaces contextlib.suppress(Exception) with explicit try/except (KaganError, OSError) + logger.debug for worktree fetch. Exception scope is appropriately narrowed.
src/kagan/tui/screens/task_editor_modal.py Auto-save contextlib.suppress(Exception) converted to explicit except Exception with logger.debug. Broad catch is retained deliberately (auto-save failure should not surface to user) but is now observable via debug logging.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Route as _routes.py
    participant Helper as _helpers.py (parse_body)
    participant Model as requests.py (Pydantic Model)
    participant Core as ctx.client

    Client->>Route: POST /api/tasks {body}
    Route->>Helper: parse_body(request, CreateTaskRequest)
    Helper->>Helper: await request.json()
    Helper->>Model: model.model_validate(payload)
    alt Validation passes
        Model-->>Helper: typed CreateTaskRequest instance
        Helper-->>Route: body
        Route->>Core: tasks.create(body.title, ...)
        Core-->>Route: task
        Route-->>Client: 200 {ok: true, data: task}
    else Missing required field (e.g. title)
        Model-->>Helper: raises ValidationError
        Helper-->>Route: raises ValidationError
        Route->>Helper: _error_response(ValidationError)
        Helper-->>Client: 422 {ok: false, error: "..."}
    else Body is not a JSON object
        Helper-->>Route: raises ValueError
        Route->>Helper: _error_response(ValueError)
        Helper-->>Client: 400 {ok: false, error: "..."}
    end
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/kagan/server/requests.py
Line: 55

Comment:
**`agent_backend: str` rejects `null` from callers**

The old route used `payload.get("agent_backend", "")`, which treated an explicit `{"agent_backend": null}` JSON value as falsy and fell back to the settings default. Pydantic now rejects `null` for a bare `str` field with a 422 — a behavioral regression for any client that omits the key with a null rather than absent it.

To preserve the original semantics (absent _or_ null → fall back to settings), declare it as `str | None`:

```suggestion
    agent_backend: str | None = None
```

The existing fallback logic in `_routes.py` (`if not agent_backend`) already handles `None` correctly, so no route change is needed.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/kagan/server/requests.py
Line: 76-81

Comment:
**Misleading error message for wrong-type input**

`_strip_text` is a `mode="before"` validator, so it runs before Pydantic's own type coercion. If a caller sends `{"text": 42}` (an integer), `not isinstance(v, str)` fires first and raises `ValueError("text is required")` — which is factually incorrect. The field was _provided_; it was just the wrong type.

Consider separating the two failure modes:

```suggestion
    @field_validator("text", mode="before")
    @classmethod
    def _strip_text(cls, v: object) -> str:
        if not isinstance(v, str):
            raise ValueError("text must be a string")
        if not v.strip():
            raise ValueError("text is required")
        return v.strip()
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/kagan/server/requests.py
Line: 21-47

Comment:
**Duplicated `_validate_criteria` validator**

`CreateTaskRequest` and `UpdateTaskRequest` define byte-for-byte identical `_validate_criteria` methods. A shared base class removes the duplication and makes future changes (e.g. verifying list items are actually strings) apply in one place:

```python
class _TaskFieldsMixin(BaseModel):
    acceptance_criteria: list[str] | None = None

    @field_validator("acceptance_criteria", mode="before")
    @classmethod
    def _validate_criteria(cls, v: object) -> list[str] | None:
        if v is None:
            return None
        if not isinstance(v, list):
            raise ValueError("acceptance_criteria must be a list of strings")
        return v

class CreateTaskRequest(_TaskFieldsMixin):
    title: str
    ...

class UpdateTaskRequest(_TaskFieldsMixin):
    title: str | None = None
    ...
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "refactor: Zen of Python refinements acro..." | Re-trigger Greptile

Comment thread src/kagan/server/requests.py Outdated


class RunTaskRequest(BaseModel):
agent_backend: str = ""
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 agent_backend: str rejects null from callers

The old route used payload.get("agent_backend", ""), which treated an explicit {"agent_backend": null} JSON value as falsy and fell back to the settings default. Pydantic now rejects null for a bare str field with a 422 — a behavioral regression for any client that omits the key with a null rather than absent it.

To preserve the original semantics (absent or null → fall back to settings), declare it as str | None:

Suggested change
agent_backend: str = ""
agent_backend: str | None = None

The existing fallback logic in _routes.py (if not agent_backend) already handles None correctly, so no route change is needed.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/kagan/server/requests.py
Line: 55

Comment:
**`agent_backend: str` rejects `null` from callers**

The old route used `payload.get("agent_backend", "")`, which treated an explicit `{"agent_backend": null}` JSON value as falsy and fell back to the settings default. Pydantic now rejects `null` for a bare `str` field with a 422 — a behavioral regression for any client that omits the key with a null rather than absent it.

To preserve the original semantics (absent _or_ null → fall back to settings), declare it as `str | None`:

```suggestion
    agent_backend: str | None = None
```

The existing fallback logic in `_routes.py` (`if not agent_backend`) already handles `None` correctly, so no route change is needed.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/kagan/server/requests.py Outdated
Comment on lines +76 to +81
@field_validator("text", mode="before")
@classmethod
def _strip_text(cls, v: object) -> str:
if not isinstance(v, str) or not v.strip():
raise ValueError("text is required")
return v.strip()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Misleading error message for wrong-type input

_strip_text is a mode="before" validator, so it runs before Pydantic's own type coercion. If a caller sends {"text": 42} (an integer), not isinstance(v, str) fires first and raises ValueError("text is required") — which is factually incorrect. The field was provided; it was just the wrong type.

Consider separating the two failure modes:

Suggested change
@field_validator("text", mode="before")
@classmethod
def _strip_text(cls, v: object) -> str:
if not isinstance(v, str) or not v.strip():
raise ValueError("text is required")
return v.strip()
@field_validator("text", mode="before")
@classmethod
def _strip_text(cls, v: object) -> str:
if not isinstance(v, str):
raise ValueError("text must be a string")
if not v.strip():
raise ValueError("text is required")
return v.strip()
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/kagan/server/requests.py
Line: 76-81

Comment:
**Misleading error message for wrong-type input**

`_strip_text` is a `mode="before"` validator, so it runs before Pydantic's own type coercion. If a caller sends `{"text": 42}` (an integer), `not isinstance(v, str)` fires first and raises `ValueError("text is required")` — which is factually incorrect. The field was _provided_; it was just the wrong type.

Consider separating the two failure modes:

```suggestion
    @field_validator("text", mode="before")
    @classmethod
    def _strip_text(cls, v: object) -> str:
        if not isinstance(v, str):
            raise ValueError("text must be a string")
        if not v.strip():
            raise ValueError("text is required")
        return v.strip()
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread src/kagan/server/requests.py Outdated
Comment on lines +21 to +47
@field_validator("acceptance_criteria", mode="before")
@classmethod
def _validate_criteria(cls, v: object) -> list[str] | None:
if v is None:
return None
if not isinstance(v, list):
raise ValueError("acceptance_criteria must be a list of strings")
return v


class UpdateTaskRequest(BaseModel):
title: str | None = None
description: str | None = None
priority: str | int | None = None
base_branch: str | None = None
acceptance_criteria: list[str] | None = None
agent_backend: str | None = None
launcher: str | None = None

@field_validator("acceptance_criteria", mode="before")
@classmethod
def _validate_criteria(cls, v: object) -> list[str] | None:
if v is None:
return None
if not isinstance(v, list):
raise ValueError("acceptance_criteria must be a list of strings")
return v
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Duplicated _validate_criteria validator

CreateTaskRequest and UpdateTaskRequest define byte-for-byte identical _validate_criteria methods. A shared base class removes the duplication and makes future changes (e.g. verifying list items are actually strings) apply in one place:

class _TaskFieldsMixin(BaseModel):
    acceptance_criteria: list[str] | None = None

    @field_validator("acceptance_criteria", mode="before")
    @classmethod
    def _validate_criteria(cls, v: object) -> list[str] | None:
        if v is None:
            return None
        if not isinstance(v, list):
            raise ValueError("acceptance_criteria must be a list of strings")
        return v

class CreateTaskRequest(_TaskFieldsMixin):
    title: str
    ...

class UpdateTaskRequest(_TaskFieldsMixin):
    title: str | None = None
    ...
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/kagan/server/requests.py
Line: 21-47

Comment:
**Duplicated `_validate_criteria` validator**

`CreateTaskRequest` and `UpdateTaskRequest` define byte-for-byte identical `_validate_criteria` methods. A shared base class removes the duplication and makes future changes (e.g. verifying list items are actually strings) apply in one place:

```python
class _TaskFieldsMixin(BaseModel):
    acceptance_criteria: list[str] | None = None

    @field_validator("acceptance_criteria", mode="before")
    @classmethod
    def _validate_criteria(cls, v: object) -> list[str] | None:
        if v is None:
            return None
        if not isinstance(v, list):
            raise ValueError("acceptance_criteria must be a list of strings")
        return v

class CreateTaskRequest(_TaskFieldsMixin):
    title: str
    ...

class UpdateTaskRequest(_TaskFieldsMixin):
    title: str | None = None
    ...
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

- RunTaskRequest.agent_backend: str -> str | None (accept null from callers)
- FollowUpRequest._strip_text: separate TypeError from ValueError
- Extract shared _CriteriaMixin to deduplicate acceptance_criteria validator

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@aorumbayev aorumbayev changed the title refactor: Zen of Python refinements across server, web, and tests refactor: minor tweaks Mar 22, 2026
@aorumbayev aorumbayev merged commit be8e0ce into main Mar 23, 2026
17 checks passed
@aorumbayev aorumbayev deleted the refactor/zen-of-python-refinements branch March 23, 2026 08:38
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