Files
connectai/tests/sleepDigest.test.ts
T
koriweb 7584c6bbc1 feat(growth): Sleep-time 지식 사전 소화 — 응답 지연을 유휴 시간으로 이동 (v2.2.224)
deep research(3표 검증) 1순위 적용: sleep-time compute (arXiv 2504.13171).
유휴 시간에 로컬 LLM이 두뇌의 raw context를 learned context로 미리 소화해,
응답 시점 RAG가 고밀도 소화 노트를 검색하게 한다 — 로컬 LLM의 최대 약점
(느린 추론)의 비용을 사용자 응대 시점에서 유휴 시간으로 구조적으로 이동.

- sleepDigest.ts: 매일 03:00 KST(설정 가능) 최근 7일 변경 파일이 많은 폴더
  순으로 소화 노트 생성 (<두뇌>/Digests/<슬러그>.md, 런당 ≤5건).
  노트 = 예상 질의 Q&A + 핵심 사실 + 문서 간 연결 (출처 제목 인용 강제,
  "원문에 없는 내용 지어내지 마라" — 환각 방지 동일 원칙).
- 노후화 자동 감지: 소스 mtime > generated_at 이면 재생성, 아니면 skip
  (steady-state 비용 0). 노트는 삭제해도 안전 (자동 재생성).
- 승인 게이트 불요 근거: 외부 지식 유입이 아니라 기존 두뇌의 재구성.
  원문 우선 원칙을 노트 머리에 명기.
- 수동 명령 "Astra: 지식 사전 소화 지금 실행" + sleepDigest.enabled/time 설정.
- 실 LLM(gemma-4-26b)+실 위키 3문서로 프롬프트 품질 검증 완료 (출처 인용·
  무환각 확인). 테스트 8건 추가.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 11:29:40 +09:00

102 lines
4.4 KiB
TypeScript

/**
* Sleep-time 사전 소화 — 순수 로직 테스트 (대상 선정·노후화 판정·노트 형식).
* LLM 호출(runSleepDigestOnce)은 제외 — 통합 검증은 수동 명령으로.
*/
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import {
selectDigestTargets, folderSlug, isDigestFresh, digestMarkdown, DIGEST_DIR,
type DigestTarget,
} from '../src/features/growth/sleepDigest';
const tmpBrain = () => fs.mkdtempSync(path.join(os.tmpdir(), 'astra-digest-'));
function mkFile(brain: string, rel: string, ageDays: number): string {
const abs = path.join(brain, rel);
fs.mkdirSync(path.dirname(abs), { recursive: true });
fs.writeFileSync(abs, `# ${path.basename(rel)}\n내용`, 'utf8');
const t = new Date(Date.now() - ageDays * 86_400_000);
fs.utimesSync(abs, t, t);
return abs;
}
describe('selectDigestTargets', () => {
test('최근 변경 파일이 많은 폴더 우선, 오래된 파일 제외', () => {
const brain = tmpBrain();
const files = [
mkFile(brain, 'Topics_Rag/a.md', 1),
mkFile(brain, 'Topics_Rag/b.md', 2),
mkFile(brain, 'Topics_Rag/c.md', 3),
mkFile(brain, 'Coding/Python/x.md', 1),
mkFile(brain, 'Coding/Python/y.md', 2),
mkFile(brain, 'Old_Folder/z.md', 30), // 윈도우 밖
];
const targets = selectDigestTargets(brain, files, Date.now(), 7);
expect(targets[0].folder).toBe('Topics_Rag');
expect(targets[0].files).toHaveLength(3);
expect(targets[1].folder).toBe('Coding/Python');
expect(targets.find(t => t.folder === 'Old_Folder')).toBeUndefined();
});
test('Digests 폴더 자신은 재소화하지 않음 + 루트 직속은 빈 키', () => {
const brain = tmpBrain();
const files = [
mkFile(brain, `${DIGEST_DIR}/old-digest.md`, 1),
mkFile(brain, 'root-note.md', 1),
];
const targets = selectDigestTargets(brain, files, Date.now(), 7);
expect(targets.find(t => t.folder.includes(DIGEST_DIR))).toBeUndefined();
expect(targets.find(t => t.folder === '')).toBeDefined();
});
test('파일 상한 8개', () => {
const brain = tmpBrain();
const files = Array.from({ length: 12 }, (_, i) => mkFile(brain, `Big/f${i}.md`, 1));
const targets = selectDigestTargets(brain, files, Date.now(), 7);
expect(targets[0].files.length).toBeLessThanOrEqual(8);
});
});
describe('folderSlug', () => {
test('경로 구분자·특수문자 정리', () => {
expect(folderSlug('Coding/Python')).toBe('Coding--Python');
expect(folderSlug('')).toBe('root');
expect(folderSlug('AI & ML!')).toBe('AI-ML-');
});
});
describe('isDigestFresh — 노후화 판정', () => {
const target = (over: Partial<DigestTarget> = {}): DigestTarget => ({
folder: 'X', slug: 'X', files: [], newestMtimeMs: Date.now(), ...over,
});
test('노트 없음 → 재생성 필요', () => {
expect(isDigestFresh(path.join(tmpBrain(), 'no.md'), Date.now())).toBe(false);
});
test('generated_at ≥ 소스 mtime → fresh', () => {
const brain = tmpBrain();
const f = path.join(brain, 'd.md');
const t = target();
fs.writeFileSync(f, digestMarkdown(t, '본문', ['s1'], new Date(t.newestMtimeMs + 1000).toISOString()), 'utf8');
expect(isDigestFresh(f, t.newestMtimeMs)).toBe(true);
});
test('소스가 노트보다 최신 → 재생성 필요', () => {
const brain = tmpBrain();
const f = path.join(brain, 'd.md');
fs.writeFileSync(f, digestMarkdown(target(), '본문', ['s1'], new Date(Date.now() - 86_400_000).toISOString()), 'utf8');
expect(isDigestFresh(f, Date.now())).toBe(false);
});
});
describe('digestMarkdown', () => {
test('frontmatter(type: digest, sources) + 원문 우선 고지', () => {
const md = digestMarkdown(
{ folder: 'Topics_Rag', slug: 'Topics_Rag', files: [], newestMtimeMs: 0 },
'## 예상 질문과 답\n- Q…', ['문서 "A"', 'B'], '2026-06-12T03:00:00Z',
);
expect(md).toMatch(/^---\ntype: digest/);
expect(md).toContain('generated_at: 2026-06-12T03:00:00Z');
expect(md).toContain("문서 'A'"); // 따옴표 이스케이프
expect(md).toContain('원문이 항상 우선');
expect(md).toContain('# 소화 노트: Topics_Rag');
});
});