149 lines
5.4 KiB
Python
149 lines
5.4 KiB
Python
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_showing_a_gate():
|
|
script = f"""
|
|
const createTodayReadiness = require({json.dumps(str(READINESS))});
|
|
const calls = [];
|
|
const item = (number, kind='issue') => ({{kind, repository:'stackchain/dashboard', number, title:'Item ' + number}});
|
|
const readiness = createTodayReadiness({{
|
|
inspect: async candidate => ({{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)]).then(result =>
|
|
process.stdout.write(JSON.stringify({{result, calls}}))
|
|
);
|
|
"""
|
|
assert json.loads(run_node(script)) == {
|
|
"result": "opened",
|
|
"calls": [["open", "start", 1]],
|
|
}
|
|
|
|
|
|
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_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 '<script src="static/today-readiness.js"></script>' 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
|
|
|
|
|
|
def test_readiness_runtime_is_available_in_offline_shell():
|
|
assert "BASE + 'static/today-readiness.js'" in SERVICE_WORKER.read_text()
|
|
|
|
|
|
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
|