initial commit
Foundation checks / foundation (push) Successful in 1m12s

This commit is contained in:
wt committed 2026-10-14 17:47:38 +07:00
commit 54bfc0f34f
357 files changed
+41075

No files matched your search

+282
View File
@@ -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
+318
View File
@@ -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()