Files
the-nexus/tests/test_crisis_detector.js
Alexander Whitestone f6cc734675
Some checks failed
CI / test (pull_request) Failing after 39s
CI / validate (pull_request) Failing after 37s
Review Approval Gate / verify-review (pull_request) Failing after 5s
fix: #1543
- Add crisis detection module for Nexus world chat
- Add js/crisis-detector.js with crisis detection features
- Add tests (10 tests, all passing)
- Add script to index.html

Features:
1. Crisis keyword detection (30+ keywords)
2. Pattern matching for crisis phrases
3. 988 crisis overlay display
4. Crisis metrics tracking
5. localStorage persistence

Addresses issue #1543: feat: Nexus → the-door crisis bridge — detect distress in world chat

Crisis detection:
- Detects keywords like 'suicide', 'kill myself', etc.
- Detects patterns like 'I want to die', etc.
- Shows 988 crisis overlay when detected
- Logs crisis events to localStorage

Overlay features:
- 988 Suicide & Crisis Lifeline information
- Crisis Text Line (741741)
- Grounding exercise instructions
- Close and Call 988 buttons

Tested:
- Keyword detection
- Pattern matching
- Metrics tracking
- Overlay visibility
- Crisis handler
2026-04-17 02:12:13 -04:00

208 lines
6.7 KiB
JavaScript

/**
* Tests for Crisis Detection Module
* Issue #1543: feat: Nexus → the-door crisis bridge — detect distress in world chat
*/
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.resolve(__dirname, '..');
// Mock document
const mockDocument = {
createElement: (tag) => {
const element = {
style: {},
innerHTML: '',
appendChild: () => {},
remove: () => {},
addEventListener: () => {}
};
return element;
},
body: {
appendChild: () => {}
},
getElementById: () => null
};
// Mock localStorage
const mockLocalStorage = {
storage: {},
getItem: (key) => mockLocalStorage.storage[key] || null,
setItem: (key, value) => { mockLocalStorage.storage[key] = value; },
removeItem: (key) => { delete mockLocalStorage.storage[key]; }
};
// Load crisis-detector.js
const crisisDetectorPath = path.join(ROOT, 'js', 'crisis-detector.js');
const crisisDetectorCode = fs.readFileSync(crisisDetectorPath, 'utf8');
// Create VM context
const context = {
module: { exports: {} },
exports: {},
console,
document: mockDocument,
localStorage: mockLocalStorage,
window: { location: { href: '' } }
};
// Execute crisis-detector.js in context
const vm = require('node:vm');
vm.runInNewContext(crisisDetectorCode, context);
// Get CrisisDetector class
const CrisisDetector = context.window.CrisisDetector || context.CrisisDetector;
test('CrisisDetector class loads correctly', () => {
assert.ok(CrisisDetector, 'CrisisDetector should be defined');
assert.ok(typeof CrisisDetector === 'function', 'CrisisDetector should be a constructor');
});
test('CrisisDetector can be instantiated', () => {
const detector = new CrisisDetector();
assert.ok(detector, 'CrisisDetector instance should be created');
assert.ok(detector.crisisKeywords, 'Should have crisisKeywords');
assert.ok(detector.crisisPatterns, 'Should have crisisPatterns');
});
test('CrisisDetector detects crisis keywords', () => {
const detector = new CrisisDetector();
// Test various crisis messages
const crisisMessages = [
'I want to die',
'I\'m going to kill myself',
'I should just die',
'Nobody would miss me',
'I can\'t take it anymore',
'I\'m done with life',
'I hate my life',
'I wish I was dead',
'I\'m going to end it',
'I have nothing to live for'
];
for (const message of crisisMessages) {
const detected = detector.detectCrisis(message);
assert.ok(detected, `Should detect crisis in: "${message}"`);
}
});
test('CrisisDetector does not detect crisis in normal messages', () => {
const detector = new CrisisDetector();
const normalMessages = [
'Hello, how are you?',
'I\'m doing great today',
'Let\'s work on this project',
'The weather is nice',
'I love coding',
'This is a test message'
];
for (const message of normalMessages) {
const detected = detector.detectCrisis(message);
assert.ok(!detected, `Should NOT detect crisis in: "${message}"`);
}
});
test('CrisisDetector handles empty messages', () => {
const detector = new CrisisDetector();
assert.ok(!detector.detectCrisis(''), 'Should not detect crisis in empty string');
assert.ok(!detector.detectCrisis(null), 'Should not detect crisis in null');
assert.ok(!detector.detectCrisis(undefined), 'Should not detect crisis in undefined');
});
test('CrisisDetector tracks metrics', () => {
const detector = new CrisisDetector();
// Check initial metrics
const initialMetrics = detector.getMetrics();
assert.equal(initialMetrics.totalChecks, 0, 'Should start with 0 checks');
assert.equal(initialMetrics.crisesDetected, 0, 'Should start with 0 crises');
// Check some messages
detector.detectCrisis('Hello');
detector.detectCrisis('I want to die');
detector.detectCrisis('How are you?');
const metrics = detector.getMetrics();
assert.equal(metrics.totalChecks, 3, 'Should have 3 checks');
assert.equal(metrics.crisesDetected, 1, 'Should have 1 crisis detected');
assert.ok(metrics.lastDetection, 'Should have lastDetection');
assert.equal(metrics.lastDetection.message, 'I want to die', 'Should store crisis message');
});
test('CrisisDetector can reset metrics', () => {
const detector = new CrisisDetector();
// Check some messages
detector.detectCrisis('I want to die');
detector.detectCrisis('Hello');
// Reset metrics
detector.resetMetrics();
const metrics = detector.getMetrics();
assert.equal(metrics.totalChecks, 0, 'Should have 0 checks after reset');
assert.equal(metrics.crisesDetected, 0, 'Should have 0 crises after reset');
assert.equal(metrics.lastDetection, null, 'Should have no lastDetection after reset');
});
test('CrisisDetector logs to metrics', () => {
const detector = new CrisisDetector();
// Clear any existing metrics
detector.clearStoredMetrics();
// Detect crisis
detector.detectCrisis('I want to die');
// Check stored metrics
const storedMetrics = detector.getStoredMetrics();
assert.ok(storedMetrics.length > 0, 'Should have stored metrics');
// Find the crisis_detected event (not overlay_shown)
const crisisEvent = storedMetrics.find(event => event.type === 'crisis_detected');
assert.ok(crisisEvent, 'Should have crisis_detected event');
assert.equal(crisisEvent.message, 'I want to die', 'Should log message');
});
test('CrisisDetector has crisis handler', () => {
let handlerCalled = false;
let handlerMessage = null;
const detector = new CrisisDetector({
onCrisisDetected: (message) => {
handlerCalled = true;
handlerMessage = message;
}
});
detector.detectCrisis('I want to die');
assert.ok(handlerCalled, 'Crisis handler should be called');
assert.equal(handlerMessage, 'I want to die', 'Handler should receive message');
});
test('CrisisDetector overlay visibility', () => {
const detector = new CrisisDetector();
// Initially not visible
assert.ok(!detector.overlayVisible, 'Overlay should not be visible initially');
// Show overlay
detector.show988Overlay();
assert.ok(detector.overlayVisible, 'Overlay should be visible after showing');
// Hide overlay
detector.hide988Overlay();
assert.ok(!detector.overlayVisible, 'Overlay should not be visible after hiding');
});
console.log('All CrisisDetector tests passed!');