Files
homeclaw/tests/gpu-specs.test.ts
T
kimandClaude Opus 5 231ecbfce6 feat: GPU 스펙 구조화 DB를 검색 결과에 자동 주입
"시어엔진에 이런 데이터 전용 API가 있나" → 253개 엔진 중 하드웨어 스펙
엔진은 0개였다(geizhals는 403 봇차단, ddg definitions는 GPU 모델명에 빈
응답). 대신 GitHub에서 RightNow-AI/RightNow-GPU-Database(Apache 2.0,
TechPowerUp 기반, 2,824개 GPU, 55필드)를 찾았고, README엔 언급이 없지만
실물엔 RTX 50 시리즈가 들어있는 것을 데이터를 받아 확인했다.

이게 지금까지의 우회로보다 나은 이유:
- nvidia.com 공식 페이지는 스펙 키워드 0개(JS 렌더링), techpowerup은
  봇 차단. HTML 스크래핑으로 살아있는 출처가 nanoreview 하나뿐이었다
- 봇차단·JS·파싱실패·죽은링크가 전부 없고, 캐시 워밍 후 64ms
- 무엇보다 모든 레코드에 releaseDate가 있다. 5060은 2025-05-19다.
  이 사태의 출발점이던 "5060은 미출시" 환각은 데이터와 만나는 순간
  성립하지 않는다 — 가드는 쓰인 거짓말을 잡지만, 이건 쓸 이유를 없앤다

신선도가 진짜 리스크라 거기에 설계를 집중했다. 호스팅 API가 아니라 GitHub
저장소라서, 저쪽이 멈추면 우리도 멈추고 그러면 지금 고치는 실패가 데이터
계층에서 재현된다. 그래서 주간 갱신 + 결과에 캐시 나이를 항상 같이 실어
낡은 답이 조용히 틀리는 대신 눈에 띄게 낡도록 했다. 갱신은 백그라운드라
질문을 느리게 만들지 않고, 실패해도 옛 캐시로 계속 답한다.

도구가 아니라 주입으로 넣은 이유: 모델이 도구를 안 부른다. 로그 3,300줄
에서 web_fetch 호출 1번, GPU 질문엔 0번이었다(오늘 같은 결론 네 번째).

- extractGpuMentions: 맥락이 있을 때만 맨숫자를 모델명으로 본다.
  "4080이 5060보다"는 잡고 "2024년 매출 3800억"은 안 잡는다 — 무관한
  답변에 스펙이 끼어들면 그 자체가 오염이다
- findGpu: "RTX 4080"은 SUPER/Mobile 이름에도 부분일치하므로 변형에
  가중치를 줘 기본 카드를 고른다. 데스크탑 질문에 노트북 칩 수치를
  물려주면 조용히 틀린 답이 된다
- 캐시 쓰기는 write-then-rename (config.json 3회 손상과 같은 실패 방지)

실측: RTX 4080 FP32 48.74 TFLOPS / 716.8 GB/s vs RTX 5060 19.18 TFLOPS /
448 GB/s — 2.54배 차이를 근거를 갖고 말할 수 있게 됐다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 14:34:34 +09:00

107 lines
4.9 KiB
TypeScript

/**
* gpu-specs — 모델명 인식과 카드 선택
*
* 순수 함수만 테스트한다. 다운로드/캐시는 네트워크와 디스크에 의존하므로 여기서 다루지 않는다.
*
* 이 테스트가 지키는 두 가지:
* 1) 맨숫자를 언제 모델명으로 볼 것인가 — "4080이 5060보다 빠른가"는 잡아야 하지만
* "2024년 실적"과 "1080p 영상"은 잡으면 안 된다. 무관한 답변에 GPU 스펙이 끼어들면
* 그 자체가 오염이다.
* 2) 변형 모델 선택 — "RTX 4080"은 SUPER/Mobile 이름에도 부분일치하는데, 데스크탑 질문에
* 노트북 칩 수치를 물려주면 조용히 틀린 답이 된다.
*/
import { test, describe } from 'node:test';
import assert from 'node:assert/strict';
import { extractGpuMentions, findGpu, formatGpuRecord, type GpuRecord } from '../src/tools/gpu-specs';
const GPUS: GpuRecord[] = [
{ name: 'GeForce RTX 4080', vendor: 'nvidia', releaseDate: '2022-09-20', architecture: 'Ada Lovelace', processSize: 5, shaders: 9728, tensorCores: 304, baseClock: 2205, boostClock: 2505, memorySize: 16, memoryType: 'GDDR6X', memoryBus: 256, memoryBandwidth: 716.8, fp32: 48.74, tdp: 320, url: 'https://example.invalid/4080' },
{ name: 'GeForce RTX 4080 SUPER', vendor: 'nvidia', releaseDate: '2024-01-31' },
{ name: 'GeForce RTX 4080 Mobile', vendor: 'nvidia', releaseDate: '2023-02-22' },
{ name: 'GeForce RTX 5060', vendor: 'nvidia', releaseDate: '2025-05-19', memoryBandwidth: 448, fp32: 19.18 },
{ name: 'GeForce RTX 5060 Ti 16 GB', vendor: 'nvidia', releaseDate: '2025-04-16' },
{ name: 'GeForce RTX 5060 Mobile', vendor: 'nvidia', releaseDate: '2025-05-01' },
{ name: 'Radeon RX 7900 XTX', vendor: 'amd', releaseDate: '2022-12-13' },
{ name: 'Radeon RX 7900 XT', vendor: 'amd', releaseDate: '2022-12-13' },
];
describe('extractGpuMentions — 모델명 인식', () => {
test('시리즈 표기가 붙은 형태', () => {
const m = extractGpuMentions('RTX 4080 vs RTX 5060 performance comparison specs');
assert.deepEqual(m.map(x => `${x.series} ${x.number}`), ['rtx 4080', 'rtx 5060']);
});
test('접미사(Ti/SUPER/XTX)를 보존한다', () => {
assert.equal(extractGpuMentions('RTX 5060 Ti 리뷰')[0].suffix, 'ti');
assert.equal(extractGpuMentions('RX 7900 XTX 성능')[0].suffix, 'xtx');
});
test('GPU 맥락이 있으면 맨숫자도 모델명으로 본다', () => {
// 사용자가 실제로 친 형태. "RTX"가 한 번만 나오고 나머지는 숫자만 쓴다.
const m = extractGpuMentions('RTX 4080이 5060 보다 얼마나 빠르지?');
assert.deepEqual(m.map(x => x.number).sort(), ['4080', '5060']);
});
test('GPU 맥락이 없으면 맨숫자를 건드리지 않는다', () => {
assert.deepEqual(extractGpuMentions('2024년 매출이 3800억이었다'), []);
assert.deepEqual(extractGpuMentions('아파트 3800세대 분양'), []);
});
test('연도 꼴은 맥락이 있어도 모델명으로 보지 않는다', () => {
const m = extractGpuMentions('이 GPU는 2022년에 나왔다');
assert.deepEqual(m, []);
});
test('중복은 한 번만', () => {
const m = extractGpuMentions('RTX 4080 성능, RTX 4080 가격');
assert.equal(m.length, 1);
});
});
describe('findGpu — 변형 모델에 밀리지 않고 기본 카드를 고른다', () => {
const pick = (q: string) => findGpu(extractGpuMentions(q)[0], GPUS)?.name;
test('"RTX 4080"은 SUPER나 Mobile이 아니라 기본 카드', () => {
assert.equal(pick('RTX 4080 specs'), 'GeForce RTX 4080');
});
test('"RTX 5060"은 Ti나 Mobile이 아니라 기본 카드', () => {
assert.equal(pick('RTX 5060 specs'), 'GeForce RTX 5060');
});
test('접미사를 명시하면 그 변형을 고른다', () => {
assert.equal(pick('RTX 4080 SUPER 스펙'), 'GeForce RTX 4080 SUPER');
assert.equal(pick('RTX 5060 Ti 스펙'), 'GeForce RTX 5060 Ti 16 GB');
});
test('XT와 XTX를 구분한다', () => {
assert.equal(pick('RX 7900 XTX 성능'), 'Radeon RX 7900 XTX');
assert.equal(pick('RX 7900 XT 성능'), 'Radeon RX 7900 XT');
});
test('데이터에 없는 모델은 null', () => {
assert.equal(findGpu({ series: 'rtx', number: '9090', suffix: '', raw: 'RTX 9090' }, GPUS), null);
});
});
describe('formatGpuRecord — 판단에 필요한 필드만', () => {
const out = formatGpuRecord(GPUS[0]);
test('출시일이 들어간다 — "미출시" 환각을 원천 차단하는 필드다', () => {
assert.match(out, /2022-09-20/);
});
test('비교에 쓰이는 수치가 들어간다', () => {
assert.match(out, /716\.8 GB\/s/);
assert.match(out, /FP32 48\.74 TFLOPS/);
assert.match(out, /9728/);
});
test('없는 필드는 빈 줄로 남기지 않는다', () => {
const sparse = formatGpuRecord({ name: 'GeForce RTX 5060', releaseDate: '2025-05-19' });
assert.equal(sparse.split('\n').some(l => l.trim() === ''), false);
assert.match(sparse, /2025-05-19/);
});
});