const test = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); 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 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`); } 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("hwp skill documents environment-aware routing 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, /@ohah\/hwpjs/); assert.match(skill, /\bhwp-mcp\b/); assert.match(skill, /Windows/i); assert.match(skill, /JSON/i); assert.match(skill, /Markdown/i); assert.match(skill, /HTML/i); assert.match(skill, /image/i); assert.match(skill, /batch/i); }); test("hwp skill documents inline image verification for markdown output", () => { const skill = read(path.join("hwp", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "hwp.md")); assert.match(skill, /hwpjs to-markdown document\.hwp -o output\.md --include-images/); assert.match(skill, /Markdown:.*(data:|base64)/); assert.match(skill, /--images-dir/); assert.doesNotMatch(skill, /Markdown:.*이미지 경로 생성 여부 확인/); assert.match(featureDoc, /--images-dir/); }); 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, /--include-images/); assert.match(featureDoc, /(data:|base64)/); assert.match(featureDoc, /Markdown 출력.*(data:|base64)/); assert.doesNotMatch(featureDoc, /Markdown 출력.*이미지 (파일 )?경로 생성 여부 확인/); }); 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("kakaotalk-mac skill documents safe macOS kakaocli usage", () => { const skillPath = path.join(repoRoot, "kakaotalk-mac", "SKILL.md"); assert.ok(fs.existsSync(skillPath), "expected kakaotalk-mac/SKILL.md 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); }); 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, /sops exec-env/); 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 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 ePost extraction flow and reliable transport example", () => { 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")); assert.match(skill, /^name: zipcode-search$/m); for (const doc of [skill, featureDoc]) { assert.match(doc, /parcel\.epost\.go\.kr\/parcel\/comm\/zipcode\/comm_newzipcd_list\.jsp/); assert.match(doc, /sch_zipcode/); assert.match(doc, /sch_address1/); assert.match(doc, /sch_bdNm/); 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, /mktemp|임시 파일/); assert.match(doc, /curl: \(23\)/); assert.match(doc, /짧은 도로명 \+ 건물번호/); assert.match(doc, /시\/군\/구 포함 전체 주소/); assert.doesNotMatch(doc, /urllib\.request/); assert.doesNotMatch(doc, /urlopen/); } assert.match(skill, /검색 결과가 없으면/i); assert.doesNotMatch(skill, /timeout\s*=/); assert.doesNotMatch(featureDoc, /timeout\s*=/); assert.match(skill, /`curl` 자체 제한/); assert.match(featureDoc, /프로토콜\/클라이언트 제약/i); assert.match(featureDoc, /`curl` 자체 제한/); }); 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("root pack:dry-run script covers all publishable workspaces", () => { const packageJson = readJson("package.json"); assert.match(packageJson.scripts["pack:dry-run"], /workspace k-lotto/); assert.match(packageJson.scripts["pack:dry-run"], /workspace daiso-product-search/); assert.match(packageJson.scripts["pack:dry-run"], /workspace blue-ribbon-nearby/); }); test("repository docs advertise the blue-ribbon-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", "blue-ribbon-nearby.md"); assert.ok(fs.existsSync(featureDocPath), "expected docs/features/blue-ribbon-nearby.md to exist"); assert.match(readme, /\| 근처 블루리본 맛집 \|/); assert.match(readme, /\[근처 블루리본 맛집 가이드\]\(docs\/features\/blue-ribbon-nearby\.md\)/); assert.match(install, /--skill blue-ribbon-nearby/); assert.match(roadmap, /근처 블루리본 맛집 스킬 출시/); assert.match(sources, /블루리본 지역 검색: https:\/\/www\.bluer\.co\.kr\/search\/zone/); assert.match(sources, /블루리본 주변 맛집 JSON: https:\/\/www\.bluer\.co\.kr\/restaurants\/map/); }); test("blue-ribbon-nearby skill documents mandatory location prompting and official Blue Ribbon nearby search flow", () => { const skillPath = path.join(repoRoot, "blue-ribbon-nearby", "SKILL.md"); assert.ok(fs.existsSync(skillPath), "expected blue-ribbon-nearby/SKILL.md to exist"); const skill = read(path.join("blue-ribbon-nearby", "SKILL.md")); const featureDoc = read(path.join("docs", "features", "blue-ribbon-nearby.md")); assert.match(skill, /^name: blue-ribbon-nearby$/m); assert.match(skill, /^description: .*근처 맛집.*블루리본.*$/m); for (const doc of [skill, featureDoc]) { assert.match(doc, /반드시.*현재 위치/u); assert.match(doc, /맛집.*기본적으로.*blue-ribbon-nearby|맛집.*기본적으로.*블루리본/u); assert.match(doc, /https:\/\/www\.bluer\.co\.kr\/search\/zone/); assert.match(doc, /https:\/\/www\.bluer\.co\.kr\/restaurants\/map/); assert.match(doc, /zone2Lat/); assert.match(doc, /zone2Lng/); assert.match(doc, /isAround=true/); assert.match(doc, /ribbon=true/); assert.match(doc, /위도|경도|동네|역명/u); assert.match(doc, /blue-ribbon-nearby|근처 블루리본 맛집/u); } }); test("blue-ribbon-nearby package README stays aligned with the location-first and official-surface guidance", () => { const packageReadme = read(path.join("packages", "blue-ribbon-nearby", "README.md")); assert.match(packageReadme, /먼저 현재 위치를 묻/u); assert.match(packageReadme, /코엑스.*삼성동\/대치동/u); assert.match(packageReadme, /https:\/\/www\.bluer\.co\.kr\/search\/zone/); assert.match(packageReadme, /https:\/\/www\.bluer\.co\.kr\/restaurants\/map/); assert.match(packageReadme, /searchNearbyByLocationQuery/); }); 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}`, ); });