Compare commits
13 Commits
door/issue
...
fix/141-cr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f007808228 | ||
|
|
c494bba728 | ||
| d412939b4f | |||
| 07c582aa08 | |||
| 5f95dc1e39 | |||
| b1f3cac36d | |||
| 07b3f67845 | |||
| c22bbbaf65 | |||
| 543cb1d40f | |||
| 3cfd01815a | |||
| 5a7ba9f207 | |||
| 8ed8f20a17 | |||
| 9d7d26033e |
19
crisis/bridge.py
Normal file
19
crisis/bridge.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""Compatibility bridge for the-door crisis gateway.
|
||||
|
||||
Issue #141 describes the shared bridge API as `crisis.bridge`.
|
||||
The canonical implementation lives in `crisis.gateway`. Re-export the public
|
||||
entrypoints here so downstream hermes-agent wiring can import the stable name
|
||||
without copying logic out of the-door.
|
||||
"""
|
||||
|
||||
from .gateway import (
|
||||
check_crisis,
|
||||
get_system_prompt,
|
||||
format_gateway_response,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"check_crisis",
|
||||
"get_system_prompt",
|
||||
"format_gateway_response",
|
||||
]
|
||||
19
crisis/tracker.py
Normal file
19
crisis/tracker.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""Compatibility bridge for crisis session tracking.
|
||||
|
||||
Issue #141 describes the shared the-door tracker surface as `crisis.tracker`.
|
||||
The canonical implementation lives in `crisis.session_tracker`, but hermes-agent
|
||||
integration should be able to import the shorter path without caring about
|
||||
internal file layout.
|
||||
"""
|
||||
|
||||
from .session_tracker import (
|
||||
CrisisSessionTracker,
|
||||
SessionState,
|
||||
check_crisis_with_session,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"CrisisSessionTracker",
|
||||
"SessionState",
|
||||
"check_crisis_with_session",
|
||||
]
|
||||
45
index.html
45
index.html
@@ -72,31 +72,6 @@ html, body {
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Subtle safety plan button in banner — always visible */
|
||||
#banner-safety-plan-btn {
|
||||
background: none;
|
||||
border: 1px solid #6e7681;
|
||||
color: #8b949e;
|
||||
cursor: pointer;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: background 0.2s, border-color 0.2s, color 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#banner-safety-plan-btn:hover,
|
||||
#banner-safety-plan-btn:focus {
|
||||
background: rgba(139, 148, 158, 0.15);
|
||||
border-color: #8b949e;
|
||||
color: #e6edf3;
|
||||
outline: 2px solid #58a6ff;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
#connection-status {
|
||||
font-size: 0.7rem;
|
||||
color: #6e7681;
|
||||
@@ -650,10 +625,6 @@ html, body {
|
||||
<a href="tel:988" aria-label="Call 988 Suicide and Crisis Lifeline">
|
||||
988 Suicide & Crisis Lifeline — Call or text 988
|
||||
</a>
|
||||
<button id="banner-safety-plan-btn" aria-label="Open my safety plan" title="My Safety Plan">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
|
||||
<span class="sr-only">My Safety Plan</span>
|
||||
</button>
|
||||
<div id="connection-status" aria-hidden="true">
|
||||
<span class="status-dot"></span>
|
||||
<span id="status-text">Online</span>
|
||||
@@ -709,7 +680,7 @@ html, body {
|
||||
|
||||
<!-- Footer -->
|
||||
<footer id="footer">
|
||||
<a href="/about" aria-label="About The Door">about</a>
|
||||
<a href="/about.html" aria-label="About The Door">about</a>
|
||||
<button id="safety-plan-btn" aria-label="Open My Safety Plan">my safety plan</button>
|
||||
<button id="clear-chat-btn" aria-label="Clear chat history">clear chat</button>
|
||||
</footer>
|
||||
@@ -837,13 +808,13 @@ Sovereignty and service always.`;
|
||||
var crisisPanel = document.getElementById('crisis-panel');
|
||||
var crisisOverlay = document.getElementById('crisis-overlay');
|
||||
var overlayDismissBtn = document.getElementById('overlay-dismiss-btn');
|
||||
var overlayCallLink = document.querySelector('.overlay-call');
|
||||
var statusDot = document.querySelector('.status-dot');
|
||||
var statusText = document.getElementById('status-text');
|
||||
|
||||
// Safety Plan Elements
|
||||
var safetyPlanBtn = document.getElementById('safety-plan-btn');
|
||||
var crisisSafetyPlanBtn = document.getElementById('crisis-safety-plan-btn');
|
||||
var bannerSafetyPlanBtn = document.getElementById('banner-safety-plan-btn');
|
||||
var safetyPlanModal = document.getElementById('safety-plan-modal');
|
||||
var closeSafetyPlan = document.getElementById('close-safety-plan');
|
||||
var cancelSafetyPlan = document.getElementById('cancel-safety-plan');
|
||||
@@ -1080,7 +1051,8 @@ Sovereignty and service always.`;
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
overlayDismissBtn.focus();
|
||||
// Focus the Call 988 link (always enabled) — disabled buttons cannot receive focus
|
||||
if (overlayCallLink) overlayCallLink.focus();
|
||||
}
|
||||
|
||||
// Register focus trap on document (always listening, gated by class check)
|
||||
@@ -1329,15 +1301,6 @@ Sovereignty and service always.`;
|
||||
});
|
||||
}
|
||||
|
||||
// Banner safety plan button — always visible in header
|
||||
if (bannerSafetyPlanBtn) {
|
||||
bannerSafetyPlanBtn.addEventListener('click', function() {
|
||||
loadSafetyPlan();
|
||||
safetyPlanModal.classList.add('active');
|
||||
_activateSafetyPlanFocusTrap(bannerSafetyPlanBtn);
|
||||
});
|
||||
}
|
||||
|
||||
// ===== TEXTAREA AUTO-RESIZE =====
|
||||
msgInput.addEventListener('input', function() {
|
||||
this.style.height = 'auto';
|
||||
|
||||
26
tests/test_crisis_hermes_contract.py
Normal file
26
tests/test_crisis_hermes_contract.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Regression tests for the crisis integration contract expected by hermes-agent.
|
||||
|
||||
Issue #141 names the-door-side shared modules as `crisis.tracker` and
|
||||
`crisis.bridge`. Keep those import paths available even if the canonical
|
||||
implementation lives in `session_tracker.py` and `gateway.py`.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
|
||||
|
||||
def test_crisis_tracker_module_exports_session_tracker_contract():
|
||||
tracker = importlib.import_module("crisis.tracker")
|
||||
session_tracker = importlib.import_module("crisis.session_tracker")
|
||||
|
||||
assert tracker.CrisisSessionTracker is session_tracker.CrisisSessionTracker
|
||||
assert tracker.SessionState is session_tracker.SessionState
|
||||
assert tracker.check_crisis_with_session is session_tracker.check_crisis_with_session
|
||||
|
||||
|
||||
def test_crisis_bridge_module_exports_gateway_contract():
|
||||
bridge = importlib.import_module("crisis.bridge")
|
||||
gateway = importlib.import_module("crisis.gateway")
|
||||
|
||||
assert bridge.check_crisis is gateway.check_crisis
|
||||
assert bridge.get_system_prompt is gateway.get_system_prompt
|
||||
assert bridge.format_gateway_response is gateway.format_gateway_response
|
||||
@@ -52,6 +52,34 @@ class TestCrisisOverlayFocusTrap(unittest.TestCase):
|
||||
'Expected overlay dismissal to restore focus to the prior target.',
|
||||
)
|
||||
|
||||
def test_overlay_initial_focus_targets_enabled_call_link(self):
|
||||
"""Overlay must focus the Call 988 link, not the disabled dismiss button."""
|
||||
# Find the showOverlay function body (up to the closing of the setInterval callback
|
||||
# and the focus call that follows)
|
||||
show_start = self.html.find('function showOverlay()')
|
||||
self.assertGreater(show_start, -1, "showOverlay function not found")
|
||||
# Find the focus call within showOverlay (before the next function registration)
|
||||
focus_section = self.html[show_start:show_start + 2000]
|
||||
self.assertIn(
|
||||
'overlayCallLink',
|
||||
focus_section,
|
||||
"Expected showOverlay to reference overlayCallLink for initial focus.",
|
||||
)
|
||||
# Ensure the old buggy pattern is gone
|
||||
focus_line_region = self.html[show_start + 800:show_start + 1200]
|
||||
self.assertNotIn(
|
||||
'overlayDismissBtn.focus()',
|
||||
focus_line_region,
|
||||
"showOverlay must not focus the disabled dismiss button.",
|
||||
)
|
||||
|
||||
def test_overlay_call_link_variable_is_declared(self):
|
||||
self.assertIn(
|
||||
"querySelector('.overlay-call')",
|
||||
self.html,
|
||||
"Expected a JS reference to the .overlay-call link element.",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -50,6 +50,22 @@ class TestCrisisOfflinePage(unittest.TestCase):
|
||||
for phrase in required_phrases:
|
||||
self.assertIn(phrase, self.lower_html)
|
||||
|
||||
def test_no_external_resources(self):
|
||||
"""Offline page must work without any network — no external CSS/JS."""
|
||||
import re
|
||||
html = self.html
|
||||
# No https:// links (except tel: and sms: which are protocol links, not network)
|
||||
external_urls = re.findall(r'href=["\']https://|src=["\']https://', html)
|
||||
self.assertEqual(external_urls, [], 'Offline page must not load external resources')
|
||||
# CSS and JS must be inline
|
||||
self.assertIn('<style>', html, 'CSS must be inline')
|
||||
self.assertIn('<script>', html, 'JS must be inline')
|
||||
|
||||
def test_retry_button_present(self):
|
||||
"""User must be able to retry connection from offline page."""
|
||||
self.assertIn('retry-connection', self.html)
|
||||
self.assertIn('Retry connection', self.html)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user