Files
the-nexus/test-hermes-session.js
Claude (Opus 4.6) 1f005b8e64
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
[claude] Hermes session save/load + integration test (#286) (#320)
2026-03-24 04:56:42 +00:00

242 lines
8.0 KiB
JavaScript

#!/usr/bin/env node
/**
* Integration test — Hermes session save and load
*
* Tests the session persistence layer of WebSocketClient in isolation.
* Runs with Node.js built-ins only — no browser, no real WebSocket.
*
* Run: node test-hermes-session.js
*/
import { readFileSync } from 'fs';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
let passed = 0;
let failed = 0;
function pass(name) {
console.log(`${name}`);
passed++;
}
function fail(name, reason) {
console.log(`${name}`);
if (reason) console.log(`${reason}`);
failed++;
}
function section(name) {
console.log(`\n${name}`);
}
// ── In-memory localStorage mock ─────────────────────────────────────────────
class MockStorage {
constructor() { this._store = new Map(); }
getItem(key) { return this._store.has(key) ? this._store.get(key) : null; }
setItem(key, value) { this._store.set(key, String(value)); }
removeItem(key) { this._store.delete(key); }
clear() { this._store.clear(); }
}
// ── Minimal WebSocketClient extracted from ws-client.js ───────────────────
// We re-implement only the session methods so the test has no browser deps.
const SESSION_STORAGE_KEY = 'hermes-session';
class SessionClient {
constructor(storage) {
this._storage = storage;
this.session = null;
}
saveSession(data) {
const payload = { ...data, savedAt: Date.now() };
this._storage.setItem(SESSION_STORAGE_KEY, JSON.stringify(payload));
this.session = data;
}
loadSession() {
const raw = this._storage.getItem(SESSION_STORAGE_KEY);
if (!raw) return null;
const data = JSON.parse(raw);
this.session = data;
return data;
}
clearSession() {
this._storage.removeItem(SESSION_STORAGE_KEY);
this.session = null;
}
}
// ── Tests ────────────────────────────────────────────────────────────────────
section('Session Save');
const store1 = new MockStorage();
const client1 = new SessionClient(store1);
// saveSession persists to storage
client1.saveSession({ token: 'abc-123', clientId: 'nexus-visitor' });
const raw = store1.getItem(SESSION_STORAGE_KEY);
if (raw) {
pass('saveSession writes to storage');
} else {
fail('saveSession writes to storage', 'storage item is null after save');
}
// Persisted JSON is parseable
try {
const parsed = JSON.parse(raw);
pass('stored value is valid JSON');
if (parsed.token === 'abc-123') {
pass('token field preserved');
} else {
fail('token field preserved', `expected "abc-123", got "${parsed.token}"`);
}
if (parsed.clientId === 'nexus-visitor') {
pass('clientId field preserved');
} else {
fail('clientId field preserved', `expected "nexus-visitor", got "${parsed.clientId}"`);
}
if (typeof parsed.savedAt === 'number' && parsed.savedAt > 0) {
pass('savedAt timestamp present');
} else {
fail('savedAt timestamp present', `got: ${parsed.savedAt}`);
}
} catch (e) {
fail('stored value is valid JSON', e.message);
}
// in-memory session property updated
if (client1.session && client1.session.token === 'abc-123') {
pass('this.session updated after saveSession');
} else {
fail('this.session updated after saveSession', JSON.stringify(client1.session));
}
// ── Session Load ─────────────────────────────────────────────────────────────
section('Session Load');
const store2 = new MockStorage();
const client2 = new SessionClient(store2);
// loadSession on empty storage returns null
const empty = client2.loadSession();
if (empty === null) {
pass('loadSession returns null when no session stored');
} else {
fail('loadSession returns null when no session stored', `got: ${JSON.stringify(empty)}`);
}
// Seed the storage and load
store2.setItem(SESSION_STORAGE_KEY, JSON.stringify({ token: 'xyz-789', clientId: 'timmy', savedAt: 1700000000000 }));
const loaded = client2.loadSession();
if (loaded && loaded.token === 'xyz-789') {
pass('loadSession returns stored token');
} else {
fail('loadSession returns stored token', `got: ${JSON.stringify(loaded)}`);
}
if (loaded && loaded.clientId === 'timmy') {
pass('loadSession returns stored clientId');
} else {
fail('loadSession returns stored clientId', `got: ${JSON.stringify(loaded)}`);
}
if (client2.session && client2.session.token === 'xyz-789') {
pass('this.session updated after loadSession');
} else {
fail('this.session updated after loadSession', JSON.stringify(client2.session));
}
// ── Full save → reload cycle ─────────────────────────────────────────────────
section('Save → Load Round-trip');
const store3 = new MockStorage();
const writer = new SessionClient(store3);
const reader = new SessionClient(store3); // simulates a page reload (new instance, same storage)
writer.saveSession({ token: 'round-trip-token', role: 'visitor' });
const reloaded = reader.loadSession();
if (reloaded && reloaded.token === 'round-trip-token') {
pass('round-trip: token survives save → load');
} else {
fail('round-trip: token survives save → load', JSON.stringify(reloaded));
}
if (reloaded && reloaded.role === 'visitor') {
pass('round-trip: extra fields survive save → load');
} else {
fail('round-trip: extra fields survive save → load', JSON.stringify(reloaded));
}
// ── clearSession ─────────────────────────────────────────────────────────────
section('Session Clear');
const store4 = new MockStorage();
const client4 = new SessionClient(store4);
client4.saveSession({ token: 'to-be-cleared' });
client4.clearSession();
const afterClear = client4.loadSession();
if (afterClear === null) {
pass('clearSession removes stored session');
} else {
fail('clearSession removes stored session', `still got: ${JSON.stringify(afterClear)}`);
}
if (client4.session === null) {
pass('this.session is null after clearSession');
} else {
fail('this.session is null after clearSession', JSON.stringify(client4.session));
}
// ── ws-client.js static check ────────────────────────────────────────────────
section('ws-client.js Session Methods (static analysis)');
const wsClientSrc = (() => {
try { return readFileSync(resolve(__dirname, 'ws-client.js'), 'utf8'); }
catch (e) { fail('ws-client.js readable', e.message); return ''; }
})();
if (wsClientSrc) {
const checks = [
['saveSession method defined', /saveSession\s*\(/],
['loadSession method defined', /loadSession\s*\(/],
['clearSession method defined', /clearSession\s*\(/],
['SESSION_STORAGE_KEY constant', /SESSION_STORAGE_KEY/],
['session-init message handled', /'session-init'/],
['session-resume sent on open', /session-resume/],
['this.session property set', /this\.session\s*=/],
];
for (const [name, re] of checks) {
if (re.test(wsClientSrc)) {
pass(name);
} else {
fail(name, `pattern not found: ${re}`);
}
}
}
// ── Summary ──────────────────────────────────────────────────────────────────
console.log(`\n${'─'.repeat(50)}`);
console.log(`Results: ${passed} passed, ${failed} failed`);
if (failed > 0) {
console.log('\nSome tests failed. Fix the issues above before committing.\n');
process.exit(1);
} else {
console.log('\nAll session tests passed.\n');
}