diff --git a/research/isolated-descendant-bytecode/README.md b/research/isolated-descendant-bytecode/README.md index f6a84ab99d..f9442e6a56 100644 --- a/research/isolated-descendant-bytecode/README.md +++ b/research/isolated-descendant-bytecode/README.md @@ -93,6 +93,20 @@ registered commands, verifier and `RESULT.json` are retained unchanged. This metadata correction was prepared for roger-sato with substantial OpenAI Codex assistance; Arham Tahir's original work and source credit are retained. +The `tests/test_research_source_history.py` row likewise authenticates the +original test copied by `pinned_sources()` from the exact base-commit Git blob; +the verifier does not import the live test file. Its original commit, blob and +SHA256 remain unchanged. Classifying that row as provenance permits the live +test's disposable Git fixtures to disable automatic maintenance, preventing +detached writers from racing temporary-directory cleanup. `VERIFY.json` still +declares the current test as a planner and publication input. The original +sixteen-run differential, verifier, commands and result remain unchanged. +Regression controls require the historical replay to pass even when a live +test copy cannot import, retain rejection of original pin corruption, and +confirm live changes still select replay and fail uncommitted-byte checks. +This fixture repair was prepared by Clear1nsight with substantial OpenAI +Codex assistance; it changes no source-history validation or research claim. + ```sh python3.12 -B scripts/research_package.py --package research/isolated-descendant-bytecode --check-only python3.12 -B scripts/research_package.py --package research/isolated-descendant-bytecode diff --git a/research/isolated-descendant-bytecode/SOURCES.json b/research/isolated-descendant-bytecode/SOURCES.json index 089b03a95a..5e3be409f1 100644 --- a/research/isolated-descendant-bytecode/SOURCES.json +++ b/research/isolated-descendant-bytecode/SOURCES.json @@ -34,7 +34,7 @@ }, { "path": "tests/test_research_source_history.py", - "usage": "input", + "usage": "provenance", "git_blob": "9c52884a4663c089e2331b769b50d14da4575d9f", "sha256": "2b974b7c1f5fcb1994c2b76be32554378cec91871224fca0af011d0640dc5514" } diff --git a/tests/test_research_descendant_source_roles.py b/tests/test_research_descendant_source_roles.py index 61378d20ee..8791e4bc03 100644 --- a/tests/test_research_descendant_source_roles.py +++ b/tests/test_research_descendant_source_roles.py @@ -17,6 +17,7 @@ ORIGINAL = '20ecf2891d80b5acf18d980b2164fb44ea7faa90' PACKAGE = 'research/isolated-descendant-bytecode' HELPER = 'scripts/research_package.py' +HISTORY_TEST = 'tests/test_research_source_history.py' def git(root, *args): @@ -64,15 +65,17 @@ def cli(self, script='research_package.py', arguments=None): def test_only_source_role_changes_and_frozen_evidence_is_retained(self): expected = copy.deepcopy(self.original) - row = next(row for row in expected['files'] if row['path'] == HELPER) - self.assertEqual(row['usage'], 'input') - row['usage'] = 'provenance' + for name in (HELPER, HISTORY_TEST): + row = next(row for row in expected['files'] if row['path'] == name) + self.assertEqual(row['usage'], 'input') + row['usage'] = 'provenance' self.assertEqual(json.loads((ROOT / PACKAGE / 'SOURCES.json').read_bytes()), expected) for name in ('VERIFY.json', 'TASK.json', 'verify.py', 'RESULT.json'): with self.subTest(name=name): self.assertEqual((ROOT / PACKAGE / name).read_bytes(), git(ROOT, 'show', ORIGINAL + ':' + PACKAGE + '/' + name)) self.assertIn(HELPER, self.manifest['inputs']) + self.assertIn(HISTORY_TEST, self.manifest['inputs']) self.assertEqual(len(self.manifest['commands']), 3) def test_original_gate_rejects_but_correct_role_accepts_current_helper(self): @@ -89,6 +92,60 @@ def test_original_gate_rejects_but_correct_role_accepts_current_helper(self): self.assertEqual(after.returncode, 0, after.stderr) self.assertIn('PASS_PACKAGE_METADATA_AND_PINS', after.stdout) + def test_live_history_guard_is_not_a_historical_source_substitution(self): + # An unimportable live test must not replace the authenticated original + # test copied by verify.py's pinned_sources() into the historical runs. + live = self.root / HISTORY_TEST + live.write_bytes(live.read_bytes() + b'\nraise RuntimeError("LIVE_HISTORY_SENTINEL")\n') + result = self.cli(arguments=['--package', PACKAGE]) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn('PASS_ISOLATED_DESCENDANT_BYTECODE', result.stdout) + self.assertIn('PASS_REGISTERED_RESEARCH_PACKAGE', result.stdout) + self.assertNotIn('LIVE_HISTORY_SENTINEL', result.stderr) + + def test_original_history_input_role_rejects_live_guard_successor(self): + migrated = json.loads((self.root / PACKAGE / 'SOURCES.json').read_bytes()) + original_role = copy.deepcopy(migrated) + next(row for row in original_role['files'] + if row['path'] == HISTORY_TEST)['usage'] = 'input' + self.write_sources(original_role) + before = self.cli() + self.assertNotEqual(before.returncode, 0) + self.assertIn('working source drift: ' + HISTORY_TEST, before.stderr) + self.write_sources(migrated) + after = self.cli() + self.assertEqual(after.returncode, 0, after.stderr) + + def test_historical_history_test_pin_corruption_rejects(self): + sources = json.loads((self.root / PACKAGE / 'SOURCES.json').read_bytes()) + for field, value, message in ( + ('git_blob', '0' * 40, 'source blob provenance mismatch'), + ('sha256', '0' * 64, 'source SHA256 provenance mismatch')): + with self.subTest(field=field): + corrupted = copy.deepcopy(sources) + next(row for row in corrupted['files'] + if row['path'] == HISTORY_TEST)[field] = value + self.write_sources(corrupted) + result = self.cli() + self.assertNotEqual(result.returncode, 0) + self.assertIn(message + ': ' + HISTORY_TEST, result.stderr) + self.assertNotIn('PASS_PACKAGE', result.stdout) + + def test_live_history_test_remains_a_planner_and_publication_input(self): + base = git(self.root, 'rev-parse', 'HEAD').decode().strip() + live = self.root / HISTORY_TEST + live.write_bytes(live.read_bytes() + b'\n# maintenance guard successor\n') + result = self.cli('check_research_commit.py', ['--package', PACKAGE]) + self.assertNotEqual(result.returncode, 0) + self.assertIn('working/commit bytes differ: ' + HISTORY_TEST, result.stderr) + self.commit() + explanation = self.root / 'history-plan.json' + result = self.cli(arguments=['--changed-since', base, '--explain-plan', str(explanation)]) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(json.loads(result.stdout), {'include': [{'package': PACKAGE}]}) + causes = json.loads(explanation.read_bytes())['selected'][0]['causes'] + self.assertEqual(causes['declared_inputs'], [HISTORY_TEST]) + def test_historical_blob_and_hash_corruption_still_rejects(self): sources = json.loads((self.root / PACKAGE / 'SOURCES.json').read_bytes()) for field, value, message in ( diff --git a/tests/test_research_source_history.py b/tests/test_research_source_history.py index 9c52884a46..aaaa47c607 100644 --- a/tests/test_research_source_history.py +++ b/tests/test_research_source_history.py @@ -1,5 +1,6 @@ """Publication history controls use synthetic Git objects, without remote fetches.""" import json +import os import subprocess import sys import tempfile @@ -33,7 +34,9 @@ def setUp(self): self.base = self.git('rev-parse', 'HEAD').strip() def git(self, *args, data=None): - result = subprocess.run(['git', *args], cwd=self.root, input=data, + # Fixture commits must not leave maintenance writing during cleanup. + result = subprocess.run(['git', '-c', 'maintenance.auto=false', '-c', 'gc.auto=0', + *args], cwd=self.root, input=data, capture_output=True, text=True, timeout=30, check=True) return result.stdout @@ -61,6 +64,35 @@ def test_base_and_explicit_ancestor_pins_are_deduplicated(self): self.assertEqual(self.check(history=False), {'commit': head, 'files': 3, 'package': self.package}) + def test_fixture_git_never_starts_automatic_maintenance(self): + # Force automatic work even in this small fixture. The positive + # control stays in the foreground so it cannot race temp cleanup. + for key, value in { + 'maintenance.auto': 'true', 'maintenance.autoDetach': 'false', + 'gc.autoDetach': 'false', 'maintenance.commit-graph.enabled': 'true', + 'maintenance.commit-graph.auto': '-1', 'gc.auto': '1', + }.items(): + self.git('config', key, value) + with tempfile.TemporaryDirectory() as directory: + trace = Path(directory) / 'git-trace.jsonl' + + def maintenance_children(): + return [event for line in trace.read_text(encoding='utf-8').splitlines() + if (event := json.loads(line)).get('event') == 'child_start' + and any(arg in {'maintenance', 'gc'} + for arg in event.get('argv', []))] + + with patch.dict(os.environ, {'GIT_TRACE2_EVENT': str(trace)}): + self.git('commit', '-qm', 'guarded fixture', '--allow-empty') + self.assertEqual(maintenance_children(), []) + + trace.unlink() + with patch.dict(os.environ, {'GIT_TRACE2_EVENT': str(trace)}): + subprocess.run(['git', '-c', 'core.hooksPath=/dev/null', 'commit', '-qm', + 'unguarded control', '--allow-empty'], cwd=self.root, + check=True, capture_output=True, timeout=30) + self.assertTrue(maintenance_children(), 'positive control must start maintenance') + def test_cached_side_commit_passes_bytes_but_not_publication_history(self): tree = self.git('rev-parse', self.base + '^{tree}').strip() foreign = self.git('commit-tree', tree, '-p', self.base,