"""Tests for LangGraph workflow-incomplete / auto-continue helpers.""" from __future__ import annotations import json import pytest from pi_test_support import ensure_agent_redact_paths ensure_agent_redact_paths() from redaction_langgraph.workflow_continue import ( # noqa: E402 build_identical_error_breaker_prompt, build_tool_call_json_retry_prompt, build_workflow_continue_prompt, consecutive_python_writes_without_run, identical_tool_error_streak, redaction_workflow_incomplete, tool_output_error_signature, ) def _write_out(written: str) -> str: return json.dumps({"written": written, "bytes": 10}) def test_incomplete_false_for_explore_only(): tools = {"list_workspace_files", "read_workspace_text"} outputs = [ ("list_workspace_files", "[]"), ("read_workspace_text", "page text"), ] assert redaction_workflow_incomplete(tools, outputs) is False def test_incomplete_pass1_doc_redact_without_apply(): assert redaction_workflow_incomplete({"doc_redact"}, []) is True assert redaction_workflow_incomplete({"doc_redact", "review_apply"}, []) is False def test_incomplete_followup_pending_python_script(): tools = {"list_workspace_files", "read_workspace_text", "write_workspace_text"} outputs = [ ("list_workspace_files", "[]"), ("read_workspace_text", "ocr"), ("write_workspace_text", _write_out("redact/doc/output_redact/extract.py")), ] assert redaction_workflow_incomplete(tools, outputs) is True prompt = build_workflow_continue_prompt(tools, outputs) assert "extract.py" in prompt assert "run_workspace_python_script" in prompt def test_incomplete_false_after_script_run_and_apply(): tools = { "write_workspace_text", "run_workspace_python_script", "review_apply", } outputs = [ ("write_workspace_text", _write_out("fix_policy.py")), ("run_workspace_python_script", '{"ok": true}'), ("review_apply", '{"ok": true}'), ] assert redaction_workflow_incomplete(tools, outputs) is False def test_incomplete_after_script_run_without_apply(): tools = {"write_workspace_text", "run_workspace_python_script"} outputs = [ ("write_workspace_text", _write_out("fix_policy.py")), ("run_workspace_python_script", '{"ok": true}'), ] assert redaction_workflow_incomplete(tools, outputs) is True def test_incomplete_after_review_csv_write_without_apply(): tools = {"write_workspace_text"} outputs = [ ( "write_workspace_text", _write_out("redact/doc/output_redact/doc_review_file.csv"), ) ] assert redaction_workflow_incomplete(tools, outputs) is True def test_langgraph_trace_config_includes_session_metadata(): from eval.arize_monitoring import arize_session_id, langgraph_trace_config assert arize_session_id(None) is None assert arize_session_id(" ") is None assert arize_session_id("abc123") == "abc123" bare = langgraph_trace_config(None, recursion_limit=50) assert bare == {"recursion_limit": 50} cfg = langgraph_trace_config("sess-1", recursion_limit=25) assert cfg["recursion_limit"] == 25 assert cfg["metadata"]["session_id"] == "sess-1" assert cfg["metadata"]["thread_id"] == "sess-1" assert cfg["configurable"]["thread_id"] == "sess-1" def test_incomplete_false_for_plain_text_write(): tools = {"write_workspace_text"} outputs = [("write_workspace_text", _write_out("notes.txt"))] assert redaction_workflow_incomplete(tools, outputs) is False def test_continue_prompt_generic_when_no_pending_script(): tools = {"run_workspace_python_script"} outputs = [("run_workspace_python_script", '{"ok": true}')] prompt = build_workflow_continue_prompt(tools, outputs) assert "review_apply" in prompt assert "NOT complete" in prompt assert "compact" in prompt.lower() or "hard-coded" in prompt.lower() def test_tool_call_json_retry_prompt_asks_for_compact_script(): prompt = build_tool_call_json_retry_prompt() assert "JSON" in prompt assert "write_workspace_text" in prompt assert "run_workspace_python_script" in prompt assert "compact" in prompt.lower() or "SHORT" in prompt def test_identical_tool_error_streak_detects_repeat(): err = json.dumps({"error": "pdf_relative_path must be a plain string"}) outputs = [ ("doc_redact", err), ("doc_redact", err), ] streak = identical_tool_error_streak(outputs, min_streak=2) assert streak == ("doc_redact", tool_output_error_signature(err)) def test_identical_tool_error_streak_ignores_success(): outputs = [ ("doc_redact", json.dumps({"error": "bad"})), ("doc_redact", json.dumps({"ok": True})), ] assert identical_tool_error_streak(outputs, min_streak=2) is None def test_continue_prompt_uses_error_breaker_on_streak(): err = json.dumps({"error": "Tool relative_path is empty."}) outputs = [ ("write_workspace_text", err), ("write_workspace_text", err), ] prompt = build_workflow_continue_prompt({"write_workspace_text"}, outputs) assert "STOP" in prompt assert "flat" in prompt.lower() or "nested" in prompt.lower() assert "write_workspace_text" in prompt def test_build_identical_error_breaker_prompt_includes_error(): err = json.dumps({"error": "nested object not allowed"}) prompt = build_identical_error_breaker_prompt( "doc_redact", [("doc_redact", err)], ) assert "doc_redact" in prompt assert "nested object not allowed" in prompt def test_consecutive_python_writes_without_run(): outputs = [ ("write_workspace_text", _write_out("a.py")), ("write_workspace_text", _write_out("a.py")), ("write_workspace_text", _write_out("a.py")), ] path, count = consecutive_python_writes_without_run(outputs) assert path == "a.py" assert count == 3 def test_continue_prompt_write_storm_nudge(): outputs = [ ("write_workspace_text", _write_out("fix_policy.py")), ("write_workspace_text", _write_out("fix_policy.py")), ] prompt = build_workflow_continue_prompt({"write_workspace_text"}, outputs) assert "run_workspace_python_script" in prompt assert "fix_policy.py" in prompt @pytest.mark.parametrize( "text", [ "Error code: 500 - {'error': {'code': 500, 'message': 'Failed to parse tool call arguments as JSON: [json.exception.parse_error.101] parse error at line 1, column 6673: syntax error while parsing value - invalid string: missing closing quote', 'type': 'server_error'}}", "failed to parse tool call arguments as json", "json.exception.parse_error.101", "invalid string: missing closing quote", ], ) def test_is_tool_call_json_parse_error(text): from redaction_langgraph.llm_errors import is_tool_call_json_parse_error assert is_tool_call_json_parse_error(RuntimeError(text)) is True def test_is_tool_call_json_parse_error_negative(): from redaction_langgraph.llm_errors import is_tool_call_json_parse_error assert is_tool_call_json_parse_error(RuntimeError("connection refused")) is False assert ( is_tool_call_json_parse_error(RuntimeError("context_length_exceeded")) is False )