Skip to content

Commit 325fd7d

Browse files
CodeRabbit Generated Unit Tests: Add unit tests for DependentString, NodeTemplate, RegisteredNode
1 parent 2edd4c6 commit 325fd7d

4 files changed

Lines changed: 465 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Notes
2+
3+
- Tests authored with pytest, matching repository conventions discovered via search.
4+
- Models use Pydantic v2 (field_validator), so assertions expect pydantic.ValidationError wrapping ValueError messages from validators.
5+
- No new dependencies introduced.
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import pytest
2+
3+
# Attempt multiple import paths to accommodate common layouts
4+
try:
5+
# e.g., src/state_manager/models/dependent_string.py
6+
from state_manager.models.dependent_string import DependentString, Dependent
7+
except Exception:
8+
try:
9+
# e.g., state_manager/dependent_string.py
10+
from state_manager.dependent_string import DependentString, Dependent
11+
except Exception:
12+
try:
13+
# e.g., state-manager/state_manager/models/dependent_string.py accessible as package
14+
from models.dependent_string import DependentString, Dependent
15+
except Exception:
16+
# Fallback: relative import if tests live alongside source
17+
from dependent_string import DependentString, Dependent # type: ignore
18+
19+
20+
class TestCreateDependentString:
21+
def test_no_placeholders_returns_head_only_and_empty_dependents(self):
22+
s = "plain string without placeholders"
23+
ds = DependentString.create_dependent_string(s)
24+
assert isinstance(ds, DependentString)
25+
assert ds.head == s
26+
assert ds.dependents == {}
27+
# get_identifier_field should be empty for no dependents
28+
assert ds.get_identifier_field() == []
29+
30+
def test_single_placeholder_happy_path(self):
31+
template = "Hello ${{ step.outputs.foo }} world"
32+
ds = DependentString.create_dependent_string(template)
33+
# Head is the prefix before the first placeholder
34+
assert ds.head == "Hello "
35+
# One dependent keyed by order 0
36+
assert list(ds.dependents.keys()) == [0]
37+
dep = ds.dependents[0]
38+
assert isinstance(dep, Dependent)
39+
assert dep.identifier == "step"
40+
assert dep.field == "foo"
41+
assert dep.tail == " world"
42+
# Not set yet -> generate_string should raise
43+
with pytest.raises(ValueError) as exc:
44+
ds.generate_string()
45+
assert "Dependent value is not set" in str(exc.value)
46+
# After setting, generation should succeed
47+
ds.set_value("step", "foo", "BAR")
48+
assert ds.generate_string() == "Hello BAR world"
49+
50+
def test_placeholder_at_end_results_in_empty_tail(self):
51+
template = "Hi ${{ a.outputs.x }}"
52+
ds = DependentString.create_dependent_string(template)
53+
assert ds.dependents[0].tail == ""
54+
ds.set_value("a", "x", "V")
55+
assert ds.generate_string() == "Hi V"
56+
57+
def test_multiple_placeholders_in_order(self):
58+
template = "Start ${{ a.outputs.x }} mid ${{ b.outputs.y }} end"
59+
ds = DependentString.create_dependent_string(template)
60+
# Keys should reflect insertion order (0, 1) when sorted
61+
assert sorted(ds.dependents.keys()) == [0, 1]
62+
assert ds.dependents[0].identifier == "a"
63+
assert ds.dependents[0].field == "x"
64+
assert ds.dependents[0].tail == " mid "
65+
assert ds.dependents[1].identifier == "b"
66+
assert ds.dependents[1].field == "y"
67+
assert ds.dependents[1].tail == " end"
68+
ds.set_value("a", "x", "AX")
69+
ds.set_value("b", "y", "BY")
70+
assert ds.generate_string() == "Start AX mid BY end"
71+
72+
def test_unclosed_placeholder_raises_value_error(self):
73+
template = "Start ${{ a.outputs.x end"
74+
with pytest.raises(ValueError) as exc:
75+
DependentString.create_dependent_string(template)
76+
msg = str(exc.value)
77+
assert "Invalid syntax string placeholder" in msg
78+
assert "'${{'" in msg or "not closed" in msg
79+
80+
def test_invalid_placeholder_wrong_parts_count_raises(self):
81+
# Missing the third part after outputs
82+
template = "Start ${{ a.outputs }} end"
83+
with pytest.raises(ValueError) as exc:
84+
DependentString.create_dependent_string(template)
85+
assert "Invalid syntax string placeholder" in str(exc.value)
86+
87+
def test_invalid_placeholder_wrong_keyword_raises(self):
88+
template = "Start ${{ a.outputz.x }} end"
89+
with pytest.raises(ValueError) as exc:
90+
DependentString.create_dependent_string(template)
91+
assert "Invalid syntax string placeholder" in str(exc.value)
92+
93+
def test_placeholder_with_extra_whitespace_is_parsed(self):
94+
template = "P ${{ step . outputs . foo }} T"
95+
ds = DependentString.create_dependent_string(template)
96+
assert ds.dependents[0].identifier == "step"
97+
assert ds.dependents[0].field == "foo"
98+
ds.set_value("step", "foo", "VAL")
99+
assert ds.generate_string() == "P VAL T"
100+
101+
102+
class TestMappingAndSetValue:
103+
def test_get_identifier_field_returns_unique_keys(self):
104+
template = "A ${{ a.outputs.x }} B ${{ a.outputs.x }} C ${{ b.outputs.y }}"
105+
ds = DependentString.create_dependent_string(template)
106+
# Should return two unique keys: ('a','x') and ('b','y'), order not guaranteed
107+
keys = ds.get_identifier_field()
108+
assert set(keys) == {("a", "x"), ("b", "y")}
109+
110+
def test_set_value_updates_all_matching_dependents(self):
111+
template = "A ${{ a.outputs.x }} B ${{ a.outputs.x }}"
112+
ds = DependentString.create_dependent_string(template)
113+
ds.set_value("a", "x", "V")
114+
# Both dependents should get the same value and render twice
115+
assert ds.generate_string() == "A V B V"
116+
117+
def test_set_value_with_unknown_mapping_raises_keyerror(self):
118+
template = "A ${{ a.outputs.x }}"
119+
ds = DependentString.create_dependent_string(template)
120+
with pytest.raises(KeyError):
121+
ds.set_value("unknown", "field", "V")
122+
123+
def test_build_mapping_is_idempotent_and_cached(self):
124+
template = "A ${{ a.outputs.x }} B ${{ b.outputs.y }}"
125+
ds = DependentString.create_dependent_string(template)
126+
# First call builds mapping
127+
keys1 = set(ds.get_identifier_field())
128+
# Second call should not change
129+
keys2 = set(ds.get_identifier_field())
130+
assert keys1 == keys2 == {("a", "x"), ("b", "y")}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""
2+
Test suite for NodeTemplate and Unites models.
3+
4+
Testing library/framework: pytest (with Pydantic v2 ValidationError assertions).
5+
6+
Covers:
7+
- Successful model creation with valid data
8+
- Validators: node_name, identifier (non-empty), next_nodes (non-empty, unique)
9+
- Optional field `next_nodes` can be None
10+
- Optional nested model `unites` with identifier non-empty when provided
11+
- get_dependent_strings: returns one entry per input; raises on non-string values; handles empty inputs
12+
"""
13+
14+
import builtins
15+
import types
16+
import sys
17+
import inspect
18+
import pytest
19+
20+
try:
21+
# Try common import paths — adapt if project layout differs.
22+
# Prefer the actual model module housing NodeTemplate and Unites.
23+
from state_manager.models.node_template_model import NodeTemplate, Unites # type: ignore
24+
import state_manager.models.node_template_model as node_template_module # type: ignore
25+
except Exception:
26+
try:
27+
from state_manager.models.node_template import NodeTemplate, Unites # type: ignore
28+
import state_manager.models.node_template as node_template_module # type: ignore
29+
except Exception:
30+
# Fallback: dynamically locate the module that defines NodeTemplate in sys.modules
31+
# This keeps tests resilient if the path changes while still validating behavior.
32+
candidates = []
33+
for name, mod in list(sys.modules.items()):
34+
if not isinstance(mod, types.ModuleType):
35+
continue
36+
try:
37+
if hasattr(mod, "NodeTemplate") and hasattr(mod, "Unites"):
38+
candidates.append(mod)
39+
except Exception:
40+
continue
41+
if not candidates:
42+
# Attempt a lazy import scan by probing typical packages under repo
43+
# This is a last-resort guard — if it fails, tests will clearly indicate missing module.
44+
raise
45+
node_template_module = candidates[0]
46+
NodeTemplate = getattr(node_template_module, "NodeTemplate")
47+
Unites = getattr(node_template_module, "Unites")
48+
49+
50+
from pydantic import ValidationError
51+
52+
53+
def _valid_payload(**overrides):
54+
data = {
55+
"node_name": "node_A",
56+
"namespace": "ns.main",
57+
"identifier": "node-A-id",
58+
"inputs": {"foo": "bar", "alpha": "beta"},
59+
"next_nodes": ["node_B", "node_C"],
60+
"unites": Unites(identifier="u-1"),
61+
}
62+
data.update(overrides)
63+
return data
64+
65+
66+
class TestNodeTemplateValidation:
67+
def test_valid_creation_happy_path(self):
68+
model = NodeTemplate(**_valid_payload())
69+
assert model.node_name == "node_A"
70+
assert model.namespace == "ns.main"
71+
assert model.identifier == "node-A-id"
72+
assert model.next_nodes == ["node_B", "node_C"]
73+
assert model.unites is not None and model.unites.identifier == "u-1"
74+
75+
@pytest.mark.parametrize(
76+
"field, bad_value, expected_msg",
77+
[
78+
("node_name", "", "Node name cannot be empty"),
79+
("identifier", "", "Node identifier cannot be empty"),
80+
],
81+
)
82+
def test_required_string_fields_must_not_be_empty(self, field, bad_value, expected_msg):
83+
payload = _valid_payload(**{field: bad_value})
84+
with pytest.raises(ValidationError) as ei:
85+
NodeTemplate(**payload)
86+
# ValueError raised in field_validator should surface inside ValidationError text
87+
assert expected_msg in str(ei.value)
88+
89+
def test_next_nodes_none_is_allowed(self):
90+
payload = _valid_payload(next_nodes=None)
91+
model = NodeTemplate(**payload)
92+
assert model.next_nodes is None
93+
94+
def test_next_nodes_rejects_empty_and_duplicates_with_aggregated_errors(self):
95+
# includes duplicate "dup" and an empty string
96+
payload = _valid_payload(next_nodes=["ok1", "dup", "dup", ""])
97+
with pytest.raises(ValidationError) as ei:
98+
NodeTemplate(**payload)
99+
msg = str(ei.value)
100+
assert "Next node identifier dup is not unique" in msg
101+
assert "Next node identifier cannot be empty" in msg
102+
103+
def test_unites_identifier_must_not_be_empty_when_provided(self):
104+
payload = _valid_payload(unites=Unites(identifier=""))
105+
with pytest.raises(ValidationError) as ei:
106+
NodeTemplate(**payload)
107+
assert "Unites identifier cannot be empty" in str(ei.value)
108+
109+
110+
class TestGetDependentStrings:
111+
def test_returns_one_dependent_string_per_input_value(self):
112+
payload = _valid_payload(inputs={"a": "X", "b": "Y", "c": "Z"})
113+
model = NodeTemplate(**payload)
114+
115+
# Prefer not to mock: assert count and type if available
116+
result = model.get_dependent_strings()
117+
assert isinstance(result, list)
118+
assert len(result) == 3
119+
120+
# If DependentString is importable from the module, verify type
121+
DepStr = getattr(node_template_module, "DependentString", None)
122+
if DepStr is not None and inspect.isclass(DepStr):
123+
assert all(isinstance(d, DepStr) for d in result)
124+
125+
def test_raises_value_error_when_any_input_value_is_not_string(self):
126+
payload = _valid_payload(inputs={"a": "X", "b": 123, "c": "Z"})
127+
model = NodeTemplate(**payload)
128+
with pytest.raises(ValueError) as ei:
129+
model.get_dependent_strings()
130+
assert "Input 123 is not a string" in str(ei.value)
131+
132+
def test_handles_empty_inputs_dict(self):
133+
payload = _valid_payload(inputs={})
134+
model = NodeTemplate(**payload)
135+
out = model.get_dependent_strings()
136+
assert out == []
137+
138+
139+
# Extra edge cases: whitespace-only strings considered non-empty by type,
140+
# but validators explicitly only check for equality with empty string.
141+
# Decide expected behavior: whitespace is allowed per current implementation.
142+
def test_whitespace_strings_are_allowed_by_validators():
143+
payload = _valid_payload(node_name=" name ", identifier=" id ")
144+
model = NodeTemplate(**payload)
145+
assert model.node_name.strip() == "name"
146+
assert model.identifier.strip() == "id"

0 commit comments

Comments
 (0)