fix(tests): read and write test files as UTF-8 so the suite runs on Windows
`tests/hermes_cli/test_plugins_cmd.py::TestNoAutoActivation::test_compressor_default_ignores_plugin`
fails on every Windows machine:
UnicodeDecodeError: 'charmap' codec can't decode byte 0x8f in
position 47744: character maps to <undefined>
The test reads `run_agent.py` back as text to assert a removed comment is
gone, but called `open()` with no `encoding=`. Python then falls back to
the locale preferred encoding, which is cp1252 on a default Windows
install rather than UTF-8. `run_agent.py` contains nine bytes cp1252
leaves undefined, so the read raises before the assertion is reached. On
Linux and macOS the preferred encoding is UTF-8 and the same line is
fine, which is why CI never caught it.
That one line is the only active failure. The rest of this change closes
the same gap in the files it touches, which `scripts/check-windows-footguns.py`
flags and which the #71014 read_text campaign has been working through
elsewhere in the tree:
- `tests/hermes_cli/test_plugins_cmd.py`: nine bare `write_text`/`read_text`
calls writing YAML manifests, config and plugin sources
- `tests/tools/test_web_tools_truncate.py`: reads stored extracted web text,
which is arbitrary content from the internet
- `tests/stress/test_atypical_scenarios.py`: writes and reads worker task
ids and a barrier file
All three files are now clean under `check-windows-footguns.py`.
Reads go through `Path.read_text(encoding="utf-8")` rather than
`open(...).read()`, which also closes the handle instead of leaving it to
the garbage collector. On Windows a live handle blocks tmpdir cleanup, so
that part is not cosmetic either.
No new test. The repaired test is the regression coverage: it fails
before this change and passes after, on Windows.
This commit is contained in:
parent
7b1f02377f
commit
5945929d4b
|
|
@ -189,14 +189,14 @@ class TestReadManifest:
|
|||
assert result == {}
|
||||
|
||||
def test_invalid_yaml_returns_empty_and_logs(self, tmp_path, caplog):
|
||||
(tmp_path / "plugin.yaml").write_text(": : : bad yaml [[[")
|
||||
(tmp_path / "plugin.yaml").write_text(": : : bad yaml [[[", encoding="utf-8")
|
||||
with caplog.at_level(logging.WARNING, logger="hermes_cli.plugins_cmd"):
|
||||
result = _read_manifest(tmp_path)
|
||||
assert result == {}
|
||||
assert any("Failed to read plugin.yaml" in r.message for r in caplog.records)
|
||||
|
||||
def test_empty_file_returns_empty(self, tmp_path):
|
||||
(tmp_path / "plugin.yaml").write_text("")
|
||||
(tmp_path / "plugin.yaml").write_text("", encoding="utf-8")
|
||||
result = _read_manifest(tmp_path)
|
||||
assert result == {}
|
||||
|
||||
|
|
@ -388,7 +388,7 @@ class TestCopyExampleFiles:
|
|||
|
||||
# Create example file
|
||||
example_file = tmp_path / "config.yaml.example"
|
||||
example_file.write_text("key: value")
|
||||
example_file.write_text("key: value", encoding="utf-8")
|
||||
|
||||
_copy_example_files(tmp_path, console)
|
||||
|
||||
|
|
@ -404,7 +404,7 @@ class TestCopyExampleFiles:
|
|||
|
||||
# Create example file
|
||||
example_file = tmp_path / "config.yaml.example"
|
||||
example_file.write_text("key: value")
|
||||
example_file.write_text("key: value", encoding="utf-8")
|
||||
|
||||
# Mock shutil.copy2 to raise an error
|
||||
with patch(
|
||||
|
|
@ -497,10 +497,10 @@ class TestProviderDiscovery:
|
|||
"""Saving a context engine persists to config.yaml."""
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
config_file = tmp_path / "config.yaml"
|
||||
config_file.write_text("context:\n engine: compressor\n")
|
||||
config_file.write_text("context:\n engine: compressor\n", encoding="utf-8")
|
||||
from hermes_cli.plugins_cmd import _save_context_engine
|
||||
_save_context_engine("lcm")
|
||||
content = yaml.safe_load(config_file.read_text())
|
||||
content = yaml.safe_load(config_file.read_text(encoding="utf-8"))
|
||||
assert content["context"]["engine"] == "lcm"
|
||||
|
||||
|
||||
|
|
@ -525,7 +525,7 @@ class TestNoAutoActivation:
|
|||
# This tests the run_agent.py logic indirectly by checking that the
|
||||
# code path for default config doesn't call get_plugin_context_engine.
|
||||
import run_agent as ra_module
|
||||
source = open(ra_module.__file__).read()
|
||||
source = Path(ra_module.__file__).read_text(encoding="utf-8")
|
||||
# The old code had: "Even with default config, check if a plugin registered one"
|
||||
# The fix removes this. Verify it's gone.
|
||||
assert "Even with default config, check if a plugin registered one" not in source
|
||||
|
|
@ -545,16 +545,19 @@ class TestSubdirInstallE2E:
|
|||
|
||||
repo_root.mkdir(parents=True, exist_ok=True)
|
||||
# Root-level noise: docs + tests that should NOT be installed.
|
||||
(repo_root / "README.md").write_text("# Monorepo docs\n")
|
||||
(repo_root / "README.md").write_text("# Monorepo docs\n", encoding="utf-8")
|
||||
(repo_root / "tests").mkdir()
|
||||
(repo_root / "tests" / "test_x.py").write_text("def test_x():\n pass\n")
|
||||
(repo_root / "tests" / "test_x.py").write_text(
|
||||
"def test_x():\n pass\n", encoding="utf-8"
|
||||
)
|
||||
# The actual plugin in a subdirectory.
|
||||
plugin_dir = repo_root / "my-plugin"
|
||||
plugin_dir.mkdir()
|
||||
(plugin_dir / "plugin.yaml").write_text(
|
||||
"name: my-plugin\nmanifest_version: 1\ndescription: A subdir plugin\n"
|
||||
"name: my-plugin\nmanifest_version: 1\ndescription: A subdir plugin\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(plugin_dir / "__init__.py").write_text("# plugin entry\n")
|
||||
(plugin_dir / "__init__.py").write_text("# plugin entry\n", encoding="utf-8")
|
||||
|
||||
env = {
|
||||
**os.environ,
|
||||
|
|
|
|||
|
|
@ -705,7 +705,7 @@ def _idempotency_race_worker(hermes_home: str, key: str, result_file: str,
|
|||
)
|
||||
finally:
|
||||
conn.close()
|
||||
with open(result_file, "w") as f:
|
||||
with open(result_file, "w", encoding="utf-8") as f:
|
||||
f.write(tid)
|
||||
|
||||
|
||||
|
|
@ -731,12 +731,16 @@ def _(home, kb):
|
|||
p.start()
|
||||
time.sleep(0.1) # let them hit the spin
|
||||
# Fire the gun
|
||||
with open(barrier, "w") as f:
|
||||
with open(barrier, "w", encoding="utf-8") as f:
|
||||
f.write("go")
|
||||
for p in procs:
|
||||
p.join(timeout=10)
|
||||
|
||||
tids = [open(r).read().strip() for r in results if os.path.exists(r)]
|
||||
tids = [
|
||||
Path(r).read_text(encoding="utf-8").strip()
|
||||
for r in results
|
||||
if os.path.exists(r)
|
||||
]
|
||||
assert len(tids) == 2, f"only {len(tids)} workers finished"
|
||||
assert tids[0] == tids[1], (
|
||||
f"idempotency key race produced two different tasks: {tids}"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ _get_extract_char_limit, and the end-to-end web_extract_tool truncation behavior
|
|||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
|
@ -49,7 +50,7 @@ class TestTruncation:
|
|||
path_line = next(ln for ln in out.splitlines() if "Full text saved to:" in ln)
|
||||
stored_path = path_line.split("Full text saved to:", 1)[1].strip()
|
||||
assert os.path.exists(stored_path)
|
||||
full = open(stored_path).read()
|
||||
full = Path(stored_path).read_text(encoding="utf-8")
|
||||
assert "UNIQUE_MIDDLE_MARKER" in full
|
||||
assert "row 2500" in full # the omitted-middle row is in the stored file
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue