From f802f3ad6631163d2a72feab5e39b7061d7fe496 Mon Sep 17 00:00:00 2001 From: dgenio Date: Tue, 3 Mar 2026 23:57:46 +0000 Subject: [PATCH 01/10] refactor: extract shared test fixtures and schemas into conftest.py Move duplicated Pydantic schemas (NumberInput, ValueOutput, ValueInput, FormattedOutput), helper functions (_double_fn, _add_ten_fn, _format_fn), and pytest fixtures (double_tool, add_ten_tool, format_tool, linear_flow, executor) from test_flow_execution.py into tests/conftest.py. Remove tests/__init__.py so pytest auto-adds the test directory to sys.path, enabling direct imports from conftest. Part of #6. --- tests/conftest.py | 115 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 tests/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..ee4dd83 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,115 @@ +"""Shared test fixtures for ChainWeaver.""" + +from __future__ import annotations + +import pytest +from pydantic import BaseModel + +from chainweaver.executor import FlowExecutor +from chainweaver.flow import Flow, FlowStep +from chainweaver.registry import FlowRegistry +from chainweaver.tools import Tool + +# --------------------------------------------------------------------------- +# Shared Pydantic schemas +# --------------------------------------------------------------------------- + + +class NumberInput(BaseModel): + number: int + + +class ValueOutput(BaseModel): + value: int + + +class ValueInput(BaseModel): + value: int + + +class FormattedOutput(BaseModel): + result: str + + +# --------------------------------------------------------------------------- +# Shared tool functions +# --------------------------------------------------------------------------- + + +def _double_fn(inp: NumberInput) -> dict: + return {"value": inp.number * 2} + + +def _add_ten_fn(inp: ValueInput) -> dict: + return {"value": inp.value + 10} + + +def _format_fn(inp: ValueInput) -> dict: + return {"result": f"Final value: {inp.value}"} + + +# --------------------------------------------------------------------------- +# Shared fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture() +def double_tool() -> Tool: + return Tool( + name="double", + description="Doubles a number.", + input_schema=NumberInput, + output_schema=ValueOutput, + fn=_double_fn, + ) + + +@pytest.fixture() +def add_ten_tool() -> Tool: + return Tool( + name="add_ten", + description="Adds 10 to a value.", + input_schema=ValueInput, + output_schema=ValueOutput, + fn=_add_ten_fn, + ) + + +@pytest.fixture() +def format_tool() -> Tool: + return Tool( + name="format_result", + description="Formats a value.", + input_schema=ValueInput, + output_schema=FormattedOutput, + fn=_format_fn, + ) + + +@pytest.fixture() +def linear_flow() -> Flow: + return Flow( + name="double_add_format", + description="Doubles a number, adds 10, and formats the result.", + steps=[ + FlowStep(tool_name="double", input_mapping={"number": "number"}), + FlowStep(tool_name="add_ten", input_mapping={"value": "value"}), + FlowStep(tool_name="format_result", input_mapping={"value": "value"}), + ], + ) + + +@pytest.fixture() +def executor( + linear_flow: Flow, + double_tool: Tool, + add_ten_tool: Tool, + format_tool: Tool, +) -> FlowExecutor: + registry = FlowRegistry() + registry.register_flow(linear_flow) + ex = FlowExecutor(registry=registry) + ex.register_tool(double_tool) + ex.register_tool(add_ten_tool) + ex.register_tool(format_tool) + return ex From 66fa4143fe59a1eaaa03c4393e8cd55b597ffa2b Mon Sep 17 00:00:00 2001 From: dgenio Date: Tue, 3 Mar 2026 23:58:13 +0000 Subject: [PATCH 02/10] refactor: remove tests/__init__.py for conftest import support Remove the empty tests/__init__.py so that pytest adds the test directory to sys.path, allowing test modules to import from conftest.py directly. --- tests/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/__init__.py diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 From 2056ebfbe8589c9c47955f6bb7bba19b6f9ecc28 Mon Sep 17 00:00:00 2001 From: dgenio Date: Tue, 3 Mar 2026 23:58:24 +0000 Subject: [PATCH 03/10] test: add edge-case tests and deduplicate fixtures in test_flow_execution - Replace inline schemas/fixtures with imports from conftest.py. - Add TestSingleStepFlow: one-step flow with input_mapping. - Add TestContextAccumulation: verify all step outputs in final_output. - Add TestToolZeroDivisionError: ZeroDivisionError wrapped as FlowExecutionError. - Add TestBoundaryValues: negative numbers, zero, and large negatives. Closes #6 (test_flow_execution.py portion). --- tests/test_flow_execution.py | 241 +++++++++++++++++++++-------------- 1 file changed, 146 insertions(+), 95 deletions(-) diff --git a/tests/test_flow_execution.py b/tests/test_flow_execution.py index 31f1652..9eab74f 100644 --- a/tests/test_flow_execution.py +++ b/tests/test_flow_execution.py @@ -3,6 +3,12 @@ from __future__ import annotations import pytest +from conftest import ( + FormattedOutput, + NumberInput, + ValueOutput, + _double_fn, +) from pydantic import BaseModel, ValidationError from chainweaver.exceptions import ( @@ -17,101 +23,6 @@ from chainweaver.registry import FlowRegistry from chainweaver.tools import Tool -# --------------------------------------------------------------------------- -# Shared fixtures -# --------------------------------------------------------------------------- - - -class NumberInput(BaseModel): - number: int - - -class ValueOutput(BaseModel): - value: int - - -class ValueInput(BaseModel): - value: int - - -class FormattedOutput(BaseModel): - result: str - - -def _double_fn(inp: NumberInput) -> dict: - return {"value": inp.number * 2} - - -def _add_ten_fn(inp: ValueInput) -> dict: - return {"value": inp.value + 10} - - -def _format_fn(inp: ValueInput) -> dict: - return {"result": f"Final value: {inp.value}"} - - -@pytest.fixture() -def double_tool() -> Tool: - return Tool( - name="double", - description="Doubles a number.", - input_schema=NumberInput, - output_schema=ValueOutput, - fn=_double_fn, - ) - - -@pytest.fixture() -def add_ten_tool() -> Tool: - return Tool( - name="add_ten", - description="Adds 10 to a value.", - input_schema=ValueInput, - output_schema=ValueOutput, - fn=_add_ten_fn, - ) - - -@pytest.fixture() -def format_tool() -> Tool: - return Tool( - name="format_result", - description="Formats a value.", - input_schema=ValueInput, - output_schema=FormattedOutput, - fn=_format_fn, - ) - - -@pytest.fixture() -def linear_flow() -> Flow: - return Flow( - name="double_add_format", - description="Doubles a number, adds 10, and formats the result.", - steps=[ - FlowStep(tool_name="double", input_mapping={"number": "number"}), - FlowStep(tool_name="add_ten", input_mapping={"value": "value"}), - FlowStep(tool_name="format_result", input_mapping={"value": "value"}), - ], - ) - - -@pytest.fixture() -def executor( - linear_flow: Flow, - double_tool: Tool, - add_ten_tool: Tool, - format_tool: Tool, -) -> FlowExecutor: - registry = FlowRegistry() - registry.register_flow(linear_flow) - ex = FlowExecutor(registry=registry) - ex.register_tool(double_tool) - ex.register_tool(add_ten_tool) - ex.register_tool(format_tool) - return ex - - # --------------------------------------------------------------------------- # Successful execution # --------------------------------------------------------------------------- @@ -659,3 +570,143 @@ class StrictOutput(BaseModel): assert len(result.execution_log) == 1 assert result.execution_log[0].step_index == 0 # len(steps) == 0 assert isinstance(result.execution_log[0].error, SchemaValidationError) + + +# --------------------------------------------------------------------------- +# Single-step flow +# --------------------------------------------------------------------------- + + +class TestSingleStepFlow: + """A flow with exactly one step — simplest chaining case.""" + + def test_single_step_succeeds( + self, + double_tool: Tool, + ) -> None: + flow = Flow( + name="single_step", + description="One-step flow that doubles a number.", + steps=[ + FlowStep(tool_name="double", input_mapping={"number": "number"}), + ], + ) + registry = FlowRegistry() + registry.register_flow(flow) + ex = FlowExecutor(registry=registry) + ex.register_tool(double_tool) + + result = ex.execute_flow("single_step", {"number": 7}) + assert result.success is True + assert result.final_output is not None + assert result.final_output["value"] == 14 + assert len(result.execution_log) == 1 + assert result.execution_log[0].tool_name == "double" + + +# --------------------------------------------------------------------------- +# Context accumulation +# --------------------------------------------------------------------------- + + +class TestContextAccumulation: + """Verify that outputs from *all* steps are merged into final_output.""" + + def test_context_accumulates_all_outputs( + self, + executor: FlowExecutor, + ) -> None: + result = executor.execute_flow("double_add_format", {"number": 5}) + assert result.success is True + assert result.final_output is not None + # Initial input key is preserved. + assert "number" in result.final_output + assert result.final_output["number"] == 5 + # Intermediate key from double/add_ten steps. + assert "value" in result.final_output + assert result.final_output["value"] == 20 + # Final key from format_result step. + assert "result" in result.final_output + assert result.final_output["result"] == "Final value: 20" + + +# --------------------------------------------------------------------------- +# Tool runtime exception: ZeroDivisionError +# --------------------------------------------------------------------------- + + +class TestToolZeroDivisionError: + """A ZeroDivisionError inside a tool fn is wrapped as FlowExecutionError.""" + + def test_zero_division_error_wrapped(self) -> None: + class DivInput(BaseModel): + numerator: int + denominator: int + + class DivOutput(BaseModel): + result: int + + def divide_fn(inp: DivInput) -> dict: + return {"result": inp.numerator // inp.denominator} + + tool = Tool( + name="divide", + description="Integer division.", + input_schema=DivInput, + output_schema=DivOutput, + fn=divide_fn, + ) + flow = Flow( + name="divide_flow", + description="Flow that divides.", + steps=[ + FlowStep( + tool_name="divide", + input_mapping={ + "numerator": "numerator", + "denominator": "denominator", + }, + ) + ], + ) + registry = FlowRegistry() + registry.register_flow(flow) + ex = FlowExecutor(registry=registry) + ex.register_tool(tool) + + result = ex.execute_flow("divide_flow", {"numerator": 10, "denominator": 0}) + assert result.success is False + record = result.execution_log[0] + assert record.success is False + assert isinstance(record.error, FlowExecutionError) + assert "integer division or modulo by zero" in str(record.error) + + +# --------------------------------------------------------------------------- +# Boundary values: negative numbers and zero +# --------------------------------------------------------------------------- + + +class TestBoundaryValues: + """Negative numbers and zero through the double→add→format chain.""" + + def test_negative_input(self, executor: FlowExecutor) -> None: + result = executor.execute_flow("double_add_format", {"number": -3}) + # double(-3) → -6, add_ten(-6) → 4, format(4) → "Final value: 4" + assert result.success is True + assert result.final_output is not None + assert result.final_output["result"] == "Final value: 4" + + def test_zero_input(self, executor: FlowExecutor) -> None: + result = executor.execute_flow("double_add_format", {"number": 0}) + # double(0) → 0, add_ten(0) → 10, format(10) → "Final value: 10" + assert result.success is True + assert result.final_output is not None + assert result.final_output["result"] == "Final value: 10" + + def test_large_negative_input(self, executor: FlowExecutor) -> None: + result = executor.execute_flow("double_add_format", {"number": -1000}) + # double(-1000) → -2000, add_ten(-2000) → -1990 + assert result.success is True + assert result.final_output is not None + assert result.final_output["result"] == "Final value: -1990" From 496c5b0c523adf469a6a113230e0ea3ecc8fb355 Mon Sep 17 00:00:00 2001 From: dgenio Date: Wed, 4 Mar 2026 05:25:51 +0000 Subject: [PATCH 04/10] test: add registry edge-case tests - Add test_empty_registry_returns_none to TestMatchFlowByIntent. - Add TestOverwritePreservesCount: verify len() stays the same after overwrite. Closes #6 (test_registry.py portion). --- tests/test_registry.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_registry.py b/tests/test_registry.py index 868255a..a8d36ac 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -123,3 +123,27 @@ def test_match_is_case_insensitive(self) -> None: registry.register_flow(flow) match = registry.match_flow_by_intent("uppercase") assert match is not None + + def test_empty_registry_returns_none(self) -> None: + """An empty registry has nothing to match.""" + registry = FlowRegistry() + assert registry.match_flow_by_intent("anything") is None + + +# --------------------------------------------------------------------------- +# Overwrite preserves count +# --------------------------------------------------------------------------- + + +class TestOverwritePreservesCount: + def test_register_flow_then_overwrite_preserves_count(self) -> None: + registry = FlowRegistry() + registry.register_flow(_make_flow("keep")) + registry.register_flow(_make_flow("replace_me")) + assert len(registry) == 2 + + new_flow = _make_flow("replace_me") + new_flow.description = "Replaced" + registry.register_flow(new_flow, overwrite=True) + assert len(registry) == 2 + assert registry.get_flow("replace_me").description == "Replaced" From 5f3d298447e37c364d3497bd06fbddd8eb89d226 Mon Sep 17 00:00:00 2001 From: dgenio Date: Wed, 4 Mar 2026 05:26:10 +0000 Subject: [PATCH 05/10] ci: add pytest-cov to dev dependencies Add pytest-cov>=4.0 to [project.optional-dependencies] dev for coverage reporting during local development. --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 986ad2b..1c766e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ dependencies = [ [project.optional-dependencies] dev = [ "pytest>=7.0", + "pytest-cov>=4.0", "ruff>=0.8", ] From d836196ddd8177b670700d8f23abc87ce91dac63 Mon Sep 17 00:00:00 2001 From: dgenio Date: Wed, 4 Mar 2026 05:54:28 +0000 Subject: [PATCH 06/10] test: replace duplicate test_zero_input with test_large_positive_input --- tests/test_flow_execution.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_flow_execution.py b/tests/test_flow_execution.py index 9eab74f..a92ec47 100644 --- a/tests/test_flow_execution.py +++ b/tests/test_flow_execution.py @@ -697,12 +697,12 @@ def test_negative_input(self, executor: FlowExecutor) -> None: assert result.final_output is not None assert result.final_output["result"] == "Final value: 4" - def test_zero_input(self, executor: FlowExecutor) -> None: - result = executor.execute_flow("double_add_format", {"number": 0}) - # double(0) → 0, add_ten(0) → 10, format(10) → "Final value: 10" + def test_large_positive_input(self, executor: FlowExecutor) -> None: + result = executor.execute_flow("double_add_format", {"number": 1000}) + # double(1000) → 2000, add_ten(2000) → 2010, format(2010) → "Final value: 2010" assert result.success is True assert result.final_output is not None - assert result.final_output["result"] == "Final value: 10" + assert result.final_output["result"] == "Final value: 2010" def test_large_negative_input(self, executor: FlowExecutor) -> None: result = executor.execute_flow("double_add_format", {"number": -1000}) From 9d252264aa71ca02266008c5e13ea8cc467087a3 Mon Sep 17 00:00:00 2001 From: dgenio Date: Wed, 4 Mar 2026 06:05:59 +0000 Subject: [PATCH 07/10] refactor: move shared schemas and helpers from conftest to tests/helpers.py --- tests/conftest.py | 48 +++++++----------------------------- tests/helpers.py | 42 +++++++++++++++++++++++++++++++ tests/test_flow_execution.py | 2 +- 3 files changed, 52 insertions(+), 40 deletions(-) create mode 100644 tests/helpers.py diff --git a/tests/conftest.py b/tests/conftest.py index ee4dd83..bcd844a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,51 +3,21 @@ from __future__ import annotations import pytest -from pydantic import BaseModel +from helpers import ( + FormattedOutput, + NumberInput, + ValueInput, + ValueOutput, + _add_ten_fn, + _double_fn, + _format_fn, +) from chainweaver.executor import FlowExecutor from chainweaver.flow import Flow, FlowStep from chainweaver.registry import FlowRegistry from chainweaver.tools import Tool -# --------------------------------------------------------------------------- -# Shared Pydantic schemas -# --------------------------------------------------------------------------- - - -class NumberInput(BaseModel): - number: int - - -class ValueOutput(BaseModel): - value: int - - -class ValueInput(BaseModel): - value: int - - -class FormattedOutput(BaseModel): - result: str - - -# --------------------------------------------------------------------------- -# Shared tool functions -# --------------------------------------------------------------------------- - - -def _double_fn(inp: NumberInput) -> dict: - return {"value": inp.number * 2} - - -def _add_ten_fn(inp: ValueInput) -> dict: - return {"value": inp.value + 10} - - -def _format_fn(inp: ValueInput) -> dict: - return {"result": f"Final value: {inp.value}"} - - # --------------------------------------------------------------------------- # Shared fixtures # --------------------------------------------------------------------------- diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 0000000..7186026 --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,42 @@ +"""Shared Pydantic schemas and helper functions for ChainWeaver tests.""" + +from __future__ import annotations + +from pydantic import BaseModel + +# --------------------------------------------------------------------------- +# Shared Pydantic schemas +# --------------------------------------------------------------------------- + + +class NumberInput(BaseModel): + number: int + + +class ValueOutput(BaseModel): + value: int + + +class ValueInput(BaseModel): + value: int + + +class FormattedOutput(BaseModel): + result: str + + +# --------------------------------------------------------------------------- +# Shared tool functions +# --------------------------------------------------------------------------- + + +def _double_fn(inp: NumberInput) -> dict: + return {"value": inp.number * 2} + + +def _add_ten_fn(inp: ValueInput) -> dict: + return {"value": inp.value + 10} + + +def _format_fn(inp: ValueInput) -> dict: + return {"result": f"Final value: {inp.value}"} diff --git a/tests/test_flow_execution.py b/tests/test_flow_execution.py index a92ec47..672385e 100644 --- a/tests/test_flow_execution.py +++ b/tests/test_flow_execution.py @@ -3,7 +3,7 @@ from __future__ import annotations import pytest -from conftest import ( +from helpers import ( FormattedOutput, NumberInput, ValueOutput, From 7ef3245f441a947bc24999c47ba253191e12543b Mon Sep 17 00:00:00 2001 From: Diogo Santos Date: Wed, 4 Mar 2026 06:13:11 +0000 Subject: [PATCH 08/10] refactor: move shared schemas and helpers from conftest to tests/helpers.py From 74eca09cbc8956617bbd6f9880736cf5fc66ccff Mon Sep 17 00:00:00 2001 From: Diogo Santos Date: Wed, 4 Mar 2026 06:19:19 +0000 Subject: [PATCH 09/10] docs: clarify last-write-wins semantics in context accumulation test --- tests/test_flow_execution.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/test_flow_execution.py b/tests/test_flow_execution.py index 672385e..a946e76 100644 --- a/tests/test_flow_execution.py +++ b/tests/test_flow_execution.py @@ -84,7 +84,7 @@ def test_tool_not_found_fails_step(self, linear_flow: Flow) -> None: registry = FlowRegistry() registry.register_flow(linear_flow) ex = FlowExecutor(registry=registry) - # No tools registered — step 0 should fail gracefully. + # No tools registered \u2014 step 0 should fail gracefully. result = ex.execute_flow("double_add_format", {"number": 5}) assert result.success is False assert len(result.execution_log) == 1 @@ -278,7 +278,7 @@ def sum_fn(inp: CtxInput) -> dict: class TestFlowExecutionError: - """Tool fn raises a generic exception → wrapped as FlowExecutionError.""" + """Tool fn raises a generic exception \u2192 wrapped as FlowExecutionError.""" def test_runtime_error_wrapped(self) -> None: class InSchema(BaseModel): @@ -578,7 +578,7 @@ class StrictOutput(BaseModel): class TestSingleStepFlow: - """A flow with exactly one step — simplest chaining case.""" + """A flow with exactly one step \u2014 simplest chaining case.""" def test_single_step_succeeds( self, @@ -622,7 +622,8 @@ def test_context_accumulates_all_outputs( # Initial input key is preserved. assert "number" in result.final_output assert result.final_output["number"] == 5 - # Intermediate key from double/add_ten steps. + # Intermediate key: both double and add_ten write "value"; + # 20 (from add_ten) confirms last-write-wins merge semantics. assert "value" in result.final_output assert result.final_output["value"] == 20 # Final key from format_result step. @@ -688,25 +689,25 @@ def divide_fn(inp: DivInput) -> dict: class TestBoundaryValues: - """Negative numbers and zero through the double→add→format chain.""" + """Negative numbers and zero through the double\u2192add\u2192format chain.""" def test_negative_input(self, executor: FlowExecutor) -> None: result = executor.execute_flow("double_add_format", {"number": -3}) - # double(-3) → -6, add_ten(-6) → 4, format(4) → "Final value: 4" + # double(-3) \u2192 -6, add_ten(-6) \u2192 4, format(4) \u2192 "Final value: 4" assert result.success is True assert result.final_output is not None assert result.final_output["result"] == "Final value: 4" def test_large_positive_input(self, executor: FlowExecutor) -> None: result = executor.execute_flow("double_add_format", {"number": 1000}) - # double(1000) → 2000, add_ten(2000) → 2010, format(2010) → "Final value: 2010" + # double(1000) \u2192 2000, add_ten(2000) \u2192 2010, format(2010) \u2192 "Final value: 2010" assert result.success is True assert result.final_output is not None assert result.final_output["result"] == "Final value: 2010" def test_large_negative_input(self, executor: FlowExecutor) -> None: result = executor.execute_flow("double_add_format", {"number": -1000}) - # double(-1000) → -2000, add_ten(-2000) → -1990 + # double(-1000) \u2192 -2000, add_ten(-2000) \u2192 -1990 assert result.success is True assert result.final_output is not None assert result.final_output["result"] == "Final value: -1990" From 425a31f7da53a71425f9fe7c913626e2f00e09d8 Mon Sep 17 00:00:00 2001 From: Diogo Santos Date: Wed, 4 Mar 2026 06:25:17 +0000 Subject: [PATCH 10/10] style: shorten comment to satisfy E501 line-length limit --- tests/test_flow_execution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_flow_execution.py b/tests/test_flow_execution.py index a946e76..f3e4c26 100644 --- a/tests/test_flow_execution.py +++ b/tests/test_flow_execution.py @@ -700,7 +700,7 @@ def test_negative_input(self, executor: FlowExecutor) -> None: def test_large_positive_input(self, executor: FlowExecutor) -> None: result = executor.execute_flow("double_add_format", {"number": 1000}) - # double(1000) \u2192 2000, add_ten(2000) \u2192 2010, format(2010) \u2192 "Final value: 2010" + # double(1000)\u21922000, add_ten(2000)\u21922010, format\u2192"Final value: 2010" assert result.success is True assert result.final_output is not None assert result.final_output["result"] == "Final value: 2010"