"""Tests for ``omi action-item`` commands."""
from __future__ import annotations
import json
import httpx
import pytest
from omi_cli.main import app
def test_action_item_list(authed_profile, respx_mock, cli_runner) -> None:
respx_mock.get("/v1/dev/user/action-items").respond(
json=[{"id": "a1", "description": "ship it", "completed": False}]
)
result = cli_runner.invoke(app, ["--json", "action-item", "list"])
assert result.exit_code == 0
payload = json.loads(result.stdout)
assert payload[0]["id"] == "a1"
def test_action_item_create(authed_profile, respx_mock, cli_runner) -> None:
route = respx_mock.post("/v1/dev/user/action-items").respond(
json={"id": "a1", "description": "buy milk", "completed": False}
)
result = cli_runner.invoke(app, ["--json", "action-item", "create", "buy milk"])
assert result.exit_code == 0
body = json.loads(route.calls.last.request.content)
assert body["description"] == "buy milk"
def test_action_item_complete_uses_patch(authed_profile, respx_mock, cli_runner) -> None:
route = respx_mock.patch("/v1/dev/user/action-items/a1").respond(
json={"id": "a1", "description": "ship", "completed": True}
)
result = cli_runner.invoke(app, ["--json", "action-item", "complete", "a1"])
assert result.exit_code == 0
body = json.loads(route.calls.last.request.content)
assert body == {"completed": True}
def test_action_item_filter_completed(authed_profile, respx_mock, cli_runner) -> None:
route = respx_mock.get("/v1/dev/user/action-items").respond(json=[])
cli_runner.invoke(app, ["action-item", "list", "--completed"])
request = route.calls.last.request
assert request.url.params["completed"] == "true"
def test_action_item_delete(authed_profile, respx_mock, cli_runner) -> None:
respx_mock.delete("/v1/dev/user/action-items/a1").respond(json={"success": True})
result = cli_runner.invoke(app, ["action-item", "delete", "a1", "-y"])
assert result.exit_code == 0
def test_action_item_get_missing_returns_not_found_exit_code(authed_profile, respx_mock, cli_runner) -> None:
"""Same agent contract as memory get — client-side miss is exit 5, not 1."""
respx_mock.get("/v1/dev/user/action-items").respond(json=[])
result = cli_runner.invoke(app, ["action-item", "get", "missing"])
assert result.exit_code == 5
assert "not found" in result.stderr.lower()
@pytest.mark.parametrize("found", [True, False])
def test_action_item_get_searches_beyond_five_pages(authed_profile, respx_mock, cli_runner, found) -> None:
"""A full fifth page cannot establish that an action item does not exist."""
offsets = []
def respond(request):
offset = int(request.url.params["offset"])
offsets.append(offset)
assert request.url.params["limit"] == "200"
if offset < 1000:
return httpx.Response(200, json=[{"id": f"a{i}"} for i in range(offset, offset + 200)])
assert offset == 1000
return httpx.Response(200, json=[{"id": "target"}] if found else [])
respx_mock.get("/v1/dev/user/action-items").mock(side_effect=respond)
result = cli_runner.invoke(app, ["--json", "action-item", "get", "target"])
assert offsets == [0, 200, 400, 600, 800, 1000]
assert result.exit_code == (0 if found else 5)
if found:
assert json.loads(result.stdout) == {"id": "target"}
def test_action_item_get_continues_past_short_pages(authed_profile, respx_mock, cli_runner) -> None:
"""A short page is not exhaustion: the API filters locked records after paging (#13214)."""
offsets = []
def respond(request):
offset = int(request.url.params["offset"])
offsets.append(offset)
if offset == 0:
return httpx.Response(200, json=[{"id": f"a{i}"} for i in range(199)])
if offset == 200:
return httpx.Response(200, json=[{"id": "target"}])
return httpx.Response(200, json=[])
respx_mock.get("/v1/dev/user/action-items").mock(side_effect=respond)
result = cli_runner.invoke(app, ["--json", "action-item", "get", "target"])
assert result.exit_code == 0
assert json.loads(result.stdout) == {"id": "target"}
assert offsets == [0, 200]
def test_action_item_get_stops_on_empty_page(authed_profile, respx_mock, cli_runner) -> None:
route = respx_mock.get("/v1/dev/user/action-items").respond(json=[])
result = cli_runner.invoke(app, ["--json", "action-item", "get", "missing"])
assert result.exit_code == 5
assert route.call_count == 1
@pytest.mark.parametrize("blank", ["", " ", "\t"])
def test_action_item_create_rejects_blank_description(authed_profile, respx_mock, cli_runner, blank) -> None:
route = respx_mock.post("/v1/dev/user/action-items").respond(json={"id": "a1"})
result = cli_runner.invoke(app, ["--json", "action-item", "create", blank])
assert result.exit_code == 1
assert not route.called
def test_action_item_create_rejects_oversized_description(authed_profile, respx_mock, cli_runner) -> None:
route = respx_mock.post("/v1/dev/user/action-items").respond(json={"id": "a1"})
result = cli_runner.invoke(app, ["--json", "action-item", "create", "x" * 501])
assert result.exit_code == 1
assert "Invalid description" in result.output
assert not route.called
def test_action_item_create_accepts_500_char_description(authed_profile, respx_mock, cli_runner) -> None:
route = respx_mock.post("/v1/dev/user/action-items").respond(json={"id": "a1"})
result = cli_runner.invoke(app, ["--json", "action-item", "create", "x" * 500])
assert result.exit_code == 0, result.output
body = json.loads(route.calls.last.request.content)
assert body["description"] == "x" * 500
def test_action_item_create_strips_surrounding_whitespace(authed_profile, respx_mock, cli_runner) -> None:
route = respx_mock.post("/v1/dev/user/action-items").respond(json={"id": "a1"})
result = cli_runner.invoke(app, ["--json", "action-item", "create", " buy milk "])
assert result.exit_code == 0, result.output
body = json.loads(route.calls.last.request.content)
assert body["description"] == "buy milk"
def test_action_item_update_rejects_blank_description(authed_profile, respx_mock, cli_runner) -> None:
route = respx_mock.patch("/v1/dev/user/action-items/a1").respond(json={"id": "a1"})
result = cli_runner.invoke(app, ["--json", "action-item", "update", "a1", "--description", " "])
assert result.exit_code == 1
assert not route.called
def test_action_item_update_rejects_oversized_description(authed_profile, respx_mock, cli_runner) -> None:
route = respx_mock.patch("/v1/dev/user/action-items/a1").respond(json={"id": "a1"})
result = cli_runner.invoke(app, ["--json", "action-item", "update", "a1", "--description", "x" * 501])
assert result.exit_code == 1
assert "Invalid description" in result.output
assert not route.called
def test_action_item_list_invalid_date_range_raises_usage_error(authed_profile, cli_runner) -> None:
result = cli_runner.invoke(
app,
["action-item", "list", "--start-date", "2026-10-01T00:00:00Z", "--end-date", "2026-09-01T00:00:00Z"],
)
assert result.exit_code == 1
assert "Invalid date range" in result.stderr
def test_action_item_list_mixed_timezone_date_range_raises_usage_error(authed_profile, cli_runner) -> None:
result = cli_runner.invoke(
app,
["action-item", "list", "--start-date", "2026-10-02", "--end-date", "2026-10-01T00:00:00Z"],
)
assert result.exit_code == 1
assert "Invalid date range" in result.stderr
def test_action_item_list_handles_non_list_payload(authed_profile, respx_mock, cli_runner) -> None:
respx_mock.get("/v1/dev/user/action-items").respond(json={"detail": "Unexpected envelope"})
result = cli_runner.invoke(app, ["action-item", "list"])
assert result.exit_code == 0
assert "Unexpected API response envelope" in result.stderr
def test_action_item_list_handles_non_dict_elements(authed_profile, respx_mock, cli_runner) -> None:
respx_mock.get("/v1/dev/user/action-items").respond(json=["malformed", 123, {"id": "valid1", "description": "ok"}])
result = cli_runner.invoke(app, ["action-item", "list"])
assert result.exit_code == 0
assert "valid1" in result.stdout
def test_action_item_get_handles_non_list_page(authed_profile, respx_mock, cli_runner) -> None:
respx_mock.get("/v1/dev/user/action-items").respond(json={"detail": "malformed page"})
result = cli_runner.invoke(app, ["action-item", "get", "item_xyz"])
assert result.exit_code == 5
assert "not found" in result.stderr.lower()
def test_action_item_get_handles_non_dict_elements_in_page(authed_profile, respx_mock, cli_runner) -> None:
respx_mock.get("/v1/dev/user/action-items").respond(json=["raw_string", {"id": "target", "description": "found"}])
result = cli_runner.invoke(app, ["--json", "action-item", "get", "target"])
assert result.exit_code == 0
assert json.loads(result.stdout)["id"] == "target"
def test_action_item_create_rejects_empty_or_whitespace_description(authed_profile, cli_runner) -> None:
result = cli_runner.invoke(app, ["action-item", "create", " "])
assert result.exit_code == 1
assert "Invalid description" in result.stderr
def test_action_item_update_rejects_empty_or_whitespace_description(authed_profile, cli_runner) -> None:
result = cli_runner.invoke(app, ["action-item", "update", "a1", "--description", " "])
assert result.exit_code == 1
assert "Invalid description" in result.stderr