Skip to content

test: HTTP/CLI route matrix and structured API error codes#42

Open
clean6378-max-it wants to merge 8 commits into
masterfrom
test/http-cli-errors-and-codes
Open

test: HTTP/CLI route matrix and structured API error codes#42
clean6378-max-it wants to merge 8 commits into
masterfrom
test/http-cli-errors-and-codes

Conversation

@clean6378-max-it
Copy link
Copy Markdown
Collaborator

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

Closes #41

Summary

Week 3 Wednesday work for claude-code-chat-browser (8 story points):

  • HTTP + CLI test depth (5 pt) — Full-app route coverage (test_api_routes.py), shared fixtures (tests/conftest.py), CLI subprocess e2e (test_cli_e2e.py), and search limit unit tests (test_search.py).
  • Structured error codes (3 pt)ErrorCode enum + error_response() helper; every API JSON error includes "code" and "error"; README error catalog.
    Canonical issues: fab633e4-4f9d-56b4-97d4-cfc84b5bbdd7 (HTTP/CLI tests) · 8c01c675-d0e7-5e5c-a179-472a2fea5056 (structured error codes)
    Detail guide: Doc/Issues/chen-week3-wednesday-claude-code-chat-browser-guide.md

Changes

API

Area Change
api/error_codes.py New — ErrorCode (StrEnum) and error_response()
api/search.py _parse_limit(); 400 SEARCH_INVALID_LIMIT; max limit 500
api/sessions.py All errors use structured codes (INVALID_PATH, SESSION_NOT_FOUND, PARSE_ERROR, INTERNAL_ERROR)
api/export_api.py Structured codes for invalid body/since, 422 empty export, session export errors

Tests

File Purpose
tests/conftest.py Shared client fixtures, export_state_file, assert_error_response()
tests/fixtures/*.jsonl Minimal session seeds for API/CLI tests
tests/test_api_routes.py Stats, export session/state/bulk, index smoke, path safety, parse-failure leak guard
tests/test_cli_e2e.py list, stats, export, invalid --since (UTF-8 env for Windows)
tests/test_error_codes.py Parametrized "code" field assertions
tests/test_search.py Limit validation + SEARCH_INVALID_LIMIT
tests/test_export_api_bulk.py Assert "code" on isolated blueprint tests

Docs

  • README.md — API error code table; bulk export 422 example includes "code"

Example error response

{
  "error": "Invalid limit: must be a positive integer",
  "code": "SEARCH_INVALID_LIMIT"
}

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * API error responses now include stable, machine-readable error codes alongside human messages.

* **Documentation**
  * Bulk Export docs expanded; new API error codes reference and standardized JSON error shape documented.

* **Bug Fixes**
  * Consistent validation and structured errors across search, export, and session endpoints (including 422 when bulk export yields nothing and echoing the requested mode).

* **Tests**
  * Added broad unit, integration, and end-to-end tests covering API, CLI, and frontend utilities.

* **Chores**
  * CI and JS test tooling configuration updates.

<!-- review_stack_entry_start -->

[![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/cppalliance/claude-code-chat-browser/pull/42?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

clean6378-max-it and others added 5 commits May 20, 2026 15:23
Extract Flask client fixtures to tests/conftest.py, use client_single
for search limit tests, and relax session count assertion to >= 1.
Include utils/ in coverage (omit untested export modules), stop
tracking .coverage, and align integration-tests CI with full cov scope.

Co-Authored-By: Cursor <cursoragent@cursor.com>
Add client_empty and client_thinking fixtures, exercise thinking
blocks via session detail API, preserve ValueError cause in _parse_limit,
and remove .coverage from git tracking (already in .gitignore).

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 140a50bf-e037-485b-b5c9-4ae40c8a726f

📥 Commits

Reviewing files that changed from the base of the PR and between 5b3f6b6 and ba2117d.

📒 Files selected for processing (5)
  • api/export_api.py
  • api/sessions.py
  • static/js/shared/state.test.js
  • tests/test_cli_e2e.py
  • tests/test_error_propagation.py

📝 Walkthrough

Walkthrough

Centralizes API error handling with an ErrorCode enum and error_response() helper, migrates search/sessions/export endpoints to structured error codes, updates README, and adds fixtures plus extensive HTTP/CLI/JS tests and CI/tooling updates.

Changes

Structured Error Codes and Comprehensive API/CLI Test Coverage

Layer / File(s) Summary
Error Code Foundation and Documentation
api/error_codes.py, README.md
ErrorCode enum and error_response(code, message, status, **extra) helper; README documents standard error JSON (error + code) and lists common codes.
Search API Limit Validation
api/search.py, tests/test_search.py
Adds _parse_limit() and _DEFAULT_LIMIT/_MAX_LIMIT; /api/search validates limit and returns SEARCH_INVALID_LIMIT (HTTP 400) via error_response() on invalid input. Tests cover integer, float, non-numeric, zero/negative, whitespace, omitted-limit, and empty-query behaviors.
Sessions API Error Handling
api/sessions.py
get_session and get_session_stats use error_response() for INVALID_PATH, SESSION_NOT_FOUND, PARSE_ERROR, and INTERNAL_ERROR, replacing ad-hoc JSON/status responses.
Export API Error Handling
api/export_api.py, tests/test_export_api_bulk.py
bulk_export returns INVALID_REQUEST_BODY (400) for non-object bodies, INVALID_SINCE_MODE (400) for invalid since, and EXPORT_NOTHING_TO_EXPORT (422) with echoed since when empty. export_session returns SESSION_NOT_FOUND and INVALID_PATH via error_response(). Tests extended to assert code fields.
Shared Test Fixtures and Helpers
tests/conftest.py, tests/fixtures/session_*.jsonl
Adds assert_error_response(resp, expected_code=None), export_state_file fixture, and Flask test-client fixtures (client, client_single, client_empty, client_thinking) seeded with JSONL fixtures.
Full-App HTTP Route Tests
tests/test_api_routes.py
Comprehensive route tests covering index HTML, session stats (happy/404/invalid path), search limit capping and validation, project sessions invalid-path behavior, export state, bulk export error cases, and export-session formats and errors.
Parametrized Error Code Tests
tests/test_error_codes.py
Parametrized matrix asserting endpoint failures return expected HTTP status and ErrorCode values; includes a specific 422 empty-export check for EXPORT_NOTHING_TO_EXPORT.
CLI End-to-End Tests
tests/test_cli_e2e.py
End-to-end subprocess tests for scripts/export.py: list (exit 0), list unknown project (exit 0 with message), stats (exit 0), invalid --since (non-zero exit), export --no-zip writes Markdown files.
JS Unit Tests and Vitest Config
static/js/shared/*.test.js, vitest.config.js, package.json
Adds Vitest unit tests for markdown, state, utils; vitest.config.js and package.json configure JS testing environment and coverage.
CI and Tooling
.github/workflows/ci.yml, .gitignore, pyproject.toml, requirements-dev.txt
Pins GH Actions to SHAs, adds integration-tests CI job that runs selected pytest with coverage artifact upload, ignores coverage artifacts in .gitignore, and adds pytest/coverage settings via pyproject.toml and requirements-dev.txt.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • wpak-ai
  • jonathanMLDev
  • timon0305

🐰 I tidy codes with a nibble and cheer,
Errors get names so clients can hear.
Tests hop along routes, CLI in tow,
Docs list the codes, CI lets them show.
A happy rabbit dances: structured and clear.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% 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 PR title 'test: HTTP/CLI route matrix and structured API error codes' clearly summarizes the primary changes: addition of comprehensive tests and implementation of structured error codes.
Linked Issues check ✅ Passed All major objectives from issue #41 are met: HTTP route matrix tests added, CLI e2e tests added, structured ErrorCode enum implemented, all api files migrated to use error_response() with code fields, error catalogs documented, and 500 responses protected from leakage.
Out of Scope Changes check ✅ Passed All changes are directly aligned with PR objectives. Configuration additions (pyproject.toml, vitest.config.js, package.json) and CI workflow updates enable the test infrastructure. JavaScript tests for shared utilities are foundational components supporting the stated goals.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/http-cli-errors-and-codes

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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

- Add ErrorCode enum and error_response() helper; migrate api/search,
  api/sessions, and api/export_api error paths to include stable "code"
- Validate search limit query param (400 SEARCH_INVALID_LIMIT; cap at 500)
- Add tests/conftest.py, fixtures, test_api_routes, test_cli_e2e,
  test_error_codes, test_search; extend test_export_api_bulk for codes
- Document error code catalog in README
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

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)

287-317: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard export_session() parse/stats failures with structured error responses.

This path still has unhandled exceptions, so 500s here won’t consistently return { "error", "code" }.

Proposed fix
-    fmt = request.args.get("format", "md")
-    session = parse_session(filepath)
+    fmt = request.args.get("format", "md")
+    try:
+        session = parse_session(filepath)
+    except Exception:
+        current_app.logger.exception("Failed to parse session %s", session_id)
+        return error_response(
+            ErrorCode.PARSE_ERROR,
+            "Failed to parse session",
+            500,
+        )
+
     rules = current_app.config.get("EXCLUSION_RULES") or []
     if is_session_excluded(rules, session, project_name):
         return error_response(
             ErrorCode.SESSION_NOT_FOUND,
             "Session not found",
             404,
         )
-    stats = compute_stats(session)
+    try:
+        stats = compute_stats(session)
+    except Exception:
+        current_app.logger.exception("Failed to compute stats for %s", session_id)
+        return error_response(
+            ErrorCode.INTERNAL_ERROR,
+            "Failed to compute session stats",
+            500,
+        )
     title_slug = slugify(session["title"], default="session")
🤖 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 287 - 317, Wrap calls to
parse_session(session filepath) and compute_stats(session) in try/except blocks
inside export_session so any exceptions are caught and returned via the existing
error_response(...) shape rather than bubbling as 500 HTMLs; specifically, catch
exceptions from parse_session (and return a structured error_response with an
appropriate ErrorCode like SESSION_NOT_FOUND or a new PARSE_ERROR and a 400/404)
and catch exceptions from compute_stats (return ErrorCode.INTERNAL_ERROR or
STATS_COMPUTE_FAILED with a 500), ensuring the rest of the flow
(session_to_json/session_to_markdown and send_file) only runs when parsing and
stats succeed.
🧹 Nitpick comments (4)
tests/test_api_routes.py (1)

68-72: ⚡ Quick win

Harden capped-limit assertion with response-shape check.

This test can pass on non-list payloads (e.g., an unexpected JSON object) because it only checks len(...) <= 500. Assert list shape explicitly before length checks.

Proposed test hardening
 def test_search_limit_capped_at_max(client):
     resp = client.get("/api/search?q=Hello&limit=9999")
     assert resp.status_code == 200
-    assert len(resp.get_json()) <= 500
+    body = resp.get_json()
+    assert isinstance(body, list)
+    assert len(body) <= 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 `@tests/test_api_routes.py` around lines 68 - 72, The
test_search_limit_capped_at_max test currently only checks len(resp.get_json())
<= 500 which will pass for non-list JSON shapes; update the test to first assert
the response body is a JSON list (e.g., use resp.is_json and assert
isinstance(resp.get_json(), list) or equivalent) before asserting its length, so
the shape is validated prior to the capped-limit check in
test_search_limit_capped_at_max.
tests/test_search.py (1)

8-12: ⚡ Quick win

Tighten happy-path assertions to validate limit behavior.

These tests currently confirm status, but not enough of the response contract. Add explicit list-shape checks and verify requested/default limit constraints in assertions.

Proposed assertion upgrades
 def test_limit_integer_string(client_single):
     resp = client_single.get("/api/search?q=Hello&limit=10")
     assert resp.status_code == 200
-    assert isinstance(resp.get_json(), list)
+    body = resp.get_json()
+    assert isinstance(body, list)
+    assert len(body) <= 10
@@
 def test_limit_default(client_single):
     resp = client_single.get("/api/search?q=Hello")
     assert resp.status_code == 200
+    body = resp.get_json()
+    assert isinstance(body, list)
+    assert len(body) <= 200

Also applies to: 26-29

🤖 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 `@tests/test_search.py` around lines 8 - 12, Update the happy-path assertions
in test_limit_integer_string (and the similar test at lines 26-29) to assert the
response is a list, each element has the expected shape (e.g., dict with
expected keys) and that the number of returned items respects the requested or
default limit: assert isinstance(resp.get_json(), list), assert
all(isinstance(item, dict) and set(expected_keys).issubset(item.keys()) for item
in resp.get_json()), and assert len(resp.get_json()) <= requested_limit (for
"/api/search?q=Hello&limit=10" assert <= 10) and also add a case that verifies
the default limit when limit is omitted by calling the endpoint without limit
and asserting len(...) <= default_limit; use the test function names
test_limit_integer_string and the other test to locate and update the
assertions.
tests/test_cli_e2e.py (2)

50-55: ⚡ Quick win

Tighten success assertion for listed project.

On Line 54, or "Project" in proc.stdout can pass even when test-project is missing (header-only output). Assert the seeded project name directly to avoid false positives.

Proposed diff
 def test_cli_list_exits_zero(tmp_path):
     base = _seed_base_dir(tmp_path)
     proc = _run_cli(["list", "--base-dir", str(base)])
     assert proc.returncode == 0
-    assert "test-project" in proc.stdout or "Project" in proc.stdout
+    assert "test-project" in proc.stdout
🤖 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 `@tests/test_cli_e2e.py` around lines 50 - 55, The test
test_cli_list_exits_zero uses a loose assertion that allows header-only output
to pass; tighten it by removing the fallback check ("or 'Project' in
proc.stdout") and assert directly that the seeded project name "test-project"
appears in proc.stdout (use the existing proc from _run_cli in
test_cli_list_exits_zero to perform the check).

70-73: ⚡ Quick win

Assert the invalid --since failure reason, not only exit code.

Line 72 currently accepts any non-zero failure. Add a stderr assertion so the test proves the --since validation path specifically failed.

Proposed diff
 def test_cli_invalid_since_exits_nonzero():
     proc = _run_cli(["--since", "yesterday"])
     assert proc.returncode != 0
+    assert "--since" in proc.stderr or "invalid choice" in proc.stderr.lower()
🤖 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 `@tests/test_cli_e2e.py` around lines 70 - 73, The test
test_cli_invalid_since_exits_nonzero only checks a non-zero exit code; update it
to also assert that _run_cli([...])'s stderr contains the specific validation
error for the --since flag (e.g., assert "Invalid value for '--since'" in
proc.stderr or a substring like "--since" and "invalid" if exact text differs)
so the test verifies the --since validation path failed; keep using the existing
helper _run_cli to capture stdout/stderr and add the stderr assertion alongside
the returncode check.
🤖 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 `@api/sessions.py`:
- Around line 65-75: The current single try around parse_session(filepath) and
compute_stats(session) masks parse failures as INTERNAL_ERROR; split the logic
so parse errors are caught and mapped to ErrorCode.PARSE_ERROR. Specifically,
call parse_session(filepath) in its own try/except that catches parsing
exceptions and returns error_response(ErrorCode.PARSE_ERROR, "Failed to parse
session", 400) (include session_id in logs via current_app.logger.exception),
then call compute_stats(session) in a separate try/except that logs and returns
the existing INTERNAL_ERROR via error_response(ErrorCode.INTERNAL_ERROR, "Failed
to compute session stats", 500) if compute_stats fails. Ensure you reference
parse_session, compute_stats, session_id, current_app.logger.exception,
error_response and ErrorCode.PARSE_ERROR in the change.

In `@tests/conftest.py`:
- Around line 22-24: The shared error helper in tests/conftest.py currently only
asserts "code" is present in body; add a type assertion to ensure body["code"]
is a string (e.g., use isinstance(body["code"], str)) before comparing with
expected_code in the helper that contains the assertions around "code" and
expected_code so tests fail if handlers return non-string codes.

---

Outside diff comments:
In `@api/export_api.py`:
- Around line 287-317: Wrap calls to parse_session(session filepath) and
compute_stats(session) in try/except blocks inside export_session so any
exceptions are caught and returned via the existing error_response(...) shape
rather than bubbling as 500 HTMLs; specifically, catch exceptions from
parse_session (and return a structured error_response with an appropriate
ErrorCode like SESSION_NOT_FOUND or a new PARSE_ERROR and a 400/404) and catch
exceptions from compute_stats (return ErrorCode.INTERNAL_ERROR or
STATS_COMPUTE_FAILED with a 500), ensuring the rest of the flow
(session_to_json/session_to_markdown and send_file) only runs when parsing and
stats succeed.

---

Nitpick comments:
In `@tests/test_api_routes.py`:
- Around line 68-72: The test_search_limit_capped_at_max test currently only
checks len(resp.get_json()) <= 500 which will pass for non-list JSON shapes;
update the test to first assert the response body is a JSON list (e.g., use
resp.is_json and assert isinstance(resp.get_json(), list) or equivalent) before
asserting its length, so the shape is validated prior to the capped-limit check
in test_search_limit_capped_at_max.

In `@tests/test_cli_e2e.py`:
- Around line 50-55: The test test_cli_list_exits_zero uses a loose assertion
that allows header-only output to pass; tighten it by removing the fallback
check ("or 'Project' in proc.stdout") and assert directly that the seeded
project name "test-project" appears in proc.stdout (use the existing proc from
_run_cli in test_cli_list_exits_zero to perform the check).
- Around line 70-73: The test test_cli_invalid_since_exits_nonzero only checks a
non-zero exit code; update it to also assert that _run_cli([...])'s stderr
contains the specific validation error for the --since flag (e.g., assert
"Invalid value for '--since'" in proc.stderr or a substring like "--since" and
"invalid" if exact text differs) so the test verifies the --since validation
path failed; keep using the existing helper _run_cli to capture stdout/stderr
and add the stderr assertion alongside the returncode check.

In `@tests/test_search.py`:
- Around line 8-12: Update the happy-path assertions in
test_limit_integer_string (and the similar test at lines 26-29) to assert the
response is a list, each element has the expected shape (e.g., dict with
expected keys) and that the number of returned items respects the requested or
default limit: assert isinstance(resp.get_json(), list), assert
all(isinstance(item, dict) and set(expected_keys).issubset(item.keys()) for item
in resp.get_json()), and assert len(resp.get_json()) <= requested_limit (for
"/api/search?q=Hello&limit=10" assert <= 10) and also add a case that verifies
the default limit when limit is omitted by calling the endpoint without limit
and asserting len(...) <= default_limit; use the test function names
test_limit_integer_string and the other test to locate and update the
assertions.
🪄 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: 93da7cea-ce58-4a41-9562-246a7f450f6a

📥 Commits

Reviewing files that changed from the base of the PR and between 4bbb456 and 20ac332.

📒 Files selected for processing (13)
  • README.md
  • api/error_codes.py
  • api/export_api.py
  • api/search.py
  • api/sessions.py
  • tests/conftest.py
  • tests/fixtures/session_minimal.jsonl
  • tests/fixtures/session_with_tools.jsonl
  • tests/test_api_routes.py
  • tests/test_cli_e2e.py
  • tests/test_error_codes.py
  • tests/test_export_api_bulk.py
  • tests/test_search.py

Comment thread api/sessions.py
Comment thread tests/conftest.py
@clean6378-max-it clean6378-max-it force-pushed the test/http-cli-errors-and-codes branch from 20ac332 to b2f1331 Compare May 21, 2026 01:00
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

♻️ Duplicate comments (1)
api/sessions.py (1)

65-75: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Split parse and compute error handling in stats endpoint.

At Line 66 and Line 67, parse and stats computation share one except, so parse failures are still returned as INTERNAL_ERROR instead of PARSE_ERROR.

Suggested fix
-    try:
-        session = parse_session(filepath)
-        stats = compute_stats(session)
-        return jsonify(stats)
-    except Exception:
-        current_app.logger.exception("Failed to compute stats for %s", session_id)
-        return error_response(
-            ErrorCode.INTERNAL_ERROR,
-            "Failed to compute session stats",
-            500,
-        )
+    try:
+        session = parse_session(filepath)
+    except Exception:
+        current_app.logger.exception("Failed to parse session %s", session_id)
+        return error_response(
+            ErrorCode.PARSE_ERROR,
+            "Failed to parse session",
+            500,
+        )
+
+    try:
+        stats = compute_stats(session)
+        return jsonify(stats)
+    except Exception:
+        current_app.logger.exception("Failed to compute stats for %s", session_id)
+        return error_response(
+            ErrorCode.INTERNAL_ERROR,
+            "Failed to compute session stats",
+            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/sessions.py` around lines 65 - 75, The endpoint currently wraps
parse_session(filepath) and compute_stats(session) in a single try/except so
parse failures are treated as INTERNAL_ERROR; split them into two blocks: call
parse_session(filepath) inside its own try/except and on parse failure log via
current_app.logger.exception("Failed to parse session %s", session_id) and
return error_response(ErrorCode.PARSE_ERROR, "Failed to parse session", 400)
(catch the specific parse exception class if available, e.g., ParseError or
ValueError), then call compute_stats(session) in a separate try/except that logs
via current_app.logger.exception("Failed to compute stats for %s", session_id)
and returns the existing INTERNAL_ERROR response on other failures.
🤖 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 `@api/export_api.py`:
- Around line 286-295: Wrap the calls to parse_session(filepath) and
compute_stats(session) in try/except blocks so parsing or stats errors return
structured error_response objects (use the existing error_response and ErrorCode
enums) instead of raising uncaught exceptions; specifically, catch exceptions
from parse_session in the block where parse_session is invoked (before
is_session_excluded) and return a clear error_response (e.g.,
ErrorCode.SESSION_NOT_FOUND or a validation error) with a short message
including the exception text and an appropriate HTTP status, and catch
exceptions from compute_stats(session) where compute_stats is called and return
error_response(ErrorCode.INTERNAL_ERROR, "Failed to compute stats: <error>",
500) so all failures yield stable responses containing the expected code field.

In `@static/js/shared/state.test.js`:
- Around line 5-10: The test suite currently only resets the shared singleton in
an afterEach block, which leaves the first test vulnerable to prior mutations;
add a beforeEach that performs the same resets as the existing afterEach (reset
state.currentProject, state.cachedSessions, state.projectDisplayNames,
state.navInProgress) so each test starts with a deterministic state; locate the
existing afterEach in the file (and the shared state object reference named
state) and duplicate its reset logic inside a beforeEach at the top of the
suite.

In `@tests/test_cli_e2e.py`:
- Around line 70-73: The test test_cli_invalid_since_exits_nonzero is
under-specified because it calls _run_cli(["--since", "yesterday"]) without a
subcommand so a non-zero exit could be from argument shape errors rather than
--since validation; update the test to invoke the CLI with a real subcommand
(use whatever existing subcommand your CLI expects when exercising --since) via
_run_cli, assert proc.returncode != 0, and also assert proc.stderr contains the
specific validation error text related to --since (or a known error fragment) to
ensure the failure is the intended validation path.

---

Duplicate comments:
In `@api/sessions.py`:
- Around line 65-75: The endpoint currently wraps parse_session(filepath) and
compute_stats(session) in a single try/except so parse failures are treated as
INTERNAL_ERROR; split them into two blocks: call parse_session(filepath) inside
its own try/except and on parse failure log via
current_app.logger.exception("Failed to parse session %s", session_id) and
return error_response(ErrorCode.PARSE_ERROR, "Failed to parse session", 400)
(catch the specific parse exception class if available, e.g., ParseError or
ValueError), then call compute_stats(session) in a separate try/except that logs
via current_app.logger.exception("Failed to compute stats for %s", session_id)
and returns the existing INTERNAL_ERROR response on other failures.
🪄 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: b3934581-cfaf-4dc7-841d-4cc84fc0bd73

📥 Commits

Reviewing files that changed from the base of the PR and between 20ac332 and b2f1331.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (24)
  • .github/workflows/ci.yml
  • .gitignore
  • README.md
  • api/error_codes.py
  • api/export_api.py
  • api/search.py
  • api/sessions.py
  • package.json
  • pyproject.toml
  • requirements-dev.txt
  • static/js/shared/markdown.test.js
  • static/js/shared/state.test.js
  • static/js/shared/utils.test.js
  • tests/conftest.py
  • tests/fixtures/session_minimal.jsonl
  • tests/fixtures/session_with_thinking.jsonl
  • tests/fixtures/session_with_tools.jsonl
  • tests/test_api_integration.py
  • tests/test_api_routes.py
  • tests/test_cli_e2e.py
  • tests/test_error_codes.py
  • tests/test_export_api_bulk.py
  • tests/test_search.py
  • vitest.config.js
✅ Files skipped from review due to trivial changes (4)
  • package.json
  • pyproject.toml
  • .gitignore
  • README.md

Comment thread api/export_api.py Outdated
Comment thread static/js/shared/state.test.js
Comment thread tests/test_cli_e2e.py 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

🤖 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 `@api/sessions.py`:
- Around line 67-73: The except block that catches parser exceptions (in the
session handling function around current_app.logger.exception("Failed to parse
session %s", session_id)) returns ErrorCode.PARSE_ERROR with HTTP 400; change
the response status to 500 to match get_session's PARSE_ERROR handling and
maintain a consistent server-error contract. Update the error_response call in
that except handler to use a 500 status, and apply the same change to the
mirrored export-session parse handling so both endpoints return PARSE_ERROR as a
500 server error.
🪄 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: 8da4a479-8f9a-42d1-9a60-9ef18f6f6cce

📥 Commits

Reviewing files that changed from the base of the PR and between b2f1331 and 5b3f6b6.

📒 Files selected for processing (7)
  • api/export_api.py
  • api/sessions.py
  • tests/conftest.py
  • tests/test_api_routes.py
  • tests/test_cli_e2e.py
  • tests/test_error_propagation.py
  • tests/test_search.py

Comment thread api/sessions.py
Comment thread api/error_codes.py
status: int,
**extra: object,
) -> tuple[Response, int]:
body: dict[str, object] = {"error": message, "code": str(code)}
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.

ErrorCode is StrEnum, so code is already a str subclass. str(code) works but code.value or just code is more idiomatic — both yield "SEARCH_INVALID_LIMIT" directly. Same nit applies to expected_code=str(code) in tests/test_error_codes.py (in the parametrized assertion at the bottom of the file). Optional.

Comment thread tests/conftest.py


@pytest.fixture
def client(tmp_path, export_state_file):
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.

Four near-identical client fixtures (client, client_single, client_empty, client_thinking) repeat the same mkdir / copy / create_app / TESTING boilerplate. Same DRY concern I flagged on PR #40 — still not addressed. Worth collapsing to a parametric helper that takes a list of (fixture_file, output_name) tuples and returns a client. Recommend: a _make_client(tmp_path, export_state_file, seeds: list[tuple[str, str]]) private helper + 4 thin fixture wrappers around it.

globalThis.marked = {
parse: vi.fn((text) => `<p>${text}</p>`),
};
globalThis.DOMPurify = {
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.

The mock replaces DOMPurify.sanitize with a regex that strips <script>, then the test asserts the output doesn't contain <script>. Same circular-mock issue I flagged on PR #40 — still not addressed.

Comment thread api/sessions.py
404,
)
return jsonify(session)
except Exception:
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.

Three except Exception: blocks catch too broadly. Same narrowing recommendation as before — (json.JSONDecodeError, KeyError, ValueError, OSError, FileNotFoundError).

Same as line 67, 78.

Comment thread api/export_api.py
)
new_sessions_map[sid] = sess_info.get("modified", 0)
count += 1
except Exception as e:
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.

Four broad except Exception: sites — the two inside the export loops (162, 224) continue silently and the two below (289, 309) wrap parse/stats failures.

Same as 224, 289, 309

Comment thread api/export_api.py
Comment on lines 235 to +241
if count > 0:
_write_state(new_sessions_map, count)

if count == 0:
return (
jsonify(
{
"error": "Nothing to export",
"since": since,
}
),
return error_response(
ErrorCode.EXPORT_NOTHING_TO_EXPORT,
"Nothing to export",
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.

Reordering to if count == 0: return ... first (then write state for the count > 0 path) reads more linearly. Cosmetic.

Comment thread tests/test_cli_e2e.py
Comment on lines +56 to +60
def test_cli_list_unknown_project_exits_zero_with_message(tmp_path):
base = _seed_base_dir(tmp_path)
proc = _run_cli(["list", "--base-dir", str(base), "--project", "does-not-exist"])
assert proc.returncode == 0
assert "No projects found" in proc.stdout
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.

Test name says "unknown project" but assertion checks for "No projects found" (about no projects at all). If --project does-not-exist is supposed to show a project-specific "no such project: does-not-exist" message, the test would currently pass even on the wrong message

Comment thread pyproject.toml
Comment on lines +7 to +12
omit = [
"tests/*",
"utils/md_exporter.py",
"utils/session_stats.py",
"utils/json_exporter.py",
]
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.

The three utils/ modules omitted from coverage are domain logic, not glue — session_stats.py produces the cost_estimate_usd shown in stats endpoints, md_exporter.py is used by every bulk-export test in this PR, and json_exporter.py likewise.

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.

HTTP/CLI tests and structured error codes

2 participants