mirror of
https://github.com/NomaDamas/k-skill.git
synced 2026-06-24 02:04:11 +00:00
The review follow-up closes legal-document safety gaps before merge by locking the expected guidance in the docs regression test and updating the skill surfaces that users and agents rely on. The representative-director clause now avoids board-resolution wording for the common two-director/no-board case, registration-license tax points to the direct 지방세법 제28조 anchor, and PII plus human-only filing boundaries are repeated where filled document workflows can expose sensitive data. Constraint: PR review requested TDD coverage for legal/tax safety wording and privacy guidance Rejected: Keep a single generic disclaimer only | document templates also need local warnings where filled artifacts are produced Confidence: high Scope-risk: narrow Directive: Do not remove the repeated privacy and human-only boundaries unless every user-facing generation surface keeps equivalent guidance Tested: node --test --test-name-pattern='corporate-registration-consulting' scripts/skill-docs.test.js Tested: npx --yes k-skill-rhwp create-blank <tmp>/blank.hwp && npx --yes k-skill-rhwp info <tmp>/blank.hwp Tested: npm run ci
3089 lines
148 KiB
JavaScript
3089 lines
148 KiB
JavaScript
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("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-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 require an explicit proxy until the hosted route is live", () => {
|
|
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, /\| 서울 지하철 도착정보 조회 \| self-host 또는 배포 확인이 끝난 `KSKILL_PROXY_BASE_URL` \|/);
|
|
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, /self-host|운영 중인 proxy|배포가 끝난 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, /기본값 `https:\/\/k-skill-proxy\.nomadamas\.org`/);
|
|
assert.doesNotMatch(doc, /없으면 hosted proxy .*기본/);
|
|
}
|
|
|
|
assert.match(proxyDoc, /GET \/v1\/seoul-subway\/arrival/);
|
|
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, /배포가 끝난 proxy|self-host/i);
|
|
assert.match(setupSkill, /서울 지하철: self-host 또는 배포 확인이 끝난 `KSKILL_PROXY_BASE_URL`/);
|
|
assert.doesNotMatch(secretsExample, /SEOUL_OPEN_API_KEY/);
|
|
assert.match(secretsExample, /KSKILL_PROXY_BASE_URL=https:\/\/your-proxy\.example\.com/);
|
|
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.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("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 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\/<productNo>|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 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("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 market-kurly-search/);
|
|
assert.match(packageJson.scripts["pack:dry-run"], /workspace blue-ribbon-nearby/);
|
|
assert.match(packageJson.scripts["pack:dry-run"], /workspace kakao-bar-nearby/);
|
|
assert.match(packageJson.scripts["pack:dry-run"], /workspace public-restroom-nearby/);
|
|
assert.match(packageJson.scripts["pack:dry-run"], /workspace kbl-results/);
|
|
assert.match(packageJson.scripts["pack:dry-run"], /workspace kleague-results/);
|
|
assert.match(packageJson.scripts["pack:dry-run"], /workspace lck-analytics/);
|
|
});
|
|
|
|
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("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(readme, /블루리본 측이 `www\.bluer\.co\.kr` 에 자동화 접근 전면 차단/);
|
|
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 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"].version, "0.2.0");
|
|
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/);
|
|
}
|
|
|
|
assert.match(doneSection, /search_interpretations/);
|
|
assert.match(doneSection, /search_ordinance/);
|
|
assert.match(doneSection, /법망|Beopmang/i);
|
|
assert.match(doneSection, /fallback/i);
|
|
|
|
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.*서울 지하철.*route가 실제 배포된 proxy URL/);
|
|
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("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 articlesTemplatePath = path.join(
|
|
"corporate-registration-consulting",
|
|
"templates",
|
|
"standard-articles-of-incorporation.md",
|
|
);
|
|
const documentPackTemplatePath = path.join(
|
|
"corporate-registration-consulting",
|
|
"templates",
|
|
"incorporation-document-pack.md",
|
|
);
|
|
|
|
assert.ok(
|
|
fs.existsSync(path.join(repoRoot, articlesTemplatePath)),
|
|
"expected a standard articles template artifact",
|
|
);
|
|
assert.ok(
|
|
fs.existsSync(path.join(repoRoot, documentPackTemplatePath)),
|
|
"expected an incorporation document pack template artifact",
|
|
);
|
|
|
|
const articlesTemplate = read(articlesTemplatePath);
|
|
const documentPackTemplate = read(documentPackTemplatePath);
|
|
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, /전자서명|세금 납부|등기 제출/);
|
|
assert.match(skill, /최종 법률.*세무 판단/);
|
|
assert.match(skill, /rhwp-edit/);
|
|
assert.match(skill, /k-skill-rhwp/);
|
|
assert.match(skill, /법인명/);
|
|
assert.match(skill, /이사/);
|
|
assert.match(skill, /주소/);
|
|
assert.match(skill, /쉬운 말/);
|
|
assert.match(skill, /사용자 결정/);
|
|
assert.match(articlesTemplate, /\{\{COMPANY_NAME\}\}/);
|
|
assert.match(articlesTemplate, /발기인/);
|
|
assert.match(articlesTemplate, /1주의 금액/);
|
|
assert.match(articlesTemplate, /3명 이상.*이사회/);
|
|
assert.match(articlesTemplate, /2명.*이사회/);
|
|
assert.match(articlesTemplate, /관할 등기소|전문가/);
|
|
assert.match(documentPackTemplate, /취임승낙서/);
|
|
assert.match(documentPackTemplate, /인감신고서/);
|
|
assert.match(documentPackTemplate, /등록면허세/);
|
|
assert.match(documentPackTemplate, /개인정보|민감정보/);
|
|
assert.match(documentPackTemplate, /레포.*커밋/);
|
|
|
|
assert.match(featureDoc, /법인등기 신청 컨설팅/);
|
|
assert.match(featureDoc, /표준 정관/);
|
|
assert.match(featureDoc, /등록면허세/);
|
|
assert.match(featureDoc, /과밀억제권역/);
|
|
assert.match(featureDoc, /조세특례제한법 제6조/);
|
|
assert.match(featureDoc, /지방세법 제28조/);
|
|
assert.match(featureDoc, /전자서명|세금 납부|등기 제출/);
|
|
assert.match(featureDoc, /개인정보|민감정보/);
|
|
assert.match(featureDoc, /인터넷등기소|온라인법인설립시스템/);
|
|
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("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"
|
|
);
|
|
|
|
});
|