Files
connectai/tests/webFetch.test.ts
T
g1nation a114d968b0 feat(core): 자기지식 접지·웹 접근·환경 자가점검 — 할루시네이션 방어 3중화 (v2.2.247)
- Alignment Self-Learning: 자가 조사(질문 전 두뇌 검색)·사용자 답변 두뇌 저장·핵심메시지/프로젝트 컨텍스트 주입 (alignmentResearch.ts 신규)
- 웹 접근: Bridge 폴백 직접 fetch(webFetch.ts 신규)·<fetch_url> 액션 태그·기업 모드 URL/아키텍처 컨텍스트 주입·bare 도메인 인식
- 트리거 버그 수정: startsWith('/') 가 절대경로를 슬래시 명령으로 오인 — 분석 지시·URL 주입 전멸 원인 (회귀 테스트 고정)
- 자기지식 접지: 기능 인벤토리 lazy 재생성·학습 메커니즘 정본 섹션·[인벤토리 대조] 태그 의무화·결정론적 재구현 제안 정정 훅(featureConceptMap.ts 신규)
- 환경 자가점검: HealthCheckMonitor 에 Bridge/두뇌 볼륨/git 자격증명/확장 버전 검사 4종 + readyBar ⚠ 표시
- 두뇌 동기화: 원격 미설정 시 로컬 새로고침 모드·staged 기준 commit 판정·인증 부재 안내
- 기타: outputFormat 기본 markdown(제목 렌더 복구)·레슨/행동제약 truncation 보호 구역 이동·[CONTEXT] 절단 우선순위 재정렬

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 23:46:07 +09:00

77 lines
3.4 KiB
TypeScript

import { extractUrls, htmlToText, decodeEntities, fetchUrlDirect } from '../src/features/web/webFetch';
describe('webFetch.extractUrls', () => {
it('extracts http(s) URLs and strips trailing punctuation', () => {
const urls = extractUrls('https://koritips.com 가서 내용 분석해줘.');
expect(urls).toEqual(['https://koritips.com']);
});
it('handles multiple URLs with dedupe and max cap', () => {
const text = 'A: https://a.com/x, B: https://b.com/y 그리고 다시 https://a.com/x 또 https://c.com';
const urls = extractUrls(text, 2);
expect(urls).toEqual(['https://a.com/x', 'https://b.com/y']);
});
it('ignores slash commands and empty input', () => {
expect(extractUrls('/wikify https://a.com')).toEqual([]);
expect(extractUrls('')).toEqual([]);
expect(extractUrls('URL 없는 문장')).toEqual([]);
});
it('strips Korean closing punctuation contamination', () => {
expect(extractUrls('링크(https://a.com/page)를 봐줘')).toEqual(['https://a.com/page']);
expect(extractUrls('「https://b.com」 분석')).toEqual(['https://b.com']);
});
it('recognizes bare domains without scheme and prepends https://', () => {
expect(extractUrls('koritips.com 가서 내용 분석해줘')).toEqual(['https://koritips.com']);
expect(extractUrls('www.example.net/path/page 확인')).toEqual(['https://www.example.net/path/page']);
expect(extractUrls('naver.co.kr 어때?')).toEqual(['https://naver.co.kr']);
});
it('does not mistake filenames or emails for bare domains', () => {
expect(extractUrls('utils.ts 와 package.json 수정해줘')).toEqual([]);
expect(extractUrls('메일은 user@gmail.com 입니다')).toEqual([]);
});
it('does not double-count a scheme URL as a bare domain', () => {
expect(extractUrls('https://koritips.com 그리고 koritips.com')).toEqual(['https://koritips.com']);
});
});
describe('webFetch.htmlToText', () => {
it('strips scripts, styles, and tags while preserving block structure', () => {
const html = `<html><head><style>.x{color:red}</style><script>alert(1)</script></head>
<body><h1>제목입니다</h1><p>첫 문단.</p><p>둘째 문단.</p></body></html>`;
const text = htmlToText(html);
expect(text).not.toContain('alert');
expect(text).not.toContain('color:red');
expect(text).toContain('제목입니다');
expect(text).toMatch(/첫 문단\.\n/);
});
it('decodes common entities', () => {
expect(decodeEntities('A &amp; B &lt;tag&gt; &quot;q&quot; &#39;s&#39; &nbsp;')).toBe(`A & B <tag> "q" 's' `);
expect(decodeEntities('&#44608;')).toBe('김');
});
it('collapses excessive whitespace', () => {
const text = htmlToText('<p>a</p>\n\n\n\n<p>b</p>');
expect(text).not.toMatch(/\n{3,}/);
});
});
describe('webFetch.fetchUrlDirect (no network)', () => {
it('rejects non-http schemes without throwing', async () => {
const r = await fetchUrlDirect('ftp://example.com');
expect(r.ok).toBe(false);
expect(r.error).toContain('http');
});
it('returns honest failure for unreachable host (no throw)', async () => {
const r = await fetchUrlDirect('http://127.0.0.1:1', { timeoutMs: 1500 });
expect(r.ok).toBe(false);
expect(typeof r.error).toBe('string');
}, 10_000);
});