const test = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const childProcess = require("node:child_process"); const repoRoot = path.join(__dirname, ".."); function read(relativePath) { return fs.readFileSync(path.join(repoRoot, relativePath), "utf8"); } function readJson(relativePath) { return JSON.parse(read(relativePath)); } function escapeRegex(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function findSection(doc, heading) { const escaped = escapeRegex(heading); const match = doc.match(new RegExp(`${escaped}[\\s\\S]*?(?=\\n## |\\n### |$)`)); assert.ok(match, `expected section headed by "${heading}"`); return match[0]; } function assertOliveYoungCloneFallbackCommands(doc, label) { assert.match(doc, /node dist\/bin\.js health/, `${label} should document the runnable local health command`); assert.match( doc, /node dist\/bin\.js get \/api\/oliveyoung\/stores --keyword 명동 --limit 5 --json/, `${label} should document the runnable local store lookup command`, ); assert.match( doc, /node dist\/bin\.js get \/api\/oliveyoung\/products --keyword 선크림 --size 5 --json/, `${label} should document the runnable local product lookup command`, ); assert.match( doc, /node dist\/bin\.js get \/api\/oliveyoung\/inventory --keyword 선크림 --storeKeyword 명동 --size 5 --json/, `${label} should document the runnable local inventory lookup command`, ); assert.doesNotMatch(doc, /^\s*npx daiso\b/m, `${label} should not publish broken clone-local npx commands`); } function assertOliveYoungCloneFallbackShorthand(doc, label) { assert.match( doc, /git clone https:\/\/github\.com\/hmmhmmhm\/daiso-mcp\.git && cd daiso-mcp && npm install && npm run build/, `${label} should include a runnable shorthand that changes into the clone before install/build`, ); assert.doesNotMatch( doc, /git clone https:\/\/github\.com\/hmmhmmhm\/daiso-mcp\.git && npm install && npm run build/, `${label} should not publish the broken shorthand that skips cd daiso-mcp`, ); } function extractQuotedEntries(block, indent) { return block .split("\n") .map((line) => line.match(new RegExp(`^ {${indent}}"([^"]+)":\\s*(.+?)(?:,)?$`))) .filter(Boolean) .map(([, key, value]) => [key, value.trim()]); } function findPrintedObjectBlock(doc, carrier) { const block = [...doc.matchAll(/print\(json\.dumps\(\{\n([\s\S]*?)\n\}, ensure_ascii=False, indent=2\)\)/g)] .map((match) => match[1]) .find((candidate) => candidate.includes(`"carrier": "${carrier}"`)); assert.ok(block, `expected ${carrier} normalized JSON example`); return block; } function findRecentEventsBlock(doc, carrier) { const block = [...doc.matchAll(/normalized_events = \[\n\s*\{\n([\s\S]*?)\n\s*\}\n\s*for [^\n]+ in events\n\]/g)] .map((match) => match[1]) .find((candidate) => candidate.includes('"status_code":') === (carrier === "cj")); assert.ok(block, `expected ${carrier} recent_events example`); return block; } function findJsonFenceAfterLabel(doc, label) { return JSON.parse(findJsonFenceTextAfterLabel(doc, label)); } function findJsonFenceTextAfterLabel(doc, label) { const escaped = escapeRegex(label); const match = doc.match(new RegExp(`${escaped}[\\s\\S]*?\\\`\\\`\\\`json\\n([\\s\\S]*?)\\n\\\`\\\`\\\``)); assert.ok(match, `expected JSON example after "${label}"`); return match[1]; } function assertSampleProvenance(doc, sectionLabel, expected, docLabel) { const escapedSectionLabel = escapeRegex(sectionLabel); const escapedVerifiedAt = escapeRegex(expected.verified_at); const escapedInvoice = escapeRegex(expected.invoice); assert.match( doc, new RegExp( `${escapedSectionLabel}[\\s\\S]*?아래 값은 ${escapedVerifiedAt} 기준 live smoke test\\(\\x60${escapedInvoice}\\x60\\)에서 확인한 정규화 결과다\\.\\n\\n\\\`\\\`\\\`json`, ), `${docLabel} ${sectionLabel} provenance line must stay pinned to the verified smoke-test date and invoice`, ); } function assertSanitizedPublicOutput(output, label) { const serialized = JSON.stringify(output); assert.doesNotMatch(serialized, /\bTEL\b/i, `${label} must not leak TEL fragments`); assert.doesNotMatch( serialized, /\d{2,4}[.\-]\d{3,4}[.\-]\d{4}/, `${label} must not leak phone-number-like strings anywhere in the published sample`, ); assert.doesNotMatch(serialized, /crgNm/, `${label} must not leak CJ assignee/source fields`); assert.doesNotMatch(serialized, /sender/i, `${label} must not leak sender fields`); assert.doesNotMatch(serialized, /receiver/i, `${label} must not leak receiver fields`); assert.doesNotMatch(serialized, /delivered_to/i, `${label} must not leak delivered_to fields`); } function assertKakaoBarNearbySadangSmokeSnapshot(smoke, label) { assert.equal(smoke.anchor.name, "사당1동먹자골목상점가", `${label} anchor should stay on the verified area landmark`); assert.equal(smoke.meta.openNowCount, 4, `${label} should publish the verified open-now count`); assert.deepEqual( smoke.items.map((item) => item.name), ["우미노식탁", "방배을지로골뱅이술집포차 사당역점", "커먼테이블"], `${label} should keep the verified top-3 ordering`, ); } test("root npm test script includes the skill docs regression suite", () => { const packageJson = JSON.parse(read("package.json")); assert.match(packageJson.scripts.test, /node --test scripts\/skill-docs\.test\.js/); }); test("README advertises OpenClaw among the supported coding agents", () => { const readme = read("README.md"); assert.match( readme, /Claude Code, Codex, OpenCode, OpenClaw\/ClawHub 등 각종 코딩 에이전트 지원합니다\./, ); }); test("repository publishes Korean contribution guidance for external contributors", () => { const contributingPath = path.join(repoRoot, "CONTRIBUTING.md"); assert.ok(fs.existsSync(contributingPath), "expected CONTRIBUTING.md to exist"); const contributing = read("CONTRIBUTING.md"); assert.match(contributing, /^# 기여 가이드$/m); assert.match(contributing, /PR 코멘트, 이슈, 리뷰 등 모든 소통은 한국어로 진행/); assert.match(contributing, /PR의 대상 브랜치는 반드시 `dev`/); assert.match(contributing, /`main` 브랜치로 PR을 만들 수 있는 사람은 `@vkehfdl1`뿐/); assert.match(contributing, /스킬을 추가하거나 변경할 때는 관련 기능 문서와 `README\.md`의 표/); assert.match(contributing, /npm 패키지를 수정할 때는 Changesets/); assert.match(contributing, /Changeset 파일의 존재 여부를 테스트로 검증하지 않는다/); assert.match(contributing, /`package\.json`과 `package-lock\.json`의 `version` 필드를 테스트에서 고정하지 않는다/); assert.match(contributing, /`name`, `license`, `engines\.node`, workspace link metadata/); assert.match(contributing, /현재 구현이 registry token 기반인 경우에도 신규 또는 재설계 흐름은 trusted publishing\/OIDC를 우선/); assert.match(contributing, /신규 proxy route는 upstream이 API key를 요구하는 무료 API인 경우에만 `k-skill-proxy` 경유를 검토/); assert.match(contributing, /인증 없이 동작하는 공개 read-only endpoint는 기본적으로 사용자 머신에서 직접 호출/); assert.doesNotMatch(contributing, /무료 API이고 1일 리미트가 충분한 경우/); assert.match(contributing, /유료 API/); assert.match(contributing, /`k-skill-proxy`를 타지 않도록 설계/); assert.match(contributing, /릴리스나 패키징 관련 변경은 `npm run ci`/); assert.match(contributing, /`~\/\.claude\/skills\/`/); assert.match(contributing, /`~\/\.agents\/skills\/`/); assert.match( contributing, /프로덕션 프록시는 \*\*Google Cloud Run\*\* \(project `k-skill-proxy`, region `asia-northeast1`\)에서 운영하며 `k-skill-proxy\.nomadamas\.org` 도메인에 매핑/, ); assert.match( contributing, /프로덕션 시크릿은 GCP Secret Manager에 보관되고 Cloud Run 런타임에 주입됩니다\. 프록시 운영자\(maintainer\)가 한 번 수행해야 하는 WIF\/Secret Manager 셋업과 운영 점검 절차는 \[`docs\/deploy-k-skill-proxy\.md`\]\(docs\/deploy-k-skill-proxy\.md\)에 정리/, ); }); test("README links to the contribution guide", () => { const readme = read("README.md"); assert.match(readme, /\[기여 가이드\]\(CONTRIBUTING\.md\)/); }); test("repository docs advertise Daangn read-only search skills", () => { const readme = read("README.md"); const sources = read(path.join("docs", "sources.md")); const skills = [ ["daangn-used-goods-search", "당근 중고거래 검색"], ["daangn-realty-search", "당근부동산 검색"], ["daangn-jobs-search", "당근알바 검색"], ["daangn-cars-search", "당근중고차 검색"], ]; assert.match(sources, /www\.daangn\.com\/kr\/api\/v1\/regions\/keyword/); assert.match(sources, /realty\.daangn\.com\/articles/); for (const [skillName, label] of skills) { const skill = read(path.join(skillName, "SKILL.md")); const featureDocPath = path.join(repoRoot, "docs", "features", `${skillName}.md`); const featureDoc = read(path.join("docs", "features", `${skillName}.md`)); assert.ok(fs.existsSync(featureDocPath), `expected docs/features/${skillName}.md to exist`); assert.ok( readme.includes(`| ${label} | \`${skillName}\``), `README should advertise ${skillName}`, ); assert.ok( readme.includes(`](docs/features/${skillName}.md)`), `README should link docs/features/${skillName}.md`, ); assert.match(skill, /kr\/api\/v1\/regions\/keyword/); assert.match(featureDoc, /kr\/api\/v1\/regions\/keyword/); assert.match(featureDoc, /(로그인|채팅|구매|문의|지원).*자동화/); } }); test("hwp skill documents kordoc-based parsing and supported operations", () => { const skillPath = path.join(repoRoot, "hwp", "SKILL.md"); assert.ok(fs.existsSync(skillPath), "expected hwp/SKILL.md to exist"); const skill = read(path.join("hwp", "SKILL.md")); assert.match(skill, /^name: hwp$/m); assert.match(skill, /\bkordoc\b/); assert.doesNotMatch(skill, /@ohah\/hwpjs/); assert.doesNotMatch(skill, /\bhwp-mcp\b/); assert.match(skill, /JSON/i); assert.match(skill, /Markdown/i); assert.match(skill, /image/i); assert.match(skill, /(batch|배치)/i); assert.match(skill, /HWPX/i); assert.match(skill, /(역변환|되돌려)/); assert.match(skill, /(비교|compare)/i); assert.match(skill, /pdfjs-dist/); assert.match(skill, /(extractFormFields|양식 필드)/); assert.doesNotMatch(skill, /fillForm/); assert.doesNotMatch(skill, /kordoc fill/); assert.doesNotMatch(skill, /kordoc mcp/); }); test("hwp docs match the published kordoc install and runtime contract", () => { const skill = read(path.join("hwp", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "hwp.md")); const install = read(path.join("docs", "install.md")); const readme = read("README.md"); const sources = read(path.join("docs", "sources.md")); assert.match(skill, /npx --yes --package kordoc --package pdfjs-dist kordoc .* -o .*\.md/); assert.match(skill, /markdownToHwpx/); assert.match(skill, /extractFormFields/); assert.match(skill, /npm init -y/); assert.match(skill, /npm install kordoc pdfjs-dist/); assert.doesNotMatch(skill, /^\s*npx kordoc\b/m); assert.doesNotMatch(skill, /export NODE_PATH/); assert.match(featureDoc, /npx --yes --package kordoc --package pdfjs-dist kordoc .* --format json/); assert.match(featureDoc, /markdownToHwpx/); assert.match(featureDoc, /(extractFormFields|양식 필드)/); assert.match(featureDoc, /npx --yes --package kordoc --package pdfjs-dist kordoc watch/); assert.match(featureDoc, /npm init -y/); assert.match(featureDoc, /npm install kordoc pdfjs-dist/); assert.doesNotMatch(featureDoc, /^\s*npx kordoc\b/m); assert.doesNotMatch(featureDoc, /export NODE_PATH/); assert.match(featureDoc, /npm install -g kordoc pdfjs-dist/); assert.doesNotMatch(featureDoc, /선택적으로 `pdfjs-dist`/); assert.doesNotMatch(featureDoc, /kordoc fill/); assert.doesNotMatch(featureDoc, /kordoc mcp/); assert.doesNotMatch(featureDoc, /fillForm/); assert.match(install, /npm install -g kordoc pdfjs-dist /); assert.match(install, /HWP Node API 예시는 전역 `NODE_PATH` 대신 로컬 프로젝트에 `npm install kordoc pdfjs-dist` 후 실행/); assert.match(install, /`kordoc` CLI를 일회성으로만 쓸 때는 `npx --yes --package kordoc --package pdfjs-dist kordoc \.\.\.` 형태를 사용한다\./); assert.match(readme, /\| HWP 문서 조회\/변환 \| .*양식 필드 추출.*Markdown→HWPX 역변환/); assert.doesNotMatch(readme, /\| HWP 문서 조회\/변환 \| .*양식 채우기/); assert.match(sources, /kordoc/); assert.match(sources, /pdfjs-dist/); }); test("repository docs advertise the hwp skill", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "hwp.md"); const featureDoc = read(path.join("docs", "features", "hwp.md")); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/hwp.md to exist"); assert.match(readme, /\| HWP 문서 조회\/변환 \|/); assert.match(readme, /\[HWP 문서 처리 가이드\]\(docs\/features\/hwp\.md\)/); assert.match(install, /--skill hwp/); assert.match(featureDoc, /\bkordoc\b/); assert.doesNotMatch(featureDoc, /@ohah\/hwpjs/); assert.doesNotMatch(featureDoc, /\bhwp-mcp\b/); assert.match(install, /npm install -g kordoc /); assert.doesNotMatch(install, /@ohah\/hwpjs/); }); test("repository docs advertise the kakaotalk-mac skill", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "kakaotalk-mac.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/kakaotalk-mac.md to exist"); assert.match(readme, /\| 카카오톡 Mac CLI \|/); assert.match(readme, /\[카카오톡 Mac CLI\]\(docs\/features\/kakaotalk-mac\.md\)/); assert.match(install, /--skill kakaotalk-mac/); }); test("repository docs advertise the used-car-price-search skill", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "used-car-price-search.md"); const skillPath = path.join(repoRoot, "used-car-price-search", "SKILL.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/used-car-price-search.md to exist"); assert.ok(fs.existsSync(skillPath), "expected used-car-price-search/SKILL.md to exist"); assert.match(readme, /\| 중고차 가격 조회 \|/); assert.match(readme, /\[중고차 가격 조회 가이드\]\(docs\/features\/used-car-price-search\.md\)/); assert.match(install, /--skill used-car-price-search/); assert.match( install, /npm install -g kordoc pdfjs-dist kbo-game kbl-results kleague-results lck-analytics toss-securities hipass-receipt k-lotto coupang-product-search used-car-price-search cheap-gas-nearby public-restroom-nearby korean-law-mcp/, ); }); test("repository docs advertise the public-restroom-nearby skill", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "public-restroom-nearby.md"); const skillPath = path.join(repoRoot, "public-restroom-nearby", "SKILL.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/public-restroom-nearby.md to exist"); assert.ok(fs.existsSync(skillPath), "expected public-restroom-nearby/SKILL.md to exist"); assert.match(readme, /\| 근처 공중화장실 찾기 \|/); assert.match(readme, /\[근처 공중화장실 찾기 가이드\]\(docs\/features\/public-restroom-nearby\.md\)/); assert.match(install, /--skill public-restroom-nearby/); assert.match(install, /npm install -g .*public-restroom-nearby/); }); test("public-restroom-nearby docs describe the maxDistanceMeters distance cap", () => { const featureDoc = read(path.join("docs", "features", "public-restroom-nearby.md")); const packageReadme = read(path.join("packages", "public-restroom-nearby", "README.md")); assert.match(featureDoc, /maxDistanceMeters/); assert.match(featureDoc, /100m/); assert.match(packageReadme, /maxDistanceMeters/); assert.match(packageReadme, /100m/); }); test("repository docs advertise the lck-analytics skill and package", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "lck-analytics.md"); const skillPath = path.join(repoRoot, "lck-analytics", "SKILL.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/lck-analytics.md to exist"); assert.ok(fs.existsSync(skillPath), "expected lck-analytics/SKILL.md to exist"); assert.match(readme, /\| LCK 경기 분석 \|/); assert.match(readme, /\[LCK 경기 분석 가이드\]\(docs\/features\/lck-analytics\.md\)/); assert.match(install, /--skill lck-analytics/); assert.match(install, /npm install -g .*lck-analytics/); }); test("lck-analytics docs and skill credit the original author and reference repo", () => { const skill = read(path.join("lck-analytics", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "lck-analytics.md")); const packageReadme = read(path.join("packages", "lck-analytics", "README.md")); const sources = read(path.join("docs", "sources.md")); for (const doc of [skill, featureDoc, packageReadme]) { assert.match(doc, /jerjangmin/); assert.match(doc, /https:\/\/github\.com\/jerjangmin\/share\/tree\/main\/SKILL\/lck-analytics/); assert.match(doc, /Riot|LoL Esports|Oracle(?:'s)? Elixir/i); } assert.match(sources, /https:\/\/github\.com\/jerjangmin\/share\/tree\/main\/SKILL\/lck-analytics/); }); test("repository docs advertise the korean-humanizer skill and credit im-not-ai and happy-nut", () => { const readme = read("README.md"); const skill = read(path.join("korean-humanizer", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "korean-humanizer.md")); const taxonomy = read(path.join("korean-humanizer", "references", "ai-tell-taxonomy.md")); assert.match(readme, /\| 한국어 AI 윤문 \| `korean-humanizer` \|/); assert.match(readme, /\[한국어 AI 윤문 가이드\]\(docs\/features\/korean-humanizer\.md\)/); for (const doc of [skill, featureDoc, taxonomy]) { assert.match(doc, /https:\/\/github\.com\/epoko77-ai\/im-not-ai/); } for (const doc of [skill, featureDoc]) { assert.match(doc, /happy-nut/); } assert.match(skill, /S1|S2|S3/); assert.match(skill, /references\/ai-tell-taxonomy\.md/); }); test("repository docs advertise the korean-spell-check skill and usage constraints", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "korean-spell-check.md"); const skillPath = path.join(repoRoot, "korean-spell-check", "SKILL.md"); const featureDoc = read(path.join("docs", "features", "korean-spell-check.md")); const skill = read(path.join("korean-spell-check", "SKILL.md")); const sources = read(path.join("docs", "sources.md")); const roadmap = read(path.join("docs", "roadmap.md")); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/korean-spell-check.md to exist"); assert.ok(fs.existsSync(skillPath), "expected korean-spell-check/SKILL.md to exist"); assert.match(readme, /\| 한국어 맞춤법 검사 \|/); assert.match(readme, /\[한국어 맞춤법 검사 가이드\]\(docs\/features\/korean-spell-check\.md\)/); assert.match(install, /--skill korean-spell-check/); assert.match(skill, /비상업적 용도|개인이나 학생만 무료/); assert.match(skill, /robots\.txt/i); assert.match(skill, /청크|chunk/i); assert.match(skill, /원문.*교정안.*이유/s); assert.match(featureDoc, /old_speller\/results/); assert.match(featureDoc, /Cloudflare|403/); assert.match(featureDoc, /python3 scripts\/korean_spell_check\.py/); assert.match(sources, /https:\/\/nara-speller\.co\.kr\/speller\//); assert.match(sources, /https:\/\/nara-speller\.co\.kr\/old_speller\//); assert.match(sources, /https:\/\/nara-speller\.co\.kr\/robots\.txt/); assert.match(roadmap, /한국어 맞춤법 검사 스킬 출시/); }); test("repository docs advertise the MFDS public-health skills and mandatory symptom interview", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const sources = read(path.join("docs", "sources.md")); const setup = read(path.join("docs", "setup.md")); const security = read(path.join("docs", "security-and-secrets.md")); const setupSkill = read(path.join("k-skill-setup", "SKILL.md")); const drugSkillPath = path.join(repoRoot, "mfds-drug-safety", "SKILL.md"); const foodSkillPath = path.join(repoRoot, "mfds-food-safety", "SKILL.md"); const drugFeaturePath = path.join(repoRoot, "docs", "features", "mfds-drug-safety.md"); const foodFeaturePath = path.join(repoRoot, "docs", "features", "mfds-food-safety.md"); assert.ok(fs.existsSync(drugSkillPath), "expected mfds-drug-safety/SKILL.md to exist"); assert.ok(fs.existsSync(foodSkillPath), "expected mfds-food-safety/SKILL.md to exist"); assert.ok(fs.existsSync(drugFeaturePath), "expected docs/features/mfds-drug-safety.md to exist"); assert.ok(fs.existsSync(foodFeaturePath), "expected docs/features/mfds-food-safety.md to exist"); assert.match(readme, /\| 의약품 안전 체크 \|/); assert.match(readme, /\| 식품 안전 체크 \|/); assert.match(readme, /\| 의약품 안전 체크 \| .* \| 불필요 \|/); assert.match(readme, /\| 식품 안전 체크 \| .* \| 불필요 \|/); assert.match(install, /--skill mfds-drug-safety/); assert.match(install, /--skill mfds-food-safety/); assert.match(sources, /15075057\/openapi\.do/); assert.match(sources, /15097208\/openapi\.do/); assert.match(sources, /15056516\/openapi\.do/); assert.match(sources, /15074318\/openapi\.do/); assert.match(sources, /foodsafetykorea\.go\.kr\/api\/openApiInfo\.do.*svc_no=I0490/); for (const doc of [setup, security, setupSkill]) { assert.match(doc, /의약품 안전 체크|식품 안전 체크/); assert.match(doc, /FOODSAFETYKOREA_API_KEY|DATA_GO_KR_API_KEY/); assert.match(doc, /사용자.*불필요|proxy 서버/u); } for (const relativePath of [ path.join("mfds-drug-safety", "SKILL.md"), path.join("mfds-food-safety", "SKILL.md"), path.join("docs", "features", "mfds-drug-safety.md"), path.join("docs", "features", "mfds-food-safety.md") ]) { const doc = read(relativePath); assert.match(doc, /인터뷰|되묻/); assert.match(doc, /호흡곤란/); assert.match(doc, /직접 진단|진단\/처방|진단\)이나/); assert.match(doc, /119|응급실/); } }); test("used-car-price-search docs document the provider survey and SK direct surface", () => { const skill = read(path.join("used-car-price-search", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "used-car-price-search.md")); const sources = read(path.join("docs", "sources.md")); const roadmap = read(path.join("docs", "roadmap.md")); for (const doc of [skill, featureDoc]) { assert.match(doc, /SK렌터카|SK렌터카 다이렉트|타고BUY/); assert.match(doc, /롯데렌탈|롯데오토옥션/); assert.match(doc, /레드캡렌터카/); assert.match(doc, /MCP/i); assert.match(doc, /Skill/i); assert.match(doc, /https:\/\/www\.skdirect\.co\.kr\/tb/); assert.match(doc, /__NEXT_DATA__/); assert.match(doc, /인수가/); assert.match(doc, /월\s*렌트료|월\s*요금|월\s*가격/); assert.match(doc, /10회 이상|최소 10회/); } assert.match(featureDoc, /2026-04-02/); assert.match(featureDoc, /inventory 규모는 시점에 따라 변동될 수/); assert.doesNotMatch(featureDoc, /총 `\d+대`/); assert.match(sources, /https:\/\/www\.skdirect\.co\.kr\/tb/); assert.match(sources, /https:\/\/www\.lotteautoauction\.net\/hp\/pub\/cmm\/viewMain\.do/); assert.match(sources, /https:\/\/biz\.redcap\.co\.kr\/rent\//); assert.match(roadmap, /중고차 가격 조회 스킬 출시/); }); test("seoul subway docs default to the hosted proxy when KSKILL_PROXY_BASE_URL is unset", () => { const readme = read("README.md"); const setup = read(path.join("docs", "setup.md")); const install = read(path.join("docs", "install.md")); const security = read(path.join("docs", "security-and-secrets.md")); const setupSkill = read(path.join("k-skill-setup", "SKILL.md")); const skill = read(path.join("seoul-subway-arrival", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "seoul-subway-arrival.md")); const proxyDoc = read(path.join("docs", "features", "k-skill-proxy.md")); const proxyReadme = read(path.join("packages", "k-skill-proxy", "README.md")); const secretsExample = read(path.join("examples", "secrets.env.example")); assert.match(readme, /\| 서울 지하철 도착정보 조회 \| .* \| 불필요 \|/); assert.match(setup, /\| 서울 지하철 도착정보 조회 \| 사용자 시크릿 불필요 \(기본 hosted proxy 사용, 운영자만 `SEOUL_OPEN_API_KEY`\) \|/); assert.match(install, /--skill seoul-subway-arrival/); for (const doc of [skill, featureDoc]) { assert.match(doc, /KSKILL_PROXY_BASE_URL/); assert.match(doc, /\/v1\/seoul-subway\/arrival/); assert.match(doc, /사용자가 .*OpenAPI key.*직접.*필요(가|는)? 없다|개인 API key 없이/i); assert.match(doc, /비우면 기본 hosted `https:\/\/k-skill-proxy\.nomadamas\.org`|없으면 기본 hosted proxy/i); assert.doesNotMatch(doc, /SEOUL_OPEN_API_KEY/); assert.doesNotMatch(doc, /swopenAPI\.seoul\.go\.kr\/api\/subway\/\$\{SEOUL_OPEN_API_KEY\}/); assert.doesNotMatch(doc, /필수: self-host|반드시 명시|먼저 확보|public hosted route rollout 전/); } assert.match(proxyDoc, /GET \/v1\/seoul-subway\/arrival/); assert.match(proxyDoc, /KSKILL_PROXY_BASE_URL/); assert.match(proxyDoc, /unset\/empty|비워 두면/); assert.match(proxyDoc, /https:\/\/k-skill-proxy\.nomadamas\.org/); assert.match(proxyDoc, /KSKILL_PROXY_BASE_URL=https:\/\/your-proxy\.example\.com/); assert.match(proxyDoc, /self-host|alternate proxy/i); assert.match(proxyDoc, /override/i); assert.match(proxyDoc, /SEOUL_OPEN_API_KEY/); assert.match(proxyReadme, /GET \/v1\/seoul-subway\/arrival/); assert.match(proxyReadme, /SEOUL_OPEN_API_KEY/); assert.match(security, /KSKILL_PROXY_BASE_URL/); assert.match(security, /서울 지하철.*한국 날씨.*기본 hosted proxy|기본 hosted proxy.*서울 지하철.*한국 날씨/i); assert.match(setupSkill, /서울 지하철: 사용자 시크릿 불필요 \(기본 hosted proxy 사용, 운영자만 `SEOUL_OPEN_API_KEY`\)/); assert.doesNotMatch(secretsExample, /SEOUL_OPEN_API_KEY/); assert.doesNotMatch(secretsExample, /^KSKILL_PROXY_BASE_URL=https:\/\/your-proxy\.example\.com$/m); assert.match(secretsExample, /^(#\s*)?KSKILL_PROXY_BASE_URL=$|^#\s*KSKILL_PROXY_BASE_URL=https:\/\/your-proxy\.example\.com$/m); assert.doesNotMatch(secretsExample, /KSKILL_PROXY_BASE_URL=https:\/\/k-skill-proxy\.nomadamas\.org/); }); test("repository docs advertise the korea-weather skill across the documented surfaces", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const roadmap = read(path.join("docs", "roadmap.md")); const sources = read(path.join("docs", "sources.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "korea-weather.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/korea-weather.md to exist"); assert.match(readme, /\| 한국 날씨 조회 \|/); assert.match(readme, /\[한국 날씨 조회 가이드\]\(docs\/features\/korea-weather\.md\)/); assert.match(install, /--skill korea-weather/); assert.match(roadmap, /한국 날씨 조회 스킬 출시/); assert.match(sources, /기상청 단기예보 조회서비스: https:\/\/www\.data\.go\.kr\/data\/15084084\/openapi\.do/); }); test("korea-weather docs route short-term forecast calls through the proxy without requiring a user API key", () => { const skillPath = path.join(repoRoot, "korea-weather", "SKILL.md"); assert.ok(fs.existsSync(skillPath), "expected korea-weather/SKILL.md to exist"); const skill = read(path.join("korea-weather", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "korea-weather.md")); const proxyDoc = read(path.join("docs", "features", "k-skill-proxy.md")); const proxyReadme = read(path.join("packages", "k-skill-proxy", "README.md")); assert.match(skill, /^name: korea-weather$/m); assert.match(skill, /^description: .*날씨.*기상청.*프록시.*$/m); for (const doc of [skill, featureDoc]) { assert.match(doc, /\/v1\/korea-weather\/forecast/); assert.match(doc, /기상청.*단기예보|단기예보.*기상청/); assert.match(doc, /사용자가 .*API key.*직접.*필요(가|는)? 없다|개인 API key 없이/i); assert.match(doc, /nx|ny|위도|경도/u); assert.match(doc, /TMP|SKY|PTY|POP/); assert.match(doc, /KSKILL_PROXY_BASE_URL|k-skill-proxy\.nomadamas\.org/); assert.match(doc, /비우면 기본 hosted `https:\/\/k-skill-proxy\.nomadamas\.org`|없으면 기본 hosted proxy/i); assert.doesNotMatch(doc, /필수: self-host|반드시 명시|먼저 확보|public hosted route rollout 전/); assert.doesNotMatch(doc, /KMA_OPEN_API_KEY=.*사용자/); } assert.match(proxyDoc, /GET \/v1\/korea-weather\/forecast/); assert.match(proxyDoc, /KMA_OPEN_API_KEY/); assert.match(proxyReadme, /GET \/v1\/korea-weather\/forecast/); assert.match(proxyReadme, /KMA_OPEN_API_KEY/); }); test("hosted proxy docs keep self-host overrides inactive and demonstrate resolver fallback", () => { const setup = read(path.join("docs", "setup.md")); const security = read(path.join("docs", "security-and-secrets.md")); const setupSkill = read(path.join("k-skill-setup", "SKILL.md")); const secretsExample = read(path.join("examples", "secrets.env.example")); const subwaySkill = read(path.join("seoul-subway-arrival", "SKILL.md")); const weatherSkill = read(path.join("korea-weather", "SKILL.md")); const subwayFeatureDoc = read(path.join("docs", "features", "seoul-subway-arrival.md")); const weatherFeatureDoc = read(path.join("docs", "features", "korea-weather.md")); const proxyDoc = read(path.join("docs", "features", "k-skill-proxy.md")); for (const doc of [setup, security, setupSkill, secretsExample]) { assert.doesNotMatch(doc, /^KSKILL_PROXY_BASE_URL=https:\/\/your-proxy\.example\.com$/m); } for (const doc of [setup, security, setupSkill, secretsExample]) { assert.match( doc, /^(#\s*)?KSKILL_PROXY_BASE_URL=$|^#\s*KSKILL_PROXY_BASE_URL=https:\/\/your-proxy\.example\.com$/m, ); } for (const doc of [subwaySkill, weatherSkill, subwayFeatureDoc, weatherFeatureDoc]) { assert.match(doc, /BASE="\$\{KSKILL_PROXY_BASE_URL:-https:\/\/k-skill-proxy\.nomadamas\.org\}"/); assert.match(doc, /curl -fsS --get "\$\{BASE\}/); assert.doesNotMatch(doc, /curl -fsS --get 'https:\/\/k-skill-proxy\.nomadamas\.org/); } assert.match(proxyDoc, /BASE="\$\{KSKILL_PROXY_BASE_URL:-https:\/\/k-skill-proxy\.nomadamas\.org\}"/); for (const endpoint of ["seoul-subway/arrival", "korea-weather/forecast"]) { assert.match(proxyDoc, new RegExp(`curl -fsS --get "\\$\\{BASE\\}/v1/${endpoint}"`)); assert.doesNotMatch(proxyDoc, new RegExp(`curl -fsS --get 'http://127\\.0\\.0\\.1:4020/v1/${endpoint}'`)); } }); test("kakaotalk-mac skill documents safe macOS kakaocli usage", () => { const skillPath = path.join(repoRoot, "kakaotalk-mac", "SKILL.md"); const helperPath = path.join(repoRoot, "scripts", "kakaotalk_mac.py"); const featureDoc = read(path.join("docs", "features", "kakaotalk-mac.md")); assert.ok(fs.existsSync(skillPath), "expected kakaotalk-mac/SKILL.md to exist"); assert.ok(fs.existsSync(helperPath), "expected scripts/kakaotalk_mac.py to exist"); const skill = read(path.join("kakaotalk-mac", "SKILL.md")); assert.match(skill, /^name: kakaotalk-mac$/m); assert.match(skill, /kakaocli/); assert.match(skill, /macOS/i); assert.match(skill, /KakaoTalk/i); assert.match(skill, /Full Disk Access/i); assert.match(skill, /Accessibility/i); assert.match(skill, /--me/); assert.match(skill, /confirm before sending/i); for (const doc of [skill, featureDoc]) { assert.match(doc, /python3 scripts\/kakaotalk_mac\.py auth/); assert.match(doc, /python3 scripts\/kakaotalk_mac\.py chats --limit 10 --json/); assert.match(doc, /python3 scripts\/kakaotalk_mac\.py messages --chat/); assert.match(doc, /python3 scripts\/kakaotalk_mac\.py search/); assert.match(doc, /user_id 자동 감지 실패|SHA-512|DESIGNATEDFRIENDSREVISION/i); assert.match(doc, /cache|캐시/); assert.match(doc, /read-only|읽기 전용/i); assert.doesNotMatch(doc, /`query`/); } }); test("repository docs advertise the KTX booking skill as supported", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "ktx-booking.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/ktx-booking.md to exist"); assert.match(readme, /\| KTX 예매 \|/); assert.match(readme, /\[KTX 예매 가이드\]\(docs\/features\/ktx-booking\.md\)/); assert.doesNotMatch(readme, /KTX 예매는 현재 작동하지 않습니다/); assert.doesNotMatch(readme, /KTX 예매 \| 현재 작동하지 않음/); assert.match(install, /--skill ktx-booking/); }); test("ktx-booking docs document the helper-based live Korail workflow", () => { const skillPath = path.join(repoRoot, "ktx-booking", "SKILL.md"); const helperPath = path.join(repoRoot, "scripts", "ktx_booking.py"); assert.ok(fs.existsSync(skillPath), "expected ktx-booking/SKILL.md to exist"); assert.ok(fs.existsSync(helperPath), "expected scripts/ktx_booking.py to exist"); const skill = read(path.join("ktx-booking", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "ktx-booking.md")); const helper = read(path.join("scripts", "ktx_booking.py")); assert.match(skill, /^name: ktx-booking$/m); for (const doc of [skill, featureDoc]) { assert.match(doc, /python3 scripts\/ktx_booking\.py search/); assert.match(doc, /python3 scripts\/ktx_booking\.py reserve/); assert.match(doc, /python3 scripts\/ktx_booking\.py reservations/); assert.match(doc, /python3 scripts\/ktx_booking\.py cancel/); assert.match(doc, /train_id/); assert.match(doc, /--train-id/); assert.match(doc, /--include-no-seats/); assert.match(doc, /--include-waiting-list/); assert.match(doc, /--try-waiting/); assert.match(doc, /credential resolution order|KSKILL_KTX_ID/); assert.match(doc, /anti-bot|Dynapath|x-dynapath-m-token/i); assert.match(doc, /결제(까지)?는 자동화하지 않는다|결제는 제외/); assert.doesNotMatch(doc, /예약 시 선택할 `--train-index`/); } assert.match(helper, /x-dynapath-m-token/); assert.match(helper, /250601002/); assert.match(helper, /def build_parser/); assert.match(helper, /train_id/); }); test("ktx-booking helper python regression tests pass", () => { const result = childProcess.spawnSync( "python3", ["-m", "unittest", "discover", "-s", "scripts", "-p", "test_ktx_booking.py"], { cwd: repoRoot, encoding: "utf8", env: { ...process.env, PYTHONNOUSERSITE: "1" }, }, ); assert.equal( result.status, 0, `expected python KTX helper regression tests to pass\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`, ); }); test("repository docs advertise the geeknews-search skill across the documented surfaces", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "geeknews-search.md"); const skillPath = path.join(repoRoot, "geeknews-search", "SKILL.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/geeknews-search.md to exist"); assert.ok(fs.existsSync(skillPath), "expected geeknews-search/SKILL.md to exist"); assert.match(readme, /\| 긱뉴스 조회 \|/); assert.match(readme, /\[긱뉴스 조회 가이드\]\(docs\/features\/geeknews-search\.md\)/); assert.match(install, /--skill geeknews-search/); }); test("geeknews-search docs lock the RSS-first list-search-detail workflow", () => { const skill = read(path.join("geeknews-search", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "geeknews-search.md")); for (const doc of [skill, featureDoc]) { assert.match(doc, /feeds\.feedburner\.com\/geeknews-feed/); assert.match(doc, /python3 scripts\/geeknews_search\.py list/); assert.match(doc, /python3 scripts\/geeknews_search\.py search/); assert.match(doc, /python3 scripts\/geeknews_search\.py detail/); assert.match(doc, /RSS-first|RSS first|RSS 피드/); assert.match(doc, /read-only|읽기 전용/); } }); test("repository docs advertise the subway-lost-property skill across the documented surfaces", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "subway-lost-property.md"); const skillPath = path.join(repoRoot, "subway-lost-property", "SKILL.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/subway-lost-property.md to exist"); assert.ok(fs.existsSync(skillPath), "expected subway-lost-property/SKILL.md to exist"); assert.match(readme, /\| 지하철 분실물 조회 \|/); assert.match(readme, /\[지하철 분실물 조회 가이드\]\(docs\/features\/subway-lost-property\.md\)/); assert.match(install, /--skill subway-lost-property/); }); test("subway-lost-property docs lock the official LOST112 guidance flow", () => { const skill = read(path.join("subway-lost-property", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "subway-lost-property.md")); for (const doc of [skill, featureDoc]) { assert.match(doc, /LOST112/); assert.match(doc, /seoulmetro\.co\.kr\/kr\/page\.do\?menuIdx=541/); assert.match(doc, /python3 scripts\/subway_lost_property\.py/); assert.match(doc, /SITE=V/); assert.match(doc, /안내형|하이브리드/); } }); test("repository docs advertise the zipcode-search skill across the documented surfaces", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const roadmap = read(path.join("docs", "roadmap.md")); const sources = read(path.join("docs", "sources.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "zipcode-search.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/zipcode-search.md to exist"); assert.match(readme, /\| 우편번호 검색 \|/); assert.match(readme, /\[우편번호 검색 가이드\]\(docs\/features\/zipcode-search\.md\)/); assert.match(install, /--skill zipcode-search/); assert.match(roadmap, /우편번호 검색/); assert.match(sources, /우체국 도로명주소 검색: https:\/\/parcel\.epost\.go\.kr\/parcel\/comm\/zipcode\/comm_newzipcd_list\.jsp/); }); test("zipcode-search docs lock the official postcode plus English-address extraction flow", () => { const skillPath = path.join(repoRoot, "zipcode-search", "SKILL.md"); assert.ok(fs.existsSync(skillPath), "expected zipcode-search/SKILL.md to exist"); const skill = read(path.join("zipcode-search", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "zipcode-search.md")); const readme = read("README.md"); const sources = read(path.join("docs", "sources.md")); assert.match(skill, /^name: zipcode-search$/m); for (const doc of [skill, featureDoc]) { assert.match(doc, /https:\/\/www\.epost\.kr\/search\.RetrieveIntegrationNewZipCdList\.comm/); assert.match(doc, /viewDetail/); assert.match(doc, /English\/집배코드/); assert.match(doc, /Rep\. of KOREA/); assert.match(doc, /curl --http1\.1 --tls-max 1\.2/); assert.match(doc, /--max-time/); assert.match(doc, /"--retry",\s+"3"/); assert.match(doc, /--retry-all-errors/); assert.match(doc, /"--retry-delay",\s+"1"/); assert.match(doc, /영문 주소|영문주소/); assert.match(doc, /python3 scripts\/zipcode_search\.py/); assert.match(doc, /\.\/scripts\/zipcode_search\.py/); assert.match(doc, /mktemp|임시 파일/); assert.doesNotMatch(doc, /urllib\.request/); } assert.match(readme, /우편번호 \+ 공식 영문주소 조회/); assert.match(sources, /우체국 통합 우편번호\/영문주소 검색: https:\/\/www\.epost\.kr\/search\.RetrieveIntegrationNewZipCdList\.comm/); assert.match(skill, /검색 결과가 없으면/i); assert.doesNotMatch(skill, /timeout\s*=/); assert.doesNotMatch(featureDoc, /timeout\s*=/); assert.match(featureDoc, /프로토콜\/클라이언트 제약/i); }); test("repository docs advertise the delivery-tracking skill across the documented surfaces", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const roadmap = read(path.join("docs", "roadmap.md")); const sources = read(path.join("docs", "sources.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "delivery-tracking.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/delivery-tracking.md to exist"); assert.match(readme, /\| 택배 배송조회 \|/); assert.match(readme, /\[택배 배송조회 가이드\]\(docs\/features\/delivery-tracking\.md\)/); assert.match(install, /--skill delivery-tracking/); assert.match(roadmap, /택배 배송조회 스킬 출시/); assert.match(sources, /CJ대한통운 배송조회: https:\/\/www\.cjlogistics\.com\/ko\/tool\/parcel\/tracking/); assert.match(sources, /우체국 배송조회: https:\/\/service\.epost\.go\.kr\/trace\.RetrieveRegiPrclDeliv\.postal\?sid1=/); }); test("delivery-tracking skill documents official CJ and ePost flows with extension guidance", () => { const skillPath = path.join(repoRoot, "delivery-tracking", "SKILL.md"); assert.ok(fs.existsSync(skillPath), "expected delivery-tracking/SKILL.md to exist"); const skill = read(path.join("delivery-tracking", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "delivery-tracking.md")); assert.match(skill, /^name: delivery-tracking$/m); for (const doc of [skill, featureDoc]) { assert.match(doc, /https:\/\/www\.cjlogistics\.com\/ko\/tool\/parcel\/tracking/); assert.match(doc, /tracking-detail/); assert.match(doc, /paramInvcNo/); assert.match(doc, /_csrf/); assert.match(doc, /10자리 또는 12자리/); assert.match(doc, /https:\/\/service\.epost\.go\.kr\/trace\.RetrieveRegiPrclDeliv\.postal\?sid1=/); assert.match(doc, /trace\.RetrieveDomRigiTraceList\.comm/); assert.match(doc, /sid1/); assert.match(doc, /13자리/); assert.match(doc, /curl --http1\.1 --tls-max 1\.2/); assert.match(doc, /carrier adapter/i); assert.match(doc, /다른 택배사/); } assert.match(skill, /1234567890/); assert.match(skill, /1234567890123/); assert.match(skill, /python3/); assert.match(featureDoc, /JSON/); assert.match(featureDoc, /HTML/); }); test("delivery-tracking published examples lock a shared normalized non-PII schema", () => { const skill = read(path.join("delivery-tracking", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "delivery-tracking.md")); const expectedTopLevelEntries = { cj: [ ["carrier", '"cj"'], ["invoice", 'payload["parcelDetailResultMap"]["paramInvcNo"]'], ["status_code", 'latest.get("crgSt")'], ["status", 'status_map.get(latest.get("crgSt"), latest.get("scanNm") or "알수없음")'], ["timestamp", 'latest.get("dTime")'], ["location", 'latest.get("regBranNm")'], ["event_count", "len(events)"], ["recent_events", "normalized_events[-min(3, len(normalized_events)):]"], ], epost: [ ["carrier", '"epost"'], ["invoice", 'clean(summary.group("tracking"))'], ["status", 'clean(summary.group("result"))'], ["timestamp", 'latest_event["timestamp"] if latest_event else None'], ["location", 'latest_event["location"] if latest_event else None'], ["event_count", "len(normalized_events)"], ["recent_events", "normalized_events[-min(3, len(normalized_events)):]"], ], }; const expectedRecentEventEntries = { cj: [ ["timestamp", 'event.get("dTime")'], ["location", 'event.get("regBranNm")'], ["status_code", 'event.get("crgSt")'], ["status", 'status_map.get(event.get("crgSt"), event.get("scanNm") or "알수없음")'], ], epost: [ ["timestamp", 'f"{day} {time_}"'], ["location", "clean_location(location)"], ["status", "clean(status)"], ], }; assert.doesNotMatch(skill, /"message":\s*latest\.get\("crgNm"\)/); assert.doesNotMatch( featureDoc, /print\(json\.dumps\(payload\["parcelDetailResultMap"\]\["resultList"\]\[-1\],\s*ensure_ascii=False,\s*indent=2\)\)/, ); for (const doc of [skill, featureDoc]) { assert.match(doc, /공통 포맷/); assert.match(doc, /공통 결과 스키마/); assert.match(doc, /최근 이벤트/); assert.match(doc, /`carrier`/); assert.match(doc, /`invoice`/); assert.match(doc, /`status`/); assert.match(doc, /`timestamp`/); assert.match(doc, /`location`/); assert.match(doc, /`event_count`/); assert.match(doc, /`recent_events`/); assert.match(doc, /최근 최대 3개 이벤트/); assert.doesNotMatch(doc, /최근 3~5개 이벤트/); assert.match(doc, /"invoice":\s*payload\["parcelDetailResultMap"\]\["paramInvcNo"\]/); assert.match(doc, /"status_code":\s*latest\.get\("crgSt"\)/); assert.match(doc, /"status":\s*status_map\.get\(latest\.get\("crgSt"\),/); assert.match(doc, /"timestamp":\s*latest\.get\("dTime"\)/); assert.match(doc, /"location":\s*latest\.get\("regBranNm"\)/); assert.match(doc, /"event_count":\s*len\(events\)/); assert.match(doc, /"recent_events":/); assert.match(doc, /"invoice":\s*clean\(summary\.group/); assert.match(doc, /"timestamp":\s*latest_event\["timestamp"\] if latest_event else None/); assert.match(doc, /"location":\s*latest_event\["location"\] if latest_event else None/); assert.match(doc, /"event_count":\s*len\(normalized_events\)/); assert.match(doc, /"recent_events":\s*normalized_events\[-min\(3,\s*len\(normalized_events\)\):\]/); assert.match(doc, /def clean_location\(raw: str\) -> str:/); assert.match(doc, /TEL/); assert.match(doc, /\\d\{2,4\}/); assert.match(doc, /"location":\s*clean_location\(location\)/); assert.doesNotMatch(doc, /"tracking_no":/); assert.doesNotMatch(doc, /"latest_event_date":/); assert.doesNotMatch(doc, /"latest_event_time":/); assert.doesNotMatch(doc, /"latest_event_location":/); assert.doesNotMatch(doc, /"delivered_to":/); assert.doesNotMatch(doc, /"delivery_result":/); } for (const [label, doc] of [ ["skill doc", skill], ["feature doc", featureDoc], ]) { assert.deepEqual( extractQuotedEntries(findPrintedObjectBlock(doc, "cj"), 4), expectedTopLevelEntries.cj, `${label} CJ example must keep the exact normalized top-level mapping`, ); assert.deepEqual( extractQuotedEntries(findPrintedObjectBlock(doc, "epost"), 4), expectedTopLevelEntries.epost, `${label} ePost example must keep the exact normalized top-level mapping`, ); assert.deepEqual( extractQuotedEntries( findRecentEventsBlock(doc, "cj"), 8, ), expectedRecentEventEntries.cj, `${label} CJ recent_events entries must keep the exact normalized mapping`, ); assert.deepEqual( extractQuotedEntries( findRecentEventsBlock(doc, "epost"), 8, ), expectedRecentEventEntries.epost, `${label} ePost recent_events entries must keep the exact normalized mapping`, ); } assert.doesNotMatch(skill, /"message":\s*latest\.get\("crgNm"\)/); assert.doesNotMatch(featureDoc, /print\(\{\s*"tracking_no"/); }); test("delivery-tracking docs publish aligned sample normalized outputs for both carriers", () => { const expectedSamples = readJson( path.join("scripts", "fixtures", "delivery-tracking-public-samples.json"), ); const skill = read(path.join("delivery-tracking", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "delivery-tracking.md")); const cjSkillOutput = findJsonFenceAfterLabel(skill, "CJ 공개 출력 예시"); const cjFeatureOutput = findJsonFenceAfterLabel(featureDoc, "CJ 공개 출력 예시"); const epostSkillOutput = findJsonFenceAfterLabel(skill, "우체국 공개 출력 예시"); const epostFeatureOutput = findJsonFenceAfterLabel(featureDoc, "우체국 공개 출력 예시"); for (const [docLabel, doc] of [ ["skill doc", skill], ["feature doc", featureDoc], ]) { for (const [carrier, label] of [ ["cj", "CJ 공개 출력 예시"], ["epost", "우체국 공개 출력 예시"], ]) { assert.equal( findJsonFenceTextAfterLabel(doc, label), JSON.stringify(expectedSamples[carrier], null, 2), `${docLabel} ${carrier} sample JSON block must stay byte-for-byte aligned with the checked-in public fixture`, ); } } assert.deepEqual(cjSkillOutput, cjFeatureOutput, "CJ sample output must stay aligned across docs"); assert.deepEqual(epostSkillOutput, epostFeatureOutput, "ePost sample output must stay aligned across docs"); assert.deepEqual(cjSkillOutput, expectedSamples.cj, "CJ sample output must stay pinned to the verified public fixture"); assert.deepEqual(epostSkillOutput, expectedSamples.epost, "ePost sample output must stay pinned to the verified public fixture"); assertSanitizedPublicOutput(cjSkillOutput, "CJ sample output"); assertSanitizedPublicOutput(epostSkillOutput, "ePost sample output"); }); test("delivery-tracking docs pin sample provenance to the verified smoke-test date and invoice", () => { const expectedProvenance = readJson( path.join("scripts", "fixtures", "delivery-tracking-public-provenance.json"), ); const skill = read(path.join("delivery-tracking", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "delivery-tracking.md")); for (const [docLabel, doc] of [ ["skill doc", skill], ["feature doc", featureDoc], ]) { assertSampleProvenance(doc, "CJ 공개 출력 예시", expectedProvenance.cj, docLabel); assertSampleProvenance(doc, "우체국 공개 출력 예시", expectedProvenance.epost, docLabel); } }); test("repository docs advertise the daiso-product-search skill", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "daiso-product-search.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/daiso-product-search.md to exist"); assert.match(readme, /\| 다이소 상품 조회 \|/); assert.match(readme, /\[다이소 상품 조회 가이드\]\(docs\/features\/daiso-product-search\.md\)/); assert.match(install, /--skill daiso-product-search/); }); test("daiso-product-search skill documents the official Daiso Mall lookup flow", () => { const skillPath = path.join(repoRoot, "daiso-product-search", "SKILL.md"); const featureDoc = read(path.join("docs", "features", "daiso-product-search.md")); assert.ok(fs.existsSync(skillPath), "expected daiso-product-search/SKILL.md to exist"); const skill = read(path.join("daiso-product-search", "SKILL.md")); assert.match(skill, /^name: daiso-product-search$/m); assert.match(skill, /다이소몰/i); assert.match(skill, /매장명/); assert.match(skill, /상품명|검색어/); assert.match(skill, /https:\/\/www\.daisomall\.co\.kr\/api\/ms\/msg\/selStr/); assert.match(skill, /https:\/\/www\.daisomall\.co\.kr\/ssn\/search\/SearchGoods/); assert.match(skill, /https:\/\/www\.daisomall\.co\.kr\/api\/pd\/pdh\/selStrPkupStck/); assert.match(skill, /공식 표면이 매장 내 진열 위치를 주지 않으면 재고 중심/); assert.match(featureDoc, /SearchGoods/); assert.match(featureDoc, /selStrPkupStck/); }); test("daiso-product-search package exposes reusable store, product, and stock helpers", () => { const pkg = require(path.join(repoRoot, "packages", "daiso-product-search", "src", "index.js")); assert.equal(typeof pkg.searchStores, "function"); assert.equal(typeof pkg.searchProducts, "function"); assert.equal(typeof pkg.getStorePickupStock, "function"); assert.equal(typeof pkg.lookupStoreProductAvailability, "function"); }); test("daiso-product-search docs record the shipped feature and official sources", () => { const roadmap = read(path.join("docs", "roadmap.md")); const sources = read(path.join("docs", "sources.md")); assert.match(roadmap, /다이소 상품 조회 스킬 출시/); assert.match(sources, /https:\/\/www\.daisomall\.co\.kr\/api\/ms\/msg\/selStr/); assert.match(sources, /https:\/\/www\.daisomall\.co\.kr\/ssn\/search\/SearchGoods/); assert.match(sources, /https:\/\/www\.daisomall\.co\.kr\/api\/pd\/pdh\/selStrPkupStck/); }); test("repository docs advertise the gangnamunni-clinic-search skill across install surfaces", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const sources = read(path.join("docs", "sources.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "gangnamunni-clinic-search.md"); const skillPath = path.join(repoRoot, "gangnamunni-clinic-search", "SKILL.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/gangnamunni-clinic-search.md to exist"); assert.ok(fs.existsSync(skillPath), "expected gangnamunni-clinic-search/SKILL.md to exist"); assert.match(readme, /\| 강남언니 병원 조회 \| `gangnamunni-clinic-search` \|/); assert.match(readme, /\[강남언니 병원 조회 가이드\]\(docs\/features\/gangnamunni-clinic-search\.md\)/); assert.match(install, /--skill gangnamunni-clinic-search/); assert.match(install, /npm install -g .*gangnamunni-clinic-search/); assert.match(sources, /강남언니 공개 검색: https:\/\/www\.gangnamunni\.com\/search\?q=/); assert.match(sources, /강남언니 공개 병원 페이지: https:\/\/www\.gangnamunni\.com\/hospitals\//); }); test("repository docs advertise the market-kurly-search skill across the documented surfaces", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const roadmap = read(path.join("docs", "roadmap.md")); const sources = read(path.join("docs", "sources.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "market-kurly-search.md"); const skillPath = path.join(repoRoot, "market-kurly-search", "SKILL.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/market-kurly-search.md to exist"); assert.ok(fs.existsSync(skillPath), "expected market-kurly-search/SKILL.md to exist"); assert.match(readme, /\| 마켓컬리 상품 조회 \|/); assert.match(readme, /\[마켓컬리 상품 조회 가이드\]\(docs\/features\/market-kurly-search\.md\)/); assert.match(install, /--skill market-kurly-search/); assert.match(install, /npm install -g .* market-kurly-search/); assert.match(roadmap, /마켓컬리 상품 조회 스킬 출시/); assert.match(sources, /https:\/\/api\.kurly\.com\/search\/v4\/sites\/market\/normal-search/); assert.match(sources, /https:\/\/api\.kurly\.com\/search\/v3\/sites\/market\/normal-search\/count/); assert.match(sources, /https:\/\/www\.kurly\.com\/goods\/5063110/); }); test("market-kurly-search skill and docs describe the unauthenticated Kurly search and detail flow", () => { const skill = read(path.join("market-kurly-search", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "market-kurly-search.md")); assert.match(skill, /^name: market-kurly-search$/m); assert.match(skill, /^description: .*마켓컬리.*상품.*가격.*$/m); for (const doc of [skill, featureDoc]) { assert.match(doc, /api\.kurly\.com\/search\/v4\/sites\/market\/normal-search/); assert.match(doc, /api\.kurly\.com\/search\/v3\/sites\/market\/normal-search\/count/); assert.match(doc, /www\.kurly\.com\/goods\/|www\.kurly\.com\/goods\/5063110/); assert.match(doc, /로그인 없이|비로그인/); assert.match(doc, /현재 가격|할인/); assert.match(doc, /품절 여부|판매 상태/); assert.match(doc, /가격.*달라질 수|시점에 따라 달라질 수/u); assert.match(doc, /주문|장바구니/); assert.match(doc, /보수적으로|보수적/); } }); test("market-kurly-search package exposes reusable search/count/detail helpers", () => { const pkg = require(path.join(repoRoot, "packages", "market-kurly-search", "src", "index.js")); assert.equal(typeof pkg.searchProducts, "function"); assert.equal(typeof pkg.countProducts, "function"); assert.equal(typeof pkg.getProductDetail, "function"); }); test("repository docs advertise the olive-young-search skill across the documented surfaces", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const roadmap = read(path.join("docs", "roadmap.md")); const sources = read(path.join("docs", "sources.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "olive-young-search.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/olive-young-search.md to exist"); assert.match(readme, /\| 올리브영 검색 \|/); assert.match(readme, /\[올리브영 검색 가이드\]\(docs\/features\/olive-young-search\.md\)/); assert.match(install, /--skill olive-young-search/); assert.match(install, /npm install -g .* daiso/); assert.match(roadmap, /올리브영 검색 스킬 출시/); assert.match(sources, /https:\/\/github\.com\/hmmhmmhm\/daiso-mcp/); assert.match(sources, /https:\/\/www\.npmjs\.com\/package\/daiso/); assert.match(sources, /https:\/\/mcp\.aka\.page\/api\/oliveyoung\/stores/); assert.match(sources, /https:\/\/mcp\.aka\.page\/api\/oliveyoung\/products/); assert.match(sources, /https:\/\/mcp\.aka\.page\/api\/oliveyoung\/inventory/); }); test("olive-young install docs warn about intermittent public endpoint failures and direct users to retry or clone fallback", () => { const install = read(path.join("docs", "install.md")); const quickstart = findSection(install, "### `olive-young-search` upstream CLI quickstart"); assert.match(install, /olive-young-search/); assert.match(install, /5xx\/503/); assert.match(install, /재시도|retry/i); assert.match(install, /clone fallback|git clone https:\/\/github\.com\/hmmhmmhm\/daiso-mcp\.git/i); assertOliveYoungCloneFallbackShorthand(quickstart, "olive-young install quickstart"); assertOliveYoungCloneFallbackCommands(quickstart, "olive-young install quickstart"); }); test("olive-young-search skill documents the upstream daiso CLI flow for stores, products, and inventory", () => { const skillPath = path.join(repoRoot, "olive-young-search", "SKILL.md"); const featureDoc = read(path.join("docs", "features", "olive-young-search.md")); assert.ok(fs.existsSync(skillPath), "expected olive-young-search/SKILL.md to exist"); const skill = read(path.join("olive-young-search", "SKILL.md")); const featureTop = findSection(featureDoc, "## 가장 중요한 규칙"); const featureFallback = findSection(featureDoc, "## 원본 저장소 clone fallback"); const skillFallback = findSection(skill, "## Fallback: clone the original repository and run the same CLI locally"); assert.match(skill, /^name: olive-young-search$/m); assert.match(skill, /^description: .*올리브영.*매장.*상품.*재고.*$/m); for (const doc of [skill, featureDoc]) { assert.match(doc, /hmmhmmhm\/daiso-mcp/); assert.match(doc, /https:\/\/github\.com\/hmmhmmhm\/daiso-mcp/); assert.match(doc, /npm install -g daiso|npx --yes daiso|npx daiso/); assert.match(doc, /git clone https:\/\/github\.com\/hmmhmmhm\/daiso-mcp\.git/); assert.match(doc, /npm install/); assert.match(doc, /npm run build/); assert.match(doc, /MCP 서버를 .*직접 설치.*않고.*CLI/u); assert.match(doc, /매장 검색/); assert.match(doc, /상품 검색/); assert.match(doc, /재고 확인/); assert.match(doc, /\/api\/oliveyoung\/stores/); assert.match(doc, /\/api\/oliveyoung\/products/); assert.match(doc, /\/api\/oliveyoung\/inventory/); assert.match(doc, /vendoring 하지 않/); } assertOliveYoungCloneFallbackShorthand(featureTop, "olive-young feature guide shorthand"); for (const fallbackDoc of [featureFallback, skillFallback]) { assertOliveYoungCloneFallbackCommands(fallbackDoc, "olive-young clone fallback docs"); } }); test("repository docs advertise the korean-cinema-search skill across the documented surfaces", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const roadmap = read(path.join("docs", "roadmap.md")); const sources = read(path.join("docs", "sources.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "korean-cinema-search.md"); const skillPath = path.join(repoRoot, "korean-cinema-search", "SKILL.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/korean-cinema-search.md to exist"); assert.ok(fs.existsSync(skillPath), "expected korean-cinema-search/SKILL.md to exist"); assert.match(readme, /\| 영화관 검색 \|/); assert.match(readme, /\[영화관 검색 가이드\]\(docs\/features\/korean-cinema-search\.md\)/); assert.match(install, /--skill korean-cinema-search/); assert.match(install, /--playDate /); assert.match(install, /5xx|retry|재시도/); assert.match(install, /git clone https:\/\/github\.com\/hmmhmmhm\/daiso-mcp\.git && cd daiso-mcp && npm install && npm run build/); assert.match(install, /node dist\/bin\.js get \/api\/cgv\/timetable --keyword 강남 --playDate --json/); assert.match(roadmap, /영화관 검색 스킬 출시/); assert.match(sources, /https:\/\/github\.com\/hmmhmmhm\/daiso-mcp/); assert.match(sources, /https:\/\/www\.npmjs\.com\/package\/daiso/); assert.match(sources, /https:\/\/mcp\.aka\.page\/api\/cgv\/theaters/); assert.match(sources, /https:\/\/mcp\.aka\.page\/api\/megabox\/theaters/); assert.match(sources, /https:\/\/mcp\.aka\.page\/api\/lottecinema\/theaters/); }); test("korean-cinema-search skill documents the upstream daiso CLI flow for Korean cinemas", () => { const skillPath = path.join(repoRoot, "korean-cinema-search", "SKILL.md"); const featureDoc = read(path.join("docs", "features", "korean-cinema-search.md")); assert.ok(fs.existsSync(skillPath), "expected korean-cinema-search/SKILL.md to exist"); const skill = read(path.join("korean-cinema-search", "SKILL.md")); assert.match(skill, /^name: korean-cinema-search$/m); assert.match(skill, /^description: .*CGV.*메가박스.*롯데시네마.*$/m); for (const doc of [skill, featureDoc]) { assert.match(doc, /hmmhmmhm\/daiso-mcp/); assert.match(doc, /npm install -g daiso|npx --yes daiso|npx daiso/); assert.match(doc, /MCP 서버를 .*직접 설치.*않고.*CLI/u); assert.match(doc, /Asia\/Seoul|KST/); assert.match(doc, /YYYYMMDD/); assert.match(doc, /--playDate /); assert.match(doc, /\/api\/cgv\/movies --keyword 강남 --playDate --json/); assert.match(doc, /\/api\/cgv\/timetable --keyword 강남 --playDate --json/); assert.match(doc, /\/api\/megabox\/seats --keyword 코엑스 --playDate --limit 10 --json/); assert.match(doc, /\/api\/lottecinema\/seats --keyword 월드타워 --playDate --limit 10 --json/); assert.match(doc, /CGV/); assert.match(doc, /메가박스/); assert.match(doc, /롯데시네마/); assert.match(doc, /\/api\/cgv\/theaters/); assert.match(doc, /\/api\/cgv\/movies/); assert.match(doc, /\/api\/cgv\/timetable/); assert.match(doc, /\/api\/megabox\/theaters/); assert.match(doc, /\/api\/megabox\/movies/); assert.match(doc, /\/api\/megabox\/seats/); assert.match(doc, /\/api\/lottecinema\/theaters/); assert.match(doc, /\/api\/lottecinema\/movies/); assert.match(doc, /\/api\/lottecinema\/seats/); assert.match(doc, /예매|결제/); } }); test("repository docs advertise the bunjang-search skill across the documented surfaces", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const roadmap = read(path.join("docs", "roadmap.md")); const sources = read(path.join("docs", "sources.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "bunjang-search.md"); const skillPath = path.join(repoRoot, "bunjang-search", "SKILL.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/bunjang-search.md to exist"); assert.ok(fs.existsSync(skillPath), "expected bunjang-search/SKILL.md to exist"); assert.match(readme, /\| 번개장터 검색 \|/); assert.match(readme, /\[번개장터 검색 가이드\]\(docs\/features\/bunjang-search\.md\)/); assert.match(install, /--skill bunjang-search/); assert.match(install, /npm install -g .* bunjang-cli/); assert.match(roadmap, /번개장터 검색 스킬 출시/); assert.match(sources, /https:\/\/www\.npmjs\.com\/package\/bunjang-cli/); assert.match(sources, /https:\/\/github\.com\/pinion05\/bunjangcli/); }); test("bunjang-search skill documents bunjang-cli search, detail, favorite, chat, and AI export flows", () => { const skill = read(path.join("bunjang-search", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "bunjang-search.md")); const install = read(path.join("docs", "install.md")); assert.match(skill, /^name: bunjang-search$/m); assert.match(skill, /^description: .*번개장터.*검색.*상세.*찜.*채팅.*$/m); for (const doc of [skill, featureDoc]) { assert.match(doc, /bunjang-cli/); assert.match(doc, /pinion05\/bunjangcli/); assert.match(doc, /npx --yes bunjang-cli --help/); assert.match(doc, /npx --yes bunjang-cli search /); assert.match(doc, /item get/); assert.match(doc, /favorite add/); assert.match(doc, /favorite remove/); assert.match(doc, /favorite list/); assert.match(doc, /chat list/); assert.match(doc, /chat start/); assert.match(doc, /chat send/); assert.match(doc, /--start-page/); assert.match(doc, /--pages/); assert.match(doc, /--max-items/); assert.match(doc, /--with-detail/); assert.match(doc, /--output/); assert.match(doc, /--ai/); assert.match(doc, /TOON|toon/i); assert.match(doc, /TTY|interactive/); assert.match(doc, /로그인.*선택적|선택적.*로그인/u); assert.match( doc, /검색 결과.*(제목.?가격|가격.?제목).*(1차|우선)|title.?price.*(triage|first)/i, ); assert.match( doc, /(description|status|location).*(item get|--with-detail).*(전|먼저|이후)|((item get|--with-detail).*(description|status|location).*(전|먼저|이후))/i, ); assert.match(doc, /노이즈|noisy|불안정|rely on/i); } assert.match(install, /### `bunjang-search` upstream CLI quickstart/); assert.match(install, /npx --yes bunjang-cli --help/); assert.match(install, /npx --yes bunjang-cli search "아이폰"/); assert.match(install, /npx --yes bunjang-cli --json item get/); }); test("repository docs advertise the coupang-product-search skill", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "coupang-product-search.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/coupang-product-search.md to exist"); assert.match(readme, /\| 쿠팡 상품 검색 \|/); assert.match(readme, /\[쿠팡 상품 검색 가이드\]\(docs\/features\/coupang-product-search\.md\)/); assert.match(install, /--skill coupang-product-search/); }); test("coupang-product-search skill and docs use retention-corp coupang_partners MCP layer", () => { const skillPath = path.join(repoRoot, "coupang-product-search", "SKILL.md"); const wrapperPath = path.join(repoRoot, "coupang-product-search", "scripts", "coupang_partners_mcp.py"); const featureDoc = read(path.join("docs", "features", "coupang-product-search.md")); const sources = read(path.join("docs", "sources.md")); assert.ok(fs.existsSync(skillPath), "expected coupang-product-search/SKILL.md to exist"); assert.ok(fs.existsSync(wrapperPath), "expected retention-corp wrapper script to exist"); const skill = read(path.join("coupang-product-search", "SKILL.md")); for (const doc of [skill, featureDoc]) { assert.match(doc, /retention-corp\/coupang_partners/); assert.match(doc, /local:\/\/coupang-mcp/); assert.match(doc, /coupang_partners_mcp\.py/); assert.match(doc, /--repo-dir/); assert.match(doc, /--no-clone/); assert.match(doc, /--update/); assert.match(doc, /coupang_partners_mcp\.py\s+tools/); assert.match(doc, /coupang_partners_mcp\.py\s+init/); assert.match(doc, /search_coupang_products/); assert.match(doc, /로켓배송/); assert.match(doc, /a\.retn\.kr\/v1\/public\/assist/); assert.match(doc, /OPENCLAW_SHOPPING_/); assert.match(doc, /(파트너스|어필리에이트|affiliate)/i); assert.match(doc, /(hosted\s*fallback|호스티드\s*폴백|호스티드\s*fallback)/i); assert.doesNotMatch(doc, /yuju777-coupang-mcp\.hf\.space\/mcp/); assert.doesNotMatch(doc, /github\.com\/uju777\/coupang-mcp/); } assert.match(sources, /retention-corp\/coupang_partners/); assert.match(sources, /a\.retn\.kr\/v1\/public\/assist/); assert.doesNotMatch(sources, /yuju777-coupang-mcp\.hf\.space\/mcp/); }); test("coupang-product-search docs drop non-allowlisted coupang-mcp-fallback and document openclaw-skill as the allowlisted hosted fallback client-id", () => { // Direct probes against https://a.retn.kr/v1/public/assist on 2026-04-21 show that // `X-OpenClaw-Client-Id: coupang-mcp-fallback` returns HTTP 403 ("Client is not // allowlisted"), while `openclaw-skill` (the upstream default that ships with // retention-corp/coupang_partners) returns HTTP 200. Until Retention Corp // re-allowlists `coupang-mcp-fallback`, k-skill docs must not recommend it and // must document `openclaw-skill` as the value the hosted fallback path uses. const skill = read(path.join("coupang-product-search", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "coupang-product-search.md")); const wrapper = read(path.join("coupang-product-search", "scripts", "coupang_partners_mcp.py")); const sources = read(path.join("docs", "sources.md")); for (const doc of [skill, featureDoc, wrapper, sources]) { assert.doesNotMatch(doc, /coupang-mcp-fallback/); } for (const doc of [skill, featureDoc, wrapper]) { assert.match(doc, /openclaw-skill/); } }); test("repository docs advertise the ohou-today-deal skill", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const roadmap = read(path.join("docs", "roadmap.md")); const sources = read(path.join("docs", "sources.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "ohou-today-deal.md"); const skillPath = path.join(repoRoot, "ohou-today-deal", "SKILL.md"); const helperPath = path.join(repoRoot, "ohou-today-deal", "scripts", "ohou_today_deal.py"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/ohou-today-deal.md to exist"); assert.ok(fs.existsSync(skillPath), "expected ohou-today-deal/SKILL.md to exist"); assert.ok(fs.existsSync(helperPath), "expected ohou-today-deal helper script to exist"); assert.match(readme, /\| 오늘의집 오늘의딜 조회 \| `ohou-today-deal` \|/); assert.match(readme, /\[오늘의집 오늘의딜 조회 가이드\]\(docs\/features\/ohou-today-deal\.md\)/); assert.match(install, /--skill ohou-today-deal/); assert.match(roadmap, /오늘의집 오늘의딜 조회 스킬 출시/); assert.match(sources, /ohou\.se\/commerces\/today_deals/); assert.match(sources, /store\.ohou\.se\/today_deals/); }); test("ohou-today-deal docs lock the public Next data read-only workflow", () => { const skill = read(path.join("ohou-today-deal", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "ohou-today-deal.md")); const helper = read(path.join("ohou-today-deal", "scripts", "ohou_today_deal.py")); const sources = read(path.join("docs", "sources.md")); for (const doc of [skill, featureDoc]) { assert.match(doc, /https:\/\/ohou\.se\/commerces\/today_deals/); assert.match(doc, /https:\/\/store\.ohou\.se\/today_deals/); assert.match(doc, /__NEXT_DATA__/); assert.match(doc, /today-deal-feed/); assert.match(doc, /ohou_today_deal\.py list/); assert.match(doc, /(로그인|API key|API 키).*(불필요|없음)|(불필요|없음).*(로그인|API key|API 키)/); assert.match(doc, /(구매|장바구니|결제).*자동화.*(하지 않는다|하지 말고|제외)/); } assert.match(helper, /DEFAULT_URL = "https:\/\/ohou\.se\/commerces\/today_deals"/); assert.match(helper, /__NEXT_DATA__/); assert.match(helper, /today-deal-feed/); assert.match(helper, /special-today-deal-feed/); assert.match( helper, /k-skill-ohou-today-deal\/1\.0 \(\+https:\/\/github\.com\/NomaDamas\/k-skill\)/, ); assert.match(sources, /ohou\.se\/commerces\/today_deals/); assert.match(sources, /store\.ohou\.se\/today_deals/); }); test("root pack:dry-run script covers all publishable workspaces", () => { const packageJson = readJson("package.json"); const packScript = packageJson.scripts["pack:dry-run"]; const publishableWorkspaces = fs .readdirSync(path.join(repoRoot, "packages"), { withFileTypes: true }) .filter((entry) => entry.isDirectory()) .map((entry) => path.join("packages", entry.name, "package.json")) .filter((packagePath) => fs.existsSync(path.join(repoRoot, packagePath))) .map((packagePath) => readJson(packagePath)) .filter((workspacePackage) => workspacePackage.private !== true) .map((workspacePackage) => workspacePackage.name); assert.ok(publishableWorkspaces.includes("donation-place-search")); for (const workspaceName of publishableWorkspaces) { assert.match(packScript, new RegExp(`workspace ${escapeRegex(workspaceName)}(?:\\s|$)`)); } }); test("README main capability table advertises the donation-place-search skill", () => { const readme = read("README.md"); const tableSection = findSection(readme, "## 어떤 걸 할 수 있나"); assert.match(tableSection, /기부처 조회/); assert.match(tableSection, /`donation-place-search`/); assert.match(tableSection, /docs\/features\/donation-place-search\.md/); }); test("donation-place-search install docs include the skill and npm helper", () => { const install = read(path.join("docs", "install.md")); assert.match(install, /--skill donation-place-search/); assert.match(install, /npm install -g .*donation-place-search/); }); test("donation-place-search docs describe 1365 links as best-effort verification assists", () => { const skill = read(path.join("donation-place-search", "SKILL.md")); const packageReadme = read(path.join("packages", "donation-place-search", "README.md")); const featureDoc = read(path.join("docs", "features", "donation-place-search.md")); const packageJson = readJson(path.join("packages", "donation-place-search", "package.json")); for (const doc of [skill, packageReadme, featureDoc, packageJson.description]) { assert.match(doc, /best-effort|보조|assist/i); assert.doesNotMatch(doc, /candidate-verified|후보별 검증 완료/); } }); test("repository docs advertise the kbl-results skill across the documented surfaces", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const roadmap = read(path.join("docs", "roadmap.md")); const sources = read(path.join("docs", "sources.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "kbl-results.md"); const skillPath = path.join(repoRoot, "kbl-results", "SKILL.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/kbl-results.md to exist"); assert.ok(fs.existsSync(skillPath), "expected kbl-results/SKILL.md to exist"); assert.match(readme, /\| KBL 경기 결과 조회 \|/); assert.match(readme, /\[KBL 경기 결과 가이드\]\(docs\/features\/kbl-results\.md\)/); assert.match(install, /--skill kbl-results/); assert.match(roadmap, /KBL 경기 결과 조회 스킬 출시/); assert.match(sources, /KBL 일정\/결과 API: https:\/\/api\.kbl\.or\.kr\/match\/list/); assert.match(sources, /KBL 팀 순위 API: https:\/\/api\.kbl\.or\.kr\/league\/rank\/team/); }); test("kbl-results skill documents the official JSON flow for date, team, and standings lookups", () => { const skillPath = path.join(repoRoot, "kbl-results", "SKILL.md"); assert.ok(fs.existsSync(skillPath), "expected kbl-results/SKILL.md to exist"); const skill = read(path.join("kbl-results", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "kbl-results.md")); assert.match(skill, /^name: kbl-results$/m); assert.match(skill, /^description: .*KBL.*경기 결과.*순위.*$/m); for (const doc of [skill, featureDoc]) { assert.match(doc, /YYYY-MM-DD/); assert.match(doc, /서울 SK|부산 KCC|팀 코드/); assert.match(doc, /https:\/\/api\.kbl\.or\.kr\/match\/list/); assert.match(doc, /https:\/\/api\.kbl\.or\.kr\/league\/rank\/team/); assert.match(doc, /공식 JSON|공식 API|공식 표면/u); assert.match(doc, /현재 순위|standings/i); assert.match(doc, /kbl-results|KBL 경기 결과/u); } }); test("kbl-results package exports reusable results and standings helpers", () => { const pkg = require(path.join(repoRoot, "packages", "kbl-results", "src", "index.js")); assert.equal(typeof pkg.getMatchResults, "function"); assert.equal(typeof pkg.getStandings, "function"); assert.equal(typeof pkg.getKBLSummary, "function"); }); test("kbl-results package README stays aligned with the official KBL JSON lookup flow", () => { const packageReadme = read(path.join("packages", "kbl-results", "README.md")); assert.match(packageReadme, /공식 KBL JSON 엔드포인트/u); assert.match(packageReadme, /api\.kbl\.or\.kr\/match\/list/); assert.match(packageReadme, /league\/rank\/team/); assert.match(packageReadme, /getKBLSummary/); assert.match(packageReadme, /서울 SK/); }); test("repository docs advertise the kleague-results skill across the documented surfaces", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const roadmap = read(path.join("docs", "roadmap.md")); const sources = read(path.join("docs", "sources.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "kleague-results.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/kleague-results.md to exist"); assert.match(readme, /\| K리그 경기 결과 조회 \|/); assert.match(readme, /\[K리그 결과 가이드\]\(docs\/features\/kleague-results\.md\)/); assert.match(install, /--skill kleague-results/); assert.match(roadmap, /K리그 경기 결과 조회 스킬 출시/); assert.match(sources, /K League 일정\/결과 JSON: https:\/\/www\.kleague\.com\/getScheduleList\.do/); assert.match(sources, /K League 팀 순위 JSON: https:\/\/www\.kleague\.com\/record\/teamRank\.do/); }); test("kleague-results skill documents the official JSON flow for date, team, and standings lookups", () => { const skillPath = path.join(repoRoot, "kleague-results", "SKILL.md"); assert.ok(fs.existsSync(skillPath), "expected kleague-results/SKILL.md to exist"); const skill = read(path.join("kleague-results", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "kleague-results.md")); assert.match(skill, /^name: kleague-results$/m); assert.match(skill, /^description: .*케이리그.*경기 결과.*순위.*$/m); for (const doc of [skill, featureDoc]) { assert.match(doc, /YYYY-MM-DD/); assert.match(doc, /K리그1|K리그2/); assert.match(doc, /FC서울|서울 이랜드|팀 코드/); assert.match(doc, /https:\/\/www\.kleague\.com\/getScheduleList\.do/); assert.match(doc, /https:\/\/www\.kleague\.com\/record\/teamRank\.do/); assert.match(doc, /공식 JSON|공식 API|공식 표면/u); assert.match(doc, /현재 순위|standings/i); assert.match(doc, /kleague-results|K리그 결과 조회/u); } }); test("kleague-results package exports reusable results and standings helpers", () => { const pkg = require(path.join(repoRoot, "packages", "kleague-results", "src", "index.js")); assert.equal(typeof pkg.getMatchResults, "function"); assert.equal(typeof pkg.getStandings, "function"); assert.equal(typeof pkg.getKLeagueSummary, "function"); }); test("kleague-results package README stays aligned with the official K League JSON lookup flow", () => { const packageReadme = read(path.join("packages", "kleague-results", "README.md")); assert.match(packageReadme, /공식 K리그 JSON 엔드포인트/u); assert.match(packageReadme, /getScheduleList\.do/); assert.match(packageReadme, /teamRank\.do/); assert.match(packageReadme, /getKLeagueSummary/); assert.match(packageReadme, /FC서울/); }); test("unsupported naver map and blue ribbon skills are archived outside default docs", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const sources = read(path.join("docs", "sources.md")); const legacyReadmePath = path.join(repoRoot, "legacy", "README.md"); const blueRibbonSkillPath = path.join(repoRoot, "legacy", "unsupported-skills", "blue-ribbon-nearby", "SKILL.md"); const naverMapSkillPath = path.join(repoRoot, "legacy", "unsupported-skills", "naver-map-route", "SKILL.md"); const blueRibbonDocPath = path.join( repoRoot, "legacy", "unsupported-skills", "blue-ribbon-nearby", "docs", "features", "blue-ribbon-nearby.md", ); const naverMapDocPath = path.join( repoRoot, "legacy", "unsupported-skills", "naver-map-route", "docs", "features", "naver-map-route.md", ); assert.ok(fs.existsSync(legacyReadmePath), "expected legacy/README.md to explain archived unsupported skills"); assert.ok(fs.existsSync(blueRibbonSkillPath), "expected blue-ribbon-nearby SKILL.md to be preserved under legacy"); assert.ok(fs.existsSync(naverMapSkillPath), "expected naver-map-route SKILL.md to be preserved under legacy"); assert.ok(fs.existsSync(blueRibbonDocPath), "expected blue-ribbon-nearby feature doc to be preserved under legacy"); assert.ok(fs.existsSync(naverMapDocPath), "expected naver-map-route feature doc to be preserved under legacy"); assert.ok(!fs.existsSync(path.join(repoRoot, "blue-ribbon-nearby", "SKILL.md"))); assert.ok(!fs.existsSync(path.join(repoRoot, "naver-map-route", "SKILL.md"))); assert.ok(!fs.existsSync(path.join(repoRoot, "docs", "features", "blue-ribbon-nearby.md"))); assert.ok(!fs.existsSync(path.join(repoRoot, "docs", "features", "naver-map-route.md"))); for (const doc of [readme, install, sources]) { assert.doesNotMatch(doc, /blue-ribbon-nearby|naver-map-route/); assert.doesNotMatch(doc, /근처 블루리본 맛집|네이버맵 길찾기|네이버맵 자동차 길찾기/); } }); test("repository docs advertise the kakao-bar-nearby skill across the documented surfaces", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const roadmap = read(path.join("docs", "roadmap.md")); const sources = read(path.join("docs", "sources.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "kakao-bar-nearby.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/kakao-bar-nearby.md to exist"); assert.match(readme, /\| 근처 술집 조회 \|/); assert.match(readme, /\[근처 술집 조회 가이드\]\(docs\/features\/kakao-bar-nearby\.md\)/); assert.match(install, /--skill kakao-bar-nearby/); assert.match(roadmap, /근처 술집 조회 스킬 출시/); assert.match(sources, /카카오맵 모바일 검색: https:\/\/m\.map\.kakao\.com\/actions\/searchView/); assert.match(sources, /카카오맵 장소 패널 JSON: https:\/\/place-api\.map\.kakao\.com\/places\/panel3\//); }); test("kakao-bar-nearby skill documents location-first Kakao Map search with open-now/menu/seating hints", () => { const skillPath = path.join(repoRoot, "kakao-bar-nearby", "SKILL.md"); assert.ok(fs.existsSync(skillPath), "expected kakao-bar-nearby/SKILL.md to exist"); const skill = read(path.join("kakao-bar-nearby", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "kakao-bar-nearby.md")); assert.match(skill, /^name: kakao-bar-nearby$/m); for (const doc of [skill, featureDoc]) { assert.match(doc, /현재 위치/); assert.match(doc, /서울역|강남|사당|논현/); assert.match(doc, /https:\/\/m\.map\.kakao\.com\/actions\/searchView/); assert.match(doc, /https:\/\/place-api\.map\.kakao\.com\/places\/panel3\//); assert.match(doc, /영업 중|영업전|영업 상태/); assert.match(doc, /메뉴/); assert.match(doc, /단체석|좌석 옵션|인원 수용/); assert.match(doc, /전화번호/); assert.match(doc, /kakao-bar-nearby|근처 술집 조회/u); } }); test("kakao-bar-nearby package README stays aligned with the Kakao Map live lookup flow", () => { const packageReadme = read(path.join("packages", "kakao-bar-nearby", "README.md")); assert.match(packageReadme, /현재 위치를 먼저 물어본다/u); assert.match(packageReadme, /서울역 술집/); assert.match(packageReadme, /https:\/\/m\.map\.kakao\.com\/actions\/searchView/); assert.match(packageReadme, /https:\/\/place-api\.map\.kakao\.com\/places\/panel3\//); assert.match(packageReadme, /searchNearbyBarsByLocationQuery/); }); test("kakao-bar-nearby feature doc keeps the verified 2026-03-29 sadang smoke snapshot current", () => { const featureDoc = read(path.join("docs", "features", "kakao-bar-nearby.md")); const smoke = findJsonFenceAfterLabel(featureDoc, "## 검증된 live smoke 예시"); assertKakaoBarNearbySadangSmokeSnapshot(smoke, "feature doc smoke snapshot"); }); test("kakao-bar-nearby package README live smoke snapshot matches the verified 2026-03-29 sadang output", () => { const packageReadme = read(path.join("packages", "kakao-bar-nearby", "README.md")); const smoke = findJsonFenceAfterLabel(packageReadme, "## Live smoke snapshot"); assertKakaoBarNearbySadangSmokeSnapshot(smoke, "package README smoke snapshot"); }); test("repository docs advertise the fine-dust-location skill across the documented surfaces", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const roadmap = read(path.join("docs", "roadmap.md")); const sources = read(path.join("docs", "sources.md")); const setup = read(path.join("docs", "setup.md")); const security = read(path.join("docs", "security-and-secrets.md")); const secretsExample = read(path.join("examples", "secrets.env.example")); const featureDocPath = path.join(repoRoot, "docs", "features", "fine-dust-location.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/fine-dust-location.md to exist"); assert.match(readme, /\| 사용자 위치 미세먼지 조회 \|/); assert.match(readme, /\[사용자 위치 미세먼지 조회 가이드\]\(docs\/features\/fine-dust-location\.md\)/); assert.match(install, /--skill fine-dust-location/); assert.match(roadmap, /사용자 위치 미세먼지 조회 스킬 출시/); assert.match(sources, /에어코리아 대기오염정보: https:\/\/www\.data\.go\.kr\/data\/15073861\/openapi\.do/); assert.match(sources, /에어코리아 측정소정보: https:\/\/www\.data\.go\.kr\/data\/15073877\/openapi\.do/); assert.match(setup, /AIR_KOREA_OPEN_API_KEY/); assert.match(security, /AIR_KOREA_OPEN_API_KEY/); assert.match(secretsExample, /^AIR_KOREA_OPEN_API_KEY=replace-me$/m); }); test("fine-dust-location skill documents the official two-api flow and fallback handling", () => { const skillPath = path.join(repoRoot, "fine-dust-location", "SKILL.md"); assert.ok(fs.existsSync(skillPath), "expected fine-dust-location/SKILL.md to exist"); const skill = read(path.join("fine-dust-location", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "fine-dust-location.md")); assert.match(skill, /^name: fine-dust-location$/m); assert.match(skill, /^description: .*미세먼지.*초미세먼지.*위치.*$/m); assert.match(skill, /k-skill-proxy\.nomadamas\.org\/v1\/fine-dust\/report/); assert.match(skill, /행정구역 이름/u); assert.match(skill, /강남구/); assert.match(skill, /python3 scripts\/fine_dust\.py/); assert.match(skill, /docs\/features\/fine-dust-location\.md/); assert.match(skill, /docs\/features\/k-skill-proxy\.md/); assert.match(skill, /PM10/); assert.match(skill, /PM2\.5|PM25/); assert.match(skill, /통합대기등급/); for (const doc of [featureDoc]) { assert.match(doc, /AIR_KOREA_OPEN_API_KEY/); assert.match(doc, /B552584\/MsrstnInfoInqireSvc\/getMsrstnList/); assert.match(doc, /B552584\/ArpltnInforInqireSvc\/getMsrstnAcctoRltmMesureDnsty/); assert.match(doc, /getCtprvnRltmMesureDnsty/); assert.match(doc, /PM10/); assert.match(doc, /PM2\.5|PM25/); assert.match(doc, /행정구역|지역명/); assert.match(doc, /fallback|폴백|대체 흐름/i); assert.match(doc, /후보 측정소|candidate_stations/); assert.match(doc, /조회 시각|조회 시점/); assert.match(doc, /python3 scripts\/fine_dust\.py/); } }); test("fine-dust helper python regression tests pass", () => { const result = childProcess.spawnSync( "python3", ["-m", "unittest", "discover", "-s", "scripts", "-p", "test_fine_dust.py"], { cwd: repoRoot, encoding: "utf8" }, ); assert.equal( result.status, 0, `expected python fine-dust helper regression tests to pass\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`, ); }); test("repository docs advertise the toss-securities skill across the documented surfaces", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const roadmap = read(path.join("docs", "roadmap.md")); const sources = read(path.join("docs", "sources.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "toss-securities.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/toss-securities.md to exist"); assert.match(readme, /\| 토스증권 조회 \|/); assert.match(readme, /\[토스증권 조회 가이드\]\(docs\/features\/toss-securities\.md\)/); assert.match(install, /--skill toss-securities/); assert.match(roadmap, /토스증권 조회 스킬 출시/); assert.match(sources, /tossinvest-cli: https:\/\/github\.com\/JungHoonGhae\/tossinvest-cli/); }); test("repository docs advertise the hipass-receipt skill across the documented surfaces", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const roadmap = read(path.join("docs", "roadmap.md")); const sources = read(path.join("docs", "sources.md")); const setup = read(path.join("docs", "setup.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "hipass-receipt.md"); const skillPath = path.join(repoRoot, "hipass-receipt", "SKILL.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/hipass-receipt.md to exist"); assert.ok(fs.existsSync(skillPath), "expected hipass-receipt/SKILL.md to exist"); assert.match(readme, /\| 하이패스 영수증 발급 \|/); assert.match(readme, /\[하이패스 영수증 발급 가이드\]\(docs\/features\/hipass-receipt\.md\)/); assert.match(install, /--skill hipass-receipt/); assert.match(setup, /하이패스 영수증 발급 \| 사용자 시크릿 불필요 \(로그인된 브라우저 세션 필요\)/); assert.match(roadmap, /하이패스 영수증 발급 스킬 출시/); assert.match(sources, /https:\/\/www\.hipass\.co\.kr\/main\.do/); assert.match(sources, /https:\/\/www\.hipass\.co\.kr\/html\/guide\/siteguide_6\.jsp/); }); test("toss-securities skill documents the tossctl install, auth, and read-only workflow", () => { const skillPath = path.join(repoRoot, "toss-securities", "SKILL.md"); assert.ok(fs.existsSync(skillPath), "expected toss-securities/SKILL.md to exist"); const skill = read(path.join("toss-securities", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "toss-securities.md")); assert.match(skill, /^name: toss-securities$/m); for (const doc of [skill, featureDoc]) { assert.match(doc, /tossctl/); assert.match(doc, /JungHoonGhae\/tossinvest-cli/); assert.match(doc, /auth login/); assert.match(doc, /account summary/); assert.match(doc, /portfolio positions/); assert.match(doc, /quote get/); assert.match(doc, /watchlist list/); assert.match(doc, /read-only|조회 전용/u); assert.doesNotMatch(doc, /order place/); } }); test("hipass-receipt skill documents the logged-in browser session contract", () => { const skillPath = path.join(repoRoot, "hipass-receipt", "SKILL.md"); const packageReadmePath = path.join(repoRoot, "packages", "hipass-receipt", "README.md"); assert.ok(fs.existsSync(skillPath), "expected hipass-receipt/SKILL.md to exist"); assert.ok(fs.existsSync(packageReadmePath), "expected packages/hipass-receipt/README.md to exist"); const skill = read(path.join("hipass-receipt", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "hipass-receipt.md")); const packageReadme = read(path.join("packages", "hipass-receipt", "README.md")); assert.match(skill, /^name: hipass-receipt$/m); assert.match(skill, /로그인은 반드시 사용자가 직접 해야 한다/); assert.match(skill, /Playwright persistent context|user-data-dir/); assert.match(skill, /세션이 만료되면 즉시 중단하고 다시 로그인/); assert.match(featureDoc, /20분/); assert.match(featureDoc, /영수증선택출력|영수증전체출력/); assert.match(featureDoc, /로그인된 브라우저 세션에서만 동작/); assert.match(featureDoc, /playwright-core/); assert.match(skill, /--encrypted-card-number/); assert.match(packageReadme, /buildUsageHistoryQuery/); assert.match(packageReadme, /parseUsageHistoryList/); assert.match(packageReadme, /inspectHipassPage/); assert.match(packageReadme, /playwright-core/); }); test("toss-securities package exposes safe read-only tossctl helpers", () => { const pkg = require(path.join(repoRoot, "packages", "toss-securities", "src", "index.js")); assert.equal(typeof pkg.buildReadOnlyCommand, "function"); assert.equal(typeof pkg.runReadOnlyCommand, "function"); assert.equal(typeof pkg.getAccountSummary, "function"); assert.equal(typeof pkg.getPortfolioPositions, "function"); assert.equal(typeof pkg.getQuote, "function"); assert.equal(typeof pkg.getQuoteBatch, "function"); assert.equal(typeof pkg.listWatchlist, "function"); }); test("hipass-receipt package exposes fixture-friendly query, parse, and session helpers", () => { const pkg = require(path.join(repoRoot, "packages", "hipass-receipt", "src", "index.js")); assert.equal(pkg.HIPASS_ENDPOINTS.loginPage, "https://www.hipass.co.kr/comm/lginpg.do"); assert.equal(typeof pkg.buildUsageHistoryQuery, "function"); assert.equal(typeof pkg.parseUsageHistoryList, "function"); assert.equal(typeof pkg.inspectHipassPage, "function"); assert.equal(typeof pkg.buildReceiptRequest, "function"); }); test("toss-securities package README stays aligned with the read-only tossctl wrapper contract", () => { const packageReadme = read(path.join("packages", "toss-securities", "README.md")); assert.match(packageReadme, /read-only tossctl wrapper/i); assert.match(packageReadme, /brew tap JungHoonGhae\/tossinvest-cli/); assert.match(packageReadme, /account summary/); assert.match(packageReadme, /quote get/); assert.match(packageReadme, /order place/); assert.match(packageReadme, /지원하지 않음|not supported/u); }); test("hipass-receipt package README and npm metadata stay aligned with the helper contract", () => { const packageReadme = read(path.join("packages", "hipass-receipt", "README.md")); const packageJson = readJson(path.join("packages", "hipass-receipt", "package.json")); assert.equal(packageJson.name, "hipass-receipt"); assert.match(packageJson.description, /Hi-Pass/); assert.ok(packageJson.files.includes("test/fixtures")); assert.match(packageReadme, /logged-in browser session/i); assert.match(packageReadme, /Playwright/); assert.equal(typeof packageJson.dependencies?.["playwright-core"], "string"); assert.match(packageReadme, /playwright-core/); assert.match(packageReadme, /buildReceiptRequest/); assert.match(packageReadme, /test\/fixtures\/usage-history-list\.html/); }); test("hipass-receipt pack dry-run ships fixture-demo assets for the published README workflow", () => { const packResult = JSON.parse( childProcess.execFileSync("npm", ["pack", "--workspace", "hipass-receipt", "--json", "--dry-run"], { cwd: repoRoot, encoding: "utf8" }), ); const files = packResult[0]?.files?.map((entry) => entry.path) || []; assert.ok(files.includes("test/fixtures/usage-history-list.html")); assert.ok(files.includes("test/fixtures/login-page.html")); assert.ok(files.includes("README.md")); }); test("pack:dry-run includes the toss-securities workspace", () => { const packageJson = JSON.parse(read("package.json")); assert.match(packageJson.scripts["pack:dry-run"], /workspace toss-securities/); assert.match(packageJson.scripts["pack:dry-run"], /workspace hipass-receipt/); assert.match(packageJson.scripts["pack:dry-run"], /workspace used-car-price-search/); }); test("package-lock captures the toss-securities workspace metadata for npm ci", () => { const packageLock = readJson("package-lock.json"); assert.deepEqual(packageLock.packages[""].workspaces, ["packages/*"]); assert.deepEqual(packageLock.packages["node_modules/toss-securities"], { resolved: "packages/toss-securities", link: true, }); assert.equal(packageLock.packages["packages/toss-securities"].license, "MIT"); assert.equal(packageLock.packages["packages/toss-securities"].engines.node, ">=18"); }); test("repository docs advertise the korean-law-search skill with mode-specific korean-law-mcp setup guidance", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const setup = read(path.join("docs", "setup.md")); const security = read(path.join("docs", "security-and-secrets.md")); const sources = read(path.join("docs", "sources.md")); const roadmap = read(path.join("docs", "roadmap.md")); const setupSkill = read(path.join("k-skill-setup", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "korean-law-search.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "korean-law-search.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/korean-law-search.md to exist"); assert.match(readme, /\| 한국 법령 검색 \|/); assert.match(readme, /\[한국 법령 검색 가이드\]\(docs\/features\/korean-law-search\.md\)/); assert.match(readme, /\| 한국 법령 검색 \| .* \| 불필요 \|/); assert.match(install, /--skill korean-law-search/); assert.match(install, /로컬 CLI\/MCP 경로는 `LAW_OC`/); assert.match(install, /remote endpoint는 `LAW_OC` 없이 `url`만/); assert.match(setup, /한국 법령 검색의 로컬 CLI\/MCP 경로용 `LAW_OC`/); assert.match(setup, /remote MCP endpoint는 사용자 `LAW_OC` 없이 `url`만으로 연결/); assert.match(featureDoc, /로컬 CLI 또는 로컬 MCP server 경로는 `LAW_OC`/); assert.match(featureDoc, /remote MCP endpoint는 사용자 `LAW_OC` 없이 `url`만으로 연결/); assert.match(setupSkill, /로컬 한국 법령 검색: `LAW_OC` \+ `korean-law-mcp`/); assert.match(setupSkill, /remote endpoint: 사용자 `LAW_OC` 없이 `url`만 등록/); for (const doc of [setup, security, setupSkill]) { assert.match(doc, /LAW_OC/); assert.match(doc, /korean-law-mcp/); } assert.match(sources, /korean-law-mcp: https:\/\/github\.com\/chrisryugj\/korean-law-mcp/); assert.match(sources, /beopmang: https:\/\/api\.beopmang\.org/); assert.match(roadmap, /한국 법령 검색 스킬 출시/); }); test("korean-law-search skill keeps korean-law-mcp-first guidance while documenting the approved Beopmang fallback", () => { const skillPath = path.join(repoRoot, "korean-law-search", "SKILL.md"); const featureDoc = read(path.join("docs", "features", "korean-law-search.md")); const examplesSecrets = read(path.join("examples", "secrets.env.example")); const packageJson = readJson("package.json"); assert.ok(fs.existsSync(skillPath), "expected korean-law-search/SKILL.md to exist"); const skill = read(path.join("korean-law-search", "SKILL.md")); const doneSectionMatch = skill.match(/## Done when([\s\S]*?)## Notes/); assert.match(skill, /^name: korean-law-search$/m); assert.ok(doneSectionMatch, "expected korean-law-search skill to include a Done when section"); const doneSection = doneSectionMatch[1]; for (const doc of [skill, featureDoc]) { assert.match(doc, /korean-law-mcp.*먼저|먼저.*korean-law-mcp|항상 `korean-law-mcp`를 먼저 사용/u); assert.match(doc, /npm install -g korean-law-mcp/); assert.match(doc, /로컬 CLI 또는 로컬 MCP server 경로는 `LAW_OC`/); assert.match(doc, /remote MCP endpoint는 사용자 `LAW_OC` 없이 `url`만으로 연결/); assert.match(doc, /open\.law\.go\.kr/); assert.match(doc, /search_law/); assert.match(doc, /get_law_text/); assert.match(doc, /search_precedents/); assert.match(doc, /search_interpretations/); assert.match(doc, /search_ordinance/); assert.match(doc, /https:\/\/korean-law-mcp\.fly\.dev\/mcp/); assert.match(doc, /법망|Beopmang/i); assert.match(doc, /https:\/\/api\.beopmang\.org/); assert.match(doc, /fallback/i); assert.match(doc, /MCP/i); assert.match(doc, /CLI/i); assert.doesNotMatch(doc, /packages\/korean-law-search/); assert.doesNotMatch(doc, /python-packages\/korean-law-search/); // Official 판례 (precedent) API evidence (#308). // List lookup and body lookup endpoints from 법제처 Open API. assert.match(doc, /lawSearch\.do\?target=prec/); assert.match(doc, /lawService\.do\?target=prec/); // Detail/body retrieval: upstream get_precedent_text or official lawService ID lookup. assert.match(doc, /get_precedent_text|lawService\.do\?target=prec/); // Supported precedent filters (concept-level, alternation tolerant). assert.match(doc, /사건번호|case number/i); assert.match(doc, /법원|court/i); assert.match(doc, /선고일자|날짜|date/i); assert.match(doc, /출처|source/i); // Precedent-specific failure modes. assert.match(doc, /0건|결과가 없|빈 결과|empty/i); assert.match(doc, /본문[^\n]*(없|불가|실패|제공)|detail/i); // Legal-advice boundary stays explicit. assert.match(doc, /법률 자문/); } assert.match(doneSection, /search_interpretations/); assert.match(doneSection, /search_ordinance/); assert.match(doneSection, /법망|Beopmang/i); assert.match(doneSection, /fallback/i); // Done-when must keep the precedent body/detail retrieval path explicit (#308). assert.match(doneSection, /get_precedent_text|lawService\.do\?target=prec/); assert.doesNotMatch( featureDoc, /[ \t]+$/m, "expected docs/features/korean-law-search.md to avoid trailing whitespace so git diff --check stays clean", ); assert.match(examplesSecrets, /^LAW_OC=replace-me$/m); assert.ok( !packageJson.workspaces.some((workspace) => workspace.includes("korean-law")), "expected no repo workspace to be added for korean-law-search", ); assert.equal(fs.existsSync(path.join(repoRoot, "packages", "korean-law-search")), false); }); test("repository docs advertise the joseon-sillok-search skill and helper", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "joseon-sillok-search.md"); const featureDoc = read(path.join("docs", "features", "joseon-sillok-search.md")); const skillPath = path.join(repoRoot, "joseon-sillok-search", "SKILL.md"); const skill = read(path.join("joseon-sillok-search", "SKILL.md")); const sources = read(path.join("docs", "sources.md")); const roadmap = read(path.join("docs", "roadmap.md")); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/joseon-sillok-search.md to exist"); assert.ok(fs.existsSync(skillPath), "expected joseon-sillok-search/SKILL.md to exist"); assert.match(readme, /\| 조선왕조실록 검색 \|/); assert.match(readme, /\[조선왕조실록 검색 가이드\]\(docs\/features\/joseon-sillok-search\.md\)/); assert.match(install, /--skill joseon-sillok-search/); assert.match(install, /python3 scripts\/sillok_search\.py --query "훈민정음" --king 세종 --year 1443/); assert.match(skill, /sillok\.history\.go\.kr/); assert.match(skill, /--king/); assert.match(skill, /--year/); assert.match(featureDoc, /python3 scripts\/sillok_search\.py --query "훈민정음"/); assert.match(featureDoc, /1443/); assert.match(featureDoc, /kda_12512030_002/); assert.match(sources, /https:\/\/sillok\.history\.go\.kr/); assert.match(sources, /https:\/\/sillok\.history\.go\.kr\/search\/searchResultList\.do/); assert.match(roadmap, /조선왕조실록 검색 스킬 출시/); }); test("joseon-sillok-search install payload includes the documented helper command", () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "joseon-sillok-")); const installedSkillPath = path.join(tempRoot, "joseon-sillok-search"); const bundledHelperPath = path.join(installedSkillPath, "scripts", "sillok_search.py"); try { fs.cpSync(path.join(repoRoot, "joseon-sillok-search"), installedSkillPath, { recursive: true }); assert.ok(fs.existsSync(bundledHelperPath), "expected joseon-sillok-search/scripts/sillok_search.py to exist"); const helpText = childProcess.execFileSync("python3", ["scripts/sillok_search.py", "--help"], { cwd: installedSkillPath, encoding: "utf8", }); assert.match(helpText, /Search Joseon Sillok records from sillok\.history\.go\.kr/); assert.match(helpText, /--query/); assert.match(helpText, /--king/); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); } }); test("repository docs advertise the korean-patent-search skill and official KIPRIS Plus API setup", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const setup = read(path.join("docs", "setup.md")); const security = read(path.join("docs", "security-and-secrets.md")); const setupSkill = read(path.join("k-skill-setup", "SKILL.md")); const examplesSecrets = read(path.join("examples", "secrets.env.example")); const featureDocPath = path.join(repoRoot, "docs", "features", "korean-patent-search.md"); const featureDoc = read(path.join("docs", "features", "korean-patent-search.md")); const skillPath = path.join(repoRoot, "korean-patent-search", "SKILL.md"); const skill = read(path.join("korean-patent-search", "SKILL.md")); const sources = read(path.join("docs", "sources.md")); const roadmap = read(path.join("docs", "roadmap.md")); const packageJson = readJson("package.json"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/korean-patent-search.md to exist"); assert.ok(fs.existsSync(skillPath), "expected korean-patent-search/SKILL.md to exist"); assert.match(readme, /\| 한국 특허 정보 검색 \|/); assert.match(readme, /\[한국 특허 정보 검색 가이드\]\(docs\/features\/korean-patent-search\.md\)/); assert.match(install, /--skill korean-patent-search/); assert.match(install, /KIPRIS_PLUS_API_KEY/); assert.match(install, /python3 scripts\/patent_search\.py --query "배터리"/); assert.match(setup, /한국 특허 정보 검색의 KIPRIS Plus 경로용 `KIPRIS_PLUS_API_KEY`/); assert.match(security, /KIPRIS_PLUS_API_KEY/); assert.match(setupSkill, /한국 특허 정보 검색: `KIPRIS_PLUS_API_KEY`/); assert.match(examplesSecrets, /^KIPRIS_PLUS_API_KEY=replace-me$/m); assert.match(skill, /^name: korean-patent-search$/m); for (const doc of [skill, featureDoc]) { assert.match(doc, /KIPRIS Plus/i); assert.match(doc, /getWordSearch/); assert.match(doc, /getBibliographyDetailInfoSearch/); assert.match(doc, /ServiceKey/); assert.match(doc, /python3 scripts\/patent_search\.py/); assert.match(doc, /Done when/i); assert.doesNotMatch(doc, /packages\/korean-patent-search/); assert.doesNotMatch(doc, /python-packages\/korean-patent-search/); } assert.match(sources, /https:\/\/plus\.kipris\.or\.kr\/portal\/data\/service\/List\.do\?subTab=SC001&entYn=N&menuNo=200100/); assert.match(sources, /https:\/\/www\.data\.go\.kr\/data\/15058788\/openapi\.do/); assert.match(roadmap, /한국 특허 정보 검색 스킬 출시/); assert.ok( !packageJson.workspaces.some((workspace) => workspace.includes("korean-patent-search")), "expected no repo workspace to be added for korean-patent-search", ); assert.equal(fs.existsSync(path.join(repoRoot, "packages", "korean-patent-search")), false); }); test("korean-patent-search install payload includes the documented helper command", () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "korean-patent-search-")); const installedSkillPath = path.join(tempRoot, "korean-patent-search"); const bundledHelperPath = path.join(installedSkillPath, "scripts", "patent_search.py"); try { fs.cpSync(path.join(repoRoot, "korean-patent-search"), installedSkillPath, { recursive: true }); assert.ok(fs.existsSync(bundledHelperPath), "expected korean-patent-search/scripts/patent_search.py to exist"); const helpText = childProcess.execFileSync("python3", ["scripts/patent_search.py", "--help"], { cwd: installedSkillPath, encoding: "utf8", }); assert.match(helpText, /Search Korean patent information via the official KIPRIS Plus Open API/); assert.match(helpText, /--query/); assert.match(helpText, /--application-number/); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); } }); test("repository docs advertise the real-estate-search skill and proxy-based approach", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const setup = read(path.join("docs", "setup.md")); const security = read(path.join("docs", "security-and-secrets.md")); const setupSkill = read(path.join("k-skill-setup", "SKILL.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "real-estate-search.md"); const featureDoc = read(path.join("docs", "features", "real-estate-search.md")); const skillPath = path.join(repoRoot, "real-estate-search", "SKILL.md"); const skill = read(path.join("real-estate-search", "SKILL.md")); const sources = read(path.join("docs", "sources.md")); const roadmap = read(path.join("docs", "roadmap.md")); const packageJson = readJson("package.json"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/real-estate-search.md to exist"); assert.ok(fs.existsSync(skillPath), "expected real-estate-search/SKILL.md to exist"); assert.match(readme, /\| 한국 부동산 실거래가 조회 \|/); assert.match(readme, /\[한국 부동산 실거래가 조회 가이드\]\(docs\/features\/real-estate-search\.md\)/); assert.match(install, /--skill real-estate-search/); for (const doc of [skill, featureDoc]) { assert.match(doc, /https:\/\/github\.com\/tae0y\/real-estate-mcp\/tree\/main/); assert.match(doc, /k-skill-proxy\.nomadamas\.org/); assert.match(doc, /\/v1\/real-estate\//); assert.match(doc, /apartment\/trade|apartment\/rent/); assert.match(doc, /region-code/); assert.doesNotMatch(doc, /packages\/real-estate-search/); assert.doesNotMatch(doc, /python-packages\/real-estate-search/); } for (const doc of [install]) { assert.match(doc, /https:\/\/github\.com\/tae0y\/real-estate-mcp\/tree\/main/); assert.match(doc, /k-skill-proxy\.nomadamas\.org|hosted proxy/); } for (const doc of [setup, security, setupSkill]) { assert.match(doc, /DATA_GO_KR_API_KEY/); } assert.match(sources, /real-estate-mcp: https:\/\/github\.com\/tae0y\/real-estate-mcp\/tree\/main/); assert.match(roadmap, /한국 부동산 실거래가 조회 스킬 출시/); assert.ok( !packageJson.workspaces.some((workspace) => workspace.includes("real-estate-search")), "expected no repo workspace to be added for real-estate-search", ); assert.equal(fs.existsSync(path.join(repoRoot, "packages", "real-estate-search")), false); }); test("repository docs advertise the korean-scholarship-search skill and official-source workflow", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "korean-scholarship-search.md"); const featureDoc = read(path.join("docs", "features", "korean-scholarship-search.md")); const skillPath = path.join(repoRoot, "korean-scholarship-search", "SKILL.md"); const skill = read(path.join("korean-scholarship-search", "SKILL.md")); const sources = read(path.join("docs", "sources.md")); const roadmap = read(path.join("docs", "roadmap.md")); const helperPath = path.join(repoRoot, "korean-scholarship-search", "scripts", "scholarship_filter.py"); const plannerPath = path.join(repoRoot, "korean-scholarship-search", "scripts", "university_search_plan.py"); const searchCluesPath = path.join(repoRoot, "korean-scholarship-search", "references", "search-clues.md"); const reportFormatPath = path.join(repoRoot, "korean-scholarship-search", "references", "report-format.md"); const packageJson = readJson("package.json"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/korean-scholarship-search.md to exist"); assert.ok(fs.existsSync(skillPath), "expected korean-scholarship-search/SKILL.md to exist"); assert.ok(fs.existsSync(helperPath), "expected korean-scholarship-search/scripts/scholarship_filter.py to exist"); assert.ok(fs.existsSync(plannerPath), "expected korean-scholarship-search/scripts/university_search_plan.py to exist"); assert.ok(fs.existsSync(searchCluesPath), "expected korean-scholarship-search/references/search-clues.md to exist"); assert.ok(fs.existsSync(reportFormatPath), "expected korean-scholarship-search/references/report-format.md to exist"); assert.match(readme, /\| 장학금 검색 및 조회 \|/); assert.match(readme, /\[장학금 검색 및 조회 가이드\]\(docs\/features\/korean-scholarship-search\.md\)/); assert.match(install, /--skill korean-scholarship-search/); for (const doc of [skill, featureDoc]) { assert.match(doc, /장학금 검색 및 조회/); assert.match(doc, /kosaf\.go\.kr/); assert.match(doc, /\*\.ac\.kr/); assert.match(doc, /전국 대학교|전국 대학/); assert.match(doc, /공식 공고 우선/); assert.match(doc, /학자금 지원구간/); assert.match(doc, /scholarship_filter\.py/); assert.match(doc, /university_search_plan\.py/); assert.match(doc, /학과/); assert.match(doc, /외부 장학 추천|등록금 감면|생활비 지원/); } assert.match(sources, /한국장학재단 학자금 지원구간 산정절차/); assert.match(sources, /한국장학재단 푸른등대 기부장학금/); assert.match(sources, /삼성꿈장학재단/); assert.match(roadmap, /장학금 검색 및 조회 스킬 출시/); assert.ok( !packageJson.workspaces.some((workspace) => workspace.includes("korean-scholarship-search")), "expected no repo workspace to be added for korean-scholarship-search", ); assert.equal(fs.existsSync(path.join(repoRoot, "packages", "korean-scholarship-search")), false); }); test("korean-scholarship-search helper filters normalized records, renders reports, and returns eligibility verdicts", () => { const helperPath = path.join(repoRoot, "korean-scholarship-search", "scripts", "scholarship_filter.py"); const plannerPath = path.join(repoRoot, "korean-scholarship-search", "scripts", "university_search_plan.py"); const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "k-skill-scholarship-")); try { const inputPath = path.join(tempRoot, "scholarships.json"); fs.writeFileSync( inputPath, JSON.stringify( [ { name: "테스트재단 생활비 장학금", organization: { name: "테스트재단", type: "foundation" }, source_url: "https://foundation.example.com/notice/1", apply_url: "https://foundation.example.com/apply/1", amount: { text: "학기당 250만 원", per_semester_krw: 2500000, category: "living" }, eligibility: { student_levels: ["undergraduate"], school_kinds: ["university"], school_names: ["서울대학교", "연세대학교"], department_names: ["컴퓨터공학부"], grade_years: [2, 3, 4], gpa_min: 3.2, income_band_min: 0, income_band_max: 6, }, deadline: { start: "2026-04-01", end: "2026-04-16" }, }, { name: "교내 성적우수 장학금", organization: { name: "샘플대학교", type: "school" }, source_url: "https://sample.ac.kr/notice/2", apply_url: "https://sample.ac.kr/apply/2", amount: { text: "등록금 전액", category: "tuition" }, eligibility: { student_levels: ["undergraduate"], school_kinds: ["university"], school_names: ["샘플대학교"], grade_years: [1], gpa_min: 4.0, income_band_min: 0, income_band_max: 10, }, deadline: { start: "2026-05-01", end: "2026-05-20" }, }, ], null, 2, ), "utf8", ); const helpText = childProcess.execFileSync("python3", [helperPath, "--help"], { cwd: repoRoot, encoding: "utf8", }); assert.match(helpText, /Filter normalized Korean scholarship records/); assert.match(helpText, /\bfilter\b/); assert.match(helpText, /\beligibility\b/); assert.match(helpText, /\breport\b/); const plannerHelpText = childProcess.execFileSync("python3", [plannerPath, "--help"], { cwd: repoRoot, encoding: "utf8", }); assert.match(plannerHelpText, /nationwide/i); assert.match(plannerHelpText, /school-name/); const filtered = JSON.parse( childProcess.execFileSync( "python3", [ helperPath, "filter", "--input", inputPath, "--org-type", "foundation", "--student-level", "undergraduate", "--department-name", "컴퓨터공학부", "--income-band", "4", "--min-amount", "2000000", "--today", "2026-04-14", "--deadline-within-days", "7", ], { cwd: repoRoot, encoding: "utf8" }, ), ); assert.equal(filtered.total, 1); assert.equal(filtered.items[0].name, "테스트재단 생활비 장학금"); assert.equal(filtered.items[0]._match.amount_krw, 2500000); assert.equal(filtered.items[0]._match.deadline.status, "open"); assert.equal(filtered.items[0]._match.deadline.days_until_end, 2); const report = childProcess.execFileSync( "python3", [ helperPath, "report", "--input", inputPath, "--today", "2026-04-14", "--only-open-now", ], { cwd: repoRoot, encoding: "utf8" }, ); assert.match(report, /# 장학금 검색 및 조회 리포트/); assert.match(report, /## 지금 지원 가능/); assert.match(report, /테스트재단 생활비 장학금/); assert.match(report, /D-2/); const plannerPayload = JSON.parse( childProcess.execFileSync( "python3", [ plannerPath, "--school-name", "부산대학교", "--department", "컴퓨터공학과", "--year", "2026", ], { cwd: repoRoot, encoding: "utf8" }, ), ); assert.equal(plannerPayload.scope, "school"); assert.equal(plannerPayload.school_name, "부산대학교"); assert.match(plannerPayload.search_queries.join("\n"), /컴퓨터공학과/); const nationwidePayload = JSON.parse( childProcess.execFileSync( "python3", [plannerPath, "--nationwide", "--year", "2026"], { cwd: repoRoot, encoding: "utf8" }, ), ); assert.equal(nationwidePayload.scope, "nationwide-universities"); assert.match(nationwidePayload.search_queries.join("\n"), /site:\*\.ac\.kr 2026 장학 공고/); const eligibility = JSON.parse( childProcess.execFileSync( "python3", [ helperPath, "eligibility", "--input", inputPath, "--school-name", "서울대학교", "--student-level", "undergraduate", "--grade-year", "2", "--gpa", "3.5", "--income-band", "4", ], { cwd: repoRoot, encoding: "utf8" }, ), ); assert.equal(eligibility.total, 2); assert.equal(eligibility.results[0].status, "eligible"); assert.equal(eligibility.results[1].status, "not_eligible"); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); } }); test("real-estate-search skill uses proxy endpoints not MCP self-host", () => { const featureDoc = read(path.join("docs", "features", "real-estate-search.md")); const skill = read(path.join("real-estate-search", "SKILL.md")); for (const doc of [skill, featureDoc]) { assert.match(doc, /k-skill-proxy\.nomadamas\.org\/v1\/real-estate/); assert.match(doc, /curl/); assert.doesNotMatch(doc, /uv run/); assert.doesNotMatch(doc, /codex mcp add/); assert.doesNotMatch(doc, /Cloudflare Tunnel/i); assert.doesNotMatch(doc, /launchd/i); assert.doesNotMatch(doc, /docker compose/i); } }); test("repository docs advertise the korean-stock-search skill and proxy-backed KRX approach", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const setup = read(path.join("docs", "setup.md")); const security = read(path.join("docs", "security-and-secrets.md")); const setupSkill = read(path.join("k-skill-setup", "SKILL.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "korean-stock-search.md"); const featureDoc = read(path.join("docs", "features", "korean-stock-search.md")); const skillPath = path.join(repoRoot, "korean-stock-search", "SKILL.md"); const skill = read(path.join("korean-stock-search", "SKILL.md")); const sources = read(path.join("docs", "sources.md")); const roadmap = read(path.join("docs", "roadmap.md")); const proxyReadme = read(path.join("packages", "k-skill-proxy", "README.md")); const proxyDoc = read(path.join("docs", "features", "k-skill-proxy.md")); const packageJson = readJson("package.json"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/korean-stock-search.md to exist"); assert.ok(fs.existsSync(skillPath), "expected korean-stock-search/SKILL.md to exist"); assert.match(readme, /\| 한국 주식 정보 조회 \|/); assert.match(readme, /\[한국 주식 정보 조회 가이드\]\(docs\/features\/korean-stock-search\.md\)/); assert.match(install, /--skill korean-stock-search/); for (const doc of [skill, featureDoc]) { assert.match(doc, /https:\/\/github\.com\/jjlabsio\/korea-stock-mcp/); assert.match(doc, /k-skill-proxy\.nomadamas\.org/); assert.match(doc, /\/v1\/korean-stock\/search/); assert.match(doc, /\/v1\/korean-stock\/base-info/); assert.match(doc, /\/v1\/korean-stock\/trade-info/); assert.match(doc, /KRX_API_KEY/); assert.match(doc, /사용자.*KRX_API_KEY.*(불필요|준비할 필요가 없)/u); assert.doesNotMatch(doc, /packages\/korean-stock-search/); assert.doesNotMatch(doc, /python-packages\/korean-stock-search/); } for (const doc of [setup, security, setupSkill]) { assert.match(doc, /KRX_API_KEY/); } for (const doc of [proxyReadme, proxyDoc]) { assert.match(doc, /\/v1\/korean-stock\/search/); assert.match(doc, /\/v1\/korean-stock\/base-info/); assert.match(doc, /\/v1\/korean-stock\/trade-info/); } assert.match(sources, /korea-stock-mcp: https:\/\/github\.com\/jjlabsio\/korea-stock-mcp/); assert.match(roadmap, /한국 주식 정보 조회 스킬 출시/); assert.ok( !packageJson.workspaces.some((workspace) => workspace.includes("korean-stock-search")), "expected no repo workspace to be added for korean-stock-search", ); assert.equal(fs.existsSync(path.join(repoRoot, "packages", "korean-stock-search")), false); }); test("korean-stock-search skill stays proxy-first and does not require local MCP install", () => { const featureDoc = read(path.join("docs", "features", "korean-stock-search.md")); const skill = read(path.join("korean-stock-search", "SKILL.md")); for (const doc of [skill, featureDoc]) { assert.match(doc, /k-skill-proxy\.nomadamas\.org\/v1\/korean-stock/); assert.match(doc, /curl/); assert.match(doc, /proxy.*서버.*KRX_API_KEY|KRX_API_KEY.*proxy.*서버/u); assert.doesNotMatch(doc, /npx\s+(?:-y|--yes)\s+korea-stock-mcp/); assert.doesNotMatch(doc, /codex mcp add/); assert.doesNotMatch(doc, /claude_desktop_config\.json/); assert.doesNotMatch(doc, /DART_API_KEY/); } }); test("repository docs advertise the shipped korean-spell-check helper assets", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "korean-spell-check.md"); const helperPath = path.join(repoRoot, "scripts", "korean_spell_check.py"); assert.equal(fs.existsSync(featureDocPath), true); assert.equal(fs.existsSync(helperPath), true); assert.match(readme, /\[한국어 맞춤법 검사 가이드\]\(docs\/features\/korean-spell-check\.md\)/); assert.match(install, /python3 scripts\/korean_spell_check\.py/); }); test("repository docs advertise the korean-character-count skill and deterministic counting contract", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "korean-character-count.md"); const featureDoc = read(path.join("docs", "features", "korean-character-count.md")); const skillPath = path.join(repoRoot, "korean-character-count", "SKILL.md"); const skill = read(path.join("korean-character-count", "SKILL.md")); const sources = read(path.join("docs", "sources.md")); const roadmap = read(path.join("docs", "roadmap.md")); const packageJson = readJson("package.json"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/korean-character-count.md to exist"); assert.ok(fs.existsSync(skillPath), "expected korean-character-count/SKILL.md to exist"); assert.match(readme, /\| 한국어 글자 수 세기 \|/); assert.match(readme, /\[한국어 글자 수 세기 가이드\]\(docs\/features\/korean-character-count\.md\)/); assert.match(install, /--skill korean-character-count/); assert.match( install, /--skill k-schoollunch-menu \\\n --skill korean-character-count/, "docs/install.md selective-install block should keep k-schoollunch-menu and korean-character-count in the same continued shell command", ); assert.match(install, /node scripts\/korean_character_count\.js --text "가나다"/); for (const doc of [skill, featureDoc]) { assert.match(doc, /grapheme|extended grapheme/i); assert.match(doc, /UTF-8/); assert.match(doc, /NEIS/i); assert.match(doc, /CRLF|U\+2028|U\+2029/); assert.match(doc, /node scripts\/korean_character_count\.js/); assert.doesNotMatch(doc, /packages\/korean-character-count/); assert.doesNotMatch(doc, /python-packages\/korean-character-count/); } assert.match(sources, /https:\/\/www\.unicode\.org\/reports\/tr29\//); assert.match(sources, /https:\/\/encoding\.spec\.whatwg\.org\//); assert.match(sources, /https:\/\/nodejs\.org\/api\/buffer\.html/); assert.match(roadmap, /한국어 글자 수 세기 스킬 출시/); assert.ok( !packageJson.workspaces.some((workspace) => workspace.includes("korean-character-count")), "expected no repo workspace to be added for korean-character-count", ); assert.equal(fs.existsSync(path.join(repoRoot, "packages", "korean-character-count")), false); }); test("korean-character-count feature doc NEIS example matches live helper output", () => { const featureDoc = read(path.join("docs", "features", "korean-character-count.md")); const helperOutput = childProcess.execFileSync( "node", [ "scripts/korean_character_count.js", "--text", "첫 줄\n둘째 줄🙂", "--profile", "neis", "--format", "text", ], { cwd: repoRoot, encoding: "utf8" }, ); const bytesMatch = helperOutput.match(/^bytes:\s+(\d+)$/m); assert.ok(bytesMatch, `expected helper text output to include a bytes line, got: ${helperOutput}`); assert.equal(bytesMatch[1], "23"); assert.match(featureDoc, new RegExp(String.raw`bytes:\s+${bytesMatch[1]}`)); assert.match(featureDoc, /bytes=23/); }); test("korean-character-count install payload includes the documented helper command", () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "korean-character-count-")); const installedSkillPath = path.join(tempRoot, "korean-character-count"); const bundledHelperPath = path.join(installedSkillPath, "scripts", "korean_character_count.js"); try { fs.cpSync(path.join(repoRoot, "korean-character-count"), installedSkillPath, { recursive: true }); assert.ok( fs.existsSync(bundledHelperPath), "expected korean-character-count/scripts/korean_character_count.js to exist", ); const helpText = childProcess.execFileSync("node", ["scripts/korean_character_count.js", "--help"], { cwd: installedSkillPath, encoding: "utf8", }); assert.match(helpText, /--profile/); assert.match(helpText, /default/); assert.match(helpText, /neis/i); assert.match(helpText, /--stdin/); } finally { fs.rmSync(tempRoot, { recursive: true, force: true }); } }); test("repository docs advertise the cheap-gas-nearby skill and Opinet key requirements", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const setup = read(path.join("docs", "setup.md")); const security = read(path.join("docs", "security-and-secrets.md")); const sources = read(path.join("docs", "sources.md")); const roadmap = read(path.join("docs", "roadmap.md")); const setupSkill = read(path.join("k-skill-setup", "SKILL.md")); const examplesSecrets = read(path.join("examples", "secrets.env.example")); const featureDocPath = path.join(repoRoot, "docs", "features", "cheap-gas-nearby.md"); const skillPath = path.join(repoRoot, "cheap-gas-nearby", "SKILL.md"); assert.equal(fs.existsSync(featureDocPath), true); assert.equal(fs.existsSync(skillPath), true); assert.match(readme, /\| 근처 가장 싼 주유소 찾기 \|/); assert.match(readme, /\[근처 가장 싼 주유소 찾기 가이드\]\(docs\/features\/cheap-gas-nearby\.md\)/); assert.match(install, /--skill cheap-gas-nearby/); for (const doc of [setup, security, setupSkill]) { assert.match(doc, /주유소 가격|OPINET_API_KEY/); assert.match(doc, /hosted proxy|proxy.*경유/); } assert.doesNotMatch(examplesSecrets, /^OPINET_API_KEY=replace-me$/m); assert.match(sources, /https:\/\/www\.opinet\.co\.kr\/user\/custapi\/openApiInfo\.do/); assert.match(sources, /https:\/\/www\.opinet\.co\.kr\/api\/aroundAll\.do/); assert.match(sources, /https:\/\/www\.opinet\.co\.kr\/api\/detailById\.do/); assert.match(roadmap, /근처 가장 싼 주유소 찾기 스킬 출시/); }); test("cheap-gas-nearby skill docs require location-first prompts and official Opinet surfaces", () => { const skill = read(path.join("cheap-gas-nearby", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "cheap-gas-nearby.md")); for (const doc of [skill, featureDoc]) { assert.match(doc, /현재 위치를 알려주세요/); assert.match(doc, /OPINET_API_KEY/); assert.match(doc, /aroundAll\.do/); assert.match(doc, /detailById\.do/); assert.match(doc, /areaCode\.do/); assert.match(doc, /휘발유|경유/); assert.match(doc, /KATEC/); assert.match(doc, /카카오맵|Kakao Map/); } }); test("repository docs advertise the han-river-water-level skill and rollout-pending proxy workflow", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const setup = read(path.join("docs", "setup.md")); const security = read(path.join("docs", "security-and-secrets.md")); const proxyDoc = read(path.join("docs", "features", "k-skill-proxy.md")); const proxyReadme = read(path.join("packages", "k-skill-proxy", "README.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "han-river-water-level.md"); const featureDoc = read(path.join("docs", "features", "han-river-water-level.md")); const skillPath = path.join(repoRoot, "han-river-water-level", "SKILL.md"); const skill = read(path.join("han-river-water-level", "SKILL.md")); const sources = read(path.join("docs", "sources.md")); const roadmap = read(path.join("docs", "roadmap.md")); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/han-river-water-level.md to exist"); assert.ok(fs.existsSync(skillPath), "expected han-river-water-level/SKILL.md to exist"); assert.match(readme, /\| 한강 수위 정보 조회 \|/); assert.match(readme, /\[한강 수위 정보 가이드\]\(docs\/features\/han-river-water-level\.md\)/); assert.match(install, /--skill han-river-water-level/); for (const doc of [skill, featureDoc]) { assert.match(doc, /\/v1\/han-river\/water-level/); assert.match(doc, /stationName|station_code|stationCode/); assert.match(doc, /수위|유량/); assert.match(doc, /candidate_stations|ambiguous_station/); assert.match(doc, /KSKILL_PROXY_BASE_URL/); } assert.match(featureDoc, /HRFCO_OPEN_API_KEY/); assert.match(skill, /기본적으로 `https:\/\/k-skill-proxy\.nomadamas\.org\/v1\/han-river\/water-level`/); assert.doesNotMatch(featureDoc, /기본 hosted 조회:/); for (const doc of [proxyDoc, proxyReadme]) { assert.match(doc, /\/v1\/han-river\/water-level/); assert.match(doc, /HRFCO_OPEN_API_KEY/); assert.match(doc, /waterlevel\/info\.json/); assert.match(doc, /waterlevel\/list\/10M/); } assert.match(setup, /한강 수위 정보 조회 \| 사용자 시크릿 불필요/); assert.match(setup, /한강 수위.*기본 hosted p/i); assert.match(security, /KSKILL_PROXY_BASE_URL.*서울 지하철.*한국 날씨.*기본 hosted proxy|서울 지하철.*한국 날씨.*KSKILL_PROXY_BASE_URL.*기본 hosted proxy/); assert.match(sources, /hrfco\.go\.kr\/web\/openapiPage\/reference\.do/); assert.match(sources, /api\.hrfco\.go\.kr/); assert.match(roadmap, /한강 수위 정보 조회 스킬 출시/); }); test("repository docs advertise the MFDS drug and food safety skills", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const sources = read(path.join("docs", "sources.md")); const drugFeatureDocPath = path.join(repoRoot, "docs", "features", "mfds-drug-safety.md"); const foodFeatureDocPath = path.join(repoRoot, "docs", "features", "mfds-food-safety.md"); const drugSkillPath = path.join(repoRoot, "mfds-drug-safety", "SKILL.md"); const foodSkillPath = path.join(repoRoot, "mfds-food-safety", "SKILL.md"); assert.equal(fs.existsSync(drugFeatureDocPath), true); assert.equal(fs.existsSync(foodFeatureDocPath), true); assert.equal(fs.existsSync(drugSkillPath), true); assert.equal(fs.existsSync(foodSkillPath), true); assert.match(readme, /\| 의약품 안전 체크 \|/); assert.match(readme, /\| 식품 안전 체크 \|/); assert.match(readme, /\[의약품 안전 체크 가이드\]\(docs\/features\/mfds-drug-safety\.md\)/); assert.match(readme, /\[식품 안전 체크 가이드\]\(docs\/features\/mfds-food-safety\.md\)/); assert.match(install, /--skill mfds-drug-safety/); assert.match(install, /--skill mfds-food-safety/); assert.match(sources, /15075057\/openapi\.do/); assert.match(sources, /15097208\/openapi\.do/); assert.match(sources, /15056516\/openapi\.do/); assert.match(sources, /foodsafetykorea\.go\.kr\/api\/openApiInfo\.do/); }); test("MFDS public-health skill docs require interview-first safety flow and official endpoints", () => { const drugSkill = read(path.join("mfds-drug-safety", "SKILL.md")); const foodSkill = read(path.join("mfds-food-safety", "SKILL.md")); const drugFeatureDoc = read(path.join("docs", "features", "mfds-drug-safety.md")); const foodFeatureDoc = read(path.join("docs", "features", "mfds-food-safety.md")); const sources = read(path.join("docs", "sources.md")); const proxyReadme = read(path.join("packages", "k-skill-proxy", "README.md")); const proxyDoc = read(path.join("docs", "features", "k-skill-proxy.md")); for (const doc of [drugSkill, drugFeatureDoc]) { assert.match(doc, /증상.*바로 단정하지 말고.*먼저 되묻/); assert.match(doc, /호흡곤란|의식저하|심한 발진/); assert.match(doc, /DrbEasyDrugInfoService\/getDrbEasyDrugList/); assert.match(doc, /SafeStadDrugService\/getSafeStadDrugInq/); assert.match(doc, /KSKILL_PROXY_BASE_URL|k-skill-proxy\.nomadamas\.org/); assert.match(doc, /사용자.*시크릿 없음|사용자 API key 없이/u); assert.match(doc, /DATA_GO_KR_API_KEY.*프록시 운영 서버/u); assert.match(doc, /\/v1\/mfds\/drug-safety\/lookup/); assert.match(doc, /python3 scripts\/mfds_drug_safety\.py/); } for (const doc of [foodSkill, foodFeatureDoc]) { assert.match(doc, /증상.*바로 단정하지 말고.*먼저 되묻/); assert.match(doc, /혈변|탈수|호흡곤란/); assert.match(doc, /PrsecImproptFoodInfoService03\/getPrsecImproptFoodList01/); assert.match(doc, /I0490/); assert.match(doc, /KSKILL_PROXY_BASE_URL|k-skill-proxy\.nomadamas\.org/); assert.match(doc, /사용자.*시크릿 없음|사용자 API key 없이/u); assert.match(doc, /DATA_GO_KR_API_KEY.*프록시 운영 서버/u); assert.match(doc, /FOODSAFETYKOREA_API_KEY/); assert.match(doc, /\/v1\/mfds\/food-safety\/search/); assert.match(doc, /python3 scripts\/mfds_food_safety\.py/); assert.match(doc, /https:\/\/openapi\.foodsafetykorea\.go\.kr\/api\/sample\/I0490\/json\/1\/5/); assert.doesNotMatch(doc, /http:\/\/openapi\.foodsafetykorea\.go\.kr/); } assert.match(sources, /https:\/\/openapi\.foodsafetykorea\.go\.kr\/api\/sample\/I0490\/json\/1\/5/); assert.doesNotMatch(sources, /http:\/\/openapi\.foodsafetykorea\.go\.kr/); for (const doc of [proxyReadme, proxyDoc]) { assert.match(doc, /\/v1\/mfds\/drug-safety\/lookup/); assert.match(doc, /\/v1\/mfds\/food-safety\/search/); assert.match(doc, /FOODSAFETYKOREA_API_KEY/); } }); test("docs/setup.md and k-skill-setup document hosted household waste proxy flow", () => { const setup = read(path.join("docs", "setup.md")); const setupSkill = read(path.join("k-skill-setup", "SKILL.md")); assert.match( setup, /한국 주식 정보 조회, 생활쓰레기 배출정보 조회, 학교 급식 식단 조회, 의약품 안전 체크, 식품 안전 체크는 기본 hosted proxy를 쓰므로/, "setup.md intro should list household waste, school lunch, and MFDS skills among hosted-proxy features with no user-side key", ); assert.match(setup, /DATA_GO_KR_API_KEY.*서버에 설정/); assert.match( setup, /미세먼지, 한강 수위, 주유소 가격, 생활쓰레기 배출정보 조회, 학교 급식 식단 조회, 의약품 안전 체크, 식품 안전 체크는 `KSKILL_PROXY_BASE_URL` 을 비워 두면 기본 hosted path\(`k-skill-proxy\.nomadamas\.org`\)/, "setup.md should list fine dust, Han River, gas, household waste, school lunch, and MFDS skills when KSKILL_PROXY_BASE_URL is unset", ); assert.match( setupSkill, /미세먼지, 한강 수위, 주유소 가격, 생활쓰레기 배출정보 조회, 학교 급식 식단 조회, 의약품 안전 체크, 식품 안전 체크는 `KSKILL_PROXY_BASE_URL`/, "k-skill-setup SKILL should mirror setup.md hosted-proxy unset-base-URL guidance including MFDS skills", ); assert.match(setup, /\| 생활쓰레기 배출정보 조회 \|/); assert.match(setup, /DATA_GO_KR_API_KEY/); assert.match(setup, /pageNo=1.*numOfRows=100|numOfRows=100.*pageNo=1/); assert.match(setup, /\[생활쓰레기 배출정보 조회 가이드\]\(features\/household-waste-info\.md\)/); assert.match(setupSkill, /\/v1\/household-waste\/info/); assert.match(setupSkill, /DATA_GO_KR_API_KEY/); assert.match(setupSkill, /생활쓰레기 배출정보 조회: 사용자 시크릿 불필요/); }); test("docs/setup.md and k-skill-setup document hosted school lunch proxy flow", () => { const setup = read(path.join("docs", "setup.md")); const setupSkill = read(path.join("k-skill-setup", "SKILL.md")); const examplesSecrets = read(path.join("examples", "secrets.env.example")); assert.match(setup, /학교 급식 식단 조회, 의약품 안전 체크, 식품 안전 체크는 기본 hosted proxy/); assert.match(setup, /KEDU_INFO_KEY.*서버에 설정/); assert.match( setup, /미세먼지, 한강 수위, 주유소 가격, 생활쓰레기 배출정보 조회, 학교 급식 식단 조회, 의약품 안전 체크, 식품 안전 체크는 `KSKILL_PROXY_BASE_URL` 을 비워 두면 기본 hosted path\(`k-skill-proxy\.nomadamas\.org`\)/, "setup.md should list fine dust, Han River, gas, household waste, school lunch, and MFDS skills when KSKILL_PROXY_BASE_URL is unset", ); assert.match( setupSkill, /미세먼지, 한강 수위, 주유소 가격, 생활쓰레기 배출정보 조회, 학교 급식 식단 조회, 의약품 안전 체크, 식품 안전 체크는 `KSKILL_PROXY_BASE_URL`/, "k-skill-setup SKILL should mirror setup.md hosted-proxy unset-base-URL guidance including MFDS skills", ); assert.match(setup, /\| 학교 급식 식단 조회 \|/); assert.match(setup, /KEDU_INFO_KEY/); assert.match(setup, /\[학교 급식 식단 조회 가이드\]\(features\/k-schoollunch-menu\.md\)/); assert.match(setupSkill, /\/v1\/neis\/school-search/); assert.match(setupSkill, /\/v1\/neis\/school-meal/); assert.match(setupSkill, /KEDU_INFO_KEY/); assert.match(setupSkill, /학교 급식 식단 조회: 사용자 시크릿 불필요/); assert.doesNotMatch( examplesSecrets, /^KEDU_INFO_KEY=/m, "client secrets example must not encourage KEDU_INFO_KEY (proxy server only)", ); }); test("repository docs advertise the hola-poke-yeoksam skill", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const sources = read(path.join("docs", "sources.md")); const roadmap = read(path.join("docs", "roadmap.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "hola-poke-yeoksam.md"); const skillPath = path.join(repoRoot, "hola-poke-yeoksam", "SKILL.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/hola-poke-yeoksam.md to exist"); assert.ok(fs.existsSync(skillPath), "expected hola-poke-yeoksam/SKILL.md to exist"); const featureDoc = read(path.join("docs", "features", "hola-poke-yeoksam.md")); const skill = read(path.join("hola-poke-yeoksam", "SKILL.md")); assert.match(readme, /\| 올라포케 역삼 포케 \|/); assert.match(readme, /\[올라포케 역삼 포케 가이드\]\(docs\/features\/hola-poke-yeoksam\.md\)/); assert.match(install, /--skill hola-poke-yeoksam/); assert.match(sources, /mnspkm\/hola-poke-yeoksam-skill/); assert.match(roadmap, /올라포케 역삼 포케 스킬 출시/); }); test("hola-poke-yeoksam docs pin the verified remote MCP contract snapshot and phone-only event flow", () => { const fixture = readJson(path.join("scripts", "fixtures", "hola-poke-yeoksam-contract-smoke.json")); const skill = read(path.join("hola-poke-yeoksam", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "hola-poke-yeoksam.md")); const snapshotLabels = [ ["initialize 결과", "initialize", "initialize snapshot"], ["tools/list 결과", "tools_list", "tools/list snapshot"], ["get_menu 구조 예시", "get_menu", "get_menu snapshot"], ["get_shop_info 구조 예시", "get_shop_info", "get_shop_info snapshot"], ["enter_event(phone='010-12') 예시", "enter_event_invalid_phone", "invalid-phone snapshot"], ["enter_event 성공 응답 필수 필드", "enter_event_success_contract", "success-contract snapshot"], ]; assert.match(skill, /^name: hola-poke-yeoksam$/m); for (const doc of [skill, featureDoc]) { assert.match(doc, /올라포케 역삼점/); assert.match(doc, /get_menu/); assert.match(doc, /get_shop_info/); assert.match(doc, /enter_event/); assert.match(doc, /이름(?:·|\/)?이메일.*받지 않/); assert.match(doc, /already_entered_today/); assert.match(doc, /message.*글자 그대로/); assert.match(doc, /주문\/결제\/배달앱 자동화는 하지 않/); assert.match(doc, /성공 경로는.*(?:fixture|스냅샷|recorded)/i); assert.match(doc, /라이브 스모크.*invalid-phone|invalid-phone.*라이브 스모크/i); assert.match(doc, /01012345678|010-1234-5678/); assert.match(doc, /hola-poke-yeoksam-skill\.onrender\.com\/mcp/); for (const [label, key, message] of snapshotLabels) { assert.equal( findJsonFenceTextAfterLabel(doc, label), JSON.stringify(fixture[key], null, 2), `${message} must stay byte-aligned with the checked-in fixture`, ); } } assert.deepEqual( fixture.tools_list.tools.map((tool) => tool.name), ["get_menu", "get_shop_info", "enter_event"], "tools/list fixture must pin the expected remote tool roster", ); assert.equal(fixture.get_shop_info.group_order_url, ""); assert.match(fixture.get_shop_info.group_order_note, /단체주문|네이버페이/); assert.deepEqual(fixture.enter_event_success_contract.required_fields, ["message", "code", "next_action"]); assert.equal(fixture.enter_event_invalid_phone.error, "phone_format"); assert.match(fixture.enter_event_invalid_phone.message, /01012345678|010-1234-5678/); }); test("repository docs advertise the library-book-search skill", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const sources = read(path.join("docs", "sources.md")); const proxyDoc = read(path.join("docs", "features", "k-skill-proxy.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "library-book-search.md"); const skillPath = path.join(repoRoot, "library-book-search", "SKILL.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/library-book-search.md to exist"); assert.ok(fs.existsSync(skillPath), "expected library-book-search/SKILL.md to exist"); const featureDoc = read(path.join("docs", "features", "library-book-search.md")); const skill = read(path.join("library-book-search", "SKILL.md")); assert.match(skill, /^name: library-book-search$/m); assert.match(readme, /\| 도서관 도서 조회 \|/); assert.match(readme, /\[도서관 도서 조회 가이드\]\(docs\/features\/library-book-search\.md\)/); assert.match(install, /--skill library-book-search/); assert.match(install, /DATA4LIBRARY_AUTH_KEY/); assert.match(sources, /data4library\.kr\/apiUtilization/); assert.match(proxyDoc, /\/v1\/data4library\/book-search/); assert.match(proxyDoc, /DATA4LIBRARY_AUTH_KEY/); for (const doc of [skill, featureDoc]) { assert.match(doc, /도서관 정보나루/); assert.match(doc, /\/v1\/data4library\/book-search/); assert.match(doc, /\/v1\/data4library\/book-detail/); assert.match(doc, /\/v1\/data4library\/book-exists/); assert.match(doc, /\/v1\/data4library\/libraries-by-book/); assert.match(doc, /DATA4LIBRARY_AUTH_KEY.*프록시 서버/s); assert.match(doc, /사용자.*시크릿.*없/); } }); test("repository docs advertise the korean-privacy-terms thin-wrapper skill", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const roadmap = read(path.join("docs", "roadmap.md")); const sources = read(path.join("docs", "sources.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "korean-privacy-terms.md"); const skillPath = path.join(repoRoot, "korean-privacy-terms", "SKILL.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/korean-privacy-terms.md to exist"); assert.ok(fs.existsSync(skillPath), "expected korean-privacy-terms/SKILL.md to exist"); assert.match(readme, /\| 한국 개인정보처리방침·이용약관 자동 생성 \|/); assert.match( readme, /\[한국 개인정보처리방침·이용약관 자동 생성 가이드\]\(docs\/features\/korean-privacy-terms\.md\)/, ); assert.match(install, /--skill korean-privacy-terms/); assert.match(roadmap, /한국 개인정보처리방침.이용약관 스킬 출시/); assert.match(sources, /https:\/\/github\.com\/kimlawtech\/korean-privacy-terms/); assert.match(sources, /Apache-2\.0/); }); test("korean-privacy-terms skill is a thin wrapper that cites upstream and enforces a legal disclaimer", () => { const skillPath = path.join(repoRoot, "korean-privacy-terms", "SKILL.md"); assert.ok(fs.existsSync(skillPath), "expected korean-privacy-terms/SKILL.md to exist"); const skill = read(path.join("korean-privacy-terms", "SKILL.md")); assert.match(skill, /^name: korean-privacy-terms$/m); assert.match(skill, /^license: Apache-2\.0$/m); assert.match(skill, /^description: .*개인정보처리방침.*이용약관.*$/m); assert.match( skill, /\[?kimlawtech\/korean-privacy-terms\]?.*https:\/\/github\.com\/kimlawtech\/korean-privacy-terms/, ); assert.match(skill, /Apache-2\.0/); assert.match(skill, /참고용 초안/); assert.match(skill, /법률 자문/); assert.match(skill, /변호사 검토/); assert.match(skill, /2026\.9\.11/); assert.match(skill, /되묻/); assert.match(skill, /개인정보처리방침/); assert.match(skill, /이용약관/); assert.match(skill, /쿠키 배너/); assert.match(skill, /~\/\.claude\/skills\/korean-privacy-terms/); assert.match(skill, /~\/\.agents\/skills\/korean-privacy-terms/); assert.match(skill, /scripts\/install\.sh/); assert.match(skill, /scripts\/upstream\.pin/); assert.match(skill, /DISCLAIMER\.md/); assert.match(skill, /## Notes/); assert.doesNotMatch(skill, /AskUserQuestion/); }); test("korean-privacy-terms preserves upstream NOTICE and DISCLAIMER for Apache-2.0 compliance", () => { const noticePath = path.join(repoRoot, "korean-privacy-terms", "NOTICE"); const disclaimerPath = path.join(repoRoot, "korean-privacy-terms", "DISCLAIMER.md"); assert.ok(fs.existsSync(noticePath), "expected korean-privacy-terms/NOTICE to exist"); assert.ok(fs.existsSync(disclaimerPath), "expected korean-privacy-terms/DISCLAIMER.md to exist"); const notice = fs.readFileSync(noticePath, "utf8"); const disclaimer = fs.readFileSync(disclaimerPath, "utf8"); assert.match(notice, /korean-privacy-terms/); assert.match(notice, /Copyright 2026 kimlawtech/); assert.match(notice, /Apache License, Version 2\.0/); assert.match(notice, /kimlawtech/i); assert.match(disclaimer, /한국어/); assert.match(disclaimer, /English/); assert.match(disclaimer, /참고용 초안/); assert.match(disclaimer, /reference drafts/i); assert.match(disclaimer, /legal advice/i); assert.match(disclaimer, /개인정보보호법/); }); test("korean-privacy-terms ships an install.sh wrapper and a pinned upstream SHA", () => { const pinPath = path.join(repoRoot, "korean-privacy-terms", "scripts", "upstream.pin"); const installPath = path.join(repoRoot, "korean-privacy-terms", "scripts", "install.sh"); assert.ok(fs.existsSync(pinPath), "expected korean-privacy-terms/scripts/upstream.pin to exist"); assert.ok(fs.existsSync(installPath), "expected korean-privacy-terms/scripts/install.sh to exist"); const pin = fs.readFileSync(pinPath, "utf8").trim(); assert.match(pin, /^[0-9a-f]{40}$/, "upstream.pin must contain a single 40-char git SHA"); assert.notStrictEqual( pin, "0".repeat(40), "upstream.pin must not be a placeholder all-zero SHA", ); assert.notStrictEqual( pin, "f".repeat(40), "upstream.pin must not be a placeholder all-f SHA", ); const install = fs.readFileSync(installPath, "utf8"); assert.match(install, /^#!\/(?:usr\/bin\/env bash|bin\/bash)/m, "install.sh must start with a bash shebang"); assert.match(install, /set -euo pipefail/, "install.sh must opt into strict bash mode"); assert.match(install, /~\/\.claude\/skills\/korean-privacy-terms/); assert.match(install, /~\/\.agents\/skills\/korean-privacy-terms/); assert.match( install, /https:\/\/github\.com\/kimlawtech\/korean-privacy-terms\.git/, "install.sh must reference the full upstream clone URL", ); assert.match( install, /git clone --filter=blob:none/, "install.sh must perform a blobless git clone of the upstream repo", ); assert.match(install, /upstream\.pin/); const stat = fs.statSync(installPath); assert.ok( (stat.mode & 0o111) !== 0, "install.sh must have the executable bit set on at least one of user/group/other", ); }); test("korean-privacy-terms bundles the Apache-2.0 LICENSE per §4(a) redistribution requirement", () => { const licensePath = path.join(repoRoot, "korean-privacy-terms", "LICENSE.upstream"); assert.ok( fs.existsSync(licensePath), "expected korean-privacy-terms/LICENSE.upstream to exist (Apache-2.0 §4(a) requires redistributors to give recipients a copy of this License)", ); const license = fs.readFileSync(licensePath, "utf8"); assert.match(license, /Apache License/); assert.match(license, /Version 2\.0, January 2004/); assert.match(license, /http:\/\/www\.apache\.org\/licenses\/LICENSE-2\.0/); assert.match(license, /TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION/); assert.match(license, /Redistribution\. You may reproduce and distribute/); assert.match(license, /END OF TERMS AND CONDITIONS/); assert.match(license, /APPENDIX: How to apply the Apache License/); assert.match(license, /Copyright 2026 kimlawtech/); const skill = read(path.join("korean-privacy-terms", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "korean-privacy-terms.md")); assert.match( skill, /LICENSE\.upstream/, "SKILL.md Notes section must link to LICENSE.upstream so §4(a) is satisfied even before install.sh runs", ); assert.match( featureDoc, /LICENSE\.upstream/, "docs/features/korean-privacy-terms.md must reference LICENSE.upstream", ); }); test("korean-privacy-terms feature doc documents the thin-wrapper install flow and legal disclaimer", () => { const featureDoc = read(path.join("docs", "features", "korean-privacy-terms.md")); assert.match(featureDoc, /kimlawtech\/korean-privacy-terms/); assert.match(featureDoc, /Apache-2\.0/); assert.match(featureDoc, /~\/\.claude\/skills\/korean-privacy-terms/); assert.match(featureDoc, /~\/\.agents\/skills\/korean-privacy-terms/); assert.match(featureDoc, /scripts\/install\.sh/); assert.match(featureDoc, /scripts\/upstream\.pin/); assert.match(featureDoc, /참고용 초안/); assert.match(featureDoc, /법률 자문/); assert.match(featureDoc, /변호사 검토/); assert.match(featureDoc, /2026\.9\.11/); assert.match(featureDoc, /Next\.js/); }); test("repository docs advertise the korean-jangbu-for thin-wrapper skill", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const sources = read(path.join("docs", "sources.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "korean-jangbu-for.md"); const skillPath = path.join(repoRoot, "korean-jangbu-for", "SKILL.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/korean-jangbu-for.md to exist"); assert.ok(fs.existsSync(skillPath), "expected korean-jangbu-for/SKILL.md to exist"); assert.match(readme, /\| 한국 사업자 장부 자동화 \|/); assert.match(readme, /\[한국 사업자 장부 자동화 가이드\]\(docs\/features\/korean-jangbu-for\.md\)/); assert.match(install, /--skill korean-jangbu-for/); assert.match(sources, /https:\/\/github\.com\/kimlawtech\/korean-jangbu-for/); assert.match(sources, /Apache-2\.0/); assert.match(sources, /kimlawtech/); assert.match(sources, /SpeciAI/); }); test("korean-jangbu-for skill cites upstream author and enforces accounting/tax disclaimers", () => { const skillPath = path.join(repoRoot, "korean-jangbu-for", "SKILL.md"); assert.ok(fs.existsSync(skillPath), "expected korean-jangbu-for/SKILL.md to exist"); const skill = read(path.join("korean-jangbu-for", "SKILL.md")); assert.match(skill, /^name: korean-jangbu-for$/m); assert.match(skill, /^license: Apache-2\.0$/m); assert.match(skill, /^description: .*장부.*사업자.*$/m); assert.match(skill, /https:\/\/github\.com\/kimlawtech\/korean-jangbu-for/); assert.match(skill, /@kimlawtech/); assert.match(skill, /SpeciAI/); assert.match(skill, /Apache-2\.0/); assert.match(skill, /참고용 초안/); assert.match(skill, /공식 회계감사/); assert.match(skill, /세무신고/); assert.match(skill, /세무사 검토/); assert.match(skill, /공인회계사/); assert.match(skill, /CODEF/); assert.match(skill, /BYOK/); assert.match(skill, /~\/\.claude\/skills\/korean-jangbu-for/); assert.match(skill, /~\/\.agents\/skills\/korean-jangbu-for/); assert.match(skill, /scripts\/install\.sh/); assert.match(skill, /scripts\/upstream\.pin/); assert.match(skill, /DISCLAIMER\.md/); assert.match(skill, /LICENSE\.upstream/); assert.doesNotMatch(skill, /AskUserQuestion/); }); test("korean-jangbu-for preserves upstream attribution, disclaimer, and Apache license", () => { const noticePath = path.join(repoRoot, "korean-jangbu-for", "NOTICE"); const disclaimerPath = path.join(repoRoot, "korean-jangbu-for", "DISCLAIMER.md"); const licensePath = path.join(repoRoot, "korean-jangbu-for", "LICENSE.upstream"); assert.ok(fs.existsSync(noticePath), "expected korean-jangbu-for/NOTICE to exist"); assert.ok(fs.existsSync(disclaimerPath), "expected korean-jangbu-for/DISCLAIMER.md to exist"); assert.ok(fs.existsSync(licensePath), "expected korean-jangbu-for/LICENSE.upstream to exist"); const notice = read(path.join("korean-jangbu-for", "NOTICE")); const disclaimer = read(path.join("korean-jangbu-for", "DISCLAIMER.md")); const license = read(path.join("korean-jangbu-for", "LICENSE.upstream")); assert.match(notice, /korean-jangbu-for/); assert.match(notice, /Copyright 2026 kimlawtech/); assert.match(notice, /SpeciAI/); assert.match(notice, /@kimlawtech/); assert.match(notice, /https:\/\/github\.com\/kimlawtech\/korean-jangbu-for/); assert.match(disclaimer, /참고용 초안/); assert.match(disclaimer, /공식 회계감사/); assert.match(disclaimer, /세무신고/); assert.match(disclaimer, /세무사 검토 필수/); assert.match(disclaimer, /공인회계사 감사 필수/); assert.match(disclaimer, /법적 효력/); assert.match(license, /Apache License/); assert.match(license, /Version 2\.0, January 2004/); assert.match(license, /END OF TERMS AND CONDITIONS/); assert.match(license, /Copyright 2026 kimlawtech \(SpeciAI\)/); }); test("korean-jangbu-for ships an install.sh wrapper and a pinned upstream SHA", () => { const pinPath = path.join(repoRoot, "korean-jangbu-for", "scripts", "upstream.pin"); const installPath = path.join(repoRoot, "korean-jangbu-for", "scripts", "install.sh"); assert.ok(fs.existsSync(pinPath), "expected korean-jangbu-for/scripts/upstream.pin to exist"); assert.ok(fs.existsSync(installPath), "expected korean-jangbu-for/scripts/install.sh to exist"); const pin = read(path.join("korean-jangbu-for", "scripts", "upstream.pin")).trim(); assert.match(pin, /^[0-9a-f]{40}$/, "upstream.pin must contain a single 40-char git SHA"); assert.notStrictEqual(pin, "0".repeat(40), "upstream.pin must not be a placeholder all-zero SHA"); const install = read(path.join("korean-jangbu-for", "scripts", "install.sh")); assert.match(install, /^#!\/(?:usr\/bin\/env bash|bin\/bash)/m); assert.match(install, /set -euo pipefail/); assert.match(install, /~\/\.claude\/skills\/korean-jangbu-for/); assert.match(install, /~\/\.agents\/skills\/korean-jangbu-for/); assert.match(install, /https:\/\/github\.com\/kimlawtech\/korean-jangbu-for\.git/); assert.match(install, /git clone --filter=blob:none/); assert.match(install, /upstream\.pin/); assert.match(install, /verify\.sh/); assert.match(install, /Re-run this wrapper installer after upstream runtime install/); const stat = fs.statSync(installPath); assert.ok((stat.mode & 0o111) !== 0, "install.sh must be executable"); }); test("korean-jangbu-for installer registers upstream subskills for Claude and agents", () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "korean-jangbu-for-install-")); const homeDir = path.join(tmpDir, "home"); const upstreamDir = path.join(tmpDir, "upstream"); const installPath = path.join(repoRoot, "korean-jangbu-for", "scripts", "install.sh"); const upstreamSubskills = [ "jangbu-connect", "jangbu-dash", "jangbu-import", "jangbu-jongso", "jangbu-tag", "jangbu-tax", ]; fs.mkdirSync(path.join(upstreamDir, "skills"), { recursive: true }); fs.mkdirSync(homeDir, { recursive: true }); for (const skillName of ["korean-jangbu-for", ...upstreamSubskills]) { const skillDir = path.join(upstreamDir, "skills", skillName); fs.mkdirSync(skillDir, { recursive: true }); fs.writeFileSync( path.join(skillDir, "SKILL.md"), `---\nname: ${skillName}\n---\n\n# ${skillName}\n`, ); } fs.mkdirSync(path.join(upstreamDir, "scripts"), { recursive: true }); fs.writeFileSync(path.join(upstreamDir, "scripts", "verify.sh"), "#!/usr/bin/env bash\nexit 0\n"); childProcess.execFileSync("git", ["init"], { cwd: upstreamDir, stdio: "ignore" }); childProcess.execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: upstreamDir }); childProcess.execFileSync("git", ["config", "user.name", "Test"], { cwd: upstreamDir }); childProcess.execFileSync("git", ["add", "."], { cwd: upstreamDir }); childProcess.execFileSync("git", ["commit", "-m", "seed upstream skills"], { cwd: upstreamDir, stdio: "ignore" }); const upstreamSha = childProcess.execFileSync("git", ["rev-parse", "HEAD"], { cwd: upstreamDir, encoding: "utf8" }).trim(); fs.mkdirSync(path.join(homeDir, ".claude", "skills"), { recursive: true }); fs.symlinkSync( path.join(upstreamDir, "skills", "korean-jangbu-for"), path.join(homeDir, ".claude", "skills", "korean-jangbu-for"), ); childProcess.execFileSync("bash", [installPath], { cwd: repoRoot, env: { ...process.env, HOME: homeDir, KOREAN_JANGBU_FOR_UPSTREAM_REPO: upstreamDir, KOREAN_JANGBU_FOR_UPSTREAM_SHA: upstreamSha, }, stdio: "pipe", }); for (const root of [".claude", ".agents"]) { const skillRoot = path.join(homeDir, root, "skills"); assert.ok( fs.existsSync(path.join(skillRoot, "korean-jangbu-for", "upstream", "skills", "jangbu-connect", "SKILL.md")), `${root} should keep the pinned upstream checkout nested under the wrapper`, ); assert.match( fs.readFileSync(path.join(skillRoot, "korean-jangbu-for", "SKILL.md"), "utf8"), /@kimlawtech/, `${root} should keep the korean-jangbu-for wrapper policy at the top level`, ); assert.ok( !fs.lstatSync(path.join(skillRoot, "korean-jangbu-for")).isSymbolicLink(), `${root} should replace conflicting upstream korean-jangbu-for symlinks with a wrapper directory`, ); for (const requiredPath of [ "scripts/install.sh", "scripts/upstream.pin", "LICENSE.upstream", "DISCLAIMER.md", "NOTICE", ]) { assert.ok( fs.existsSync(path.join(skillRoot, "korean-jangbu-for", requiredPath)), `${root} should install wrapper support payload ${requiredPath}`, ); } for (const skillName of upstreamSubskills) { const installedSubskillPath = path.join(skillRoot, skillName, "SKILL.md"); assert.ok( fs.existsSync(installedSubskillPath), `${root} should register upstream subskill ${skillName} as a top-level discoverable skill`, ); const installedSubskill = fs.readFileSync(installedSubskillPath, "utf8"); assert.match(installedSubskill, /https:\/\/github\.com\/kimlawtech\/korean-jangbu-for/); assert.match(installedSubskill, /@kimlawtech/); assert.match(installedSubskill, /SpeciAI/); assert.match(installedSubskill, /Apache-2\.0/); assert.match(installedSubskill, /참고용 초안/); assert.match(installedSubskill, /공식 회계감사/); assert.match(installedSubskill, /세무신고/); } } for (const installedRoot of [".claude", ".agents"]) { childProcess.execFileSync("bash", [path.join(homeDir, installedRoot, "skills", "korean-jangbu-for", "scripts", "install.sh")], { cwd: repoRoot, env: { ...process.env, HOME: homeDir, KOREAN_JANGBU_FOR_UPSTREAM_REPO: upstreamDir, KOREAN_JANGBU_FOR_UPSTREAM_SHA: upstreamSha, }, stdio: "pipe", }); } }); test("korean-jangbu-for installer preflights promoted subskill collisions before home writes", () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "korean-jangbu-for-collision-")); const homeDir = path.join(tmpDir, "home"); const upstreamDir = path.join(tmpDir, "upstream"); const installPath = path.join(repoRoot, "korean-jangbu-for", "scripts", "install.sh"); const upstreamSubskills = [ "jangbu-connect", "jangbu-dash", "jangbu-import", "jangbu-jongso", "jangbu-tag", "jangbu-tax", ]; for (const skillName of upstreamSubskills) { fs.mkdirSync(path.join(upstreamDir, "skills", skillName), { recursive: true }); fs.writeFileSync( path.join(upstreamDir, "skills", skillName, "SKILL.md"), `---\nname: ${skillName}\n---\n\n# ${skillName}\n`, ); } fs.mkdirSync(path.join(upstreamDir, "scripts"), { recursive: true }); fs.writeFileSync(path.join(upstreamDir, "scripts", "verify.sh"), "#!/usr/bin/env bash\nexit 0\n"); childProcess.execFileSync("git", ["init"], { cwd: upstreamDir, stdio: "ignore" }); childProcess.execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: upstreamDir }); childProcess.execFileSync("git", ["config", "user.name", "Test"], { cwd: upstreamDir }); childProcess.execFileSync("git", ["add", "."], { cwd: upstreamDir }); childProcess.execFileSync("git", ["commit", "-m", "seed upstream skills"], { cwd: upstreamDir, stdio: "ignore" }); const upstreamSha = childProcess.execFileSync("git", ["rev-parse", "HEAD"], { cwd: upstreamDir, encoding: "utf8" }).trim(); const unrelatedSkillDir = path.join(homeDir, ".agents", "skills", "jangbu-tax"); fs.mkdirSync(unrelatedSkillDir, { recursive: true }); fs.writeFileSync( path.join(unrelatedSkillDir, "SKILL.md"), "---\nname: jangbu-tax\n---\n\n# user-authored jangbu-tax\n", ); assert.throws( () => childProcess.execFileSync("bash", [installPath], { cwd: repoRoot, env: { ...process.env, HOME: homeDir, KOREAN_JANGBU_FOR_UPSTREAM_REPO: upstreamDir, KOREAN_JANGBU_FOR_UPSTREAM_SHA: upstreamSha, }, stdio: "pipe", }), /refusing to overwrite unrelated skill/, ); assert.match( fs.readFileSync(path.join(unrelatedSkillDir, "SKILL.md"), "utf8"), /user-authored jangbu-tax/, "unrelated existing subskill should be preserved after installer refusal", ); for (const root of [".claude", ".agents"]) { const skillRoot = path.join(homeDir, root, "skills"); assert.ok( !fs.existsSync(path.join(skillRoot, "korean-jangbu-for")), `${root} should not create the wrapper directory after a promoted-subskill preflight failure`, ); for (const skillName of upstreamSubskills.filter((name) => name !== "jangbu-tax")) { assert.ok( !fs.existsSync(path.join(skillRoot, skillName)), `${root} should not create promoted subskill ${skillName} after a promoted-subskill preflight failure`, ); } } }); test("korean-jangbu-for feature doc documents source-first use and mandatory attribution", () => { const featureDoc = read(path.join("docs", "features", "korean-jangbu-for.md")); assert.match(featureDoc, /kimlawtech\/korean-jangbu-for/); assert.match(featureDoc, /https:\/\/github\.com\/kimlawtech\/korean-jangbu-for/); assert.match(featureDoc, /@kimlawtech/); assert.match(featureDoc, /SpeciAI/); assert.match(featureDoc, /Apache-2\.0/); assert.match(featureDoc, /~\/\.claude\/skills\/korean-jangbu-for/); assert.match(featureDoc, /~\/\.agents\/skills\/korean-jangbu-for/); assert.match(featureDoc, /scripts\/install\.sh/); assert.match(featureDoc, /scripts\/upstream\.pin/); assert.match(featureDoc, /CODEF/); assert.match(featureDoc, /BYOK/); assert.match(featureDoc, /세무사 검토/); assert.match(featureDoc, /공인회계사/); }); test("corporate-registration-consulting skill covers court registry workflow, tax pitfalls, and rhwp automation", () => { const skillPath = path.join(repoRoot, "corporate-registration-consulting", "SKILL.md"); const featureDocPath = path.join(repoRoot, "docs", "features", "corporate-registration-consulting.md"); assert.ok(fs.existsSync(skillPath), "expected corporate-registration-consulting/SKILL.md to exist"); assert.ok(fs.existsSync(featureDocPath), "expected corporate-registration-consulting feature doc to exist"); const documentPackTemplatePath = path.join( "corporate-registration-consulting", "templates", "incorporation-document-pack.md", ); const officialFormSourcesPath = path.join( "corporate-registration-consulting", "templates", "official-form-sources.md", ); const officialPromoterHwpPath = path.join( "corporate-registration-consulting", "templates", "official", "form-65-1-stock-company-incorporation-promoter.hwp", ); const officialSubscriptionHwpPath = path.join( "corporate-registration-consulting", "templates", "official", "form-65-2-stock-company-incorporation-subscription.hwp", ); const officialFillMapPath = path.join( "corporate-registration-consulting", "templates", "official", "form-65-1-fill-map.json", ); const officialSourceManifestPath = path.join( "corporate-registration-consulting", "templates", "official", "source-manifest.json", ); const officialFillScriptPath = path.join( "corporate-registration-consulting", "scripts", "fill_official_hwp.py", ); const attachmentHwpDir = path.join("corporate-registration-consulting", "templates", "attachment-hwp"); const attachmentHwpArtifacts = [ "articles-of-incorporation.hwp", "standard-articles-startup-moj.hwp", "share-issuance-consent.hwp", "share-subscription.hwp", "founder-meeting-minutes.hwp", "founder-meeting-period-shortening-consent.hwp", "shareholder-register.hwp", "inspection-report.hwp", "officer-acceptance-director-ceo.hwp", "officer-acceptance-auditor.hwp", "board-minutes.hwp", "corporate-seal-report.hwp", "power-of-attorney.hwp", ].map((fileName) => path.join(attachmentHwpDir, fileName)); assert.ok( fs.existsSync(path.join(repoRoot, documentPackTemplatePath)), "expected an incorporation document pack template artifact", ); assert.ok( fs.existsSync(path.join(repoRoot, officialFormSourcesPath)), "expected an official form sources artifact", ); for (const artifactPath of [officialPromoterHwpPath, officialSubscriptionHwpPath, officialFillMapPath, officialSourceManifestPath, officialFillScriptPath, ...attachmentHwpArtifacts, path.join(attachmentHwpDir, "source-manifest.json")]) { assert.ok(fs.existsSync(path.join(repoRoot, artifactPath)), `expected ${artifactPath} to exist`); } const documentPackTemplate = read(documentPackTemplatePath); const officialFormSources = read(officialFormSourcesPath); const officialFillMap = JSON.parse(read(officialFillMapPath)); const officialSourceManifest = JSON.parse(read(officialSourceManifestPath)); const attachmentSourceManifest = JSON.parse(read(path.join(attachmentHwpDir, "source-manifest.json"))); for (const hwpPath of [officialPromoterHwpPath, officialSubscriptionHwpPath, ...attachmentHwpArtifacts]) { const hwpMagic = fs.readFileSync(path.join(repoRoot, hwpPath)).subarray(0, 8).toString("hex"); assert.equal(hwpMagic, "d0cf11e0a1b11ae1", `${hwpPath} should be an OLE HWP file`); } const skill = read(path.join("corporate-registration-consulting", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "corporate-registration-consulting.md")); const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const sources = read(path.join("docs", "sources.md")); assert.match(skill, /^---\nname: corporate-registration-consulting\n/); assert.match(skill, /참고용/); assert.match(skill, /법률 자문/); assert.match(skill, /변호사|법무사|세무사/); assert.match(skill, /상호/); assert.match(skill, /정관/); assert.match(skill, /잔고증명|주금납입/); assert.match(skill, /취임승낙서/); assert.match(skill, /조사보고서/); assert.match(skill, /등록면허세/); assert.match(skill, /과밀억제권역/); assert.match(skill, /지방세법 제28조/); assert.match(skill, /소프트웨어/); assert.match(skill, /주민등록번호/); assert.match(skill, /마스킹/); assert.match(skill, /에이전트는[\s\S]*로그인[\s\S]*전자서명[\s\S]*세금 납부[\s\S]*등기 제출[\s\S]*수행하지 않는다/); assert.match(skill, /사용자 사칭[\s\S]*수행하지 않는다/); assert.match(skill, /최종 법률 판단[\s\S]*수행하지 않는다/); assert.match(skill, /최종 세무 판단[\s\S]*수행하지 않는다/); assert.match(skill, /모집설립/); assert.match(skill, /모집설립은 일반적이지[\s\S]*기본 플로우에서는 제외/); assert.match(skill, /저장된 발기설립 양식과 첨부서류 양식/); assert.match(skill, /등기신청양식/); assert.match(skill, /첨부서면예시/); assert.match(skill, /상업등기신청서의 양식에 관한 예규/); assert.match(skill, /official-form-sources\.md/); assert.match(skill, /form-65-1-stock-company-incorporation-promoter\.hwp/); assert.match(skill, /fill_official_hwp\.py/); assert.match(skill, /form-65-1-fill-map\.json/); assert.match(skill, /기본 산출물은 실제 HWP 파일/); assert.match(skill, /Markdown만 반환하지 말고/); assert.match(skill, /dummy[\s\S]*지양|dummy[\s\S]*남기지 않는다/); assert.match(skill, /반드시 작성할 문서와 저장된 양식 경로/); assert.match(skill, /저장해 둔 HWP 양식을 우선 활용|저장된 HWP 양식을 우선 활용/); assert.match(skill, /에이전트가 매번 공식 양식을 새로 찾게 하지 말고/); assert.match(skill, /정관은 최대한 저장된 표준정관을 그대로 따른다/); assert.match(skill, /정관[\s\S]*제2조[\s\S]*업태·종목/); assert.match(skill, /정관[\s\S]*맨 마지막[\s\S]*작성일자[\s\S]*서명|정관[\s\S]*맨 마지막[\s\S]*날짜[\s\S]*기명날인/); assert.match(skill, /간인/); assert.match(skill, /법인인감/); assert.match(skill, /templates\/attachment-hwp\//); assert.match(skill, /자리표시자/); assert.match(skill, /articles-of-incorporation\.hwp/); assert.match(skill, /founder-meeting-minutes\.hwp/); assert.match(skill, /share-subscription\.hwp/); assert.match(skill, /inspection-report\.hwp/); assert.match(skill, /officer-acceptance-director-ceo\.hwp/); assert.match(skill, /rhwp-edit/); assert.match(skill, /k-skill-rhwp/); assert.match(skill, /본문[\s\S]*replace-all|replace-all[\s\S]*본문/); assert.match(skill, /replace-all[\s\S]*shortcut/); assert.match(skill, /한 장 한 장[\s\S]*순차/); assert.match(skill, /set-cell-text/); assert.match(skill, /info.*list-paragraphs/); assert.match(skill, /render|kordoc/); assert.match(skill, /표.*셀/); assert.match(skill, /레포.*밖|레포.*외부|저장소.*밖|저장소.*외부/); assert.doesNotMatch(skill, /\.\/out\/court-form-filled\.hwp/); assert.match(skill, /등기신청수수료 영수필확인서/); assert.match(skill, /등기이사[\s\S]*인감증명서 또는 본인서명사실확인서/); assert.match(skill, /등기이사[\s\S]*주민등록초본\/등본/); assert.match(skill, /상법 제291조/); assert.match(skill, /명의개서대리인/); assert.match(skill, /현물출자[\s\S]*재산인수/); assert.match(skill, /인허가/); assert.match(skill, /정관 공증|의사록 인증/); assert.match(skill, /법인명/); assert.match(skill, /이사/); assert.match(skill, /주소/); assert.match(skill, /쉬운 말/); assert.match(skill, /사용자 결정/); assert.match(documentPackTemplate, /취임승낙서/); assert.match(documentPackTemplate, /인감신고서/); assert.match(documentPackTemplate, /등록면허세/); assert.match(documentPackTemplate, /등기신청수수료 영수필확인서/); assert.match(documentPackTemplate, /등기이사[\s\S]*인감증명서 또는 본인서명사실확인서/); assert.match(documentPackTemplate, /등기이사[\s\S]*주민등록초본\/등본/); assert.match(documentPackTemplate, /명의개서대리인/); assert.match(documentPackTemplate, /현물출자[\s\S]*재산인수/); assert.match(documentPackTemplate, /인허가/); assert.match(documentPackTemplate, /정관 공증|의사록 인증/); assert.match(documentPackTemplate, /개인정보|민감정보/); assert.match(documentPackTemplate, /양식별 수정 위치/); assert.match(documentPackTemplate, /간인/); assert.match(documentPackTemplate, /법인인감/); assert.match(documentPackTemplate, /레포.*커밋/); assert.match(documentPackTemplate, /\{\{INSPECTION_CONCLUSION_AFTER_USER_OR_EXPERT_REVIEW\}\}/); assert.doesNotMatch(documentPackTemplate, /중대한 흠이 없음을 보고합니다/); assert.match(officialFormSources, /인터넷등기소/); assert.match(officialFormSources, /등기신청양식/); assert.match(officialFormSources, /첨부서면예시/); assert.match(officialFormSources, /상업등기신청서의 양식에 관한 예규/); assert.match(officialFormSources, /양식 제65-1호/); assert.match(officialFormSources, /양식 제65-2호/); assert.match(officialFormSources, /이미 저장된 HWP 양식 경로/); assert.match(officialFormSources, /새 양식을 찾는 것부터 시작하지 말고/); assert.match(officialFormSources, /모집설립[\s\S]*기본 플로우에서는 사용하지 않음/); assert.match(officialFormSources, /HWP\/HWPX\/PDF/); assert.match(officialFormSources, /templates\/official/); assert.match(officialFormSources, /form-65-1-stock-company-incorporation-promoter\.hwp/); assert.match(officialFormSources, /form-65-2-stock-company-incorporation-subscription\.hwp/); assert.match(officialFormSources, /fill_official_hwp\.py/); assert.match(officialFormSources, /templates\/attachment-hwp\//); assert.match(officialFormSources, /실제 공개 배포 첨부서류 HWP 양식 묶음/); assert.match(officialFormSources, /자리표시자/); assert.match(officialFormSources, /articles-of-incorporation\.hwp/); assert.match(officialFormSources, /corporate-seal-report\.hwp/); assert.match(officialFormSources, /최신본을 다시 확인|최신 예규/); assert.match(officialFormSources, /replace-all[\s\S]*shortcut/); assert.match(officialFormSources, /간인/); assert.match(officialFormSources, /법인인감/); assert.match(officialFormSources, /등기신청수수료 영수필확인서/); assert.match(officialFormSources, /등기이사[\s\S]*인감증명서 또는 본인서명사실확인서/); assert.match(officialFormSources, /등기이사[\s\S]*주민등록초본\/등본/); assert.match(officialFormSources, /상업등기규칙 제129조/); assert.match(officialFormSources, /명의개서대리인/); assert.match(officialFormSources, /현물출자[\s\S]*재산인수/); assert.match(officialFormSources, /인허가/); assert.match(officialFormSources, /정관 공증|의사록 인증/); assert.equal(officialFillMap.fields.company_name.cell, 21); assert.equal(officialFillMap.fields.head_office_address.cell, 23); assert.equal(officialFillMap.fields.capital_krw.cell, 33); assert.equal(officialSourceManifest.source.adm_rul_seq, "2200000106061"); assert.match(officialSourceManifest.files[0].sha256, /^[a-f0-9]{64}$/); assert.match(attachmentSourceManifest.note, /sanitized to placeholders|자리표시자/); assert.match(featureDoc, /법인등기 신청 컨설팅/); assert.match(featureDoc, /정관/); assert.match(featureDoc, /등록면허세/); assert.match(featureDoc, /과밀억제권역/); assert.match(featureDoc, /조세특례제한법 제6조/); assert.match(featureDoc, /지방세법 제28조/); assert.match(featureDoc, /에이전트는[\s\S]*로그인[\s\S]*전자서명[\s\S]*세금 납부[\s\S]*등기 제출[\s\S]*지원하지 않는다|에이전트는[\s\S]*로그인[\s\S]*전자서명[\s\S]*세금 납부[\s\S]*등기 제출[\s\S]*수행하지 않는다/); assert.match(featureDoc, /사용자 사칭[\s\S]*(지원하지 않는다|수행하지 않는다|사용자가 직접 또는 전문가)/); assert.match(featureDoc, /최종 법률 판단[\s\S]*(지원하지 않는다|수행하지 않는다|사용자가 직접 또는 전문가)/); assert.match(featureDoc, /최종 세무 판단[\s\S]*(지원하지 않는다|수행하지 않는다|사용자가 직접 또는 전문가)/); assert.match(featureDoc, /개인정보|민감정보/); assert.match(featureDoc, /자리표시자/); assert.match(featureDoc, /최종 산출물은 실제 `?\.hwp`? 사본/); assert.match(featureDoc, /저장된 HWP 양식 사본/); assert.match(featureDoc, /표 셀|set-cell-text/); assert.match(featureDoc, /모집설립/); assert.match(featureDoc, /모집설립은 일반적이지[\s\S]*기본 플로우/); assert.match(featureDoc, /저장된 양식 경로/); assert.match(featureDoc, /필수 문서와 양식/); assert.match(featureDoc, /replace-all/); assert.match(featureDoc, /replace-all[\s\S]*shortcut/); assert.match(featureDoc, /한 장 한 장[\s\S]*순차/); assert.match(featureDoc, /표 셀|set-cell-text/); assert.match(featureDoc, /간인/); assert.match(featureDoc, /법인인감/); assert.match(featureDoc, /등기신청수수료 영수필확인서/); assert.match(featureDoc, /등기이사[\s\S]*인감증명서\/본인서명사실확인서|등기이사[\s\S]*인감증명서 또는 본인서명사실확인서/); assert.match(featureDoc, /등기이사[\s\S]*주민등록초본\/등본/); assert.match(featureDoc, /상법 제291조/); assert.match(featureDoc, /명의개서대리인/); assert.match(featureDoc, /현물출자[\s\S]*재산인수/); assert.match(featureDoc, /인허가/); assert.match(featureDoc, /정관 공증|의사록 인증/); assert.match(featureDoc, /업태·종목/); assert.match(featureDoc, /표.*셀/); assert.match(featureDoc, /세금 납부/); assert.match(featureDoc, /실제 제출은 사용자가 직접/); assert.match(featureDoc, /인터넷등기소|온라인법인설립시스템/); assert.match(featureDoc, /form-65-1-stock-company-incorporation-promoter\.hwp/); assert.doesNotMatch(featureDoc, /양식 제65-2호/); assert.match(featureDoc, /templates\/official\/\*\.hwp|templates\/official\//); assert.match(featureDoc, /templates\/attachment-hwp\//); assert.match(featureDoc, /templates\/attachment-hwp\/\*\.hwp|첨부/); assert.match(featureDoc, /참고용/); assert.match(readme, /\| 법인등기 신청 컨설팅 \|/); assert.match(readme, /docs\/features\/corporate-registration-consulting\.md/); assert.match(install, /--skill corporate-registration-consulting/); assert.match(sources, /corporate-registration-consulting/); assert.match(sources, /startbiz\.go\.kr/); assert.match(sources, /law\.go\.kr/); }); test("iros-registry-automation skill documents safe IROS registry certificate automation and upstream credit", () => { const skillPath = path.join(repoRoot, "iros-registry-automation", "SKILL.md"); const featureDocPath = path.join(repoRoot, "docs", "features", "iros-registry-automation.md"); const upstreamPinPath = path.join(repoRoot, "iros-registry-automation", "scripts", "upstream.pin"); assert.ok(fs.existsSync(skillPath), "expected iros-registry-automation/SKILL.md to exist"); assert.ok(fs.existsSync(featureDocPath), "expected iros-registry-automation feature doc to exist"); assert.ok(fs.existsSync(upstreamPinPath), "expected iros-registry-automation/scripts/upstream.pin to exist"); const skill = read(path.join("iros-registry-automation", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "iros-registry-automation.md")); const upstreamPin = read(path.join("iros-registry-automation", "scripts", "upstream.pin")).trim(); const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const sources = read(path.join("docs", "sources.md")); const roadmap = read(path.join("docs", "roadmap.md")); assert.match(upstreamPin, /^[0-9a-f]{40}$/, "upstream pin should be a reviewed 40-character Git SHA"); assert.match(skill, /^---\nname: iros-registry-automation\n/); assert.match(skill, /등기부등본|등기사항증명서/); assert.match(skill, /법인/); assert.match(skill, /부동산/); assert.match(skill, /인터넷등기소|IROS/); assert.match(skill, /TouchEn nxKey/); assert.match(skill, /Playwright|Chromium/); assert.match(skill, /로그인[\s\S]*수동|사용자.*직접.*로그인/); assert.match(skill, /결제[\s\S]*수동|사용자.*직접.*결제/); assert.match(skill, /카드|공동인증서|간편인증/); assert.match(skill, /페이지당\s*10건|10건/); assert.match(skill, /장바구니/); assert.match(skill, /열람/); assert.match(skill, /저장|다운로드/); assert.match(skill, /법인등록번호/); assert.match(skill, /상호명/); assert.match(skill, /주소/); assert.match(skill, /개인정보|민감정보/); assert.match(skill, /법률 자문|참고용/); assert.match(skill, /challengekim/); assert.match(skill, /https:\/\/github\.com\/challengekim\/iros-registry-automation/); assert.match(skill, /MIT/); assert.match(skill, /원 저작자|원저작자|upstream|참고 구현/); assert.match(skill, new RegExp(`git checkout ${upstreamPin}`), "skill install flow should check out the pinned upstream SHA"); assert.match(skill, /scripts\/upstream\.pin/, "skill should document where the reviewed upstream pin lives"); assert.match(skill, /pin update|핀 업데이트|업스트림 핀|review/i, "skill should require review before updating the pin"); assert.match(skill, /\$workdir\/corp-input\.json/, "skill should put real corporate inputs under the private workdir"); assert.match( skill, /\$workdir\/companies-input\.json[\s\S]*(iros_download\.py|결제 후|열람|저장)/, "skill should prepare the company-name list required by iros_download.py before the download flow after payment", ); assert.match( skill, /"excel_path":\s*str\(workdir \/ "customer-list\.xlsx"\)/, "skill should redirect upstream customer Excel input to the private workdir", ); assert.match(skill, /\$workdir\/downloads/, "skill should point save_dir/download output at the private workdir"); assert.match(skill, /config\.json[\s\S]*save_dir[\s\S]*\$workdir/, "skill should wire config.json save_dir to the private workdir"); assert.match(skill, /upstream repo `data\/`|upstream `data\/`|data\/.*실제/, "skill should warn not to use upstream data/ for real inputs"); for (const doc of [featureDoc, sources]) { assert.match(doc, /challengekim/); assert.match(doc, /https:\/\/github\.com\/challengekim\/iros-registry-automation/); assert.match(doc, /인터넷등기소|IROS|iros\.go\.kr/); } assert.match(featureDoc, new RegExp(`git checkout ${upstreamPin}`), "feature doc install flow should check out the pinned upstream SHA"); assert.match(featureDoc, /scripts\/upstream\.pin/, "feature doc should document where the reviewed upstream pin lives"); assert.match(featureDoc, /pin update|핀 업데이트|업스트림 핀|review/i, "feature doc should require review before updating the pin"); assert.match(featureDoc, /로그인[\s\S]*수동|사용자.*직접.*로그인/); assert.match(featureDoc, /결제[\s\S]*수동|사용자.*직접.*결제/); assert.match(featureDoc, /법인[\s\S]*장바구니[\s\S]*열람[\s\S]*저장/); assert.match(featureDoc, /부동산[\s\S]*장바구니[\s\S]*수동/); assert.match(featureDoc, /TouchEn nxKey/); assert.match(featureDoc, /페이지당\s*10건|10건/); assert.match(featureDoc, /i?ros_cart_by_corpnum\.py|법인등록번호 기반/); assert.match(featureDoc, /i?ros_cart_realty\.py|부동산 장바구니/); assert.match(featureDoc, /저장소 밖|레포 밖|커밋하지/); assert.match(featureDoc, /\$workdir\/corp-input\.json/, "feature doc should put real corporate inputs under the private workdir"); assert.match( featureDoc, /\$workdir\/companies-input\.json[\s\S]*(iros_download\.py|결제 후|열람|저장)/, "feature doc should prepare the company-name list required by iros_download.py before the download flow after payment", ); assert.match( featureDoc, /"excel_path":\s*str\(workdir \/ "customer-list\.xlsx"\)/, "feature doc should redirect upstream customer Excel input to the private workdir", ); assert.match(featureDoc, /\$workdir\/downloads/, "feature doc should point save_dir/download output at the private workdir"); assert.match(featureDoc, /config\.json[\s\S]*save_dir[\s\S]*\$workdir/, "feature doc should wire config.json save_dir to the private workdir"); assert.match(featureDoc, /upstream repo `data\/`|upstream `data\/`|data\/.*실제/, "feature doc should warn not to use upstream data/ for real inputs"); assert.doesNotMatch(featureDoc, /결제.*자동화.*지원/); assert.match(readme, /\| 등기부등본 자동화 \| `iros-registry-automation` \|/); assert.match(readme, /docs\/features\/iros-registry-automation\.md/); assert.match(install, /--skill iros-registry-automation/); assert.match(roadmap, /등기부등본 자동화 스킬 출시/); }); test("rhwp-edit skill pins the k-skill-rhwp CLI as the editing engine and disclaims kordoc/rhwp-advanced routing", () => { const skill = read(path.join("rhwp-edit", "SKILL.md")); assert.match(skill, /^---\nname: rhwp-edit\n/); assert.match(skill, /k-skill-rhwp/); assert.match(skill, /@rhwp\/core/); assert.match(skill, /hwp\/SKILL\.md/); assert.match(skill, /rhwp-advanced\/SKILL\.md/); assert.match(skill, /insert-text/); assert.match(skill, /delete-text/); assert.match(skill, /replace-all/); assert.match(skill, /create-table/); assert.match(skill, /set-cell-text/); assert.match(skill, /create-blank/); assert.match(skill, /#196/); assert.match(skill, /본문 문단만/, "SKILL.md must document body-only scope for search/replace-all"); assert.match(skill, /set-cell-text/, "SKILL.md must reference set-cell-text for cell content workflow"); assert.match(skill, /non-overlapping|개행|문단 경계/, "SKILL.md must document replace-all edge cases"); assert.match( skill, /UTF-?16|U\+0130|İ|case[ -]?fold/i, "SKILL.md must disclose the case-insensitive UTF-16 length-drift guard (Unicode follow-up)" ); }); test("rhwp-advanced skill pins the upstream rhwp Rust CLI debug/dump/convert surface", () => { const skill = read(path.join("rhwp-advanced", "SKILL.md")); assert.match(skill, /^---\nname: rhwp-advanced\n/); assert.match(skill, /cargo install rhwp/); assert.match(skill, /export-svg/); assert.match(skill, /--debug-overlay/); assert.match(skill, /\brhwp dump\b/); assert.match(skill, /dump-pages/); assert.match(skill, /ir-diff/); assert.match(skill, /thumbnail/); assert.match(skill, /\brhwp convert\b/); assert.match(skill, /편집 서브커맨드[가는]? (없다|부재|제공하지 않는다|않는다)/); assert.match(skill, /rhwp-edit/); }); test("rhwp feature docs, README, install, roadmap, and sources are wired for the new skills", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const roadmap = read(path.join("docs", "roadmap.md")); const sources = read(path.join("docs", "sources.md")); const editDoc = read(path.join("docs", "features", "rhwp-edit.md")); const advancedDoc = read(path.join("docs", "features", "rhwp-advanced.md")); assert.match(readme, /\| HWP 문서 편집 \|/); assert.match(readme, /\| HWP 레이아웃·IR 디버깅 \|/); assert.match(readme, /\[HWP 문서 편집\]\(docs\/features\/rhwp-edit\.md\)/); assert.match(readme, /\[HWP 레이아웃·IR 디버깅\]\(docs\/features\/rhwp-advanced\.md\)/); assert.match(install, /--skill rhwp-edit/); assert.match(install, /--skill rhwp-advanced/); assert.match(roadmap, /rhwp-edit/); assert.match(roadmap, /rhwp-advanced/); assert.match(roadmap, /#155/); assert.match(sources, /edwardkim\/rhwp/); assert.match(sources, /@rhwp\/core/); assert.match(sources, /issues\/196/); assert.match(editDoc, /k-skill-rhwp/); assert.match(editDoc, /insert-text/); assert.match(editDoc, /create-table/); assert.match(editDoc, /#196/); assert.match( editDoc, /본문\S* 문단만|본문 \(body\) 문단만|body paragraphs only/, "rhwp-edit feature doc must disclose search/replace-all body-only scope" ); assert.match( editDoc, /UTF-?16|U\+0130|İ|case[ -]?fold/i, "rhwp-edit feature doc must disclose the case-insensitive UTF-16 length-drift guard (Unicode follow-up)" ); assert.match(advancedDoc, /cargo install rhwp/); assert.match(advancedDoc, /export-svg/); assert.match(advancedDoc, /ir-diff/); assert.match(advancedDoc, /편집/); }); test("k-skill-rhwp package ships CLI bin, WASM-init shim, and minor semver changeset", () => { const packagePath = path.join(repoRoot, "packages", "k-skill-rhwp", "package.json"); assert.ok(fs.existsSync(packagePath), "expected packages/k-skill-rhwp/package.json"); const pkg = JSON.parse(fs.readFileSync(packagePath, "utf8")); assert.equal(pkg.name, "k-skill-rhwp"); assert.ok(pkg.bin && pkg.bin["k-skill-rhwp"] === "bin/k-skill-rhwp.js", "expected bin mapping"); assert.ok(pkg.dependencies && pkg.dependencies["@rhwp/core"], "expected @rhwp/core dependency"); assert.ok(pkg.engines && /\^|>=\s*1[89]/.test(pkg.engines.node || ""), "expected Node 18+"); assert.ok( fs.existsSync(path.join(repoRoot, "packages", "k-skill-rhwp", "src", "wasm-init.js")), "expected src/wasm-init.js" ); assert.ok( fs.existsSync(path.join(repoRoot, "packages", "k-skill-rhwp", "bin", "k-skill-rhwp.js")), "expected bin/k-skill-rhwp.js" ); }); const README_SKILL_NAME_COLUMN_MAPPING = [ ["SRT 예매", "srt-booking"], ["KTX 예매", "ktx-booking"], ["카카오톡 Mac CLI", "kakaotalk-mac"], ["서울 지하철 도착정보 조회", "seoul-subway-arrival"], ["지하철 분실물 조회", "subway-lost-property"], ["긱뉴스 조회", "geeknews-search"], ["한국 날씨 조회", "korea-weather"], ["사용자 위치 미세먼지 조회", "fine-dust-location"], ["한강 수위 정보 조회", "han-river-water-level"], ["한국 법령 검색", "korean-law-search"], ["법인등기 신청 컨설팅", "corporate-registration-consulting"], ["한국 개인정보처리방침·이용약관 자동 생성", "korean-privacy-terms"], ["한국 부동산 실거래가 조회", "real-estate-search"], ["LH 청약 공고문 조회", "lh-notice-search"], ["장학금 검색 및 조회", "korean-scholarship-search"], ["생활쓰레기 배출정보 조회", "household-waste-info"], ["학교 급식 식단 조회", "k-schoollunch-menu"], ["도서관 도서 조회", "library-book-search"], ["의약품 안전 체크", "mfds-drug-safety"], ["식품 안전 체크", "mfds-food-safety"], ["한국 주식 정보 조회", "korean-stock-search"], ["금감원 DART 전자공시 조회", "k-dart"], ["조선왕조실록 검색", "joseon-sillok-search"], ["한국 특허 정보 검색", "korean-patent-search"], ["근처 가장 싼 주유소 찾기", "cheap-gas-nearby"], ["근처 공중화장실 찾기", "public-restroom-nearby"], ["근처 공영주차장 찾기", "parking-lot-search"], ["KBO 경기 결과 조회", "kbo-results"], ["KBL 경기 결과 조회", "kbl-results"], ["K리그 경기 결과 조회", "kleague-results"], ["LCK 경기 분석", "lck-analytics"], ["토스증권 조회", "toss-securities"], ["하이패스 영수증 발급", "hipass-receipt"], ["캐치테이블 예약 스나이핑", "catchtable-sniper"], ["로또 당첨 확인", "lotto-results"], ["HWP 문서 조회/변환", "hwp"], ["HWP 문서 편집", "rhwp-edit"], ["HWP 레이아웃·IR 디버깅", "rhwp-advanced"], ["근처 술집 조회", "kakao-bar-nearby"], ["우편번호 검색", "zipcode-search"], ["다이소 상품 조회", "daiso-product-search"], ["마켓컬리 상품 조회", "market-kurly-search"], ["올리브영 검색", "olive-young-search"], ["영화관 검색", "korean-cinema-search"], ["올라포케 역삼 포케", "hola-poke-yeoksam"], ["택배 배송조회", "delivery-tracking"], ["쿠팡 상품 검색", "coupang-product-search"], ["번개장터 검색", "bunjang-search"], ["중고차 가격 조회", "used-car-price-search"], ["한국어 맞춤법 검사", "korean-spell-check"], ["네이버 블로그 리서치", "naver-blog-research"], ["네이버 쇼핑 가격비교", "naver-shopping-search"], ["네이버 뉴스 검색", "naver-news-search"], ["한국어 글자 수 세기", "korean-character-count"], ["한국어 유행어 글쓰기", "korean-slang-writing"], ["K-스킬 클리너", "k-skill-cleaner"], ]; test("README skill table header advertises the new 스킬 이름 column (issue #165)", () => { const readme = read("README.md"); assert.match( readme, /\| 할 수 있는 일 \| 스킬 이름 \| 설명 \| 사용자 로그인 \| 문서 \|\n\| --- \| --- \| --- \| --- \| --- \|/, "expected the 어떤 걸 할 수 있나 table header to include 스킬 이름 between 할 수 있는 일 and 설명 with a 5-column separator", ); }); test("README skill table includes inline-code skill names for every documented row (issue #165)", () => { const readme = read("README.md"); assert.ok( README_SKILL_NAME_COLUMN_MAPPING.some(([, skillName]) => skillName === "k-skill-cleaner"), "expected k-skill-cleaner to be covered by the central README skill-name mapping fixture", ); for (const [label, skillName] of README_SKILL_NAME_COLUMN_MAPPING) { const escapedLabel = escapeRegex(label); const escapedName = escapeRegex(skillName); assert.match( readme, new RegExp(`\\| ${escapedLabel} \\| \`${escapedName}\` \\|`), `expected README row "${label}" to surface skill name \`${skillName}\` as the second column`, ); } }); test("legacy blue ribbon package is not part of npm workspaces or pack dry run", () => { const packageJson = readJson("package.json"); const packageLock = readJson("package-lock.json"); const packScript = packageJson.scripts["pack:dry-run"]; assert.doesNotMatch(packScript, /workspace blue-ribbon-nearby(?:\s|$)/); assert.ok(!fs.existsSync(path.join(repoRoot, "packages", "blue-ribbon-nearby", "package.json"))); assert.ok(fs.existsSync(path.join(repoRoot, "legacy", "unsupported-packages", "blue-ribbon-nearby", "package.json"))); assert.ok(!Object.hasOwn(packageLock.packages, "packages/blue-ribbon-nearby")); assert.ok(!Object.hasOwn(packageLock.packages, "node_modules/blue-ribbon-nearby")); }); test("README skill table skill-name column entries match real on-disk skill directories (issue #165)", () => { const allEntries = README_SKILL_NAME_COLUMN_MAPPING.map(([, skillName]) => skillName); for (const skillName of allEntries) { const skillFile = path.join(repoRoot, skillName, "SKILL.md"); assert.ok( fs.existsSync(skillFile), `expected ${skillName}/SKILL.md to exist on disk so the README table never advertises a non-existent skill identifier`, ); const frontmatterMatch = read(path.join(skillName, "SKILL.md")).match(/^name:\s*(\S+)\s*$/m); assert.ok(frontmatterMatch, `expected ${skillName}/SKILL.md to declare a name in frontmatter`); assert.equal( frontmatterMatch[1].replace(/^"|"$/g, ""), skillName, `expected ${skillName}/SKILL.md frontmatter name to match the directory name (validate-skills.sh invariant)`, ); } }); test("repository docs advertise the k-skill-cleaner skill and agent usage sources", () => { const readme = read("README.md"); const install = read(path.join("docs", "install.md")); const featureDocPath = path.join(repoRoot, "docs", "features", "k-skill-cleaner.md"); const skillPath = path.join(repoRoot, "k-skill-cleaner", "SKILL.md"); const skillLocalHelperPath = path.join(repoRoot, "k-skill-cleaner", "scripts", "k_skill_cleaner.py"); assert.ok(fs.existsSync(skillPath), "expected k-skill-cleaner/SKILL.md to exist"); assert.ok(fs.existsSync(skillLocalHelperPath), "expected k-skill-cleaner/scripts/k_skill_cleaner.py to be included in standalone skill installs"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/k-skill-cleaner.md to exist"); const skill = read(path.join("k-skill-cleaner", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "k-skill-cleaner.md")); assert.match(skill, /^name: k-skill-cleaner$/m); assert.match(skill, /Claude Code/); assert.match(skill, /Codex/); assert.match(skill, /OpenCode/); assert.match(skill, /OpenClaw\/ClawHub/); assert.match(skill, /Hermes Agent/); assert.match(skill, /python3 scripts\/k_skill_cleaner\.py/); assert.match(skill, /--days 90/); assert.match(featureDoc, /k-skill-cleaner\/scripts\/k_skill_cleaner\.py/); assert.match(featureDoc, /--days 90/); assert.match(featureDoc, /인터뷰/); assert.match(featureDoc, /트리거 횟수/); assert.match(readme, /\| K-스킬 클리너 \| `k-skill-cleaner` \|/); assert.match(readme, /\[K-스킬 클리너 가이드\]\(docs\/features\/k-skill-cleaner\.md\)/); assert.match(install, /--skill k-skill-cleaner/); }); test("court auction Workflow C docs preserve the PGJ151 safety contract", () => { const featureDoc = read(path.join("docs", "features", "court-auction-notice-search.md")); assert.match( featureDoc, /물건 자유 조건검색[\s\S]*PGJ151F00/, "Workflow C docs should name the PGJ151F00 warmup path for property search", ); assert.match( featureDoc, /pageSize[\s\S]*`10`\/`20`\/`50`\/`100`/, "Workflow C docs should keep pageSize aligned with the observed PGJ151 dropdown values", ); assert.doesNotMatch( featureDoc, /세션 cookie\([^\n]+\)는 `GET \/pgj\/index\.on\?w2xPath=\/pgj\/ui\/pgj100\/PGJ143M01\.xml&pgjId=143M01` 으로 사전에 한 번 받아둡니다\./, "Workflow C docs should not imply every endpoint warms up through PGJ143M01 only", ); }); test("court auction pending changeset does not publish stale fallback or pageSize guidance", () => { const changesetDir = path.join(repoRoot, ".changeset"); if (!fs.existsSync(changesetDir)) return; const pendingCourtAuctionChangesets = fs .readdirSync(changesetDir) .filter((entry) => entry.endsWith(".md")) .map((entry) => read(path.join(".changeset", entry))) .filter((contents) => contents.includes('"court-auction-notice-search"') && contents.includes("searchProperties()")); for (const contents of pendingCourtAuctionChangesets) { assert.doesNotMatch( contents, /direct HTTP call returns `BLOCKED` or `UPSTREAM_ERROR` 400/, "Changeset must not tell users that confirmed BLOCKED/ipcheck=false auto-falls back by default", ); assert.doesNotMatch( contents, /`pageSize` is capped at 100/, "Changeset must not describe pageSize as an arbitrary 1..100 cap", ); assert.match( contents, /`UPSTREAM_ERROR` 400/, "Changeset should still document the raw-HTTP WAF-style 400 fallback path", ); assert.match( contents, /`BLOCKED`[\s\S]*`fallbackOnBlocked:true`/, "Changeset should document that confirmed BLOCKED retry is explicit opt-in", ); assert.match( contents, /`10`\/`20`\/`50`\/`100`/, "Changeset should document the exact PGJ151 pageSize allowlist", ); } });