forked from HAL9000/cleveragents-core
263 lines
9.1 KiB
Python
263 lines
9.1 KiB
Python
"""
|
|
Behave step definitions to cover remaining branches in langgraph/state.py.
|
|
"""
|
|
|
|
import json
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
|
|
from cleveragents.langgraph.state import GraphState, StateManager, StateUpdateMode
|
|
|
|
|
|
def _ensure(context: Context) -> None:
|
|
context.state_instances = getattr(context, "state_instances", {}) or {}
|
|
context.state_managers = getattr(context, "state_managers", {}) or {}
|
|
context.test_data = getattr(context, "test_data", {}) or {}
|
|
context.results = getattr(context, "results", {}) or {}
|
|
|
|
|
|
@given('I have a GraphState with metadata {metadata_json} and current node "{node}"')
|
|
def step_graphstate_replace(context: Context, metadata_json: str, node: str):
|
|
_ensure(context)
|
|
metadata = json.loads(metadata_json)
|
|
context.state_instances["replace_state"] = GraphState(
|
|
metadata=metadata, current_node=node
|
|
)
|
|
|
|
|
|
@when(
|
|
'I replace the state with updates containing metadata {metadata_json} and current node "{node}"'
|
|
)
|
|
def step_replace_updates(context: Context, metadata_json: str, node: str):
|
|
_ensure(context)
|
|
state = context.state_instances["replace_state"]
|
|
updates = {"metadata": json.loads(metadata_json), "current_node": node}
|
|
state.update(updates, StateUpdateMode.REPLACE)
|
|
|
|
|
|
@then("the state's metadata should equal {metadata_json}")
|
|
def step_verify_metadata_equals(context: Context, metadata_json: str):
|
|
state = context.state_instances.get("replace_state")
|
|
assert state is not None
|
|
expected = json.loads(metadata_json)
|
|
assert state.metadata == expected
|
|
|
|
|
|
@then('the state\'s current node should equal "{node}"')
|
|
def step_verify_current_node_equals(context: Context, node: str):
|
|
state = context.state_instances.get("replace_state")
|
|
assert state is not None
|
|
assert state.current_node == node
|
|
|
|
|
|
@given("I have a GraphState with metadata {metadata_json}")
|
|
def step_graphstate_metadata(context: Context, metadata_json: str):
|
|
_ensure(context)
|
|
context.state_instances["merge_meta"] = GraphState(
|
|
metadata=json.loads(metadata_json)
|
|
)
|
|
|
|
|
|
@given("I have merge metadata updates {metadata_json}")
|
|
def step_merge_metadata_updates(context: Context, metadata_json: str):
|
|
_ensure(context)
|
|
context.test_data["merge_metadata"] = json.loads(metadata_json)
|
|
|
|
|
|
@when("I merge metadata updates into the state")
|
|
def step_merge_metadata(context: Context):
|
|
_ensure(context)
|
|
state = context.state_instances["merge_meta"]
|
|
state.update(
|
|
{"metadata": context.test_data["merge_metadata"]}, StateUpdateMode.MERGE
|
|
)
|
|
|
|
|
|
@then('the state\'s metadata should contain keys "{key1}" and "{key2}"')
|
|
def step_verify_metadata_keys(context: Context, key1: str, key2: str):
|
|
state = context.state_instances.get("merge_meta")
|
|
assert state is not None
|
|
assert key1 in state.metadata
|
|
assert key2 in state.metadata
|
|
|
|
|
|
@given("I have a GraphState with 1 message")
|
|
def step_graphstate_one_message(context: Context):
|
|
_ensure(context)
|
|
context.state_instances["append_single"] = GraphState(messages=[{"id": 0}])
|
|
|
|
|
|
@when("I append a single message with id {message_id:d} into the state")
|
|
def step_append_single_message(context: Context, message_id: int):
|
|
_ensure(context)
|
|
state = context.state_instances["append_single"]
|
|
state.update({"messages": {"id": message_id}}, StateUpdateMode.APPEND)
|
|
|
|
|
|
@then("the state's messages should end with id {message_id:d}")
|
|
def step_verify_messages_end(context: Context, message_id: int):
|
|
state = context.state_instances.get("append_single")
|
|
assert state is not None
|
|
assert state.messages[-1]["id"] == message_id
|
|
|
|
|
|
@then("the message list length should be 2")
|
|
def step_verify_message_length_two(context: Context):
|
|
state = context.state_instances.get("append_single")
|
|
assert state is not None
|
|
assert len(state.messages) == 2
|
|
|
|
|
|
@given("I have a StateManager without checkpointing")
|
|
def step_manager_without_checkpoint(context: Context):
|
|
_ensure(context)
|
|
context.state_managers["no_checkpoint"] = StateManager()
|
|
|
|
|
|
@when("I invoke checkpoint saving manually")
|
|
def step_invoke_save_checkpoint(context: Context):
|
|
manager = context.state_managers["no_checkpoint"]
|
|
manager._save_checkpoint()
|
|
|
|
|
|
@then("the update count should remain at 0")
|
|
def step_verify_update_count_zero(context: Context):
|
|
manager = context.state_managers["no_checkpoint"]
|
|
assert manager.update_count == 0
|
|
|
|
|
|
@given(
|
|
"I have a checkpoint file with execution count {count:d} and metadata {metadata_json}"
|
|
)
|
|
def step_prepare_checkpoint_file(context: Context, count: int, metadata_json: str):
|
|
_ensure(context)
|
|
checkpoint_dir = Path(tempfile.mkdtemp())
|
|
checkpoint_file = checkpoint_dir / "checkpoint_test.json"
|
|
data = {
|
|
"state": GraphState(metadata=json.loads(metadata_json)).to_dict(),
|
|
"timestamp": "test",
|
|
"update_count": count,
|
|
}
|
|
checkpoint_file.write_text(json.dumps(data), encoding="utf-8")
|
|
context.test_data["checkpoint_file"] = checkpoint_file
|
|
|
|
|
|
@when("I load the checkpoint into a StateManager")
|
|
def step_load_checkpoint(context: Context):
|
|
manager = StateManager()
|
|
checkpoint_file = context.test_data["checkpoint_file"]
|
|
manager.load_checkpoint(checkpoint_file)
|
|
context.results["loaded_manager"] = manager
|
|
|
|
|
|
@then("the manager state should include metadata {metadata_json}")
|
|
def step_verify_loaded_metadata(context: Context, metadata_json: str):
|
|
manager: StateManager = context.results["loaded_manager"]
|
|
expected = json.loads(metadata_json)
|
|
assert manager.state.metadata == expected
|
|
|
|
|
|
@then("the manager update count should be {count:d}")
|
|
def step_verify_loaded_update_count(context: Context, count: int):
|
|
manager: StateManager = context.results["loaded_manager"]
|
|
assert manager.update_count == count
|
|
|
|
|
|
@when("I request the latest checkpoint")
|
|
def step_request_latest_checkpoint(context: Context):
|
|
manager = context.state_managers.get("no_checkpoint") or context.results.get(
|
|
"latest_manager"
|
|
)
|
|
context.results["latest_checkpoint"] = (
|
|
manager.get_latest_checkpoint() if manager else None
|
|
)
|
|
|
|
|
|
@then("no checkpoint path should be returned")
|
|
def step_verify_latest_none(context: Context):
|
|
assert context.results["latest_checkpoint"] is None
|
|
|
|
|
|
@given("I have a StateManager with two checkpoints written at different times")
|
|
def step_manager_with_two_checkpoints(context: Context):
|
|
_ensure(context)
|
|
checkpoint_dir = Path(tempfile.mkdtemp())
|
|
manager = StateManager(checkpoint_dir=checkpoint_dir)
|
|
|
|
# Write first checkpoint
|
|
manager.state.metadata = {"first": True}
|
|
manager.update_count = 1
|
|
manager._save_checkpoint()
|
|
|
|
# Write second checkpoint with newer mtime
|
|
manager.state.metadata = {"second": True}
|
|
manager.update_count = 2
|
|
manager._save_checkpoint()
|
|
|
|
context.results["latest_manager"] = manager
|
|
|
|
|
|
@then("the newest checkpoint path should be returned")
|
|
def step_verify_latest_checkpoint(context: Context):
|
|
manager: StateManager = context.results["latest_manager"]
|
|
latest = manager.get_latest_checkpoint()
|
|
assert latest is not None
|
|
checkpoint_files = list(manager.checkpoint_dir.glob("checkpoint_*.json"))
|
|
assert latest == max(checkpoint_files, key=lambda p: p.stat().st_mtime)
|
|
|
|
|
|
@given("I have a StateManager with time travel disabled")
|
|
def step_manager_time_travel_disabled(context: Context):
|
|
_ensure(context)
|
|
context.state_managers["tt_disabled"] = StateManager(enable_time_travel=False)
|
|
|
|
|
|
@when("I request time travel")
|
|
def step_request_time_travel(context: Context):
|
|
manager = context.state_managers["tt_disabled"]
|
|
context.results["tt_result"] = manager.time_travel()
|
|
|
|
|
|
@then("the returned time travel state should be None")
|
|
def step_verify_time_travel_none(context: Context):
|
|
assert context.results["tt_result"] is None
|
|
|
|
|
|
@given("I have a StateManager with time travel history entries")
|
|
def step_manager_with_history(context: Context):
|
|
_ensure(context)
|
|
manager = StateManager(enable_time_travel=True)
|
|
manager.update_state({"execution_count": 1, "metadata": {"keep": "me"}})
|
|
manager.update_state({"execution_count": 2, "metadata": {"keep": "me2"}})
|
|
context.state_managers["history_reset"] = manager
|
|
|
|
|
|
@when("I clear the history and then reset with metadata {metadata_json}")
|
|
def step_clear_and_reset(context: Context, metadata_json: str):
|
|
manager = context.state_managers["history_reset"]
|
|
manager.clear_history()
|
|
manager.reset(GraphState(metadata=json.loads(metadata_json)))
|
|
context.results["reset_manager"] = manager
|
|
|
|
|
|
@then("the history should be empty after reset")
|
|
def step_verify_history_empty(context: Context):
|
|
manager: StateManager = context.results["reset_manager"]
|
|
assert len(manager.history) == 0
|
|
|
|
|
|
@then("the execution count should be zero after reset")
|
|
def step_verify_execution_zero(context: Context):
|
|
manager: StateManager = context.results["reset_manager"]
|
|
assert manager.update_count == 0
|
|
|
|
|
|
@then("the state metadata should include {metadata_json}")
|
|
def step_verify_state_metadata_after_reset(context: Context, metadata_json: str):
|
|
manager: StateManager = context.results["reset_manager"]
|
|
expected = json.loads(metadata_json)
|
|
assert manager.state.metadata == expected
|