import { beforeAll, 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 the tools and narrates the real result. The model is * whatever is loaded (via LM Studio's native API), so it never JIT-swaps. */ const LIVE = !!process.env.COMMITEA_MODEL_LIVE const BASE = process.env.COMMITEA_MODEL_URL ?? 'http://localhost:1234/v1' let MODEL = process.env.COMMITEA_MODEL_SMALL ?? '' beforeAll(async () => { if (!LIVE || MODEL) return const root = BASE.replace(/\/v1\/?$/, '') const loaded = await fetch(`${root}/api/v0/models`) .then((r) => (r.ok ? (r.json() as Promise<{ data?: { id: string; state?: string; type?: string }[] }>) : null)) .then((d) => d?.data?.find((m) => m.state === 'loaded' && m.type !== 'embeddings')?.id) .catch(() => undefined) MODEL = loaded ?? '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, ) it.skipIf(!LIVE)( 'records a standing instruction via record_directive', async () => { const client = createChatClient({ baseUrl: BASE, model: MODEL }, globalThis.fetch as unknown as FetchLike) const recorded: unknown[] = [] const turn = await runAgentTurn({ complete: (m, t) => client.complete(m, t), messages: [ { role: 'system', content: REGINALD_SYSTEM }, { role: 'user', content: 'Log this standing directive: pilots come first, everything else waits.' }, ], tools: REGINALD_TOOLS, execute: async (name, args) => { if (name === 'record_directive') { recorded.push(args) return { recorded: { kind: (args as { kind?: string }).kind ?? 'note' } } } return name === 'query_project' ? buildProjectView((args as { view: any }).view, (args as any).filters, SNAP, new Date()) : { error: `unknown tool ${name}` } }, }) // the model logged the directive rather than trying to apply it expect(turn.steps.some((s) => s.tool === 'record_directive')).toBe(true) expect(recorded.length).toBeGreaterThan(0) }, 60_000, ) })