Phase 1 cleanup: - Remove deletePost() method (dead code, replaced by PUT in-place updates) - Remove _postInfo Map tracking (no longer needed) - Remove pin/unpin API calls from watcher-manager.js (incompatible with PUT updates) - Add JSDoc note on (edited) label limitation in _flushUpdate() - Add integration test: test/integration/poll-fallback.test.js - Fix addSession() lastOffset===0 falsy bug (0 was treated as 'no offset') - Fix pre-existing test failures: add lastOffset:0 where tests expect backlog reads - Fix pre-existing session-monitor test: create stub transcript files - Fix pre-existing status-formatter test: update indent check for blockquote format - Format plugin/ files with Prettier (pre-existing formatting drift)
263 lines
8.1 KiB
JavaScript
263 lines
8.1 KiB
JavaScript
'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));
|
|
});
|
|
});
|
|
});
|