import json import subprocess from pathlib import Path import pytest from tests.dashboard_bundle import dashboard ROOT = Path(__file__).parents[1] READINESS = ROOT / "frontend" / "today-readiness.js" SERVICE_WORKER = ROOT / "frontend" / "service-worker.js" def run_node(script): return subprocess.run( ["node", "-e", script], check=True, capture_output=True, text=True ).stdout def test_readiness_opens_ready_target_without_inspecting_the_rest_of_the_queue(): script = f""" const createTodayReadiness = require({json.dumps(str(READINESS))}); const calls = []; const inspections = []; const item = (number, kind='issue') => ({{kind, repository:'stackchain/dashboard', number, title:'Item ' + number}}); const readiness = createTodayReadiness({{ inspect: async candidate => {{ inspections.push(candidate.number); return {{available:true, dependencies:[]}}; }}, onOpen: (action, candidate) => calls.push(['open', action, candidate.number]), onGate: state => calls.push(['gate', state.status]), }}); readiness.run('start', [item(1), item(2), item(3)]).then(result => process.stdout.write(JSON.stringify({{result, calls, inspections}})) ); """ assert json.loads(run_node(script)) == { "result": "opened", "calls": [["open", "start", 1]], "inspections": [1], } def test_readiness_checks_fallbacks_concurrently_but_preserves_queue_order(): script = f""" const createTodayReadiness = require({json.dumps(str(READINESS))}); const items = [1, 2, 3].map(number => ({{kind:'issue', repository:'r', number}})); const starts = []; const resolvers = {{}}; const readiness = createTodayReadiness({{ inspect: candidate => {{ starts.push(candidate.number); if (candidate.number === 1) return Promise.resolve({{available:true, dependencies:[{{state:'open'}}]}}); return new Promise(resolve => {{ resolvers[candidate.number] = resolve; }}); }}, onOpen: () => undefined, onGate: state => {{ globalThis.gated = state.nextReady.number; }}, }}); (async () => {{ const running = readiness.run('start', items); await new Promise(resolve => setImmediate(resolve)); const startedTogether = starts.slice(); if (resolvers[3]) resolvers[3]({{available:true, dependencies:[]}}); resolvers[2]({{available:true, dependencies:[]}}); await new Promise(resolve => setImmediate(resolve)); if (!startedTogether.includes(3)) resolvers[3]({{available:true, dependencies:[]}}); const result = await running; process.stdout.write(JSON.stringify({{result, startedTogether, gated:globalThis.gated}})); }})(); """ assert json.loads(run_node(script)) == { "result": "gated", "startedTogether": [1, 2, 3], "gated": 2, } def test_readiness_gates_blocked_target_and_offers_earliest_ready_without_reordering(): script = f""" const createTodayReadiness = require({json.dumps(str(READINESS))}); const calls = []; const items = [1, 2, 3].map(number => ({{kind:'issue', repository:'r', number, title:'Issue ' + number}})); const readiness = createTodayReadiness({{ inspect: async candidate => candidate.number === 1 ? {{available:true, dependencies:[{{repository:'r', number:99, title:'Prerequisite', state:'open', url:'https://forge.example/r/issues/99'}}]}} : {{available:true, dependencies:[]}}, onOpen: (action, candidate) => calls.push(['open', action, candidate.number]), onGate: state => calls.push(['gate', state.status, state.target.number, state.nextReady.number, state.items.map(item => item.number)]), }}); readiness.run('resume', items, items[0]).then(async result => {{ const opened = readiness.startNextReady(); process.stdout.write(JSON.stringify({{result, opened, calls, order:items.map(item => item.number)}})); }}); """ assert json.loads(run_node(script)) == { "result": "gated", "opened": True, "calls": [ ["gate", "blocked", 1, 2, [1, 2, 3]], ["open", "resume", 2], ], "order": [1, 2, 3], } def test_readiness_previews_only_a_current_blocker_without_closing_or_reordering_gate(): script = f""" const createTodayReadiness = require({json.dumps(str(READINESS))}); const previews = []; const items = [1, 2].map(number => ({{kind:'issue', repository:'r', number}})); const blocker = {{repository:'r', number:99, title:'Prerequisite', state:'open'}}; const readiness = createTodayReadiness({{ inspect: async candidate => candidate.number === 1 ? {{available:true, dependencies:[blocker]}} : {{available:true, dependencies:[]}}, onPreview: candidate => previews.push(candidate.number), }}); (async () => {{ await readiness.run('start', items, items[0]); const accepted = readiness.previewBlocker(blocker); const rejected = readiness.previewBlocker({{repository:'r', number:100}}); const snapshot = readiness.snapshot(); process.stdout.write(JSON.stringify({{ accepted, rejected, previews, open:snapshot.status === 'blocked', target:snapshot.target.number, order:snapshot.items.map(item => item.number), }})); }})(); """ assert json.loads(run_node(script)) == { "accepted": True, "rejected": False, "previews": [99], "open": True, "target": 1, "order": [1, 2], } def test_readiness_never_calls_unknown_ready_and_supports_retry_or_explicit_override(): script = f""" const createTodayReadiness = require({json.dumps(str(READINESS))}); const calls = []; const target = {{kind:'issue', repository:'r', number:1}}; let available = false; const readiness = createTodayReadiness({{ inspect: async () => available ? {{available:true, dependencies:[]}} : {{available:false, dependencies:[]}}, onOpen: (action, candidate) => calls.push(['open', action, candidate.number]), onGate: state => calls.push(['gate', state.status]), }}); readiness.run('next', [target], target).then(async result => {{ available = true; const retried = await readiness.retry(); available = false; await readiness.run('continue', [target], target); const overridden = readiness.workAnyway(); process.stdout.write(JSON.stringify({{result, retried, overridden, calls}})); }}); """ assert json.loads(run_node(script)) == { "result": "gated", "retried": "opened", "overridden": True, "calls": [ ["gate", "unknown"], ["open", "next", 1], ["gate", "unknown"], ["open", "continue", 1], ], } def test_readiness_treats_non_issue_work_as_runnable_without_dependency_io(): script = f""" const createTodayReadiness = require({json.dumps(str(READINESS))}); let inspections = 0; const pull = {{kind:'pull', repository:'r', number:7}}; const readiness = createTodayReadiness({{ inspect: async () => {{ inspections += 1; throw new Error('not expected'); }}, onOpen: () => undefined, onGate: () => undefined, }}); readiness.run('start', [pull]).then(result => process.stdout.write(JSON.stringify({{result, inspections}})) ); """ assert json.loads(run_node(script)) == {"result": "opened", "inspections": 0} @pytest.mark.anyio async def test_dashboard_exposes_mobile_today_readiness_gate_and_runtime(): html = await dashboard() assert '' in html assert 'id="today-readiness-sheet"' in html assert 'id="today-readiness-next"' in html assert 'id="today-readiness-anyway"' in html assert 'id="today-readiness-retry"' in html assert 'id="today-readiness-plan"' in html assert "createTodayReadiness({" in html assert "runTodayTransition('start'" in html assert "runTodayTransition('resume'" in html assert "runTodayTransition('continue'" in html assert "runTodayTransition('next'" in html assert "runTodayTransition('complete'" in html @pytest.mark.anyio async def test_today_blocker_opens_existing_preview_and_preserves_readiness_gate(): html = await dashboard() assert 'data-today-blocker-index="' in html assert "todayReadiness.previewBlocker(blocker)" in html assert "searchPreview.open({ ...blocker, kind:'issue' })" in html assert "taskOverlayHistory.open('search-preview')" in html assert "suspendTodayReadiness()" in html assert "todayReadiness.cancel()" in html assert "todayReadinessBlockerFocus" in html def test_readiness_runtime_is_available_in_offline_shell(): service_worker = SERVICE_WORKER.read_text() assert "const CACHE = 'stackchain-dashboard-shell-v127';" in service_worker assert "BASE + 'static/today-readiness.js'" in service_worker def test_mobile_readiness_gate_has_touch_safe_wrapping_actions(): css = (ROOT / "frontend" / "dashboard.css").read_text() assert ".today-readiness-actions button" in css assert "min-height:44px" in css assert ".today-readiness-item" in css assert "overflow-wrap:anywhere" in css assert "env(safe-area-inset-bottom)" in css assert ".today-readiness-blocker" in css assert "min-height:44px" in css