""" Lab Auto-Grader: validates student lab outputs against expected results. Usage: python grade.py # Grade all labs python grade.py L1-first-agent # Grade specific lab """ import os, sys, subprocess from pathlib import Path LABS_DIR = Path(__file__).resolve().parent sys.path.insert(0, str(LABS_DIR / '_shared')) LAB_TESTS = { 'L1-first-agent': { 'file': 'solution.py', 'args': [str(LABS_DIR / 'L1-first-agent' / 'test.txt'), 'what is in this file'], 'assert_output_contains': ['Final answer'], }, 'L2-multi-tool': { 'file': 'solution.py', 'args': ['test query'], 'assert_output_contains': [], 'allow_exit_code': [0, 1], }, 'L2-context': { 'file': 'solution.py', 'args': [], 'assert_output_contains': ['messages'], }, 'L3-whitelist-hook': { 'file': 'solution.py', 'args': [], 'assert_output_contains': ['ALLOW', 'BLOCK'], }, 'L3-verifier': { 'file': 'solution.py', 'args': [], 'assert_output_contains': ['status'], }, 'L5-cicd': { 'file': 'solution.py', 'args': [], 'assert_output_contains': ['PIPELINE'], 'allow_exit_code': [0, 1], # Pipeline exits 1 when gate fails (expected) }, 'L5-observability': { 'file': 'solution.py', 'args': [], 'assert_output_contains': ['Session'], }, 'L6-cost-optimization': { 'file': 'solution.py', 'args': [], 'assert_output_contains': ['OPTIMIZATION'], 'allow_exit_code': [0, 1], }, 'L6-eval-harness': { 'file': 'solution.py', 'args': [], 'assert_output_contains': ['HARNESS'], 'allow_exit_code': [0, 1], }, 'L7-autoresearch': { 'file': 'solution.py', 'args': [], 'assert_output_contains': ['Baseline'], 'allow_exit_code': [0, 1], }, 'L7-meta-agent': { 'file': 'solution.py', 'args': [], 'assert_output_contains': ['Building'], 'allow_exit_code': [0, 1], }, } def grade_lab(lab_name: str) -> dict: config = LAB_TESTS.get(lab_name) if not config: return {'lab': lab_name, 'status': 'SKIP', 'reason': 'No test'} lab_dir = LABS_DIR / lab_name script = lab_dir / config['file'] if not script.exists(): return {'lab': lab_name, 'status': 'FAIL', 'reason': f'{config["file"]} not found'} cmd = [sys.executable, str(script)] + config.get('args', []) try: result = subprocess.run(cmd, capture_output=True, text=True, timeout=config.get('timeout', 15), cwd=str(lab_dir)) except subprocess.TimeoutExpired: return {'lab': lab_name, 'status': 'FAIL', 'reason': 'Timed out'} output = result.stdout + result.stderr allowed = config.get('allow_exit_code', [0]) if result.returncode not in allowed: err = result.stderr[:200] if result.stderr else f'Exit code {result.returncode}' return {'lab': lab_name, 'status': 'FAIL', 'reason': err} errors = [] for pattern in config.get('assert_output_contains', []): if pattern.lower() not in output.lower(): errors.append(f'Missing: "{pattern}"') if errors: return {'lab': lab_name, 'status': 'FAIL', 'reason': '; '.join(errors)} return {'lab': lab_name, 'status': 'PASS'} def grade_all(): results = [] for lab_name in sorted(LAB_TESTS.keys()): result = grade_lab(lab_name) results.append(result) icon = '+' if result['status'] == 'PASS' else '-' line = f' [{icon}] {lab_name:25s} {result["status"]}' if 'reason' in result: line += f' - {result["reason"][:100]}' print(line) passed = sum(1 for r in results if r['status'] == 'PASS') total = len(results) print(f'\n {passed}/{total} passing') return results if __name__ == '__main__': if len(sys.argv) > 1: result = grade_lab(sys.argv[1]) print(f'[{result["status"]}] {result["lab"]}' + (f' - {result.get("reason","")}' if 'reason' in result else '')) else: grade_all()