Skip to content

feat(time-accounting): add time_unit support to update_ticket and add_article#211

Open
matiasiglesias wants to merge 1 commit intobasher83:mainfrom
matiasiglesias:main
Open

feat(time-accounting): add time_unit support to update_ticket and add_article#211
matiasiglesias wants to merge 1 commit intobasher83:mainfrom
matiasiglesias:main

Conversation

@matiasiglesias
Copy link
Copy Markdown

@matiasiglesias matiasiglesias commented Mar 19, 2026

Summary

  • Add time_unit parameter to zammad_update_ticket and zammad_add_article MCP tools for Zammad time accounting support
  • Add time_unit field to TicketUpdateParams, TicketUpdate, and ArticleCreate Pydantic models with validation (must be > 0)
  • Add time_unit parameter to client.update_ticket() and client.add_article() methods, only included in API payload when not None

Test plan

  • All 191 existing tests pass
  • New tests for update_ticket with time_unit (client and server level)
  • New tests for add_article with time_unit (client and server level)
  • Validation tests: rejects time_unit=0 and time_unit=-5
  • Exclusion tests: time_unit not sent in payload when None
  • Manually verified with live Zammad instance (ticket #99494, 2.5 time units logged)

Summary by CodeRabbit

  • New Features
    • Added optional time tracking when updating tickets
    • Added optional time tracking when adding ticket articles

…_article

- Add time_unit parameter to TicketUpdateParams, TicketUpdate models
- Add time_unit parameter to client.update_ticket() method
- Add time_unit parameter to ArticleCreate model and client.add_article()
- Update server docstring for zammad_update_ticket tool
- Add comprehensive tests for time_unit in both tools
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 19, 2026

Walkthrough

The pull request adds optional time_unit: float | None parameter support to ticket and article operations. Changes include method signature updates in the Zammad client, corresponding Pydantic model field additions with validation (gt=0), server tool documentation, and comprehensive test coverage verifying both parameter presence and absence scenarios.

Changes

Cohort / File(s) Summary
Client & Model Updates
mcp_zammad/client.py, mcp_zammad/models.py
Added time_unit: float | None = None parameter to update_ticket() and add_article() methods. Conditionally includes time_unit in request payloads only when not None. Added corresponding time_unit fields to TicketUpdate, ArticleCreate, and TicketUpdateParams Pydantic models with gt=0 validation.
Server Documentation
mcp_zammad/server.py
Extended zammad_update_ticket tool parameter documentation to include new time_unit field; existing parameter extraction flow remains unchanged.
Test Coverage
tests/test_client.py, tests/test_client_methods.py, tests/test_server.py
Added 207 lines of test cases verifying: payload inclusion when time_unit is provided, exclusion when omitted, validation rejection of invalid values (≤ 0), and correct API call forwarding across both client methods and server tools.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Suggested labels

✨ enhancement, ⚡ api, 👨‍🚀 crew-backend, 🏷️ auto-labeled

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title follows conventional commit format (feat prefix) and accurately describes the main change: adding time_unit support to update_ticket and add_article methods.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Tip

CodeRabbit can generate a title for your PR based on the changes.

Add @coderabbitai placeholder anywhere in the title of your PR and CodeRabbit will replace it with a title based on the changes in the PR. You can change the placeholder by changing the reviews.auto_title_placeholder setting.

Copy link
Copy Markdown
Contributor

@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

Caution

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

⚠️ Outside diff range comments (1)
mcp_zammad/client.py (1)

220-236: ⚠️ Potential issue | 🟠 Major

Validate time_unit in client methods before sending API requests

update_ticket() and add_article() accept time_unit but do not enforce > 0 locally. Direct client users can pass 0/negative values and only fail downstream.

🔧 Proposed fix
     def update_ticket(
         self,
         ticket_id: int,
         title: str | None = None,
         state: str | None = None,
         priority: str | None = None,
         owner: str | None = None,
         group: str | None = None,
         time_unit: float | None = None,
     ) -> dict[str, Any]:
         """Update an existing ticket."""
+        if time_unit is not None and time_unit <= 0:
+            raise ValueError("time_unit must be greater than 0")
+
         update_data = {}
         if title is not None:
             update_data["title"] = title
@@
     def add_article(
         self,
         ticket_id: int,
         body: str,
         article_type: str = "note",
         internal: bool = False,
         sender: str = "Agent",
         time_unit: float | None = None,
         attachments: list[dict[str, str]] | None = None,
     ) -> dict[str, Any]:
         """Add an article (comment/note) to a ticket with optional attachments.
@@
         """
+        if time_unit is not None and time_unit <= 0:
+            raise ValueError("time_unit must be greater than 0")
+
         article_data = {
             "ticket_id": ticket_id,
             "body": body,

As per coding guidelines, "Always validate input with Pydantic models" and "Validate all user inputs".

Also applies to: 246-276

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

In `@mcp_zammad/client.py` around lines 220 - 236, The update_ticket and
add_article methods accept a time_unit parameter but do not validate it locally,
allowing 0 or negative values to be sent to the API; before adding time_unit to
update_data (or the article payload) check that time_unit is not None and > 0
and otherwise raise a ValueError (or use the existing Pydantic request model
validation) so invalid values are rejected client-side—update the checks around
the time_unit parameter in update_ticket and add_article (and any similar blocks
that build payloads from time_unit) to perform this validation before including
time_unit in the request payload.
🤖 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/test_server.py`:
- Around line 785-818: The tests call the mocked client's update_ticket directly
instead of exercising the server/tool handler, so they don't verify
zammad_update_ticket's parameter forwarding; change both
test_update_ticket_with_time_unit_tool and
test_update_ticket_without_time_unit_tool to invoke the server's tool handler
(zammad_update_ticket) on the ZammadMCPServer instance (e.g., call
server_inst.zammad_update_ticket or the server's tool dispatch method that runs
the zammad_update_ticket handler) with the same args used now, then assert
mock_instance.update_ticket was called with the expected parameters (including
time_unit when provided and omitted when not).

---

Outside diff comments:
In `@mcp_zammad/client.py`:
- Around line 220-236: The update_ticket and add_article methods accept a
time_unit parameter but do not validate it locally, allowing 0 or negative
values to be sent to the API; before adding time_unit to update_data (or the
article payload) check that time_unit is not None and > 0 and otherwise raise a
ValueError (or use the existing Pydantic request model validation) so invalid
values are rejected client-side—update the checks around the time_unit parameter
in update_ticket and add_article (and any similar blocks that build payloads
from time_unit) to perform this validation before including time_unit in the
request payload.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 020c0df1-95b3-48be-b050-311ce0521270

📥 Commits

Reviewing files that changed from the base of the PR and between a20d24f and 0cff05f.

📒 Files selected for processing (6)
  • mcp_zammad/client.py
  • mcp_zammad/models.py
  • mcp_zammad/server.py
  • tests/test_client.py
  • tests/test_client_methods.py
  • tests/test_server.py

Comment thread tests/test_server.py
Comment on lines +785 to +818
def test_update_ticket_with_time_unit_tool(mock_zammad_client, sample_ticket_data):
"""Test update ticket tool with time_unit for time accounting."""
mock_instance, _ = mock_zammad_client

updated_ticket = sample_ticket_data.copy()
updated_ticket["title"] = "Updated Title"

mock_instance.update_ticket.return_value = updated_ticket

server_inst = ZammadMCPServer()
server_inst.client = mock_instance
client = server_inst.get_client()

ticket_data = client.update_ticket(1, title="Updated Title", time_unit=2.5)
result = Ticket(**ticket_data)

assert result.id == 1
mock_instance.update_ticket.assert_called_once_with(1, title="Updated Title", time_unit=2.5)


def test_update_ticket_without_time_unit_tool(mock_zammad_client, sample_ticket_data):
"""Test update ticket tool without time_unit does not pass it."""
mock_instance, _ = mock_zammad_client

mock_instance.update_ticket.return_value = sample_ticket_data

server_inst = ZammadMCPServer()
server_inst.client = mock_instance
client = server_inst.get_client()

client.update_ticket(1, title="Updated Title")

mock_instance.update_ticket.assert_called_once_with(1, title="Updated Title")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

update_ticket time-unit tests currently assert mock behavior, not tool behavior

At Line 798 and Line 815 the test calls a mocked client directly, so it does not verify zammad_update_ticket parameter forwarding at the server/tool layer.

🔧 Proposed fix (exercise the actual tool handler)
-def test_update_ticket_with_time_unit_tool(mock_zammad_client, sample_ticket_data):
+def test_update_ticket_with_time_unit_tool(mock_zammad_client, sample_ticket_data, decorator_capturer):
@@
-    client = server_inst.get_client()
-
-    ticket_data = client.update_ticket(1, title="Updated Title", time_unit=2.5)
-    result = Ticket(**ticket_data)
+    test_tools, capture_tool = decorator_capturer(server_inst.mcp.tool)
+    server_inst.mcp.tool = capture_tool  # type: ignore[method-assign, assignment]
+    server_inst.get_client = lambda: server_inst.client  # type: ignore[method-assign, assignment, return-value]
+    server_inst._setup_tools()
+    result = test_tools["zammad_update_ticket"](TicketUpdateParams(ticket_id=1, title="Updated Title", time_unit=2.5))
@@
-    mock_instance.update_ticket.assert_called_once_with(1, title="Updated Title", time_unit=2.5)
+    mock_instance.update_ticket.assert_called_once_with(ticket_id=1, title="Updated Title", time_unit=2.5)

-def test_update_ticket_without_time_unit_tool(mock_zammad_client, sample_ticket_data):
+def test_update_ticket_without_time_unit_tool(mock_zammad_client, sample_ticket_data, decorator_capturer):
@@
-    client = server_inst.get_client()
-
-    client.update_ticket(1, title="Updated Title")
-
-    mock_instance.update_ticket.assert_called_once_with(1, title="Updated Title")
+    test_tools, capture_tool = decorator_capturer(server_inst.mcp.tool)
+    server_inst.mcp.tool = capture_tool  # type: ignore[method-assign, assignment]
+    server_inst.get_client = lambda: server_inst.client  # type: ignore[method-assign, assignment, return-value]
+    server_inst._setup_tools()
+    test_tools["zammad_update_ticket"](TicketUpdateParams(ticket_id=1, title="Updated Title"))
+
+    mock_instance.update_ticket.assert_called_once_with(ticket_id=1, title="Updated Title")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_server.py` around lines 785 - 818, The tests call the mocked
client's update_ticket directly instead of exercising the server/tool handler,
so they don't verify zammad_update_ticket's parameter forwarding; change both
test_update_ticket_with_time_unit_tool and
test_update_ticket_without_time_unit_tool to invoke the server's tool handler
(zammad_update_ticket) on the ZammadMCPServer instance (e.g., call
server_inst.zammad_update_ticket or the server's tool dispatch method that runs
the zammad_update_ticket handler) with the same args used now, then assert
mock_instance.update_ticket was called with the expected parameters (including
time_unit when provided and omitted when not).

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