feat: lazy-load security center (Closes #605)
This commit is contained in:
1 parent
17823352f2
commit
fb58ba30b2
11 files changed
+308
-186
No files matched your search
@@ -9,6 +9,7 @@ from tests.dashboard_bundle import dashboard
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SESSION_JS = ROOT / "frontend" / "session.js"
|
||||
SECURITY_CENTER_JS = ROOT / "frontend" / "security-center.js"
|
||||
|
||||
|
||||
def run_session_scenario(scenario: str) -> dict:
|
||||
@@ -753,7 +754,7 @@ process.stdout.write(JSON.stringify(state));
|
||||
|
||||
|
||||
def test_security_activity_renders_authentication_alerts_before_journal_events():
|
||||
source = SESSION_JS.read_text()
|
||||
source = SECURITY_CENTER_JS.read_text()
|
||||
|
||||
alerts = source.index("page.authentication_alerts.forEach")
|
||||
events = source.index("page.events.forEach")
|
||||
@@ -765,14 +766,14 @@ def test_security_activity_renders_authentication_alerts_before_journal_events()
|
||||
|
||||
|
||||
def test_security_activity_explains_pending_outcome_confirmation():
|
||||
source = SESSION_JS.read_text()
|
||||
source = SECURITY_CENTER_JS.read_text()
|
||||
|
||||
assert "Outcome confirmation pending" in source
|
||||
assert "event.status === 'pending'" in source
|
||||
|
||||
|
||||
def test_security_activity_labels_passkey_enrollment_without_html_rendering():
|
||||
source = SESSION_JS.read_text()
|
||||
source = SECURITY_CENTER_JS.read_text()
|
||||
|
||||
assert "passkey_enrolled: 'Passkey enrolled'" in source
|
||||
assert "title.textContent = labels[event.kind]" in source
|
||||
@@ -780,7 +781,7 @@ def test_security_activity_labels_passkey_enrollment_without_html_rendering():
|
||||
|
||||
|
||||
def test_security_activity_labels_consequential_pull_review_decisions():
|
||||
source = SESSION_JS.read_text()
|
||||
source = SECURITY_CENTER_JS.read_text()
|
||||
|
||||
assert "pull_review_approved: 'Pull request approved'" in source
|
||||
assert "pull_review_changes_requested: 'Changes requested'" in source
|
||||
@@ -818,6 +819,6 @@ async def test_dashboard_loads_session_boundary_first_and_offers_sign_out():
|
||||
|
||||
|
||||
def test_security_activity_names_confirmed_gitea_time_logging():
|
||||
source = SESSION_JS.read_text()
|
||||
source = SECURITY_CENTER_JS.read_text()
|
||||
|
||||
assert "gitea_time_logged: 'Gitea time logged'" in source
|
||||
@@ -89,3 +89,22 @@ console.log(JSON.stringify({first,second,failedStatus,opened,appends:state.appen
|
||||
"opened": 1,
|
||||
"appends": 2,
|
||||
}
|
||||
|
||||
|
||||
def test_security_center_failure_is_accessible_and_retryable_from_active_devices():
|
||||
result = run_loader("""
|
||||
const trigger={disabled:false}; const status={textContent:''}; let opened=0;
|
||||
const loader=createFeatureLoader({document, urls:{'security-center':'feature-security.js'}, timeoutMs:100});
|
||||
const failed=loader.run('security-center',{trigger,status,retryLabel:'Tap Active devices to retry.'},()=>{opened++;});
|
||||
state.node.onerror(); const first=await failed; const failedStatus=status.textContent;
|
||||
const retry=loader.run('security-center',{trigger,status,retryLabel:'Tap Active devices to retry.'},()=>{opened++;});
|
||||
state.node.onload(); const second=await retry;
|
||||
console.log(JSON.stringify({first,second,failedStatus,opened,appends:state.appends}));
|
||||
""")
|
||||
assert result == {
|
||||
"first": False,
|
||||
"second": True,
|
||||
"failedStatus": "Security center could not load. Tap Active devices to retry.",
|
||||
"opened": 1,
|
||||
"appends": 2,
|
||||
}
|
||||
@@ -49,7 +49,7 @@ def test_product_workflows_are_stable_lazy_feature_chunks(tmp_path):
|
||||
|
||||
assert set(first.feature_bundles) == {
|
||||
"comment-actions", "issue-capture", "pull-workflow", "push-notifications", "device-setup",
|
||||
"today-timer",
|
||||
"today-timer", "security-center",
|
||||
}
|
||||
assert (
|
||||
f'<script src="{first.feature_bundles["today-timer"].runtime_name}"></script>\n'
|
||||
@@ -63,12 +63,19 @@ def test_product_workflows_are_stable_lazy_feature_chunks(tmp_path):
|
||||
assert b"function createReviewController" not in first.runtime_bytes
|
||||
assert b"function createPullSheet" in pull_workflow.runtime_bytes
|
||||
assert b"function createReviewController" in pull_workflow.runtime_bytes
|
||||
security_center = first.feature_bundles["security-center"]
|
||||
assert b"function attachSecurityCenter" not in first.runtime_bytes
|
||||
assert b"function attachSecurityCenter" in security_center.runtime_bytes
|
||||
assert b"gitea_time_logged" not in first.runtime_bytes
|
||||
assert b"gitea_time_logged" in security_center.runtime_bytes
|
||||
# The recap adds only startup wiring; its UI remains in the lazy Today bundle.
|
||||
assert len(first.runtime_gzip_bytes) <= 96 * 1024
|
||||
assert len(first.runtime_gzip_bytes) <= 95 * 1024
|
||||
assert f'name="stackchain-feature-issue-capture" content="{capture.runtime_name}"' in first.dashboard_html
|
||||
assert f'name="stackchain-feature-pull-workflow" content="{pull_workflow.runtime_name}"' in first.dashboard_html
|
||||
assert f"BASE + '{capture.runtime_name}'" in first.service_worker_source
|
||||
assert f"BASE + '{pull_workflow.runtime_name}'" in first.service_worker_source
|
||||
assert f'name="stackchain-feature-security-center" content="{security_center.runtime_name}"' in first.dashboard_html
|
||||
assert f"BASE + '{security_center.runtime_name}'" in first.service_worker_source
|
||||
|
||||
changed_frontend = tmp_path / "frontend"
|
||||
shutil.copytree(FRONTEND, changed_frontend)
|
||||
@@ -85,6 +92,12 @@ def test_product_workflows_are_stable_lazy_feature_chunks(tmp_path):
|
||||
assert pull_changed.runtime_name == first.runtime_name
|
||||
assert pull_changed.feature_bundles["pull-workflow"].runtime_name != pull_workflow.runtime_name
|
||||
|
||||
security_source = changed_frontend / "security-center.js"
|
||||
security_source.write_text(security_source.read_text() + "\n// security-center-only change\n")
|
||||
security_changed = build_frontend(changed_frontend)
|
||||
assert security_changed.runtime_name == first.runtime_name
|
||||
assert security_changed.feature_bundles["security-center"].runtime_name != security_center.runtime_name
|
||||
|
||||
|
||||
def test_shipped_browser_bundles_are_valid_javascript(tmp_path):
|
||||
build = build_frontend(FRONTEND)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SECURITY_CENTER = Path(__file__).parents[1] / "frontend" / "security-center.js"
|
||||
|
||||
|
||||
def test_open_security_center_loads_all_sections_concurrently_and_is_awaitable():
|
||||
harness = f"""
|
||||
const attachSecurityCenter=require({json.dumps(str(SECURITY_CENTER))});
|
||||
const calls=[];
|
||||
const element=()=>({{
|
||||
hidden:true, disabled:false, textContent:'', children:[],
|
||||
addEventListener(){{}}, focus(){{this.focused=true;}},
|
||||
replaceChildren(){{this.children=[];}}, append(value){{this.children.push(value);}},
|
||||
}});
|
||||
const ids={{
|
||||
'active-devices':element(), 'active-devices-sheet':element(),
|
||||
'active-devices-list':element(), 'active-devices-status':element(),
|
||||
'enrolled-passkeys-list':element(), 'enrolled-passkeys-status':element(),
|
||||
'enroll-passkey':element(), 'security-activity-list':element(),
|
||||
'security-activity-status':element(), 'load-more-security-activity':element(),
|
||||
'close-active-devices':element(),
|
||||
}};
|
||||
const root={{document:{{getElementById:id=>ids[id]||null,createElement:()=>element()}}}};
|
||||
const boundary={{
|
||||
listActiveDevices:async()=>{{calls.push('devices');return[];}},
|
||||
listPasskeys:async()=>{{calls.push('passkeys');return[];}},
|
||||
listSecurityEvents:async()=>{{calls.push('activity');return{{events:[],authentication_alerts:[],next_cursor:null}};}},
|
||||
}};
|
||||
(async()=>{{
|
||||
const controller=attachSecurityCenter({{root,boundary}});
|
||||
const loading=controller.open();
|
||||
const awaitable=Boolean(loading&&typeof loading.then==='function');
|
||||
if (loading) await loading;
|
||||
console.log(JSON.stringify({{calls,awaitable,hidden:ids['active-devices-sheet'].hidden,focused:ids['close-active-devices'].focused||false}}));
|
||||
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
||||
"""
|
||||
completed = subprocess.run(
|
||||
["node", "-e", harness], check=True, capture_output=True, text=True
|
||||
)
|
||||
|
||||
assert json.loads(completed.stdout) == {
|
||||
"calls": ["devices", "passkeys", "activity"],
|
||||
"awaitable": True,
|
||||
"hidden": False,
|
||||
"focused": True,
|
||||
}
|
||||
@@ -661,6 +661,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||
"/dashboard/static/icons/stackchain-512.png",
|
||||
"/dashboard/static/session.js",
|
||||
"/dashboard/static/feature-loader.js",
|
||||
"/dashboard/static/security-center.js",
|
||||
"/dashboard/static/markdown.js",
|
||||
"/dashboard/static/commands.js",
|
||||
"/dashboard/static/search-preview.js",
|
||||
|
||||
Reference in new issue
Block a user