', '
'),
+ ],
+ ])('%s이면 일부나 빈 목록 대신 미확인(null)으로 반환한다', (_, html) => {
+ expect(parseOliveyoungUpcomingDeals(html)).toBeNull();
+ });
+ it('페이지 요청 하나로 예고를 함께 반환하고 예고를 읽지 못해도 오특은 성공한다', async () => {
+ for (const [html, expected] of [
+ [page + comingSoon, 3],
+ [page, null],
+ ] as const) {
+ const fetch = vi.fn(
+ async (url: string) =>
+ new Response(
+ url.includes('getHotdealList.do') ? html : url.includes('pageIdx=1') ? list1 : list3,
+ ),
+ );
+ vi.stubGlobal('fetch', fetch);
+ const result = await fetchOliveyoungTodayDeals({}, now);
+ expect(fetch).toHaveBeenCalledTimes(3);
+ expect(result.todayDeals).toHaveLength(17);
+ if (expected === null) expect(result.upcomingDeals).toBeNull();
+ else expect(result.upcomingDeals).toHaveLength(expected);
+ }
+ });
+});
diff --git a/tests/services/oliveyoung/tools/getTodayDeals.test.ts b/tests/services/oliveyoung/tools/getTodayDeals.test.ts
index becbc42f..9a0f47fc 100644
--- a/tests/services/oliveyoung/tools/getTodayDeals.test.ts
+++ b/tests/services/oliveyoung/tools/getTodayDeals.test.ts
@@ -28,6 +28,12 @@ const deal = {
imageUrl: 'https://image.oliveyoung.co.kr/a.jpg',
productUrl: 'https://www.oliveyoung.co.kr/store/goods/getGoodsDetail.do?goodsNo=A000000171422',
};
+const upcoming = {
+ goodsNo: 'A000000202414',
+ name: '라보에이치 헤어라인앰플 더블기획',
+ imageUrl: 'https://image.oliveyoung.co.kr/A00000020241475ko.jpg',
+ productUrl: 'https://www.oliveyoung.co.kr/store/goods/getGoodsDetail.do?goodsNo=A000000202414',
+};
beforeEach(() => {
fetchOliveyoungTodayDeals.mockReset();
@@ -36,6 +42,7 @@ beforeEach(() => {
brandDeals: [deal],
todayDeals: [deal, { ...deal, rank: 2 }],
truncated: false,
+ upcomingDeals: [upcoming, { ...upcoming, goodsNo: null, productUrl: null }],
});
});
@@ -50,8 +57,10 @@ describe('createGetTodayDealsTool', () => {
date: '2026-10-26',
brandDealCount: 1,
todayDealCount: 2,
+ upcomingDealCount: 2,
truncated: false,
brandDeals: [deal],
+ upcomingDeals: [upcoming, { ...upcoming, goodsNo: null, productUrl: null }],
});
expect(body.note).toContain('check_inventory');
expect(result.structuredContent).toEqual(body);
@@ -70,6 +79,19 @@ describe('createGetTodayDealsTool', () => {
await createGetTodayDealsTool({ timeout: 9000 }).handler({});
expect(fetchOliveyoungTodayDeals).toHaveBeenLastCalledWith({ timeout: 9000 });
});
+ it('예고를 확인하지 못하면 빈 목록이 아니라 미확인(null)으로 반환한다', async () => {
+ fetchOliveyoungTodayDeals.mockResolvedValue({
+ date: '2026-10-26',
+ brandDeals: [deal],
+ todayDeals: [deal],
+ truncated: false,
+ upcomingDeals: null,
+ });
+ const tool = createGetTodayDealsTool();
+ const { structuredContent: body } = await tool.handler({});
+ expect(body).toMatchObject({ upcomingDealCount: null, upcomingDeals: null });
+ expect(z.object(tool.metadata.outputSchema!).parse(body)).toEqual(body);
+ });
it('원본 실패를 빈 목록으로 바꾸지 않는다', async () => {
fetchOliveyoungTodayDeals.mockRejectedValue(new Error('올리브영 직접 요청 실패'));
await expect(createGetTodayDealsTool().handler({})).rejects.toThrow('직접 요청 실패');
diff --git a/vitest.config.ts b/vitest.config.ts
index cf96160a..2617e7de 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -40,6 +40,7 @@ export default defineConfig({
'scripts/ops/mcp-smoke-validation.ts',
'scripts/relay/daiso-jp.ts',
'scripts/ops/health-check-retry.ts',
+ 'scripts/ops/oliveyoung-coming-soon-check.ts',
'src/**/*.ts',
'scripts/relay/cache.ts',
'scripts/relay/logging.ts',