/** * 액션 — 수학·수치 계산의 Python 위임 검증. * * 소형 모델 추론 강화 전략의 1축: "모델은 코드로 변환만, 계산은 도구가". * 실행 결과/에러가 chatHistory 에 internal system 메시지로 재주입되어 * agent 의 자동 후속 턴(자가수정 루프)을 트리거하는 계약을 검증한다. */ import { execFileSync } from 'child_process'; import { applyCalculateActions, _resetPythonCache } from '../src/agent/actions/calculate'; // vscode 런타임 의존 없음(type-only import) — 별도 mock 불필요. function hasPython(): boolean { for (const cmd of ['python3', 'python', 'py']) { try { execFileSync(cmd, ['--version'], { timeout: 3000 }); return true; } catch { /* next */ } } return false; } const PY = hasPython(); const itPy = PY ? it : it.skip; function makeCtx(aiMessage: string): any { return { aiMessage, report: [], chatHistory: [] }; } describe('applyCalculateActions', () => { beforeEach(() => _resetPythonCache()); it('calculate 태그가 없으면 아무것도 하지 않는다', async () => { const ctx = makeCtx('그냥 일반 답변입니다. 2+2=4 입니다.'); await applyCalculateActions(ctx); expect(ctx.report).toEqual([]); expect(ctx.chatHistory).toEqual([]); }); itPy('정상 계산: stdout 이 internal system 메시지로 재주입된다', async () => { const ctx = makeCtx('print(1234 * 5678)'); await applyCalculateActions(ctx); expect(ctx.report.some((r: string) => r.includes('Calculated'))).toBe(true); expect(ctx.chatHistory.length).toBe(1); expect(ctx.chatHistory[0].role).toBe('system'); expect(ctx.chatHistory[0].internal).toBe(true); expect(ctx.chatHistory[0].content).toContain('[Result of calculate]'); expect(ctx.chatHistory[0].content).toContain('7006652'); // 1234*5678 }); itPy('에러 코드: stderr 가 ERROR 블록으로 재주입되어 자가수정 루프를 유도한다', async () => { const ctx = makeCtx('print(undefined_variable_xyz)'); await applyCalculateActions(ctx); expect(ctx.report.some((r: string) => r.includes('failed'))).toBe(true); expect(ctx.chatHistory[0].content).toContain('[Result of calculate — ERROR]'); expect(ctx.chatHistory[0].content).toContain('다시 emit'); }); itPy('print 누락: 빈 출력이면 print 힌트를 재주입한다', async () => { const ctx = makeCtx('x = 40 + 2'); await applyCalculateActions(ctx); expect(ctx.chatHistory[0].content).toContain('EMPTY OUTPUT'); expect(ctx.chatHistory[0].content).toContain('print'); }); itPy('여러 태그는 각각 실행되고, 한도(4개) 초과분은 건너뛴다', async () => { const tags = Array.from({ length: 6 }, (_, i) => `print(${i} + 1)`).join('\n'); const ctx = makeCtx(tags); await applyCalculateActions(ctx); const results = ctx.chatHistory.filter((m: any) => m.content.includes('[Result of calculate]')); expect(results.length).toBe(4); expect(ctx.report.some((r: string) => r.includes('한도'))).toBe(true); }); it('과도하게 긴 코드는 실행 전 차단한다', async () => { const ctx = makeCtx(`${'x=1\n'.repeat(3000)}print(x)`); await applyCalculateActions(ctx); expect(ctx.report.some((r: string) => r.includes('너무 깁니다'))).toBe(true); expect(ctx.chatHistory).toEqual([]); }); });