Files
LumaWeb/tests/tooling/test_automation.py
T
wt c28412ae8f S00-012: Record blocked task and remediation handoff
Changes: Preserve incomplete work and blocker metadata for S00-012. This checkpoint does not mark implementation accepted.
Validation: Session ended with status blocked.
Blocker: Required automation suite fails in existing all-prompts budget test: S21-006 exceeds even a diagnostic 90000-character prompt limit. This blocks verification of the stdin remediation; production limit remains 80000.

Task: S00-012
Agent: manual-ci-cd-stdin-20261015b
Role: ci-cd
Action: escalate
Status: blocked

Task title: Fix runner prompt delivery after partial stdin timeout
2026-10-15 01:37:36 +07:00

307 lines
17 KiB
Python

"""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', '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')
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 = {'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)
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, {'git_commits': False, '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, {'git_commits': False, '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_delayed_reader_receives_complete_large_utf8_prompt_and_eof(self):
fake = self.root / 'fake_agent.py'
fake.write_text(fake.read_text().replace(
"prompt=sys.stdin.read();assert 'fresh session' in prompt",
"import time;time.sleep(.5)\n"
"payload=sys.stdin.buffer.read()\n"
"assert payload == pathlib.Path(os.environ['LUMA_PROMPT_FILE']).read_bytes()\n"
"prompt=payload.decode('utf-8');assert 'fresh session' in prompt\n"
"print('Complete UTF-8 prompt and EOF:',len(payload),flush=True)"))
(self.root / 'state/first-blocker').write_text('skip fixture blocker')
prompt = 'This is a fresh session.\n' + '\U0001f642\u0416\n' * 20000
runner = RUN_MODULE.Runner(self.root, dict(self.config, max_sessions=1,
timeout_seconds=3))
with patch.object(runner, 'prompt', return_value=prompt):
result = runner.run()
self.assertEqual(result['launched'], 1)
self.assertEqual(self.tracker.state()['tasks']['S00-001']['status'], 'review')
record_path = next((self.root / 'state/runs').glob('*/run.json'))
record = json.loads(record_path.read_text())
self.assertEqual(record['result'], {'status': 'review'})
self.assertEqual(record['scope_violations'], [])
self.assertIn('Complete UTF-8 prompt and EOF: ' + str(len(prompt.encode('utf-8'))),
record_path.with_name('output.log').read_text())
def test_scope_violation_quarantines_and_requires_input(self):
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')
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, {'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')
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