open-notebook/tests/test_graphs.py
Luis Novo 9bdfd99f1b
feat: simplify reverse proxy configuration with Next.js rewrites (#213)
* feat: simplify reverse proxy configuration with Next.js rewrites

Add Next.js API rewrites to proxy /api/* requests internally from port 8502
to the FastAPI backend on port 5055. This eliminates the need for complex
reverse proxy configurations with multiple upstreams and location blocks.

Changes:
- Add rewrites to next.config.ts proxying /api/* to INTERNAL_API_URL
- Introduce INTERNAL_API_URL env var (defaults to http://localhost:5055)
- Update supervisord configs to pass INTERNAL_API_URL to Next.js
- Document INTERNAL_API_URL in .env.example with usage examples
- Add simplified reverse proxy examples for nginx, Traefik, Caddy, Coolify
- Update README architecture diagram to show internal proxying
- Add explanatory comments to _config route handler

Benefits:
- Reduces reverse proxy config from 12 lines to 3 (75% reduction)
- Single-port deployment (8502 only) for 95% of use cases
- Zero breaking changes - backward compatible with existing setups
- Zero performance overhead (validated through testing)
- Preserves proxy headers (X-Forwarded-*) for rate limiting/SSL

Resolves: #179
Related: OSS-321

* fix: rename _config to config to fix production routing

CRITICAL BUG FIX: The /_config endpoint has never worked in production builds
because Next.js treats folders starting with underscore as "private folders"
and excludes them from routing entirely.

This endpoint is critical for:
- Providing API_URL to the browser at runtime
- Enabling zero-config deployments with auto-detection
- Supporting reverse proxy scenarios where API URL differs from frontend URL

Changes:
- Rename frontend/src/app/_config/ → frontend/src/app/config/
- Update client code references (/_config → /config)
- Update documentation with correct endpoint path
- Bump version to 1.1.0 (minor version for new rewrites feature + bug fix)

Impact:
- Runtime configuration now works in production builds
- /config returns {"apiUrl":"http://localhost:5055"} correctly
- Auto-detection for reverse proxy deployments now functional

Related: #179, OSS-321

* fix: resolve React hook exhaustive-deps warning in AddExistingSourceDialog

Wrap performSearch function in useCallback to properly memoize it and satisfy
React Hook exhaustive-deps rule. This prevents unnecessary re-renders and
ensures the useEffect dependency array is correctly specified.

Changes:
- Import useCallback from React
- Wrap performSearch with useCallback([debouncedSearchQuery, allSources])
- Add performSearch to useEffect dependency array

* final fixes
2025-10-24 11:24:14 -03:00

158 lines
5.1 KiB
Python

"""
Unit tests for the open_notebook.graphs module.
This test suite focuses on testing graph structures, tools, and validation
without heavy mocking of the actual processing logic.
"""
from datetime import datetime
import pytest
from open_notebook.graphs.prompt import PatternChainState, graph
from open_notebook.graphs.tools import get_current_timestamp
from open_notebook.graphs.transformation import (
TransformationState,
run_transformation,
)
from open_notebook.graphs.transformation import (
graph as transformation_graph,
)
# ============================================================================
# TEST SUITE 1: Graph Tools
# ============================================================================
class TestGraphTools:
"""Test suite for graph tool definitions."""
def test_get_current_timestamp_format(self):
"""Test timestamp tool returns correct format."""
timestamp = get_current_timestamp.func()
assert isinstance(timestamp, str)
assert len(timestamp) == 14 # YYYYMMDDHHmmss format
assert timestamp.isdigit()
def test_get_current_timestamp_validity(self):
"""Test timestamp represents valid datetime."""
timestamp = get_current_timestamp.func()
# Parse it back to datetime to verify validity
year = int(timestamp[0:4])
month = int(timestamp[4:6])
day = int(timestamp[6:8])
hour = int(timestamp[8:10])
minute = int(timestamp[10:12])
second = int(timestamp[12:14])
# Should be valid date components
assert 2020 <= year <= 2100
assert 1 <= month <= 12
assert 1 <= day <= 31
assert 0 <= hour <= 23
assert 0 <= minute <= 59
assert 0 <= second <= 59
# Should parse as datetime
dt = datetime.strptime(timestamp, "%Y%m%d%H%M%S")
assert isinstance(dt, datetime)
def test_get_current_timestamp_is_tool(self):
"""Test that function is properly decorated as a tool."""
# Check it has tool attributes
assert hasattr(get_current_timestamp, "name")
assert hasattr(get_current_timestamp, "description")
# ============================================================================
# TEST SUITE 2: Prompt Graph State
# ============================================================================
class TestPromptGraph:
"""Test suite for prompt pattern chain graph."""
def test_pattern_chain_state_structure(self):
"""Test PatternChainState structure and fields."""
state = PatternChainState(
prompt="Test prompt",
parser=None,
input_text="Test input",
output=""
)
assert state["prompt"] == "Test prompt"
assert state["parser"] is None
assert state["input_text"] == "Test input"
assert state["output"] == ""
def test_prompt_graph_compilation(self):
"""Test that prompt graph compiles correctly."""
assert graph is not None
# Graph should have the expected structure
assert hasattr(graph, "invoke")
assert hasattr(graph, "ainvoke")
# ============================================================================
# TEST SUITE 3: Transformation Graph
# ============================================================================
class TestTransformationGraph:
"""Test suite for transformation graph workflows."""
def test_transformation_state_structure(self):
"""Test TransformationState structure and fields."""
from unittest.mock import MagicMock
from open_notebook.domain.notebook import Source
from open_notebook.domain.transformation import Transformation
mock_source = MagicMock(spec=Source)
mock_transformation = MagicMock(spec=Transformation)
state = TransformationState(
input_text="Test text",
source=mock_source,
transformation=mock_transformation,
output=""
)
assert state["input_text"] == "Test text"
assert state["source"] == mock_source
assert state["transformation"] == mock_transformation
assert state["output"] == ""
@pytest.mark.asyncio
async def test_run_transformation_assertion_no_content(self):
"""Test transformation raises assertion with no content."""
from unittest.mock import MagicMock
from open_notebook.domain.transformation import Transformation
mock_transformation = MagicMock(spec=Transformation)
state = {
"input_text": None,
"transformation": mock_transformation,
"source": None
}
config = {"configurable": {"model_id": None}}
with pytest.raises(AssertionError, match="No content to transform"):
await run_transformation(state, config)
def test_transformation_graph_compilation(self):
"""Test that transformation graph compiles correctly."""
assert transformation_graph is not None
assert hasattr(transformation_graph, "invoke")
assert hasattr(transformation_graph, "ainvoke")
if __name__ == "__main__":
pytest.main([__file__, "-v"])