Files
waggle-os/packages/server/tests/routes/workspace-context.test.ts
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

303 lines
11 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { MindDB } from '@waggle/core';
import { buildWorkspaceNowBlock, formatWorkspaceNowPrompt, type WorkspaceNowBlock } from '../../src/local/routes/workspace-context.js';
describe('workspace-context', () => {
let tmpDir: string;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-ctx-test-'));
});
afterEach(() => {
// On Windows, SQLite WAL files may keep handles open briefly; best-effort cleanup
try {
fs.rmSync(tmpDir, { recursive: true, force: true });
} catch {
// Non-critical — OS will clean temp dir
}
});
// Helper: create a workspace directory structure with a mind DB
function setupWorkspace(id: string, opts?: {
frames?: Array<{ content: string; importance: string }>;
sessions?: Array<{ title: string; messages: Array<{ role: string; content: string }> }>;
}) {
const wsDir = path.join(tmpDir, 'workspaces', id);
fs.mkdirSync(wsDir, { recursive: true });
// Write workspace.json
fs.writeFileSync(
path.join(wsDir, 'workspace.json'),
JSON.stringify({ id, name: `Test Workspace ${id}`, group: 'test', created: new Date().toISOString() }),
);
// Create mind DB with frames
const mindPath = path.join(wsDir, 'workspace.mind');
if (opts?.frames && opts.frames.length > 0) {
const db = new MindDB(mindPath);
const raw = db.getDatabase();
// Insert a session first (frames require a session via gop_id FK)
raw.prepare(
`INSERT INTO sessions (gop_id, status, started_at)
VALUES ('session:test', 'active', datetime('now'))`
).run();
for (const frame of opts.frames) {
raw.prepare(
`INSERT INTO memory_frames (gop_id, frame_type, content, importance, created_at)
VALUES ('session:test', 'I', ?, ?, datetime('now'))`
).run(frame.content, frame.importance);
}
db.close();
}
// Create session files
if (opts?.sessions) {
const sessDir = path.join(wsDir, 'sessions');
fs.mkdirSync(sessDir, { recursive: true });
for (let i = 0; i < opts.sessions.length; i++) {
const sess = opts.sessions[i];
const sessionId = `session-${i}`;
const lines: string[] = [
JSON.stringify({ type: 'meta', title: sess.title, created: new Date().toISOString() }),
];
for (const msg of sess.messages) {
lines.push(JSON.stringify({ role: msg.role, content: msg.content }));
}
fs.writeFileSync(path.join(sessDir, `${sessionId}.jsonl`), lines.join('\n') + '\n');
}
}
return { mindPath, wsDir };
}
function makeManager(workspaces: Map<string, { id: string; name: string }>) {
return {
get: (id: string) => workspaces.get(id) ?? null,
getMindPath: (id: string) => path.join(tmpDir, 'workspaces', id, 'workspace.mind'),
};
}
const noopActivate = (_id: string) => true;
// ── Test 1: Returns null for non-existent workspace ──────────────
it('returns null for non-existent workspace', () => {
const wsManager = makeManager(new Map());
const result = buildWorkspaceNowBlock({
dataDir: tmpDir,
workspaceId: 'does-not-exist',
wsManager,
activateWorkspaceMind: noopActivate,
});
expect(result).toBeNull();
});
// ── Test 2: Returns null for workspace with no data ──────────────
it('returns null for workspace with no mind file', () => {
const wsDir = path.join(tmpDir, 'workspaces', 'empty-ws');
fs.mkdirSync(wsDir, { recursive: true });
const wsManager = makeManager(new Map([
['empty-ws', { id: 'empty-ws', name: 'Empty Workspace' }],
]));
const result = buildWorkspaceNowBlock({
dataDir: tmpDir,
workspaceId: 'empty-ws',
wsManager,
activateWorkspaceMind: noopActivate,
});
expect(result).toBeNull();
});
it('returns null for workspace with mind file but zero frames', () => {
const wsManager = makeManager(new Map([
['zero-ws', { id: 'zero-ws', name: 'Zero Frames' }],
]));
// Create a mind DB with no frames
setupWorkspace('zero-ws', { frames: [] });
// MindDB file exists but has no frames — we need the file to exist
const mindPath = path.join(tmpDir, 'workspaces', 'zero-ws', 'workspace.mind');
const db = new MindDB(mindPath);
db.close();
const result = buildWorkspaceNowBlock({
dataDir: tmpDir,
workspaceId: 'zero-ws',
wsManager,
activateWorkspaceMind: noopActivate,
});
expect(result).toBeNull();
});
// ── Test 3: Returns correctly shaped block ───────────────────────
it('returns correctly shaped block for workspace with data', () => {
const wsManager = makeManager(new Map([
['rich-ws', { id: 'rich-ws', name: 'Rich Workspace' }],
]));
setupWorkspace('rich-ws', {
frames: [
{ content: 'Working on the Waggle project, a workspace-native AI agent platform.', importance: 'important' },
{ content: 'Decision: Use SQLite for local storage instead of PostgreSQL.', importance: 'critical' },
{ content: 'Implemented the memory frame system with FTS5 search.', importance: 'normal' },
],
sessions: [
{
title: 'Planning session',
messages: [
{ role: 'user', content: 'Let us plan the architecture for the next milestone' },
{ role: 'assistant', content: 'I have reviewed the current state and here is my recommendation.' },
],
},
],
});
const result = buildWorkspaceNowBlock({
dataDir: tmpDir,
workspaceId: 'rich-ws',
wsManager,
activateWorkspaceMind: noopActivate,
});
expect(result).not.toBeNull();
expect(result!.workspaceName).toBe('Rich Workspace');
expect(result!.summary).toBeTruthy();
expect(result!.summary.length).toBeGreaterThan(10);
expect(Array.isArray(result!.recentDecisions)).toBe(true);
expect(Array.isArray(result!.activeThreads)).toBe(true);
expect(Array.isArray(result!.progressItems)).toBe(true);
expect(Array.isArray(result!.nextActions)).toBe(true);
// Should have at least one thread from the session
expect(result!.activeThreads.length).toBeGreaterThanOrEqual(1);
// Should have at least one decision (the critical frame)
expect(result!.recentDecisions.length).toBeGreaterThanOrEqual(1);
});
// ── Test 4: Caps at correct limits ───────────────────────────────
it('caps decisions at 3, threads at 3, progress at 5', () => {
const wsManager = makeManager(new Map([
['capped-ws', { id: 'capped-ws', name: 'Capped Workspace' }],
]));
// Create many decisions and sessions
const frames = [];
for (let i = 0; i < 10; i++) {
frames.push({ content: `Decision: chose option ${i} for the architecture component ${i}`, importance: 'critical' as const });
}
const sessions = [];
for (let i = 0; i < 8; i++) {
sessions.push({
title: `Thread ${i}: discussing component ${i}`,
messages: [
{ role: 'user', content: `Tell me about component ${i}` },
{ role: 'assistant', content: `Here is info about component ${i}` },
],
});
}
setupWorkspace('capped-ws', { frames, sessions });
const result = buildWorkspaceNowBlock({
dataDir: tmpDir,
workspaceId: 'capped-ws',
wsManager,
activateWorkspaceMind: noopActivate,
});
expect(result).not.toBeNull();
expect(result!.recentDecisions.length).toBeLessThanOrEqual(3);
expect(result!.activeThreads.length).toBeLessThanOrEqual(3);
expect(result!.progressItems.length).toBeLessThanOrEqual(5);
expect(result!.nextActions.length).toBeLessThanOrEqual(3);
});
// ── Test 5: formatWorkspaceNowPrompt produces clean markdown ─────
describe('formatWorkspaceNowPrompt', () => {
it('produces clean markdown with all sections', () => {
const block: WorkspaceNowBlock = {
workspaceName: 'My Project',
summary: 'Working on an AI agent platform. Active today with 42 memories across 5 sessions.',
recentDecisions: ['Use SQLite for local storage', 'Deploy via Tauri'],
activeThreads: ['Architecture planning (2h ago)', 'Memory model design (yesterday)'],
progressItems: ['[blocker] Waiting for API key', '[task] Implement search', '[completed] Setup CI'],
nextActions: ['Resolve: Waiting for API key', 'Implement search'],
};
const result = formatWorkspaceNowPrompt(block);
expect(result).toContain('# Workspace Now — My Project');
expect(result).toContain('Working on an AI agent platform');
expect(result).toContain('## Recent Decisions');
expect(result).toContain('- Use SQLite for local storage');
expect(result).toContain('- Deploy via Tauri');
expect(result).toContain('## Active Threads');
expect(result).toContain('- Architecture planning (2h ago)');
expect(result).toContain('## Progress');
expect(result).toContain('- [blocker] Waiting for API key');
expect(result).toContain('## Likely Next Actions');
expect(result).toContain('- Resolve: Waiting for API key');
// No trailing whitespace
expect(result).toBe(result.trimEnd());
});
it('omits empty sections', () => {
const block: WorkspaceNowBlock = {
workspaceName: 'Minimal',
summary: 'A minimal workspace.',
recentDecisions: [],
activeThreads: [],
progressItems: [],
nextActions: [],
};
const result = formatWorkspaceNowPrompt(block);
expect(result).toContain('# Workspace Now — Minimal');
expect(result).toContain('A minimal workspace.');
expect(result).not.toContain('## Recent Decisions');
expect(result).not.toContain('## Active Threads');
expect(result).not.toContain('## Progress');
expect(result).not.toContain('## Likely Next Actions');
});
it('omits summary section if empty', () => {
const block: WorkspaceNowBlock = {
workspaceName: 'No Summary',
summary: '',
recentDecisions: ['Some decision'],
activeThreads: [],
progressItems: [],
nextActions: [],
};
const result = formatWorkspaceNowPrompt(block);
expect(result).toContain('# Workspace Now — No Summary');
expect(result).toContain('## Recent Decisions');
// Should not have blank lines between heading and decisions (no empty summary block)
const lines = result.split('\n');
const headingIdx = lines.findIndex(l => l.startsWith('# Workspace Now'));
const decisionsIdx = lines.findIndex(l => l === '## Recent Decisions');
// There should be only one blank line between heading and decisions section
expect(decisionsIdx - headingIdx).toBeLessThanOrEqual(2);
});
});
});