Files
hermes-agent/tests/tools/test_write_verification.py
T
Teknium 650f69b2b8 fix(file-tools): native writes create the temp file at its final mode before any byte lands
The in-process atomic_write created its temp 0o666 (umask-filtered, 0644 under 022), wrote the new
contents, and only then chmod-ed it to the target's mode, so a rewritten 0600 file's new contents sat
world-readable in the target's directory until the chmod. The shell path it replaces used mktemp's
0600 temp, which never exposed them.

An existing target's temp is now created 0o600 and fchmod-ed to the target's mode before the write
(best effort, as before: a failed fchmod leaves it 0600, never wider). A new target keeps the umask
default (O_CREAT 0o666), the same mode the shell path's chmod "=rw" gave it.

Probe (mode of the temp at the first write, umask 022, existing 0600 target): 0o644 on f980eca8e4,
0o600 now; final modes unchanged (0600 existing, 0644 new).
2026-10-17 04:23:04 -07:00

84 lines
3.6 KiB
Python

"""Tests for write_file post-write content verification (verified flag)."""
import json
from unittest.mock import patch as mock_patch
import pytest
from tools.file_tools import write_file_tool
@pytest.fixture
def workdir(tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
return tmp_path
class TestWriteVerification:
def test_successful_write_reports_verified(self, workdir):
f = workdir / "out.txt"
r = json.loads(write_file_tool(str(f), "hello verified world\n", task_id="t-wv"))
assert r.get("bytes_written") == len("hello verified world\n")
assert r.get("verified") is True
def test_unicode_content_verified(self, workdir):
f = workdir / "uni.txt"
content = "línea → uno · ✓\n"
r = json.loads(write_file_tool(str(f), content, task_id="t-wv"))
assert r.get("verified") is True
def test_crlf_preservation_still_verifies(self, workdir):
# Existing CRLF file: write_file converts LF content to CRLF before
# writing; verification hashes the shim-adjusted content, so it must
# still report verified.
f = workdir / "win.txt"
f.write_bytes(b"old line\r\n")
# Establish a full-read baseline: write_file refuses to overwrite
# existing files the task has never read (stale-write guard).
from tools.file_tools import read_file_tool
json.loads(read_file_tool(str(f), task_id="t-wv"))
r = json.loads(write_file_tool(str(f), "new line\nsecond\n", task_id="t-wv"))
assert "error" not in r
assert r.get("verified") is True
assert b"\r\n" in f.read_bytes()
def test_hash_mismatch_is_hard_error(self, workdir, monkeypatch):
monkeypatch.setenv("HERMES_NATIVE_FILE_READ", "0") # the shell sha256sum transport
f = workdir / "bad.txt"
import tools.file_operations as fo
real_sha = fo.hashlib.sha256
class _WrongHash:
def __init__(self, *a, **k):
self._h = real_sha(b"different content entirely")
def hexdigest(self):
return self._h.hexdigest()
with mock_patch.object(fo.hashlib, "sha256", _WrongHash):
r = json.loads(write_file_tool(str(f), "actual content\n", task_id="t-wv"))
assert "error" in r
@pytest.mark.platforms("posix") # the native write path is POSIX-only (_native_read_enabled)
def test_native_hash_mismatch_is_hard_error(self, workdir):
f = workdir / "bad_native.txt"
with mock_patch("tools.file_operations_native.sha256_of", return_value="0" * 64):
r = json.loads(write_file_tool(str(f), "actual content\n", task_id="t-wv3"))
assert "error" in r and "verification failed" in r["error"]
def test_verification_failure_never_breaks_write(self, workdir, monkeypatch):
# sha256sum unavailable/failing -> verified omitted, write still ok.
monkeypatch.setenv("HERMES_NATIVE_FILE_READ", "0") # the shell sha256sum transport
f = workdir / "ok.txt"
import tools.file_operations as fo
real_exec = fo.ShellFileOperations._exec
def flaky_exec(self, cmd, **kw):
if "sha256sum" in cmd:
raise RuntimeError("no hash binary")
return real_exec(self, cmd, **kw)
with mock_patch.object(fo.ShellFileOperations, "_exec", flaky_exec):
r = json.loads(write_file_tool(str(f), "content lands anyway\n", task_id="t-wv2"))
assert "error" not in r
assert f.read_text() == "content lands anyway\n"
assert "verified" not in r or r.get("verified") is None