- 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
208 lines
6.7 KiB
JavaScript
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!'); |