2afd1ac589
신뢰성 코어 (P1~P2): - Requirement Graph: 업무 유형(회의록/시장조사/업무조사/일정) 필수 요소 주입 + 커버리지 hook - Confidence Engine(0~100 결정론적) / Escalation Engine(검토 요청) / Epistemic Guard(모름·추정·확실 3분류) - Provenance: citationTrace 에 출처 수정일·오래됨 경고 - Critic Loop: 문제 신호 turn 만 LLM 검수 1회 + 보완 카드 성장 루프 (P3): - Gap Detector(Requirement-Knowledge) / Need Engine(30/25/20/15/10 공식) / Knowledge Inventory - Learning Queue(proposed 전용 병합 — 승인은 사람만) / Decision Journal / Reflection 기록 - 반복 누락 요소(3회+)는 다음 turn 체크리스트에 자동 강조 (T5 루프) 지식 운영 (P4) + 기억 (P5) + 학습 실행 (P6): - Knowledge Validation + Belief Revision(중복 reject·충돌 시 update/add 권고) - Knowledge Decay(분야별 반감기 감사) / Knowledge Debt(blocked x impact) - Organizational Memory(.astra/organization.md 상시 주입) - Research Agent(approved 큐 -> 조사 브리프+추정 라벨 초안+Validation 게이트 -> proposals/) - Skill Score(전/후반 추세) + Success Pattern DB(전요소충족+확신도90+ 자동 적재) 병렬 트랙: - 캘린더 충돌 게이트: conflictCheck + 구조화 이벤트 캐시 + create_calendar_event 차단(force 는 사용자 승인 후) - Task Eval Harness: 회의록 골든셋 자동 채점 명령 + 성장 리포트/학습 큐/노후 점검 명령 신규 모듈 17종(src/intelligence/), VS Code 명령 5종, 설정 11종, 테스트 +89건(전체 508 통과). 설계 문서: docs/SELF_EVOLVING_OS_MASTER_PLAN.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
201 lines
8.1 KiB
TypeScript
201 lines
8.1 KiB
TypeScript
/**
|
|
* Critic Agent / Reflection Store / Task Eval Harness (Self-Evolving OS P1 잔여 + P3) 테스트.
|
|
*/
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import {
|
|
buildCritiquePrompt,
|
|
parseCritique,
|
|
runCriticReview,
|
|
formatCriticFooter,
|
|
} from '../src/intelligence/criticAgent';
|
|
import {
|
|
appendReflection,
|
|
loadReflections,
|
|
summarizeFailurePatterns,
|
|
recurrentMisses,
|
|
formatGrowthReport,
|
|
ReflectionRecord,
|
|
} from '../src/intelligence/reflectionStore';
|
|
import {
|
|
loadTaskGoldenSet,
|
|
scoreTaskAnswer,
|
|
runTaskEval,
|
|
formatTaskEvalReport,
|
|
TASK_GOLDEN_DIR,
|
|
TaskGoldenRecord,
|
|
} from '../src/intelligence/taskEvalHarness';
|
|
import { DEFAULT_TASK_REQUIREMENTS, buildRequirementGraphBlock } from '../src/intelligence/requirementGraph';
|
|
|
|
const MEETING_REQ = DEFAULT_TASK_REQUIREMENTS.find((r) => r.id === 'meeting-minutes')!;
|
|
|
|
function tmpBrain(): string {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'astra-test-brain-'));
|
|
}
|
|
|
|
function mkReflection(partial: Partial<ReflectionRecord>): ReflectionRecord {
|
|
return {
|
|
ts: '2026-06-11T10:00:00.000Z',
|
|
taskId: 'meeting-minutes',
|
|
taskLabel: '회의록',
|
|
confidenceScore: 70,
|
|
confidenceBand: 'medium',
|
|
missing: [],
|
|
escalated: false,
|
|
criticIssues: null,
|
|
promptPreview: '회의록 정리',
|
|
...partial,
|
|
};
|
|
}
|
|
|
|
describe('criticAgent', () => {
|
|
it('critique 프롬프트에 필수 요소와 누락 신호가 포함된다', () => {
|
|
const { system, user } = buildCritiquePrompt('회의록 정리해줘', '초안...', MEETING_REQ, ['담당자', '기한']);
|
|
expect(system).toContain('JSON');
|
|
expect(user).toContain('담당자, 기한');
|
|
expect(user).toContain('회의록');
|
|
});
|
|
|
|
it('코드펜스·잡설 섞인 응답에서도 JSON 을 파싱한다', () => {
|
|
const raw = '검토 결과입니다.\n```json\n{"pass": false, "issues": [{"severity": "major", "description": "기한 누락"}], "supplement": "## 기한\\n- (기한 미정)"}\n```';
|
|
const c = parseCritique(raw);
|
|
expect(c).not.toBeNull();
|
|
expect(c!.pass).toBe(false);
|
|
expect(c!.issues[0].severity).toBe('major');
|
|
expect(c!.supplement).toContain('기한');
|
|
});
|
|
|
|
it('pass=true 여도 issues 가 있으면 pass 취급하지 않는다', () => {
|
|
const c = parseCritique('{"pass": true, "issues": [{"severity": "minor", "description": "x"}], "supplement": ""}');
|
|
expect(c!.pass).toBe(false);
|
|
});
|
|
|
|
it('runCriticReview — LLM 실패 시 null (silent skip)', async () => {
|
|
const result = await runCriticReview({
|
|
userPrompt: 'q', draft: 'd', requirement: MEETING_REQ, missingLabels: [],
|
|
callLlm: async () => { throw new Error('LLM down'); },
|
|
});
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('formatCriticFooter — pass 면 빈 문자열, 실패면 이슈+보완 표시', () => {
|
|
expect(formatCriticFooter({ pass: true, issues: [], supplement: '' })).toBe('');
|
|
const f = formatCriticFooter({
|
|
pass: false,
|
|
issues: [{ severity: 'major', description: '결정과 미결이 섞임' }],
|
|
supplement: '## 보완',
|
|
});
|
|
expect(f).toContain('검수 (Critic)');
|
|
expect(f).toContain('결정과 미결이 섞임');
|
|
expect(f).toContain('보완 제안');
|
|
});
|
|
});
|
|
|
|
describe('reflectionStore', () => {
|
|
it('append → load 라운드트립', () => {
|
|
const brain = tmpBrain();
|
|
expect(appendReflection(brain, mkReflection({ missing: ['기한'] }))).toBe(true);
|
|
expect(appendReflection(brain, mkReflection({ missing: ['기한', '담당자'] }))).toBe(true);
|
|
const records = loadReflections(brain);
|
|
expect(records.length).toBe(2);
|
|
expect(records[1].missing).toEqual(['기한', '담당자']);
|
|
});
|
|
|
|
it('summarizeFailurePatterns — 반복 누락 집계 (많은 순)', () => {
|
|
const records = [
|
|
mkReflection({ missing: ['기한'] }),
|
|
mkReflection({ missing: ['기한'] }),
|
|
mkReflection({ missing: ['기한', '담당자'] }),
|
|
];
|
|
const patterns = summarizeFailurePatterns(records);
|
|
expect(patterns[0]).toMatchObject({ element: '기한', count: 3 });
|
|
expect(patterns[1]).toMatchObject({ element: '담당자', count: 1 });
|
|
});
|
|
|
|
it('recurrentMisses — threshold 이상만 반환', () => {
|
|
const records = [
|
|
mkReflection({ missing: ['기한'] }),
|
|
mkReflection({ missing: ['기한'] }),
|
|
mkReflection({ missing: ['기한'] }),
|
|
mkReflection({ missing: ['담당자'] }),
|
|
];
|
|
expect(recurrentMisses(records, 'meeting-minutes', 3)).toEqual(['기한']);
|
|
expect(recurrentMisses(records, 'market-research', 3)).toEqual([]);
|
|
});
|
|
|
|
it('반복 누락 요소가 Requirement Graph 블록에 강조된다 (T5 루프)', () => {
|
|
const block = buildRequirementGraphBlock('회의록 정리해줘', undefined, ['기한']);
|
|
expect(block).toContain('과거에 자주 누락된 요소');
|
|
});
|
|
|
|
it('formatGrowthReport — 주별 추이 테이블 + 반복 실수 Top', () => {
|
|
const records = [
|
|
mkReflection({ ts: '2026-06-01T10:00:00.000Z', confidenceScore: 60, missing: ['기한'] }),
|
|
mkReflection({ ts: '2026-06-09T10:00:00.000Z', confidenceScore: 85, missing: [] }),
|
|
];
|
|
const md = formatGrowthReport(records);
|
|
expect(md).toContain('평균 확신도');
|
|
expect(md).toContain('기한');
|
|
expect(formatGrowthReport([])).toContain('기록 없음');
|
|
});
|
|
});
|
|
|
|
describe('taskEvalHarness', () => {
|
|
const record: TaskGoldenRecord = {
|
|
id: 'mm-test',
|
|
query: '이 회의 내용을 회의록으로 정리해줘',
|
|
sourceFile: 'fake.txt',
|
|
expectedElements: ['참석자', '결정사항', '액션 아이템', '담당자', '기한'],
|
|
reference: 'ref',
|
|
};
|
|
|
|
it('골든셋 로드 — 주석·깨진 줄 처리', () => {
|
|
const brain = tmpBrain();
|
|
const dir = path.join(brain, TASK_GOLDEN_DIR);
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
fs.writeFileSync(path.join(dir, 'meeting-minutes.golden.jsonl'), [
|
|
'// 주석',
|
|
JSON.stringify(record),
|
|
'{broken',
|
|
'',
|
|
].join('\n'), 'utf8');
|
|
const { records, parseErrors } = loadTaskGoldenSet(brain);
|
|
expect(records.length).toBe(1);
|
|
expect(parseErrors).toBe(1);
|
|
expect(records[0].id).toBe('mm-test');
|
|
});
|
|
|
|
it('scoreTaskAnswer — 커버리지·정직성·구조 채점', () => {
|
|
const answer = '# 회의록\n## 참석자: 김OO\n## 결정사항: A안\n## 액션 아이템\n- 발송 (담당자: 김OO, (기한 미정))';
|
|
const s = scoreTaskAnswer(answer, record);
|
|
expect(s.coverageRate).toBe(1);
|
|
expect(s.honestyMarkers).toBeGreaterThanOrEqual(1);
|
|
expect(s.sectionCount).toBeGreaterThanOrEqual(3);
|
|
});
|
|
|
|
it('runTaskEval — 생성 실패가 전체를 막지 않고 에러 레코드로 남는다', async () => {
|
|
const result = await runTaskEval({
|
|
records: [record, { ...record, id: 'mm-fail' }],
|
|
readSource: () => '전사 내용',
|
|
generate: async (r) => {
|
|
if (r.id === 'mm-fail') throw new Error('engine down');
|
|
return '## 참석자 a ## 결정사항 b ## 액션 아이템 c 담당자 d 기한 e';
|
|
},
|
|
});
|
|
expect(result.scores.length).toBe(2);
|
|
expect(result.scores[0].coverageRate).toBe(1);
|
|
expect(result.scores[1].error).toContain('engine down');
|
|
expect(result.avgCoverage).toBe(1); // 실패 레코드는 평균에서 제외
|
|
});
|
|
|
|
it('formatTaskEvalReport — 요약·테이블 포함', () => {
|
|
const md = formatTaskEvalReport(
|
|
{ scores: [scoreTaskAnswer('참석자 결정사항', record)], avgCoverage: 0.4, perfectCount: 0 },
|
|
{ taskLabel: '회의록', brainName: 'B', dateStr: 'now', modelName: 'gemma' },
|
|
);
|
|
expect(md).toContain('평균 요소 커버리지');
|
|
expect(md).toContain('mm-test');
|
|
});
|
|
});
|