',
+ '',
`
Cloudflare 요청 수 (${startDate} ~ ${endDate}, ${days}일)
`,
'',
- `

`,
+ `

`,
'',
`
기준 워커: ${scriptName} · 마지막 갱신: ${updatedAt}`,
`
집계: ${includesRootRedirect ? 'Worker 실행 + 루트 GET 리디렉션 요청' : 'Worker 실행'} · 사용자 수와 다릅니다.`,
@@ -308,8 +310,6 @@ export function buildReadmeSection({
'> [!IMPORTANT]',
'> 최근 공개 서버 사용량이 크게 증가하여 2026년 7월 18일부터 올리브영·CGV·CU·GS25의 검색을 포함한 공개 GET API에 IP당 하루 합산 3,000회(KST 기준)의 호출 제한을 적용합니다. 한도를 초과하는 사용이 필요하다면 Daiso MCP는 오픈 소스이므로 이 저장소를 직접 배포해 이용해 주세요.',
'',
- '
',
- '',
'',
].join('\n');
}
diff --git a/skills/daiso-cli/SKILL.md b/skills/daiso-cli/SKILL.md
index b3fa8424..bb02fea6 100644
--- a/skills/daiso-cli/SKILL.md
+++ b/skills/daiso-cli/SKILL.md
@@ -1,6 +1,6 @@
---
name: daiso-cli
-description: Use this when a user wants to search Daiso/다이소, compare product price candidates, nearby restaurants/cafes/음식점/카페, convenience stores/편의점, marts, Olive Young/올리브영, Megabox/메가박스, Lotte Cinema/롯데시네마, or CGV data through the Daiso project. Prefer the daiso CLI for direct execution, use the MCP endpoint when the host app supports remote MCP, and choose commands for products, stores, inventory/재고, compare, places, movies, showtimes, seats, health checks, and raw JSON output.
+description: Use this when a user wants to search Daiso/다이소, compare product price candidates, nearby restaurants/cafes/음식점/카페, convenience stores/편의점, marts, Olive Young/올리브영, Megabox/메가박스, Lotte Cinema/롯데시네마, or CGV data through the Daiso project. Honor explicit no-install requests with remote MCP first; otherwise prefer the daiso CLI for direct execution, use the MCP endpoint when the host app supports remote MCP, and choose commands for products, stores, inventory/재고, compare, places, movies, showtimes, seats, health checks, and raw JSON output.
version: 1.0.6
metadata:
openclaw:
@@ -19,11 +19,28 @@ metadata:
Use this skill to operate the Daiso MCP project through `npx daiso` and the public MCP endpoint.
+## 설치 없이 첫 조회
+
+사용자가 "설치 없이", "무설치"를 명시하면 로컬 CLI·스킬 설치 없이 원격 조회를 먼저 실행합니다. 연결된 원격 MCP 도구가 있으면 `daiso_search_products`를 바로 호출합니다. 원격 MCP 클라이언트의 서버 주소는 `https://mcp.aka.page`입니다.
+
+셸에서 직접 확인할 때는 아래 POST가 실제 MCP 도구 호출입니다. 이 공개 서버는 세션 없이 호출할 수 있으며 응답은 SSE의 `data:` JSON입니다. 범용 MCP 클라이언트 연결은 클라이언트가 initialize와 세션 절차를 처리합니다.
+
+```bash
+curl -fsS --max-time 20 'https://mcp.aka.page' \
+ -H 'Content-Type: application/json' \
+ -H 'Accept: application/json, text/event-stream' \
+ --data-binary '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"daiso_search_products","arguments":{"query":"수납박스","pageSize":3}}}'
+```
+
+첫 조회 전에 `npx`, 패키지·스킬 설치, health/help, 전체 스키마나 도구 목록 탐색을 하지 않습니다. 위 예제의 검색어를 사용자 요청으로 바꾸고 조회 결과부터 제공합니다. 사용자 조건이나 호출 오류 때문에 필요한 경우에만 추가 안내를 읽습니다. 호출에 성공하면 추가 진단 없이 결과를 보여줍니다.
+
+`GET https://mcp.aka.page/api/daiso/products?q=수납박스`는 같은 서비스의 REST 직접 조회입니다. GET만 가능한 환경에서는 사용할 수 있지만 MCP 도구를 호출했다고 설명하지 않습니다. HTTP·네트워크 오류만으로 로그인 필요나 서버 장애를 단정하지 않고 실제 오류와 실행 환경을 확인합니다.
+
## Core Rule
-CLI를 우선 사용한다. The CLI is the most reliable path when you can run shell commands. Use the MCP endpoint when the user is configuring an AI app or explicitly asks for MCP connection details.
+무설치 요청이 아니고 CLI 사용이 허용된 환경에서는 CLI를 우선 사용한다. The CLI is the preferred path when you can run shell commands and package execution is allowed. Use the MCP endpoint when the user is configuring an AI app or explicitly asks for MCP connection details.
-## Quick Checks
+## Quick Checks (오류 진단이 필요할 때만)
```bash
npx daiso health
@@ -65,6 +82,8 @@ For more command selection examples, read `references/cli-command-map.md`.
## Request Recipes
+아래 CLI 레시피는 CLI 사용이 허용된 경우에만 적용합니다. 무설치 요청에서는 같은 조회를 원격 MCP 도구로 실행합니다.
+
- "콜라 어디가 싸?", "컵라면 가격 비교해줘": run `npx daiso compare --json` first. Confirm stock or sale prices with service-specific inventory only when the user asks for store-level availability.
- "강남역 근처 카페", "성수동 브런치 음식점": run `npx daiso places --category cafe|restaurant --json` or use `--keyword` for the food or mood. Do not ask which retail brand they mean.
- "다이소 핫식스 재고", "올리브영 두바이초콜릿 있어?": keep the named brand. Search that service first even if the product seems unusual for the brand, then suggest alternatives only after no results.
diff --git a/src/index.ts b/src/index.ts
index 8e02efbc..0736befb 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -35,6 +35,7 @@ import { createPlacesService } from './services/places/index.js';
import { createOpinetService } from './services/opinet/index.js';
import { createCompareService } from './services/compare/index.js';
import { createFeedbackService } from './services/feedback/index.js';
+import { createAgentStartResponse } from './pages/agentStart.js';
import { createPromptResponse } from './pages/prompt.js';
import {
createFullOpenApiJsonResponse,
@@ -152,6 +153,7 @@ function buildRootInfo() {
description: 'Multi-Service MCP Server for Cloudflare Workers',
endpoints: {
mcp: '/ 또는 /mcp (POST) - MCP 프로토콜 엔드포인트',
+ quickStart: '/llms.txt (GET) - 설치 없는 첫 조회 안내',
health: '/health (GET) - 헬스 체크',
healthChecks: '/api/health/checks (GET) - 서비스별 상세 헬스 체크',
openapi: '/openapi.json (GET) - OpenAI Actions용 축약 OpenAPI',
@@ -355,6 +357,9 @@ registerRouteHealthRoutes(app);
registerRateLimitStatsRoutes(app);
registerServiceRoutes(app);
+// 설치 없는 첫 조회 안내
+app.get('/llms.txt', (c) => createAgentStartResponse(new URL(c.req.url).origin));
+
// 프롬프트 페이지 (MCP 미지원 에이전트용)
app.get('/prompt', (c) => {
const baseUrl = new URL(c.req.url).origin;
diff --git a/src/pages/agentStart.ts b/src/pages/agentStart.ts
new file mode 100644
index 00000000..98db9963
--- /dev/null
+++ b/src/pages/agentStart.ts
@@ -0,0 +1,40 @@
+/** 설치 없이 첫 실제 조회를 실행하는 에이전트 안내 */
+export function buildAgentStartText(baseUrl: string): string {
+ return `# Daiso MCP: 설치 없이 시작하기
+
+공개 원격 MCP: https://mcp.aka.page
+전송: Streamable HTTP · 인증: 없음 (사용자 API 키 불필요)
+전체 도구 목록이나 OpenAPI를 먼저 읽을 필요는 없습니다. 사용 가능한 실행 수단으로 아래 첫 조회를 실행하세요.
+
+1. 원격 MCP 클라이언트가 있으면 공개 URL에 연결하세요. 표준 initialize → notifications/initialized 흐름을 사용하고 서버가 발급한 mcp-session-id를 전달합니다. 첫 도구는 daiso_search_products, 인자는 {"query":"수납박스","pageSize":3}입니다.
+2. HTTP POST나 셸이 있으면 아래 한 요청으로 실제 MCP 도구를 호출할 수 있습니다. npm 설치는 필요 없습니다. 루트 POST의 stateless 호출은 이 서버가 지원하는 단축 경로이며, /mcp는 initialize가 필요합니다.
+
+\`\`\`bash
+curl --max-time 20 -sS -N ${baseUrl}/ \\
+ -H 'Content-Type: application/json' \\
+ -H 'Accept: application/json, text/event-stream' \\
+ --data '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"daiso_search_products","arguments":{"query":"수납박스","pageSize":3}}}'
+\`\`\`
+
+응답은 SSE의 data: JSON-RPC 메시지입니다. result.isError 또는 error를 확인하고, 상품은 result.structuredContent.products에서 읽으세요. content[].text에도 상품 JSON이 있습니다. 오류를 상품 없음으로 해석하지 마세요.
+
+3. HTTP GET만 가능하면 공개 REST GET을 사용하세요. 이것은 MCP 프로토콜 호출이 아닙니다.
+${baseUrl}/api/daiso/products?q=%EC%88%98%EB%82%A9%EB%B0%95%EC%8A%A4&pageSize=3
+
+실제 상품 결과의 id로 재고를 조회하세요. 예제 ID를 쓰거나 상품 검색 결과만으로 매장 재고를 단정하지 마세요.
+상세 API와 서비스 안내: ${baseUrl}/prompt
+
+---
+
+`;
+}
+
+/** 캐시 가능한 시작 안내 응답을 생성합니다. */
+export function createAgentStartResponse(baseUrl: string): Response {
+ return new Response(buildAgentStartText(baseUrl), {
+ headers: {
+ 'Content-Type': 'text/plain; charset=utf-8',
+ 'Cache-Control': 'public, max-age=3600',
+ },
+ });
+}
diff --git a/src/pages/prompt.ts b/src/pages/prompt.ts
index 88e620a2..8745abab 100644
--- a/src/pages/prompt.ts
+++ b/src/pages/prompt.ts
@@ -5,6 +5,7 @@
* 에이전트가 이 페이지를 읽고 GET API를 사용할 수 있습니다.
*/
+import { buildAgentStartText } from './agentStart.js';
import { buildPromptCoreText } from './promptCoreText.js';
import { buildPromptReferenceText } from './promptReferenceText.js';
import { buildPromptRetailCinemaText } from './promptRetailCinemaText.js';
@@ -14,6 +15,7 @@ import { buildPromptRetailCinemaText } from './promptRetailCinemaText.js';
*/
export function generatePromptText(baseUrl: string): string {
return [
+ buildAgentStartText(baseUrl),
buildPromptCoreText(baseUrl),
buildPromptRetailCinemaText(baseUrl),
buildPromptReferenceText(baseUrl),
diff --git a/src/pages/promptCoreText.ts b/src/pages/promptCoreText.ts
index cd2fee0e..8fd97f22 100644
--- a/src/pages/promptCoreText.ts
+++ b/src/pages/promptCoreText.ts
@@ -6,7 +6,7 @@ export function buildPromptCoreText(baseUrl: string): string {
return `# 다이소 MCP API
다이소 제품 검색, 매장 찾기, 재고 확인을 위한 API입니다.
-모든 요청은 GET 방식이며, 결과는 JSON으로 반환됩니다.
+아래 상세 REST API 요청은 GET 방식이며, 결과는 JSON으로 반환됩니다.
Base URL: ${baseUrl}
diff --git a/tests/app/app-info-pages.test.ts b/tests/app/app-info-pages.test.ts
index f9fc0480..0cf03d90 100644
--- a/tests/app/app-info-pages.test.ts
+++ b/tests/app/app-info-pages.test.ts
@@ -18,6 +18,7 @@ describe('GET /', () => {
const data = await res.json();
expect(data.name).toBe('multi-service-mcp');
expect(data.version).toBe('1.0.0');
+ expect(data.endpoints.quickStart).toContain('/llms.txt');
expect(data.services).toBeDefined();
expect(data.tools).toBeDefined();
expect(data.totalServices).toBeGreaterThan(0);
@@ -370,6 +371,18 @@ describe('기본 페이지', () => {
});
describe('문서 페이지', () => {
+ it('GET /llms.txt는 외부 조회 없이 설치 없는 시작 안내를 반환한다', async () => {
+ const callsBefore = mockFetch.mock.calls.length;
+ const res = await app.fetch(new Request('https://guide.example/llms.txt'));
+ expect(res.status).toBe(200);
+ expect(res.headers.get('Content-Type')).toBe('text/plain; charset=utf-8');
+ expect(res.headers.get('Cache-Control')).toBe('public, max-age=3600');
+ const text = await res.text();
+ expect(text).toContain('https://guide.example/prompt');
+ expect(text).toContain('daiso_search_products');
+ expect(mockFetch.mock.calls.length).toBe(callsBefore);
+ });
+
it('GET /openapi.json 응답을 반환한다', async () => {
const res = await app.request('/openapi.json');
expect(res.status).toBe(200);
diff --git a/tests/app/app-mcp-client.test.ts b/tests/app/app-mcp-client.test.ts
index dda57406..205a7913 100644
--- a/tests/app/app-mcp-client.test.ts
+++ b/tests/app/app-mcp-client.test.ts
@@ -5,6 +5,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import app from '../../src/index.js';
+import { buildAgentStartText } from '../../src/pages/agentStart.js';
import type { AppBindings } from '../../src/api/response.js';
import { createMockProductResponse } from '../api/testHelpers.js';
@@ -48,6 +49,34 @@ async function createLocalMcpClient(bindings?: AppBindings): Promise {
}
describe('MCP client smoke', () => {
+ it('시작 안내의 단일 POST가 initialize 없이 실제 상품을 반환한다', async () => {
+ mockFetch.mockResolvedValueOnce(new Response(JSON.stringify(
+ createMockProductResponse([{ PD_NO: '1049516', PDNM: '수납박스', PD_PRC: '5000' }], 1),
+ )));
+ const guide = buildAgentStartText('https://local.test');
+ const body = guide.match(/--data '([^']+)'/)?.[1];
+ expect(body).toBeDefined();
+ const response = await app.fetch(new Request('https://local.test/', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream' },
+ body,
+ }));
+ expect(response.status).toBe(200);
+ expect(response.headers.get('mcp-session-id')).toBeNull();
+ expect(response.headers.get('Content-Type')).toContain('text/event-stream');
+ const dataLine = (await response.text()).split('\n').find((line) => line.startsWith('data: '));
+ expect(dataLine).toBeDefined();
+ const message = JSON.parse(dataLine!.slice(6));
+ expect(message.error).toBeUndefined();
+ expect(message.result.isError).not.toBe(true);
+ expect(message.result.structuredContent.products).toEqual([
+ expect.objectContaining({ id: '1049516', name: '수납박스', price: 5000 }),
+ ]);
+ expect(message.result.structuredContent.query).toBe('수납박스');
+ expect(message.result.structuredContent.pageSize).toBe(3);
+ expect(mockFetch).toHaveBeenCalledTimes(1);
+ });
+
it('SDK 도구 목록의 모든 도구가 outputSchema를 노출한다', async () => {
const client = await createLocalMcpClient();
try {
diff --git a/tests/pages/agentStart.test.ts b/tests/pages/agentStart.test.ts
new file mode 100644
index 00000000..37715dae
--- /dev/null
+++ b/tests/pages/agentStart.test.ts
@@ -0,0 +1,52 @@
+/** 설치 없는 에이전트 시작 안내 테스트 */
+import { describe, it, expect } from 'vitest';
+import { buildAgentStartText, createAgentStartResponse } from '../../src/pages/agentStart.js';
+import { generatePromptText } from '../../src/pages/prompt.js';
+
+describe('에이전트 시작 안내', () => {
+ it('첫 MCP 요청을 실행 가능한 JSON-RPC로 제공한다', () => {
+ const text = buildAgentStartText('https://example.com');
+ const body = text.match(/--data '([^']+)'/)?.[1];
+ expect(body).toBeDefined();
+ expect(JSON.parse(body!)).toEqual({
+ jsonrpc: '2.0', id: 1, method: 'tools/call',
+ params: { name: 'daiso_search_products', arguments: { query: '수납박스', pageSize: 3 } },
+ });
+ expect(text).toContain('https://example.com/');
+ expect(text).toContain('application/json, text/event-stream');
+ expect(text).toContain('structuredContent');
+ expect(text).toContain('isError');
+ });
+
+ it('원격 MCP 연결과 HTTP POST, REST GET을 구분하고 전체 문서 읽기를 요구하지 않는다', () => {
+ const text = buildAgentStartText('https://example.com');
+ expect(text).toContain('https://mcp.aka.page');
+ expect(text).toContain('Streamable HTTP');
+ expect(text).toContain('인증: 없음');
+ expect(text).toContain('initialize');
+ expect(text).toContain('stateless');
+ expect(text).toContain('REST GET');
+ expect(text).toContain('MCP 프로토콜 호출이 아닙니다');
+ expect(text).toContain('/api/daiso/products?q=%EC%88%98%EB%82%A9%EB%B0%95%EC%8A%A4&pageSize=3');
+ expect(text).toContain('https://example.com/prompt');
+ expect(text).toContain('전체 도구 목록이나 OpenAPI를 먼저 읽을 필요는 없습니다');
+ expect(text.split('\n').length).toBeLessThan(45);
+ });
+
+ it('동일한 시작 안내 뒤에 기존 상세 프롬프트를 보존한다', () => {
+ const baseUrl = 'https://example.com';
+ const text = generatePromptText(baseUrl);
+ expect(text.startsWith(buildAgentStartText(baseUrl))).toBe(true);
+ expect(text.indexOf('# 다이소 MCP API')).toBeGreaterThan(text.indexOf('REST GET'));
+ expect(text).toContain('/api/daiso/inventory?productId={제품ID}');
+ expect(text).toContain('MCP 지원 서비스');
+ });
+
+ it('캐시 가능한 평문 응답을 반환한다', async () => {
+ const response = createAgentStartResponse('https://example.com');
+ expect(response.status).toBe(200);
+ expect(response.headers.get('Content-Type')).toBe('text/plain; charset=utf-8');
+ expect(response.headers.get('Cache-Control')).toBe('public, max-age=3600');
+ expect(await response.text()).toBe(buildAgentStartText('https://example.com'));
+ });
+});
diff --git a/tests/scripts/repository-config.test.ts b/tests/scripts/repository-config.test.ts
index bf469d63..0a717ada 100644
--- a/tests/scripts/repository-config.test.ts
+++ b/tests/scripts/repository-config.test.ts
@@ -144,9 +144,7 @@ describe('repository maintenance configuration', () => {
expect(workflow).toContain(
'CLI_SMOKE_SERVICES: daiso gs25 seveneleven emart24 oliveyoung megabox lottecinema cgv opinet',
);
- expect(workflow).toContain(
- 'MCP_SMOKE_SERVICES: daiso gs25 seveneleven emart24 opinet',
- );
+ expect(workflow).toContain('MCP_SMOKE_SERVICES: daiso gs25 seveneleven emart24 opinet');
expect(workflow).not.toContain('lottemart');
expect(workflow).toContain('SMOKE_ATTEMPT_TIMEOUT_SECONDS: 90');
expect(workflow).toContain('for SMOKE_SERVICE in ${CLI_SMOKE_SERVICES}; do');
@@ -206,7 +204,7 @@ describe('repository maintenance configuration', () => {
expect(workflow).toContain('CLOUDFLARE_ZONE_ID');
expect(workflow).toContain('WORKERS_CHART_ROOT_REDIRECT_START');
expect(workflow).toContain(
- 'git add README.md assets/analytics/workers-invocations.json assets/analytics/workers-invocations.png',
+ 'git add scripts/ops/README.md assets/analytics/workers-invocations.json assets/analytics/workers-invocations.png',
);
expect(workflow).toContain('git pull --rebase --autostash origin main');
expect(workflow).toContain('git push origin HEAD:main');
@@ -225,7 +223,9 @@ describe('repository maintenance configuration', () => {
expect(workflow).toContain(
'/api/health/checks?mode=full&fresh=true&includeSamples=true&timeoutMs=20000&slowThresholdMs=9000',
);
- expect(workflow).toContain('/api/health/checks?mode=quick&fresh=true&timeoutMs=5000&slowThresholdMs=3000');
+ expect(workflow).toContain(
+ '/api/health/checks?mode=quick&fresh=true&timeoutMs=5000&slowThresholdMs=3000',
+ );
expect(workflow).toContain('HEALTH_CHECK_FORCE_FRESH="true"');
expect(workflow).not.toContain('HEALTH_CHECK_FORCE_FRESH="false"');
expect(workflow).toContain('x-health-check-force-fresh: ${HEALTH_CHECK_FORCE_FRESH}');
@@ -254,6 +254,7 @@ describe('repository maintenance configuration', () => {
for (const workflow of [ci, coverage, deploy, codeql]) {
expect(workflow).toContain('paths-ignore:');
expect(workflow).toContain("'README.md'");
+ expect(workflow).toContain("'scripts/ops/README.md'");
expect(workflow).toContain("'assets/analytics/**'");
}
});
@@ -297,10 +298,21 @@ describe('repository maintenance configuration', () => {
expect(readme).toContain('Daiso MCP 및 Skill');
expect(readme).toContain('skills/daiso-cli/SKILL.md');
expect(readme).toContain('npx daiso');
+ const fastStart = readme.split('\n').slice(0, 100).join('\n');
+ expect(fastStart).toContain('https://mcp.aka.page');
+ expect(fastStart).toContain('Streamable HTTP');
+ expect(fastStart).toContain('API 키·토큰·OAuth 설정 불필요');
+ expect(fastStart).toContain('로컬 설치·빌드·API 키 준비 없이');
+ expect(fastStart).toContain('Accept: application/json, text/event-stream');
+ expect(fastStart).toContain('"method":"tools/call"');
+ expect(fastStart).toContain('"name":"daiso_search_products"');
+ expect(fastStart).toContain('"query":"수납박스"');
+ expect(readme).not.toContain('WORKERS_INVOCATIONS_CHART:START');
+ expect(readme).toContain('scripts/ops/README.md');
});
it('운영 문서는 정확한 429 통계 조회 계약과 범위를 설명한다', () => {
- const readme = readText('README.md');
+ const readme = readText('scripts/ops/README.md');
const serviceReference = readText('docs/service-reference.md');
const readmeOperations = readme.slice(
readme.indexOf('### 운영 통계'),
@@ -312,17 +324,17 @@ describe('repository maintenance configuration', () => {
);
const curlBlockMatch = readmeOperations.match(/```bash\r?\n([\s\S]*?)\r?\n```/);
if (!curlBlockMatch?.[1]) {
- throw new Error('README 운영 통계 절에 `bash` 코드 블록 호출 예시가 필요합니다.');
+ throw new Error('운영 가이드 통계 절에 `bash` 코드 블록 호출 예시가 필요합니다.');
}
const jsonBlockMatch = readmeOperations.match(/```json\r?\n([\s\S]*?)\r?\n```/);
if (!jsonBlockMatch?.[1]) {
- throw new Error('README 운영 통계 절에 `json` 코드 블록 성공 응답 예시가 필요합니다.');
+ throw new Error('운영 가이드 통계 절에 `json` 코드 블록 성공 응답 예시가 필요합니다.');
}
let statsExample: unknown;
try {
statsExample = JSON.parse(jsonBlockMatch[1]);
} catch (error) {
- throw new Error(`README 운영 통계 JSON 예시를 파싱할 수 없습니다: ${String(error)}`);
+ throw new Error(`운영 가이드 통계 JSON 예시를 파싱할 수 없습니다: ${String(error)}`);
}
expect(readmeOperations).toContain('GET /api/rate-limit/stats');
diff --git a/tests/scripts/workers-chart-entrypoint.test.ts b/tests/scripts/workers-chart-entrypoint.test.ts
index 0df4ff6d..cff51480 100644
--- a/tests/scripts/workers-chart-entrypoint.test.ts
+++ b/tests/scripts/workers-chart-entrypoint.test.ts
@@ -22,7 +22,8 @@ async function fixture() {
]) {
await fs.copyFile(path.join('scripts/ops', name), path.join(root, 'scripts/ops', name));
}
- await fs.writeFile(path.join(root, 'README.md'), '# Test\n');
+ await fs.writeFile(path.join(root, 'README.md'), '# Public guide\n');
+ await fs.writeFile(path.join(root, 'scripts/ops/README.md'), '# Test\n');
const date = formatKstDate(
new Date(parseKstDateText(formatKstDate(new Date())).getTime() - 86400000),
);
@@ -94,7 +95,7 @@ describe('workers chart entrypoint', () => {
}),
).toThrow('Incomplete daily traffic');
expect(await fs.readFile(dataPath, 'utf8')).toBe(original);
- expect(await fs.readFile(path.join(root, 'README.md'), 'utf8')).toBe('# Test\n');
+ expect(await fs.readFile(path.join(root, 'scripts/ops/README.md'), 'utf8')).toBe('# Test\n');
});
it('실시간 실행에서 저장된 완전한 날짜를 읽고 보존한다', async () => {
const { root, payload, dataPath } = await fixture();
@@ -116,6 +117,10 @@ describe('workers chart entrypoint', () => {
run(root, { WORKERS_CHART_INPUT_JSON: dataPath });
const output = JSON.parse(await fs.readFile(dataPath, 'utf8'));
expect(output).toMatchObject(payload);
+ expect(await fs.readFile(path.join(root, 'README.md'), 'utf8')).toBe('# Public guide\n');
+ const operations = await fs.readFile(path.join(root, 'scripts/ops/README.md'), 'utf8');
+ expect(operations.startsWith('# Test\n')).toBe(true);
+ expect(operations).toContain('../../assets/analytics/workers-invocations.png');
expect(
(await fs.stat(path.join(root, 'assets/analytics/workers-invocations.png'))).size,
).toBeGreaterThan(0);
@@ -123,11 +128,11 @@ describe('workers chart entrypoint', () => {
it('CRLF 문서에서도 기존 차트 위치와 앞뒤 내용을 보존한다', async () => {
const { root, dataPath } = await fixture();
await fs.writeFile(
- path.join(root, 'README.md'),
+ path.join(root, 'scripts/ops/README.md'),
'# Header\r\n\r\n\r\nold\r\n\r\n\r\n## Footer\r\n',
);
run(root, { WORKERS_CHART_INPUT_JSON: dataPath });
- const readme = await fs.readFile(path.join(root, 'README.md'), 'utf8');
+ const readme = await fs.readFile(path.join(root, 'scripts/ops/README.md'), 'utf8');
expect(readme.startsWith('# Header\n\n')).toBe(true);
expect(readme.endsWith('\n\n## Footer\n')).toBe(true);
expect(readme).toContain('Worker 실행 + 루트 GET 리디렉션 요청');
diff --git a/tests/scripts/workers-chart-helpers.test.ts b/tests/scripts/workers-chart-helpers.test.ts
index 2bbca621..a0260f86 100644
--- a/tests/scripts/workers-chart-helpers.test.ts
+++ b/tests/scripts/workers-chart-helpers.test.ts
@@ -31,7 +31,7 @@ describe('buildReadmeSection', () => {
);
});
- it('GitHub 알림 카드가 되도록 중앙 정렬 영역 밖에 안내문을 둔다', () => {
+ it('운영 문서 상대 이미지 경로와 독립된 중앙 정렬 영역을 사용한다', () => {
const section = buildReadmeSection({
scriptName: 'daiso-mcp',
updatedAt: '2026-07-17 23:05 KST',
@@ -42,7 +42,10 @@ describe('buildReadmeSection', () => {
});
expect(section).toContain('
\n\n> [!IMPORTANT]');
- expect(section).toContain('이용해 주세요.\n\n
');
+ expect(section).toContain('../../assets/analytics/workers-invocations.png');
+ expect(section).toContain('\n
');
+ expect(section.match(/
/g)).toHaveLength(1);
});
});