This commit is contained in:
178
packages/server/tests/wave-h-continuity.test.ts
Normal file
178
packages/server/tests/wave-h-continuity.test.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Wave H — Memory / Continuity Surface Productization tests.
|
||||
*
|
||||
* Verifies:
|
||||
* 1. Freshness computation (fresh/aging/stale thresholds)
|
||||
* 2. Workspace state structure (categories, nextActions)
|
||||
* 3. Next actions priority cascade (blockers → questions → tasks → stale)
|
||||
* 4. State prompt formatting
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
computeFreshness,
|
||||
formatWorkspaceStatePrompt,
|
||||
type WorkspaceState,
|
||||
type StateItem,
|
||||
} from '../src/local/workspace-state.js';
|
||||
|
||||
function makeItem(content: string, freshness: 'fresh' | 'aging' | 'stale', source: 'memory' | 'session' | 'awareness' = 'session'): StateItem {
|
||||
const dayOffsets = { fresh: 0, aging: 4, stale: 10 };
|
||||
const date = new Date(Date.now() - dayOffsets[freshness] * 86400 * 1000);
|
||||
return { content, freshness, source, dateLastTouched: date.toISOString().slice(0, 10) };
|
||||
}
|
||||
|
||||
describe('Wave H: Continuity Surface', () => {
|
||||
describe('Freshness Computation', () => {
|
||||
it('returns fresh for dates within 2 days', () => {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
expect(computeFreshness(today)).toBe('fresh');
|
||||
});
|
||||
|
||||
it('returns fresh for yesterday', () => {
|
||||
const yesterday = new Date(Date.now() - 86400 * 1000).toISOString().slice(0, 10);
|
||||
expect(computeFreshness(yesterday)).toBe('fresh');
|
||||
});
|
||||
|
||||
it('returns aging for dates 2-7 days old', () => {
|
||||
const fourDaysAgo = new Date(Date.now() - 4 * 86400 * 1000).toISOString().slice(0, 10);
|
||||
expect(computeFreshness(fourDaysAgo)).toBe('aging');
|
||||
});
|
||||
|
||||
it('returns stale for dates older than 7 days', () => {
|
||||
const tenDaysAgo = new Date(Date.now() - 10 * 86400 * 1000).toISOString().slice(0, 10);
|
||||
expect(computeFreshness(tenDaysAgo)).toBe('stale');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Workspace State Structure', () => {
|
||||
it('WorkspaceState has all required categories', () => {
|
||||
const state: WorkspaceState = {
|
||||
active: [],
|
||||
openQuestions: [],
|
||||
pending: [],
|
||||
blocked: [],
|
||||
completed: [],
|
||||
stale: [],
|
||||
recentDecisions: [],
|
||||
nextActions: [],
|
||||
};
|
||||
expect(Object.keys(state)).toHaveLength(8);
|
||||
expect(state.nextActions).toBeDefined();
|
||||
});
|
||||
|
||||
it('nextActions are strings (derived, not StateItems)', () => {
|
||||
const state: WorkspaceState = {
|
||||
active: [],
|
||||
openQuestions: [],
|
||||
pending: [makeItem('Fix the bug', 'fresh')],
|
||||
blocked: [makeItem('Waiting for API key', 'fresh')],
|
||||
completed: [],
|
||||
stale: [],
|
||||
recentDecisions: [],
|
||||
nextActions: ['Resolve: Waiting for API key', 'Fix the bug'],
|
||||
};
|
||||
for (const action of state.nextActions) {
|
||||
expect(typeof action).toBe('string');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Next Actions Priority', () => {
|
||||
it('blockers come before tasks in nextActions', () => {
|
||||
const state: WorkspaceState = {
|
||||
active: [],
|
||||
openQuestions: [],
|
||||
pending: [makeItem('Write tests', 'fresh')],
|
||||
blocked: [makeItem('CI is broken', 'fresh')],
|
||||
completed: [],
|
||||
stale: [],
|
||||
recentDecisions: [],
|
||||
nextActions: ['Resolve: CI is broken', 'Write tests'],
|
||||
};
|
||||
expect(state.nextActions[0]).toMatch(/^Resolve:/);
|
||||
});
|
||||
|
||||
it('stale items get Resume: prefix', () => {
|
||||
const state: WorkspaceState = {
|
||||
active: [],
|
||||
openQuestions: [],
|
||||
pending: [],
|
||||
blocked: [],
|
||||
completed: [],
|
||||
stale: [makeItem('Old research thread', 'stale')],
|
||||
recentDecisions: [],
|
||||
nextActions: ['Resume: Old research thread'],
|
||||
};
|
||||
expect(state.nextActions[0]).toMatch(/^Resume:/);
|
||||
});
|
||||
|
||||
it('questions get Decide: prefix', () => {
|
||||
const state: WorkspaceState = {
|
||||
active: [],
|
||||
openQuestions: [makeItem('Which database to use?', 'fresh')],
|
||||
pending: [],
|
||||
blocked: [],
|
||||
completed: [],
|
||||
stale: [],
|
||||
recentDecisions: [],
|
||||
nextActions: ['Decide: Which database to use?'],
|
||||
};
|
||||
expect(state.nextActions[0]).toMatch(/^Decide:/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('State Prompt Formatting', () => {
|
||||
it('formats state into a readable prompt', () => {
|
||||
const state: WorkspaceState = {
|
||||
active: [makeItem('Working on auth', 'fresh')],
|
||||
openQuestions: [],
|
||||
pending: [makeItem('Deploy to staging', 'fresh')],
|
||||
blocked: [],
|
||||
completed: [makeItem('Fixed login bug', 'fresh')],
|
||||
stale: [],
|
||||
recentDecisions: [makeItem('Using JWT for auth', 'fresh')],
|
||||
nextActions: ['Deploy to staging'],
|
||||
};
|
||||
|
||||
const prompt = formatWorkspaceStatePrompt(state, 'Test Project');
|
||||
expect(prompt).toContain('Test Project');
|
||||
expect(prompt).toContain('Working on auth');
|
||||
expect(prompt).toContain('Deploy to staging');
|
||||
expect(prompt).toContain('Using JWT for auth');
|
||||
});
|
||||
|
||||
it('includes next actions section', () => {
|
||||
const state: WorkspaceState = {
|
||||
active: [],
|
||||
openQuestions: [],
|
||||
pending: [],
|
||||
blocked: [],
|
||||
completed: [],
|
||||
stale: [],
|
||||
recentDecisions: [],
|
||||
nextActions: ['Do the thing'],
|
||||
};
|
||||
const prompt = formatWorkspaceStatePrompt(state, 'WS');
|
||||
expect(prompt).toContain('Likely Next Actions');
|
||||
expect(prompt).toContain('Do the thing');
|
||||
});
|
||||
|
||||
it('omits empty sections', () => {
|
||||
const state: WorkspaceState = {
|
||||
active: [],
|
||||
openQuestions: [],
|
||||
pending: [],
|
||||
blocked: [],
|
||||
completed: [],
|
||||
stale: [],
|
||||
recentDecisions: [],
|
||||
nextActions: [],
|
||||
};
|
||||
const prompt = formatWorkspaceStatePrompt(state, 'Empty WS');
|
||||
expect(prompt).not.toContain('Active');
|
||||
expect(prompt).not.toContain('Blocked');
|
||||
expect(prompt).not.toContain('Next Actions');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user