148 lines
7.6 KiB
Python
148 lines
7.6 KiB
Python
import copy
|
|
import json
|
|
import re
|
|
import unittest
|
|
from harness.tests._support.contracts import read_contract, REQUIRED
|
|
from harness.tests._support.paths import ROOT
|
|
|
|
COMMIT_FILES_START = "<!-- commit-files-comparison-example:start -->"
|
|
COMMIT_FILES_END = "<!-- commit-files-comparison-example:end -->"
|
|
|
|
|
|
def _execute_documented_commit_files_comparator():
|
|
"""Load the single trusted Python example from the canonical workflow."""
|
|
path = ROOT / "harness/agents/impl/skills/gitea-issue-rework/references/workflow.md"
|
|
text = path.read_text()
|
|
start = text.count(COMMIT_FILES_START)
|
|
end = text.count(COMMIT_FILES_END)
|
|
if start != 1 or end != 1:
|
|
raise AssertionError("workflow must contain exactly one commit-files example boundary pair")
|
|
section = text.split(COMMIT_FILES_START, 1)[1].split(COMMIT_FILES_END, 1)[0]
|
|
snippets = re.findall(r"```python\s*\n(.*?)\n```", section, re.DOTALL)
|
|
if len(snippets) != 1:
|
|
raise AssertionError("commit-files example must contain exactly one Python fenced block")
|
|
namespace = {}
|
|
exec(compile(snippets[0], str(path) + "#commit-files-example", "exec"), namespace)
|
|
comparator = namespace.get("normalize_commit_files")
|
|
if not callable(comparator):
|
|
raise AssertionError("Python example must define normalize_commit_files(rows)")
|
|
heading = "#### commit.files 순서 비교 계약\n"
|
|
if text.count(heading) != 1:
|
|
raise AssertionError("workflow must contain one scoped commit-files comparison contract")
|
|
contract = text.split(heading, 1)[1].split("\n### 7.", 1)[0]
|
|
return comparator, contract
|
|
|
|
|
|
def _commit(sha, message, parent, files):
|
|
return {
|
|
"sha": sha,
|
|
"commit": {"message": message, "author": {"name": "Test Author", "email": "author@example.invalid"}},
|
|
"url": "https://example.invalid/commit/" + sha,
|
|
"parents": [{"sha": parent, "url": "https://example.invalid/commit/" + parent}],
|
|
"verification": {"verified": True, "reason": "valid", "arbitrary": [1, {"keep": True}]},
|
|
"files": files,
|
|
"provider_extension": {"retained": ["opaque", 7]},
|
|
}
|
|
|
|
|
|
def _file(filename, status, additions):
|
|
return {
|
|
"filename": filename,
|
|
"status": status,
|
|
"additions": additions,
|
|
"deletions": 2,
|
|
"changes": additions + 2,
|
|
"patch": "@@ -1 +1 @@\n-old\n+new",
|
|
"provider_extension": {"nested": [filename, {"opaque": True}]},
|
|
}
|
|
|
|
class DocumentedCommitFilesComparatorTests(unittest.TestCase):
|
|
SHA_A = '0123456789abcdef0123456789abcdef01234567'
|
|
SHA_B = '89abcdef0123456789abcdef0123456789abcdef'
|
|
PARENT_A = '1111111111111111111111111111111111111111'
|
|
PARENT_B = '2222222222222222222222222222222222222222'
|
|
|
|
def setUp(self):
|
|
self.normalize, self.workflow = _execute_documented_commit_files_comparator()
|
|
|
|
def fixture(self):
|
|
return [_commit(self.SHA_A, 'first message', self.PARENT_A, [_file('src/zeta.py', 'modified', 3), _file('docs/alpha.md', 'added', 5)]), _commit(self.SHA_B, 'second message', self.PARENT_B, [_file('tests/test_beta.py', 'modified', 7), _file('README.md', 'added', 11)])]
|
|
|
|
def test_inner_file_order_is_ignored_but_outer_commit_order_is_preserved(self):
|
|
first = self.fixture()
|
|
second = copy.deepcopy(first)
|
|
for commit in second:
|
|
commit['files'].reverse()
|
|
self.assertEqual(self.normalize(first), self.normalize(second))
|
|
self.assertNotEqual(self.normalize(first), self.normalize(list(reversed(second))))
|
|
|
|
def test_identity_and_all_file_metadata_remain_comparison_bound(self):
|
|
baseline = self.normalize(self.fixture())
|
|
mutations = (lambda rows: rows[0]['files'][0].update(filename='src/renamed.py'), lambda rows: rows[0]['files'][0].update(status='added'), lambda rows: rows[0]['files'][0].update(provider_extension={'changed': True}), lambda rows: rows[0]['files'].append(_file('new.txt', 'added', 1)), lambda rows: rows[0]['files'].pop(), lambda rows: rows[0].update(commit={'message': 'changed message', 'author': {'name': 'Test Author', 'email': 'author@example.invalid'}}), lambda rows: rows[0]['parents'].__setitem__(0, {'sha': self.SHA_B, 'url': 'https://example.invalid/commit/' + self.SHA_B}), lambda rows: rows[0].update(sha='3333333333333333333333333333333333333333'))
|
|
for mutate in mutations:
|
|
with self.subTest(mutation=mutate):
|
|
changed = self.fixture()
|
|
mutate(changed)
|
|
self.assertNotEqual(baseline, self.normalize(changed))
|
|
|
|
def test_normalization_preserves_input_and_returns_independent_deep_copy(self):
|
|
rows = self.fixture()
|
|
before = json.dumps(rows, sort_keys=True, separators=(',', ':'))
|
|
normalized = self.normalize(rows)
|
|
self.assertEqual(before, json.dumps(rows, sort_keys=True, separators=(',', ':')))
|
|
self.assertEqual(rows, self.fixture())
|
|
normalized[0]['files'][0]['provider_extension']['nested'][1]['opaque'] = False
|
|
normalized[0]['provider_extension']['retained'].append('output-only')
|
|
self.assertTrue(rows[0]['files'][0]['provider_extension']['nested'][1]['opaque'])
|
|
self.assertNotIn('output-only', rows[0]['provider_extension']['retained'])
|
|
|
|
def test_empty_file_list_is_valid_and_order_is_only_normalized_within_commits(self):
|
|
rows = self.fixture()
|
|
rows[0]['files'] = []
|
|
result = self.normalize(rows)
|
|
self.assertEqual([], result[0]['files'])
|
|
self.assertEqual([self.SHA_A, self.SHA_B], [row['sha'] for row in result])
|
|
self.assertEqual(sorted((file['filename'] for file in rows[1]['files'])), [file['filename'] for file in result[1]['files']])
|
|
|
|
def test_invalid_collection_commit_and_file_shapes_raise_value_error(self):
|
|
valid = self.fixture()
|
|
invalid_cases = []
|
|
for value in (None, {}, 'rows', (valid,)):
|
|
invalid_cases.append(value)
|
|
invalid_cases.extend(([None], ['commit'], [{'sha': self.SHA_A}], [{'sha': self.SHA_A, 'files': None}], [{'sha': self.SHA_A, 'files': {}}]))
|
|
for bad in invalid_cases:
|
|
with self.subTest(rows=bad):
|
|
with self.assertRaises(ValueError):
|
|
self.normalize(bad)
|
|
for bad_sha in ('', 'A' * 40, 'a' * 39, 'g' * 40, None, 7):
|
|
with self.subTest(sha=bad_sha):
|
|
rows = self.fixture()
|
|
rows[0]['sha'] = bad_sha
|
|
with self.assertRaises(ValueError):
|
|
self.normalize(rows)
|
|
duplicate_sha = self.fixture()
|
|
duplicate_sha[1]['sha'] = duplicate_sha[0]['sha']
|
|
with self.assertRaises(ValueError):
|
|
self.normalize(duplicate_sha)
|
|
for bad_file in (None, 'file', {}, {'filename': 'x'}):
|
|
with self.subTest(file=bad_file):
|
|
rows = self.fixture()
|
|
rows[0]['files'] = [bad_file]
|
|
with self.assertRaises(ValueError):
|
|
self.normalize(rows)
|
|
for key in ('filename', 'status'):
|
|
for value in (None, 1, ''):
|
|
with self.subTest(key=key, value=value):
|
|
rows = self.fixture()
|
|
rows[0]['files'][0][key] = value
|
|
with self.assertRaises(ValueError):
|
|
self.normalize(rows)
|
|
duplicate_file = self.fixture()
|
|
duplicate_file[0]['files'][1]['filename'] = duplicate_file[0]['files'][0]['filename']
|
|
with self.assertRaises(ValueError):
|
|
self.normalize(duplicate_file)
|
|
|
|
def test_minimal_file_record_and_literal_filename_are_retained(self):
|
|
rows = [{'sha': self.SHA_A, 'files': [{'filename': ' dir/../literal name.txt ', 'status': 'modified'}]}]
|
|
self.assertEqual(rows, self.normalize(rows))
|