fix: weather_openmeteo가 maxHourlyRows로 자른 걸 숨기던 문제

hours=168처럼 도구 스펙상 정상 범위(최대 384)를 요청해도 시간별 표는 maxHourlyRows(기본 48)에서
조용히 잘렸는데, 헤더는 여전히 '지금부터 168시간'이라고 적어 잘렸다는 사실을 모델에게 숨겼다.
그 결과 모델이 표에 없는 뒷부분을 지어내거나(다른 경로로 API를 재호출해 우회) 반대로 '데이터
부족'이라 정직하게 오보고했다(2026-09-27 실측, gemini-3.5-flash-lite로 '한주간 예보' 질의).

maxHourlyRows가 실제 병목(API엔 더 있는데 표만 자른 경우)일 때만 잘렸다고 명시하고 daily 변수로
재요청하라고 안내. API가 애초에 적게 준 경우(정상적 데이터 부족)는 기존 동작 유지 — 이 구분을
놓친 첫 시도는 기존 테스트가 실패로 잡아줬다. 회귀 테스트 2건 추가, 전체 736개 통과.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
kim
2026-09-27 22:44:23 +09:00
co-authored by Claude Sonnet 5
parent 85254e5f0d
commit 54461fd26b
2 changed files with 58 additions and 3 deletions
+16 -3
View File
@@ -177,9 +177,22 @@ export function formatOpenMeteoReport(input: OpenMeteoReportInput): string {
const hourlyTimes: string[] = hourlyBlock?.time || [];
if (hourlyVars.length && hourlyTimes.length) {
const shown = Math.min(hours, maxHourlyRows, hourlyTimes.length);
// Say plainly that this window starts now, so its max is never mistaken for a daily max.
out.push('', `[시간별] 지금부터 ${hours}시간 (달력상 하루가 아니라 현재 시각 기준 연속 구간)`);
// "available"은 API가 실제로 준 것과 요청한 hours 중 작은 쪽(정상적인 부족) — 기존에도 이 경우
// 헤더는 그냥 hours를 그대로 적었다(위 테스트 고정 동작, 건드리지 않음). maxHourlyRows가 진짜
// 병목일 때만 — 즉 API엔 더 있는데 우리가 표만 줄인 경우만 — 아래서 별도로 알린다.
const available = Math.min(hours, hourlyTimes.length);
const shown = Math.min(available, maxHourlyRows);
// maxHourlyRows(기본 48)가 병목이면 표에 안 보이는 뒷부분이 있다는 걸 명시해야 한다 — 예전엔
// 조용히 잘라내면서 헤더엔 "지금부터 {hours}시간"이라고 그대로 적어서, 168시간을 요청했는데
// 표는 48행(2일)에서 끊긴 걸 모델이 알 방법이 없었다. 그러면 나머지 날짜를 지어내거나(다른
// 모델은 직접 API를 다시 불러 우회), 반대로 "데이터 부족"이라고 정직하게 보고했는데 실은
// Open-Meteo엔 데이터가 있고 이 함수가 버린 것뿐이었다(실측 2026-09-27, gemini-3.5-flash-lite,
// hours=168 요청 → 48시간에서 끊김).
if (shown < available) {
out.push('', `[시간별] 지금부터 ${shown}시간만 표시(API엔 ${available}시간치가 더 있으나 표가 길어지지 않게 자름). 그 이후 날짜까지 필요하면 daily 변수(temperature_2m_max/min, precipitation_probability_max 등)를 days 파라미터와 함께 요청할 것 — 아래 [일별] 구간은 이 제한이 없음.`);
} else {
out.push('', `[시간별] 지금부터 ${hours}시간 (달력상 하루가 아니라 현재 시각 기준 연속 구간)`);
}
out.push(`단위: ${hourlyVars.map(k => `${k}=${hourlyUnits[k] || '-'}`).join(', ')}`);
out.push(['시각', ...hourlyVars.map(k => k.replace(/_/g, ' '))].join('\t'));
let lastDate = '';
+42
View File
@@ -264,4 +264,46 @@ describe('formatOpenMeteoReport — 베를린 실패 사례', () => {
assert.match(text, /시각\ttemperature 2m/);
assert.equal(text.split('\n').filter(l => l.startsWith('2026-')).length, 5);
});
// 2026-09-27 실측 회귀: hours=168(일주일) 요청 + API가 168시간치를 실제로 다 줬는데도
// maxHourlyRows(기본 48)에 걸려 표가 2일치에서 끊겼다. 그런데 헤더는 여전히 "지금부터 168시간"
// 이라고 적어서, 모델이 표에 없는 뒷부분을 데이터 자체가 없는 것으로 오인했다(gemini-3.5-flash-lite
// 가 "데이터 부족"이라 정직하게 보고 — 모델 잘못이 아니라 이 함수가 잘라낸 걸 숨긴 게 원인).
test('maxHourlyRows가 API 데이터보다 작으면(진짜 병목) 잘렸다고 명시한다', () => {
const times = Array.from({ length: 168 }, (_, i) => {
const d = new Date(Date.UTC(2026, 8, 27) + i * 3600_000);
return d.toISOString().slice(0, 16);
});
const text = formatOpenMeteoReport({
displayName: '구미',
timezone: 'Asia/Seoul',
utcOffsetSeconds: 9 * 3600,
hours: 168,
hourlyVars: ['temperature_2m'],
dailyVars: [],
hourlyBlock: { time: times, temperature_2m: times.map(() => 20) },
now,
});
assert.match(text, /48시간만 표시/);
assert.match(text, /168시간치가 더 있으나/);
assert.match(text, /daily 변수/);
});
// 반대로 API가 요청보다 적게 줬을 뿐(진짜 병목이 아님)이면 기존 동작(헤더에 요청 hours 그대로)을
// 유지한다 — "시간별 구간은 '달력상 하루가 아님'이 명시된다" 테스트가 이 케이스를 이미 고정한다.
test('API 데이터가 hours보다 적을 뿐이면 잘렸다는 문구를 붙이지 않는다', () => {
const times = ['2026-07-29T15:00', '2026-07-29T23:00'];
const text = formatOpenMeteoReport({
displayName: '구미',
timezone: 'Asia/Seoul',
utcOffsetSeconds: 9 * 3600,
hours: 24,
hourlyVars: ['temperature_2m'],
dailyVars: [],
hourlyBlock: { time: times, temperature_2m: times.map(() => 20) },
now,
});
assert.doesNotMatch(text, /표가 길어지지 않게/);
assert.match(text, /지금부터 24시간 \(/);
});
});