This commit is contained in:
commit
54bfc0f34f
357 files changed
+41075
No files matched your search
@@ -0,0 +1,282 @@
|
||||
"""Isolated blocker orchestration tests. No real model/provider/product task is run."""
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
import uuid
|
||||
from unittest.mock import patch
|
||||
|
||||
from test_tasks import WorkflowTests, ROOT, MODULE
|
||||
|
||||
RUN_SPEC = importlib.util.spec_from_file_location('luma_runner', ROOT / 'scripts/runner.py')
|
||||
RUN_MODULE = importlib.util.module_from_spec(RUN_SPEC)
|
||||
RUN_SPEC.loader.exec_module(RUN_MODULE)
|
||||
|
||||
|
||||
class BlockerTests(WorkflowTests):
|
||||
# Inherit temporary fixture helpers, but do not re-run the inherited test suite.
|
||||
def issue(self, **changes):
|
||||
result = {'title': 'Fix concrete cause', 'reason': 'A required local condition is unavailable',
|
||||
'role': 'front', 'reviewer_role': 'qa', 'scope': ['src/fix.*'],
|
||||
'outcome': 'Make the single missing condition verifiably available',
|
||||
'steps': ['Reproduce', 'Fix local cause', 'Check'],
|
||||
'acceptance': ['Original failure resolved'], 'checks': ['Positive and negative fixture'],
|
||||
'requirements': ['REQ-001']}
|
||||
result.update(changes)
|
||||
return result
|
||||
|
||||
def finish(self, ident, owner, reviewer):
|
||||
self.tracker.mutate('claim', ident, owner, role='front')
|
||||
self.tracker.mutate('submit', ident, owner,
|
||||
evidence=self.evidence(ident + '-' + owner + '.md'),
|
||||
handoff=self.evidence(ident + '.md', 'state/handoffs'))
|
||||
self.tracker.mutate('approve', ident, reviewer, role='qa',
|
||||
evidence=self.evidence(ident + '-review.md'))
|
||||
|
||||
def test_escalation_blocks_parent_creates_ready_child_and_retires(self):
|
||||
self.claim()
|
||||
result = self.tracker.escalate('S00-001', 'owner', 'front', self.issue())
|
||||
child = result['resolution_task']
|
||||
self.assertTrue(result['agent_must_exit'])
|
||||
self.assertEqual(self.tracker.state()['tasks']['S00-001']['status'], 'blocked')
|
||||
self.assertIn(child, self.tracker.task('S00-001')['depends_on'])
|
||||
self.assertNotIn('S00-001', self.tracker.task(child)['depends_on'])
|
||||
self.assertEqual(self.tracker.ready()[0]['id'], child)
|
||||
with self.assertRaisesRegex(ValueError, 'fresh session'):
|
||||
self.claim('S00-003', 'owner')
|
||||
self.tracker.validate() # Child card is in the same journal, no render race.
|
||||
self.tracker.render()
|
||||
|
||||
def test_reviewed_resolution_requeues_parent_for_new_agent(self):
|
||||
self.claim()
|
||||
child = self.tracker.escalate('S00-001', 'owner', 'front', self.issue())['resolution_task']
|
||||
self.finish(child, 'resolver', 'resolver-reviewer')
|
||||
row = self.tracker.state()['tasks']['S00-001']
|
||||
self.assertEqual((row['status'], row['owner']), ('todo', None))
|
||||
self.assertEqual(self.tracker.ready()[0]['id'], 'S00-001')
|
||||
with self.assertRaises(ValueError):
|
||||
self.claim('S00-001', 'owner')
|
||||
self.claim('S00-001', 'fresh-owner')
|
||||
self.assertIn('Previous handoff', self.tracker.packet('S00-001', 'fresh-owner'))
|
||||
|
||||
def test_submit_only_does_not_unblock(self):
|
||||
self.claim()
|
||||
child = self.tracker.escalate('S00-001', 'owner', 'front', self.issue())['resolution_task']
|
||||
self.tracker.mutate('claim', child, 'resolver', role='front')
|
||||
self.tracker.mutate('submit', child, 'resolver', evidence=self.evidence('resolve.md'), handoff=self.evidence('resolve.md', 'state/handoffs'))
|
||||
self.assertEqual(self.tracker.state()['tasks']['S00-001']['status'], 'blocked')
|
||||
|
||||
def test_reviewer_can_escalate_then_new_reviewer_continues(self):
|
||||
self.claim()
|
||||
self.submit()
|
||||
child = self.tracker.escalate('S00-001', 'reviewer', 'qa', self.issue())['resolution_task']
|
||||
self.finish(child, 'resolver', 'resolve-review')
|
||||
self.assertEqual(self.tracker.state()['tasks']['S00-001']['status'], 'review')
|
||||
with self.assertRaises(ValueError):
|
||||
self.tracker.mutate('approve', 'S00-001', 'reviewer', role='qa', evidence=self.evidence('review.md'))
|
||||
self.tracker.mutate('approve', 'S00-001', 'fresh-reviewer', role='qa', evidence=self.evidence('fresh-review.md'))
|
||||
|
||||
def test_cycle_or_unbounded_scope_is_rejected_without_partial_state(self):
|
||||
self.claim()
|
||||
before = self.tracker.state()
|
||||
for changes in [{'depends_on': ['S00-001']}, {'depends_on': ['S00-002']}, {'scope': ['**']}, {'scope': ['../outside/**']}, {'scope': ['specs/tz_clear.md']}]:
|
||||
with self.assertRaises(ValueError):
|
||||
self.tracker.escalate('S00-001', 'owner', 'front', self.issue(**changes))
|
||||
self.assertEqual(self.tracker.state(), before)
|
||||
self.assertEqual(len(self.tracker.definitions()['tasks']), 3)
|
||||
|
||||
def test_external_blocker_waits_until_release_and_verification(self):
|
||||
self.claim()
|
||||
child = self.tracker.escalate('S00-001', 'owner', 'front', self.issue(requires_user_input=True))['resolution_task']
|
||||
self.assertEqual(self.tracker.state()['tasks'][child]['status'], 'blocked')
|
||||
self.assertNotIn(child, [r['id'] for r in self.tracker.ready()])
|
||||
self.tracker.render()
|
||||
self.tracker.validate()
|
||||
MODULE.task_extensions.release_external(self.tracker, child, 'coordinator', 'orchestrator', 'Required test service restored')
|
||||
self.assertEqual(self.tracker.state()['tasks'][child]['status'], 'todo')
|
||||
self.assertEqual(self.tracker.state()['tasks']['S00-001']['status'], 'blocked')
|
||||
|
||||
def test_duplicate_escalation_cannot_spawn_second_child(self):
|
||||
self.claim()
|
||||
self.tracker.escalate('S00-001', 'owner', 'front', self.issue())
|
||||
before = self.tracker.state()
|
||||
with self.assertRaises(ValueError):
|
||||
self.tracker.escalate('S00-001', 'owner', 'front', self.issue())
|
||||
self.assertEqual(before, self.tracker.state())
|
||||
self.assertEqual(len(self.tracker.definitions()['tasks']), 4)
|
||||
|
||||
def test_invalid_context_or_string_boolean_rejected(self):
|
||||
self.claim()
|
||||
for change in [{'requires_user_input': 'false'}, {'ui_sections': [999]}, {'spec_sections': 'all'}]:
|
||||
with self.assertRaises(ValueError):
|
||||
self.tracker.escalate('S00-001', 'owner', 'front', self.issue(**change))
|
||||
self.assertEqual(len(self.tracker.definitions()['tasks']), 3)
|
||||
|
||||
def test_nested_blocker_limit_waits_for_external_resolution(self):
|
||||
self.claim()
|
||||
current, agent = 'S00-001', 'owner'
|
||||
for depth in range(1, 5):
|
||||
result = self.tracker.escalate(current, agent, 'front', self.issue())
|
||||
current = result['resolution_task']
|
||||
if depth < 4:
|
||||
agent = 'resolver-' + str(depth)
|
||||
self.claim(current, agent)
|
||||
self.assertTrue(result['requires_user_input'])
|
||||
self.assertEqual(self.tracker.task(current)['blocker_depth'], 4)
|
||||
|
||||
def test_plan_state_journal_recovers_after_interrupted_artifact_write(self):
|
||||
self.claim()
|
||||
real = MODULE.atomic_write
|
||||
calls = []
|
||||
def fail_after_journal(path, text):
|
||||
calls.append(str(path))
|
||||
if len(calls) == 2:
|
||||
raise OSError('Simulated interruption')
|
||||
return real(path, text)
|
||||
with patch.object(MODULE, 'atomic_write', side_effect=fail_after_journal):
|
||||
with self.assertRaises(OSError):
|
||||
self.tracker.escalate('S00-001', 'owner', 'front', self.issue())
|
||||
self.assertTrue(self.tracker.journal_path.exists())
|
||||
recovered = self.tracker.state()
|
||||
self.assertFalse(self.tracker.journal_path.exists())
|
||||
self.assertEqual(recovered['tasks']['S00-001']['status'], 'blocked')
|
||||
child = recovered['tasks']['S00-001']['blocked_by'][0]
|
||||
self.assertIn(child, self.tracker.index())
|
||||
self.tracker.render()
|
||||
self.tracker.validate()
|
||||
|
||||
|
||||
class RunnerTests(WorkflowTests):
|
||||
def setUp(self):
|
||||
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']:
|
||||
path = self.root / rel
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text('# Fresh one-task instructions\n', encoding='utf-8')
|
||||
fake = self.root / 'fake_agent.py'
|
||||
fake.write_text('''import importlib.util,json,os,pathlib,sys
|
||||
spec=importlib.util.spec_from_file_location('tool',sys.argv[1]);m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
|
||||
r=pathlib.Path(os.environ['LUMA_PROJECT_ROOT']);t=m.Tracker(r)
|
||||
id=os.environ['LUMA_TASK_ID'];agent=os.environ['LUMA_AGENT_ID'];role=os.environ['LUMA_ROLE'];action=os.environ['LUMA_ACTION']
|
||||
prompt=sys.stdin.read();assert 'fresh session' in prompt
|
||||
log=r/'state/test-launches.json';entries=json.loads(log.read_text()) if log.exists() else [];entries.append({'task':id,'agent':agent,'action':action});log.write_text(json.dumps(entries))
|
||||
if id=='S00-001' and action=='claim' and not (r/'state/first-blocker').exists():
|
||||
(r/'state/first-blocker').write_text('recorded')
|
||||
issue={'title':'Fix local missing input','reason':'fixture blocker','role':'front','reviewer_role':'qa','scope':['src/fix.*'],'outcome':'Restore required local input','steps':['Restore local input'],'acceptance':['Input available'],'checks':['Original check succeeds'],'requirements':['REQ-001']}
|
||||
t.escalate(id,agent,role,issue);sys.exit(0)
|
||||
e=r/'state/evidence';h=r/'state/handoffs';e.mkdir(exist_ok=True);h.mkdir(exist_ok=True)
|
||||
evidence=e/(id+'-'+agent+'.md');evidence.write_text('# Evidence\\n\\nActual isolated fixture check succeeded; expected negative outcome checked.\\n')
|
||||
if action=='review':t.mutate('approve',id,agent,role=role,evidence=str(evidence))
|
||||
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}
|
||||
|
||||
def test_fresh_process_blocker_resolver_review_resume_chain(self):
|
||||
runner = RUN_MODULE.Runner(self.root, self.config)
|
||||
result = runner.run()
|
||||
self.assertEqual(result['stop'], 'complete')
|
||||
launches = json.loads((self.root / 'state/test-launches.json').read_text())
|
||||
self.assertEqual(len(launches), 9)
|
||||
self.assertEqual(len({x['agent'] for x in launches}), 9)
|
||||
original = [x for x in launches if x['task'] == 'S00-001' and x['action'] == 'claim']
|
||||
self.assertEqual(len(original), 2)
|
||||
self.assertNotEqual(original[0]['agent'], original[1]['agent'])
|
||||
self.assertIn(original[0]['agent'], self.tracker.state()['retired_agents'])
|
||||
self.assertFalse(runner.lock_path.exists())
|
||||
|
||||
def test_dry_run_does_not_claim_or_write(self):
|
||||
runner = RUN_MODULE.Runner(self.root, {})
|
||||
before = self.tracker.state()
|
||||
result = runner.run(dry_run=True)
|
||||
self.assertTrue(result['dry_run'])
|
||||
self.assertEqual(self.tracker.state(), before)
|
||||
self.assertFalse((self.root / 'state/runs').exists())
|
||||
|
||||
def test_runner_lock_prevents_second_coordinator(self):
|
||||
runner = RUN_MODULE.Runner(self.root, self.config)
|
||||
with runner.lock():
|
||||
with self.assertRaises(ValueError):
|
||||
runner.run()
|
||||
|
||||
def test_missing_executable_creates_external_resolution_and_stops(self):
|
||||
runner = RUN_MODULE.Runner(self.root, {'argv': ['missing-tool-8b5e6d']})
|
||||
result = runner.run()
|
||||
self.assertEqual(result['stop'], 'needs_input')
|
||||
self.assertEqual(result['launched'], 1)
|
||||
self.assertEqual(self.tracker.state()['tasks']['S00-001']['status'], 'blocked')
|
||||
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})
|
||||
result = runner.run()
|
||||
self.assertEqual(result['stop'], 'session_limit')
|
||||
state = self.tracker.state()
|
||||
self.assertEqual(state['tasks']['S00-001']['status'], 'blocked')
|
||||
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')"]})
|
||||
result = runner.run()
|
||||
self.assertEqual(result['stop'], 'needs_input')
|
||||
self.assertEqual(self.tracker.state()['tasks']['S00-001']['status'], 'blocked')
|
||||
record = json.loads(next((self.root / 'state/runs').glob('*/run.json')).read_text())
|
||||
self.assertEqual(record['scope_violations'], ['bad-edit.txt'])
|
||||
|
||||
def test_reporter_process_is_stopped_after_escalate_without_waiting_timeout(self):
|
||||
import time
|
||||
fake = self.root / 'fake_agent.py'
|
||||
fake.write_text(fake.read_text().replace('t.escalate(id,agent,role,issue);sys.exit(0)', 't.escalate(id,agent,role,issue);import time;time.sleep(10);sys.exit(0)'))
|
||||
config = dict(self.config, max_sessions=1, timeout_seconds=5)
|
||||
started = time.monotonic()
|
||||
result = RUN_MODULE.Runner(self.root, config).run()
|
||||
self.assertLess(time.monotonic() - started, 3)
|
||||
self.assertEqual(result['launched'], 1)
|
||||
record = json.loads(next((self.root / 'state/runs').glob('*/run.json')).read_text())
|
||||
self.assertTrue(record['stopped_after_transition'])
|
||||
self.assertEqual(len(self.tracker.definitions()['tasks']), 4)
|
||||
self.assertEqual(record['result']['blocked'], True)
|
||||
|
||||
def test_invalid_stage_is_not_silent_idle(self):
|
||||
with self.assertRaisesRegex(ValueError, 'Unknown stage'):
|
||||
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})
|
||||
result = runner.run()
|
||||
self.assertEqual(result['stop'], 'session_limit')
|
||||
self.assertEqual(self.tracker.state()['tasks']['S00-001']['status'], 'blocked')
|
||||
|
||||
def test_scope_matcher_checks_modified_created_deleted_files(self):
|
||||
self.assertEqual(RUN_MODULE.scope_violations({'a': 'old', 'gone': 'old'}, {'a': 'new', 'created': 'new'}, ['a']), ['created', 'gone'])
|
||||
|
||||
|
||||
class FoundationRunnerTests(unittest.TestCase):
|
||||
def test_all_current_fresh_agent_prompts_fit_configured_budget(self):
|
||||
runner = RUN_MODULE.Runner(ROOT, {})
|
||||
lengths = []
|
||||
for task in runner.tracker.definitions()['tasks']:
|
||||
lengths.append(len(runner.prompt({'id': task['id'], 'role': task['role'], 'action': 'claim'}, 'auto-preview')))
|
||||
self.assertLessEqual(max(lengths), 80000)
|
||||
|
||||
def test_unconfigured_runner_does_not_claim_product_work(self):
|
||||
before = (ROOT / 'state/tasks.json').read_bytes()
|
||||
result = subprocess.run([sys.executable, str(ROOT / 'scripts/runner.py'), '--config', 'missing-' + uuid.uuid4().hex + '.json'], capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 2)
|
||||
self.assertIn('configuration', result.stderr)
|
||||
self.assertEqual(before, (ROOT / 'state/tasks.json').read_bytes())
|
||||
|
||||
|
||||
# Imported helper class is not a second copy of its original tests.
|
||||
# Remove inherited test methods from these subclasses' discovery via explicit suite.
|
||||
def load_tests(loader, tests, pattern):
|
||||
import unittest
|
||||
suite = unittest.TestSuite()
|
||||
for cls in [BlockerTests, RunnerTests, FoundationRunnerTests]:
|
||||
suite.addTests(cls(name) for name in cls.__dict__ if name.startswith('test_'))
|
||||
return suite
|
||||
@@ -0,0 +1,318 @@
|
||||
"""Meaningful workflow/schema tests on temporary projects, never on shipped state."""
|
||||
import copy
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SPEC = importlib.util.spec_from_file_location('luma_tasks', ROOT / 'scripts/tasks.py')
|
||||
MODULE = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(MODULE)
|
||||
Tracker = MODULE.Tracker
|
||||
|
||||
|
||||
class WorkflowTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.root = Path(self.temp.name)
|
||||
for folder in ['planning/tasks', 'state/evidence', 'state/handoffs', 'roles', 'specs']:
|
||||
(self.root / folder).mkdir(parents=True, exist_ok=True)
|
||||
source = '# Spec\n\n## 1. Core\n\n**REQ-001.** One real behavior.\n'
|
||||
(self.root / 'specs/tz_clear.md').write_text(source)
|
||||
self.write('specs/manifest.json', {'sha256': {'tz_clear.md': hashlib.sha256(source.encode()).hexdigest()}})
|
||||
self.write('roles/index.json', {'roles': {'front': {}, 'qa': {}}})
|
||||
for role in ['front', 'qa']:
|
||||
(self.root / f'roles/{role}.md').write_text('# Role\n')
|
||||
self.write('planning/requirements.json', [{'id': 'REQ-001', 'source': 'specs/tz_clear.md', 'text': 'One real behavior.', 'applicability': 'web', 'reason': 'Applies'}])
|
||||
tasks = []
|
||||
for i in range(1, 4):
|
||||
task_id = f'S00-{i:03}'
|
||||
tasks.append({'id': task_id, 'stage': 'S00', 'role': 'front', 'reviewer_role': 'qa', 'title': task_id, 'outcome': 'One bounded result', 'depends_on': ['S00-001'] if i == 2 else [], 'requirements': ['REQ-001'], 'scope': [f'src/{i}.*'], 'inputs': ['roles/front.md'], 'steps': ['Implement accepted result'], 'acceptance': ['Behavior checked'], 'checks': ['Positive and negative scenario'], 'spec_sections': [1], 'ui_sections': []})
|
||||
(self.root / f'planning/tasks/{task_id}.md').write_text('# Placeholder\n')
|
||||
self.write('planning/tasks.json', {'stages': [{'id': 'S00', 'title': 'Stage', 'goal': 'Goal', 'entry_gate': None}], 'tasks': tasks})
|
||||
self.write('state/tasks.json', {'revision': 0, 'tasks': {t['id']: {'status': 'todo', 'owner': None} for t in tasks}, 'events': []})
|
||||
self.tracker = Tracker(self.root)
|
||||
|
||||
def tearDown(self):
|
||||
self.temp.cleanup()
|
||||
|
||||
def write(self, path, data):
|
||||
(self.root / path).write_text(json.dumps(data, ensure_ascii=False, indent=2) + '\n')
|
||||
|
||||
def evidence(self, name='owner.md', folder='state/evidence'):
|
||||
path = f'{folder}/{name}'
|
||||
(self.root / path).write_text('# Real evidence\n\nCommand: test scenario; actual exit code 0; checked fixture and negative case.\n')
|
||||
return path
|
||||
|
||||
def claim(self, task='S00-001', agent='owner'):
|
||||
return self.tracker.mutate('claim', task, agent, role='front')
|
||||
|
||||
def submit(self):
|
||||
return self.tracker.mutate('submit', 'S00-001', 'owner', evidence=self.evidence(), handoff=self.evidence('S00-001.md', 'state/handoffs'))
|
||||
|
||||
def complete(self):
|
||||
self.claim()
|
||||
self.submit()
|
||||
return self.tracker.mutate('approve', 'S00-001', 'reviewer', role='qa', evidence=self.evidence('review.md'))
|
||||
|
||||
def test_fixture_validates(self):
|
||||
self.assertEqual(self.tracker.validate()['tasks'], 3)
|
||||
|
||||
def test_next_filters_dependencies_and_role(self):
|
||||
self.assertEqual([x['id'] for x in self.tracker.ready('front')], ['S00-001', 'S00-003'])
|
||||
self.assertEqual(self.tracker.ready('qa'), [])
|
||||
|
||||
def test_claim_dependency_denied_without_state_mutation(self):
|
||||
before = self.tracker.state()
|
||||
with self.assertRaises(ValueError):
|
||||
self.claim('S00-002')
|
||||
self.assertEqual(self.tracker.state(), before)
|
||||
|
||||
def test_claim_wrong_role_denied(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self.tracker.mutate('claim', 'S00-001', 'owner', role='qa')
|
||||
|
||||
def test_double_claim_and_multiple_owned_tasks_denied(self):
|
||||
self.claim()
|
||||
with self.assertRaises(ValueError):
|
||||
self.claim(agent='other')
|
||||
with self.assertRaises(ValueError):
|
||||
self.claim('S00-003')
|
||||
|
||||
def test_submit_requires_owner_and_files(self):
|
||||
self.claim()
|
||||
with self.assertRaises(ValueError):
|
||||
self.tracker.mutate('submit', 'S00-001', 'other')
|
||||
with self.assertRaises(ValueError):
|
||||
self.tracker.mutate('submit', 'S00-001', 'owner')
|
||||
self.assertEqual(self.tracker.state()['tasks']['S00-001']['status'], 'in_progress')
|
||||
|
||||
def test_submit_and_review_queue(self):
|
||||
self.claim()
|
||||
self.submit()
|
||||
self.assertEqual(self.tracker.ready('qa')[0]['action'], 'review')
|
||||
self.assertEqual(self.tracker.ready('front'), [{'id': 'S00-003', 'action': 'claim', 'role': 'front', 'title': 'S00-003'}])
|
||||
|
||||
def test_self_review_and_wrong_reviewer_role_denied(self):
|
||||
self.claim()
|
||||
self.submit()
|
||||
path = self.evidence('review.md')
|
||||
with self.assertRaises(ValueError):
|
||||
self.tracker.mutate('approve', 'S00-001', 'owner', role='qa', evidence=path)
|
||||
with self.assertRaises(ValueError):
|
||||
self.tracker.mutate('approve', 'S00-001', 'reviewer', role='front', evidence=path)
|
||||
|
||||
def test_approval_unlocks_dependency_and_keeps_history(self):
|
||||
self.complete()
|
||||
self.assertIn('S00-002', [x['id'] for x in self.tracker.ready('front')])
|
||||
self.assertEqual([e['action'] for e in self.tracker.state()['events']], ['claim', 'submit', 'approve'])
|
||||
self.assertEqual(self.tracker.state()['revision'], 3)
|
||||
self.tracker.validate()
|
||||
|
||||
def test_reject_requeues_for_new_owner(self):
|
||||
self.claim()
|
||||
self.submit()
|
||||
self.tracker.mutate('reject', 'S00-001', 'reviewer', role='qa', reason='Fix observed case')
|
||||
row = self.tracker.state()['tasks']['S00-001']
|
||||
self.assertEqual((row['status'], row['owner']), ('todo', None))
|
||||
|
||||
def test_bare_block_unblock_denied(self):
|
||||
self.claim()
|
||||
for action in ['block', 'unblock']:
|
||||
with self.assertRaisesRegex(ValueError, 'Use escalate'):
|
||||
self.tracker.mutate(action, 'S00-001', 'owner', reason='No remediation')
|
||||
self.assertEqual(self.tracker.state()['tasks']['S00-001']['status'], 'in_progress')
|
||||
|
||||
def test_reassign_explicit_and_one_owner(self):
|
||||
self.claim()
|
||||
self.claim('S00-003', 'busy')
|
||||
with self.assertRaises(ValueError):
|
||||
self.tracker.mutate('reassign', 'S00-001', 'owner', target='busy', reason='Authorized transfer')
|
||||
self.tracker.mutate('reassign', 'S00-001', 'owner', target='new-owner', reason='Authorized transfer')
|
||||
self.assertEqual(self.tracker.state()['tasks']['S00-001']['owner'], 'new-owner')
|
||||
|
||||
def test_path_traversal_and_external_evidence_denied(self):
|
||||
path = self.root / 'outside.md'
|
||||
path.write_text('x' * 80)
|
||||
with self.assertRaises(ValueError):
|
||||
self.tracker.safe_file('state/evidence/../../outside.md', 'state/evidence')
|
||||
with self.assertRaises(ValueError):
|
||||
self.tracker.safe_file(str(path), 'state/evidence')
|
||||
|
||||
def test_symlink_evidence_denied(self):
|
||||
path = self.root / 'outside.md'
|
||||
path.write_text('x' * 80)
|
||||
link = self.root / 'state/evidence/link.md'
|
||||
try:
|
||||
link.symlink_to(path)
|
||||
except OSError:
|
||||
self.skipTest('Symlink unavailable on this platform')
|
||||
with self.assertRaises(ValueError):
|
||||
self.tracker.safe_file('state/evidence/link.md', 'state/evidence')
|
||||
|
||||
def test_lock_excludes_concurrent_mutation(self):
|
||||
with self.tracker.lock():
|
||||
with self.assertRaises(ValueError):
|
||||
self.claim()
|
||||
self.assertFalse(self.tracker.lock_path.exists())
|
||||
self.claim()
|
||||
|
||||
def test_two_processes_cannot_claim_same_task(self):
|
||||
code = "import importlib.util,sys; s=importlib.util.spec_from_file_location('t',sys.argv[1]);m=importlib.util.module_from_spec(s);s.loader.exec_module(m);m.Tracker(sys.argv[2]).mutate('claim','S00-001',sys.argv[3],role='front')"
|
||||
processes = [subprocess.Popen([sys.executable, '-c', code, str(ROOT / 'scripts/tasks.py'), str(self.root), a], stdout=subprocess.PIPE, stderr=subprocess.PIPE) for a in ['race-1', 'race-2']]
|
||||
for process in processes:
|
||||
process.communicate()
|
||||
exits = [p.returncode for p in processes]
|
||||
self.assertEqual(exits.count(0), 1)
|
||||
self.assertEqual(len(self.tracker.state()['events']), 1)
|
||||
|
||||
def test_dependency_cycle_detected(self):
|
||||
defs = self.tracker.definitions()
|
||||
defs['tasks'][0]['depends_on'] = ['S00-002']
|
||||
self.write('planning/tasks.json', defs)
|
||||
with self.assertRaisesRegex(ValueError, 'cycle'):
|
||||
self.tracker.validate()
|
||||
|
||||
def test_unknown_dependency_detected(self):
|
||||
defs = self.tracker.definitions()
|
||||
defs['tasks'][0]['depends_on'] = ['S99-999']
|
||||
self.write('planning/tasks.json', defs)
|
||||
with self.assertRaisesRegex(ValueError, 'Missing dependency'):
|
||||
self.tracker.validate()
|
||||
|
||||
def test_duplicate_task_and_missing_coverage_detected(self):
|
||||
defs = self.tracker.definitions()
|
||||
defs['tasks'].append(copy.deepcopy(defs['tasks'][0]))
|
||||
self.write('planning/tasks.json', defs)
|
||||
with self.assertRaisesRegex(ValueError, 'Duplicate'):
|
||||
self.tracker.validate()
|
||||
|
||||
def test_missing_requirement_allocation_detected(self):
|
||||
defs = self.tracker.definitions()
|
||||
for task in defs['tasks']:
|
||||
task['requirements'] = []
|
||||
self.write('planning/tasks.json', defs)
|
||||
with self.assertRaisesRegex(ValueError, 'Unallocated'):
|
||||
self.tracker.validate()
|
||||
|
||||
def test_original_spec_edit_detected(self):
|
||||
with (self.root / 'specs/tz_clear.md').open('a') as f:
|
||||
f.write('Changed\n')
|
||||
with self.assertRaisesRegex(ValueError, 'snapshot changed'):
|
||||
self.tracker.validate()
|
||||
|
||||
def test_sync_adds_without_erasing_status(self):
|
||||
self.claim()
|
||||
defs = self.tracker.definitions()
|
||||
new = copy.deepcopy(defs['tasks'][2])
|
||||
new['id'] = 'S00-004'
|
||||
defs['tasks'].append(new)
|
||||
self.write('planning/tasks.json', defs)
|
||||
self.assertEqual(self.tracker.sync(), ['S00-004'])
|
||||
self.assertEqual(self.tracker.state()['tasks']['S00-001']['owner'], 'owner')
|
||||
|
||||
def test_sync_refuses_history_removal(self):
|
||||
defs = self.tracker.definitions()
|
||||
defs['tasks'].pop()
|
||||
self.write('planning/tasks.json', defs)
|
||||
with self.assertRaisesRegex(ValueError, 'remove task history'):
|
||||
self.tracker.sync()
|
||||
|
||||
def test_packet_contains_requirements_and_dependency_state(self):
|
||||
packet = self.tracker.packet('S00-002', 'owner')
|
||||
self.assertIn('One real behavior.', packet)
|
||||
self.assertIn('S00-001', packet)
|
||||
self.assertIn('status=todo', packet)
|
||||
self.assertLess(len(packet), 48000)
|
||||
|
||||
def test_render_deterministic_matrix_csv_and_cards(self):
|
||||
self.tracker.render()
|
||||
first = (self.root / 'planning/TASK_MATRIX.md').read_bytes()
|
||||
self.tracker.render()
|
||||
self.assertEqual(first, (self.root / 'planning/TASK_MATRIX.md').read_bytes())
|
||||
self.assertIn('depends_on', (self.root / 'planning/task_matrix.csv').read_text())
|
||||
self.assertIn('Один результат', (self.root / 'planning/tasks/S00-001.md').read_text())
|
||||
|
||||
def test_invalid_done_detected(self):
|
||||
state = self.tracker.state()
|
||||
state['tasks']['S00-001'] = {'status': 'done', 'owner': 'owner'}
|
||||
self.write('state/tasks.json', state)
|
||||
with self.assertRaises(ValueError):
|
||||
self.tracker.validate()
|
||||
|
||||
|
||||
def test_unsafe_scope_detected(self):
|
||||
defs = self.tracker.definitions()
|
||||
defs['tasks'][0]['scope'] = ['../other-repo/**']
|
||||
self.write('planning/tasks.json', defs)
|
||||
with self.assertRaisesRegex(ValueError, 'Unsafe task scope'):
|
||||
self.tracker.validate()
|
||||
|
||||
def test_todo_cannot_be_approved(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self.tracker.mutate('approve', 'S00-001', 'reviewer', role='qa', evidence=self.evidence())
|
||||
|
||||
def test_unknown_operator_role_rejected(self):
|
||||
with self.assertRaisesRegex(ValueError, 'Unknown operator'):
|
||||
self.tracker.ready('frnot')
|
||||
|
||||
def test_duplicate_stage_rejected(self):
|
||||
defs = self.tracker.definitions()
|
||||
defs['stages'].append(copy.deepcopy(defs['stages'][0]))
|
||||
self.write('planning/tasks.json', defs)
|
||||
with self.assertRaisesRegex(ValueError, 'Duplicate stage'):
|
||||
self.tracker.validate()
|
||||
|
||||
def test_packet_refuses_oversized_context(self):
|
||||
defs = self.tracker.definitions()
|
||||
defs['tasks'][0]['outcome'] = 'x' * 49000
|
||||
self.write('planning/tasks.json', defs)
|
||||
with self.assertRaisesRegex(ValueError, '>48k'):
|
||||
self.tracker.packet('S00-001', 'owner')
|
||||
|
||||
def test_failed_submit_keeps_revision_and_removes_short_lock(self):
|
||||
self.claim()
|
||||
before = self.tracker.state()
|
||||
with self.assertRaises(ValueError):
|
||||
self.tracker.mutate('submit', 'S00-001', 'owner', evidence='missing.md', handoff='missing.md')
|
||||
self.assertEqual(before, self.tracker.state())
|
||||
self.assertFalse(self.tracker.lock_path.exists())
|
||||
|
||||
|
||||
class FoundationTests(unittest.TestCase):
|
||||
def test_real_foundation_validates(self):
|
||||
result = Tracker(ROOT).validate()
|
||||
self.assertGreaterEqual(result['tasks'], 200)
|
||||
self.assertGreaterEqual(result['stages'], 20)
|
||||
|
||||
def test_every_packet_fits_budget(self):
|
||||
tracker = Tracker(ROOT)
|
||||
lengths = [len(tracker.packet(t['id'], 'context-test')) for t in tracker.definitions()['tasks']]
|
||||
self.assertLessEqual(max(lengths), 48000)
|
||||
|
||||
def test_cli_list_filters_future_tasks_without_claiming(self):
|
||||
result = subprocess.run([sys.executable, str(ROOT / 'scripts/tasks.py'), 'list', '--role', 'crypto', '--stage', 'S12', '--status', 'todo', '--query', 'OMEMO', '--limit', '2'], capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0)
|
||||
rows = json.loads(result.stdout)
|
||||
self.assertGreater(len(rows), 0)
|
||||
self.assertLessEqual(len(rows), 2)
|
||||
self.assertTrue(all(x['stage'] == 'S12' and x['role'] == 'crypto' for x in rows))
|
||||
self.assertTrue(all(x['unmet_dependencies'] for x in rows))
|
||||
|
||||
def test_cli_next_and_invalid_command_exit(self):
|
||||
good = subprocess.run([sys.executable, str(ROOT / 'scripts/tasks.py'), 'next', '--role', 'orchestrator', '--json'], capture_output=True, text=True)
|
||||
self.assertEqual(good.returncode, 0)
|
||||
self.assertIsInstance(json.loads(good.stdout), list)
|
||||
bad = subprocess.run([sys.executable, str(ROOT / 'scripts/tasks.py'), 'show', 'MISSING'], capture_output=True, text=True)
|
||||
self.assertEqual(bad.returncode, 2)
|
||||
self.assertIn('Unknown task', bad.stderr)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user