moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,432 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { MindDB } from '@waggle/core';
import {
buildWorkspaceState,
formatWorkspaceStatePrompt,
computeFreshness,
type WorkspaceState,
} from '../../src/local/workspace-state.js';
describe('workspace-state', () => {
let tmpDir: string;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-ws-state-'));
});
afterEach(() => {
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* */ }
});
function setupWorkspace(id: string, opts?: {
frames?: Array<{ content: string; importance: string; created_at?: string }>;
sessions?: Array<{
title: string;
messages: Array<{ role: string; content: string }>;
ageMs?: number;
}>;
awareness?: Array<{ category: string; content: string; priority: number }>;
}) {
const wsDir = path.join(tmpDir, 'workspaces', id);
fs.mkdirSync(wsDir, { recursive: true });
const mindPath = path.join(wsDir, 'workspace.mind');
if (opts?.frames && opts.frames.length > 0) {
const db = new MindDB(mindPath);
const raw = db.getDatabase();
raw.prepare(
`INSERT INTO sessions (gop_id, status, started_at)
VALUES ('session:test', 'active', datetime('now'))`,
).run();
for (const frame of opts.frames) {
const createdAt = frame.created_at ?? new Date().toISOString();
raw.prepare(
`INSERT INTO memory_frames (gop_id, frame_type, content, importance, created_at)
VALUES ('session:test', 'I', ?, ?, ?)`,
).run(frame.content, frame.importance, createdAt);
}
if (opts?.awareness) {
for (const item of opts.awareness) {
raw.prepare(
`INSERT INTO awareness (category, content, priority, created_at)
VALUES (?, ?, ?, datetime('now'))`,
).run(item.category, item.content, item.priority);
}
}
db.close();
}
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 created = new Date(Date.now() - (sess.ageMs ?? 0)).toISOString();
const lines: string[] = [
JSON.stringify({ type: 'meta', title: sess.title, created }),
];
for (const msg of sess.messages) {
lines.push(JSON.stringify({ role: msg.role, content: msg.content }));
}
const filePath = path.join(sessDir, `${sessionId}.jsonl`);
fs.writeFileSync(filePath, lines.join('\n') + '\n');
if (sess.ageMs) {
const pastTime = new Date(Date.now() - sess.ageMs);
fs.utimesSync(filePath, pastTime, pastTime);
}
}
}
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;
// ── computeFreshness ──────────────────────────────────────────
describe('computeFreshness', () => {
it('returns fresh for today', () => {
expect(computeFreshness(new Date().toISOString())).toBe('fresh');
});
it('returns fresh for 1 day ago', () => {
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
expect(computeFreshness(yesterday)).toBe('fresh');
});
it('returns aging for 3 days ago', () => {
const threeDays = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString();
expect(computeFreshness(threeDays)).toBe('aging');
});
it('returns stale for 10 days ago', () => {
const tenDays = new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString();
expect(computeFreshness(tenDays)).toBe('stale');
});
});
// ── buildWorkspaceState ───────────────────────────────────────
describe('buildWorkspaceState', () => {
it('returns null for non-existent workspace', () => {
const result = buildWorkspaceState({
dataDir: tmpDir,
workspaceId: 'nonexistent',
wsManager: makeManager(new Map()),
activateWorkspaceMind: noopActivate,
});
expect(result).toBeNull();
});
it('returns null for workspace with no mind file', () => {
const wsDir = path.join(tmpDir, 'workspaces', 'empty');
fs.mkdirSync(wsDir, { recursive: true });
const result = buildWorkspaceState({
dataDir: tmpDir,
workspaceId: 'empty',
wsManager: makeManager(new Map([['empty', { id: 'empty', name: 'Empty' }]])),
activateWorkspaceMind: noopActivate,
});
expect(result).toBeNull();
});
it('returns null for workspace with zero memory frames', () => {
fs.mkdirSync(path.join(tmpDir, 'workspaces', 'noframes'), { recursive: true });
const db = new MindDB(path.join(tmpDir, 'workspaces', 'noframes', 'workspace.mind'));
db.close();
const result = buildWorkspaceState({
dataDir: tmpDir,
workspaceId: 'noframes',
wsManager: makeManager(new Map([['noframes', { id: 'noframes', name: 'No Frames' }]])),
activateWorkspaceMind: noopActivate,
});
expect(result).toBeNull();
});
it('returns state with decisions from memory frames', () => {
setupWorkspace('ws1', {
frames: [
{ content: 'Decision: Use SQLite for local storage', importance: 'critical' },
{ content: 'Working on the authentication module', importance: 'normal' },
],
});
const result = buildWorkspaceState({
dataDir: tmpDir,
workspaceId: 'ws1',
wsManager: makeManager(new Map([['ws1', { id: 'ws1', name: 'Project Alpha' }]])),
activateWorkspaceMind: noopActivate,
});
expect(result).not.toBeNull();
expect(result!.recentDecisions.length).toBeGreaterThanOrEqual(1);
expect(result!.recentDecisions[0].content).toContain('SQLite');
expect(result!.recentDecisions[0].source).toBe('memory');
});
it('extracts pending tasks from sessions', () => {
setupWorkspace('ws2', {
frames: [{ content: 'Project context established', importance: 'normal' }],
sessions: [{
title: 'Planning',
messages: [
{ role: 'user', content: 'We need to implement the rate limiting middleware for the API' },
{ role: 'assistant', content: 'I will add that to the plan.' },
{ role: 'user', content: 'Also, we should add comprehensive error handling to the service layer' },
],
}],
});
const result = buildWorkspaceState({
dataDir: tmpDir,
workspaceId: 'ws2',
wsManager: makeManager(new Map([['ws2', { id: 'ws2', name: 'API Project' }]])),
activateWorkspaceMind: noopActivate,
});
expect(result).not.toBeNull();
// pending items come from TASK_PATTERNS in extractProgressItems
// "need to" triggers task pattern
expect(result!.pending.length).toBeGreaterThanOrEqual(1);
});
it('extracts open questions from sessions', () => {
setupWorkspace('ws3', {
frames: [{ content: 'Working on infrastructure', importance: 'normal' }],
sessions: [{
title: 'Infra discussion',
messages: [
{ role: 'user', content: 'Should we use containers or bare metal for the deployment?' },
{ role: 'assistant', content: 'Both have tradeoffs.' },
{ role: 'user', content: 'We still need to decide on the hosting provider strategy.' },
],
}],
});
const result = buildWorkspaceState({
dataDir: tmpDir,
workspaceId: 'ws3',
wsManager: makeManager(new Map([['ws3', { id: 'ws3', name: 'Infra' }]])),
activateWorkspaceMind: noopActivate,
});
expect(result).not.toBeNull();
expect(result!.openQuestions.length).toBeGreaterThanOrEqual(1);
});
it('classifies fresh and stale threads', () => {
const tenDays = 10 * 24 * 60 * 60 * 1000;
setupWorkspace('ws4', {
frames: [{ content: 'Multi-thread workspace', importance: 'normal' }],
sessions: [
{
title: 'Fresh thread',
messages: [
{ role: 'user', content: 'Working on fresh stuff right now.' },
{ role: 'assistant', content: 'On it.' },
],
},
{
title: 'Old thread',
messages: [
{ role: 'user', content: 'This was from ten days ago.' },
{ role: 'assistant', content: 'Noted.' },
],
ageMs: tenDays,
},
],
});
const result = buildWorkspaceState({
dataDir: tmpDir,
workspaceId: 'ws4',
wsManager: makeManager(new Map([['ws4', { id: 'ws4', name: 'Multi' }]])),
activateWorkspaceMind: noopActivate,
});
expect(result).not.toBeNull();
// Fresh session should appear in active
expect(result!.active.some(a => a.content === 'Fresh thread')).toBe(true);
// Stale session should appear in stale
expect(result!.stale.some(s => s.content === 'Old thread')).toBe(true);
});
it('derives nextActions from blockers and pending items', () => {
setupWorkspace('ws5', {
frames: [{ content: 'Active project', importance: 'normal' }],
sessions: [{
title: 'Work session',
messages: [
{ role: 'user', content: 'We are blocked by the missing SSL certificate for the staging server' },
{ role: 'assistant', content: 'That is a critical blocker.' },
{ role: 'user', content: 'We need to update the CI pipeline configuration as well' },
],
}],
});
const result = buildWorkspaceState({
dataDir: tmpDir,
workspaceId: 'ws5',
wsManager: makeManager(new Map([['ws5', { id: 'ws5', name: 'Blocked' }]])),
activateWorkspaceMind: noopActivate,
});
expect(result).not.toBeNull();
expect(result!.nextActions.length).toBeGreaterThanOrEqual(1);
// Blockers should appear first in nextActions
if (result!.blocked.length > 0) {
expect(result!.nextActions[0]).toContain('Resolve:');
}
});
it('returns all expected fields', () => {
setupWorkspace('ws6', {
frames: [
{ content: 'Decision: Use React for frontend', importance: 'critical' },
{ content: 'Project initialized', importance: 'normal' },
],
sessions: [{
title: 'Setup session',
messages: [
{ role: 'user', content: 'Let us set up the project structure.' },
{ role: 'assistant', content: 'I will create the scaffold.' },
],
}],
});
const result = buildWorkspaceState({
dataDir: tmpDir,
workspaceId: 'ws6',
wsManager: makeManager(new Map([['ws6', { id: 'ws6', name: 'Full' }]])),
activateWorkspaceMind: noopActivate,
});
expect(result).not.toBeNull();
expect(result).toHaveProperty('active');
expect(result).toHaveProperty('openQuestions');
expect(result).toHaveProperty('pending');
expect(result).toHaveProperty('blocked');
expect(result).toHaveProperty('completed');
expect(result).toHaveProperty('stale');
expect(result).toHaveProperty('recentDecisions');
expect(result).toHaveProperty('nextActions');
expect(Array.isArray(result!.active)).toBe(true);
expect(Array.isArray(result!.nextActions)).toBe(true);
});
});
// ── formatWorkspaceStatePrompt ────────────────────────────────
describe('formatWorkspaceStatePrompt', () => {
it('formats empty state with just the header', () => {
const state: WorkspaceState = {
active: [],
openQuestions: [],
pending: [],
blocked: [],
completed: [],
stale: [],
recentDecisions: [],
nextActions: [],
};
const result = formatWorkspaceStatePrompt(state, 'Test Workspace');
expect(result).toContain('# Workspace Now — Test Workspace');
expect(result).not.toContain('## Active');
});
it('includes Active section when there are active items', () => {
const state: WorkspaceState = {
active: [{
content: 'Working on auth',
freshness: 'fresh',
source: 'session',
dateLastTouched: '2026-03-14',
}],
openQuestions: [],
pending: [],
blocked: [],
completed: [],
stale: [],
recentDecisions: [],
nextActions: [],
};
const result = formatWorkspaceStatePrompt(state, 'Auth Project');
expect(result).toContain('## Active');
expect(result).toContain('Working on auth');
});
it('annotates aging items', () => {
const state: WorkspaceState = {
active: [{
content: 'Database migration',
freshness: 'aging',
source: 'session',
dateLastTouched: '2026-03-10',
}],
openQuestions: [],
pending: [],
blocked: [],
completed: [],
stale: [],
recentDecisions: [],
nextActions: [],
};
const result = formatWorkspaceStatePrompt(state, 'DB');
expect(result).toContain('(aging)');
});
it('includes all populated sections', () => {
const state: WorkspaceState = {
active: [{ content: 'Thread A', freshness: 'fresh', source: 'session', dateLastTouched: '2026-03-14' }],
openQuestions: [{ content: 'Which DB?', freshness: 'fresh', source: 'session', dateLastTouched: '2026-03-14' }],
pending: [{ content: 'Add tests', freshness: 'fresh', source: 'session', dateLastTouched: '2026-03-14' }],
blocked: [{ content: 'Missing API key', freshness: 'fresh', source: 'session', dateLastTouched: '2026-03-14' }],
completed: [{ content: 'Setup done', freshness: 'fresh', source: 'session', dateLastTouched: '2026-03-14' }],
stale: [{ content: 'Old thread', freshness: 'stale', source: 'session', dateLastTouched: '2026-03-01' }],
recentDecisions: [{ content: 'Use React', freshness: 'fresh', source: 'memory', dateLastTouched: '2026-03-14' }],
nextActions: ['Resolve: Missing API key', 'Add tests'],
};
const result = formatWorkspaceStatePrompt(state, 'Full');
expect(result).toContain('## Active');
expect(result).toContain('## Open Questions');
expect(result).toContain('## Blocked');
expect(result).toContain('## Pending');
expect(result).toContain('## Completed');
expect(result).toContain('## Needs Attention (stale)');
expect(result).toContain('## Recent Decisions');
expect(result).toContain('## Likely Next Actions');
});
it('marks stale items as not touched recently', () => {
const state: WorkspaceState = {
active: [],
openQuestions: [],
pending: [],
blocked: [],
completed: [],
stale: [{ content: 'Forgotten thread', freshness: 'stale', source: 'session', dateLastTouched: '2026-02-01' }],
recentDecisions: [],
nextActions: [],
};
const result = formatWorkspaceStatePrompt(state, 'Stale');
expect(result).toContain('not touched recently');
});
});
});