/** * 액션 — 코딩 피드백 루프("생성→실행→에러 확인→수정")의 실행 축 검증. * stdout/stderr/종료코드가 internal system 메시지로 재주입되어 자동 후속 턴을 * 트리거하는 계약 + 샌드박스(워크스페이스 밖 차단)·확장자 화이트리스트 검증. */ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { applyRunCodeActions } from '../src/agent/actions/runCode'; import { buildSyntaxFixInstruction } from '../src/features/selfReflector/selfReflectorExecution'; // vscode 는 jest.config.js 의 moduleNameMapper(tests/mocks/vscode.js)가 전역 mock 으로 처리한다. let root: string; beforeAll(() => { root = fs.mkdtempSync(path.join(os.tmpdir(), 'astra-runcode-')); }); afterAll(() => { try { fs.rmSync(root, { recursive: true, force: true }); } catch { /* noop */ } }); function makeCtx(aiMessage: string): any { return { aiMessage, rootPath: root, report: [], chatHistory: [] }; } describe('applyRunCodeActions', () => { it('정상 실행: stdout 이 exit 0 과 함께 재주입된다 (node)', async () => { fs.writeFileSync(path.join(root, 'ok.js'), "console.log('HELLO_' + (40 + 2));"); const ctx = makeCtx(''); await applyRunCodeActions(ctx); expect(ctx.report.some((r: string) => r.includes('run_code OK'))).toBe(true); expect(ctx.chatHistory[0].internal).toBe(true); expect(ctx.chatHistory[0].content).toContain('exit 0'); expect(ctx.chatHistory[0].content).toContain('HELLO_42'); }); it('실행 에러: stderr + 수정→실행→확인 루프 지시가 재주입된다', async () => { fs.writeFileSync(path.join(root, 'boom.js'), "throw new Error('KABOOM');"); const ctx = makeCtx(''); await applyRunCodeActions(ctx); expect(ctx.report.some((r: string) => r.includes('FAIL'))).toBe(true); expect(ctx.chatHistory[0].content).toContain('[Result of run_code — ERROR]'); expect(ctx.chatHistory[0].content).toContain('KABOOM'); expect(ctx.chatHistory[0].content).toContain(''); }); it('워크스페이스 밖 경로는 차단된다', async () => { const ctx = makeCtx(''); await applyRunCodeActions(ctx); expect(ctx.report.some((r: string) => r.includes('blocked'))).toBe(true); expect(ctx.chatHistory).toEqual([]); }); it('지원하지 않는 확장자는 run_command 안내와 함께 거절된다', async () => { fs.writeFileSync(path.join(root, 'note.md'), '# hi'); const ctx = makeCtx(''); await applyRunCodeActions(ctx); expect(ctx.chatHistory[0].content).toContain('UNSUPPORTED'); expect(ctx.chatHistory[0].content).toContain('run_command'); }); it('존재하지 않는 파일이면 list_files 안내를 재주입한다', async () => { const ctx = makeCtx(''); await applyRunCodeActions(ctx); expect(ctx.chatHistory[0].content).toContain('존재하지 않습니다'); }); it('한 턴 실행 한도(2회)를 초과하면 건너뛴다', async () => { fs.writeFileSync(path.join(root, 'a.js'), "console.log('a');"); const tags = ''.repeat(3); const ctx = makeCtx(tags); await applyRunCodeActions(ctx); const results = ctx.chatHistory.filter((m: any) => m.content.includes('[Result of run_code]')); expect(results.length).toBe(2); expect(ctx.report.some((r: string) => r.includes('한도'))).toBe(true); }); }); describe('buildSyntaxFixInstruction — 문법 오류 자가수정 루프 메시지', () => { it('실패 라인들과 edit_file 수정 지시를 담는다', () => { const msg = buildSyntaxFixInstruction(['❌ node --check FAIL: a.js — SyntaxError: x']); expect(msg).toContain('SYNTAX ERRORS'); expect(msg).toContain('a.js'); expect(msg).toContain(''); expect(msg).toContain('수동 수정을 요구하지 마라'); }); });