diff --git a/harness/tests/README.md b/harness/tests/README.md index 796688e..610f8f4 100644 --- a/harness/tests/README.md +++ b/harness/tests/README.md @@ -1,5 +1,45 @@ # Canonical harness tests +## Installation-root prerequisite + +Before executing the suite, provide `HARNESS_ROOT` in the process environment. +It names the installation root containing `harness/` and `repositories/`, not +the `harness/` directory itself. For the current installation, configure the +current agent's `$HERMES_HOME/.env` with: + +```dotenv +HARNESS_ROOT=/opt/data/gitea-agent +``` + +The tests consume the environment supplied by Hermes; they do not open or +source `.env` files, search other profiles, or implement a second configuration +loader. When running outside Hermes, supply the variable explicitly: + +```sh +export HARNESS_ROOT=/opt/data/gitea-agent +python3 -B "$HARNESS_ROOT/harness/tests/run.py" +``` + +On another system, replace the value with that installation's absolute root. +The managed-lock subprocess contention test requires a nonempty, existing +absolute directory and compares its resolved location with the installation +containing the test file. Missing, empty, relative, nonexistent, non-directory, +or different-installation values raise an explicit `HARNESS_ROOT` error before +creating a lock or starting the contention subprocess. There is no fallback to +`Path.cwd()` or to the test-file-derived root. Only the validated import root +is passed as a subprocess argument; no `.env` contents are read or serialized. +This test-only prerequisite does not change the production lock implementation +or the canonical runner's source-bound collection behavior; `--collect-only` +does not require the variable. + +An external-cwd regression runs only the contention test from a temporary +working directory, with `PYTHONPATH` removed and an explicit fixture +`HARNESS_ROOT`, using isolated Python and bounded subprocess timeouts. Invalid +configuration coverage checks that neither lock acquisition nor subprocess +execution occurs. It does not recursively invoke the entire suite. + +## Running the canonical suite + The canonical runner is anchored to its own source tree and works from any current directory. From the repository root: diff --git a/harness/tests/common/test_managed_lock.py b/harness/tests/common/test_managed_lock.py index e026dc0..73abf69 100644 --- a/harness/tests/common/test_managed_lock.py +++ b/harness/tests/common/test_managed_lock.py @@ -241,7 +241,61 @@ class ManagedLockTests(unittest.TestCase): after = path.stat() self.assertEqual((before.st_ino, before.st_size, before.st_mode), (after.st_ino, after.st_size, after.st_mode)) + def test_lock_contention_from_external_cwd_without_pythonpath(self): + root = Path(__file__).resolve().parents[3] + script = """ +import sys +import unittest +sys.path.insert(0, sys.argv[1]) +suite = unittest.defaultTestLoader.loadTestsFromName( + 'harness.tests.common.test_managed_lock.ManagedLockTests.' + 'test_other_process_cannot_acquire_while_context_alive') +result = unittest.TextTestRunner().run(suite) +raise SystemExit(not result.wasSuccessful()) +""" + env = os.environ.copy() + env.pop("PYTHONPATH", None) + env["HARNESS_ROOT"] = str(root) + with tempfile.TemporaryDirectory() as cwd: + result = subprocess.run( + [sys.executable, "-I", "-B", "-c", script, str(root)], + cwd=cwd, env=env, text=True, capture_output=True, timeout=30) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_invalid_harness_root_stops_before_lock_and_subprocess(self): + with tempfile.TemporaryDirectory() as other: + file_path = Path(other) / "not-a-directory" + file_path.write_text("fixture") + for value in (None, "", "relative", other, + str(Path(other) / "missing"), str(file_path)): + with self.subTest(value=value), patch.dict(os.environ), \ + patch(__name__ + ".ManagedLock") as lock, \ + patch(__name__ + ".subprocess.run") as run: + if value is None: + os.environ.pop("HARNESS_ROOT", None) + else: + os.environ["HARNESS_ROOT"] = value + run.return_value.stdout = "blocked" + error = None + try: + self.test_other_process_cannot_acquire_while_context_alive() + except Exception as exc: + error = exc + self.assertIsInstance(error, RuntimeError) + self.assertIn("HARNESS_ROOT", str(error)) + lock.assert_not_called() + run.assert_not_called() + def test_other_process_cannot_acquire_while_context_alive(self): + value = os.environ.get("HARNESS_ROOT") + if not value: + raise RuntimeError("HARNESS_ROOT must be configured") + root = Path(value) + if not root.is_absolute() or not root.is_dir(): + raise RuntimeError("HARNESS_ROOT must be an existing absolute directory") + root = root.resolve() + if root != Path(__file__).resolve().parents[3]: + raise RuntimeError("HARNESS_ROOT does not match this test installation") with tempfile.TemporaryDirectory() as d: path = Path(d) / "process.lock" script = """ @@ -255,7 +309,7 @@ except LockTimeoutError: print('blocked') """ with ManagedLock(path): - result = subprocess.run([sys.executable, "-c", script, str(path), str(Path.cwd())], text=True, capture_output=True, check=True) + result = subprocess.run([sys.executable, "-c", script, str(path), str(root)], text=True, capture_output=True, check=True, timeout=30) self.assertEqual(result.stdout.strip(), "blocked")