Skip to content

Add Circles (Teams) tools — 14 tools via OCS#51

Merged
oleksandr-nc merged 8 commits into
mainfrom
feature/p7-circles-tools
Apr 25, 2026
Merged

Add Circles (Teams) tools — 14 tools via OCS#51
oleksandr-nc merged 8 commits into
mainfrom
feature/p7-circles-tools

Conversation

@oleksandr-nc
Copy link
Copy Markdown
Contributor

@oleksandr-nc oleksandr-nc commented Apr 24, 2026

Summary

Adds 14 MCP tools for the Nextcloud Circles / Teams app using the OCS LocalController API at /ocs/v2.php/apps/circles/.... Full circle and membership management: create, rename, configure, add/remove members, promote/demote, join/leave, search, delete.

Why Circles: only pure-OCS app remaining with substantial scope that matches the OCS-only invariant. Bookmarks/Notes/Deck/Photos all require non-OCS REST endpoints and were declined.

Tools

  • Read (4): list_circles, get_circle, list_circle_members, search_circles
  • Write (8): create_circle, update_circle_name, update_circle_description, update_circle_config, add_circle_member, update_circle_member_level, join_circle, leave_circle
  • Destructive (2): delete_circle, remove_circle_member

Design notes

  • Circle and member IDs are strings (24-char hashes), not integers — different from Forms/Shares. The id of a member is its membership record id (not the user's userId or singleId); use the id field returned by list_circle_members when calling remove_circle_member / update_circle_member_level.
  • member_type accepts human-readable strings ("user", "group", "mail", "contact", "circle"), mapped server-side to the integer constants (1, 2, 4, 8, 16).
  • level for update_circle_member_level accepts "member"/"moderator"/"admin"/"owner" (1/4/8/9).
  • update_circle_config takes the raw bitmask int; docstring documents the flag values (VISIBLE=8, OPEN=16, INVITE=32, HIDDEN=1024, etc.).
  • leave_circle docstring warns: leaving as the sole owner destroys the circle (verified live against NC 34). Promote another member to "owner" first to avoid this.
  • Admin-emulated routes (/admin/{emulated}/...) intentionally omitted — callers are already authenticated as themselves; emulated ops add no value for MCP.
  • Federated-remote and settings routes skipped as not end-user useful.

Total tool count: 126 → 140 (+ upload_file_from_path conditional → 141).

Test plan

  • 22 integration tests covering lifecycle, member ops, permission enforcement, edge cases (invalid type/level, nonexistent circle)
  • test_server.py::test_all_tools_registered sees all 14 new tools
  • 104 unit tests still pass
  • Ruff + pyright strict clean
  • Full regression: Forms (34) + Server (8) tests still green
  • CI: added occ app:enable circles (circles is bundled but not always enabled on minimal images)
  • CI green on NC 32 + NC 33

Summary by CodeRabbit

  • New Features

    • Added Circles (Teams) management: create, update, delete, list, search, join/leave, and member management with permission levels.
  • Documentation

    • Updated README and progress docs to include Circles, adjusted tool counts and "Next Up" notes for skipped tools when API is limited.
  • Tests

    • Added integration tests and test cleanup for Circles; integration workflow now tolerates a missing Circles app during test runs.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

📝 Walkthrough

Walkthrough

Adds Circles (Teams) MCP tools and registration, comprehensive integration tests and cleanup, README/PROGRESS updates, and a CI workflow step that attempts to enable the Nextcloud circles app (failure treated non‑fatally).

Changes

Cohort / File(s) Summary
Circles implementation
src/nc_mcp_server/tools/circles.py
New MCP tools module registering circle lifecycle, membership, listing, search, and config tools; defines MEMBER_TYPES, MEMBER_LEVELS, and register(mcp: FastMCP).
Server integration
src/nc_mcp_server/server.py
Imports and registers the circles tools module during server creation.
Integration tests
tests/integration/test_circles.py, tests/integration/test_server.py
Adds comprehensive circles integration tests (CRUD, members, join/leave, search, permissions) and expands EXPECTED_TOOLS with circle tool names.
Test fixtures / cleanup
tests/integration/conftest.py
Refactors cleanup into targeted helpers and adds circle cleanup to remove test-created circles and related artifacts.
CI workflow
.github/workflows/tests-integration.yml
Attempts to php occ app:enable circles during integration setup; treats enable failure as non-fatal to support environments without the app.
Docs & progress
README.md, PROGRESS.md
Adds "Circles (Teams)" to tool inventory and test coverage tables; updates totals and documents circle tools and permissions.

Sequence Diagram(s)

sequenceDiagram
    participant Client as "MCP Client"
    participant MCP as "nc_mcp_server (circles tools)"
    participant OCS as "Nextcloud OCS API"
    rect rgba(200,200,255,0.5)
    Client->>MCP: call create_circle(name, config)
    MCP->>OCS: POST /ocs/v2.php/apps/circles/api/v1/circles (payload)
    OCS-->>MCP: 201 {circle_id, details}
    MCP-->>Client: return JSON string with created circle
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I nibbled code beneath the moonlit beams,
I stitched up circles into teamly dreams,
Members hop, add, promote — a tiny cheer,
Tests chase the carrots, cleanup keeps things clear,
A happy thump — the MCP rings near! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.36% 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 directly and concisely describes the main change: adding 14 Circles (Teams) tools via the OCS API, matching the primary feature in the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 feature/p7-circles-tools

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 24, 2026

Codecov Report

❌ Patch coverage is 98.29060% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.99%. Comparing base (17b86e1) to head (8504ad7).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/nc_mcp_server/tools/circles.py 98.27% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #51      +/-   ##
==========================================
+ Coverage   95.74%   95.99%   +0.24%     
==========================================
  Files          29       30       +1     
  Lines        3105     3222     +117     
==========================================
+ Hits         2973     3093     +120     
+ Misses        132      129       -3     
Flag Coverage Δ
integration 94.75% <98.29%> (+0.29%) ⬆️
nc32 94.75% <98.29%> (+0.29%) ⬆️
nc33 94.72% <98.29%> (+0.29%) ⬆️
py3.12 9.90% <0.00%> (-0.38%) ⬇️
py3.13 9.90% <0.00%> (-0.38%) ⬇️
py3.14 9.90% <0.00%> (-0.38%) ⬇️
session-cache 20.20% <11.11%> (-0.35%) ⬇️
unit 9.90% <0.00%> (-0.38%) ⬇️
user-permissions 39.91% <51.28%> (+0.42%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@oleksandr-nc oleksandr-nc marked this pull request as ready for review April 24, 2026 10:41
@oleksandr-nc
Copy link
Copy Markdown
Contributor Author

(AI) CI green on both NC 32 and NC 33 after the fixture fix (peer user provisioned via fixture). 22 Circles integration tests + 8 server registration tests + 104 unit tests all pass. Marked ready for review.

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

🧹 Nitpick comments (2)
tests/integration/test_circles.py (1)

50-55: Pagination assertion can be tighter.

len(circles) <= 2 is satisfied even if the server ignored limit and returned 0. Since the test just created three mcp-test-circle-page-* circles and the autouse cleanup runs before each test, asserting == 2 gives you a real pagination check.

♻️ Proposed nit
         circles: list[dict[str, Any]] = json.loads(await nc_mcp.call("list_circles", limit=2))
-        assert len(circles) <= 2
+        assert len(circles) == 2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integration/test_circles.py` around lines 50 - 55, The pagination test
in test_list_pagination is too permissive; change the assertion to require
exactly 2 results so the server honor of the "limit" parameter is actually
verified: after creating three circles via _make_circle and calling
nc_mcp.call("list_circles", limit=2) in test_list_pagination, replace the
current len(circles) <= 2 assertion with an assertion that len(circles) == 2.
.github/workflows/tests-integration.yml (1)

76-76: Silent fallback will surface as noisy test failures if circles isn't available.

If app:enable circles fails on a future NC image where the bundled app is absent, every test in tests/integration/test_circles.py will hit the Nextcloud API and fail rather than skip. Consider gating the circles tests on a module/session fixture that verifies the app is enabled (e.g. GET /ocs/v2.php/apps/circles/circles and pytest.skip(...) on failure), so CI on bare-bones images doesn't turn red. Current behavior is fine for NC 32/33 but this makes the “may not be shipped” comment actually actionable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/tests-integration.yml at line 76, The CI job silently
ignores failure of `php occ app:enable circles`, causing tests in
tests/integration/test_circles.py to fail noisily when the app is absent; add a
module- or session-scoped pytest fixture (referencing
tests/integration/test_circles.py) that checks the app is enabled by performing
an HTTP GET to the Circles OCS endpoint (e.g. GET
/ocs/v2.php/apps/circles/circles) using the existing test HTTP client, and call
pytest.skip(...) if the request indicates the app is missing or returns a
non-success status; ensure all tests in test_circles.py depend on that fixture
so CI on bare-bones Nextcloud images will skip instead of fail.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/integration/test_circles.py`:
- Around line 188-202: The test class TestJoinLeave currently only exercises
create_circle → add_circle_member → remove_circle_member and never calls the
join_circle or leave_circle endpoints; update tests to actually cover both flows
by (1) adding a new test that creates an OPEN circle (config=16) using
McpTestHelper/admin, then authenticates a separate peer client (e.g.,
instantiate a NextcloudClient or a peer-authenticated McpTestHelper using
circle_peer credentials) and calls the join_circle endpoint, then asserts the
peer appears via list_circle_members; and (2) add a test where a non-owner peer
calls leave_circle (after being added or joined) and assert they are removed
from list_circle_members while the circle still exists; alternatively, if you
cannot add these tests now, rename TestJoinLeave to TestMemberRemoval to reflect
the current coverage.

---

Nitpick comments:
In @.github/workflows/tests-integration.yml:
- Line 76: The CI job silently ignores failure of `php occ app:enable circles`,
causing tests in tests/integration/test_circles.py to fail noisily when the app
is absent; add a module- or session-scoped pytest fixture (referencing
tests/integration/test_circles.py) that checks the app is enabled by performing
an HTTP GET to the Circles OCS endpoint (e.g. GET
/ocs/v2.php/apps/circles/circles) using the existing test HTTP client, and call
pytest.skip(...) if the request indicates the app is missing or returns a
non-success status; ensure all tests in test_circles.py depend on that fixture
so CI on bare-bones Nextcloud images will skip instead of fail.

In `@tests/integration/test_circles.py`:
- Around line 50-55: The pagination test in test_list_pagination is too
permissive; change the assertion to require exactly 2 results so the server
honor of the "limit" parameter is actually verified: after creating three
circles via _make_circle and calling nc_mcp.call("list_circles", limit=2) in
test_list_pagination, replace the current len(circles) <= 2 assertion with an
assertion that len(circles) == 2.
🪄 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: 4f0a6079-3927-4486-972f-af3215d63ba0

📥 Commits

Reviewing files that changed from the base of the PR and between 17b86e1 and 9d3a48c.

📒 Files selected for processing (8)
  • .github/workflows/tests-integration.yml
  • PROGRESS.md
  • README.md
  • src/nc_mcp_server/server.py
  • src/nc_mcp_server/tools/circles.py
  • tests/integration/conftest.py
  • tests/integration/test_circles.py
  • tests/integration/test_server.py

Comment thread src/nc_mcp_server/tools/circles.py Outdated
Comment thread tests/integration/test_circles.py
@oleksandr-nc
Copy link
Copy Markdown
Contributor Author

(AI) Addressed all three CodeRabbit findings in 6624df7:

  • Pagination (<= 2== 2): since we create three circles and autouse cleanup runs beforehand, the server should return exactly 2 under limit=2. Tightened to catch a regression where the server ignores the limit.
  • Silent app:enable fallback: added a session-scoped sync probe against /ocs/v2.php/apps/circles/circles and an autouse pytest.skip for every test in this module when Circles isn't enabled. Bare-bones images now skip cleanly instead of hard-failing.
  • TestJoinLeave was misnamed: the previous test_leave_as_non_owner only exercised add+remove. Replaced it with two real tests: test_join_open_circle (peer joins an OPEN circle via join_circle) and test_leave_as_member (peer calls leave_circle on themselves). Both use a new _as_peer async context manager that swaps the global state so McpTestHelper.call() authenticates as the peer user for the duration of the block.

Local: 23/23 pass (131s). CI re-running.

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)
tests/integration/test_circles.py (1)

65-89: _as_peer teardown can shadow the original test exception.

If the body under yield raises, the finally block runs set_state(...) (fine) and then await peer_client.close(). If the close itself raises (e.g., transient network error, cancelled task during event-loop teardown), that new exception replaces the original test failure as the visible error, making triage harder. Since the only purpose of close in the finally is resource cleanup, it's worth suppressing failures here.

♻️ Proposed tweak
     try:
         yield
     finally:
         set_state(admin_client, admin_config)
-        await peer_client.close()
+        with contextlib.suppress(Exception):
+            await peer_client.close()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integration/test_circles.py` around lines 65 - 89, The teardown in the
asynccontextmanager _as_peer can let exceptions raised by await
peer_client.close() mask an original test failure; update the finally block in
_as_peer (where set_state(admin_client, admin_config) and await
peer_client.close() are called) to catch and suppress (or log) any exceptions
from peer_client.close() so cleanup failures do not replace the original
exception — i.e., wrap the await peer_client.close() in a try/except Exception:
(or asyncio.CancelledError handling as appropriate) and swallow or log the error
instead of letting it propagate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/integration/test_circles.py`:
- Around line 116-120: Tests test_create_sets_owner_to_caller and
test_owner_present_in_members hardcode "admin" as the expected initiator userId;
change them to read the active admin from the runtime config instead. Locate the
assertions referencing created["initiator"]["userId"] and replace the literal
"admin" with the configured value (e.g., Config.user or the NEXTCLOUD_USER env
var via os.environ.get("NEXTCLOUD_USER")), ensuring you import/access the same
Config used by the test harness (or nc_mcp helper) so the assertion uses the
actual admin account name in the environment.

---

Nitpick comments:
In `@tests/integration/test_circles.py`:
- Around line 65-89: The teardown in the asynccontextmanager _as_peer can let
exceptions raised by await peer_client.close() mask an original test failure;
update the finally block in _as_peer (where set_state(admin_client,
admin_config) and await peer_client.close() are called) to catch and suppress
(or log) any exceptions from peer_client.close() so cleanup failures do not
replace the original exception — i.e., wrap the await peer_client.close() in a
try/except Exception: (or asyncio.CancelledError handling as appropriate) and
swallow or log the error instead of letting it propagate.
🪄 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: c9be8417-4b85-465f-bf43-60a922747c9d

📥 Commits

Reviewing files that changed from the base of the PR and between 9d3a48c and b423e29.

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

Comment thread tests/integration/test_circles.py
Deep re-review against the Nextcloud Circles source and live API surfaced
three docstring bugs and one permission mismatch:

- update_circle_member_level: docstring claimed it returned the updated
  circle, but the server returns the updated MEMBER (verified in
  FederatedItems/MemberLevel.php — outcome is the cloned member with the
  new level).
- search_circles: docstring named a "source" field; actual response has
  "userType" and keys ['displayName','id','instance','userId','userType'].
- leave_circle: docstring claimed it returned a membership record; the
  server actually returns the CIRCLE object (CircleLeave::verify calls
  circleRequest->getCircle and serializes it as the outcome). Empty when
  the caller loses visibility (sole-owner destroy case).
- leave_circle permission: was WRITE + ADDITIVE_IDEMPOTENT. Sole-owner
  leave silently destroys the whole circle, and the matching
  leave_conversation tool in Talk is DESTRUCTIVE. Align annotations and
  permission level to DESTRUCTIVE.

New tests cover the gaps the review surfaced: offset pagination,
full_details=True response shape, owner transfer via update_circle_member_level
(plus peer cleanup for the post-transfer circle), join on a non-OPEN
circle failing, sole-owner leave destroying the circle, and the new
permission gate on leave_circle.
@oleksandr-nc
Copy link
Copy Markdown
Contributor Author

(AI) Deep re-review of the feature surfaced three docstring bugs and one permission mismatch — all verified against the Nextcloud Circles source and live API calls before pushing a fix.

Real bugs

  • update_circle_member_level docstring claimed it returned the updated circle; the server actually returns the updated member (FederatedItems/MemberLevel.php:114-116 — outcome is clone $member with the new level). An AI reading result["name"] would KeyError.
  • search_circles docstring named a source field; response entries actually have userType (keys: ['displayName','id','instance','userId','userType']). Filtering by source silently matches nothing.
  • leave_circle docstring claimed it returned a membership record; the server returns the circle object (CircleLeave::verify at line 141-142 calls circleRequest->getCircle and serializes it as the outcome). Empty outcome when caller loses visibility (sole-owner destroy case).

Safety/consistency

  • leave_circle was WRITE + ADDITIVE_IDEMPOTENT. Verified via live test that sole-owner leave silently destroys the whole circle (HTTP 200, no warning). leave_conversation in Talk — same user-facing intent — is DESTRUCTIVE. Bumped leave_circle to match.

Test gaps closed

  • offset pagination (previously only limit was exercised)
  • full_details=True list shape (adds circle field)
  • Owner transfer via update_circle_member_level(level="owner") — asserts new-owner level=9 and demoted caller level=8; tears down as peer since admin loses destroy rights
  • Join on a non-OPEN circle (should fail)
  • Sole-owner leave destroys the circle
  • Permission gate: WRITE-level users now blocked from leave_circle

Circles test count: 23 → 29. Commit: 0a68ff4. Monitoring CI.

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.

🧹 Nitpick comments (1)
tests/integration/test_circles.py (1)

65-89: _as_peer relies on shared global state — document the sequential-execution assumption.

set_state() swaps the module-level singleton client/config, so any concurrent test (e.g., under pytest-xdist with processes/threads sharing the interpreter, or parallel asyncio tests within the same module) that calls a Circles tool while _as_peer is active would execute under the wrong identity. This is safe today because pytest-asyncio runs tests sequentially, but a future concurrency change could silently break auth assumptions without a visible failure.

Low-risk mitigation: add a short comment on the helper noting the sequential-only assumption, or guard with an asyncio.Lock at module scope to document intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integration/test_circles.py` around lines 65 - 89, The _as_peer helper
swaps module-global client/config via set_state (using get_client/get_config and
NextcloudClient/peer_client) which is unsafe under concurrent tests; add a brief
comment above _as_peer stating it assumes sequential test execution and is not
concurrency-safe, and protect the swap by introducing a module-level
asyncio.Lock used by _as_peer to serialize entry/exit (acquire before changing
state and release after restoring state and closing peer_client) so concurrent
tasks can't observe or corrupt the global client/config.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/integration/test_circles.py`:
- Around line 65-89: The _as_peer helper swaps module-global client/config via
set_state (using get_client/get_config and NextcloudClient/peer_client) which is
unsafe under concurrent tests; add a brief comment above _as_peer stating it
assumes sequential test execution and is not concurrency-safe, and protect the
swap by introducing a module-level asyncio.Lock used by _as_peer to serialize
entry/exit (acquire before changing state and release after restoring state and
closing peer_client) so concurrent tasks can't observe or corrupt the global
client/config.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3b0fdc77-1d60-48fd-9518-34597bbf47cb

📥 Commits

Reviewing files that changed from the base of the PR and between b423e29 and 0a68ff4.

📒 Files selected for processing (4)
  • PROGRESS.md
  • src/nc_mcp_server/tools/circles.py
  • tests/integration/test_circles.py
  • tests/integration/test_forms.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • PROGRESS.md

@oleksandr-nc
Copy link
Copy Markdown
Contributor Author

(AI) Addressed the CodeRabbit nitpick in 112b39a — added a short Concurrency: note to the _as_peer docstring making the sequential-only assumption explicit and pointing to the mitigations (asyncio.Lock or a per-call client) if this ever runs under pytest-xdist. Skipped adding the Lock itself since pytest-asyncio's default is single-loop sequential and the lock would be dead code today.

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.

🧹 Nitpick comments (1)
tests/integration/test_circles.py (1)

297-314: Consider moving the peer-auth cleanup into a finally block.

If either assertion at lines 310–311 fails after ownership has been transferred, the peer-authenticated delete_circle at 313–314 never runs, and the remaining circle is owned solely by CIRCLE_TEST_USER. Whether the admin-credentialed conftest cleanup can then delete it depends on admin privileges on the running NC version, which could leak state across runs.

♻️ Proposed fix
     async def test_promote_to_owner_transfers_and_demotes_caller(self, nc_mcp: McpTestHelper, circle_peer: str) -> None:
         """Promoting a member to owner transfers ownership; previous owner becomes admin (level=8)."""
         circle = await _make_circle(nc_mcp, "mcp-test-circle-xfer-owner")
         added = json.loads(await nc_mcp.call("add_circle_member", circle_id=circle["id"], user_id=circle_peer))
         await nc_mcp.call(
             "update_circle_member_level",
             circle_id=circle["id"],
             member_id=added["id"],
             level="owner",
         )
-        members: list[dict[str, Any]] = json.loads(await nc_mcp.call("list_circle_members", circle_id=circle["id"]))
-        peer_level = next(m["level"] for m in members if m.get("userId") == circle_peer)
-        caller_level = next(m["level"] for m in members if m.get("userId") == get_config().user)
-        assert peer_level == 9, "promoted member should be owner (9)"
-        assert caller_level == 8, "previous owner should be demoted to admin (8)"
-        # peer is now the only owner — admin cleanup can't destroy it. Tear down as peer.
-        async with _as_peer(circle_peer, CIRCLE_TEST_PWD):
-            await nc_mcp.call("delete_circle", circle_id=circle["id"])
+        try:
+            members: list[dict[str, Any]] = json.loads(
+                await nc_mcp.call("list_circle_members", circle_id=circle["id"])
+            )
+            peer_level = next(m["level"] for m in members if m.get("userId") == circle_peer)
+            caller_level = next(m["level"] for m in members if m.get("userId") == get_config().user)
+            assert peer_level == 9, "promoted member should be owner (9)"
+            assert caller_level == 8, "previous owner should be demoted to admin (8)"
+        finally:
+            # peer is now the only owner — admin cleanup can't destroy it. Tear down as peer.
+            async with _as_peer(circle_peer, CIRCLE_TEST_PWD):
+                await nc_mcp.call("delete_circle", circle_id=circle["id"])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integration/test_circles.py` around lines 297 - 314, The
test_promote_to_owner_transfers_and_demotes_caller test should guarantee the
peer-authenticated cleanup runs even if assertions fail: wrap the section from
adding/updating the member through the assertions in a try/finally, and move the
async with _as_peer(circle_peer, CIRCLE_TEST_PWD): await
nc_mcp.call("delete_circle", circle_id=circle["id"]) into the finally block;
ensure you check that circle and its id still exist (created by _make_circle)
before attempting delete to avoid raising new errors, and keep calls to
add_circle_member, update_circle_member_level, list_circle_members, get_config,
and delete_circle unchanged otherwise.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/integration/test_circles.py`:
- Around line 297-314: The test_promote_to_owner_transfers_and_demotes_caller
test should guarantee the peer-authenticated cleanup runs even if assertions
fail: wrap the section from adding/updating the member through the assertions in
a try/finally, and move the async with _as_peer(circle_peer, CIRCLE_TEST_PWD):
await nc_mcp.call("delete_circle", circle_id=circle["id"]) into the finally
block; ensure you check that circle and its id still exist (created by
_make_circle) before attempting delete to avoid raising new errors, and keep
calls to add_circle_member, update_circle_member_level, list_circle_members,
get_config, and delete_circle unchanged otherwise.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 426c8313-7e1c-44e2-9fc8-bf937155e7ee

📥 Commits

Reviewing files that changed from the base of the PR and between 0a68ff4 and 8504ad7.

📒 Files selected for processing (3)
  • PROGRESS.md
  • src/nc_mcp_server/tools/circles.py
  • tests/integration/test_circles.py
✅ Files skipped from review due to trivial changes (2)
  • PROGRESS.md
  • src/nc_mcp_server/tools/circles.py

@oleksandr-nc oleksandr-nc merged commit 99470be into main Apr 25, 2026
12 checks passed
@oleksandr-nc oleksandr-nc deleted the feature/p7-circles-tools branch April 25, 2026 17:16
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