Skip to content

Add search limit validation, typed models, and mypy strict CI#38

Open
clean6378-max-it wants to merge 6 commits into
masterfrom
feat/typed-models-search-mypy
Open

Add search limit validation, typed models, and mypy strict CI#38
clean6378-max-it wants to merge 6 commits into
masterfrom
feat/typed-models-search-mypy

Conversation

@clean6378-max-it
Copy link
Copy Markdown
Collaborator

@clean6378-max-it clean6378-max-it commented May 18, 2026

Closes #37

Add search limit validation, typed models, and mypy strict CI

Summary

Week 3 Monday type-safety foundations (9 pt) for claude-code-chat-browser:

  • Validate /api/search limit and return 400 for bad input (no more 500 crashes).
  • Add models/ with TypedDict shapes at public API/parser boundaries.
  • Enable mypy --strict on api, utils, and models plus a CI job.

No intentional JSON or UI behavior changes.

Problem

  1. Search crashes on bad limitint(request.args.get("limit", 50)) raised ValueError for abc, 1.5, etc., surfacing HTTP 500.
  2. Implicit data shapes — Sessions, projects, search hits, and stats used ad-hoc dicts with no shared types.
  3. No static typing in CI — Type regressions were only caught at runtime.

Changes

Block A — Search limit validation (1 pt)

  • api/search.py: _parse_limit() validates limit.
  • Invalid input → 400 + {"error": "Invalid limit: must be a positive integer"}.
  • Omitted or empty limit → default 50.
  • tests/test_search.py: 5 tests (integer string, float, non-numeric, default, empty query).

Block B — Typed models (5 pt)

New models/ package:

Module Types
session.py SessionDict, MessageDict, SessionMetadataDict, QuickSessionInfoDict
project.py ProjectDict, SessionListItemDict, ProjectSessionRowDict
search.py SearchHitDict
stats.py SessionStatsDict, FilesTouchedDict
errors.py ErrorResponse
export.py ExportStateDict

Annotated boundaries in utils/jsonl_parser.py, utils/session_path.py, utils/session_stats.py, api/*, exporters, and exclusion helpers.

Block C — Mypy strict + CI (3 pt)

  • pyproject.tomlstrict = true for api, utils, models
  • requirements-dev.txtmypy==1.15.0, types-Flask==1.1.6
  • .github/workflows/ci.yml — new mypy job
  • api/_flask_types.pyFlaskReturn, json_ok()

Out of scope

  • Structured ErrorCode on error responses
  • Full HTTP / CLI test matrix
  • Frontend / Playwright integration tests
  • API reference and contribution docs

Test plan

  • pytest tests/test_search.py -v — 5 passed
  • pytest -q — 234 passed
  • mypy — clean on 23 source files
  • curl.exe "http://127.0.0.1:5000/api/search?q=test&limit=abc"400 JSON
  • curl.exe "http://127.0.0.1:5000/api/search?q=test&limit=5"200
  • UI: projects, session view, /#search unchanged
pip install -r requirements-dev.txt
pytest tests/test_search.py -v
pytest -q
mypy

Notes for reviewers

  • scripts/export.py is not in the mypy package list; shared utils/ + models/ are typed at boundaries the CLI uses.
  • Error bodies remain {"error": "..."} only; code fields come in a follow-up PR.
  • Restart the dev server after pull when testing api/search.py manually.

Related

  • Week 3 Monday plan: search fix + TypedDict + mypy CI (Chen, 9 pt)
  • Issue: type-safety foundations (High × 3)

Summary by CodeRabbit

  • Tests

    • Added automated tests for search limit validation and empty-query behavior.
  • Bug Fixes

    • Search: stricter limit validation; empty queries return an empty list.
    • API: more consistent JSON error responses and safer error handling to reduce uncaught failures.
  • Chores

    • Added static typing checks to CI and expanded type schemas across the codebase.
  • UI

    • Improved click/navigation handling for search results and session sidebar/download actions.
  • Exports

    • Markdown/JSON exports: improved image rendering and assistant "thinking" content.

Review Change Stack

- Return 400 with a clear JSON error when /api/search limit is invalid
  instead of crashing with ValueError on bad input (abc, 1.5, etc.)
- Add models/ TypedDict shapes and annotate parser/API boundaries
- Enable mypy --strict on api, utils, and models; add CI mypy job
- Add tests/test_search.py for limit validation cases
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds TypedDict models, a shared Flask response helper, validates search limit input with tests, annotates API handlers and utilities with typed signatures, and enables strict mypy in CI. No functional behavior changes except returning 400 for invalid search limits.

Changes

Type Safety and Search Validation Overhaul

Layer / File(s) Summary
Type model definitions
models/__init__.py, models/errors.py, models/export.py, models/project.py, models/search.py, models/session.py, models/stats.py
Introduces TypedDict schemas for sessions, messages, metadata, projects, search hits, stats, export state, and error responses.
Flask response typing foundation
api/_flask_types.py
Defines FlaskReturn and json_ok(...) to standardize typed Flask JSON responses.
Search endpoint validation and typing
api/search.py, tests/test_search.py
Adds _parse_limit() to validate limit as a positive integer, returns 400 on invalid input, uses json_ok and FlaskReturn, and adds tests covering edge cases.
API endpoint typing: projects and sessions list
api/projects.py
Annotates handlers with FlaskReturn, normalizes session-row fields (sorted models_used, tokens), handles parse errors with logging and error rows, and returns via json_ok.
API endpoint typing: session detail and stats
api/sessions.py
Adds typed parameters/returns, consistent path validation and file-not-found handling, exception logging, and json_ok responses for all outcomes.
API endpoint typing: export
api/export_api.py
Refactors export-state helpers with typed local functions, annotates handlers with FlaskReturn, uses json_ok for errors/results, and preserves send_file behavior (with type-ignore for download_name).
Utility function typing: core parsers and exporters
utils/jsonl_parser.py, utils/json_exporter.py, utils/md_exporter.py, utils/session_stats.py
Applies TypedDicts across parsing, serialization, markdown export, and stats; minor defensive logic change in web-search result counting and markdown rendering tweaks.
Utility function typing: path discovery and state management
utils/session_path.py, utils/export_state_store.py, utils/export_day_filter.py, utils/exclusion_rules.py
Types session/project listings, export-state store/lock API, export-day filtering, and exclusion-rule APIs without changing runtime behavior.
Frontend event handling
static/js/search.js, static/js/sessions.js
Replaces inline onclick attributes with delegated click listeners that use data-* attributes; adds downloadSession import and binding helpers.
MyPy configuration and CI enablement
pyproject.toml, requirements-dev.txt, .github/workflows/ci.yml
Adds strict mypy config targeting Python 3.12 (api/utils/models), pins mypy in dev requirements, and adds a mypy CI job.
Search tests
tests/test_search.py
Adds pytest fixture and tests verifying limit parsing behavior and empty-query handling.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • jonathanMLDev
  • wpak-ai

"🐰 I hopped through types and lined each trace,
I taught the search to fail with grace.
Mypy watches, tests now sing,
No more crashes from an odd string.
Hooray — the code sleeps well tonight!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the three main changes: search limit validation, typed models addition, and mypy strict CI setup.
Linked Issues check ✅ Passed The PR successfully implements all three core requirements from issue #37: (A) search limit validation with 400 errors and tests, (B) typed models with TypedDict in models/ package at public boundaries, and (C) mypy strict configuration and CI job.
Out of Scope Changes check ✅ Passed All changes directly support the three stated objectives. Frontend changes (search.js, sessions.js) are minimal delegated-handler refactors; they support the API typing work but introduce no unrelated functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/typed-models-search-mypy

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
api/export_api.py (1)

279-306: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard single-session export parse/render errors with a stable JSON 500 response.

export_session currently lets parse_session(...), compute_stats(...), and render steps raise uncaught exceptions. This can break API response consistency on failures.

Proposed fix
     fmt = request.args.get("format", "md")
-    session = parse_session(filepath)
-    rules = current_app.config.get("EXCLUSION_RULES") or []
-    if is_session_excluded(rules, session, project_name):
-        return json_ok({"error": "Session not found"}), 404
-    stats = compute_stats(session)
-    title_slug = slugify(session["title"], default="session")
+    try:
+        session = parse_session(filepath)
+        rules = current_app.config.get("EXCLUSION_RULES") or []
+        if is_session_excluded(rules, session, project_name):
+            return json_ok({"error": "Session not found"}), 404
+        stats = compute_stats(session)
+        title_slug = slugify(session["title"], default="session")
+    except Exception:
+        current_app.logger.exception("Failed to export session %s", session_id)
+        return json_ok({"error": "Failed to export session"}), 500
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/export_api.py` around lines 279 - 306, The export_session route currently
calls parse_session, compute_stats, session_to_json and session_to_markdown
without error handling; wrap the parse/render/stat computation block in a
try/except that catches Exception, logs the exception (e.g.,
current_app.logger.exception) and returns a stable JSON 500 response (body like
{"error": "Internal server error exporting session"}) instead of letting
exceptions bubble; ensure you guard the calls to parse_session, compute_stats,
session_to_json/session_to_markdown and slugify/ send_file decision logic so any
failure returns that JSON 500 (optionally include error details only when
app.debug is true).
🧹 Nitpick comments (2)
pyproject.toml (1)

5-9: 💤 Low value

Consider removing redundant test exclusion override.

The exclude = ["tests/"] at line 5 already prevents mypy from analyzing the tests directory. The override block at lines 7–9 that ignores errors for tests.* modules is redundant unless checked packages explicitly import from tests (which would be unusual).

♻️ Simplify configuration by removing override
 [tool.mypy]
 python_version = "3.12"
 strict = true
 packages = ["api", "utils", "models"]
 exclude = ["tests/"]
-
-[[tool.mypy.overrides]]
-module = "tests.*"
-ignore_errors = true
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyproject.toml` around lines 5 - 9, Remove the redundant mypy override that
ignores errors for tests by deleting the [[tool.mypy.overrides]] block that sets
module = "tests.*" and ignore_errors = true, since exclude = ["tests/"] already
prevents analysis of the tests directory; ensure only the exclude setting
remains to keep configuration simple and avoid masking issues.
utils/session_path.py (1)

56-63: 💤 Low value

Consider removing the redundant fallback check.

After updating _get_display_name to accept name as the fallback parameter (line 58), the check at lines 62-63 is now redundant. When extraction fails, _get_display_name returns name directly (via fallback or ""), so if not display_name: will never be true (assuming name is non-empty).

The current code works correctly, but you could remove lines 62-63 to eliminate the redundant check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@utils/session_path.py` around lines 56 - 63, Remove the redundant fallback
assignment: since _get_display_name(os.path.join(project_dir, jf), name) already
returns name when extraction fails, drop the post-loop check that reassigns
display_name = name; specifically update the loop in which display_name is set
(references: jsonl_files, project_dir, name, and _get_display_name) to rely on
the function's fallback and remove the subsequent if not display_name block.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 69-70: Update the pip cache dependency path so it includes both
requirements-dev.txt and requirements.txt (the same pattern used in the pytest
job) to ensure cache invalidation when either file changes; locate the cache
block that sets "cache: pip" and "cache-dependency-path" in the CI workflow and
change the dependency path value to list both files (requirements-dev.txt and
requirements.txt).

In `@api/sessions.py`:
- Around line 55-58: The stats endpoint currently parses the session and returns
compute_stats without applying exclusion rules; modify the get_session_stats
handler to call is_session_excluded(...) (the same check used by get_session)
after obtaining the session (or reuse get_session to centralize logic), and if
the session is excluded return the same error/response (e.g., 404 or json_error)
as get_session instead of json_ok(stats); ensure you reference parse_session,
compute_stats and is_session_excluded so the exclusion check is applied before
returning stats.

In `@requirements-dev.txt`:
- Around line 3-4: Remove the obsolete types-Flask entry from
requirements-dev.txt (the package name "types-Flask" from the diff) because
Flask 3.1.3 provides native type stubs, and bump the mypy version string from
"mypy==1.15.0" to the current stable release (e.g., "mypy==2.1.0") in
requirements-dev.txt so the dev requirements use the up-to-date type checker.

---

Outside diff comments:
In `@api/export_api.py`:
- Around line 279-306: The export_session route currently calls parse_session,
compute_stats, session_to_json and session_to_markdown without error handling;
wrap the parse/render/stat computation block in a try/except that catches
Exception, logs the exception (e.g., current_app.logger.exception) and returns a
stable JSON 500 response (body like {"error": "Internal server error exporting
session"}) instead of letting exceptions bubble; ensure you guard the calls to
parse_session, compute_stats, session_to_json/session_to_markdown and slugify/
send_file decision logic so any failure returns that JSON 500 (optionally
include error details only when app.debug is true).

---

Nitpick comments:
In `@pyproject.toml`:
- Around line 5-9: Remove the redundant mypy override that ignores errors for
tests by deleting the [[tool.mypy.overrides]] block that sets module = "tests.*"
and ignore_errors = true, since exclude = ["tests/"] already prevents analysis
of the tests directory; ensure only the exclude setting remains to keep
configuration simple and avoid masking issues.

In `@utils/session_path.py`:
- Around line 56-63: Remove the redundant fallback assignment: since
_get_display_name(os.path.join(project_dir, jf), name) already returns name when
extraction fails, drop the post-loop check that reassigns display_name = name;
specifically update the loop in which display_name is set (references:
jsonl_files, project_dir, name, and _get_display_name) to rely on the function's
fallback and remove the subsequent if not display_name block.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 371cc1f8-ed5c-4963-b8bf-2da316d77719

📥 Commits

Reviewing files that changed from the base of the PR and between 4bbb456 and 72f0f96.

📒 Files selected for processing (24)
  • .github/workflows/ci.yml
  • api/_flask_types.py
  • api/export_api.py
  • api/projects.py
  • api/search.py
  • api/sessions.py
  • models/__init__.py
  • models/errors.py
  • models/export.py
  • models/project.py
  • models/search.py
  • models/session.py
  • models/stats.py
  • pyproject.toml
  • requirements-dev.txt
  • tests/test_search.py
  • utils/exclusion_rules.py
  • utils/export_day_filter.py
  • utils/export_state_store.py
  • utils/json_exporter.py
  • utils/jsonl_parser.py
  • utils/md_exporter.py
  • utils/session_path.py
  • utils/session_stats.py

Comment thread .github/workflows/ci.yml Outdated
Comment thread api/sessions.py
Comment thread requirements-dev.txt Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
utils/session_path.py (1)

92-93: ⚡ Quick win

Consider narrowing exception handling or adding logging.

The broad except Exception: pass silently swallows all errors, including unexpected ones, making debugging harder. Since this is a defensive helper that reads JSONL files, consider catching specific exceptions (OSError, json.JSONDecodeError, UnicodeDecodeError) or adding minimal logging for visibility.

♻️ Proposed refinement
-    except Exception:
-        pass
+    except (OSError, json.JSONDecodeError, UnicodeDecodeError):
+        # Failed to extract display name; caller will use fallback
+        pass
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@utils/session_path.py` around lines 92 - 93, The broad "except Exception:
pass" in utils/session_path.py silently swallows file/read/parse errors; replace
it by catching specific exceptions (e.g., OSError, json.JSONDecodeError,
UnicodeDecodeError) around the JSONL read/parse logic (the block containing the
except) and log the error instead of passing—use the module or passed-in logger
(e.g., logger.error or logger.exception) with a concise message including the
filename and error to aid debugging while still allowing other unexpected
exceptions to surface.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@utils/session_path.py`:
- Around line 55-62: The loop that determines display_name now breaks on the
first JSONL file because _get_display_name is being called with name as the
fallback, making failures return a truthy value; revert to passing None (or
otherwise detect a true extraction) so failures return None and the loop
continues: change the call to _get_display_name(os.path.join(project_dir, jf),
None) (or keep name but only break when candidate is not None and candidate !=
name) so display_name is only set/breaks when a real extracted candidate is
returned; this fixes the unintended early exit from the jsonl_files loop
(symbols: display_name, jsonl_files, _get_display_name, project_dir, name).

---

Nitpick comments:
In `@utils/session_path.py`:
- Around line 92-93: The broad "except Exception: pass" in utils/session_path.py
silently swallows file/read/parse errors; replace it by catching specific
exceptions (e.g., OSError, json.JSONDecodeError, UnicodeDecodeError) around the
JSONL read/parse logic (the block containing the except) and log the error
instead of passing—use the module or passed-in logger (e.g., logger.error or
logger.exception) with a concise message including the filename and error to aid
debugging while still allowing other unexpected exceptions to surface.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9442bfc9-cf96-4ea4-a3c9-25f07f5d9b11

📥 Commits

Reviewing files that changed from the base of the PR and between 72f0f96 and a38b162.

📒 Files selected for processing (6)
  • .github/workflows/ci.yml
  • api/export_api.py
  • api/sessions.py
  • pyproject.toml
  • requirements-dev.txt
  • utils/session_path.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • .github/workflows/ci.yml
  • api/export_api.py
  • api/sessions.py

Comment thread utils/session_path.py
@timon0305
Copy link
Copy Markdown
Collaborator

image When I click every conversation, it is showing like strange.

@timon0305
Copy link
Copy Markdown
Collaborator

in light mode, when I reload project, it is showing dark and then showing light again
image

@timon0305
Copy link
Copy Markdown
Collaborator

Why hamburger button is needed? Is this clickable?

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@static/js/search.js`:
- Around line 31-35: The click handler currently assumes e.target is an Element
and proceeds even if data-session-id is missing; update the guard so you first
check that e.target is an instanceof Element before calling closest, then
require both attributes (e.g., result.getAttribute('data-project') and
result.getAttribute('data-session-id')) to be non-null/ non-empty before
updating window.location.hash (use encodeURIComponent as before); reference the
variables result, project, sessionId and the window.location.hash assignment
when locating where to add these checks.

In `@static/js/sessions.js`:
- Around line 102-104: The click handler attached to sidebar calls
e.target.closest without ensuring e.target is an Element, which can throw for
non-Element event targets; update the sidebar.addEventListener callback to first
guard that e.target is an Element (e.g., check instanceof Element or that
closest exists) and return early if not, then call
e.target.closest('button.sidebar-item[data-session-id]') to assign btn and
continue as before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a5c44ad7-86d0-43f8-8214-db7df0681ca8

📥 Commits

Reviewing files that changed from the base of the PR and between 62e557c and 46452dd.

📒 Files selected for processing (2)
  • static/js/search.js
  • static/js/sessions.js

Comment thread static/js/search.js
Comment thread static/js/sessions.js
Comment thread requirements-dev.txt
@@ -1,2 +1,3 @@
-r requirements.txt
pytest==9.0.2
mypy==2.1.0
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

PR description says mypy==1.15.0 and types-Flask==1.1.6. The diff actually adds mypy==2.1.0 and no types-Flask. Either the description is stale or the diff doesn't match what was tested locally. Mypy 2.x is a major version with notable behavioural differences from 1.x — worth confirming which version was used for "mypy clean on 23 source files" in the test plan and aligning the description/pin.

Comment thread api/projects.py
Comment on lines +68 to +76
result.append(cast(ProjectSessionRowDict, {
**s,
"title": parsed["title"],
"models": meta["models_used"],
"models": sorted(models) if isinstance(models, set) else list(models),
"tokens": meta["total_input_tokens"] + meta["total_output_tokens"],
"tool_calls": meta["total_tool_calls"],
"first_timestamp": meta["first_timestamp"],
"last_timestamp": meta["last_timestamp"],
})
}))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

result.append(cast(ProjectSessionRowDict, {
**s,
"title": parsed["title"],
...
}))
cast is a runtime no-op that silences mypy without checking. The whole point of TypedDict is compile-time checking; the cast removes it.

Comment thread api/sessions.py
filepath = safe_join(base, project_name, f"{session_id}.jsonl")
except ValueError:
return jsonify({"error": "Invalid path"}), 400
return json_ok({"error": "Invalid path"}), 400
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

return json_ok({"error": "Invalid path"}), 400
return json_ok({"error": f"Session {session_id} not found"}), 404
return json_ok({"error": "Session not found"}), 404
return json_ok({"error": "Failed to parse session"}), 500

json_ok is used for both success and error responses. The name says "success", the call sites add a non-2xx status tuple. Rename to json_response() / as_json(), or add a json_error(message, status) sibling that returns the (Response, int) pair in one call. As-is, every error site has to remember the trailing , 404 — confusing.

Comment thread api/sessions.py

if not os.path.isfile(filepath):
return jsonify({"error": f"Session {session_id} not found"}), 404
return json_ok({"error": f"Session {session_id} not found"}), 404
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

same

Comment thread api/sessions.py
if is_session_excluded(rules, session, project_name):
return jsonify({"error": "Session not found"}), 404
return jsonify(session)
return json_ok({"error": "Session not found"}), 404
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

same

Comment thread pyproject.toml
python_version = "3.12"
strict = true
packages = ["api", "utils", "models"]
exclude = ["tests/"]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Mypy strict skips the whole tests/ tree. Test code can drift (untyped helpers, broken exclusion regexes) without the gate catching it. Either include tests/ with a [[tool.mypy.overrides]] block for strict = false, or at minimum run a non-strict pass over tests/.

Comment thread .github/workflows/ci.yml
Comment on lines +60 to +77

mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
cache-dependency-path: |
requirements.txt
requirements-dev.txt
- name: Install dev dependencies
run: pip install -r requirements-dev.txt

- name: Run mypy (strict)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Job added but the diff doesn't show whether it's listed as a required status check on the branch protection. A failing mypy job in CI doesn't block merge unless the gate is enforced — verify with whoever owns the repo settings.

Comment thread api/_flask_types.py

from flask import Response, jsonify

FlaskReturn = Union[Response, tuple[Response, int]]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Misses Flask's other valid return shapes: tuple[Response, int, dict[str, str]] (with headers), tuple[str, int] (plain text), etc. For now the codebase only emits the two listed shapes — fine — but add a one-line comment noting the narrow scope so the next contributor knows to widen if they add a header response.

Comment thread api/search.py
Comment on lines +18 to +28
def _parse_limit(raw: str | None, default: int = _DEFAULT_LIMIT) -> int:
"""Parse a positive integer limit from a query string value."""
if raw is None or raw.strip() == "":
return default
try:
value = int(raw)
except ValueError:
raise ValueError("Invalid limit: must be a positive integer") from None
if value < 1:
raise ValueError("Invalid limit: must be a positive integer")
return value
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

def _parse_limit(raw: str | None, default: int = _DEFAULT_LIMIT) -> int:
if raw is None or raw.strip() == "":
return default
try:
value = int(raw)
except ValueError:
raise ValueError("Invalid limit: must be a positive integer") from None
if value < 1:
raise ValueError("Invalid limit: must be a positive integer")
return value

Comment thread tests/test_search.py
Comment on lines +46 to +72
resp = client.get("/api/search?q=searchable&limit=10")
assert resp.status_code == 200
data = resp.get_json()
assert isinstance(data, list)
assert len(data) >= 1

def test_limit_float_string_returns_400(self, client):
resp = client.get("/api/search?q=test&limit=1.5")
assert resp.status_code == 400
body = resp.get_json()
assert "error" in body
assert "limit" in body["error"].lower()

def test_limit_non_numeric_returns_400(self, client):
resp = client.get("/api/search?q=test&limit=abc")
assert resp.status_code == 400
body = resp.get_json()
assert "error" in body
assert "limit" in body["error"].lower()

def test_limit_default_when_omitted(self, client, tmp_path):
_write_searchable_session(tmp_path, "proj-a", "sess-1", "findme keyword here")
resp = client.get("/api/search?q=findme")
assert resp.status_code == 200
assert isinstance(resp.get_json(), list)

def test_empty_query_returns_empty_list(self, client):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Cover integer / float / non-numeric / default / empty query. Missing two edge cases worth adding: limit=0 → 400 (per value < 1 at api/search.py:26) and limit=-5 → 400. Both hit the same branch as limit=abc but they're the corner cases someone empirically passes through a UI bug.

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.

Fix search limit validation, add typed data models, and enable mypy strict in CI

2 participants