diff --git a/.github/workflows/unittest.yaml b/.github/workflows/unittest.yaml index 7e5c9fb..5e36cb9 100644 --- a/.github/workflows/unittest.yaml +++ b/.github/workflows/unittest.yaml @@ -23,7 +23,23 @@ jobs: uv pip install -e ".[dev]" uv pip install -e ".[test]" - - name: Run test cases + - name: Run test cases with coverage run: | source .venv/bin/activate - TAVILY_API_KEY=mock-key make test \ No newline at end of file + TAVILY_API_KEY=mock-key make coverage + + - name: Generate HTML Coverage Report + run: | + source .venv/bin/activate + python -m coverage html -d coverage_html + + - name: Upload Coverage Report + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage_html/ + + - name: Display Coverage Summary + run: | + source .venv/bin/activate + python -m coverage report \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3d08e1f..5dc0a3a 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ conf.yaml .idea/ .langgraph_api/ +# coverage report +coverage.xml +coverage/ diff --git a/Makefile b/Makefile index b1bcada..3b662a6 100644 --- a/Makefile +++ b/Makefile @@ -19,4 +19,4 @@ langgraph-dev: uvx --refresh --from "langgraph-cli[inmem]" --with-editable . --python 3.12 langgraph dev --allow-blocking coverage: - uv run pytest --cov=src tests/ --cov-report=term-missing + uv run pytest --cov=src tests/ --cov-report=term-missing --cov-report=xml diff --git a/README.md b/README.md index 374337c..2311dfa 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![DeepWiki](https://img.shields.io/badge/DeepWiki-bytedance%2Fdeer--flow-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/bytedance/deer-flow) +[![DeepWiki](https://img.shields.io/badge/DeepWiki-bytedance%2Fdeer--flow-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McCcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/bytedance/deer-flow) diff --git a/pyproject.toml b/pyproject.toml index 08e32a4..4f3ae92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,9 @@ filterwarnings = [ "ignore::UserWarning", ] +[tool.coverage.report] +fail_under = 25 + [tool.hatch.build.targets.wheel] packages = ["src"] diff --git a/test_fix.py b/test_fix.py new file mode 100644 index 0000000..f7054c6 --- /dev/null +++ b/test_fix.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +""" +This script manually patches sys.modules to fix the LLM import issue +so that tests can run without requiring LLM configuration. +""" + +import sys +from unittest.mock import MagicMock + +# Create mocks +mock_llm = MagicMock() +mock_llm.invoke.return_value = "Mock LLM response" + +# Create a mock module for llm.py +mock_llm_module = MagicMock() +mock_llm_module.get_llm_by_type = lambda llm_type: mock_llm +mock_llm_module.basic_llm = mock_llm +mock_llm_module._create_llm_use_conf = lambda llm_type, conf: mock_llm + +# Set the mock module +sys.modules["src.llms.llm"] = mock_llm_module + +print("Successfully patched LLM module. You can now run your tests.") +print("Example: uv run pytest tests/test_types.py -v") diff --git a/tests/test_state.py b/tests/test_state.py new file mode 100644 index 0000000..a24c91c --- /dev/null +++ b/tests/test_state.py @@ -0,0 +1,131 @@ +import pytest +import sys +import os +from typing import Annotated, List, Optional + +# Import MessagesState directly from langgraph rather than through our application +from langgraph.graph import MessagesState + + +# Create stub versions of Plan/Step/StepType to avoid dependencies +class StepType: + RESEARCH = "research" + PROCESSING = "processing" + + +class Step: + def __init__(self, need_web_search, title, description, step_type): + self.need_web_search = need_web_search + self.title = title + self.description = description + self.step_type = step_type + + +class Plan: + def __init__(self, locale, has_enough_context, thought, title, steps): + self.locale = locale + self.has_enough_context = has_enough_context + self.thought = thought + self.title = title + self.steps = steps + + +# Import the actual State class by loading the module directly +# This avoids the cascade of imports that would normally happen +def load_state_class(): + # Get the absolute path to the types.py file + src_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src")) + types_path = os.path.join(src_dir, "graph", "types.py") + + # Create a namespace for the module + import types + + module_name = "src.graph.types_direct" + spec = types.ModuleType(module_name) + + # Add the module to sys.modules to avoid import loops + sys.modules[module_name] = spec + + # Set up the namespace with required imports + spec.__dict__["operator"] = __import__("operator") + spec.__dict__["Annotated"] = Annotated + spec.__dict__["MessagesState"] = MessagesState + spec.__dict__["Plan"] = Plan + + # Execute the module code + with open(types_path, "r") as f: + module_code = f.read() + + exec(module_code, spec.__dict__) + + # Return the State class + return spec.State + + +# Load the actual State class +State = load_state_class() + + +def test_state_initialization(): + """Test that State class has correct default attribute definitions.""" + # Test that the class has the expected attribute definitions + assert State.locale == "en-US" + assert State.observations == [] + assert State.plan_iterations == 0 + assert State.current_plan is None + assert State.final_report == "" + assert State.auto_accepted_plan is False + assert State.enable_background_investigation is True + assert State.background_investigation_results is None + + # Verify state initialization + state = State(messages=[]) + assert "messages" in state + + # Without explicitly passing attributes, they're not in the state + assert "locale" not in state + assert "observations" not in state + + +def test_state_with_custom_values(): + """Test that State can be initialized with custom values.""" + test_step = Step( + need_web_search=True, + title="Test Step", + description="Step description", + step_type=StepType.RESEARCH, + ) + + test_plan = Plan( + locale="en-US", + has_enough_context=False, + thought="Test thought", + title="Test Plan", + steps=[test_step], + ) + + # Initialize state with custom values and required messages field + state = State( + messages=[], + locale="fr-FR", + observations=["Observation 1"], + plan_iterations=2, + current_plan=test_plan, + final_report="Test report", + auto_accepted_plan=True, + enable_background_investigation=False, + background_investigation_results="Test results", + ) + + # Access state keys - these are explicitly initialized + assert state["locale"] == "fr-FR" + assert state["observations"] == ["Observation 1"] + assert state["plan_iterations"] == 2 + assert state["current_plan"].title == "Test Plan" + assert state["current_plan"].thought == "Test thought" + assert len(state["current_plan"].steps) == 1 + assert state["current_plan"].steps[0].title == "Test Step" + assert state["final_report"] == "Test report" + assert state["auto_accepted_plan"] is True + assert state["enable_background_investigation"] is False + assert state["background_investigation_results"] == "Test results"