This commit is contained in:
497
packages/server/tests/routes/session-state-extraction.test.ts
Normal file
497
packages/server/tests/routes/session-state-extraction.test.ts
Normal file
@@ -0,0 +1,497 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import {
|
||||
extractOpenQuestions,
|
||||
classifyThreads,
|
||||
extractSessionOutcome,
|
||||
persistSessionOutcome,
|
||||
type OpenQuestion,
|
||||
type ThreadInfo,
|
||||
type SessionOutcome,
|
||||
} from '../../src/local/routes/sessions.js';
|
||||
|
||||
describe('extractOpenQuestions', () => {
|
||||
let tmpDir: string;
|
||||
let sessionsDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-oq-test-'));
|
||||
sessionsDir = path.join(tmpDir, 'sessions');
|
||||
fs.mkdirSync(sessionsDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* */ }
|
||||
});
|
||||
|
||||
function writeSession(id: string, messages: Array<{ role: string; content: string }>) {
|
||||
const meta = JSON.stringify({ type: 'meta', title: `Session ${id}`, created: new Date().toISOString() });
|
||||
const lines = [meta, ...messages.map(m => JSON.stringify(m))];
|
||||
fs.writeFileSync(path.join(sessionsDir, `${id}.jsonl`), lines.join('\n') + '\n');
|
||||
}
|
||||
|
||||
it('returns empty for non-existent dir', () => {
|
||||
expect(extractOpenQuestions('/nonexistent/path')).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns empty for sessions with no questions', () => {
|
||||
writeSession('s1', [
|
||||
{ role: 'user', content: 'Hello, how are you today?' },
|
||||
{ role: 'assistant', content: 'I am doing well, thank you.' },
|
||||
{ role: 'user', content: 'Great, let us continue working.' },
|
||||
]);
|
||||
expect(extractOpenQuestions(sessionsDir)).toEqual([]);
|
||||
});
|
||||
|
||||
it('detects literal question-form open questions', () => {
|
||||
writeSession('s1', [
|
||||
{ role: 'user', content: 'What should we use for the database layer in this project?' },
|
||||
{ role: 'assistant', content: 'There are several options to consider.' },
|
||||
{ role: 'user', content: 'Should we go with PostgreSQL or SQLite for local storage?' },
|
||||
]);
|
||||
const result = extractOpenQuestions(sessionsDir);
|
||||
expect(result.length).toBeGreaterThanOrEqual(1);
|
||||
expect(result.some(q => q.content.toLowerCase().includes('database') || q.content.toLowerCase().includes('postgresql'))).toBe(true);
|
||||
});
|
||||
|
||||
it('detects "we still need to decide" pattern', () => {
|
||||
writeSession('s1', [
|
||||
{ role: 'user', content: 'We made progress on the API.' },
|
||||
{ role: 'assistant', content: 'Yes, but we still need to decide on the authentication approach for the API layer.' },
|
||||
{ role: 'user', content: 'Good point, let us think about it.' },
|
||||
]);
|
||||
const result = extractOpenQuestions(sessionsDir);
|
||||
expect(result.length).toBeGreaterThanOrEqual(1);
|
||||
expect(result.some(q => q.content.toLowerCase().includes('authentication'))).toBe(true);
|
||||
});
|
||||
|
||||
it('detects "not yet clear" pattern', () => {
|
||||
writeSession('s1', [
|
||||
{ role: 'assistant', content: 'The deployment strategy is not yet clear whether we should use containers or bare metal.' },
|
||||
{ role: 'user', content: 'Right, we need to figure that out.' },
|
||||
{ role: 'assistant', content: 'Let me research both options.' },
|
||||
]);
|
||||
const result = extractOpenQuestions(sessionsDir);
|
||||
expect(result.length).toBeGreaterThanOrEqual(1);
|
||||
expect(result.some(q => q.content.toLowerCase().includes('container') || q.content.toLowerCase().includes('deployment'))).toBe(true);
|
||||
});
|
||||
|
||||
it('detects "pending decision" and "TBD" patterns', () => {
|
||||
writeSession('s1', [
|
||||
{ role: 'user', content: 'The hosting provider is TBD: we are evaluating AWS vs GCP for this.' },
|
||||
{ role: 'assistant', content: 'There is also a pending decision on the CI pipeline tool selection.' },
|
||||
{ role: 'user', content: 'Yes, both are important.' },
|
||||
]);
|
||||
const result = extractOpenQuestions(sessionsDir);
|
||||
expect(result.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('detects "need to figure out" pattern', () => {
|
||||
writeSession('s1', [
|
||||
{ role: 'user', content: 'We need to figure out how to handle rate limiting in the proxy layer.' },
|
||||
{ role: 'assistant', content: 'That is an important consideration.' },
|
||||
{ role: 'user', content: 'Agreed.' },
|
||||
]);
|
||||
const result = extractOpenQuestions(sessionsDir);
|
||||
expect(result.length).toBeGreaterThanOrEqual(1);
|
||||
expect(result.some(q => q.content.toLowerCase().includes('rate limiting'))).toBe(true);
|
||||
});
|
||||
|
||||
it('excludes greeting-style questions', () => {
|
||||
writeSession('s1', [
|
||||
{ role: 'user', content: 'Hello there!' },
|
||||
{ role: 'assistant', content: 'Hi! How are you doing today?' },
|
||||
{ role: 'user', content: 'Good thanks.' },
|
||||
]);
|
||||
expect(extractOpenQuestions(sessionsDir)).toEqual([]);
|
||||
});
|
||||
|
||||
it('deduplicates similar questions across sessions', () => {
|
||||
writeSession('s1', [
|
||||
{ role: 'user', content: 'What should we use for the caching layer in this system?' },
|
||||
{ role: 'assistant', content: 'Good question.' },
|
||||
{ role: 'user', content: 'Let me think about it.' },
|
||||
]);
|
||||
writeSession('s2', [
|
||||
{ role: 'user', content: 'What should we use for the caching layer in this system?' },
|
||||
{ role: 'assistant', content: 'Redis or Memcached are popular choices.' },
|
||||
{ role: 'user', content: 'Thanks for the info.' },
|
||||
]);
|
||||
const result = extractOpenQuestions(sessionsDir);
|
||||
// Should deduplicate — only one entry for the same question
|
||||
const cachingQuestions = result.filter(q => q.content.toLowerCase().includes('caching'));
|
||||
expect(cachingQuestions.length).toBeLessThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('skips sessions with fewer than 3 lines', () => {
|
||||
const meta = JSON.stringify({ type: 'meta', title: 'Short', created: new Date().toISOString() });
|
||||
fs.writeFileSync(
|
||||
path.join(sessionsDir, 'short.jsonl'),
|
||||
meta + '\n' + JSON.stringify({ role: 'user', content: 'What should we do about the API design?' }) + '\n',
|
||||
);
|
||||
expect(extractOpenQuestions(sessionsDir)).toEqual([]);
|
||||
});
|
||||
|
||||
it('includes sessionId and date in results', () => {
|
||||
writeSession('test-session-42', [
|
||||
{ role: 'user', content: 'Should we use WebSocket or SSE for the streaming transport?' },
|
||||
{ role: 'assistant', content: 'Both have tradeoffs.' },
|
||||
{ role: 'user', content: 'Let me think.' },
|
||||
]);
|
||||
const result = extractOpenQuestions(sessionsDir);
|
||||
expect(result.length).toBeGreaterThanOrEqual(1);
|
||||
expect(result[0].sessionId).toBe('test-session-42');
|
||||
expect(result[0].date).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
||||
});
|
||||
|
||||
it('detects "remains open" and "left open" patterns', () => {
|
||||
writeSession('s1', [
|
||||
{ role: 'assistant', content: 'The question of whether to use monorepo or polyrepo remains open for the new project.' },
|
||||
{ role: 'user', content: 'Yes it does.' },
|
||||
{ role: 'assistant', content: 'We should revisit this.' },
|
||||
]);
|
||||
const result = extractOpenQuestions(sessionsDir);
|
||||
expect(result.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('classifyThreads', () => {
|
||||
let tmpDir: string;
|
||||
let sessionsDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-thread-test-'));
|
||||
sessionsDir = path.join(tmpDir, 'sessions');
|
||||
fs.mkdirSync(sessionsDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* */ }
|
||||
});
|
||||
|
||||
function writeSession(
|
||||
id: string,
|
||||
messages: Array<{ role: string; content: string }>,
|
||||
opts?: { title?: string; ageMs?: number },
|
||||
) {
|
||||
const created = new Date(Date.now() - (opts?.ageMs ?? 0)).toISOString();
|
||||
const meta = JSON.stringify({ type: 'meta', title: opts?.title ?? null, created });
|
||||
const lines = [meta, ...messages.map(m => JSON.stringify(m))];
|
||||
const filePath = path.join(sessionsDir, `${id}.jsonl`);
|
||||
fs.writeFileSync(filePath, lines.join('\n') + '\n');
|
||||
|
||||
// Backdate the file's mtime if ageMs is provided
|
||||
if (opts?.ageMs) {
|
||||
const pastTime = new Date(Date.now() - opts.ageMs);
|
||||
fs.utimesSync(filePath, pastTime, pastTime);
|
||||
}
|
||||
}
|
||||
|
||||
it('returns empty for non-existent dir', () => {
|
||||
expect(classifyThreads('/nonexistent/path')).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns empty for empty dir', () => {
|
||||
expect(classifyThreads(sessionsDir)).toEqual([]);
|
||||
});
|
||||
|
||||
it('classifies a recent session as fresh', () => {
|
||||
writeSession('s1', [
|
||||
{ role: 'user', content: 'Let us work on the dashboard.' },
|
||||
{ role: 'assistant', content: 'Sure, I will help with that.' },
|
||||
], { title: 'Dashboard work' });
|
||||
|
||||
const threads = classifyThreads(sessionsDir);
|
||||
expect(threads).toHaveLength(1);
|
||||
expect(threads[0].title).toBe('Dashboard work');
|
||||
expect(threads[0].freshness).toBe('fresh');
|
||||
expect(threads[0].messageCount).toBe(2);
|
||||
expect(threads[0].sessionId).toBe('s1');
|
||||
});
|
||||
|
||||
it('classifies 3-day-old session as aging', () => {
|
||||
const threeDays = 3 * 24 * 60 * 60 * 1000;
|
||||
writeSession('s1', [
|
||||
{ role: 'user', content: 'Working on auth.' },
|
||||
{ role: 'assistant', content: 'Let me help.' },
|
||||
], { title: 'Auth work', ageMs: threeDays });
|
||||
|
||||
const threads = classifyThreads(sessionsDir);
|
||||
expect(threads).toHaveLength(1);
|
||||
expect(threads[0].freshness).toBe('aging');
|
||||
});
|
||||
|
||||
it('classifies 10-day-old session as stale', () => {
|
||||
const tenDays = 10 * 24 * 60 * 60 * 1000;
|
||||
writeSession('s1', [
|
||||
{ role: 'user', content: 'Planning the architecture.' },
|
||||
{ role: 'assistant', content: 'Here are my thoughts.' },
|
||||
], { title: 'Architecture planning', ageMs: tenDays });
|
||||
|
||||
const threads = classifyThreads(sessionsDir);
|
||||
expect(threads).toHaveLength(1);
|
||||
expect(threads[0].freshness).toBe('stale');
|
||||
// Stale means "not recently touched" — it says nothing about importance
|
||||
expect(threads[0].title).toBe('Architecture planning');
|
||||
});
|
||||
|
||||
it('skips sessions with fewer than 2 messages', () => {
|
||||
const meta = JSON.stringify({ type: 'meta', title: 'Short', created: new Date().toISOString() });
|
||||
fs.writeFileSync(
|
||||
path.join(sessionsDir, 'short.jsonl'),
|
||||
meta + '\n' + JSON.stringify({ role: 'user', content: 'Hi' }) + '\n',
|
||||
);
|
||||
expect(classifyThreads(sessionsDir)).toEqual([]);
|
||||
});
|
||||
|
||||
it('falls back to first user message for title', () => {
|
||||
writeSession('s1', [
|
||||
{ role: 'user', content: 'Implementing the notification system for the app' },
|
||||
{ role: 'assistant', content: 'Great, let me help with that.' },
|
||||
]);
|
||||
|
||||
const threads = classifyThreads(sessionsDir);
|
||||
expect(threads).toHaveLength(1);
|
||||
expect(threads[0].title).toContain('notification system');
|
||||
});
|
||||
|
||||
it('sorts threads by recency (most recent first)', () => {
|
||||
const oneDay = 1 * 24 * 60 * 60 * 1000;
|
||||
const fiveDays = 5 * 24 * 60 * 60 * 1000;
|
||||
|
||||
writeSession('old', [
|
||||
{ role: 'user', content: 'Old thread content here.' },
|
||||
{ role: 'assistant', content: 'Old response.' },
|
||||
], { title: 'Old thread', ageMs: fiveDays });
|
||||
|
||||
writeSession('recent', [
|
||||
{ role: 'user', content: 'Recent thread content here.' },
|
||||
{ role: 'assistant', content: 'Recent response.' },
|
||||
], { title: 'Recent thread', ageMs: oneDay });
|
||||
|
||||
const threads = classifyThreads(sessionsDir);
|
||||
expect(threads).toHaveLength(2);
|
||||
expect(threads[0].title).toBe('Recent thread');
|
||||
expect(threads[1].title).toBe('Old thread');
|
||||
});
|
||||
|
||||
it('respects maxSessions limit', () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
writeSession(`s${i}`, [
|
||||
{ role: 'user', content: `Thread ${i} content here.` },
|
||||
{ role: 'assistant', content: `Response ${i}.` },
|
||||
], { title: `Thread ${i}` });
|
||||
}
|
||||
|
||||
const threads = classifyThreads(sessionsDir, 3);
|
||||
expect(threads).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('includes lastActive as ISO string', () => {
|
||||
writeSession('s1', [
|
||||
{ role: 'user', content: 'Working on tests.' },
|
||||
{ role: 'assistant', content: 'Let me help.' },
|
||||
], { title: 'Test work' });
|
||||
|
||||
const threads = classifyThreads(sessionsDir);
|
||||
expect(threads[0].lastActive).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
||||
});
|
||||
|
||||
it('truncates long titles to 80 chars', () => {
|
||||
const longContent = 'A'.repeat(200);
|
||||
writeSession('s1', [
|
||||
{ role: 'user', content: longContent },
|
||||
{ role: 'assistant', content: 'Noted.' },
|
||||
]);
|
||||
|
||||
const threads = classifyThreads(sessionsDir);
|
||||
expect(threads[0].title.length).toBeLessThanOrEqual(80);
|
||||
});
|
||||
|
||||
it('handles mixed freshness levels across threads', () => {
|
||||
const oneHour = 60 * 60 * 1000;
|
||||
const fourDays = 4 * 24 * 60 * 60 * 1000;
|
||||
const fifteenDays = 15 * 24 * 60 * 60 * 1000;
|
||||
|
||||
writeSession('fresh', [
|
||||
{ role: 'user', content: 'Fresh work happening now.' },
|
||||
{ role: 'assistant', content: 'On it.' },
|
||||
], { title: 'Fresh', ageMs: oneHour });
|
||||
|
||||
writeSession('aging', [
|
||||
{ role: 'user', content: 'Aging work from a few days ago.' },
|
||||
{ role: 'assistant', content: 'Noted.' },
|
||||
], { title: 'Aging', ageMs: fourDays });
|
||||
|
||||
writeSession('stale', [
|
||||
{ role: 'user', content: 'Stale work from two weeks ago.' },
|
||||
{ role: 'assistant', content: 'Archived.' },
|
||||
], { title: 'Stale', ageMs: fifteenDays });
|
||||
|
||||
const threads = classifyThreads(sessionsDir);
|
||||
expect(threads).toHaveLength(3);
|
||||
|
||||
const freshThread = threads.find(t => t.title === 'Fresh');
|
||||
const agingThread = threads.find(t => t.title === 'Aging');
|
||||
const staleThread = threads.find(t => t.title === 'Stale');
|
||||
|
||||
expect(freshThread?.freshness).toBe('fresh');
|
||||
expect(agingThread?.freshness).toBe('aging');
|
||||
expect(staleThread?.freshness).toBe('stale');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractSessionOutcome', () => {
|
||||
function makeLines(messages: Array<{ role: string; content: string }>): string[] {
|
||||
return messages.map(m => JSON.stringify(m));
|
||||
}
|
||||
|
||||
it('returns null for fewer than 4 message lines', () => {
|
||||
const lines = makeLines([
|
||||
{ role: 'user', content: 'Hello' },
|
||||
{ role: 'assistant', content: 'Hi there' },
|
||||
]);
|
||||
expect(extractSessionOutcome(lines)).toBeNull();
|
||||
});
|
||||
|
||||
it('extracts whatChanged from completion signals', () => {
|
||||
const lines = makeLines([
|
||||
{ role: 'user', content: 'Can you fix the login bug?' },
|
||||
{ role: 'assistant', content: 'Looking into it now.' },
|
||||
{ role: 'assistant', content: 'I found the issue in the auth handler.' },
|
||||
{ role: 'assistant', content: 'Fixed the authentication token validation logic.' },
|
||||
{ role: 'user', content: 'Great, thanks!' },
|
||||
]);
|
||||
const outcome = extractSessionOutcome(lines);
|
||||
expect(outcome).not.toBeNull();
|
||||
expect(outcome!.whatChanged.toLowerCase()).toContain('authentication');
|
||||
});
|
||||
|
||||
it('extracts openItems from unresolved signals', () => {
|
||||
const lines = makeLines([
|
||||
{ role: 'user', content: 'Let us work on the API endpoints.' },
|
||||
{ role: 'assistant', content: 'I completed the GET endpoints.' },
|
||||
{ role: 'assistant', content: 'We still need to implement the rate limiting middleware.' },
|
||||
{ role: 'user', content: 'OK, we can do that next time.' },
|
||||
]);
|
||||
const outcome = extractSessionOutcome(lines);
|
||||
expect(outcome).not.toBeNull();
|
||||
expect(outcome!.openItems).not.toBeNull();
|
||||
expect(outcome!.openItems!.toLowerCase()).toContain('rate limiting');
|
||||
});
|
||||
|
||||
it('extracts nextStep from forward-looking signals', () => {
|
||||
const lines = makeLines([
|
||||
{ role: 'user', content: 'How is the migration going?' },
|
||||
{ role: 'assistant', content: 'I finished migrating the user table.' },
|
||||
{ role: 'assistant', content: 'Next step: migrate the permissions table and update foreign keys.' },
|
||||
{ role: 'user', content: 'Sounds good.' },
|
||||
]);
|
||||
const outcome = extractSessionOutcome(lines);
|
||||
expect(outcome).not.toBeNull();
|
||||
expect(outcome!.nextStep).not.toBeNull();
|
||||
expect(outcome!.nextStep!.toLowerCase()).toContain('permissions');
|
||||
});
|
||||
|
||||
it('falls back to first substantive user message for whatChanged', () => {
|
||||
const lines = makeLines([
|
||||
{ role: 'user', content: 'Can you review the database schema design for the new feature?' },
|
||||
{ role: 'assistant', content: 'Sure, let me take a look.' },
|
||||
{ role: 'assistant', content: 'The schema looks reasonable.' },
|
||||
{ role: 'user', content: 'OK thanks.' },
|
||||
]);
|
||||
const outcome = extractSessionOutcome(lines);
|
||||
expect(outcome).not.toBeNull();
|
||||
expect(outcome!.whatChanged.toLowerCase()).toContain('database schema');
|
||||
});
|
||||
|
||||
it('returns null for all-greeting sessions', () => {
|
||||
const lines = makeLines([
|
||||
{ role: 'user', content: 'Hi' },
|
||||
{ role: 'assistant', content: 'Hello!' },
|
||||
{ role: 'user', content: 'Thanks' },
|
||||
{ role: 'assistant', content: 'You are welcome.' },
|
||||
]);
|
||||
expect(extractSessionOutcome(lines)).toBeNull();
|
||||
});
|
||||
|
||||
it('handles outcome with all three fields populated', () => {
|
||||
const lines = makeLines([
|
||||
{ role: 'user', content: 'Let us finish the notification system.' },
|
||||
{ role: 'assistant', content: 'I implemented the email notification sender module.' },
|
||||
{ role: 'assistant', content: 'We still need to add SMS notification support.' },
|
||||
{ role: 'assistant', content: 'Next step: integrate the Twilio API for SMS delivery.' },
|
||||
{ role: 'user', content: 'Perfect, let us do that tomorrow.' },
|
||||
]);
|
||||
const outcome = extractSessionOutcome(lines);
|
||||
expect(outcome).not.toBeNull();
|
||||
expect(outcome!.whatChanged).toBeTruthy();
|
||||
expect(outcome!.openItems).not.toBeNull();
|
||||
expect(outcome!.nextStep).not.toBeNull();
|
||||
});
|
||||
|
||||
it('truncates long content', () => {
|
||||
const longTask = 'A'.repeat(200);
|
||||
const lines = makeLines([
|
||||
{ role: 'user', content: `Can you work on the feature?` },
|
||||
{ role: 'assistant', content: `I completed ${longTask} successfully.` },
|
||||
{ role: 'user', content: 'Great job.' },
|
||||
{ role: 'assistant', content: 'All done.' },
|
||||
]);
|
||||
const outcome = extractSessionOutcome(lines);
|
||||
expect(outcome).not.toBeNull();
|
||||
expect(outcome!.whatChanged.length).toBeLessThanOrEqual(120);
|
||||
});
|
||||
});
|
||||
|
||||
describe('persistSessionOutcome', () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-outcome-test-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* */ }
|
||||
});
|
||||
|
||||
it('writes outcome to meta line', () => {
|
||||
const filePath = path.join(tmpDir, 'test.jsonl');
|
||||
const meta = { type: 'meta', title: 'Test', created: new Date().toISOString() };
|
||||
const msg = { role: 'user', content: 'Hello' };
|
||||
fs.writeFileSync(filePath, JSON.stringify(meta) + '\n' + JSON.stringify(msg) + '\n');
|
||||
|
||||
const outcome: SessionOutcome = {
|
||||
whatChanged: 'Fixed the auth bug',
|
||||
openItems: 'Rate limiting still needed',
|
||||
nextStep: 'Add rate limiter',
|
||||
};
|
||||
persistSessionOutcome(filePath, outcome);
|
||||
|
||||
// Read back and verify
|
||||
const content = fs.readFileSync(filePath, 'utf-8').trim();
|
||||
const lines = content.split('\n');
|
||||
const updatedMeta = JSON.parse(lines[0]);
|
||||
expect(updatedMeta.outcome).toEqual(outcome);
|
||||
// Original message should still be there
|
||||
expect(lines).toHaveLength(2);
|
||||
const originalMsg = JSON.parse(lines[1]);
|
||||
expect(originalMsg.content).toBe('Hello');
|
||||
});
|
||||
|
||||
it('preserves existing meta fields', () => {
|
||||
const filePath = path.join(tmpDir, 'test.jsonl');
|
||||
const meta = { type: 'meta', title: 'My Session', summary: 'A good session', created: '2026-03-14T00:00:00Z' };
|
||||
fs.writeFileSync(filePath, JSON.stringify(meta) + '\n');
|
||||
|
||||
persistSessionOutcome(filePath, { whatChanged: 'Updated config', openItems: null, nextStep: null });
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf-8').trim();
|
||||
const updatedMeta = JSON.parse(content.split('\n')[0]);
|
||||
expect(updatedMeta.title).toBe('My Session');
|
||||
expect(updatedMeta.summary).toBe('A good session');
|
||||
expect(updatedMeta.outcome.whatChanged).toBe('Updated config');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user