| author | |
| committer | |
| log | 142c8f9c9d68cacd0497db0f916de8d72514d52c |
| tree | 0fdb5c98f8de6ed07c5a964fd4e74d6d476a51df |
| parent | 550d71c9d8ee053ba3bf16b0269e6bdac3352136 |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover) |
Keep the random-edit graph oracle strict while expecting revision FILETIME, the named author object, and attribution only on edited paragraphs. Check new author initials and preserve existing author/metadata fields; reject damaged attribution as well as unrelated content. Exercise repeated edits that reuse attribution.
Run the cleanup-failure test with its CI boundary inside the disposable fixture and assert the package-scoped clean command is reached before any concurrent lane starts.
Assisted-by: gpt-6.1-sol4 files changed, 86 insertions(+), 5 deletions(-)
tools/random_edit_campaign.py+43-1| ... | ... | @@ -25,7 +25,7 @@ def export(path): |
| 25 | 25 | sorted(hashlib.sha256(p.read_bytes()).hexdigest() for p in (output / 'assets').iterdir())) |
| 26 | 26 | |
| 27 | 27 | |
| 28 | def verify(before, after, record): | |
| 28 | def verify(before, after, record, *, author="Random editor"): | |
| 29 | 29 | old, old_text, old_assets = before |
| 30 | 30 | new, new_text, new_assets = after |
| 31 | 31 | assert old_assets == new_assets, 'An edit changed embedded payload bytes' |
| ... | ... | @@ -90,9 +90,51 @@ def verify(before, after, record): |
| 90 | 90 | else: |
| 91 | 91 | parent = previous['nodes'][key] |
| 92 | 92 | pending.extend((child, path + [key]) for child in parent['children'] + parent['content'] + parent['structure']) |
| 93 | assert '4' in current['roots'], 'An edit omitted revision attribution' | |
| 94 | metadata_id = current['roots']['4'] | |
| 95 | metadata = current['nodes'][metadata_id] | |
| 96 | assert metadata['kind']['type'] == 'RevisionMetadata', 'Revision attribution has another object kind' | |
| 97 | filetime = metadata['kind']['modified_filetime'] | |
| 98 | assert record['started_ms'] * 10000 + 116444736000000000 <= filetime < (record['finished_ms'] + 1) * 10000 + 116444736000000000, 'Revision time falls outside the edit interval' | |
| 99 | author_id = metadata['latest_author'] | |
| 100 | assert author_id in current['nodes'], 'Revision attribution has no author object' | |
| 101 | blank = { | |
| 102 | 'children': [], 'content': [], 'structure': [], 'spaces': [], | |
| 103 | 'child_level': None, 'layout': {key: None for key in node['layout']}, | |
| 104 | 'format': {key: None for key in node['format']}, 'created': None, 'modified': None, | |
| 105 | 'original_author': None, 'latest_author': None, 'media_ids': [], | |
| 106 | 'media_time_ms': None, 'tags': [], 'extra': [[]], | |
| 107 | } | |
| 108 | existing_authors = {key for key, value in previous['nodes'].items() | |
| 109 | if value['kind'] == {'type': 'Author', 'name': author}} | |
| 110 | assert not existing_authors or author_id in existing_authors, 'An edit duplicated an existing author' | |
| 111 | if author_id not in previous['nodes']: | |
| 112 | initials = ''.join(word[0].upper() for word in author.split()) | |
| 113 | revision['nodes'][author_id] = {**copy.deepcopy(blank), 'jcid': 0x120001, | |
| 114 | 'kind': {'type': 'Author', 'name': author}, | |
| 115 | 'extra': [[{'id': 0x1c001df8, 'value': {'Bytes': (initials + '\0').encode('utf-16-le').hex()}}]]} | |
| 116 | assert current['nodes'][author_id]['kind'] == {'type': 'Author', 'name': author}, 'The edit names another author' | |
| 117 | if '4' in previous['roots']: | |
| 118 | assert metadata_id == previous['roots']['4'], 'An edit replaced existing revision metadata identity' | |
| 119 | else: | |
| 120 | assert metadata_id not in previous['nodes'], 'Revision metadata reused another object identity' | |
| 121 | revision['roots']['4'] = metadata_id | |
| 122 | revision['nodes'][metadata_id] = {**copy.deepcopy(blank), 'jcid': 0x20044, | |
| 123 | 'kind': {'type': 'RevisionMetadata', 'modified_filetime': None}} | |
| 124 | detached_authors = {revision['nodes'][metadata_id]['latest_author']} | |
| 125 | revision['nodes'][metadata_id]['latest_author'] = author_id | |
| 126 | revision['nodes'][metadata_id]['kind']['modified_filetime'] = filetime | |
| 93 | 127 | for key in ancestors: |
| 94 | 128 | if previous['nodes'][key]['modified'] is not None: |
| 95 | 129 | revision['nodes'][key]['modified'] = changed['modified'] |
| 130 | if previous['nodes'][key]['kind']['type'] == 'Paragraph': | |
| 131 | detached_authors.add(revision['nodes'][key]['latest_author']) | |
| 132 | revision['nodes'][key]['latest_author'] = author_id | |
| 133 | revision['nodes'][key]['modified'] = filetime // 10000000 - 11960006400 | |
| 134 | for key in detached_authors - {None, author_id}: | |
| 135 | assert revision['nodes'][key]['kind']['type'] == 'Author' | |
| 136 | if not any(json.dumps(key) in json.dumps(value) for other, value in revision['nodes'].items() if other != key): | |
| 137 | del revision['nodes'][key] | |
| 96 | 138 | del node['modified'], changed['modified'] |
| 97 | 139 | del node['kind']['runs'], changed['kind']['runs'] |
| 98 | 140 | assert expected == observed, 'An edit changed unrelated document structure, metadata or properties' |
tools/test_cargo_fresh.py+4-1| ... | ... | @@ -94,15 +94,18 @@ class CargoFreshTest(unittest.TestCase): |
| 94 | 94 | def test_clean_failure_stops_ci_before_concurrent_lanes(self): |
| 95 | 95 | with tempfile.TemporaryDirectory(prefix='snowbound-ci-clean-') as folder, \ |
| 96 | 96 | patch.object(ci, 'RUNS', Path(folder)), \ |
| 97 | patch.object(ci, 'CI', Path(folder) / 'checkout'), \ | |
| 97 | 98 | patch.object(ci, 'lanes', return_value=[{'name': 'test'}]), \ |
| 98 | 99 | patch.object(ci, 'checkout', return_value='commit'), \ |
| 99 | 100 | patch.object(ci, 'jj', return_value='fixture'), \ |
| 100 | 101 | patch.object(ci, 'local_packages', return_value=['fixture']), \ |
| 101 | 102 | patch.object(ci.sys, 'argv', ['ci.py']), \ |
| 102 | patch.object(ci.Run, 'execute', return_value=101), \ | |
| 103 | patch.object(ci.Run, 'execute', return_value=101) as execute, \ | |
| 103 | 104 | patch.object(ci, 'ThreadPoolExecutor') as pool: |
| 104 | 105 | with self.assertRaisesRegex(SystemExit, 'Workspace cleanup failed'): |
| 105 | 106 | ci.main() |
| 107 | execute.assert_called_once() | |
| 108 | self.assertEqual(execute.call_args.args[0], ['cargo', 'clean', '--locked', '-p', 'fixture']) | |
| 106 | 109 | pool.assert_not_called() |
| 107 | 110 | |
| 108 | 111 |
tools/test_notebook_editor.py+1-1| ... | ... | @@ -59,7 +59,7 @@ class EditorTest(unittest.TestCase): |
| 59 | 59 | edit = intent['edit'] |
| 60 | 60 | verify(self.before, export(self.file), {'page': self.row['object'], 'space': edit['space'], 'object': self.oid, |
| 61 | 61 | 'range': [edit['action']['start'], edit['action']['end']], 'replacement': replacement, 'run_start': edit['action']['start'], |
| 62 | 'run_before': self.text, 'started_ms': intent['started_ms'], 'finished_ms': outcome['finished_ms']}) | |
| 62 | 'run_before': self.text, 'started_ms': intent['started_ms'], 'finished_ms': outcome['finished_ms']}, author='') | |
| 63 | 63 | after = self.file.read_bytes() |
| 64 | 64 | fresh = self.session.output / 'fresh-report' |
| 65 | 65 | editor.generate(self.session.output / 'g/1/snapshot', fresh, editable=True) |
tools/test_random_edit_campaign.py+38-2| ... | ... | @@ -5,7 +5,7 @@ import subprocess |
| 5 | 5 | import tempfile |
| 6 | 6 | import unittest |
| 7 | 7 | from random_edit_campaign import EDITOR, ROOT, export, verify |
| 8 | from document_model import ordered_pages | |
| 8 | from document_model import ordered_pages, view | |
| 9 | 9 | |
| 10 | 10 | |
| 11 | 11 | class RandomEditOracleTests(unittest.TestCase): |
| ... | ... | @@ -35,13 +35,28 @@ class RandomEditOracleTests(unittest.TestCase): |
| 35 | 35 | verify(before, after, record) |
| 36 | 36 | sid, oid = record['space'], record['object'] |
| 37 | 37 | rid = after[0]['spaces'][sid]['contexts']['{00000000-0000-0000-0000-000000000000},0'] |
| 38 | for kind in ['format', 'metadata', 'payload', 'structure', 'ancestor-time', 'unrelated-time']: | |
| 38 | for kind in ['format', 'metadata', 'payload', 'structure', 'ancestor-time', 'unrelated-time', | |
| 39 | 'revision-time', 'revision-author', 'revision-extra', 'missing-attribution', | |
| 40 | 'author-name', 'author-initials', 'paragraph-author', 'original-author']: | |
| 39 | 41 | damaged = copy.deepcopy(after) |
| 40 | 42 | if kind == 'format': damaged[1][sid][rid][oid][0]['format']['bold'] = not damaged[1][sid][rid][oid][0]['format']['bold'] |
| 41 | 43 | if kind == 'metadata': damaged[0]['spaces'][sid]['revisions'][rid]['nodes'][oid]['created'] = 0 |
| 42 | 44 | if kind == 'payload': damaged[2].append('another payload') |
| 43 | 45 | if kind == 'structure': damaged[0]['spaces'][sid]['revisions'][rid]['nodes'][oid]['children'].append('another object') |
| 44 | 46 | if kind == 'ancestor-time': damaged[0]['spaces'][sid]['revisions'][rid]['nodes'][record['page']]['modified'] = 0 |
| 47 | nodes = damaged[0]['spaces'][sid]['revisions'][rid]['nodes'] | |
| 48 | metadata = nodes[after[0]['spaces'][sid]['revisions'][rid]['roots']['4']] | |
| 49 | author_id = metadata['latest_author'] | |
| 50 | paragraph = next(key for key, node in nodes.items() | |
| 51 | if node['kind']['type'] == 'Paragraph' and oid in node['content']) | |
| 52 | if kind == 'revision-time': metadata['kind']['modified_filetime'] = 0 | |
| 53 | if kind == 'revision-author': metadata['latest_author'] = paragraph | |
| 54 | if kind == 'revision-extra': metadata['extra'][0].append({'id': 123, 'value': {'Bytes': '00'}}) | |
| 55 | if kind == 'missing-attribution': del damaged[0]['spaces'][sid]['revisions'][rid]['roots']['4'] | |
| 56 | if kind == 'author-name': nodes[author_id]['kind']['name'] = 'Another author' | |
| 57 | if kind == 'author-initials': nodes[author_id]['extra'][0][0]['value']['Bytes'] = '0000' | |
| 58 | if kind == 'paragraph-author': nodes[paragraph]['latest_author'] = nodes[paragraph]['original_author'] | |
| 59 | if kind == 'original-author': nodes[paragraph]['original_author'] = author_id | |
| 45 | 60 | if kind == 'unrelated-time': |
| 46 | 61 | old_rid = before[0]['spaces'][sid]['contexts']['{00000000-0000-0000-0000-000000000000},0'] |
| 47 | 62 | old = before[0]['spaces'][sid]['revisions'][old_rid]['nodes'] |
| ... | ... | @@ -50,6 +65,27 @@ class RandomEditOracleTests(unittest.TestCase): |
| 50 | 65 | nodes[key]['modified'] = 0 |
| 51 | 66 | with self.assertRaises(AssertionError, msg=kind): verify(before, damaged, record) |
| 52 | 67 | |
| 68 | def test_repeated_edits_reuse_attribution_and_preserve_existing_author_data(self): | |
| 69 | source = ROOT / 'corpus/native/20260905-05/snapshots/06-attachment/notebook/synthetic.one' | |
| 70 | with tempfile.TemporaryDirectory() as temporary: | |
| 71 | destination = Path(temporary) / 'edited.one' | |
| 72 | record = json.loads(subprocess.check_output([EDITOR, source, destination, '42'])) | |
| 73 | before = export(destination) | |
| 74 | _, previous = view(before[0], record['space']) | |
| 75 | author_id = previous['nodes'][previous['roots']['4']]['latest_author'] | |
| 76 | record = json.loads(subprocess.check_output([EDITOR, destination, '--in-place', '43', record['page']])) | |
| 77 | after = export(destination) | |
| 78 | verify(before, after, record) | |
| 79 | _, current = view(after[0], record['space']) | |
| 80 | self.assertEqual(current['roots']['4'], previous['roots']['4']) | |
| 81 | self.assertEqual(current['nodes'][current['roots']['4']]['latest_author'], author_id) | |
| 82 | self.assertEqual(current['nodes'][author_id], previous['nodes'][author_id]) | |
| 83 | damaged = copy.deepcopy(after) | |
| 84 | _, damaged_revision = view(damaged[0], record['space']) | |
| 85 | damaged_revision['nodes'][author_id]['extra'][0][0]['value']['Bytes'] = '0000' | |
| 86 | with self.assertRaises(AssertionError): | |
| 87 | verify(before, damaged, record) | |
| 88 | ||
| 53 | 89 | def test_seeded_edits_work_on_native_legacy_text(self): |
| 54 | 90 | source = ROOT / 'corpus/native/20260905-05/snapshots/02-text/notebook/synthetic.one' |
| 55 | 91 | with tempfile.TemporaryDirectory() as temporary: |