feat: AI context builder
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import { getDb, resetDb } from '../../db';
|
||||
import { insertSession } from '../../db/sessions';
|
||||
import { insertScheduleEntry, updateScheduleStatus } from '../../db/schedule';
|
||||
import { setSetting } from '../../db/settings';
|
||||
import { buildContext } from '../../ai/context';
|
||||
|
||||
beforeEach(() => { resetDb(); });
|
||||
|
||||
describe('buildContext', () => {
|
||||
it('returns empty history when no data exists', () => {
|
||||
const db = getDb();
|
||||
const ctx = buildContext(db, '2026-05-20');
|
||||
expect(ctx.recentSessions).toHaveLength(0);
|
||||
expect(ctx.upcomingSchedule).toHaveLength(0);
|
||||
expect(ctx.currentStreak).toBe(0);
|
||||
});
|
||||
|
||||
it('includes recent sessions', () => {
|
||||
const db = getDb();
|
||||
insertSession(db, '2026-05-19', 60, 'good session');
|
||||
const ctx = buildContext(db, '2026-05-20');
|
||||
expect(ctx.recentSessions).toHaveLength(1);
|
||||
expect(ctx.recentSessions[0]!.duration_min).toBe(60);
|
||||
});
|
||||
|
||||
it('includes upcoming schedule', () => {
|
||||
const db = getDb();
|
||||
insertScheduleEntry(db, '2026-05-21', 60, null);
|
||||
const ctx = buildContext(db, '2026-05-20');
|
||||
expect(ctx.upcomingSchedule).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('reads training goal from settings', () => {
|
||||
const db = getDb();
|
||||
setSetting(db, 'goal', 'wear 2 hours comfortably');
|
||||
const ctx = buildContext(db, '2026-05-20');
|
||||
expect(ctx.goal).toBe('wear 2 hours comfortably');
|
||||
});
|
||||
|
||||
it('calculates current streak', () => {
|
||||
const db = getDb();
|
||||
insertSession(db, '2026-05-18', 60, null);
|
||||
insertSession(db, '2026-05-19', 60, null);
|
||||
insertScheduleEntry(db, '2026-05-18', 60, null);
|
||||
updateScheduleStatus(db, '2026-05-18', 'completed', null);
|
||||
insertScheduleEntry(db, '2026-05-19', 60, null);
|
||||
updateScheduleStatus(db, '2026-05-19', 'completed', null);
|
||||
const ctx = buildContext(db, '2026-05-20');
|
||||
expect(ctx.currentStreak).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { Database } from 'better-sqlite3';
|
||||
import { getRecentSessions, type Session } from '../db/sessions';
|
||||
import { getUpcomingSchedule, type ScheduleEntry } from '../db/schedule';
|
||||
import { getSetting } from '../db/settings';
|
||||
|
||||
export interface AIContext {
|
||||
today: string;
|
||||
goal: string;
|
||||
recentSessions: Session[];
|
||||
upcomingSchedule: ScheduleEntry[];
|
||||
currentStreak: number;
|
||||
recentSkips: Array<{ date: string; reason: string | null }>;
|
||||
}
|
||||
|
||||
function calculateStreak(db: Database): number {
|
||||
const rows = db.prepare(
|
||||
"SELECT date FROM schedule WHERE status = 'completed' ORDER BY date DESC"
|
||||
).all() as Array<{ date: string }>;
|
||||
|
||||
if (rows.length === 0) return 0;
|
||||
|
||||
let streak = 0;
|
||||
let prev: string | null = null;
|
||||
|
||||
for (const { date } of rows) {
|
||||
if (prev === null) {
|
||||
streak = 1;
|
||||
prev = date;
|
||||
continue;
|
||||
}
|
||||
const prevDate = new Date(prev + 'T12:00:00');
|
||||
const currDate = new Date(date + 'T12:00:00');
|
||||
const diffDays = Math.round((prevDate.getTime() - currDate.getTime()) / 86400000);
|
||||
if (diffDays <= 2) {
|
||||
streak++;
|
||||
prev = date;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return streak;
|
||||
}
|
||||
|
||||
export function buildContext(db: Database, today: string): AIContext {
|
||||
const recentSessions = getRecentSessions(db, 30);
|
||||
const upcomingSchedule = getUpcomingSchedule(db, today);
|
||||
const goal = getSetting(db, 'goal') ?? 'Not set';
|
||||
const currentStreak = calculateStreak(db);
|
||||
|
||||
const recentSkips = (db.prepare(
|
||||
"SELECT date, skip_reason as reason FROM schedule WHERE status = 'skipped' ORDER BY date DESC LIMIT 10"
|
||||
).all() as Array<{ date: string; reason: string | null }>);
|
||||
|
||||
return { today, goal, recentSessions, upcomingSchedule, currentStreak, recentSkips };
|
||||
}
|
||||
Reference in New Issue
Block a user