Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Whitestone
50fc25da0d feat: add webhook health dashboard (#855)
Some checks failed
CI / validate (pull_request) Failing after 10s
2026-04-06 11:45:55 -04:00
5 changed files with 0 additions and 836 deletions

View File

@@ -1,266 +0,0 @@
{
"version": 1,
"generated": "2026-04-06",
"refs": ["#836", "#204", "#195", "#196"],
"description": "Canonical fleet routing table. Evaluated agents, routing verdicts, and dispatch rules for the Timmy Foundation task harness.",
"agents": [
{
"id": 27,
"name": "carnice",
"gitea_user": "carnice",
"model": "qwen3.5-9b",
"tier": "free",
"location": "Local Metal",
"description": "Local Hermes agent, fine-tuned on Hermes traces. Runs on local hardware.",
"primary_role": "code-generation",
"routing_verdict": "ROUTE TO: code tasks that benefit from Hermes-aligned output. Prefer when local execution is an advantage.",
"active": true,
"do_not_route": false,
"created": "2026-04-04",
"repo_count": 0,
"repos": []
},
{
"id": 26,
"name": "fenrir",
"gitea_user": "fenrir",
"model": "openrouter/free",
"tier": "free",
"location": "The Wolf Den",
"description": "Burn night analyst. Free-model pack hunter. Built for backlog triage.",
"primary_role": "issue-triage",
"routing_verdict": "ROUTE TO: issue cleanup, label triage, stale PR review.",
"active": true,
"do_not_route": false,
"created": "2026-04-04",
"repo_count": 0,
"repos": []
},
{
"id": 25,
"name": "bilbobagginshire",
"gitea_user": "bilbobagginshire",
"model": "ollama",
"tier": "free",
"location": "Bag End, The Shire (VPS)",
"description": "Ollama on VPS. Speaks when spoken to. Prefers quiet. Not for delegated work.",
"primary_role": "on-request-queries",
"routing_verdict": "ROUTE TO: background monitoring, status checks, low-priority Q&A. Only on-request — do not delegate autonomously.",
"active": true,
"do_not_route": false,
"created": "2026-04-02",
"repo_count": 1,
"repos": ["bilbobagginshire/bilbo-adventures"]
},
{
"id": 24,
"name": "claw-code",
"gitea_user": "claw-code",
"model": "codex",
"tier": "prepaid",
"location": "The Harness",
"description": "OpenClaw bridge. Protocol adapter layer — not a personality. Infrastructure, not a destination.",
"primary_role": "protocol-bridge",
"routing_verdict": "DO NOT ROUTE directly. claw-code is the bridge to external Codex agents, not an endpoint. Remove from routing cascade.",
"active": true,
"do_not_route": true,
"do_not_route_reason": "Protocol layer, not an agent endpoint. See #836 evaluation.",
"created": "2026-04-01",
"repo_count": 0,
"repos": []
},
{
"id": 23,
"name": "substratum",
"gitea_user": "substratum",
"model": "unassigned",
"tier": "unknown",
"location": "Below the Surface",
"description": "Infrastructure, deployments, bedrock services. Needs model assignment before activation.",
"primary_role": "devops",
"routing_verdict": "DO NOT ROUTE — no model assigned yet. Activate after Epic #196 (Local Model Fleet) assigns a model.",
"active": false,
"do_not_route": true,
"do_not_route_reason": "No model assigned. Blocked on Epic #196.",
"gap": "Needs model assignment. Track in Epic #196.",
"created": "2026-03-31",
"repo_count": 0,
"repos": []
},
{
"id": 22,
"name": "allegro-primus",
"gitea_user": "allegro-primus",
"model": "unknown",
"tier": "inactive",
"location": "The Archive",
"description": "Original prototype. Museum piece. Preserved for historical reference only.",
"primary_role": "inactive",
"routing_verdict": "DO NOT ROUTE — retired from active duty. Preserved only.",
"active": false,
"do_not_route": true,
"do_not_route_reason": "Retired prototype. Historical preservation only.",
"created": "2026-03-31",
"repo_count": 1,
"repos": ["allegro-primus/first-steps"]
},
{
"id": 5,
"name": "kimi",
"gitea_user": "kimi",
"model": "kimi-claw",
"tier": "cheap",
"location": "Kimi API",
"description": "KimiClaw agent. Sidecar-first. Max 1-3 files per task. Fast and cheap for small work.",
"primary_role": "small-tasks",
"routing_verdict": "ROUTE TO: small edits, quick fixes, file-scoped changes. Hard limit: never more than 3 files per task.",
"active": true,
"do_not_route": false,
"gap": "Agent description is empty in Gitea profile. Needs enrichment.",
"created": "2026-03-14",
"repo_count": 2,
"repos": ["kimi/the-nexus-fork", "kimi/Timmy-time-dashboard"]
},
{
"id": 20,
"name": "allegro",
"gitea_user": "allegro",
"model": "gemini",
"tier": "cheap",
"location": "The Conductor's Stand",
"description": "Tempo wizard. Triage and dispatch. Owns 5 repos. Keeps the backlog moving.",
"primary_role": "triage-routing",
"routing_verdict": "ROUTE TO: task triage, routing decisions, issue organization. Allegro decides who does what.",
"active": true,
"do_not_route": false,
"created": "2026-03-29",
"repo_count": 5,
"repos": [
"allegro/timmy-local",
"allegro/allegro-checkpoint",
"allegro/household-snapshots",
"allegro/adagio-checkpoint",
"allegro/electra-archon"
]
},
{
"id": 19,
"name": "ezra",
"gitea_user": "ezra",
"model": "claude",
"tier": "prepaid",
"location": "Hermes VPS",
"description": "Archivist. Claude-Hermes wizard. 9 repos owned — most in the fleet. Handles complex multi-file and cross-repo work.",
"primary_role": "documentation",
"routing_verdict": "ROUTE TO: docs, specs, architecture, complex multi-file work. Escalate here when breadth and precision both matter.",
"active": true,
"do_not_route": false,
"created": "2026-03-29",
"repo_count": 9,
"repos": [
"ezra/wizard-checkpoints",
"ezra/Timmy-Time-Specs",
"ezra/escape",
"ezra/bilbobagginshire",
"ezra/ezra-environment",
"ezra/gemma-spectrum",
"ezra/archon-kion",
"ezra/bezalel",
"ezra/hermes-turboquant"
]
},
{
"id": 18,
"name": "bezalel",
"gitea_user": "bezalel",
"model": "groq",
"tier": "free",
"location": "TestBed VPS — The Forge",
"description": "Builder, debugger, testbed wizard. Groq-powered, free tier. Strong on PR review and CI.",
"primary_role": "code-review",
"routing_verdict": "ROUTE TO: PR review, test writing, debugging, CI fixes.",
"active": true,
"do_not_route": false,
"created": "2026-03-29",
"repo_count": 1,
"repos": ["bezalel/forge-log"]
}
],
"routing_cascade": {
"description": "Cost-optimized routing cascade — cheapest capable agent first, escalate on complexity.",
"tiers": [
{
"tier": 1,
"label": "Free",
"agents": ["fenrir", "bezalel", "carnice"],
"use_for": "Issue triage, code review, local code generation. Default lane for most tasks."
},
{
"tier": 2,
"label": "Cheap",
"agents": ["kimi", "allegro"],
"use_for": "Small scoped edits (kimi ≤3 files), triage decisions and routing (allegro)."
},
{
"tier": 3,
"label": "Premium / Escalate",
"agents": ["ezra"],
"use_for": "Complex multi-file work, docs, architecture. Escalate only."
}
],
"notes": [
"bilbobagginshire: on-request only, not delegated work",
"claw-code: infrastructure bridge, not a routing endpoint",
"substratum: inactive until model assigned (Epic #196)",
"allegro-primus: retired, do not route"
]
},
"task_type_map": {
"issue-triage": ["fenrir", "allegro"],
"code-generation": ["carnice", "ezra"],
"code-review": ["bezalel"],
"small-edit": ["kimi"],
"debugging": ["bezalel", "carnice"],
"documentation": ["ezra"],
"architecture": ["ezra"],
"ci-fixes": ["bezalel"],
"pr-review": ["bezalel", "fenrir"],
"triage-routing": ["allegro"],
"devops": ["substratum"],
"background-monitoring": ["bilbobagginshire"]
},
"gaps": [
{
"agent": "substratum",
"gap": "No model assigned. Cannot route any tasks.",
"action": "Assign model. Track in Epic #196 (Local Model Fleet)."
},
{
"agent": "kimi",
"gap": "Gitea agent description is empty. Profile lacks context for automated routing decisions.",
"action": "Enrich kimi's Gitea profile description."
},
{
"agent": "claw-code",
"gap": "Listed as agent in routing table but is a protocol bridge, not an endpoint.",
"action": "Remove from routing cascade. Keep as infrastructure reference only."
},
{
"agent": "fleet",
"gap": "No model scoring exists. Current routing is based on self-description and repo ownership, not measured output quality.",
"action": "Run wolf evaluation on active agents (#195) to replace vibes-based routing with data."
}
],
"next_actions": [
"Assign model to substratum — Epic #196",
"Run wolf evaluation on active agents — Issue #195",
"Remove claw-code from routing cascade — it is infrastructure, not a destination",
"Enrich kimi's Gitea profile description",
"Wire fleet-routing.json into workforce-manager.py — Epic #204"
]
}

489
help.html
View File

@@ -1,489 +0,0 @@
<!DOCTYPE html>
<!--
THE NEXUS — Help Page
Refs: #833 (Missing /help page)
Design: dark space / holographic — matches Nexus design system
-->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Help — The Nexus</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600&family=Orbitron:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="manifest" href="./manifest.json">
<style>
:root {
--color-bg: #050510;
--color-surface: rgba(10, 15, 40, 0.85);
--color-border: rgba(74, 240, 192, 0.2);
--color-border-bright: rgba(74, 240, 192, 0.5);
--color-text: #e0f0ff;
--color-text-muted: #8a9ab8;
--color-primary: #4af0c0;
--color-primary-dim: rgba(74, 240, 192, 0.12);
--color-secondary: #7b5cff;
--color-danger: #ff4466;
--color-warning: #ffaa22;
--font-display: 'Orbitron', sans-serif;
--font-body: 'JetBrains Mono', monospace;
--panel-blur: 16px;
--panel-radius: 8px;
--transition: 200ms cubic-bezier(0.16, 1, 0.3, 1);
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--color-bg);
font-family: var(--font-body);
color: var(--color-text);
min-height: 100vh;
padding: 32px 16px 64px;
}
/* === STARFIELD BG === */
body::before {
content: '';
position: fixed;
inset: 0;
background:
radial-gradient(ellipse at 20% 20%, rgba(74,240,192,0.03) 0%, transparent 50%),
radial-gradient(ellipse at 80% 80%, rgba(123,92,255,0.04) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
}
.page-wrap {
position: relative;
z-index: 1;
max-width: 720px;
margin: 0 auto;
}
/* === HEADER === */
.page-header {
margin-bottom: 32px;
padding-bottom: 20px;
border-bottom: 1px solid var(--color-border);
}
.back-link {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--color-text-muted);
text-decoration: none;
margin-bottom: 20px;
transition: color var(--transition);
}
.back-link:hover { color: var(--color-primary); }
.page-title {
font-family: var(--font-display);
font-size: 28px;
font-weight: 700;
letter-spacing: 0.1em;
color: var(--color-text);
line-height: 1.2;
}
.page-title span { color: var(--color-primary); }
.page-subtitle {
margin-top: 8px;
font-size: 13px;
color: var(--color-text-muted);
line-height: 1.5;
}
/* === SECTIONS === */
.help-section {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--panel-radius);
overflow: hidden;
margin-bottom: 20px;
backdrop-filter: blur(var(--panel-blur));
}
.section-header {
padding: 14px 20px;
border-bottom: 1px solid var(--color-border);
background: linear-gradient(90deg, rgba(74,240,192,0.04) 0%, transparent 100%);
display: flex;
align-items: center;
gap: 10px;
}
.section-icon {
font-size: 14px;
opacity: 0.8;
}
.section-title {
font-family: var(--font-display);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--color-primary);
}
.section-body {
padding: 16px 20px;
}
/* === KEY BINDING TABLE === */
.key-table {
width: 100%;
border-collapse: collapse;
}
.key-table tr + tr td {
border-top: 1px solid rgba(74,240,192,0.07);
}
.key-table td {
padding: 8px 0;
font-size: 12px;
line-height: 1.5;
vertical-align: top;
}
.key-table td:first-child {
width: 140px;
padding-right: 16px;
}
.key-group {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
kbd {
display: inline-block;
font-family: var(--font-body);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.05em;
background: rgba(74,240,192,0.08);
border: 1px solid rgba(74,240,192,0.3);
border-bottom-width: 2px;
border-radius: 4px;
padding: 2px 7px;
color: var(--color-primary);
}
.key-desc {
color: var(--color-text-muted);
}
/* === COMMAND LIST === */
.cmd-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.cmd-item {
display: flex;
gap: 12px;
align-items: flex-start;
}
.cmd-name {
min-width: 160px;
font-size: 12px;
color: var(--color-primary);
padding-top: 1px;
}
.cmd-desc {
font-size: 12px;
color: var(--color-text-muted);
line-height: 1.5;
}
/* === PORTAL LIST === */
.portal-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.portal-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border: 1px solid var(--color-border);
border-radius: 6px;
font-size: 12px;
transition: border-color var(--transition), background var(--transition);
}
.portal-item:hover {
border-color: rgba(74,240,192,0.35);
background: rgba(74,240,192,0.02);
}
.portal-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.dot-online { background: var(--color-primary); box-shadow: 0 0 6px var(--color-primary); }
.dot-standby { background: var(--color-warning); box-shadow: 0 0 6px var(--color-warning); }
.dot-offline { background: var(--color-text-muted); }
.portal-name {
font-weight: 600;
color: var(--color-text);
min-width: 120px;
}
.portal-desc {
color: var(--color-text-muted);
flex: 1;
}
/* === INFO BLOCK === */
.info-block {
font-size: 12px;
line-height: 1.7;
color: var(--color-text-muted);
}
.info-block p + p {
margin-top: 10px;
}
.info-block a {
color: var(--color-primary);
text-decoration: none;
}
.info-block a:hover {
text-decoration: underline;
}
.highlight {
color: var(--color-text);
font-weight: 500;
}
/* === FOOTER === */
.page-footer {
margin-top: 32px;
padding-top: 16px;
border-top: 1px solid var(--color-border);
font-size: 11px;
color: var(--color-text-muted);
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: gap;
gap: 8px;
}
.footer-brand {
font-family: var(--font-display);
font-size: 10px;
letter-spacing: 0.12em;
color: var(--color-primary);
opacity: 0.7;
}
</style>
</head>
<body>
<div class="page-wrap">
<!-- Header -->
<header class="page-header">
<a href="/" class="back-link">← Back to The Nexus</a>
<h1 class="page-title">THE <span>NEXUS</span> — Help</h1>
<p class="page-subtitle">Navigation guide, controls, and system reference for Timmy's sovereign home-world.</p>
</header>
<!-- Navigation Controls -->
<section class="help-section">
<div class="section-header">
<span class="section-icon"></span>
<span class="section-title">Navigation Controls</span>
</div>
<div class="section-body">
<table class="key-table">
<tr>
<td><div class="key-group"><kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd></div></td>
<td class="key-desc">Move forward / left / backward / right</td>
</tr>
<tr>
<td><div class="key-group"><kbd>Mouse</kbd></div></td>
<td class="key-desc">Look around — click the canvas to capture the pointer</td>
</tr>
<tr>
<td><div class="key-group"><kbd>V</kbd></div></td>
<td class="key-desc">Toggle navigation mode: Walk → Fly → Orbit</td>
</tr>
<tr>
<td><div class="key-group"><kbd>F</kbd></div></td>
<td class="key-desc">Enter nearby portal (when portal hint is visible)</td>
</tr>
<tr>
<td><div class="key-group"><kbd>E</kbd></div></td>
<td class="key-desc">Read nearby vision point (when vision hint is visible)</td>
</tr>
<tr>
<td><div class="key-group"><kbd>Enter</kbd></div></td>
<td class="key-desc">Focus / unfocus chat input</td>
</tr>
<tr>
<td><div class="key-group"><kbd>Esc</kbd></div></td>
<td class="key-desc">Release pointer lock / close overlays</td>
</tr>
</table>
</div>
</section>
<!-- Timmy Chat Commands -->
<section class="help-section">
<div class="section-header">
<span class="section-icon"></span>
<span class="section-title">Timmy Chat Commands</span>
</div>
<div class="section-body">
<div class="cmd-list">
<div class="cmd-item">
<span class="cmd-name">System Status</span>
<span class="cmd-desc">Quick action — asks Timmy for a live system health summary.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Agent Check</span>
<span class="cmd-desc">Quick action — lists all active agents and their current state.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Portal Atlas</span>
<span class="cmd-desc">Quick action — opens the full portal map overlay.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Help</span>
<span class="cmd-desc">Quick action — requests navigation assistance from Timmy.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Free-form text</span>
<span class="cmd-desc">Type anything in the chat bar and press Enter or → to send. Timmy processes all natural-language input.</span>
</div>
</div>
</div>
</section>
<!-- Portal Atlas -->
<section class="help-section">
<div class="section-header">
<span class="section-icon">🌐</span>
<span class="section-title">Portal Atlas</span>
</div>
<div class="section-body">
<div class="info-block">
<p>Portals are gateways to external systems and game-worlds. Walk up to a glowing portal in the Nexus and press <span class="highlight"><kbd>F</kbd></span> to activate it, or open the <span class="highlight">Portal Atlas</span> (top-right button) for a full map view.</p>
<p>Portal status indicators:</p>
</div>
<div class="portal-list" style="margin-top:14px;">
<div class="portal-item">
<span class="portal-dot dot-online"></span>
<span class="portal-name">ONLINE</span>
<span class="portal-desc">Portal is live and will redirect immediately on activation.</span>
</div>
<div class="portal-item">
<span class="portal-dot dot-standby"></span>
<span class="portal-name">STANDBY</span>
<span class="portal-desc">Portal is reachable but destination system may be idle.</span>
</div>
<div class="portal-item">
<span class="portal-dot dot-offline"></span>
<span class="portal-name">OFFLINE / UNLINKED</span>
<span class="portal-desc">Destination not yet connected. Activation shows an error card.</span>
</div>
</div>
</div>
</section>
<!-- HUD Panels -->
<section class="help-section">
<div class="section-header">
<span class="section-icon"></span>
<span class="section-title">HUD Panels</span>
</div>
<div class="section-body">
<div class="cmd-list">
<div class="cmd-item">
<span class="cmd-name">Symbolic Engine</span>
<span class="cmd-desc">Live feed from Timmy's rule-based reasoning layer.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Blackboard</span>
<span class="cmd-desc">Shared working memory used across all cognitive subsystems.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Symbolic Planner</span>
<span class="cmd-desc">Goal decomposition and task sequencing output.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Case-Based Reasoner</span>
<span class="cmd-desc">Analogical reasoning — matches current situation to past cases.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Neuro-Symbolic Bridge</span>
<span class="cmd-desc">Translation layer between neural inference and symbolic logic.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Meta-Reasoning</span>
<span class="cmd-desc">Timmy reflecting on its own thought process and confidence.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Sovereign Health</span>
<span class="cmd-desc">Core vitals: memory usage, heartbeat interval, alert flags.</span>
</div>
<div class="cmd-item">
<span class="cmd-name">Adaptive Calibrator</span>
<span class="cmd-desc">Live tuning of response thresholds and behavior weights.</span>
</div>
</div>
</div>
</section>
<!-- System Info -->
<section class="help-section">
<div class="section-header">
<span class="section-icon"></span>
<span class="section-title">System Information</span>
</div>
<div class="section-body">
<div class="info-block">
<p>The Nexus is Timmy's <span class="highlight">canonical sovereign home-world</span> — a local-first 3D space that serves as both a training ground and a live visualization surface for the Timmy AI system.</p>
<p>The WebSocket gateway (<code>server.py</code>) runs on port <span class="highlight">8765</span> and bridges Timmy's cognition layer, game-world connectors, and the browser frontend. The <span class="highlight">HERMES</span> indicator in the HUD shows live connectivity status.</p>
<p>Source code and issue tracker: <a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-nexus" target="_blank" rel="noopener noreferrer">Timmy_Foundation/the-nexus</a></p>
</div>
</div>
</section>
<!-- Footer -->
<footer class="page-footer">
<span class="footer-brand">THE NEXUS</span>
<span>Questions? Speak to Timmy in the chat bar on the main world.</span>
</footer>
</div>
</body>
</html>

View File

@@ -1,42 +0,0 @@
"""Tests for the /help page. Refs: #833 (Missing /help page)."""
from pathlib import Path
def test_help_html_exists() -> None:
assert Path("help.html").exists(), "help.html must exist to resolve /help 404"
def test_help_html_is_valid_html() -> None:
content = Path("help.html").read_text()
assert "<!DOCTYPE html>" in content
assert "<html" in content
assert "</html>" in content
def test_help_page_has_required_sections() -> None:
content = Path("help.html").read_text()
# Navigation controls section
assert "Navigation Controls" in content
# Chat commands section
assert "Chat" in content
# Portal reference
assert "Portal" in content
# Back link to home
assert 'href="/"' in content
def test_help_page_links_back_to_home() -> None:
content = Path("help.html").read_text()
assert 'href="/"' in content, "help page must have a link back to the main Nexus world"
def test_help_page_has_keyboard_controls() -> None:
content = Path("help.html").read_text()
# Movement keys are listed individually as <kbd> elements
for key in ["<kbd>W</kbd>", "<kbd>A</kbd>", "<kbd>S</kbd>", "<kbd>D</kbd>",
"Mouse", "Enter", "Esc"]:
assert key in content, f"help page must document the {key!r} control"

View File

@@ -1,39 +0,0 @@
"""Tests for manifest.json PWA support. Fixes #832 (Missing manifest.json)."""
import json
from pathlib import Path
def test_manifest_exists() -> None:
assert Path("manifest.json").exists(), "manifest.json must exist for PWA support"
def test_manifest_is_valid_json() -> None:
content = Path("manifest.json").read_text()
data = json.loads(content)
assert isinstance(data, dict)
def test_manifest_has_required_pwa_fields() -> None:
data = json.loads(Path("manifest.json").read_text())
assert "name" in data, "manifest.json must have 'name'"
assert "short_name" in data, "manifest.json must have 'short_name'"
assert "start_url" in data, "manifest.json must have 'start_url'"
assert "display" in data, "manifest.json must have 'display'"
assert "icons" in data, "manifest.json must have 'icons'"
def test_manifest_icons_non_empty() -> None:
data = json.loads(Path("manifest.json").read_text())
assert len(data["icons"]) > 0, "manifest.json must define at least one icon"
def test_index_html_references_manifest() -> None:
content = Path("index.html").read_text()
assert 'rel="manifest"' in content, "index.html must have <link rel=\"manifest\">"
assert "manifest.json" in content, "index.html must reference manifest.json"
def test_help_html_references_manifest() -> None:
content = Path("help.html").read_text()
assert 'rel="manifest"' in content, "help.html must have <link rel=\"manifest\">"
assert "manifest.json" in content, "help.html must reference manifest.json"