Skip to content

Add dedicated Flask test coverage for search, workspaces, and export APIs (#101)#104

Open
bradjin8 wants to merge 3 commits into
masterfrom
feat/three-api-tests
Open

Add dedicated Flask test coverage for search, workspaces, and export APIs (#101)#104
bradjin8 wants to merge 3 commits into
masterfrom
feat/three-api-tests

Conversation

@bradjin8

@bradjin8 bradjin8 commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Split the monolithic tests/test_api_endpoints.py into three dedicated route test modules: test_api_search.py, test_api_workspaces.py, and test_api_export.py
  • Add missing coverage for POST /api/export and GET /api/export/state, including zip response headers, export state persistence, and since=last behavior
  • Each endpoint suite exercises happy path (200), error paths (400/404/500), and edge cases (empty data, missing params, exclusion rules) using the existing temp SQLite fixtures — no real Cursor data required
  • Add shared client_with_rules() helper in conftest.py and update CI to run the new test files

Closes #101.

Test plan

  • python -m pytest tests/test_api_search.py tests/test_api_workspaces.py tests/test_api_export.py -v (33 passed)
  • CI unittest + pytest matrix passes on push

Summary by CodeRabbit

  • Tests
    • Reorganized API test suite into dedicated modules for export, search, and workspace endpoints; added broad coverage and new helpers.
  • Bug Fixes
    • Export state handling: API now treats malformed saved state as empty, improving stability.
  • Chores
    • Updated CI workflow to run the reorganized test subsets and clarified typehint guidance in typecheck job.

@bradjin8 bradjin8 self-assigned this Jun 11, 2026
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9c5b9b1f-6b3e-493b-b59a-b6a0116c68a7

📥 Commits

Reviewing files that changed from the base of the PR and between a30c6b4 and 94bb396.

📒 Files selected for processing (1)
  • tests/test_api_export.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_api_export.py

📝 Walkthrough

Walkthrough

Splits monolithic endpoint tests into three focused modules, adds a reusable exclusion-rule test client helper, hardens export_state JSON parsing to require a dict, and updates CI to run the new test modules with updated mypy guidance.

Changes

Endpoint Test Coverage Expansion (#101)

Layer / File(s) Summary
Export state type validation and safeguard
api/export_api.py
_get_export_state now validates that loaded JSON is a dict, logs a warning, and returns {} on type mismatch; drops unused cast import.
Reusable test client fixture with exclusion rules
tests/conftest.py
Adds client_with_rules(rule_lines) helper that tokenizes rule input and configures app.config['EXCLUSION_RULES'] for a Flask test client.
Search endpoint test suite
tests/test_api_search.py
Adds result-shape validator, happy-path tests, error-response tests for missing/empty/whitespace q (400) and simulated internal failure (500), and edge-case tests for no matches, exclusion filtering, and workspace scoping.
Workspace endpoint test suite
tests/test_api_workspaces.py
Adds project-shape validator and tests for /api/workspaces (list/empty/500), /api/workspaces/{id} and /api/workspaces/global, /api/workspaces/{id}/tabs (including summary=1), single tab fetch behavior, and exclusion-rule filtering.
Export endpoint test suite and fixtures
tests/test_api_export.py
Adds export_state_dir fixture to redirect state to a temp dir, helpers for POST/ZIP parsing, tests for /api/export/state and /api/export happy-path, error cases (missing storage, no conversations, internal failure), and since edge cases.
CI workflow update and mypy guidance
.github/workflows/tests.yml
Replaces the monolithic tests/test_api_endpoints.py target with the split modules (test_api_search.py, test_api_workspaces.py, test_api_export.py, test_pdf_export.py) and expands inline mypy guidance about Flask 3.1+ typing (avoid types-Flask).

Sequence Diagram

sequenceDiagram
  participant Test as Test Client
  participant Flask as Flask Route Handler
  participant DB as SQLite / Fixtures
  participant Export as api.export_api

  Test->>Flask: GET /api/search?q=term
  Flask->>DB: query conversations/search index
  DB-->>Flask: SearchResult rows
  Flask-->>Test: 200 + SearchResult list

  Test->>Flask: GET /api/workspaces
  Flask->>DB: list workspace projects
  DB-->>Flask: projects array
  Flask-->>Test: 200 + projects

  Test->>Flask: POST /api/export
  Flask->>DB: fetch conversations
  DB-->>Flask: conversation records
  Flask->>Export: build ZIP and update export_state.json
  Export-->>Flask: ZIP bytes / state persisted
  Flask-->>Test: 200 + ZIP (application/zip)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • cppalliance/cppa-cursor-browser#32: Refactors the original monolithic endpoint tests into split modules and introduces shared conftest helpers; this PR replaces the earlier tests/test_api_endpoints.py with the new modular suites.
  • cppalliance/cppa-cursor-browser#82: Updates CI to include tests/test_pdf_export.py, related to endpoint export/PDF test coverage and CI wiring.
  • cppalliance/cppa-cursor-browser#61: Related export-state handling changes in api/export_api.py; both PRs touch export persistence and since handling.

Suggested reviewers

  • timon0305
  • clean6378-max-it
  • wpak-ai

Poem

A rabbit wrote tests tidy and neat,
Splitting files so the endpoints meet. 🐇
Export state now checks for a dict,
Rules tokenized so filters click.
CI runs the suites with a cheerful beat.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.85% 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 accurately summarizes the main change: adding dedicated Flask test coverage for three API routes (search, workspaces, export), directly matching the PR's core objective of splitting test_api_endpoints.py into three focused test modules.
Linked Issues check ✅ Passed The PR fulfills all coding requirements from issue #101: three dedicated test files with happy/error/edge case coverage for /api/search [test_api_search.py], /api/workspaces [test_api_workspaces.py], and /api/export [test_api_export.py]; response JSON structure verification; and temporary SQLite fixtures (no real data required).
Out of Scope Changes check ✅ Passed All changes directly support issue #101: three test modules cover the targeted endpoints, conftest.py adds the shared client_with_rules helper, export_api.py improves validation (scoped to export), and CI updates run the new tests—no unrelated scope creep.

✏️ 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 feat/three-api-tests

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

@bradjin8 bradjin8 force-pushed the feat/three-api-tests branch from ca22532 to 1f36f81 Compare June 11, 2026 19:58

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
.github/workflows/tests.yml (1)

152-152: 💤 Low value

Consider clarifying why the Flask types note appears here.

The comment about Flask 3.1+ inline types is accurate and useful, but it appears before installation commands that don't attempt to install types-Flask. Consider expanding it to explain the connection:

-        # Flask 3.1+ ships inline types — do not install types-Flask (conflicts).
+        # Flask 3.1+ ships inline types and conflicts with types-Flask; we rely on
+        # the inline types from Flask in requirements-lock.txt (no stub package needed).

This makes it clearer that the comment is preventative documentation for future maintainers.

🤖 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 @.github/workflows/tests.yml at line 152, Update the inline comment about
Flask 3.1+ to explicitly state that Flask now ships type hints so maintainers
should not add or install the separate types-Flask package (it will conflict),
and note this is preventative documentation for the following pip install steps
that install various typing packages; change the comment to something like
“Flask 3.1+ includes inline type hints — do not add or install types-Flask (it
will conflict with our pip installs), keeping this as preventative guidance for
future maintainers.”
services/workspace_tabs.py (1)

910-916: ⚡ Quick win

Reuse the shared composer-row SQL constant in this path.

Line [910]-[916] rebuilds the composer filter query inline, while other paths already use COMPOSER_ROWS_WITH_HEADERS_SQL. Reusing the constant here reduces drift risk and keeps filtering semantics consistent across tabs endpoints.

Proposed diff
-        composer_rows = safe_fetchall(
-            global_db,
-            "SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'"
-            " AND value IS NOT NULL"
-            " AND value LIKE '%fullConversationHeadersOnly%'"
-            " AND value NOT LIKE '%fullConversationHeadersOnly\":[]%'",
-        )
+        composer_rows = safe_fetchall(global_db, COMPOSER_ROWS_WITH_HEADERS_SQL)
🤖 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 `@services/workspace_tabs.py` around lines 910 - 916, Replace the inline SQL
used to populate composer_rows with the shared constant
COMPOSER_ROWS_WITH_HEADERS_SQL to avoid duplication and drift: locate the call
to safe_fetchall that currently passes the hard-coded "SELECT key, value FROM
cursorDiskKV ..." string and change it to use COMPOSER_ROWS_WITH_HEADERS_SQL
(keeping the same safe_fetchall(global_db, COMPOSER_ROWS_WITH_HEADERS_SQL)
signature and existing variable name composer_rows), ensuring any surrounding
logic that expects the same filter semantics remains unchanged.
🤖 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 @.gitignore:
- Line 12: The .gitignore contains an unexplained entry `.mypy-ci-test/` that
isn't referenced elsewhere; either remove that ignore or document/produce the
directory during CI. Fix by either deleting the `.mypy-ci-test/` line from
`.gitignore` or adding documentation and CI changes that create/populate
`.mypy-ci-test/` (or set mypy's cache_dir) so the entry is justified; check
`pyproject.toml`'s `[tool.mypy]` and existing `.mypy_cache/` usage to ensure
consistency with the chosen approach.

In `@api/export_api.py`:
- Around line 42-49: The _get_export_state function currently casts json.load(f)
to dict[str, Any] without runtime validation; change it to read the parsed value
into a local (e.g., parsed = json.load(f)), check isinstance(parsed, dict) and
return parsed if true, otherwise return an empty dict (and optionally log/debug
that the file contained non-object JSON) so export_chats()'s state.get(...)
won't raise.

---

Nitpick comments:
In @.github/workflows/tests.yml:
- Line 152: Update the inline comment about Flask 3.1+ to explicitly state that
Flask now ships type hints so maintainers should not add or install the separate
types-Flask package (it will conflict), and note this is preventative
documentation for the following pip install steps that install various typing
packages; change the comment to something like “Flask 3.1+ includes inline type
hints — do not add or install types-Flask (it will conflict with our pip
installs), keeping this as preventative guidance for future maintainers.”

In `@services/workspace_tabs.py`:
- Around line 910-916: Replace the inline SQL used to populate composer_rows
with the shared constant COMPOSER_ROWS_WITH_HEADERS_SQL to avoid duplication and
drift: locate the call to safe_fetchall that currently passes the hard-coded
"SELECT key, value FROM cursorDiskKV ..." string and change it to use
COMPOSER_ROWS_WITH_HEADERS_SQL (keeping the same safe_fetchall(global_db,
COMPOSER_ROWS_WITH_HEADERS_SQL) signature and existing variable name
composer_rows), ensuring any surrounding logic that expects the same filter
semantics remains unchanged.
🪄 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: c41a648b-85c0-4e13-92b0-ed8b5d386c57

📥 Commits

Reviewing files that changed from the base of the PR and between fe3c728 and ca22532.

📒 Files selected for processing (41)
  • .github/workflows/tests.yml
  • .gitignore
  • CHANGELOG.md
  • api/composers.py
  • api/config_api.py
  • api/export_api.py
  • api/flask_config.py
  • api/logs.py
  • api/pdf.py
  • api/search.py
  • api/workspaces.py
  • app.py
  • models/__init__.py
  • models/conversation.py
  • models/parse_warnings.py
  • models/search.py
  • pyproject.toml
  • services/cli_tabs.py
  • services/search.py
  • services/summary_cache.py
  • services/workspace_context.py
  • services/workspace_db.py
  • services/workspace_listing.py
  • services/workspace_resolver.py
  • services/workspace_tabs.py
  • tests/conftest.py
  • tests/test_api_endpoints.py
  • tests/test_api_export.py
  • tests/test_api_search.py
  • tests/test_api_workspaces.py
  • tests/test_models.py
  • tests/test_normalize_file_path.py
  • tests/test_search_helpers.py
  • utils/cli_chat_reader.py
  • utils/cursor_md_exporter.py
  • utils/debug_flag.py
  • utils/exclusion_rules.py
  • utils/path_helpers.py
  • utils/text_extract.py
  • utils/tool_parser.py
  • utils/workspace_descriptor.py
💤 Files with no reviewable changes (1)
  • tests/test_api_endpoints.py

Comment thread .gitignore Outdated
Comment thread api/export_api.py
@bradjin8 bradjin8 force-pushed the feat/three-api-tests branch from 51188ca to a30c6b4 Compare June 11, 2026 20:44

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
tests/test_api_export.py (1)

42-59: ⚡ Quick win

Optional: Add test for malformed export_state.json to verify warning behavior.

The validation added in api/export_api._get_export_state (lines 49-55) logs a warning and returns {} when the state file contains non-dict JSON (e.g., an array or string). Consider adding a test that writes such a file and verifies the endpoint still returns a valid response.

📋 Example test case
def test_get_state_handles_malformed_file(self, client, export_state_dir):
    """Non-dict JSON in state file should not crash the endpoint."""
    state_path = export_state_dir / "export_state.json"
    state_path.write_text('["not", "a", "dict"]', encoding="utf-8")
    response = client.get("/api/export/state")
    assert response.status_code == 200
    body = response.get_json()
    assert isinstance(body, dict)
    assert body == {}  # Falls back to empty dict
🤖 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_export.py` around lines 42 - 59, Add a test to exercise
api/export_api._get_export_state's handling of malformed JSON: in
TestExportState add a method (e.g., test_get_state_handles_malformed_file) that
writes non-dict JSON (like '["not","a","dict"]') to export_state.json in the
export_state_dir, calls client.get("/api/export/state"), asserts a 200 response,
asserts the returned body is a dict and equals {} to verify the function logs
the warning and falls back to an empty dict; reference TestExportState and the
endpoint "/api/export/state" to locate where to add the new test.
🤖 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.

Nitpick comments:
In `@tests/test_api_export.py`:
- Around line 42-59: Add a test to exercise api/export_api._get_export_state's
handling of malformed JSON: in TestExportState add a method (e.g.,
test_get_state_handles_malformed_file) that writes non-dict JSON (like
'["not","a","dict"]') to export_state.json in the export_state_dir, calls
client.get("/api/export/state"), asserts a 200 response, asserts the returned
body is a dict and equals {} to verify the function logs the warning and falls
back to an empty dict; reference TestExportState and the endpoint
"/api/export/state" to locate where to add the new test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 70c3e4b6-13dd-4a17-86cb-8278dd754906

📥 Commits

Reviewing files that changed from the base of the PR and between 51188ca and a30c6b4.

📒 Files selected for processing (7)
  • .github/workflows/tests.yml
  • api/export_api.py
  • tests/conftest.py
  • tests/test_api_endpoints.py
  • tests/test_api_export.py
  • tests/test_api_search.py
  • tests/test_api_workspaces.py
💤 Files with no reviewable changes (1)
  • tests/test_api_endpoints.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/test_api_workspaces.py
  • tests/conftest.py
  • tests/test_api_search.py

@bradjin8 bradjin8 requested a review from timon0305 June 11, 2026 21:27
assert body["name"] == "Other chats"


class TestGetWorkspaceTabs:

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 split was supposed to keep all the old tests. This one for "global workspace + summary mode" is gone. Please add it back to TestGetWorkspaceTabs. Everything else carried over fine, so this looks accidental.

Comment thread tests/conftest.py
return app.test_client()


def client_with_rules(rule_lines: list[str]) -> FlaskClient:

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.

Importing helpers from conftest.py is discouraged (pytest treats it specially). Works, but move it to a normal file like tests/_helpers.py — same pattern you already use with tests/_fixture_ids.py. Minor.

Comment thread tests/test_api_export.py
Comment on lines +134 to +143
def test_since_last_after_export_returns_404_when_nothing_new(
self, client, export_state_dir
):
first = _post_export(client, {"since": "all"})
assert first.status_code == 200

second = _post_export(client, {"since": "last"})
assert second.status_code == 404
body = second.get_json()
assert body.get("error") == "No conversations to export since last 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.

This only passes because the seeded chat's timestamp is in the past. Fine now, but add a one-line comment saying so — otherwise changing fixture dates later would break this test in a confusing way.

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.

Three API endpoints: add dedicated test coverage

2 participants