158 lines
8.0 KiB
Python
158 lines
8.0 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import hashlib
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
RUNTIME = REPO_ROOT / "harness/runtime"
|
|
|
|
|
|
class ProofRunnerTest(unittest.TestCase):
|
|
def test_cli_builds_fixed_verified_manifest(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
(root / "source.md").write_text("첫 줄\n고정 인용\n", encoding="utf-8")
|
|
request = root / "request.json"
|
|
request.write_text(json.dumps({
|
|
"schema_version": "proof-request/v1",
|
|
"run": {"id": "run-1", "profile": "audit"},
|
|
"proofs": [{"finding": {"id": "L1-F01", "role": "current_state"}, "source": {"path": "source.md", "quote_utf8": "고정 인용"}}],
|
|
}, ensure_ascii=False), encoding="utf-8")
|
|
output = root / "proof.json"
|
|
result = subprocess.run(
|
|
[sys.executable, str(RUNTIME / "proof_runner.py"), str(request), "--repo-root", str(root), "--output", str(output)],
|
|
check=False, capture_output=True, text=True,
|
|
)
|
|
self.assertEqual(result.returncode, 0, result.stdout)
|
|
run_result = json.loads(result.stdout)
|
|
self.assertEqual(run_result["schema_version"], "proof-runner-result/v1")
|
|
self.assertEqual(run_result["status"], "PASS")
|
|
manifest = json.loads(output.read_text(encoding="utf-8"))
|
|
self.assertEqual(manifest["verification"]["status"], "PASS")
|
|
self.assertEqual(manifest["proofs"][0]["execution"]["argv"][0], "proof-runner/exact-utf8-v1")
|
|
self.assertEqual(run_result["manifest"]["sha256"], hashlib.sha256(output.read_bytes()).hexdigest())
|
|
summary = root / "proof-summary.md"
|
|
self.assertTrue(summary.is_file())
|
|
summary_text = summary.read_text(encoding="utf-8")
|
|
self.assertIn(run_result["manifest"]["sha256"], summary_text)
|
|
self.assertIn("- PASS: 1", summary_text)
|
|
self.assertIn("- FAIL: 0", summary_text)
|
|
|
|
def test_ambiguous_quote_fails_without_output(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
(root / "source.md").write_text("same\nsame\n", encoding="utf-8")
|
|
request = root / "request.json"
|
|
request.write_text(json.dumps({"schema_version": "proof-request/v1", "run": {"id": "run-1", "profile": "audit"}, "proofs": [{"finding": {"id": "F1", "role": "quote"}, "source": {"path": "source.md", "quote_utf8": "same"}}]}), encoding="utf-8")
|
|
output = root / "proof.json"
|
|
result = subprocess.run([sys.executable, str(RUNTIME / "proof_runner.py"), str(request), "--repo-root", str(root), "--output", str(output)], check=False, capture_output=True, text=True)
|
|
self.assertEqual(result.returncode, 2)
|
|
self.assertFalse(output.exists())
|
|
self.assertFalse((root / "proof-summary.md").exists())
|
|
self.assertIn("AMBIGUOUS_QUOTE", result.stdout)
|
|
|
|
def test_atomic_summary_failure_rolls_back_manifest(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
(root / "source.md").write_text("proof\n", encoding="utf-8")
|
|
request = root / "request.json"
|
|
request.write_text(json.dumps({
|
|
"schema_version": "proof-request/v1",
|
|
"run": {"id": "run-1", "profile": "audit"},
|
|
"proofs": [{"finding": {"id": "F1", "role": "quote"}, "source": {"path": "source.md", "quote_utf8": "proof"}}],
|
|
}), encoding="utf-8")
|
|
output = root / "proof-manifest.json"
|
|
summary = root / "occupied"
|
|
summary.mkdir()
|
|
result = subprocess.run(
|
|
[sys.executable, str(RUNTIME / "proof_runner.py"), str(request), "--repo-root", str(root), "--output", str(output), "--summary-output", str(summary)],
|
|
check=False, capture_output=True, text=True,
|
|
)
|
|
self.assertEqual(result.returncode, 2, result.stdout)
|
|
self.assertFalse(output.exists())
|
|
|
|
def test_run_namespace_is_confined_to_explicit_run_root(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
base = Path(directory)
|
|
repo = base / "repo"
|
|
run = base / "run"
|
|
repo.mkdir()
|
|
run.mkdir()
|
|
(run / "source.md").write_text("격리 인용\n", encoding="utf-8")
|
|
request = run / "request.json"
|
|
request.write_text(json.dumps({
|
|
"schema_version": "proof-request/v1",
|
|
"run": {"id": "run-namespace", "profile": "audit"},
|
|
"proofs": [{
|
|
"finding": {"id": "F1", "role": "quote"},
|
|
"source": {"namespace": "run", "path": "source.md", "quote_utf8": "격리 인용"},
|
|
}],
|
|
}, ensure_ascii=False), encoding="utf-8")
|
|
output = run / "proof-manifest.json"
|
|
command = [
|
|
sys.executable,
|
|
str(RUNTIME / "proof_runner.py"),
|
|
str(request),
|
|
"--repo-root",
|
|
str(repo),
|
|
"--output",
|
|
str(output),
|
|
]
|
|
|
|
missing_root = subprocess.run(command, check=False, capture_output=True, text=True)
|
|
self.assertEqual(missing_root.returncode, 2)
|
|
self.assertIn("RUN_ROOT_REQUIRED", missing_root.stdout)
|
|
|
|
passed = subprocess.run([*command, "--run-root", str(run)], check=False, capture_output=True, text=True)
|
|
self.assertEqual(passed.returncode, 0, passed.stdout)
|
|
manifest = json.loads(output.read_text(encoding="utf-8"))
|
|
self.assertEqual(manifest["proofs"][0]["source"]["namespace"], "run")
|
|
self.assertEqual(manifest["proofs"][0]["source"]["path"], "source.md")
|
|
|
|
def test_run_namespace_rejects_escape_and_output_outside_allowed_roots(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
base = Path(directory)
|
|
repo = base / "repo"
|
|
run = base / "run"
|
|
outside = base / "outside"
|
|
repo.mkdir()
|
|
run.mkdir()
|
|
outside.mkdir()
|
|
(base / "secret.md").write_text("secret\n", encoding="utf-8")
|
|
request = run / "request.json"
|
|
request.write_text(json.dumps({
|
|
"schema_version": "proof-request/v1",
|
|
"run": {"id": "run-escape", "profile": "audit"},
|
|
"proofs": [{
|
|
"finding": {"id": "F1", "role": "quote"},
|
|
"source": {"namespace": "run", "path": "../secret.md", "quote_utf8": "secret"},
|
|
}],
|
|
}), encoding="utf-8")
|
|
escaped = subprocess.run([
|
|
sys.executable, str(RUNTIME / "proof_runner.py"), str(request),
|
|
"--repo-root", str(repo), "--run-root", str(run), "--output", str(run / "proof.json"),
|
|
], check=False, capture_output=True, text=True)
|
|
self.assertEqual(escaped.returncode, 2)
|
|
self.assertIn("SOURCE_OUTSIDE_NAMESPACE", escaped.stdout)
|
|
|
|
request_body = json.loads(request.read_text(encoding="utf-8"))
|
|
(run / "source.md").write_text("proof\n", encoding="utf-8")
|
|
request_body["proofs"][0]["source"] = {"namespace": "run", "path": "source.md", "quote_utf8": "proof"}
|
|
request.write_text(json.dumps(request_body), encoding="utf-8")
|
|
rejected_output = subprocess.run([
|
|
sys.executable, str(RUNTIME / "proof_runner.py"), str(request),
|
|
"--repo-root", str(repo), "--run-root", str(run), "--output", str(outside / "proof.json"),
|
|
], check=False, capture_output=True, text=True)
|
|
self.assertEqual(rejected_output.returncode, 2)
|
|
self.assertIn("OUTPUT_OUTSIDE_ALLOWED_ROOT", rejected_output.stdout)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|