/** * weather-multi-location.test.ts * * What a weather tool says when `location` holds several cities instead of one. * * Asked to compare cities, models batch them into a single argument * ("London, Berlin, Paris, Rome, Madrid"). That geocodes to nothing, and the old error — * a bare "위치를 찾을 수 없습니다" — gave the model no reason to do anything but reissue the * identical call, so the turn looped until it ran out of rounds (observed live 2026-08-09 09:09). * * The property under test is not "does it fail" but "does the failure tell the model how to * recover", plus its mirror: inputs that are legitimately one place must not be mistaken for * a list. These tests hit the real Open-Meteo geocoding API, so they are skipped when it is * unreachable rather than failing the suite on a network blip. */ import { test, describe, before } from 'node:test'; import assert from 'node:assert/strict'; import { weatherOpenMeteoTool } from '../src/tools/weather'; const ask = (location: string) => weatherOpenMeteoTool.execute({ location, variables: 'temperature_2m_max' }) as Promise<{ success: boolean; error?: string; }>; let online = false; before(async () => { try { const res = await fetch('https://geocoding-api.open-meteo.com/v1/search?name=Seoul&count=1', { signal: AbortSignal.timeout(8_000), }); online = res.ok; } catch { online = false; } if (!online) console.log(' (geocoding API 접속 불가 — 네트워크 의존 테스트 건너뜀)'); }); describe('배치 입력은 복구 방법을 알려준다', () => { test('여러 도시를 한 번에 넘기면 도구별 호출을 지시한다', async (t) => { if (!online) return t.skip('offline'); const r = await ask('London, Berlin, Paris, Rome, Madrid'); assert.equal(r.success, false); const err = r.error ?? ''; // 무엇이 잘못됐는지 + 어떻게 고치는지 + 재시도가 무의미하다는 것, 세 가지가 다 있어야 // 모델이 같은 호출을 반복하지 않는다. assert.match(err, /한 번에 한 곳만/, '단일 위치 제약을 밝혀야 함'); assert.match(err, /weather_openmeteo/, '어떤 도구를 다시 부를지 알려야 함'); assert.match(err, /"location":"London"/, '구체적인 재호출 예시가 있어야 함'); assert.match(err, /다시 호출하면 똑같이 실패/, '동일 재시도가 무의미함을 밝혀야 함'); // 파싱한 도시를 전부 되돌려줘야 모델이 남은 도시를 안 빠뜨린다. for (const city of ['London', 'Berlin', 'Paris', 'Rome', 'Madrid']) { assert.ok(err.includes(city), `${city}가 목록에 없음`); } }); }); describe('단일 위치는 목록으로 오해하지 않는다', () => { test('"City, Country"는 정상 조회된다', async (t) => { if (!online) return t.skip('offline'); assert.equal((await ask('Seoul, Korea')).success, true); }); test('"City,CountryCode"는 정상 조회된다', async (t) => { if (!online) return t.skip('offline'); assert.equal((await ask('London,GB')).success, true); }); test('쉼표가 있는 좌표는 정상 조회된다', async (t) => { if (!online) return t.skip('offline'); assert.equal((await ask('37.5,127.0')).success, true); }); }); describe('진짜 없는 지명은 기존 메시지를 유지한다', () => { test('배치가 아닌 미확인 지명에는 배치 안내를 붙이지 않는다', async (t) => { if (!online) return t.skip('offline'); const r = await ask('Zzxqwv'); assert.equal(r.success, false); assert.match(r.error ?? '', /위치를 찾을 수 없습니다/); assert.doesNotMatch(r.error ?? '', /한 번에 한 곳만/, '단일 지명에 배치 안내가 붙으면 안 됨'); }); });