/** * link-validator.test.ts * * rewriteDeadLinks was extracted from validateLinksInText (2026-08-10) specifically so this * bug could be pinned: models very commonly cite a URL using the URL itself as the link label, * [https://x.com](https://x.com). The old BARE_RE's character class didn't stop at `]`/`[`, so * a bare-URL match starting right after the label's `[` ran straight through the label's `]`, * the href's `(`, and the whole href — merging two markdown tokens into one match. Wrapping * that in ~~...~~ produced garbage the user actually saw in production: * "~~[~~https://stormatlasx.com/ko](https://stormatlasx.com/ko~~ ⚠️ (링크 끊김)))~~ ⚠️ ...". */ import { test, describe } from 'node:test'; import assert from 'node:assert/strict'; import { rewriteDeadLinks } from '../src/gateway/guards/link-validator'; describe('rewriteDeadLinks', () => { test('링크 텍스트가 없으면 원문 그대로', () => { const text = '아무 링크도 없는 평범한 답변입니다.'; assert.equal(rewriteDeadLinks(text, new Map()), text); }); test('전부 살아있으면 원문 그대로', () => { const text = '자세한 내용은 [기상청](https://weather.go.kr)에서 확인하세요.'; assert.equal(rewriteDeadLinks(text, new Map([['https://weather.go.kr', true]])), text); }); test('일반 마크다운 링크 — 라벨이 URL과 다르면 정상적으로 취소선 처리', () => { const text = '자세한 내용은 [여기](https://dead.example.com)에서 확인하세요.'; const out = rewriteDeadLinks(text, new Map([['https://dead.example.com', false]])); assert.equal(out, '자세한 내용은 ~~[여기](https://dead.example.com)~~ ⚠️ (링크 끊김)에서 확인하세요.'); }); test('죽은 이미지는 경고 문구로 치환', () => { const text = '![낙원 사진](https://dead.example.com/x.jpg)'; const out = rewriteDeadLinks(text, new Map([['https://dead.example.com/x.jpg', false]])); assert.equal(out, '⚠️ 이미지 불러올 수 없음: 낙원 사진'); }); test('평문 URL은 취소선으로 치환', () => { const text = '참고: https://dead.example.com/page 여기 있습니다.'; const out = rewriteDeadLinks(text, new Map([['https://dead.example.com/page', false]])); assert.equal(out, '참고: ~~https://dead.example.com/page~~ ⚠️ (링크 끊김) 여기 있습니다.'); }); // 실제 프로덕션 사고 재현 (2026-08-10, 세션 papa, "16호 태풍" 질문 답변). test('라벨이 URL 자체인 죽은 링크도 깨지지 않고 정상 처리된다', () => { const url = 'https://stormatlasx.com/ko'; const text = `Storm Atlas (실시간 경로 추적): [${url}](${url})`; const out = rewriteDeadLinks(text, new Map([[url, false]])); assert.equal(out, `Storm Atlas (실시간 경로 추적): ~~[${url}](${url})~~ ⚠️ (링크 끊김)`); // 회귀 방지: 예전 버그의 특징적 증상 — 마크다운 링크 경계를 넘어 뒤섞인 취소선/괄호. assert.ok(!out.includes(')~~ ⚠️ (링크 끊김))'), '마크다운 경계를 넘어 뒤섞이면 안 됨'); assert.equal((out.match(/⚠️/g) || []).length, 1, '경고 문구가 중복되면 안 됨'); }); test('URL을 포함한 괄호가 있는 일반 URL은 여전히 매칭된다(회귀 확인)', () => { const text = '위키백과: https://en.wikipedia.org/wiki/Foo_bar 참고'; const out = rewriteDeadLinks(text, new Map([['https://en.wikipedia.org/wiki/Foo_bar', false]])); assert.equal(out, '위키백과: ~~https://en.wikipedia.org/wiki/Foo_bar~~ ⚠️ (링크 끊김) 참고'); }); test('여러 링크가 연달아 있어도 서로 침범하지 않는다', () => { const text = '[A](https://a.example.com) [https://b.example.com](https://b.example.com)'; const out = rewriteDeadLinks(text, new Map([ ['https://a.example.com', true], ['https://b.example.com', false], ])); assert.equal(out, '[A](https://a.example.com) ~~[https://b.example.com](https://b.example.com)~~ ⚠️ (링크 끊김)'); }); });