[verified] Enforce delegation adoption at runtime consumers
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from harness.lib import delegation_adoption as adoption
|
||||
|
||||
|
||||
class DelegationAdoptionTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory(dir="/opt/data/cache/scratch")
|
||||
self.base = Path(self.tmp.name)
|
||||
self.bundle = self.base / "bundle"
|
||||
(self.bundle / "src").mkdir(parents=True)
|
||||
(self.bundle / "evidence").mkdir()
|
||||
(self.bundle / "src/value.py").write_bytes(b"VALUE = 1\n")
|
||||
(self.bundle / "evidence/run.txt").write_bytes(b"run\n")
|
||||
self.files = {"src/value.py": hashlib.sha256((self.bundle / "src/value.py").read_bytes()).hexdigest()}
|
||||
self.bundle_sha = self.digest(self.files)
|
||||
self.context = {
|
||||
"schema": "gitea-agent.delegation-evidence-context/v1",
|
||||
"source": {"kind": "captured_worktree", "base_revision": "a" * 40},
|
||||
"stage": "implementation",
|
||||
"bundle_sha256": self.bundle_sha,
|
||||
"files": self.files,
|
||||
"allowed_actions": ["static"],
|
||||
"obligations": [{"id": "static", "kind": "static"}],
|
||||
"executions": [],
|
||||
}
|
||||
self.result = {
|
||||
"schema": "gitea-agent.delegation-evidence-result/v1",
|
||||
"source": copy.deepcopy(self.context["source"]),
|
||||
"stage": self.context["stage"],
|
||||
"bundle_sha256": self.bundle_sha,
|
||||
"claims": [{"id": "static", "status": "supported", "anchors": [{
|
||||
"kind": "lines", "path": "src/value.py", "start_line": 1,
|
||||
"end_line": 1, "text_sha256": hashlib.sha256(b"VALUE = 1\n").hexdigest(),
|
||||
}], "execution_ids": []}],
|
||||
}
|
||||
self.context_path = self.base / "context.json"
|
||||
self.result_path = self.base / "result.json"
|
||||
self.context_path.write_text(json.dumps(self.context))
|
||||
self.result_path.write_text(json.dumps(self.result))
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
@staticmethod
|
||||
def digest(value):
|
||||
return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
|
||||
|
||||
def declaration(self):
|
||||
return {
|
||||
"mode": "delegated",
|
||||
"bundle_root": str(self.bundle),
|
||||
"context_path": str(self.context_path),
|
||||
"result_path": str(self.result_path),
|
||||
"git_dir": None,
|
||||
}
|
||||
|
||||
def test_valid_delegated_declaration_returns_fresh_pass_result(self):
|
||||
expected_source = copy.deepcopy(self.context["source"])
|
||||
out = adoption.require_adoption(
|
||||
self.declaration(), expected_source=expected_source, expected_stage="implementation"
|
||||
)
|
||||
self.assertEqual(out["status"], "pass")
|
||||
self.assertEqual(out["counts"]["obligations"], 1)
|
||||
|
||||
def test_valid_git_tree_declaration_uses_expected_revision(self):
|
||||
repo = self.base / "repo"
|
||||
repo.mkdir()
|
||||
(repo / "src").mkdir()
|
||||
(repo / "src/value.py").write_bytes(b"VALUE = 1\n")
|
||||
subprocess.run(["/usr/bin/git", "init", "-q", str(repo)], check=True)
|
||||
subprocess.run(["/usr/bin/git", "-C", str(repo), "config", "user.email", "test@example.invalid"], check=True)
|
||||
subprocess.run(["/usr/bin/git", "-C", str(repo), "config", "user.name", "Test"], check=True)
|
||||
subprocess.run(["/usr/bin/git", "-C", str(repo), "add", "."], check=True)
|
||||
subprocess.run(["/usr/bin/git", "-C", str(repo), "commit", "-q", "-m", "fixture"], check=True)
|
||||
revision = subprocess.check_output(["/usr/bin/git", "-C", str(repo), "rev-parse", "HEAD"], text=True).strip()
|
||||
context = copy.deepcopy(self.context)
|
||||
context["source"] = {"kind": "git_tree", "revision": revision}
|
||||
result = copy.deepcopy(self.result)
|
||||
result["source"] = copy.deepcopy(context["source"])
|
||||
self.context_path.write_text(json.dumps(context))
|
||||
self.result_path.write_text(json.dumps(result))
|
||||
declaration = self.declaration()
|
||||
declaration["git_dir"] = str(repo / ".git")
|
||||
out = adoption.require_adoption(declaration, expected_source=context["source"], expected_stage="implementation")
|
||||
self.assertEqual(out["status"], "pass")
|
||||
|
||||
def test_explicit_none_is_the_only_non_delegated_success(self):
|
||||
self.assertIsNone(adoption.require_adoption(
|
||||
{"mode": "none"}, expected_source=self.context["source"], expected_stage="implementation"
|
||||
))
|
||||
|
||||
def test_missing_declaration_is_rejected_with_sanitized_error(self):
|
||||
with self.assertRaisesRegex(adoption.AdoptionError, "^delegation adoption rejected$"):
|
||||
adoption.require_adoption(None, expected_source=self.context["source"], expected_stage="implementation")
|
||||
|
||||
def test_unsupported_or_extra_declaration_fields_are_rejected(self):
|
||||
for declaration in (
|
||||
{"mode": "worker"},
|
||||
{"mode": "none", "bundle_root": str(self.bundle)},
|
||||
{**self.declaration(), "extra": 1},
|
||||
):
|
||||
with self.subTest(declaration=declaration):
|
||||
with self.assertRaises(adoption.AdoptionError):
|
||||
adoption.require_adoption(declaration, expected_source=self.context["source"], expected_stage="implementation")
|
||||
|
||||
def test_expected_source_and_stage_are_independent_and_exact(self):
|
||||
for source, stage in (
|
||||
({"kind": "captured_worktree", "base_revision": "b" * 40}, "implementation"),
|
||||
(self.context["source"], "review"),
|
||||
):
|
||||
with self.subTest(source=source, stage=stage), self.assertRaises(adoption.AdoptionError):
|
||||
adoption.require_adoption(self.declaration(), expected_source=source, expected_stage=stage)
|
||||
|
||||
def test_stale_context_result_or_bundle_is_rejected(self):
|
||||
for field, value in (("stage", "review"), ("bundle_sha256", "0" * 64)):
|
||||
context = copy.deepcopy(self.context)
|
||||
context[field] = value
|
||||
self.context_path.write_text(json.dumps(context))
|
||||
with self.subTest(field=field), self.assertRaises(adoption.AdoptionError):
|
||||
adoption.require_adoption(self.declaration(), expected_source=self.context["source"], expected_stage="implementation")
|
||||
self.context_path.write_text(json.dumps(self.context))
|
||||
self.result_path.write_text(json.dumps({**self.result, "stage": "review"}))
|
||||
with self.assertRaises(adoption.AdoptionError):
|
||||
adoption.require_adoption(self.declaration(), expected_source=self.context["source"], expected_stage="implementation")
|
||||
self.result_path.write_text(json.dumps(self.result))
|
||||
(self.bundle / "src/value.py").write_bytes(b"VALUE = 2\n")
|
||||
with self.assertRaises(adoption.AdoptionError):
|
||||
adoption.require_adoption(self.declaration(), expected_source=self.context["source"], expected_stage="implementation")
|
||||
|
||||
def test_empty_obligations_or_claims_cannot_be_adopted(self):
|
||||
for context, result in (
|
||||
({**self.context, "obligations": []}, self.result),
|
||||
(self.context, {**self.result, "claims": []}),
|
||||
):
|
||||
self.context_path.write_text(json.dumps(context))
|
||||
self.result_path.write_text(json.dumps(result))
|
||||
with self.subTest(context=context, result=result), self.assertRaises(adoption.AdoptionError):
|
||||
adoption.require_adoption(self.declaration(), expected_source=self.context["source"], expected_stage="implementation")
|
||||
self.context_path.write_text(json.dumps(self.context))
|
||||
self.result_path.write_text(json.dumps(self.result))
|
||||
|
||||
def test_block_and_verifier_exception_fail_closed(self):
|
||||
with mock.patch.object(adoption.delegation_evidence, "verify_evidence", return_value={"status": "block"}):
|
||||
with self.assertRaises(adoption.AdoptionError):
|
||||
adoption.require_adoption(self.declaration(), expected_source=self.context["source"], expected_stage="implementation")
|
||||
with mock.patch.object(adoption.delegation_evidence, "verify_evidence", side_effect=RuntimeError("secret")):
|
||||
with self.assertRaisesRegex(adoption.AdoptionError, "^delegation adoption rejected$"):
|
||||
adoption.require_adoption(self.declaration(), expected_source=self.context["source"], expected_stage="implementation")
|
||||
|
||||
def test_verifier_is_called_again_after_a_prior_pass(self):
|
||||
with mock.patch.object(adoption.delegation_evidence, "verify_evidence", wraps=adoption.delegation_evidence.verify_evidence) as verifier:
|
||||
adoption.require_adoption(self.declaration(), expected_source=self.context["source"], expected_stage="implementation")
|
||||
adoption.require_adoption(self.declaration(), expected_source=self.context["source"], expected_stage="implementation")
|
||||
self.assertEqual(verifier.call_count, 2)
|
||||
|
||||
def test_malformed_expected_inputs_and_paths_fail_closed(self):
|
||||
cases = (
|
||||
({"kind": "captured_worktree", "base_revision": "A" * 40}, "implementation"),
|
||||
(self.context["source"], ""),
|
||||
(self.context["source"], None),
|
||||
)
|
||||
for source, stage in cases:
|
||||
with self.subTest(source=source, stage=stage), self.assertRaises(adoption.AdoptionError):
|
||||
adoption.require_adoption({"mode": "none"}, expected_source=source, expected_stage=stage)
|
||||
for key, value in (("bundle_root", str(self.base / ".." / self.base.name / "bundle")),
|
||||
("context_path", "relative.json"), ("result_path", str(self.result_path) + "\x00"),
|
||||
("git_dir", 3)):
|
||||
declaration = self.declaration()
|
||||
declaration[key] = value
|
||||
with self.subTest(key=key), self.assertRaises(adoption.AdoptionError):
|
||||
adoption.require_adoption(declaration, expected_source=self.context["source"], expected_stage="implementation")
|
||||
|
||||
def test_all_git_declaration_paths_reject_symlinks_at_runtime(self):
|
||||
from harness.tests.common.test_delegation_consumers_integration import RealEvidence
|
||||
evidence = RealEvidence(self.base / 'safe-path-fixture')
|
||||
declaration = evidence.declaration()
|
||||
for field in ('bundle_root', 'context_path', 'result_path', 'git_dir'):
|
||||
alias = self.base / ('alias-' + field)
|
||||
alias.symlink_to(declaration[field], target_is_directory=field in ('bundle_root', 'git_dir'))
|
||||
bad = {**declaration, field: str(alias)}
|
||||
with self.subTest(field=field), self.assertRaisesRegex(adoption.AdoptionError, '^delegation adoption rejected$'):
|
||||
adoption.require_adoption(bad, expected_source=evidence.context['source'], expected_stage='spec-review')
|
||||
alias_parent = self.base / 'alias-parent'
|
||||
alias_parent.symlink_to(evidence.root, target_is_directory=True)
|
||||
with self.assertRaises(adoption.AdoptionError):
|
||||
adoption.require_adoption({**declaration, 'context_path': str(alias_parent / 'context.json')},
|
||||
expected_source=evidence.context['source'], expected_stage='spec-review')
|
||||
|
||||
def test_duplicate_json_and_missing_files_fail_closed(self):
|
||||
self.context_path.write_text('{"schema":"x","schema":"y"}')
|
||||
with self.assertRaises(adoption.AdoptionError):
|
||||
adoption.require_adoption(self.declaration(), expected_source=self.context["source"], expected_stage="implementation")
|
||||
self.context_path.write_text(json.dumps(self.context))
|
||||
self.result_path.unlink()
|
||||
with self.assertRaises(adoption.AdoptionError):
|
||||
adoption.require_adoption(self.declaration(), expected_source=self.context["source"], expected_stage="implementation")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Executable consumer declaration contract wiring."""
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
|
||||
class DelegationAdoptionContractTests(unittest.TestCase):
|
||||
def test_common_runtime_contract_is_explicit(self):
|
||||
text = (ROOT / 'harness/operations/repository-cache.md').read_text()
|
||||
for token in ('require_adoption', 'expected_source', 'expected_stage',
|
||||
'--no-delegated-evidence', '--delegation-adoption',
|
||||
'spec-review', 'code-review', 'implementation-delivery',
|
||||
'PASS receipt', 'sandbox'):
|
||||
with self.subTest(token=token):
|
||||
self.assertIn(token, text)
|
||||
|
||||
def test_each_consumer_skill_requires_runtime_declaration(self):
|
||||
paths = (
|
||||
'spec/skills/gitea-implementation-spec-review',
|
||||
'impl/skills/gitea-issue-implementation',
|
||||
'impl/skills/gitea-issue-rework',
|
||||
'review/skills/gitea-code-review',
|
||||
)
|
||||
for path in paths:
|
||||
text = (ROOT / 'harness/agents' / path / 'SKILL.md').read_text()
|
||||
with self.subTest(path=path):
|
||||
self.assertIn('require_adoption', text)
|
||||
self.assertIn('mode', text)
|
||||
self.assertIn('none', text)
|
||||
self.assertIn('delegated', text)
|
||||
self.assertIn('delegation-adoption-runtime-consumption', text)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,46 @@
|
||||
import importlib.util
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
PATH = ROOT / "harness/agents/impl/skills/gitea-issue-implementation/scripts/delivery_adoption.py"
|
||||
spec = importlib.util.spec_from_file_location("delivery_adoption", PATH)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
|
||||
|
||||
class DeliveryConsumerTests(unittest.TestCase):
|
||||
def test_delivery_requires_adoption_then_calls_adapter_once(self):
|
||||
calls = []
|
||||
with patch.object(mod, "require_adoption", return_value={"status": "pass"}) as adopted:
|
||||
result = mod.deliver(lambda: {"adapter": "actual"}, head="a" * 40,
|
||||
declaration={"mode": "delegated"})
|
||||
adopted.assert_called_once_with({"mode": "delegated"}, expected_source={"kind": "git_tree", "revision": "a" * 40}, expected_stage="implementation-delivery")
|
||||
self.assertEqual(result, {"adapter": "actual"})
|
||||
|
||||
def test_rejected_adoption_blocks_adapter(self):
|
||||
calls = []
|
||||
with patch.object(mod, "require_adoption", side_effect=mod.AdoptionError("no")):
|
||||
with self.assertRaises(mod.AdoptionError):
|
||||
mod.deliver(lambda: calls.append("called"), head="a" * 40,
|
||||
declaration={"mode": "delegated"})
|
||||
self.assertEqual(calls, [])
|
||||
|
||||
def test_missing_declaration_blocks_adapter(self):
|
||||
calls = []
|
||||
with self.assertRaises(mod.AdoptionError):
|
||||
mod.deliver(lambda: calls.append("called"), head="a" * 40, declaration=None)
|
||||
self.assertEqual(calls, [])
|
||||
|
||||
def test_explicit_none_still_validates_head_and_calls_adapter(self):
|
||||
calls = []
|
||||
with patch.object(mod, "require_adoption", return_value={"status": "none"}):
|
||||
with self.assertRaises(mod.AdoptionError):
|
||||
mod.deliver(lambda: calls.append("called"), head="bad", declaration={"mode": "none"})
|
||||
mod.deliver(lambda: calls.append("called"), head="a" * 40, declaration={"mode": "none"})
|
||||
self.assertEqual(calls, ["called"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,298 @@
|
||||
import copy
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
SCRATCH = Path('/opt/data/cache/scratch/delegation-adoption-integration')
|
||||
SPEC = ROOT / 'harness/agents/spec/skills/gitea-implementation-spec-review/scripts/review_support.py'
|
||||
REWORK = ROOT / 'harness/agents/impl/skills/gitea-issue-rework/scripts/rework_comment.py'
|
||||
DELIVERY = ROOT / 'harness/agents/impl/skills/gitea-issue-implementation/scripts/delivery_adoption.py'
|
||||
NATIVE = ROOT / 'harness/agents/review/skills/gitea-code-review/scripts/native_review.py'
|
||||
|
||||
def load(path, name):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
support = load(SPEC, 'integration_spec_support')
|
||||
delivery = load(DELIVERY, 'integration_delivery')
|
||||
native = load(NATIVE, 'integration_native')
|
||||
rework = load(REWORK, 'integration_rework')
|
||||
|
||||
class RealEvidence:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.bundle = root / 'bundle'
|
||||
(self.bundle / 'src').mkdir(parents=True)
|
||||
(self.bundle / 'tests').mkdir()
|
||||
(self.bundle / 'evidence').mkdir()
|
||||
(self.bundle / 'src/a.py').write_text('VALUE = 1\n')
|
||||
(self.bundle / 'tests/test_a.py').write_text('def test_value():\n assert 1 == 1\n')
|
||||
(self.bundle / 'evidence/red.txt').write_text('red\n')
|
||||
(self.bundle / 'evidence/green.txt').write_text('green\n')
|
||||
subprocess.run(['/usr/bin/git', 'init', '-q', str(self.root / 'repo')], check=True)
|
||||
repo = self.root / 'repo'
|
||||
(repo / 'src').mkdir(); (repo / 'tests').mkdir()
|
||||
(repo / 'src/a.py').write_text('VALUE = 1\n')
|
||||
(repo / 'tests/test_a.py').write_text('def test_value():\n assert 1 == 1\n')
|
||||
subprocess.run(['/usr/bin/git', '-C', str(repo), 'config', 'user.email', 'test@example.invalid'], check=True)
|
||||
subprocess.run(['/usr/bin/git', '-C', str(repo), 'config', 'user.name', 'Integration'], check=True)
|
||||
subprocess.run(['/usr/bin/git', '-C', str(repo), 'add', '.'], check=True)
|
||||
subprocess.run(['/usr/bin/git', '-C', str(repo), 'commit', '-q', '-m', 'fixture'], check=True)
|
||||
self.revision = subprocess.check_output(['/usr/bin/git', '-C', str(repo), 'rev-parse', 'HEAD'], text=True).strip()
|
||||
self.git_dir = repo / '.git'
|
||||
self.files = {p: hashlib.sha256((self.bundle / p).read_bytes()).hexdigest() for p in ('src/a.py', 'tests/test_a.py')}
|
||||
self.bundle_sha = self.digest(self.files)
|
||||
self.context = {'schema': 'gitea-agent.delegation-evidence-context/v1',
|
||||
'source': {'kind': 'git_tree', 'revision': self.revision},
|
||||
'stage': 'spec-review', 'bundle_sha256': self.bundle_sha,
|
||||
'files': self.files, 'allowed_actions': ['static', 'execute'],
|
||||
'obligations': [{'id': 'static', 'kind': 'static'}, {'id': 'rg', 'kind': 'red_green'}],
|
||||
'executions': [self.execution('red', 1, 'assertion', 'evidence/red.txt'),
|
||||
self.execution('green', 0, 'none', 'evidence/green.txt')]}
|
||||
line_hash = hashlib.sha256(b'VALUE = 1\n').hexdigest()
|
||||
self.result = {'schema': 'gitea-agent.delegation-evidence-result/v1',
|
||||
'source': copy.deepcopy(self.context['source']), 'stage': 'spec-review',
|
||||
'bundle_sha256': self.bundle_sha,
|
||||
'claims': [{'id': 'static', 'status': 'supported', 'anchors': [{'kind': 'lines', 'path': 'src/a.py', 'start_line': 1, 'end_line': 1, 'text_sha256': line_hash}], 'execution_ids': []},
|
||||
{'id': 'rg', 'status': 'supported', 'anchors': [{'kind': 'test', 'path': 'tests/test_a.py', 'symbol': 'test_value'}], 'execution_ids': ['red', 'green']}]}
|
||||
self.context_path = root / 'context.json'; self.result_path = root / 'result.json'
|
||||
self.write()
|
||||
|
||||
@staticmethod
|
||||
def digest(value):
|
||||
return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':')).encode()).hexdigest()
|
||||
|
||||
def execution(self, eid, code, kind, path):
|
||||
return {'id': eid, 'test_id': 'tests/test_a.py::test_value', 'bundle_sha256': self.bundle_sha,
|
||||
'test_file_sha256': self.files['tests/test_a.py'], 'argv': ['python', '-m', 'pytest'],
|
||||
'exit_code': code, 'failure_kind': kind, 'evidence_path': path,
|
||||
'evidence_sha256': hashlib.sha256((self.bundle / path).read_bytes()).hexdigest(), 'origin': 'direct'}
|
||||
|
||||
def write(self):
|
||||
self.context_path.write_text(json.dumps(self.context))
|
||||
self.result_path.write_text(json.dumps(self.result))
|
||||
|
||||
def declaration(self):
|
||||
return {'mode': 'delegated', 'bundle_root': str(self.bundle), 'context_path': str(self.context_path),
|
||||
'result_path': str(self.result_path), 'git_dir': str(self.git_dir)}
|
||||
|
||||
def set_stage(self, stage):
|
||||
self.context['stage'] = stage; self.result['stage'] = stage; self.write()
|
||||
|
||||
class IntegrationTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
SCRATCH.mkdir(parents=True, exist_ok=True)
|
||||
self.tmp = tempfile.TemporaryDirectory(dir=SCRATCH)
|
||||
self.ev = RealEvidence(Path(self.tmp.name))
|
||||
|
||||
def tearDown(self): self.tmp.cleanup()
|
||||
|
||||
def test_spec_real_git_pass_roundtrip_and_blocks_drift(self):
|
||||
self.ev.set_stage('spec-review')
|
||||
result = {'schema': support.SCHEMA, 'target': {'owner': 'acme', 'repository': 'repo', 'issue': 3, 'pull': 7, 'head': self.ev.revision},
|
||||
'contract_sha256': hashlib.sha256(b'contract').hexdigest(), 'verdict': 'approve',
|
||||
'criteria': [{'criterion': 'REQ-1', 'contract': 'contract', 'implementation_paths': ['src/a.py'], 'test_paths': ['tests/test_a.py'], 'evidence': 'verified', 'result': 'satisfied', 'reason': 'ok'}],
|
||||
'scope': {'missing': [], 'excess': [], 'spec_conflicts': []}, 'conclusion': 'ok'}
|
||||
body = support.render_review(result, {'src/a.py', 'tests/test_a.py'}, declaration=self.ev.declaration())
|
||||
self.assertEqual(support.parse_review(body, {'src/a.py', 'tests/test_a.py'}), result)
|
||||
self.ev.set_stage('other'); self.ev.write()
|
||||
with self.assertRaises(support.ValidationError): support.render_review(result, {'src/a.py', 'tests/test_a.py'}, declaration=self.ev.declaration())
|
||||
|
||||
def test_spec_cli_delegated_import_without_pythonpath_and_block_is_quiet(self):
|
||||
self.ev.set_stage('spec-review')
|
||||
result = {'schema': support.SCHEMA, 'target': {'owner': 'acme', 'repository': 'repo', 'issue': 3, 'pull': 7, 'head': self.ev.revision},
|
||||
'contract_sha256': hashlib.sha256(b'contract').hexdigest(), 'verdict': 'approve',
|
||||
'criteria': [{'criterion': 'REQ-1', 'contract': 'contract', 'implementation_paths': ['src/a.py'], 'test_paths': [], 'evidence': 'verified', 'result': 'satisfied', 'reason': 'ok'}],
|
||||
'scope': {'missing': [], 'excess': [], 'spec_conflicts': []}, 'conclusion': 'ok'}
|
||||
result_path = Path(self.tmp.name) / 'spec-result.json'; paths_path = Path(self.tmp.name) / 'paths.json'; decl_path = Path(self.tmp.name) / 'decl.json'; result_path.write_text(json.dumps(result)); paths_path.write_text(json.dumps(['src/a.py'])); decl_path.write_text(json.dumps(self.ev.declaration()))
|
||||
env = os.environ.copy(); env.pop('PYTHONPATH', None)
|
||||
cmd = [sys.executable, str(SPEC), 'render', '--result', str(result_path), '--allowed-paths', str(paths_path), '--delegation-adoption', str(decl_path)]
|
||||
run = subprocess.run(cmd, cwd=self.tmp.name, env=env, text=True, capture_output=True)
|
||||
self.assertEqual(run.returncode, 0, run.stderr); self.assertIn('implementation-spec-review/v1', run.stdout)
|
||||
self.ev.context['stage'] = 'drift'; self.ev.write()
|
||||
blocked = subprocess.run(cmd, cwd=self.tmp.name, env=env, text=True, capture_output=True)
|
||||
self.assertNotEqual(blocked.returncode, 0); self.assertEqual(blocked.stdout, '')
|
||||
|
||||
def test_delivery_real_pass_returns_adapter_and_blocks_without_write(self):
|
||||
self.ev.set_stage('implementation-delivery')
|
||||
calls = []
|
||||
out = delivery.deliver(lambda: {'receipt': 'actual'}, head=self.ev.revision, declaration=self.ev.declaration())
|
||||
self.assertEqual(out, {'receipt': 'actual'})
|
||||
self.ev.bundle.joinpath('src/a.py').write_text('VALUE = 2\n')
|
||||
with self.assertRaises(delivery.AdoptionError): delivery.deliver(lambda: calls.append(1), head=self.ev.revision, declaration=self.ev.declaration())
|
||||
self.assertEqual(calls, [])
|
||||
|
||||
def bound_publication_fixture(self):
|
||||
from harness.tests.review import test_native_review as fixture
|
||||
plan, before, after, exact = fixture.publication_fixture()
|
||||
old_head = plan['target']['head']
|
||||
def replace(value):
|
||||
if isinstance(value, str): return self.ev.revision if value == old_head else value
|
||||
if isinstance(value, list): return [replace(x) for x in value]
|
||||
if isinstance(value, dict): return {k: replace(v) for k, v in value.items()}
|
||||
return value
|
||||
plan, before, after, exact = map(replace, (plan, before, after, exact))
|
||||
plan['body'] = native.marker(plan) + '\n- Verdict: `approve`\n검토 완료.\n'
|
||||
exact['body'] = plan['body']
|
||||
for review in before['reviews'] + after['reviews']:
|
||||
if review['id'] == plan['spec_review_id']:
|
||||
original = fixture.publication_fixture()[1]['reviews'][1]['body']
|
||||
sr = support.parse_review(original, {'src/a.py'})
|
||||
sr['target']['head'] = self.ev.revision
|
||||
review['body'] = support.render_review(sr, {'src/a.py'}, declaration={'mode': 'none'})
|
||||
review['commit_id'] = self.ev.revision
|
||||
elif review['id'] == 10:
|
||||
review['body'] = plan['body']
|
||||
exact['commit_id'] = self.ev.revision
|
||||
native.validate_snapshot(plan, before)
|
||||
return fixture, plan, before, after, exact
|
||||
|
||||
def test_review_real_git_adoption_posts_once_then_noops(self):
|
||||
self.ev.set_stage('code-review')
|
||||
fixture, plan, before, after, exact = self.bound_publication_fixture()
|
||||
api = fixture.FakeAPI(plan, before, after, exact)
|
||||
with tempfile.TemporaryDirectory(dir=self.tmp.name) as journal:
|
||||
first = native.publish(plan, before, api, fixture.FakeLease(), journal, declaration=self.ev.declaration())
|
||||
second = native.publish(plan, before, api, fixture.FakeLease(), journal, declaration=self.ev.declaration())
|
||||
self.assertEqual(first['mode'], 'created')
|
||||
self.assertEqual(second['mode'], 'no_op')
|
||||
self.assertEqual(api.posts, 1)
|
||||
|
||||
def test_review_fresh_adoption_blocks_post_if_bundle_changes_before_create(self):
|
||||
self.ev.set_stage('code-review')
|
||||
fixture, plan, before, after, exact = self.bound_publication_fixture()
|
||||
api = fixture.FakeAPI(plan, before, after, exact)
|
||||
original_snapshot = api.snapshot
|
||||
calls = {'n': 0}
|
||||
def mutating_snapshot(target):
|
||||
calls['n'] += 1
|
||||
out = original_snapshot(target)
|
||||
if calls['n'] == 2:
|
||||
self.ev.bundle.joinpath('src/a.py').write_text('VALUE = 9\n')
|
||||
return out
|
||||
api.snapshot = mutating_snapshot
|
||||
with tempfile.TemporaryDirectory(dir=self.tmp.name) as journal:
|
||||
with self.assertRaisesRegex(native.ValidationError, 'delegation adoption'):
|
||||
native.publish(plan, before, api, fixture.FakeLease(), journal, declaration=self.ev.declaration())
|
||||
self.assertEqual(calls['n'], 2, 'must reach both provider reads and final adoption check')
|
||||
self.assertTrue((Path(journal) / 'intent.json').exists())
|
||||
self.assertEqual(api.posts, 0)
|
||||
# Conservatively preserve durable intent: restoring evidence cannot trigger POST.
|
||||
self.ev.bundle.joinpath('src/a.py').write_text('VALUE = 1\n')
|
||||
with self.assertRaises(native.UncertainPublication):
|
||||
native.publish(plan, before, api, fixture.FakeLease(), journal, declaration=self.ev.declaration())
|
||||
self.assertEqual(api.posts, 0)
|
||||
|
||||
def spec_result(self):
|
||||
return {'schema': support.SCHEMA,
|
||||
'target': {'owner': 'acme', 'repository': 'repo', 'issue': 3, 'pull': 7, 'head': self.ev.revision},
|
||||
'contract_sha256': hashlib.sha256(b'contract').hexdigest(), 'verdict': 'approve',
|
||||
'criteria': [{'criterion': 'REQ-1', 'contract': 'contract', 'implementation_paths': ['src/a.py'],
|
||||
'test_paths': [], 'evidence': 'verified', 'result': 'satisfied', 'reason': 'ok'}],
|
||||
'scope': {'missing': [], 'excess': [], 'spec_conflicts': []}, 'conclusion': 'ok'}
|
||||
|
||||
def rework_args(self, declaration):
|
||||
from types import SimpleNamespace
|
||||
root = Path(self.tmp.name)
|
||||
data = {'owner': 'acme', 'repository': 'repo', 'issue': 3, 'pr': 7, 'review': 9,
|
||||
'reviewed_head': self.ev.revision, 'final_head': self.ev.revision,
|
||||
'contract_sha256': hashlib.sha256(b'contract').hexdigest(), 'result': 'rework_delivered',
|
||||
'findings': [{'id': 'F-1', 'status': 'resolved', 'classification': 'valid_fix',
|
||||
'changes': 'fixed', 'verification': 'passed', 'reason': 'ok'}],
|
||||
'local_checks': 'passed', 'runtime_browser': 'passed', 'commit_status': 'success', 'actions_runs': '없음'}
|
||||
(root / 'input.json').write_text(json.dumps(data))
|
||||
path = root / 'declaration.json'
|
||||
if declaration is not None:
|
||||
path.write_text(json.dumps(declaration))
|
||||
return SimpleNamespace(input=str(root / 'input.json'), comment=str(root / 'denied.md'),
|
||||
expected=str(root / 'denied.json'), no_delegated_evidence=False,
|
||||
delegation_adoption=str(path) if declaration is not None else None)
|
||||
|
||||
def test_all_consumers_reject_missing_malformed_and_indeterminate_without_effects(self):
|
||||
for mode in ('missing', 'malformed', 'indeterminate'):
|
||||
self.ev.result['claims'][0]['status'] = 'indeterminate' if mode == 'indeterminate' else 'supported'
|
||||
self.ev.write()
|
||||
declaration = None if mode == 'missing' else {'mode': 'bad'} if mode == 'malformed' else self.ev.declaration()
|
||||
with self.subTest(mode=mode, consumer='spec'):
|
||||
self.ev.set_stage('spec-review')
|
||||
with self.assertRaises(support.ValidationError):
|
||||
support.render_review(self.spec_result(), {'src/a.py'}, declaration=declaration)
|
||||
self.ev.set_stage('implementation-delivery')
|
||||
calls = []
|
||||
with self.subTest(mode=mode, consumer='delivery'):
|
||||
with self.assertRaises(delivery.AdoptionError):
|
||||
delivery.deliver(lambda: calls.append(1), head=self.ev.revision, declaration=declaration)
|
||||
self.assertEqual(calls, [])
|
||||
with self.subTest(mode=mode, consumer='rework'):
|
||||
args = self.rework_args(declaration)
|
||||
with self.assertRaises(Exception):
|
||||
rework.do_render(args)
|
||||
self.assertFalse(Path(args.comment).exists())
|
||||
self.assertFalse(Path(args.expected).exists())
|
||||
self.ev.set_stage('code-review')
|
||||
fixture, plan, before, after, exact = self.bound_publication_fixture()
|
||||
api = fixture.FakeAPI(plan, before, after, exact)
|
||||
journal = Path(self.tmp.name) / ('denied-journal-' + mode)
|
||||
with self.subTest(mode=mode, consumer='review'):
|
||||
with self.assertRaises(native.ValidationError):
|
||||
native.publish(plan, before, api, fixture.FakeLease(), journal, declaration=declaration)
|
||||
self.assertEqual(api.posts, 0)
|
||||
self.assertEqual(api.events, [])
|
||||
self.assertFalse(journal.exists())
|
||||
|
||||
def test_rework_explicit_none_does_not_bypass_checker_failure(self):
|
||||
from unittest.mock import patch
|
||||
from harness.lib.delegation_adoption import AdoptionError
|
||||
args = self.rework_args(None)
|
||||
args.no_delegated_evidence = True
|
||||
with patch('harness.lib.delegation_adoption.require_adoption', side_effect=AdoptionError('rejected')):
|
||||
try:
|
||||
rework.do_render(args)
|
||||
except rework.Bad:
|
||||
pass
|
||||
else:
|
||||
self.fail('explicit none must still invoke common checker')
|
||||
self.assertFalse(Path(args.comment).exists())
|
||||
self.assertFalse(Path(args.expected).exists())
|
||||
|
||||
def test_spec_cli_rejects_symlink_declaration(self):
|
||||
self.ev.set_stage('spec-review')
|
||||
root = Path(self.tmp.name)
|
||||
(root / 'spec.json').write_text(json.dumps(self.spec_result()))
|
||||
(root / 'paths.json').write_text(json.dumps(['src/a.py']))
|
||||
(root / 'real-decl.json').write_text(json.dumps(self.ev.declaration()))
|
||||
(root / 'alias.json').symlink_to(root / 'real-decl.json')
|
||||
env = os.environ.copy(); env.pop('PYTHONPATH', None)
|
||||
cmd = [sys.executable, str(SPEC), 'render', '--result', str(root / 'spec.json'),
|
||||
'--allowed-paths', str(root / 'paths.json'), '--delegation-adoption', str(root / 'alias.json')]
|
||||
proc = subprocess.run(cmd, cwd=root, env=env, text=True, capture_output=True, timeout=10)
|
||||
self.assertNotEqual(proc.returncode, 0)
|
||||
self.assertEqual(proc.stdout, '')
|
||||
|
||||
def test_cli_import_and_safe_declaration_reader_without_pythonpath(self):
|
||||
self.ev.set_stage('implementation-delivery')
|
||||
inp = Path(self.tmp.name) / 'rework.json'; comment = Path(self.tmp.name) / 'comment.md'; expected = Path(self.tmp.name) / 'expected.json'
|
||||
data = {'owner': 'acme', 'repository': 'repo', 'issue': 3, 'pr': 7, 'review': 9, 'reviewed_head': self.ev.revision, 'final_head': self.ev.revision,
|
||||
'contract_sha256': hashlib.sha256(b'contract').hexdigest(), 'result': 'rework_delivered',
|
||||
'findings': [{'id': 'F-1', 'status': 'resolved', 'classification': 'valid_fix', 'changes': 'fixed', 'verification': 'passed', 'reason': 'ok'}],
|
||||
'local_checks': 'passed', 'runtime_browser': 'passed', 'commit_status': 'success', 'actions_runs': '없음'}
|
||||
inp.write_text(json.dumps(data)); decl = Path(self.tmp.name) / 'decl.json'; decl.write_text(json.dumps(self.ev.declaration()))
|
||||
env = os.environ.copy(); env.pop('PYTHONPATH', None)
|
||||
cmd = [sys.executable, str(REWORK), 'render', '--input', str(inp), '--comment', str(comment), '--expected', str(expected), '--delegation-adoption', str(decl)]
|
||||
run = subprocess.run(cmd, cwd=self.tmp.name, env=env, text=True, capture_output=True)
|
||||
self.assertEqual(run.returncode, 0, run.stderr)
|
||||
self.assertTrue(comment.exists() and expected.exists())
|
||||
os.symlink(self.ev.result_path, Path(self.tmp.name) / 'evil.json')
|
||||
bad = subprocess.run([sys.executable, str(REWORK), 'render', '--input', str(inp), '--comment', str(comment), '--expected', str(expected), '--delegation-adoption', str(self.tmp.name) + '/evil.json'], cwd=self.tmp.name, env=env, text=True, capture_output=True)
|
||||
self.assertNotEqual(bad.returncode, 0); self.assertEqual(bad.stdout, '')
|
||||
|
||||
if __name__ == '__main__': unittest.main()
|
||||
@@ -0,0 +1,150 @@
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
PATH = ROOT / "harness/agents/impl/skills/gitea-issue-implementation/scripts/implementation_delivery.py"
|
||||
INTEGRATION = ROOT / "harness/tests/common/test_delegation_consumers_integration.py"
|
||||
|
||||
|
||||
def load(path, name):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class RecordingAdapter:
|
||||
def __init__(self, head, *, remote_head=None, pr=None, mutate=None):
|
||||
self.head = head
|
||||
self.remote_head = remote_head if remote_head is not None else head
|
||||
self.pr = pr if pr is not None else {"number": 17, "head": head, "state": "open"}
|
||||
self.mutate = mutate
|
||||
self.calls = []
|
||||
|
||||
def push(self, head):
|
||||
self.calls.append(("push", head))
|
||||
if self.mutate:
|
||||
self.mutate("push")
|
||||
|
||||
def read_remote_head(self):
|
||||
self.calls.append(("read_remote_head",))
|
||||
if self.mutate:
|
||||
self.mutate("read_remote_head")
|
||||
return self.remote_head
|
||||
|
||||
def create_or_reuse_pr(self, head):
|
||||
self.calls.append(("create_or_reuse_pr", head))
|
||||
if self.mutate:
|
||||
self.mutate("create_or_reuse_pr")
|
||||
return self.pr["number"]
|
||||
|
||||
def read_pr(self, number):
|
||||
self.calls.append(("read_pr", number))
|
||||
return self.pr
|
||||
|
||||
|
||||
class ImplementationDeliveryTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.delivery = load(PATH, "implementation_delivery_under_test")
|
||||
cls.evidence = load(INTEGRATION, "implementation_delivery_evidence")
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = TemporaryDirectory(dir="/opt/data/cache/scratch")
|
||||
self.ev = self.evidence.RealEvidence(Path(self.tmp.name))
|
||||
self.ev.set_stage("implementation-delivery")
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def test_real_git_delivery_pushes_once_creates_once_and_returns_verified_readback(self):
|
||||
adapter = RecordingAdapter(self.ev.revision)
|
||||
result = self.delivery.deliver_implementation(
|
||||
adapter, head=self.ev.revision, declaration=self.ev.declaration()
|
||||
)
|
||||
self.assertEqual(result, {
|
||||
"remote_head": self.ev.revision,
|
||||
"pr": {"number": 17, "head": self.ev.revision, "state": "open"},
|
||||
})
|
||||
self.assertEqual(
|
||||
[call[0] for call in adapter.calls],
|
||||
["push", "read_remote_head", "create_or_reuse_pr", "read_pr"],
|
||||
)
|
||||
|
||||
def test_explicit_none_is_checked_and_exact_readback_is_required(self):
|
||||
adapter = RecordingAdapter(self.ev.revision)
|
||||
result = self.delivery.deliver_implementation(
|
||||
adapter, head=self.ev.revision, declaration={"mode": "none"}
|
||||
)
|
||||
self.assertEqual(result["remote_head"], self.ev.revision)
|
||||
self.assertEqual(result["pr"]["state"], "open")
|
||||
|
||||
def test_missing_or_malformed_declaration_has_no_effects(self):
|
||||
for declaration in (None, {"mode": "bad"}):
|
||||
adapter = RecordingAdapter(self.ev.revision)
|
||||
with self.subTest(declaration=declaration), self.assertRaises(self.delivery.AdoptionError):
|
||||
self.delivery.deliver_implementation(adapter, head=self.ev.revision, declaration=declaration)
|
||||
self.assertEqual(adapter.calls, [])
|
||||
|
||||
def test_remote_drift_prevents_pr_creation(self):
|
||||
adapter = RecordingAdapter(self.ev.revision, remote_head="0" * 40)
|
||||
with self.assertRaises(self.delivery.AdoptionError):
|
||||
self.delivery.deliver_implementation(adapter, head=self.ev.revision, declaration=self.ev.declaration())
|
||||
self.assertEqual([call[0] for call in adapter.calls], ["push", "read_remote_head"])
|
||||
|
||||
def test_evidence_mutation_during_remote_readback_prevents_pr_creation(self):
|
||||
def mutate(phase):
|
||||
if phase == "read_remote_head":
|
||||
self.ev.bundle.joinpath("src/a.py").write_text("VALUE = 2\n")
|
||||
|
||||
adapter = RecordingAdapter(self.ev.revision, mutate=mutate)
|
||||
with self.assertRaises(self.delivery.AdoptionError):
|
||||
self.delivery.deliver_implementation(adapter, head=self.ev.revision, declaration=self.ev.declaration())
|
||||
self.assertEqual([call[0] for call in adapter.calls], ["push", "read_remote_head"])
|
||||
|
||||
def test_missing_adapter_capability_prevents_push(self):
|
||||
class Incomplete:
|
||||
def read_remote_head(self):
|
||||
return self.ev.revision
|
||||
|
||||
with self.assertRaises(self.delivery.AdoptionError):
|
||||
self.delivery.deliver_implementation(Incomplete(), head=self.ev.revision, declaration={"mode": "none"})
|
||||
|
||||
def test_pr_readback_mismatch_does_not_complete(self):
|
||||
for pr in (
|
||||
{"number": 17, "head": "0" * 40, "state": "open"},
|
||||
{"number": 17, "head": self.ev.revision, "state": "closed"},
|
||||
{"number": True, "head": self.ev.revision, "state": "open"},
|
||||
):
|
||||
adapter = RecordingAdapter(self.ev.revision, pr=pr)
|
||||
with self.subTest(pr=pr), self.assertRaises(self.delivery.AdoptionError):
|
||||
self.delivery.deliver_implementation(adapter, head=self.ev.revision, declaration=self.ev.declaration())
|
||||
|
||||
def test_workflow_can_load_from_external_cwd_without_pythonpath(self):
|
||||
import os
|
||||
import subprocess
|
||||
code = "import importlib.util,json; s=importlib.util.spec_from_file_location('delivery'," + repr(str(PATH)) + "); m=importlib.util.module_from_spec(s); s.loader.exec_module(m); head='a'*40; A=type('A',(),{'push':lambda self,h:None,'read_remote_head':lambda self:head,'create_or_reuse_pr':lambda self,h:17,'read_pr':lambda self,n:{'number':n,'head':head,'state':'open'}}); print(json.dumps(m.deliver_implementation(A(),head=head,declaration={'mode':'none'})))"
|
||||
env = os.environ.copy(); env.pop('PYTHONPATH', None)
|
||||
proc = subprocess.run([sys.executable, '-c', code], cwd=self.tmp.name, env=env,
|
||||
capture_output=True, text=True, timeout=10)
|
||||
self.assertEqual(proc.returncode, 0, 'canonical delivery must load independently of cwd')
|
||||
self.assertIn('remote_head', proc.stdout)
|
||||
|
||||
def test_uncertain_write_is_not_retried(self):
|
||||
class FailingAdapter(RecordingAdapter):
|
||||
def create_or_reuse_pr(self, head):
|
||||
self.calls.append(("create_or_reuse_pr", head))
|
||||
raise RuntimeError("transport uncertain")
|
||||
|
||||
adapter = FailingAdapter(self.ev.revision)
|
||||
with self.assertRaises(RuntimeError):
|
||||
self.delivery.deliver_implementation(adapter, head=self.ev.revision, declaration=self.ev.declaration())
|
||||
self.assertEqual([call[0] for call in adapter.calls], ["push", "read_remote_head", "create_or_reuse_pr"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -11,7 +11,7 @@ class ReworkCommentTests(unittest.TestCase):
|
||||
"result":"partial","findings":[{"id":"F-1","status":"resolved","classification":"valid_fix","changes":"src/main.py","verification":"pytest -q (0), pass","reason":"bounded fix"}],
|
||||
"local_checks":"pytest -q (0), pass","runtime_browser":"해당 없음·미검증","commit_status":"success","actions_runs":"없음"}
|
||||
self.write("input.json", self.data)
|
||||
self.invoke("render", "--input", self.p("input.json"), "--comment", self.p("comment.md"), "--expected", self.p("expected.json"))
|
||||
self.invoke("render", "--no-delegated-evidence", "--input", self.p("input.json"), "--comment", self.p("comment.md"), "--expected", self.p("expected.json"))
|
||||
self.expected = json.loads(self.d.joinpath("expected.json").read_text())
|
||||
|
||||
def tearDown(self): self.t.cleanup()
|
||||
@@ -47,7 +47,7 @@ class ReworkCommentTests(unittest.TestCase):
|
||||
self.setUp_render_again(); self.readback(); c=self.read("comments.json"); c["comments"].append(dict(c["comments"][0],id=502)); c["total_count"]=2; self.write("comments.json",c); self.assertNotEqual(self.verify("reuse",1).returncode,0)
|
||||
|
||||
def setUp_render_again(self):
|
||||
self.invoke("render","--input",self.p("input.json"),"--comment",self.p("comment.md"),"--expected",self.p("expected.json")); self.expected=self.read("expected.json")
|
||||
self.invoke("render","--no-delegated-evidence","--input",self.p("input.json"),"--comment",self.p("comment.md"),"--expected",self.p("expected.json")); self.expected=self.read("expected.json")
|
||||
def test_create_reuse_cardinality(self):
|
||||
self.readback(); self.assertEqual(self.verify("create",0).returncode,0); self.assertEqual(self.read("receipt.json")["status"],"verified")
|
||||
original=self.read("comments.json")["comments"][0]
|
||||
@@ -59,7 +59,7 @@ class ReworkCommentTests(unittest.TestCase):
|
||||
def test_unresolved_delivery_and_incomplete_or_wrong_scope_readback_fail(self):
|
||||
bad=dict(self.data); bad["result"]="rework_delivered"; bad["findings"]=[dict(self.data["findings"][0],status="unresolved")]
|
||||
self.write("bad-input.json",bad)
|
||||
self.assertNotEqual(self.invoke("render","--input",self.p("bad-input.json"),"--comment",self.p("bad.md"),"--expected",self.p("bad.json")).returncode,0)
|
||||
self.assertNotEqual(self.invoke("render","--no-delegated-evidence","--input",self.p("bad-input.json"),"--comment",self.p("bad.md"),"--expected",self.p("bad.json")).returncode,0)
|
||||
self.readback(); comments=self.read("comments.json"); comments["complete"]=False; self.write("comments.json",comments); self.assertNotEqual(self.verify().returncode,0)
|
||||
self.readback(); pr=self.read("pr.json"); pr["repository"]="other"; self.write("pr.json",pr); self.assertNotEqual(self.verify().returncode,0)
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ def publication_fixture():
|
||||
'official': True, 'dismissed': False, 'stale': head != t['head'],
|
||||
'submitted_at': old_time, 'updated_at': old_time, 'comments_count': 0,
|
||||
'pull_request_url': url}
|
||||
sr = review(9, p['spec_reviewer'], support.render_review(spec_result, {'src/a.py'}),
|
||||
sr = review(9, p['spec_reviewer'], support.render_review(spec_result, {'src/a.py'}, declaration={'mode': 'none'}),
|
||||
t['head'], 'APPROVED')
|
||||
previous = review(8, p['reviewer'], 'old review body', 'd' * 40, 'REQUEST_CHANGES')
|
||||
s = {'identity': copy.deepcopy(p['reviewer']), 'spec_user': copy.deepcopy(p['spec_reviewer']),
|
||||
@@ -159,22 +159,22 @@ class NativeReviewTests(unittest.TestCase):
|
||||
p, before, after, exact = publication_fixture()
|
||||
api = FakeAPI(p, before, after, exact); lease = FakeLease()
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
result = m.publish(p, before, api, lease, td)
|
||||
result = m.publish(p, before, api, lease, td, declaration={'mode': 'none'})
|
||||
self.assertEqual(result['mode'], 'created')
|
||||
self.assertEqual(api.posts, 1)
|
||||
self.assertEqual(m.publish(p, before, api, lease, td)['mode'], 'no_op')
|
||||
self.assertEqual(m.publish(p, before, api, lease, td, declaration={'mode': 'none'})['mode'], 'no_op')
|
||||
self.assertEqual(api.posts, 1)
|
||||
self.assertNotIn('body', json.loads((Path(td) / 'intent.json').read_text()))
|
||||
for applies in [True, False]:
|
||||
api = FakeAPI(p, before, after, exact); api.timeout = True; api.apply = applies
|
||||
with self.subTest(applies=applies), tempfile.TemporaryDirectory() as td:
|
||||
if applies:
|
||||
self.assertEqual(m.publish(p, before, api, lease, td)['mode'], 'reconciled')
|
||||
self.assertEqual(m.publish(p, before, api, lease, td)['mode'], 'no_op')
|
||||
self.assertEqual(m.publish(p, before, api, lease, td, declaration={'mode': 'none'})['mode'], 'reconciled')
|
||||
self.assertEqual(m.publish(p, before, api, lease, td, declaration={'mode': 'none'})['mode'], 'no_op')
|
||||
else:
|
||||
for _ in range(2):
|
||||
with self.assertRaises(m.UncertainPublication) as error:
|
||||
m.publish(p, before, api, lease, td)
|
||||
m.publish(p, before, api, lease, td, declaration={'mode': 'none'})
|
||||
self.assertNotIn('secret transport detail', str(error.exception))
|
||||
self.assertEqual(api.posts, 1)
|
||||
def test_api_adapter_checks_identity_first_and_collects_all_pages(self):
|
||||
@@ -296,7 +296,7 @@ class NativeReviewTests(unittest.TestCase):
|
||||
p['spec_paths'] = ['src/a.py', 'tests/existing.py']
|
||||
spec_result = support.parse_review(before['reviews'][1]['body'], {'src/a.py'})
|
||||
spec_result['criteria'][0]['test_paths'] = ['tests/existing.py']
|
||||
before['reviews'][1]['body'] = support.render_review(spec_result, set(p['spec_paths']))
|
||||
before['reviews'][1]['body'] = support.render_review(spec_result, set(p['spec_paths']), declaration={'mode': 'none'})
|
||||
try:
|
||||
m.validate_snapshot(p, before)
|
||||
except m.ValidationError:
|
||||
@@ -333,7 +333,7 @@ class NativeReviewTests(unittest.TestCase):
|
||||
p, before, after, exact = publication_fixture(); api = FakeAPI(p, before, after, exact); lease = FakeLease()
|
||||
if kind == 'expired': lease.valid = False
|
||||
else: api.identity = lambda: p['spec_reviewer']
|
||||
with self.assertRaises(m.ValidationError): m.publish(p, before, api, lease, td)
|
||||
with self.assertRaises(m.ValidationError): m.publish(p, before, api, lease, td, declaration={'mode': 'none'})
|
||||
self.assertEqual(api.posts, 0)
|
||||
self.assertFalse((Path(td) / 'intent.json').exists())
|
||||
|
||||
@@ -348,7 +348,7 @@ class NativeReviewTests(unittest.TestCase):
|
||||
with self.subTest(case=i), tempfile.TemporaryDirectory() as td:
|
||||
p, before, after, exact = publication_fixture(); api = FakeAPI(p, copy.deepcopy(before), after, exact)
|
||||
change(api.before)
|
||||
with self.assertRaises(m.ValidationError): m.publish(p, before, api, FakeLease(), td)
|
||||
with self.assertRaises(m.ValidationError): m.publish(p, before, api, FakeLease(), td, declaration={'mode': 'none'})
|
||||
self.assertEqual(api.posts, 0)
|
||||
|
||||
def test_pagination_total_duplicate_and_truncation_are_hard_errors(self):
|
||||
@@ -424,7 +424,7 @@ class NativeReviewTests(unittest.TestCase):
|
||||
m = self.module(); p, before, after, exact = publication_fixture()
|
||||
api = FakeAPI(p, before, after, exact); api.created = True
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
result = m.publish(p, after, api, FakeLease(), td)
|
||||
result = m.publish(p, after, api, FakeLease(), td, declaration={'mode': 'none'})
|
||||
self.assertEqual(result['mode'], 'no_op'); self.assertFalse(result['wrote'])
|
||||
self.assertEqual(api.posts, 0)
|
||||
self.assertFalse((Path(td) / 'intent.json').exists())
|
||||
@@ -441,7 +441,7 @@ class NativeReviewTests(unittest.TestCase):
|
||||
after['pagination']['reviews'][0].update(count=4, declared_total=4)
|
||||
after['pagination']['review_comments_11'] = [{'page': 1, 'count': 0, 'declared_total': 0, 'link': None}]
|
||||
api = FakeAPI(p, before, after, exact); api.created = True
|
||||
with self.assertRaises(m.ValidationError): m.publish(p, before, api, FakeLease(), td)
|
||||
with self.assertRaises(m.ValidationError): m.publish(p, before, api, FakeLease(), td, declaration={'mode': 'none'})
|
||||
self.assertEqual(api.posts, 0)
|
||||
|
||||
def test_readback_failure_and_changed_journal_plan_never_retry_post(self):
|
||||
@@ -449,11 +449,11 @@ class NativeReviewTests(unittest.TestCase):
|
||||
api.read_review = lambda target, rid: (_ for _ in ()).throw(TimeoutError('DO_NOT_ECHO_TRANSPORT_SECRET'))
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
for _ in range(2):
|
||||
with self.assertRaises(Exception) as raised: m.publish(p, before, api, FakeLease(), td)
|
||||
with self.assertRaises(Exception) as raised: m.publish(p, before, api, FakeLease(), td, declaration={'mode': 'none'})
|
||||
self.assertNotIn('DO_NOT_ECHO_TRANSPORT_SECRET', str(raised.exception))
|
||||
self.assertEqual(api.posts, 1)
|
||||
different = copy.deepcopy(p); different['body'] += 'different'
|
||||
with self.assertRaises(m.ValidationError): m.publish(different, before, api, FakeLease(), td)
|
||||
with self.assertRaises(m.ValidationError): m.publish(different, before, api, FakeLease(), td, declaration={'mode': 'none'})
|
||||
self.assertEqual(api.posts, 1)
|
||||
|
||||
def test_journal_symlinks_hardlinks_and_changed_baseline_are_rejected(self):
|
||||
@@ -463,10 +463,10 @@ class NativeReviewTests(unittest.TestCase):
|
||||
with self.assertRaises(m.ValidationError): m.Journal(link)
|
||||
api = FakeAPI(p, before, after, exact); api.timeout = True; api.apply = False
|
||||
journal = root / 'journal'
|
||||
with self.assertRaises(m.UncertainPublication): m.publish(p, before, api, FakeLease(), journal)
|
||||
with self.assertRaises(m.UncertainPublication): m.publish(p, before, api, FakeLease(), journal, declaration={'mode': 'none'})
|
||||
saved = json.loads((journal / 'before.json').read_text()); saved['issue']['body'] = 'changed'
|
||||
(journal / 'before.json').write_text(json.dumps(saved))
|
||||
with self.assertRaises(m.ValidationError): m.publish(p, before, api, FakeLease(), journal)
|
||||
with self.assertRaises(m.ValidationError): m.publish(p, before, api, FakeLease(), journal, declaration={'mode': 'none'})
|
||||
self.assertEqual(api.posts, 1)
|
||||
with m.Journal(root / 'hardlinks') as j:
|
||||
j.write('a.json', {'safe': True}); os.link(j.path / 'a.json', j.path / 'b.json')
|
||||
@@ -485,7 +485,7 @@ class NativeReviewTests(unittest.TestCase):
|
||||
m = self.module(); p, before, after, exact = publication_fixture(); api = FakeAPI(p, before, after, exact)
|
||||
lease = FakeLease(); lease.check = lambda plan: False
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
with self.assertRaises(m.ValidationError): m.publish(p, before, api, lease, td)
|
||||
with self.assertRaises(m.ValidationError): m.publish(p, before, api, lease, td, declaration={'mode': 'none'})
|
||||
self.assertEqual(api.posts, 0)
|
||||
def test_live_rest_bridge_over_loopback_publishes_once_then_no_ops(self):
|
||||
import threading
|
||||
@@ -533,10 +533,10 @@ class NativeReviewTests(unittest.TestCase):
|
||||
api = m.ApiAdapter(request, origin=m.ORIGIN + '/api/v1', credential_source='$HERMES_HOME/.env:GITEA_TOKEN')
|
||||
baseline = api.snapshot(p['target'])
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
result = m.publish(p, baseline, api, FakeLease(), td)
|
||||
result = m.publish(p, baseline, api, FakeLease(), td, declaration={'mode': 'none'})
|
||||
self.assertEqual(result['mode'], 'created'); self.assertEqual(result['projected_review_ids'], [8])
|
||||
self.assertTrue(result['commit_file_order_normalized'])
|
||||
second = m.publish(p, baseline, api, FakeLease(), td)
|
||||
second = m.publish(p, baseline, api, FakeLease(), td, declaration={'mode': 'none'})
|
||||
self.assertEqual(second['mode'], 'no_op'); self.assertFalse(second['wrote'])
|
||||
self.assertEqual(state['posts'], 1); self.assertEqual(server.received, m.build_payload(p))
|
||||
finally:
|
||||
@@ -591,10 +591,10 @@ class NativeReviewTests(unittest.TestCase):
|
||||
with self.subTest(field=field), tempfile.TemporaryDirectory() as td:
|
||||
p, before, after, exact = publication_fixture(); api = FakeAPI(p, before, after, exact)
|
||||
api.timeout = True; api.apply = False
|
||||
with self.assertRaises(m.UncertainPublication): m.publish(p, before, api, FakeLease(), td)
|
||||
with self.assertRaises(m.UncertainPublication): m.publish(p, before, api, FakeLease(), td, declaration={'mode': 'none'})
|
||||
path = Path(td) / 'intent.json'; intent = json.loads(path.read_text()); intent[field] = 'corrupt'
|
||||
path.write_text(json.dumps(intent)); api.created = True
|
||||
with self.assertRaises(m.ValidationError): m.publish(p, before, api, FakeLease(), td)
|
||||
with self.assertRaises(m.ValidationError): m.publish(p, before, api, FakeLease(), td, declaration={'mode': 'none'})
|
||||
self.assertEqual(api.posts, 1)
|
||||
|
||||
def test_issue_repository_mismatch_is_not_accepted(self):
|
||||
|
||||
@@ -33,7 +33,7 @@ def result(verdict="approve"):
|
||||
|
||||
def publication_fixture():
|
||||
r = result(); paths = {"src/a.py", "tests/test_a.py"}
|
||||
body = mod.render_review(r, paths)
|
||||
body = mod.render_review(r, paths, declaration={"mode": "none"})
|
||||
url = "https://example.test/acme/repo/pulls/7"
|
||||
user = {"id": 42, "login": "spec-agent"}
|
||||
review = {"id": 11, "body": body, "commit_id": r["target"]["head"], "state": "APPROVED",
|
||||
@@ -71,10 +71,10 @@ class SupportTests(unittest.TestCase):
|
||||
def test_validate_and_render_roundtrip(self):
|
||||
r = result()
|
||||
self.assertEqual(mod.validate_result(r, {"src/a.py", "tests/test_a.py"}), r)
|
||||
body = mod.render_review(r, {"src/a.py", "tests/test_a.py"})
|
||||
body = mod.render_review(r, {"src/a.py", "tests/test_a.py"}, declaration={"mode": "none"})
|
||||
parsed = mod.parse_review(body, {"src/a.py", "tests/test_a.py"})
|
||||
self.assertEqual(parsed, r)
|
||||
self.assertEqual(mod.render_review(parsed, {"src/a.py", "tests/test_a.py"}), body)
|
||||
self.assertEqual(mod.render_review(parsed, {"src/a.py", "tests/test_a.py"}, declaration={"mode": "none"}), body)
|
||||
|
||||
def test_validation_rejects_bool_path_traversal_bad_verdict(self):
|
||||
r = result()
|
||||
@@ -148,7 +148,7 @@ class SupportTests(unittest.TestCase):
|
||||
p = subprocess.run(base + ["validate-result", "--result", str(rf), "--allowed-paths", str(af)],
|
||||
text=True, capture_output=True, env=env)
|
||||
self.assertEqual(p.returncode, 0, p.stderr)
|
||||
p = subprocess.run(base + ["render", "--result", str(rf), "--allowed-paths", str(af)],
|
||||
p = subprocess.run(base + ["render", "--no-delegated-evidence", "--result", str(rf), "--allowed-paths", str(af)],
|
||||
text=True, capture_output=True, env=env)
|
||||
self.assertEqual(p.returncode, 0, p.stderr); bf.write_text(p.stdout, encoding="utf-8")
|
||||
p = subprocess.run(base + ["parse", "--body", str(bf), "--allowed-paths", str(af)],
|
||||
@@ -160,12 +160,12 @@ class SupportTests(unittest.TestCase):
|
||||
def test_result_codec_preserves_existing_native_format(self):
|
||||
r = result()
|
||||
paths = {"src/a.py", "tests/test_a.py"}
|
||||
body = mod.render_review(r, paths)
|
||||
body = mod.render_review(r, paths, declaration={"mode": "none"})
|
||||
self.assertIn(" - 구현 근거: src/a.py; 증거\n", body)
|
||||
self.assertIn("모든 기준을 충족했다.\n\n<!--", body)
|
||||
self.assertEqual(mod.parse_review(body, paths), r)
|
||||
unordered = json.loads(json.dumps(r, sort_keys=True))
|
||||
self.assertEqual(mod.render_review(unordered, paths), body)
|
||||
self.assertEqual(mod.render_review(unordered, paths, declaration={"mode": "none"}), body)
|
||||
|
||||
def test_strict_codec_rejects_unrenderable_and_sensitive_human_values(self):
|
||||
paths = {"src/a.py", "tests/test_a.py"}
|
||||
@@ -188,7 +188,7 @@ class SupportTests(unittest.TestCase):
|
||||
|
||||
def test_parser_rejects_forged_marker_and_body_drift(self):
|
||||
paths = {"src/a.py", "tests/test_a.py"}
|
||||
body = mod.render_review(result(), paths)
|
||||
body = mod.render_review(result(), paths, declaration={"mode": "none"})
|
||||
for bad in (body.replace("issue=3", "issue=4"), body.replace("증거", "다른 증거"),
|
||||
body.replace("\n", "\r\n"), body + "\n", body.replace("correlation=", "other=")):
|
||||
with self.subTest(body=bad[-100:]), self.assertRaises(mod.ValidationError):
|
||||
@@ -199,7 +199,7 @@ class SupportTests(unittest.TestCase):
|
||||
r["criteria"][0]["evidence"] = "확인; 모두 — 일치"
|
||||
r["verdict"] = "blocked_spec"; r["scope"]["spec_conflicts"] = ["하나, 둘", "세 가지"]
|
||||
paths = {"src/a, b.py", "src/c; d.py", "tests/test_a.py"}
|
||||
self.assertEqual(mod.parse_review(mod.render_review(r, paths), paths), r)
|
||||
self.assertEqual(mod.parse_review(mod.render_review(r, paths, declaration={"mode": "none"}), paths), r)
|
||||
|
||||
|
||||
def test_cache_enforces_root_boundary_uid_and_dangling_locks(self):
|
||||
@@ -379,7 +379,7 @@ class SupportTests(unittest.TestCase):
|
||||
def test_target_identifiers_preserve_dots_but_reject_marker_breaking_whitespace(self):
|
||||
r = result(); paths = {"src/a.py", "tests/test_a.py"}
|
||||
r["target"].update(owner="acme.name", repository="repo.name")
|
||||
body = mod.render_review(r, paths)
|
||||
body = mod.render_review(r, paths, declaration={"mode": "none"})
|
||||
self.assertEqual(mod.parse_review(body, paths), r)
|
||||
r["target"]["owner"] = "acme name"
|
||||
with self.assertRaises(mod.ValidationError):
|
||||
@@ -389,7 +389,7 @@ class SupportTests(unittest.TestCase):
|
||||
r = result(); path = "src/<script>@user`[link].py"
|
||||
r["criteria"][0]["implementation_paths"] = [path]
|
||||
paths = {path, "tests/test_a.py"}
|
||||
body = mod.render_review(r, paths)
|
||||
body = mod.render_review(r, paths, declaration={"mode": "none"})
|
||||
self.assertNotIn("<script>", body)
|
||||
self.assertNotIn("@user", body)
|
||||
self.assertNotIn("`[link]", body)
|
||||
|
||||
Reference in New Issue
Block a user