Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ repos:
- id: check-toml
- id: check-json
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.8
rev: v0.14.14
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
Expand Down
602 changes: 311 additions & 291 deletions poetry.lock

Large diffs are not rendered by default.

24 changes: 12 additions & 12 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "coreason_navigator"
version = "0.2.0"
version = "0.2.1"
description = "coreason-navigator"
authors = ["Gowtham A Rao <gowtham.rao@coreason.ai>"]
license = "Prosperity-3.0"
Expand All @@ -10,23 +10,23 @@ packages = [{include = "coreason_navigator", from = "src"}]
[tool.poetry.dependencies]
python = ">=3.12, <3.15"
loguru = "^0.7.2"
playwright = "1.56.0"
playwright = "1.57.0"
pydantic = "^2.12.5"
pillow = "^12.1.0"
readability-lxml = "^0.8.4.1"
markdownify = "^1.2.2"
fastmcp = "^2.14.3"
fastmcp = "^2.14.4"
anyio = "^4.12.1"
coreason-identity = "^0.1.0"
httpx = ">=0.28.1"
aiofiles = "^23.2.1"
coreason-identity = "^0.4.2"
httpx = "^0.28.1"
aiofiles = "^25.1.0"

[tool.poetry.group.dev.dependencies]
types-aiofiles = "^23.2.0.0"
pytest = "^8.2.2"
ruff = "^0.4.8"
pre-commit = "^3.7.1"
pytest-cov = "^5.0.0"
types-aiofiles = "^25.1.0"
pytest = "^9.0.2"
ruff = "^0.14.14"
pre-commit = "^4.5.1"
pytest-cov = "^7.0.0"
mkdocs = "^1.6.0"
mkdocs-material = "^9.5.26"
pytest-playwright = "^0.7.2"
Expand All @@ -38,7 +38,7 @@ build-backend = "poetry.core.masonry.api"

[project]
name = "coreason_navigator"
version = "0.2.0"
version = "0.2.1"
description = "coreason-navigator"
readme = "README.md"
requires-python = ">=3.11"
Expand Down
4 changes: 2 additions & 2 deletions src/coreason_navigator/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ async def navigate(self, url: str, *, context: UserContext) -> BrowserState:

logger.info(
"Navigating to URL",
user_id=context.sub,
user_id=context.user_id,
url=url,
)

Expand Down Expand Up @@ -279,7 +279,7 @@ async def perform_action(self, action: BrowserAction, *, context: UserContext) -

logger.debug(
"Executing browser interaction",
user_id=context.sub,
user_id=context.user_id,
action=type(action).__name__,
)

Expand Down
2 changes: 2 additions & 0 deletions src/coreason_navigator/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
async def run_maps(args: argparse.Namespace) -> None:
"""Executes the Maps command logic."""
context = UserContext(
user_id="cli-user",
sub="cli-user",
email="cli-user@example.com",
permissions=["system"],
Expand Down Expand Up @@ -61,6 +62,7 @@ async def run_maps(args: argparse.Namespace) -> None:
def run_serve(args: argparse.Namespace) -> None:
"""Executes the serve command logic."""
context = UserContext(
user_id="system-server",
sub="system-server",
email="system-server@example.com",
permissions=["system"],
Expand Down
1 change: 1 addition & 0 deletions src/coreason_navigator/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def get_server_context() -> UserContext:
if _server_context is None:
# Create a default context for MCP server usage when not initialized via main.py
_server_context = UserContext(
user_id="mcp-server-default",
sub="mcp-server-default",
email="mcp-server-default@example.com",
permissions=["system"],
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@

@pytest.fixture
def user_context() -> UserContext:
return UserContext(sub="test-user", email="test@example.com", permissions=["tester"])
return UserContext(user_id="test-user", sub="test-user", email="test@example.com", permissions=["tester"])
1 change: 1 addition & 0 deletions tests/test_content_extraction.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import pytest
from coreason_identity.models import UserContext

from coreason_navigator.driver import PlaywrightNavigatorAsync

# Sample HTML with noise and content
Expand Down
3 changes: 2 additions & 1 deletion tests/test_domain_allowlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@

import pytest
from coreason_identity.models import UserContext
from playwright.async_api import Error as PlaywrightError

from coreason_navigator.driver import PlaywrightNavigatorAsync
from coreason_navigator.exceptions import DomainNotAllowedError
from playwright.async_api import Error as PlaywrightError


@pytest.mark.asyncio
Expand Down
1 change: 1 addition & 0 deletions tests/test_driver_complex.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import pytest
from coreason_identity.models import UserContext

from coreason_navigator.driver import PlaywrightNavigatorAsync
from coreason_navigator.types import (
ClickAction,
Expand Down
1 change: 1 addition & 0 deletions tests/test_driver_complex_scenarios.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import pytest
from coreason_identity.models import UserContext

from coreason_navigator.driver import PlaywrightNavigatorAsync
from coreason_navigator.types import ClickAction, ScrollAction

Expand Down
1 change: 1 addition & 0 deletions tests/test_driver_interactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import pytest
from coreason_identity.models import UserContext

from coreason_navigator.driver import PlaywrightNavigatorAsync
from coreason_navigator.types import (
ClickAction,
Expand Down
1 change: 1 addition & 0 deletions tests/test_driver_navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import pytest
from coreason_identity.models import UserContext

from coreason_navigator.driver import PlaywrightNavigatorAsync
from coreason_navigator.types import BrowserState

Expand Down
1 change: 1 addition & 0 deletions tests/test_driver_som.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from coreason_navigator.driver import PlaywrightNavigatorAsync
from coreason_navigator.types import ElementBoundingBox

Expand Down
3 changes: 2 additions & 1 deletion tests/test_edge_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
# Source Code: https://github.com/CoReason-AI/coreason_navigator

import pytest
from pydantic import ValidationError

from coreason_navigator import BrowserState, ElementBoundingBox, PlaywrightNavigatorAsync
from coreason_navigator.types import ClickAction, TypeAction
from pydantic import ValidationError


@pytest.mark.asyncio
Expand Down
3 changes: 2 additions & 1 deletion tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from unittest.mock import AsyncMock, patch

import pytest

from coreason_navigator.main import main, run_maps, run_serve


Expand All @@ -28,7 +29,7 @@ async def test_run_maps_navigate() -> None:

assert request["command"] == "navigate"
assert request["url"] == "http://example.com"
assert context.sub == "cli-user"
assert context.user_id == "cli-user"


@pytest.mark.asyncio
Expand Down
29 changes: 15 additions & 14 deletions tests/test_mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import pytest
from coreason_identity.models import UserContext

from coreason_navigator import server
from coreason_navigator.types import BrowserState, ClickAction, ElementBoundingBox

Expand Down Expand Up @@ -354,7 +355,7 @@ async def test_coordinate_click_over_id(reset_singleton: None, mock_navigator: M
@pytest.mark.asyncio
async def test_handle_request_navigate(reset_singleton: None, mock_navigator: MagicMock) -> None:
"""Test handle_request navigate."""
context = UserContext(sub="cli", email="test@test.com", permissions=[])
context = UserContext(user_id="cli", sub="cli", email="test@test.com", permissions=[])
res = await server.handle_request({"command": "navigate", "url": "http://test.com"}, context)
assert "Navigated to" in res
mock_navigator.navigate.assert_called_with("http://test.com", context=context)
Expand All @@ -363,7 +364,7 @@ async def test_handle_request_navigate(reset_singleton: None, mock_navigator: Ma
@pytest.mark.asyncio
async def test_handle_request_navigate_missing_url(reset_singleton: None, mock_navigator: MagicMock) -> None:
"""Test handle_request navigate missing url."""
context = UserContext(sub="cli", email="test@test.com", permissions=[])
context = UserContext(user_id="cli", sub="cli", email="test@test.com", permissions=[])
res = await server.handle_request({"command": "navigate"}, context)
assert "Error: URL required for navigation" in res

Expand All @@ -374,7 +375,7 @@ async def test_handle_request_click_coordinates_direct(reset_singleton: None, mo
Test passing explicit coordinates to handle_request directly.
This ensures the coordinate parsing logic in server.py is covered.
"""
context = UserContext(sub="cli", email="test@test.com", permissions=[])
context = UserContext(user_id="cli", sub="cli", email="test@test.com", permissions=[])
# Pass raw list as expected from MCP/JSON
await server.handle_request({"command": "interact", "action": "click", "coordinate": [12.5, 34.2]}, context)
args, kwargs = mock_navigator.perform_action.call_args
Expand All @@ -387,7 +388,7 @@ async def test_handle_request_click_coordinates_direct(reset_singleton: None, mo
@pytest.mark.asyncio
async def test_handle_request_interact_click(reset_singleton: None, mock_navigator: MagicMock) -> None:
"""Test handle_request interact click."""
context = UserContext(sub="cli", email="test@test.com", permissions=[])
context = UserContext(user_id="cli", sub="cli", email="test@test.com", permissions=[])
res = await server.handle_request({"command": "interact", "action": "click", "target_id": "1"}, context)
assert "Interaction complete" in res
args, kwargs = mock_navigator.perform_action.call_args
Expand All @@ -399,7 +400,7 @@ async def test_handle_request_interact_click(reset_singleton: None, mock_navigat
@pytest.mark.asyncio
async def test_handle_request_interact_type(reset_singleton: None, mock_navigator: MagicMock) -> None:
"""Test handle_request interact type."""
context = UserContext(sub="cli", email="test@test.com", permissions=[])
context = UserContext(user_id="cli", sub="cli", email="test@test.com", permissions=[])
res = await server.handle_request({"command": "interact", "action": "type", "text": "hello"}, context)
assert "Interaction complete" in res
args, kwargs = mock_navigator.perform_action.call_args
Expand All @@ -411,15 +412,15 @@ async def test_handle_request_interact_type(reset_singleton: None, mock_navigato
@pytest.mark.asyncio
async def test_handle_request_interact_type_missing_text(reset_singleton: None, mock_navigator: MagicMock) -> None:
"""Test handle_request interact type missing text."""
context = UserContext(sub="cli", email="test@test.com", permissions=[])
context = UserContext(user_id="cli", sub="cli", email="test@test.com", permissions=[])
res = await server.handle_request({"command": "interact", "action": "type"}, context)
assert "Error: Text required" in res


@pytest.mark.asyncio
async def test_handle_request_interact_scroll(reset_singleton: None, mock_navigator: MagicMock) -> None:
"""Test handle_request interact scroll."""
context = UserContext(sub="cli", email="test@test.com", permissions=[])
context = UserContext(user_id="cli", sub="cli", email="test@test.com", permissions=[])
res = await server.handle_request({"command": "interact", "action": "scroll", "amount": 200}, context)
assert "Interaction complete" in res
args, kwargs = mock_navigator.perform_action.call_args
Expand All @@ -431,7 +432,7 @@ async def test_handle_request_interact_scroll(reset_singleton: None, mock_naviga
@pytest.mark.asyncio
async def test_handle_request_interact_wait(reset_singleton: None, mock_navigator: MagicMock) -> None:
"""Test handle_request interact wait."""
context = UserContext(sub="cli", email="test@test.com", permissions=[])
context = UserContext(user_id="cli", sub="cli", email="test@test.com", permissions=[])
res = await server.handle_request({"command": "interact", "action": "wait"}, context)
assert "Interaction complete" in res
args, kwargs = mock_navigator.perform_action.call_args
Expand All @@ -442,7 +443,7 @@ async def test_handle_request_interact_wait(reset_singleton: None, mock_navigato
@pytest.mark.asyncio
async def test_handle_request_interact_goto(reset_singleton: None, mock_navigator: MagicMock) -> None:
"""Test handle_request interact goto."""
context = UserContext(sub="cli", email="test@test.com", permissions=[])
context = UserContext(user_id="cli", sub="cli", email="test@test.com", permissions=[])
res = await server.handle_request({"command": "interact", "action": "goto", "url": "http://foo.com"}, context)
assert "Interaction complete" in res
args, kwargs = mock_navigator.perform_action.call_args
Expand All @@ -454,15 +455,15 @@ async def test_handle_request_interact_goto(reset_singleton: None, mock_navigato
@pytest.mark.asyncio
async def test_handle_request_interact_goto_missing_url(reset_singleton: None, mock_navigator: MagicMock) -> None:
"""Test handle_request interact goto missing url."""
context = UserContext(sub="cli", email="test@test.com", permissions=[])
context = UserContext(user_id="cli", sub="cli", email="test@test.com", permissions=[])
res = await server.handle_request({"command": "interact", "action": "goto"}, context)
assert "Error: URL required" in res


@pytest.mark.asyncio
async def test_handle_request_interact_screenshot(reset_singleton: None, mock_navigator: MagicMock) -> None:
"""Test handle_request interact screenshot."""
context = UserContext(sub="cli", email="test@test.com", permissions=[])
context = UserContext(user_id="cli", sub="cli", email="test@test.com", permissions=[])
res = await server.handle_request({"command": "interact", "action": "screenshot"}, context)
assert "Interaction complete" in res
args, kwargs = mock_navigator.perform_action.call_args
Expand All @@ -473,15 +474,15 @@ async def test_handle_request_interact_screenshot(reset_singleton: None, mock_na
@pytest.mark.asyncio
async def test_handle_request_interact_unknown_action(reset_singleton: None, mock_navigator: MagicMock) -> None:
"""Test handle_request interact unknown action."""
context = UserContext(sub="cli", email="test@test.com", permissions=[])
context = UserContext(user_id="cli", sub="cli", email="test@test.com", permissions=[])
res = await server.handle_request({"command": "interact", "action": "fly"}, context)
assert "Error: Unknown action type" in res


@pytest.mark.asyncio
async def test_handle_request_extract(reset_singleton: None, mock_navigator: MagicMock) -> None:
"""Test handle_request extract."""
context = UserContext(sub="cli", email="test@test.com", permissions=[])
context = UserContext(user_id="cli", sub="cli", email="test@test.com", permissions=[])
res = await server.handle_request({"command": "extract", "format": "html"}, context)
assert res == "# Fake Markdown Content"
mock_navigator.extract_content.assert_called_with(format="html", context=context)
Expand All @@ -490,6 +491,6 @@ async def test_handle_request_extract(reset_singleton: None, mock_navigator: Mag
@pytest.mark.asyncio
async def test_handle_request_unknown(reset_singleton: None) -> None:
"""Test handle_request unknown command."""
context = UserContext(sub="cli", email="test@test.com", permissions=[])
context = UserContext(user_id="cli", sub="cli", email="test@test.com", permissions=[])
res = await server.handle_request({"command": "dance"}, context)
assert "Error: Unknown command" in res
3 changes: 2 additions & 1 deletion tests/test_perception_overlay.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
import base64
import io

from PIL import Image

from coreason_navigator.perception.overlay import draw_som_overlay
from coreason_navigator.types import ElementBoundingBox
from PIL import Image


def test_draw_som_overlay_creates_valid_image() -> None:
Expand Down
3 changes: 2 additions & 1 deletion tests/test_perception_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
# Source Code: https://github.com/CoReason-AI/coreason_navigator

import pytest
from playwright.async_api import async_playwright

from coreason_navigator.driver import PlaywrightNavigatorAsync
from coreason_navigator.perception.ax_tree import get_interactive_elements
from playwright.async_api import async_playwright


@pytest.mark.asyncio
Expand Down
1 change: 1 addition & 0 deletions tests/test_pii_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import pytest
from coreason_identity.models import UserContext

from coreason_navigator.driver import PlaywrightNavigatorAsync
from coreason_navigator.guardrails.pii import PIIViolationError, RegexPIIGuard
from coreason_navigator.types import TypeAction
Expand Down
1 change: 1 addition & 0 deletions tests/test_rate_limiting.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import pytest
from coreason_identity.models import UserContext

from coreason_navigator.driver import PlaywrightNavigatorAsync
from coreason_navigator.types import ScreenshotAction

Expand Down
5 changes: 3 additions & 2 deletions tests/test_rate_limiting_edge_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import pytest
from coreason_identity.models import UserContext

from coreason_navigator.driver import PlaywrightNavigatorAsync
from coreason_navigator.types import ScreenshotAction, WaitAction

Expand Down Expand Up @@ -134,5 +135,5 @@ async def cancel_me() -> None:
duration_since_t0 = end_3 - t0

# It should respect the rate limit from T0
# Use 0.15 tolerance to account for potential slight variability even with WaitAction
assert duration_since_t0 >= rate_limit - 0.15, "Rate limit violated after cancellation"
# Use 0.2 tolerance to account for potential slight variability even with WaitAction
assert duration_since_t0 >= rate_limit - 0.2, "Rate limit violated after cancellation"
1 change: 1 addition & 0 deletions tests/test_scaffold.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
# Source Code: https://github.com/CoReason-AI/coreason_navigator

import pytest

from coreason_navigator import BrowserState, ElementBoundingBox, PlaywrightNavigatorAsync


Expand Down
Loading