This commit is contained in:
272
packages/server/tests/proactive-handlers.test.ts
Normal file
272
packages/server/tests/proactive-handlers.test.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import { MindDB, WorkspaceManager, AwarenessLayer } from '@waggle/core';
|
||||
import {
|
||||
generateMorningBriefing,
|
||||
checkStaleWorkspaces,
|
||||
checkPendingTasks,
|
||||
suggestCapabilities,
|
||||
type ProactiveContext,
|
||||
} from '../src/local/proactive-handlers.js';
|
||||
|
||||
describe('Proactive Handlers', () => {
|
||||
let tmpDir: string;
|
||||
let wsManager: WorkspaceManager;
|
||||
const mindDbs = new Map<string, MindDB>();
|
||||
|
||||
function makeContext(): ProactiveContext {
|
||||
return {
|
||||
dataDir: tmpDir,
|
||||
workspaceManager: wsManager,
|
||||
getWorkspaceMindDb: (workspaceId: string) => {
|
||||
const existing = mindDbs.get(workspaceId);
|
||||
if (existing) return existing;
|
||||
const mindPath = path.join(tmpDir, 'workspaces', workspaceId, 'workspace.mind');
|
||||
if (!fs.existsSync(mindPath)) return null;
|
||||
try {
|
||||
const db = new MindDB(mindPath);
|
||||
mindDbs.set(workspaceId, db);
|
||||
return db;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-proactive-'));
|
||||
wsManager = new WorkspaceManager(tmpDir);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const [, db] of mindDbs) {
|
||||
try { db.close(); } catch { /* already closed */ }
|
||||
}
|
||||
mindDbs.clear();
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// ── Morning Briefing ─────────────────────────────────────────────
|
||||
|
||||
describe('generateMorningBriefing', () => {
|
||||
it('returns null when no workspaces exist', () => {
|
||||
const ctx = makeContext();
|
||||
const result = generateMorningBriefing(ctx);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when no pending items and no stale workspaces', () => {
|
||||
// Create a workspace with a recent session file
|
||||
const ws = wsManager.create({ name: 'Active Project', group: 'Work' });
|
||||
const sessionsDir = path.join(tmpDir, 'workspaces', ws.id, 'sessions');
|
||||
fs.mkdirSync(sessionsDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(sessionsDir, '2026-03-19.jsonl'), '{"role":"user","content":"hello"}\n');
|
||||
|
||||
const ctx = makeContext();
|
||||
const result = generateMorningBriefing(ctx);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('includes workspace summaries when pending items exist', () => {
|
||||
const ws = wsManager.create({ name: 'My Research', group: 'Work' });
|
||||
|
||||
// Initialize the mind DB (write actual data so MindDB opens it properly)
|
||||
const mindPath = path.join(tmpDir, 'workspaces', ws.id, 'workspace.mind');
|
||||
const db = new MindDB(mindPath);
|
||||
const awareness = new AwarenessLayer(db);
|
||||
awareness.add('task', 'Review paper draft', 1);
|
||||
awareness.add('pending', 'Waiting for feedback', 0);
|
||||
mindDbs.set(ws.id, db);
|
||||
|
||||
const ctx = makeContext();
|
||||
const result = generateMorningBriefing(ctx);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.type).toBe('morning_briefing');
|
||||
expect(result!.title).toContain('morning');
|
||||
expect(result!.body).toContain('2 pending');
|
||||
expect(result!.body).toContain('My Research');
|
||||
});
|
||||
|
||||
it('reports stale workspaces in briefing', () => {
|
||||
const ws = wsManager.create({ name: 'Old Project', group: 'Archive' });
|
||||
|
||||
// Create a session file with old modification time
|
||||
const sessionsDir = path.join(tmpDir, 'workspaces', ws.id, 'sessions');
|
||||
fs.mkdirSync(sessionsDir, { recursive: true });
|
||||
const sessionFile = path.join(sessionsDir, '2026-02-01.jsonl');
|
||||
fs.writeFileSync(sessionFile, '{"role":"user","content":"test"}\n');
|
||||
// Set mtime to 30 days ago
|
||||
const oldTime = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
||||
fs.utimesSync(sessionFile, oldTime, oldTime);
|
||||
|
||||
const ctx = makeContext();
|
||||
const result = generateMorningBriefing(ctx);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.body).toContain('not visited in 14+ days');
|
||||
});
|
||||
});
|
||||
|
||||
// ── Stale Workspace Check ────────────────────────────────────────
|
||||
|
||||
describe('checkStaleWorkspaces', () => {
|
||||
it('returns empty array when no workspaces', () => {
|
||||
const ctx = makeContext();
|
||||
const result = checkStaleWorkspaces(ctx);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('detects stale workspaces based on session activity', () => {
|
||||
const ws = wsManager.create({ name: 'Forgotten Project', group: 'Old' });
|
||||
|
||||
// Create a session file with old modification time
|
||||
const sessionsDir = path.join(tmpDir, 'workspaces', ws.id, 'sessions');
|
||||
fs.mkdirSync(sessionsDir, { recursive: true });
|
||||
const sessionFile = path.join(sessionsDir, '2026-01-15.jsonl');
|
||||
fs.writeFileSync(sessionFile, '{"role":"user","content":"test"}\n');
|
||||
const oldTime = new Date(Date.now() - 20 * 24 * 60 * 60 * 1000);
|
||||
fs.utimesSync(sessionFile, oldTime, oldTime);
|
||||
|
||||
const ctx = makeContext();
|
||||
const result = checkStaleWorkspaces(ctx);
|
||||
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].type).toBe('stale_workspace');
|
||||
expect(result[0].title).toContain('Forgotten Project');
|
||||
expect(result[0].workspaceId).toBe(ws.id);
|
||||
});
|
||||
|
||||
it('skips recently active workspaces', () => {
|
||||
const ws = wsManager.create({ name: 'Active Project', group: 'Work' });
|
||||
const sessionsDir = path.join(tmpDir, 'workspaces', ws.id, 'sessions');
|
||||
fs.mkdirSync(sessionsDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(sessionsDir, '2026-03-19.jsonl'), '{"role":"user","content":"hello"}\n');
|
||||
|
||||
const ctx = makeContext();
|
||||
const result = checkStaleWorkspaces(ctx);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('uses creation date when no sessions exist', () => {
|
||||
// Create workspace with backdated creation
|
||||
const ws = wsManager.create({ name: 'Empty Old Workspace', group: 'Test' });
|
||||
// Backdate the workspace config
|
||||
const configPath = path.join(tmpDir, 'workspaces', ws.id, 'workspace.json');
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
||||
config.created = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
||||
|
||||
const ctx = makeContext();
|
||||
const result = checkStaleWorkspaces(ctx);
|
||||
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].title).toContain('Empty Old Workspace');
|
||||
});
|
||||
});
|
||||
|
||||
// ── Task Reminder ────────────────────────────────────────────────
|
||||
|
||||
describe('checkPendingTasks', () => {
|
||||
it('returns empty array when no workspaces', () => {
|
||||
const ctx = makeContext();
|
||||
const result = checkPendingTasks(ctx);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('aggregates pending tasks across workspaces', () => {
|
||||
const ws1 = wsManager.create({ name: 'Project A', group: 'Work' });
|
||||
const ws2 = wsManager.create({ name: 'Project B', group: 'Work' });
|
||||
|
||||
// Add awareness items to ws1
|
||||
const mindPath1 = path.join(tmpDir, 'workspaces', ws1.id, 'workspace.mind');
|
||||
const db1 = new MindDB(mindPath1);
|
||||
const awareness1 = new AwarenessLayer(db1);
|
||||
awareness1.add('task', 'Fix bug #123', 1);
|
||||
awareness1.add('task', 'Write tests', 0);
|
||||
mindDbs.set(ws1.id, db1);
|
||||
|
||||
// Add awareness items to ws2
|
||||
const mindPath2 = path.join(tmpDir, 'workspaces', ws2.id, 'workspace.mind');
|
||||
const db2 = new MindDB(mindPath2);
|
||||
const awareness2 = new AwarenessLayer(db2);
|
||||
awareness2.add('pending', 'Waiting for review', 0);
|
||||
mindDbs.set(ws2.id, db2);
|
||||
|
||||
const ctx = makeContext();
|
||||
const result = checkPendingTasks(ctx);
|
||||
|
||||
expect(result.length).toBe(2);
|
||||
const projectA = result.find(r => r.workspaceId === ws1.id);
|
||||
const projectB = result.find(r => r.workspaceId === ws2.id);
|
||||
expect(projectA).toBeDefined();
|
||||
expect(projectA!.title).toContain('2 pending');
|
||||
expect(projectB).toBeDefined();
|
||||
expect(projectB!.title).toContain('1 pending');
|
||||
});
|
||||
|
||||
it('skips workspaces with no pending items', () => {
|
||||
wsManager.create({ name: 'Clean Workspace', group: 'Work' });
|
||||
|
||||
const ctx = makeContext();
|
||||
const result = checkPendingTasks(ctx);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('sets high priority when many pending items', () => {
|
||||
const ws = wsManager.create({ name: 'Busy Project', group: 'Work' });
|
||||
const mindPath = path.join(tmpDir, 'workspaces', ws.id, 'workspace.mind');
|
||||
const db = new MindDB(mindPath);
|
||||
const awareness = new AwarenessLayer(db);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
awareness.add('task', `Task ${i}`, 1);
|
||||
}
|
||||
mindDbs.set(ws.id, db);
|
||||
|
||||
const ctx = makeContext();
|
||||
const result = checkPendingTasks(ctx);
|
||||
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].priority).toBe('high');
|
||||
});
|
||||
});
|
||||
|
||||
// ── Capability Suggestion ────────────────────────────────────────
|
||||
|
||||
describe('suggestCapabilities', () => {
|
||||
it('returns null when no workspaces and capabilities installed', () => {
|
||||
// Create a fake skills directory to simulate installed capabilities
|
||||
fs.mkdirSync(path.join(tmpDir, 'skills', 'some-skill'), { recursive: true });
|
||||
const ctx = makeContext();
|
||||
const result = suggestCapabilities(ctx);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('suggests capability packs when none are installed', () => {
|
||||
const ctx = makeContext();
|
||||
const result = suggestCapabilities(ctx);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.type).toBe('capability_suggestion');
|
||||
expect(result!.title).toContain('capability packs');
|
||||
expect(result!.actionUrl).toBe('/skills');
|
||||
});
|
||||
|
||||
it('returns null when capabilities installed and low memory count', () => {
|
||||
// Create skills dir (simulates installed capabilities)
|
||||
fs.mkdirSync(path.join(tmpDir, 'skills', 'research-skill'), { recursive: true });
|
||||
// Create a workspace with few frames
|
||||
const ws = wsManager.create({ name: 'Small Project', group: 'Test' });
|
||||
const mindPath = path.join(tmpDir, 'workspaces', ws.id, 'workspace.mind');
|
||||
const db = new MindDB(mindPath);
|
||||
mindDbs.set(ws.id, db);
|
||||
|
||||
const ctx = makeContext();
|
||||
const result = suggestCapabilities(ctx);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user