import { describe, expect, it } from 'vitest' import { extractLabelFacts } from '../labels/label-schema.js' import type { FetchLike, GiteaIssue } from '../gitea/types.js' import { runAgentTurn } from './agent-loop.js' import { REGINALD_SYSTEM, REGINALD_TOOLS } from './agent-tools.js' import { createChatClient } from './chat-client.js' import { buildProjectView, type ProjectSnapshot } from './query-project.js' /** * Opt-in (COMMITEA_MODEL_LIVE=1). Drives the real chat client + agent loop * against a local OpenAI-compatible server (LM Studio on :1234 by default), * proving the model calls query_project and narrates the real result. */ const LIVE = !!process.env.COMMITEA_MODEL_LIVE const BASE = process.env.COMMITEA_MODEL_URL ?? 'http://localhost:1234/v1' const MODEL = process.env.COMMITEA_MODEL_SMALL ?? 'google/gemma-4-e4b' function issue(over: Partial): GiteaIssue { const labels = over.labels ?? [] return { number: 1, title: '#1', body: '', state: 'open', labels, facts: extractLabelFacts(labels), milestone: null, assignee: null, assignees: [], createdAt: '2026-01-05T09:00:00Z', updatedAt: '2026-01-05T09:00:00Z', closedAt: null, url: '', ...over, } } const SNAP: ProjectSnapshot = { issues: [ issue({ number: 2, title: 'ChangeSource interface + polling source', labels: ['est/3d', 'p/1'] }), issue({ number: 3, title: 'SQLite cache bootstrap', labels: ['est/2d', 'p/1'] }), ], timelines: {}, deps: [{ issue: 3, dependsOn: 2 }], } describe('agent loop (live model)', () => { it.skipIf(!LIVE)( 'calls query_project and narrates the real focus', async () => { const client = createChatClient({ baseUrl: BASE, model: MODEL }, globalThis.fetch as unknown as FetchLike) const turn = await runAgentTurn({ complete: (m, t) => client.complete(m, t), messages: [ { role: 'system', content: REGINALD_SYSTEM }, { role: 'user', content: 'What should I work on right now?' }, ], tools: REGINALD_TOOLS, execute: async (name, args) => name === 'query_project' ? buildProjectView((args as { view: any }).view, (args as any).filters, SNAP, new Date()) : { error: `unknown tool ${name}` }, }) // it consulted the project, then answered in prose about the real top item (#2) expect(turn.steps.some((s) => s.tool === 'query_project')).toBe(true) expect(turn.content.trim().length).toBeGreaterThan(0) expect(turn.content).toMatch(/#?2\b|ChangeSource/i) }, 60_000, ) })