'use strict'; /** * Integration tests for session-monitor.js * Tests session detection by writing mock sessions.json files. */ const { describe, it, beforeEach, afterEach } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('fs'); const path = require('path'); const os = require('os'); const { SessionMonitor } = require('../../src/session-monitor'); function createTmpDir() { return fs.mkdtempSync(path.join(os.tmpdir(), 'sm-test-')); } function writeSessionsJson(dir, agentId, sessions) { const agentDir = path.join(dir, agentId, 'sessions'); fs.mkdirSync(agentDir, { recursive: true }); fs.writeFileSync(path.join(agentDir, 'sessions.json'), JSON.stringify(sessions, null, 2)); // Create stub transcript files so SessionMonitor's stale-check doesn't skip them for (const key of Object.keys(sessions)) { const entry = sessions[key]; // eslint-disable-line security/detect-object-injection const sessionId = entry.sessionId || entry.uuid; if (sessionId) { const transcriptPath = path.join(agentDir, `${sessionId}.jsonl`); if (!fs.existsSync(transcriptPath)) { fs.writeFileSync(transcriptPath, ''); } } } } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } describe('SessionMonitor', () => { let tmpDir; let monitor; beforeEach(() => { tmpDir = createTmpDir(); }); afterEach(() => { if (monitor) { monitor.stop(); monitor = null; } try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_e) { /* ignore */ } }); describe('parseChannelId()', () => { it('parses channel ID from mattermost session key', () => { const key = 'agent:main:mattermost:channel:abc123:thread:xyz'; assert.equal(SessionMonitor.parseChannelId(key), 'abc123'); }); it('parses DM channel', () => { const key = 'agent:main:mattermost:dm:user456'; assert.equal(SessionMonitor.parseChannelId(key), 'user456'); }); it('returns null for non-MM session', () => { const key = 'agent:main:hook:session:xyz'; assert.equal(SessionMonitor.parseChannelId(key), null); }); }); describe('parseRootPostId()', () => { it('parses thread ID from session key', () => { const key = 'agent:main:mattermost:channel:abc123:thread:rootpost999'; assert.equal(SessionMonitor.parseRootPostId(key), 'rootpost999'); }); it('returns null if no thread', () => { const key = 'agent:main:mattermost:channel:abc123'; assert.equal(SessionMonitor.parseRootPostId(key), null); }); }); describe('parseAgentId()', () => { it('extracts agent ID', () => { assert.equal(SessionMonitor.parseAgentId('agent:main:mattermost:channel:abc'), 'main'); assert.equal(SessionMonitor.parseAgentId('agent:coder-agent:session'), 'coder-agent'); }); }); describe('isMattermostSession()', () => { it('detects mattermost sessions', () => { assert.equal(SessionMonitor.isMattermostSession('agent:main:mattermost:channel:abc'), true); assert.equal(SessionMonitor.isMattermostSession('agent:main:hook:abc'), false); }); }); describe('session-added event', () => { it('emits session-added for new session in sessions.json', async () => { const sessionKey = 'agent:main:mattermost:channel:testchan:thread:testroot'; const sessionId = 'aaaa-1111-bbbb-2222'; writeSessionsJson(tmpDir, 'main', { [sessionKey]: { sessionId, spawnedBy: null, spawnDepth: 0, label: null, channel: 'mattermost', }, }); monitor = new SessionMonitor({ transcriptDir: tmpDir, pollMs: 50, }); const added = []; monitor.on('session-added', (info) => added.push(info)); monitor.start(); await sleep(200); assert.equal(added.length, 1); assert.equal(added[0].sessionKey, sessionKey); assert.equal(added[0].channelId, 'testchan'); assert.equal(added[0].rootPostId, 'testroot'); assert.ok(added[0].transcriptFile.endsWith(`${sessionId}.jsonl`)); }); it('emits session-removed when session disappears', async () => { const sessionKey = 'agent:main:mattermost:channel:testchan:thread:testroot'; const sessionId = 'cccc-3333-dddd-4444'; writeSessionsJson(tmpDir, 'main', { [sessionKey]: { sessionId, spawnedBy: null, spawnDepth: 0, channel: 'mattermost' }, }); monitor = new SessionMonitor({ transcriptDir: tmpDir, pollMs: 50 }); const removed = []; monitor.on('session-removed', (key) => removed.push(key)); monitor.start(); await sleep(200); assert.equal(removed.length, 0); // Remove the session writeSessionsJson(tmpDir, 'main', {}); await sleep(200); assert.equal(removed.length, 1); assert.equal(removed[0], sessionKey); }); it('skips non-MM sessions with no default channel', async () => { const sessionKey = 'agent:main:hook:session:xyz'; writeSessionsJson(tmpDir, 'main', { [sessionKey]: { sessionId: 'hook-uuid', channel: 'hook' }, }); monitor = new SessionMonitor({ transcriptDir: tmpDir, pollMs: 50, defaultChannel: null }); const added = []; monitor.on('session-added', (info) => added.push(info)); monitor.start(); await sleep(200); assert.equal(added.length, 0); }); it('includes non-MM sessions when defaultChannel is set', async () => { const sessionKey = 'agent:main:hook:session:xyz'; writeSessionsJson(tmpDir, 'main', { [sessionKey]: { sessionId: 'hook-uuid', channel: 'hook' }, }); monitor = new SessionMonitor({ transcriptDir: tmpDir, pollMs: 50, defaultChannel: 'default-channel-id', }); const added = []; monitor.on('session-added', (info) => added.push(info)); monitor.start(); await sleep(200); assert.equal(added.length, 1); assert.equal(added[0].channelId, 'default-channel-id'); }); it('detects multiple agents', async () => { writeSessionsJson(tmpDir, 'main', { 'agent:main:mattermost:channel:c1:thread:t1': { sessionId: 'sess-main', spawnedBy: null, channel: 'mattermost', }, }); writeSessionsJson(tmpDir, 'coder-agent', { 'agent:coder-agent:mattermost:channel:c2:thread:t2': { sessionId: 'sess-coder', spawnedBy: null, channel: 'mattermost', }, }); monitor = new SessionMonitor({ transcriptDir: tmpDir, pollMs: 50 }); const added = []; monitor.on('session-added', (info) => added.push(info)); monitor.start(); await sleep(200); assert.equal(added.length, 2); const keys = added.map((s) => s.sessionKey).sort(); assert.ok(keys.includes('agent:main:mattermost:channel:c1:thread:t1')); assert.ok(keys.includes('agent:coder-agent:mattermost:channel:c2:thread:t2')); }); it('handles malformed sessions.json gracefully', async () => { const agentDir = path.join(tmpDir, 'main', 'sessions'); fs.mkdirSync(agentDir, { recursive: true }); fs.writeFileSync(path.join(agentDir, 'sessions.json'), 'not valid json'); monitor = new SessionMonitor({ transcriptDir: tmpDir, pollMs: 50 }); const added = []; monitor.on('session-added', (info) => added.push(info)); monitor.start(); await sleep(200); // Should not throw and should produce no sessions assert.equal(added.length, 0); }); }); describe('getKnownSessions()', () => { it('returns current known sessions', async () => { const sessionKey = 'agent:main:mattermost:channel:c1:thread:t1'; writeSessionsJson(tmpDir, 'main', { [sessionKey]: { sessionId: 'test-uuid', channel: 'mattermost' }, }); monitor = new SessionMonitor({ transcriptDir: tmpDir, pollMs: 50 }); monitor.start(); await sleep(200); const sessions = monitor.getKnownSessions(); assert.equal(sessions.size, 1); assert.ok(sessions.has(sessionKey)); }); }); });