Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/conductor/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def init_file_logging(log_path: Path) -> None:
"""
global _file_console, _file_handle
log_path.parent.mkdir(parents=True, exist_ok=True)
_file_handle = open(log_path, "w") # noqa: SIM115
_file_handle = open(log_path, "w", encoding="utf-8") # noqa: SIM115
_file_console = Console(file=_file_handle, no_color=True, highlight=False, width=200)


Expand Down
46 changes: 25 additions & 21 deletions tests/test_cli/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
from __future__ import annotations

import contextlib
import sys
from pathlib import Path
from unittest.mock import patch

import pytest
from typer.testing import CliRunner

from conductor.cli.app import (
Expand Down Expand Up @@ -732,7 +734,7 @@ def test_verbose_log_writes_to_file(self, tmp_path: Path) -> None:
verbose_log("test file message")
close_file_logging()

content = log_path.read_text()
content = log_path.read_text(encoding="utf-8")
assert "test file message" in content
finally:
verbose_mode.reset(token)
Expand All @@ -750,7 +752,7 @@ def test_file_output_is_plain_text(self, tmp_path: Path) -> None:
verbose_log("styled message", style="bold red")
close_file_logging()

content = log_path.read_text()
content = log_path.read_text(encoding="utf-8")
assert "styled message" in content
# No ANSI escape sequences
ansi_pattern = re.compile(r"\x1b\[[0-9;]*m")
Expand All @@ -772,7 +774,7 @@ def test_file_gets_untruncated_content(self, tmp_path: Path) -> None:
verbose_log_section("Test", long_content)
close_file_logging()

content = log_path.read_text()
content = log_path.read_text(encoding="utf-8")
# File should have full content, not truncated
assert "truncated" not in content
assert content.count("x") == 1000
Expand All @@ -797,7 +799,7 @@ def test_file_logging_in_silent_mode(self, tmp_path: Path) -> None:
verbose_log_timing("test op", 1.5)
close_file_logging()

content = log_path.read_text()
content = log_path.read_text(encoding="utf-8")
assert "silent mode message" in content
assert "test op" in content
assert "1.50" in content
Expand Down Expand Up @@ -1023,7 +1025,7 @@ def test_file_gets_sections_in_minimal_mode(self, tmp_path: Path) -> None:
verbose_log_section("Prompt", "full prompt content here")
close_file_logging()

content = log_path.read_text()
content = log_path.read_text(encoding="utf-8")
assert "Prompt" in content
assert "full prompt content here" in content
finally:
Expand Down Expand Up @@ -1078,7 +1080,7 @@ def test_agent_start_writes_to_file(self, tmp_path: Path) -> None:
verbose_log_agent_start("test-agent", 1)
close_file_logging()

content = log_path.read_text()
content = log_path.read_text(encoding="utf-8")
assert "test-agent" in content
assert "iter 1" in content
finally:
Expand All @@ -1101,7 +1103,7 @@ def test_agent_complete_writes_to_file(self, tmp_path: Path) -> None:
)
close_file_logging()

content = log_path.read_text()
content = log_path.read_text(encoding="utf-8")
assert "test-agent" in content
assert "1.50" in content
assert "gpt-4" in content
Expand All @@ -1120,7 +1122,7 @@ def test_route_writes_to_file(self, tmp_path: Path) -> None:
verbose_log_route("next-agent")
close_file_logging()

content = log_path.read_text()
content = log_path.read_text(encoding="utf-8")
assert "next-agent" in content
finally:
verbose_mode.reset(token)
Expand All @@ -1136,7 +1138,7 @@ def test_route_end_writes_to_file(self, tmp_path: Path) -> None:
verbose_log_route("$end")
close_file_logging()

content = log_path.read_text()
content = log_path.read_text(encoding="utf-8")
assert "$end" in content
finally:
verbose_mode.reset(token)
Expand All @@ -1152,7 +1154,7 @@ def test_timing_writes_to_file(self, tmp_path: Path) -> None:
verbose_log_timing("Workflow execution", 3.456)
close_file_logging()

content = log_path.read_text()
content = log_path.read_text(encoding="utf-8")
assert "Workflow execution" in content
assert "3.46" in content
finally:
Expand All @@ -1173,7 +1175,7 @@ def test_parallel_start_writes_to_file(self, tmp_path: Path) -> None:
verbose_log_parallel_start("parallel-group", 3)
close_file_logging()

content = log_path.read_text()
content = log_path.read_text(encoding="utf-8")
assert "parallel-group" in content
assert "3 agents" in content
finally:
Expand All @@ -1194,7 +1196,7 @@ def test_parallel_agent_complete_writes_to_file(self, tmp_path: Path) -> None:
verbose_log_parallel_agent_complete("agent-a", 2.0, model="gpt-4", tokens=200)
close_file_logging()

content = log_path.read_text()
content = log_path.read_text(encoding="utf-8")
assert "agent-a" in content
assert "2.00" in content
finally:
Expand All @@ -1215,7 +1217,7 @@ def test_parallel_agent_failed_writes_to_file(self, tmp_path: Path) -> None:
verbose_log_parallel_agent_failed("agent-b", 0.5, "RuntimeError", "Something broke")
close_file_logging()

content = log_path.read_text()
content = log_path.read_text(encoding="utf-8")
assert "agent-b" in content
assert "RuntimeError" in content
assert "Something broke" in content
Expand All @@ -1237,7 +1239,7 @@ def test_parallel_summary_writes_to_file(self, tmp_path: Path) -> None:
verbose_log_parallel_summary("group1", 2, 1, 3.0)
close_file_logging()

content = log_path.read_text()
content = log_path.read_text(encoding="utf-8")
assert "group1" in content
assert "2 succeeded" in content
assert "1 failed" in content
Expand All @@ -1259,7 +1261,7 @@ def test_for_each_start_writes_to_file(self, tmp_path: Path) -> None:
verbose_log_for_each_start("loop-group", 5, 2, "fail_fast")
close_file_logging()

content = log_path.read_text()
content = log_path.read_text(encoding="utf-8")
assert "loop-group" in content
assert "5 items" in content
finally:
Expand All @@ -1280,7 +1282,7 @@ def test_for_each_item_complete_writes_to_file(self, tmp_path: Path) -> None:
verbose_log_for_each_item_complete("item-0", 1.2, tokens=50)
close_file_logging()

content = log_path.read_text()
content = log_path.read_text(encoding="utf-8")
assert "item-0" in content
assert "1.20" in content
finally:
Expand All @@ -1301,7 +1303,7 @@ def test_for_each_item_failed_writes_to_file(self, tmp_path: Path) -> None:
verbose_log_for_each_item_failed("item-2", 0.3, "ValueError", "bad input")
close_file_logging()

content = log_path.read_text()
content = log_path.read_text(encoding="utf-8")
assert "item-2" in content
assert "ValueError" in content
assert "bad input" in content
Expand All @@ -1323,7 +1325,7 @@ def test_for_each_summary_writes_to_file(self, tmp_path: Path) -> None:
verbose_log_for_each_summary("loop1", 4, 1, 5.0)
close_file_logging()

content = log_path.read_text()
content = log_path.read_text(encoding="utf-8")
assert "loop1" in content
assert "4 succeeded" in content
assert "1 failed" in content
Expand Down Expand Up @@ -1351,7 +1353,7 @@ def test_display_usage_summary_writes_to_file(self, tmp_path: Path) -> None:
)
close_file_logging()

content = log_path.read_text()
content = log_path.read_text(encoding="utf-8")
assert "Token Usage" in content
assert "500" in content
assert "200" in content
Expand All @@ -1371,7 +1373,7 @@ def test_section_writes_full_content_to_file_in_silent_mode(self, tmp_path: Path
verbose_log_section("Prompt", "full prompt content")
close_file_logging()

content = log_path.read_text()
content = log_path.read_text(encoding="utf-8")
assert "Prompt" in content
assert "full prompt content" in content
finally:
Expand Down Expand Up @@ -1528,6 +1530,7 @@ def test_log_path_not_printed_when_init_fails(self, tmp_path: Path) -> None:
class TestFileLoggingErrorHandling:
"""Tests for file logging error handling."""

@pytest.mark.skipif(sys.platform == "win32", reason="Unix-specific chmod permissions")
def test_init_file_logging_permission_denied(self, tmp_path: Path) -> None:
"""Test that init_file_logging raises OSError for permission issues."""
import os
Expand All @@ -1551,6 +1554,7 @@ def test_init_file_logging_permission_denied(self, tmp_path: Path) -> None:
# Restore permissions for cleanup
os.chmod(readonly_dir, 0o755)

@pytest.mark.skipif(sys.platform == "win32", reason="Unix-specific path behavior")
def test_run_workflow_handles_log_file_error_gracefully(self, tmp_path: Path) -> None:
"""Test that run_workflow_async handles log file errors gracefully."""
import asyncio
Expand Down Expand Up @@ -1635,7 +1639,7 @@ def test_file_output_no_ansi_for_all_styles(self, tmp_path: Path) -> None:
verbose_log_timing("operation", 1.5)
close_file_logging()

content = log_path.read_text()
content = log_path.read_text(encoding="utf-8")
ansi_pattern = re.compile(r"\x1b\[[0-9;]*m")
assert not ansi_pattern.search(content), f"ANSI codes found in file output: {content}"
finally:
Expand Down
8 changes: 4 additions & 4 deletions tests/test_engine/test_checkpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import stat
import sys
from datetime import UTC, datetime
from pathlib import Path
from pathlib import Path, PurePosixPath
from typing import Any
from unittest.mock import patch

Expand Down Expand Up @@ -106,13 +106,13 @@ def test_path(self) -> None:
assert _make_json_serializable(p) == str(p)

def test_dict_recursive(self) -> None:
d = {"path": Path("/a"), "nested": {"b": b"data"}}
d = {"path": PurePosixPath("/a"), "nested": {"b": b"data"}}
result = _make_json_serializable(d)
assert result["path"] == "/a"
assert result["nested"]["b"] == "data"

def test_list_recursive(self) -> None:
result = _make_json_serializable([Path("/a"), 42, [b"x"]])
result = _make_json_serializable([PurePosixPath("/a"), 42, [b"x"]])
assert result == ["/a", 42, ["x"]]

def test_set_converted_to_sorted_list(self) -> None:
Expand Down Expand Up @@ -244,7 +244,7 @@ def test_handles_non_serializable_inputs(self, tmp_path: Path) -> None:
limits = _make_limits()
error = RuntimeError("err")

inputs_with_path: dict[str, Any] = {"file": Path("/tmp/x"), "data": b"bytes"}
inputs_with_path: dict[str, Any] = {"file": PurePosixPath("/tmp/x"), "data": b"bytes"}

with patch.object(CheckpointManager, "get_checkpoints_dir", return_value=tmp_path):
path = CheckpointManager.save_checkpoint(wf, ctx, limits, "a", error, inputs_with_path)
Expand Down
5 changes: 3 additions & 2 deletions tests/test_engine/test_event_emission.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from __future__ import annotations

import sys
from unittest.mock import MagicMock

import pytest
Expand Down Expand Up @@ -510,8 +511,8 @@ async def test_script_started_and_completed(self) -> None:
AgentDef(
name="run_echo",
type="script",
command="echo",
args=["hello"],
command=sys.executable,
args=["-c", "print('hello')"],
routes=[RouteDef(to="$end")],
),
],
Expand Down
37 changes: 21 additions & 16 deletions tests/test_engine/test_script_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from __future__ import annotations

import sys
from unittest.mock import MagicMock

import pytest
Expand Down Expand Up @@ -52,8 +53,8 @@ async def test_script_step_runs_to_end(self) -> None:
AgentDef(
name="run_echo",
type="script",
command="echo",
args=["hello world"],
command=sys.executable,
args=["-c", "print('hello world')"],
routes=[RouteDef(to="$end")],
),
],
Expand Down Expand Up @@ -83,8 +84,8 @@ async def test_script_output_in_context(self) -> None:
AgentDef(
name="checker",
type="script",
command="echo",
args=["test output"],
command=sys.executable,
args=["-c", "print('test output')"],
routes=[RouteDef(to="processor")],
),
AgentDef(
Expand Down Expand Up @@ -131,7 +132,8 @@ async def test_route_on_exit_code_simpleeval_success(self) -> None:
AgentDef(
name="checker",
type="script",
command="true",
command=sys.executable,
args=["-c", "import sys; sys.exit(0)"],
routes=[
RouteDef(to="success_handler", when="exit_code == 0"),
RouteDef(to="failure_handler"),
Expand Down Expand Up @@ -175,7 +177,8 @@ async def test_route_on_exit_code_simpleeval_failure(self) -> None:
AgentDef(
name="checker",
type="script",
command="false",
command=sys.executable,
args=["-c", "import sys; sys.exit(1)"],
routes=[
RouteDef(to="success_handler", when="exit_code == 0"),
RouteDef(to="failure_handler"),
Expand Down Expand Up @@ -219,7 +222,8 @@ async def test_route_on_exit_code_jinja2(self) -> None:
AgentDef(
name="checker",
type="script",
command="true",
command=sys.executable,
args=["-c", "import sys; sys.exit(0)"],
routes=[
RouteDef(to="success_handler", when="{{ output.exit_code == 0 }}"),
RouteDef(to="failure_handler"),
Expand Down Expand Up @@ -267,15 +271,15 @@ async def test_script_counts_toward_iteration_limit(self) -> None:
AgentDef(
name="step1",
type="script",
command="echo",
args=["step1"],
command=sys.executable,
args=["-c", "print('step1')"],
routes=[RouteDef(to="step2")],
),
AgentDef(
name="step2",
type="script",
command="echo",
args=["step2"],
command=sys.executable,
args=["-c", "print('step2')"],
routes=[RouteDef(to="$end")],
),
],
Expand Down Expand Up @@ -303,7 +307,8 @@ async def test_script_non_zero_exit_no_routes_ends(self) -> None:
AgentDef(
name="failing",
type="script",
command="false",
command=sys.executable,
args=["-c", "import sys; sys.exit(1)"],
),
],
output={
Expand Down Expand Up @@ -337,8 +342,8 @@ async def test_mixed_agent_and_script(self) -> None:
AgentDef(
name="setup_script",
type="script",
command="echo",
args=["setup complete"],
command=sys.executable,
args=["-c", "print('setup complete')"],
routes=[RouteDef(to="analyzer")],
),
AgentDef(
Expand Down Expand Up @@ -380,8 +385,8 @@ async def test_script_command_with_workflow_input(self) -> None:
AgentDef(
name="runner",
type="script",
command="echo",
args=["{{ workflow.input.message }}"],
command=sys.executable,
args=["-c", "import sys; print(sys.argv[1])", "{{ workflow.input.message }}"],
routes=[RouteDef(to="$end")],
),
],
Expand Down
Loading
Loading