mirror of
https://github.com/EvoMap/evolver.git
synced 2026-09-18 21:47:53 +08:00
289 lines
8.7 KiB
JavaScript
289 lines
8.7 KiB
JavaScript
'use strict';
|
|
|
|
const { describe, it, before, after } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
const { MailboxStore } = require('../src/proxy/mailbox/store');
|
|
const { SkillUpdater } = require('../src/proxy/extensions/skillUpdater');
|
|
const { DmHandler } = require('../src/proxy/extensions/dmHandler');
|
|
const { SessionHandler } = require('../src/proxy/extensions/sessionHandler');
|
|
|
|
function tmpDataDir() {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'extensions-test-'));
|
|
}
|
|
|
|
describe('SkillUpdater', () => {
|
|
let store, dataDir, skillDir;
|
|
|
|
before(() => {
|
|
dataDir = tmpDataDir();
|
|
skillDir = tmpDataDir();
|
|
store = new MailboxStore(dataDir);
|
|
});
|
|
|
|
after(() => {
|
|
store.close();
|
|
try { fs.rmSync(dataDir, { recursive: true }); } catch {}
|
|
try { fs.rmSync(skillDir, { recursive: true }); } catch {}
|
|
});
|
|
|
|
it('updates skill.md from inbound message', () => {
|
|
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
const updater = new SkillUpdater({
|
|
store,
|
|
skillPath,
|
|
logger: { log: () => {}, warn: () => {}, error: () => {} },
|
|
});
|
|
|
|
const result = updater.processSkillUpdate({
|
|
payload: { content: '# Updated Skill\nNew content here.', version: '1.1.0' },
|
|
});
|
|
assert.equal(result, true);
|
|
assert.equal(fs.readFileSync(skillPath, 'utf8'), '# Updated Skill\nNew content here.');
|
|
assert.equal(store.getState('skill_version'), '1.1.0');
|
|
});
|
|
|
|
it('creates backup before overwriting', () => {
|
|
const skillPath = path.join(skillDir, 'SKILL2.md');
|
|
fs.writeFileSync(skillPath, 'original content', 'utf8');
|
|
|
|
const updater = new SkillUpdater({
|
|
store,
|
|
skillPath,
|
|
logger: { log: () => {}, warn: () => {}, error: () => {} },
|
|
});
|
|
|
|
updater.processSkillUpdate({
|
|
payload: { content: 'updated content', version: '2.0' },
|
|
});
|
|
assert.equal(fs.readFileSync(skillPath, 'utf8'), 'updated content');
|
|
assert.equal(fs.readFileSync(skillPath + '.bak', 'utf8'), 'original content');
|
|
});
|
|
|
|
it('returns false without skill path', () => {
|
|
const updater = new SkillUpdater({
|
|
store,
|
|
logger: { log: () => {}, warn: () => {}, error: () => {} },
|
|
});
|
|
assert.equal(updater.processSkillUpdate({ payload: { content: 'x' } }), false);
|
|
});
|
|
|
|
it('returns false without content', () => {
|
|
const updater = new SkillUpdater({
|
|
store,
|
|
skillPath: path.join(skillDir, 'noop.md'),
|
|
logger: { log: () => {}, warn: () => {}, error: () => {} },
|
|
});
|
|
assert.equal(updater.processSkillUpdate({ payload: {} }), false);
|
|
});
|
|
|
|
it('pollAndApply processes pending skill_update messages', () => {
|
|
const dir2 = tmpDataDir();
|
|
const s2 = new MailboxStore(dir2);
|
|
const sp = path.join(skillDir, 'polled.md');
|
|
|
|
s2.writeInbound({
|
|
type: 'skill_update',
|
|
payload: { content: '# Polled skill', version: '3.0' },
|
|
});
|
|
|
|
const updater = new SkillUpdater({
|
|
store: s2,
|
|
skillPath: sp,
|
|
logger: { log: () => {}, warn: () => {}, error: () => {} },
|
|
});
|
|
|
|
const applied = updater.pollAndApply();
|
|
assert.equal(applied, 1);
|
|
assert.equal(fs.readFileSync(sp, 'utf8'), '# Polled skill');
|
|
s2.close();
|
|
try { fs.rmSync(dir2, { recursive: true }); } catch {}
|
|
});
|
|
});
|
|
|
|
describe('DmHandler', () => {
|
|
let store, handler, dataDir;
|
|
|
|
before(() => {
|
|
dataDir = tmpDataDir();
|
|
store = new MailboxStore(dataDir);
|
|
handler = new DmHandler({ store });
|
|
});
|
|
|
|
after(() => {
|
|
store.close();
|
|
try { fs.rmSync(dataDir, { recursive: true }); } catch {}
|
|
});
|
|
|
|
it('sends a DM and creates outbound message', () => {
|
|
const result = handler.send({
|
|
recipientNodeId: 'node_abc',
|
|
content: 'Hello there',
|
|
});
|
|
assert.ok(result.message_id);
|
|
assert.equal(result.status, 'pending');
|
|
|
|
const msg = store.getById(result.message_id);
|
|
assert.equal(msg.type, 'dm');
|
|
assert.equal(msg.direction, 'outbound');
|
|
assert.equal(msg.payload.recipient_node_id, 'node_abc');
|
|
assert.equal(msg.payload.content, 'Hello there');
|
|
});
|
|
|
|
it('throws on missing recipientNodeId', () => {
|
|
assert.throws(() => handler.send({ content: 'x' }), /recipientNodeId/);
|
|
});
|
|
|
|
it('throws on missing content', () => {
|
|
assert.throws(() => handler.send({ recipientNodeId: 'n' }), /content/);
|
|
});
|
|
|
|
it('polls inbound DMs', () => {
|
|
store.writeInbound({ type: 'dm', payload: { content: 'incoming dm' } });
|
|
const msgs = handler.poll();
|
|
assert.ok(msgs.length >= 1);
|
|
assert.equal(msgs[0].type, 'dm');
|
|
});
|
|
|
|
it('acks DM messages', () => {
|
|
const id = store.writeInbound({ type: 'dm', payload: { content: 'to ack' } });
|
|
const count = handler.ack(id);
|
|
assert.equal(count, 1);
|
|
const msg = store.getById(id);
|
|
assert.equal(msg.status, 'delivered');
|
|
});
|
|
|
|
it('lists DM history', () => {
|
|
const msgs = handler.list();
|
|
assert.ok(Array.isArray(msgs));
|
|
});
|
|
});
|
|
|
|
describe('SessionHandler', () => {
|
|
let store, handler, dataDir;
|
|
|
|
before(() => {
|
|
dataDir = tmpDataDir();
|
|
store = new MailboxStore(dataDir);
|
|
handler = new SessionHandler({ store });
|
|
});
|
|
|
|
after(() => {
|
|
store.close();
|
|
try { fs.rmSync(dataDir, { recursive: true }); } catch {}
|
|
});
|
|
|
|
it('creates a session and stores outbound message', () => {
|
|
const result = handler.createSession({
|
|
title: 'Test Session',
|
|
description: 'A test collaboration session',
|
|
inviteNodeIds: ['node_a', 'node_b'],
|
|
});
|
|
assert.ok(result.message_id);
|
|
assert.equal(result.status, 'pending');
|
|
|
|
const msg = store.getById(result.message_id);
|
|
assert.equal(msg.type, 'session_create');
|
|
assert.equal(msg.direction, 'outbound');
|
|
assert.equal(msg.payload.title, 'Test Session');
|
|
assert.deepEqual(msg.payload.invite_node_ids, ['node_a', 'node_b']);
|
|
});
|
|
|
|
it('throws on missing title', () => {
|
|
assert.throws(() => handler.createSession({}), /title/);
|
|
});
|
|
|
|
it('joins a session', () => {
|
|
const result = handler.joinSession({ sessionId: 'sess_123' });
|
|
assert.ok(result.message_id);
|
|
const msg = store.getById(result.message_id);
|
|
assert.equal(msg.type, 'session_join');
|
|
assert.equal(msg.payload.session_id, 'sess_123');
|
|
});
|
|
|
|
it('throws on join without sessionId', () => {
|
|
assert.throws(() => handler.joinSession({}), /sessionId/);
|
|
});
|
|
|
|
it('leaves a session', () => {
|
|
const result = handler.leaveSession({ sessionId: 'sess_456' });
|
|
assert.ok(result.message_id);
|
|
const msg = store.getById(result.message_id);
|
|
assert.equal(msg.type, 'session_leave');
|
|
});
|
|
|
|
it('sends a message to a session', () => {
|
|
const result = handler.sendMessage({
|
|
sessionId: 'sess_789',
|
|
toNodeId: 'node_c',
|
|
msgType: 'context_update',
|
|
payload: { key: 'value' },
|
|
});
|
|
assert.ok(result.message_id);
|
|
const msg = store.getById(result.message_id);
|
|
assert.equal(msg.type, 'session_message');
|
|
assert.equal(msg.payload.session_id, 'sess_789');
|
|
assert.equal(msg.payload.to_node_id, 'node_c');
|
|
});
|
|
|
|
it('throws on send message with oversized payload', () => {
|
|
const bigPayload = { data: 'x'.repeat(17000) };
|
|
assert.throws(() => handler.sendMessage({
|
|
sessionId: 'sess_big',
|
|
payload: bigPayload,
|
|
}), /too large/);
|
|
});
|
|
|
|
it('delegates a subtask', () => {
|
|
const result = handler.delegateSubtask({
|
|
sessionId: 'sess_del',
|
|
toNodeId: 'node_worker',
|
|
title: 'Implement feature X',
|
|
role: 'builder',
|
|
});
|
|
assert.ok(result.message_id);
|
|
const msg = store.getById(result.message_id);
|
|
assert.equal(msg.type, 'session_delegate');
|
|
assert.equal(msg.payload.role, 'builder');
|
|
assert.equal(msg.payload.title, 'Implement feature X');
|
|
});
|
|
|
|
it('normalizes invalid role to builder', () => {
|
|
const result = handler.delegateSubtask({
|
|
sessionId: 'sess_role',
|
|
title: 'Fix bug',
|
|
role: 'invalid_role',
|
|
});
|
|
const msg = store.getById(result.message_id);
|
|
assert.equal(msg.payload.role, 'builder');
|
|
});
|
|
|
|
it('submits a result', () => {
|
|
const result = handler.submitResult({
|
|
sessionId: 'sess_sub',
|
|
taskId: 'task_1',
|
|
resultAssetId: 'asset_1',
|
|
summary: 'Completed the implementation',
|
|
});
|
|
assert.ok(result.message_id);
|
|
const msg = store.getById(result.message_id);
|
|
assert.equal(msg.type, 'session_submit');
|
|
assert.equal(msg.payload.task_id, 'task_1');
|
|
});
|
|
|
|
it('polls session invites', () => {
|
|
store.writeInbound({ type: 'collaboration_invite', payload: { session_id: 's1' } });
|
|
const msgs = handler.pollInvites();
|
|
assert.ok(msgs.length >= 1);
|
|
});
|
|
|
|
it('lists active sessions', () => {
|
|
const sessions = handler.listActiveSessions();
|
|
assert.ok(Array.isArray(sessions));
|
|
assert.ok(sessions.length > 0);
|
|
});
|
|
});
|