This commit is contained in:
1 parent
c1d4345316
commit
522311022f
12 files changed
+337
-22
No files matched your search
@@ -153,7 +153,7 @@ class RunnerTests(WorkflowTests):
|
||||
super().setUp()
|
||||
for rel in ['AGENTS.md', 'README.md', 'master_prompt.md', 'roles/common.md',
|
||||
'docs/WEB_PROFILE.md', 'docs/ARCHITECTURE.md', 'docs/CONTRACTS.md',
|
||||
'docs/WORKFLOW.md', 'docs/AUTOMATION.md']:
|
||||
'docs/WORKFLOW.md', 'docs/AUTOMATION.md', 'docs/GIT_WORKFLOW.md']:
|
||||
path = self.root / rel
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text('# Fresh one-task instructions\n', encoding='utf-8')
|
||||
@@ -175,7 +175,7 @@ else:
|
||||
handoff=h/(id+'.md');handoff.write_text('# Handoff\\n\\nFixture implementation completed; next step independent review of this task.\\n')
|
||||
t.mutate('submit',id,agent,evidence=str(evidence),handoff=str(handoff))
|
||||
''', encoding='utf-8')
|
||||
self.config = {'argv': [sys.executable, str(fake), str(ROOT / 'scripts/tasks.py')], 'max_sessions': 20, 'timeout_seconds': 5}
|
||||
self.config = {'git_commits': False, 'argv': [sys.executable, str(fake), str(ROOT / 'scripts/tasks.py')], 'max_sessions': 20, 'timeout_seconds': 5}
|
||||
|
||||
def test_fresh_process_blocker_resolver_review_resume_chain(self):
|
||||
runner = RUN_MODULE.Runner(self.root, self.config)
|
||||
@@ -205,7 +205,7 @@ else:
|
||||
runner.run()
|
||||
|
||||
def test_missing_executable_creates_external_resolution_and_stops(self):
|
||||
runner = RUN_MODULE.Runner(self.root, {'argv': ['missing-tool-8b5e6d']})
|
||||
runner = RUN_MODULE.Runner(self.root, {'git_commits': False, 'argv': ['missing-tool-8b5e6d']})
|
||||
result = runner.run()
|
||||
self.assertEqual(result['stop'], 'needs_input')
|
||||
self.assertEqual(result['launched'], 1)
|
||||
@@ -213,7 +213,7 @@ else:
|
||||
self.assertTrue(result['external_blockers'])
|
||||
|
||||
def test_incomplete_agent_exit_creates_remediation(self):
|
||||
runner = RUN_MODULE.Runner(self.root, {'argv': [sys.executable, '-c', 'pass'], 'max_sessions': 1})
|
||||
runner = RUN_MODULE.Runner(self.root, {'git_commits': False, 'argv': [sys.executable, '-c', 'pass'], 'max_sessions': 1})
|
||||
result = runner.run()
|
||||
self.assertEqual(result['stop'], 'session_limit')
|
||||
state = self.tracker.state()
|
||||
@@ -221,7 +221,7 @@ else:
|
||||
self.assertTrue(state['retired_agents'])
|
||||
|
||||
def test_scope_violation_quarantines_and_requires_input(self):
|
||||
runner = RUN_MODULE.Runner(self.root, {'argv': [sys.executable, '-c', "from pathlib import Path;Path('bad-edit.txt').write_text('wrong scope')"]})
|
||||
runner = RUN_MODULE.Runner(self.root, {'git_commits': False, 'argv': [sys.executable, '-c', "from pathlib import Path;Path('bad-edit.txt').write_text('wrong scope')"]})
|
||||
result = runner.run()
|
||||
self.assertEqual(result['stop'], 'needs_input')
|
||||
self.assertEqual(self.tracker.state()['tasks']['S00-001']['status'], 'blocked')
|
||||
@@ -247,7 +247,7 @@ else:
|
||||
RUN_MODULE.Runner(self.root, {}).run(stage='S99', dry_run=True)
|
||||
|
||||
def test_timeout_terminates_before_queuing_resolution(self):
|
||||
runner = RUN_MODULE.Runner(self.root, {'argv': [sys.executable, '-c', 'import time;time.sleep(2)'], 'timeout_seconds': .05, 'max_sessions': 1})
|
||||
runner = RUN_MODULE.Runner(self.root, {'git_commits': False, 'argv': [sys.executable, '-c', 'import time;time.sleep(2)'], 'timeout_seconds': .05, 'max_sessions': 1})
|
||||
result = runner.run()
|
||||
self.assertEqual(result['stop'], 'session_limit')
|
||||
self.assertEqual(self.tracker.state()['tasks']['S00-001']['status'], 'blocked')
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
"""Real isolated Git repositories, fake agents; no user checkout commits."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import unittest
|
||||
|
||||
from test_automation import RunnerTests, RUN_MODULE
|
||||
|
||||
|
||||
class GitCommitTests(RunnerTests):
|
||||
def git(self, *args):
|
||||
return subprocess.check_output(['git', '-C', str(self.root), *args], text=True).strip()
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
(self.root / '.gitignore').write_text('state/runs/\n.runner.lock/\n.state.lock/\n__pycache__/\n')
|
||||
fake = self.root / 'fake_agent.py'
|
||||
text = fake.read_text()
|
||||
text = text.replace("if action=='review':t.mutate", """messages=r/'state/commit_messages';messages.mkdir(exist_ok=True)
|
||||
(messages/(agent+'.txt')).write_text(('Review restored input and record acceptance' if action=='review' else 'Restore required input within assigned scope')+'\\n\\nChanges: Record the bounded fixture result and handoff.\\nValidation: Isolated positive and negative fixture checks passed.\\n')
|
||||
if action=='claim':
|
||||
source=r/('src/fix.txt' if id.startswith('B') else 'src/'+str(int(id.split('-')[-1]))+'.txt');source.parent.mkdir(exist_ok=True);source.write_text('verified task '+id)
|
||||
if action=='review':t.mutate""")
|
||||
# Remediation IDs use the actual parent-derived naming convention.
|
||||
text = text.replace("id.startswith('B')", "id not in {'S00-001','S00-002','S00-003'}")
|
||||
fake.write_text(text)
|
||||
self.tracker.render()
|
||||
self.git('init', '-b', 'main')
|
||||
self.git('config', 'user.name', 'Fixture Agent')
|
||||
self.git('config', 'user.email', 'fixture@example.invalid')
|
||||
self.git('config', 'commit.gpgsign', 'false')
|
||||
self.git('add', '.')
|
||||
self.git('commit', '-m', 'Initialize isolated fixture')
|
||||
self.config['git_commits'] = True
|
||||
|
||||
def test_chain_commits_each_transition_with_task_and_actual_message(self):
|
||||
result = RUN_MODULE.Runner(self.root, self.config).run()
|
||||
self.assertEqual(result['stop'], 'complete', result)
|
||||
self.assertEqual(self.git('rev-list', '--count', 'HEAD'), '10')
|
||||
self.assertEqual(self.git('status', '--porcelain'), '')
|
||||
log = self.git('log', '--format=%B')
|
||||
self.assertIn('S00-001: Restore required input within assigned scope', log)
|
||||
self.assertIn('S00-001: Review restored input and record acceptance', log)
|
||||
self.assertIn('Record blocked task and remediation handoff', log)
|
||||
self.assertIn('Validation: Isolated positive and negative fixture checks passed.', log)
|
||||
self.assertEqual(result['last_commit'], self.git('rev-parse', 'HEAD'))
|
||||
commits = self.git('log', '--format=%H', '--grep=^S00-002: Restore').splitlines()
|
||||
files = self.git('show', '--format=', '--name-only', commits[0])
|
||||
self.assertIn('src/2.txt', files)
|
||||
self.assertIn('state/tasks.json', files)
|
||||
self.assertNotIn('output.log', files)
|
||||
|
||||
def test_dirty_existing_file_is_not_claimed_or_committed(self):
|
||||
before = self.tracker.state()
|
||||
(self.root / 'unrelated.txt').write_text('user work')
|
||||
with self.assertRaisesRegex(ValueError, 'must be clean'):
|
||||
RUN_MODULE.Runner(self.root, self.config).run()
|
||||
self.assertEqual(before, self.tracker.state())
|
||||
self.assertEqual(self.git('rev-list', '--count', 'HEAD'), '1')
|
||||
|
||||
def test_missing_message_stops_before_next_agent_and_preserves_work(self):
|
||||
fake = self.root / 'fake_agent.py'
|
||||
fake.write_text(fake.read_text().replace("(messages/(agent+'.txt')).write_text", "(messages/'wrong-name.txt').write_text"))
|
||||
self.git('add', 'fake_agent.py')
|
||||
self.git('commit', '-m', 'Simulate missing task message')
|
||||
result = RUN_MODULE.Runner(self.root, self.config).run()
|
||||
self.assertEqual(result['stop'], 'git_error')
|
||||
self.assertEqual(result['launched'], 2) # Blocker checkpoint, then failed resolver submit.
|
||||
self.assertIn('Missing task commit message', result['commit_error'])
|
||||
self.assertTrue(self.git('status', '--porcelain'))
|
||||
|
||||
def test_rejected_git_hook_stops_without_starting_another_agent(self):
|
||||
hook = self.root / '.git/hooks/pre-commit'
|
||||
hook.write_text('#!/bin/sh\nexit 1\n')
|
||||
hook.chmod(0o755)
|
||||
result = RUN_MODULE.Runner(self.root, self.config).run()
|
||||
self.assertEqual(result['stop'], 'git_error')
|
||||
self.assertEqual(result['launched'], 1)
|
||||
self.assertIn('Git commit failed', result['commit_error'])
|
||||
self.assertEqual(self.git('rev-list', '--count', 'HEAD'), '1')
|
||||
self.assertTrue(self.git('diff', '--cached', '--name-only'))
|
||||
|
||||
def test_changed_head_is_not_accepted_as_coordinator_commit(self):
|
||||
committer = RUN_MODULE.Runner(self.root, self.config).commits
|
||||
head = committer.preflight()
|
||||
self.git('commit', '--allow-empty', '-m', 'Unexpected agent commit')
|
||||
with self.assertRaisesRegex(ValueError, 'changed Git HEAD'):
|
||||
committer.commit({'id': 'S00-001'}, 'agent', {}, {}, head, 'Fixture')
|
||||
|
||||
def test_nested_project_rejects_edits_to_parent_files(self):
|
||||
project = self.root / 'nested'
|
||||
project.mkdir()
|
||||
committer = RUN_MODULE.TaskCommits(project)
|
||||
head = committer.preflight()
|
||||
(self.root / 'parent.txt').write_text('unrelated')
|
||||
with self.assertRaisesRegex(ValueError, 'outside project root'):
|
||||
committer.commit({'id': 'S00-001'}, 'agent', {}, {}, head, 'Fixture')
|
||||
self.assertEqual(self.git('diff', '--cached', '--name-only'), '')
|
||||
|
||||
def test_nested_project_commits_literal_paths_and_deletions(self):
|
||||
project = self.root / 'nested'
|
||||
project.mkdir()
|
||||
(project / '.gitignore').write_text('state/runs/\n')
|
||||
removed = project / 'old.txt'
|
||||
removed.write_text('old')
|
||||
self.git('add', 'nested')
|
||||
self.git('commit', '-m', 'Initialize nested fixture')
|
||||
committer = RUN_MODULE.TaskCommits(project)
|
||||
head = committer.preflight()
|
||||
removed.unlink()
|
||||
(project / 'file [1].txt').write_text('bounded output')
|
||||
draft = project / 'state/commit_messages/agent.txt'
|
||||
draft.parent.mkdir(parents=True)
|
||||
draft.write_text('Replace obsolete fixture with literal named output\n\nChanges: Replace the obsolete fixture file.\nValidation: Checked deletion and literal path staging.\n')
|
||||
# A nested foundation ships its own project-relative ignore rules.
|
||||
commit = committer.commit({'id': 'S00-001', 'role': 'front', 'action': 'claim'},
|
||||
'agent', {'status': 'review'}, {}, head, 'Fixture')
|
||||
files = self.git('show', '--format=', '--name-only', commit)
|
||||
self.assertIn('nested/file [1].txt', files)
|
||||
self.assertIn('nested/old.txt', files)
|
||||
self.assertFalse(removed.exists())
|
||||
self.assertEqual(self.git('status', '--porcelain'), '')
|
||||
|
||||
def test_commit_message_rejects_empty_or_unstructured_completion(self):
|
||||
committer = RUN_MODULE.TaskCommits(self.root)
|
||||
message = self.root / 'state/commit_messages/agent.txt'
|
||||
message.parent.mkdir(parents=True)
|
||||
message.write_text('Task done\n')
|
||||
with self.assertRaisesRegex(ValueError, 'concrete English subject'):
|
||||
committer.message({'id': 'S00-001'}, 'agent', {'status': 'review'}, {})
|
||||
|
||||
|
||||
def load_tests(loader, tests, pattern):
|
||||
return unittest.TestSuite(GitCommitTests(name) for name in GitCommitTests.__dict__ if name.startswith('test_'))
|
||||
Reference in new issue
Block a user