k-skill/scripts/skill-docs.test.js
Jeffrey (Dongkyu) Kim ef2c69b81c Replace sops+age encryption with plain dotenv and agent-native credential resolution
Agent environments (OpenClaw, Claude Code, Codex) assume users delegate
credentials to the agent. sops+age added setup friction without real
security benefit since the agent decrypts on every call anyway.

New model: skills declare required env var names; how they are supplied
is up to the agent (own vault, shell env, or ~/.config/k-skill/secrets.env
as the default fallback with 0600 permissions).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 22:12:04 +09:00

891 lines
40 KiB
JavaScript

const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const childProcess = require("node:child_process");
const repoRoot = path.join(__dirname, "..");
function read(relativePath) {
return fs.readFileSync(path.join(repoRoot, relativePath), "utf8");
}
function readJson(relativePath) {
return JSON.parse(read(relativePath));
}
function escapeRegex(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function extractQuotedEntries(block, indent) {
return block
.split("\n")
.map((line) => line.match(new RegExp(`^ {${indent}}"([^"]+)":\\s*(.+?)(?:,)?$`)))
.filter(Boolean)
.map(([, key, value]) => [key, value.trim()]);
}
function findPrintedObjectBlock(doc, carrier) {
const block = [...doc.matchAll(/print\(json\.dumps\(\{\n([\s\S]*?)\n\}, ensure_ascii=False, indent=2\)\)/g)]
.map((match) => match[1])
.find((candidate) => candidate.includes(`"carrier": "${carrier}"`));
assert.ok(block, `expected ${carrier} normalized JSON example`);
return block;
}
function findRecentEventsBlock(doc, carrier) {
const block = [...doc.matchAll(/normalized_events = \[\n\s*\{\n([\s\S]*?)\n\s*\}\n\s*for [^\n]+ in events\n\]/g)]
.map((match) => match[1])
.find((candidate) => candidate.includes('"status_code":') === (carrier === "cj"));
assert.ok(block, `expected ${carrier} recent_events example`);
return block;
}
function findJsonFenceAfterLabel(doc, label) {
return JSON.parse(findJsonFenceTextAfterLabel(doc, label));
}
function findJsonFenceTextAfterLabel(doc, label) {
const escaped = escapeRegex(label);
const match = doc.match(new RegExp(`${escaped}[\\s\\S]*?\\\`\\\`\\\`json\\n([\\s\\S]*?)\\n\\\`\\\`\\\``));
assert.ok(match, `expected JSON example after "${label}"`);
return match[1];
}
function assertSampleProvenance(doc, sectionLabel, expected, docLabel) {
const escapedSectionLabel = escapeRegex(sectionLabel);
const escapedVerifiedAt = escapeRegex(expected.verified_at);
const escapedInvoice = escapeRegex(expected.invoice);
assert.match(
doc,
new RegExp(
`${escapedSectionLabel}[\\s\\S]*?아래 값은 ${escapedVerifiedAt} 기준 live smoke test\\(\\x60${escapedInvoice}\\x60\\)에서 확인한 정규화 결과다\\.\\n\\n\\\`\\\`\\\`json`,
),
`${docLabel} ${sectionLabel} provenance line must stay pinned to the verified smoke-test date and invoice`,
);
}
function assertSanitizedPublicOutput(output, label) {
const serialized = JSON.stringify(output);
assert.doesNotMatch(serialized, /\bTEL\b/i, `${label} must not leak TEL fragments`);
assert.doesNotMatch(
serialized,
/\d{2,4}[.\-]\d{3,4}[.\-]\d{4}/,
`${label} must not leak phone-number-like strings anywhere in the published sample`,
);
assert.doesNotMatch(serialized, /crgNm/, `${label} must not leak CJ assignee/source fields`);
assert.doesNotMatch(serialized, /sender/i, `${label} must not leak sender fields`);
assert.doesNotMatch(serialized, /receiver/i, `${label} must not leak receiver fields`);
assert.doesNotMatch(serialized, /delivered_to/i, `${label} must not leak delivered_to fields`);
}
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("hwp skill documents environment-aware routing and supported operations", () => {
const skillPath = path.join(repoRoot, "hwp", "SKILL.md");
assert.ok(fs.existsSync(skillPath), "expected hwp/SKILL.md to exist");
const skill = read(path.join("hwp", "SKILL.md"));
assert.match(skill, /^name: hwp$/m);
assert.match(skill, /@ohah\/hwpjs/);
assert.match(skill, /\bhwp-mcp\b/);
assert.match(skill, /Windows/i);
assert.match(skill, /JSON/i);
assert.match(skill, /Markdown/i);
assert.match(skill, /HTML/i);
assert.match(skill, /image/i);
assert.match(skill, /batch/i);
});
test("hwp skill documents inline image verification for markdown output", () => {
const skill = read(path.join("hwp", "SKILL.md"));
const featureDoc = read(path.join("docs", "features", "hwp.md"));
assert.match(skill, /hwpjs to-markdown document\.hwp -o output\.md --include-images/);
assert.match(skill, /Markdown:.*(data:|base64)/);
assert.match(skill, /--images-dir/);
assert.doesNotMatch(skill, /Markdown:.*이미지 경로 생성 여부 확인/);
assert.match(featureDoc, /--images-dir/);
});
test("repository docs advertise the hwp skill", () => {
const readme = read("README.md");
const install = read(path.join("docs", "install.md"));
const featureDocPath = path.join(repoRoot, "docs", "features", "hwp.md");
const featureDoc = read(path.join("docs", "features", "hwp.md"));
assert.ok(fs.existsSync(featureDocPath), "expected docs/features/hwp.md to exist");
assert.match(readme, /\| HWP 문서 처리 \|/);
assert.match(readme, /\[HWP 문서 처리\]\(docs\/features\/hwp\.md\)/);
assert.match(install, /--skill hwp/);
assert.match(featureDoc, /--include-images/);
assert.match(featureDoc, /(data:|base64)/);
assert.match(featureDoc, /Markdown 출력.*(data:|base64)/);
assert.doesNotMatch(featureDoc, /Markdown 출력.*이미지 (파일 )?경로 생성 여부 확인/);
});
test("repository docs advertise the kakaotalk-mac skill", () => {
const readme = read("README.md");
const install = read(path.join("docs", "install.md"));
const featureDocPath = path.join(repoRoot, "docs", "features", "kakaotalk-mac.md");
assert.ok(fs.existsSync(featureDocPath), "expected docs/features/kakaotalk-mac.md to exist");
assert.match(readme, /\| 카카오톡 Mac CLI \|/);
assert.match(readme, /\[카카오톡 Mac CLI\]\(docs\/features\/kakaotalk-mac\.md\)/);
assert.match(install, /--skill kakaotalk-mac/);
});
test("kakaotalk-mac skill documents safe macOS kakaocli usage", () => {
const skillPath = path.join(repoRoot, "kakaotalk-mac", "SKILL.md");
assert.ok(fs.existsSync(skillPath), "expected kakaotalk-mac/SKILL.md to exist");
const skill = read(path.join("kakaotalk-mac", "SKILL.md"));
assert.match(skill, /^name: kakaotalk-mac$/m);
assert.match(skill, /kakaocli/);
assert.match(skill, /macOS/i);
assert.match(skill, /KakaoTalk/i);
assert.match(skill, /Full Disk Access/i);
assert.match(skill, /Accessibility/i);
assert.match(skill, /--me/);
assert.match(skill, /confirm before sending/i);
});
test("repository docs advertise the KTX booking skill as supported", () => {
const readme = read("README.md");
const install = read(path.join("docs", "install.md"));
const featureDocPath = path.join(repoRoot, "docs", "features", "ktx-booking.md");
assert.ok(fs.existsSync(featureDocPath), "expected docs/features/ktx-booking.md to exist");
assert.match(readme, /\| KTX 예매 \|/);
assert.match(readme, /\[KTX 예매 가이드\]\(docs\/features\/ktx-booking\.md\)/);
assert.doesNotMatch(readme, /KTX 예매는 현재 작동하지 않습니다/);
assert.doesNotMatch(readme, /KTX 예매 \| 현재 작동하지 않음/);
assert.match(install, /--skill ktx-booking/);
});
test("ktx-booking docs document the helper-based live Korail workflow", () => {
const skillPath = path.join(repoRoot, "ktx-booking", "SKILL.md");
const helperPath = path.join(repoRoot, "scripts", "ktx_booking.py");
assert.ok(fs.existsSync(skillPath), "expected ktx-booking/SKILL.md to exist");
assert.ok(fs.existsSync(helperPath), "expected scripts/ktx_booking.py to exist");
const skill = read(path.join("ktx-booking", "SKILL.md"));
const featureDoc = read(path.join("docs", "features", "ktx-booking.md"));
const helper = read(path.join("scripts", "ktx_booking.py"));
assert.match(skill, /^name: ktx-booking$/m);
for (const doc of [skill, featureDoc]) {
assert.match(doc, /python3 scripts\/ktx_booking\.py search/);
assert.match(doc, /python3 scripts\/ktx_booking\.py reserve/);
assert.match(doc, /python3 scripts\/ktx_booking\.py reservations/);
assert.match(doc, /python3 scripts\/ktx_booking\.py cancel/);
assert.match(doc, /train_id/);
assert.match(doc, /--train-id/);
assert.match(doc, /--include-no-seats/);
assert.match(doc, /--include-waiting-list/);
assert.match(doc, /--try-waiting/);
assert.match(doc, /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 zipcode-search skill across the documented surfaces", () => {
const readme = read("README.md");
const install = read(path.join("docs", "install.md"));
const roadmap = read(path.join("docs", "roadmap.md"));
const sources = read(path.join("docs", "sources.md"));
const featureDocPath = path.join(repoRoot, "docs", "features", "zipcode-search.md");
assert.ok(fs.existsSync(featureDocPath), "expected docs/features/zipcode-search.md to exist");
assert.match(readme, /\| 우편번호 검색 \|/);
assert.match(readme, /\[우편번호 검색 가이드\]\(docs\/features\/zipcode-search\.md\)/);
assert.match(install, /--skill zipcode-search/);
assert.match(roadmap, /우편번호 검색/);
assert.match(sources, /우체국 도로명주소 검색: https:\/\/parcel\.epost\.go\.kr\/parcel\/comm\/zipcode\/comm_newzipcd_list\.jsp/);
});
test("zipcode-search docs lock the official ePost extraction flow and reliable transport example", () => {
const skillPath = path.join(repoRoot, "zipcode-search", "SKILL.md");
assert.ok(fs.existsSync(skillPath), "expected zipcode-search/SKILL.md to exist");
const skill = read(path.join("zipcode-search", "SKILL.md"));
const featureDoc = read(path.join("docs", "features", "zipcode-search.md"));
assert.match(skill, /^name: zipcode-search$/m);
for (const doc of [skill, featureDoc]) {
assert.match(doc, /parcel\.epost\.go\.kr\/parcel\/comm\/zipcode\/comm_newzipcd_list\.jsp/);
assert.match(doc, /sch_zipcode/);
assert.match(doc, /sch_address1/);
assert.match(doc, /sch_bdNm/);
assert.match(doc, /curl --http1\.1 --tls-max 1\.2/);
assert.match(doc, /--max-time/);
assert.match(doc, /"--retry",\s+"3"/);
assert.match(doc, /--retry-all-errors/);
assert.match(doc, /"--retry-delay",\s+"1"/);
assert.match(doc, /mktemp|임시 파일/);
assert.match(doc, /curl: \(23\)/);
assert.match(doc, /짧은 도로명 \+ 건물번호/);
assert.match(doc, /시\/군\/구 포함 전체 주소/);
assert.doesNotMatch(doc, /urllib\.request/);
assert.doesNotMatch(doc, /urlopen/);
}
assert.match(skill, /검색 결과가 없으면/i);
assert.doesNotMatch(skill, /timeout\s*=/);
assert.doesNotMatch(featureDoc, /timeout\s*=/);
assert.match(skill, /`curl` 자체 제한/);
assert.match(featureDoc, /프로토콜\/클라이언트 제약/i);
assert.match(featureDoc, /`curl` 자체 제한/);
});
test("repository docs advertise the delivery-tracking skill across the documented surfaces", () => {
const readme = read("README.md");
const install = read(path.join("docs", "install.md"));
const roadmap = read(path.join("docs", "roadmap.md"));
const sources = read(path.join("docs", "sources.md"));
const featureDocPath = path.join(repoRoot, "docs", "features", "delivery-tracking.md");
assert.ok(fs.existsSync(featureDocPath), "expected docs/features/delivery-tracking.md to exist");
assert.match(readme, /\| 택배 배송조회 \|/);
assert.match(readme, /\[택배 배송조회 가이드\]\(docs\/features\/delivery-tracking\.md\)/);
assert.match(install, /--skill delivery-tracking/);
assert.match(roadmap, /택배 배송조회 스킬 출시/);
assert.match(sources, /CJ대한통운 배송조회: https:\/\/www\.cjlogistics\.com\/ko\/tool\/parcel\/tracking/);
assert.match(sources, /우체국 배송조회: https:\/\/service\.epost\.go\.kr\/trace\.RetrieveRegiPrclDeliv\.postal\?sid1=/);
});
test("delivery-tracking skill documents official CJ and ePost flows with extension guidance", () => {
const skillPath = path.join(repoRoot, "delivery-tracking", "SKILL.md");
assert.ok(fs.existsSync(skillPath), "expected delivery-tracking/SKILL.md to exist");
const skill = read(path.join("delivery-tracking", "SKILL.md"));
const featureDoc = read(path.join("docs", "features", "delivery-tracking.md"));
assert.match(skill, /^name: delivery-tracking$/m);
for (const doc of [skill, featureDoc]) {
assert.match(doc, /https:\/\/www\.cjlogistics\.com\/ko\/tool\/parcel\/tracking/);
assert.match(doc, /tracking-detail/);
assert.match(doc, /paramInvcNo/);
assert.match(doc, /_csrf/);
assert.match(doc, /10자리 또는 12자리/);
assert.match(doc, /https:\/\/service\.epost\.go\.kr\/trace\.RetrieveRegiPrclDeliv\.postal\?sid1=/);
assert.match(doc, /trace\.RetrieveDomRigiTraceList\.comm/);
assert.match(doc, /sid1/);
assert.match(doc, /13자리/);
assert.match(doc, /curl --http1\.1 --tls-max 1\.2/);
assert.match(doc, /carrier adapter/i);
assert.match(doc, /다른 택배사/);
}
assert.match(skill, /1234567890/);
assert.match(skill, /1234567890123/);
assert.match(skill, /python3/);
assert.match(featureDoc, /JSON/);
assert.match(featureDoc, /HTML/);
});
test("delivery-tracking published examples lock a shared normalized non-PII schema", () => {
const skill = read(path.join("delivery-tracking", "SKILL.md"));
const featureDoc = read(path.join("docs", "features", "delivery-tracking.md"));
const expectedTopLevelEntries = {
cj: [
["carrier", '"cj"'],
["invoice", 'payload["parcelDetailResultMap"]["paramInvcNo"]'],
["status_code", 'latest.get("crgSt")'],
["status", 'status_map.get(latest.get("crgSt"), latest.get("scanNm") or "알수없음")'],
["timestamp", 'latest.get("dTime")'],
["location", 'latest.get("regBranNm")'],
["event_count", "len(events)"],
["recent_events", "normalized_events[-min(3, len(normalized_events)):]"],
],
epost: [
["carrier", '"epost"'],
["invoice", 'clean(summary.group("tracking"))'],
["status", 'clean(summary.group("result"))'],
["timestamp", 'latest_event["timestamp"] if latest_event else None'],
["location", 'latest_event["location"] if latest_event else None'],
["event_count", "len(normalized_events)"],
["recent_events", "normalized_events[-min(3, len(normalized_events)):]"],
],
};
const expectedRecentEventEntries = {
cj: [
["timestamp", 'event.get("dTime")'],
["location", 'event.get("regBranNm")'],
["status_code", 'event.get("crgSt")'],
["status", 'status_map.get(event.get("crgSt"), event.get("scanNm") or "알수없음")'],
],
epost: [
["timestamp", 'f"{day} {time_}"'],
["location", "clean_location(location)"],
["status", "clean(status)"],
],
};
assert.doesNotMatch(skill, /"message":\s*latest\.get\("crgNm"\)/);
assert.doesNotMatch(
featureDoc,
/print\(json\.dumps\(payload\["parcelDetailResultMap"\]\["resultList"\]\[-1\],\s*ensure_ascii=False,\s*indent=2\)\)/,
);
for (const doc of [skill, featureDoc]) {
assert.match(doc, /공통 포맷/);
assert.match(doc, /공통 결과 스키마/);
assert.match(doc, /최근 이벤트/);
assert.match(doc, /`carrier`/);
assert.match(doc, /`invoice`/);
assert.match(doc, /`status`/);
assert.match(doc, /`timestamp`/);
assert.match(doc, /`location`/);
assert.match(doc, /`event_count`/);
assert.match(doc, /`recent_events`/);
assert.match(doc, /최근 최대 3개 이벤트/);
assert.doesNotMatch(doc, /최근 3~5개 이벤트/);
assert.match(doc, /"invoice":\s*payload\["parcelDetailResultMap"\]\["paramInvcNo"\]/);
assert.match(doc, /"status_code":\s*latest\.get\("crgSt"\)/);
assert.match(doc, /"status":\s*status_map\.get\(latest\.get\("crgSt"\),/);
assert.match(doc, /"timestamp":\s*latest\.get\("dTime"\)/);
assert.match(doc, /"location":\s*latest\.get\("regBranNm"\)/);
assert.match(doc, /"event_count":\s*len\(events\)/);
assert.match(doc, /"recent_events":/);
assert.match(doc, /"invoice":\s*clean\(summary\.group/);
assert.match(doc, /"timestamp":\s*latest_event\["timestamp"\] if latest_event else None/);
assert.match(doc, /"location":\s*latest_event\["location"\] if latest_event else None/);
assert.match(doc, /"event_count":\s*len\(normalized_events\)/);
assert.match(doc, /"recent_events":\s*normalized_events\[-min\(3,\s*len\(normalized_events\)\):\]/);
assert.match(doc, /def clean_location\(raw: str\) -> str:/);
assert.match(doc, /TEL/);
assert.match(doc, /\\d\{2,4\}/);
assert.match(doc, /"location":\s*clean_location\(location\)/);
assert.doesNotMatch(doc, /"tracking_no":/);
assert.doesNotMatch(doc, /"latest_event_date":/);
assert.doesNotMatch(doc, /"latest_event_time":/);
assert.doesNotMatch(doc, /"latest_event_location":/);
assert.doesNotMatch(doc, /"delivered_to":/);
assert.doesNotMatch(doc, /"delivery_result":/);
}
for (const [label, doc] of [
["skill doc", skill],
["feature doc", featureDoc],
]) {
assert.deepEqual(
extractQuotedEntries(findPrintedObjectBlock(doc, "cj"), 4),
expectedTopLevelEntries.cj,
`${label} CJ example must keep the exact normalized top-level mapping`,
);
assert.deepEqual(
extractQuotedEntries(findPrintedObjectBlock(doc, "epost"), 4),
expectedTopLevelEntries.epost,
`${label} ePost example must keep the exact normalized top-level mapping`,
);
assert.deepEqual(
extractQuotedEntries(
findRecentEventsBlock(doc, "cj"),
8,
),
expectedRecentEventEntries.cj,
`${label} CJ recent_events entries must keep the exact normalized mapping`,
);
assert.deepEqual(
extractQuotedEntries(
findRecentEventsBlock(doc, "epost"),
8,
),
expectedRecentEventEntries.epost,
`${label} ePost recent_events entries must keep the exact normalized mapping`,
);
}
assert.doesNotMatch(skill, /"message":\s*latest\.get\("crgNm"\)/);
assert.doesNotMatch(featureDoc, /print\(\{\s*"tracking_no"/);
});
test("delivery-tracking docs publish aligned sample normalized outputs for both carriers", () => {
const expectedSamples = readJson(
path.join("scripts", "fixtures", "delivery-tracking-public-samples.json"),
);
const skill = read(path.join("delivery-tracking", "SKILL.md"));
const featureDoc = read(path.join("docs", "features", "delivery-tracking.md"));
const cjSkillOutput = findJsonFenceAfterLabel(skill, "CJ 공개 출력 예시");
const cjFeatureOutput = findJsonFenceAfterLabel(featureDoc, "CJ 공개 출력 예시");
const epostSkillOutput = findJsonFenceAfterLabel(skill, "우체국 공개 출력 예시");
const epostFeatureOutput = findJsonFenceAfterLabel(featureDoc, "우체국 공개 출력 예시");
for (const [docLabel, doc] of [
["skill doc", skill],
["feature doc", featureDoc],
]) {
for (const [carrier, label] of [
["cj", "CJ 공개 출력 예시"],
["epost", "우체국 공개 출력 예시"],
]) {
assert.equal(
findJsonFenceTextAfterLabel(doc, label),
JSON.stringify(expectedSamples[carrier], null, 2),
`${docLabel} ${carrier} sample JSON block must stay byte-for-byte aligned with the checked-in public fixture`,
);
}
}
assert.deepEqual(cjSkillOutput, cjFeatureOutput, "CJ sample output must stay aligned across docs");
assert.deepEqual(epostSkillOutput, epostFeatureOutput, "ePost sample output must stay aligned across docs");
assert.deepEqual(cjSkillOutput, expectedSamples.cj, "CJ sample output must stay pinned to the verified public fixture");
assert.deepEqual(epostSkillOutput, expectedSamples.epost, "ePost sample output must stay pinned to the verified public fixture");
assertSanitizedPublicOutput(cjSkillOutput, "CJ sample output");
assertSanitizedPublicOutput(epostSkillOutput, "ePost sample output");
});
test("delivery-tracking docs pin sample provenance to the verified smoke-test date and invoice", () => {
const expectedProvenance = readJson(
path.join("scripts", "fixtures", "delivery-tracking-public-provenance.json"),
);
const skill = read(path.join("delivery-tracking", "SKILL.md"));
const featureDoc = read(path.join("docs", "features", "delivery-tracking.md"));
for (const [docLabel, doc] of [
["skill doc", skill],
["feature doc", featureDoc],
]) {
assertSampleProvenance(doc, "CJ 공개 출력 예시", expectedProvenance.cj, docLabel);
assertSampleProvenance(doc, "우체국 공개 출력 예시", expectedProvenance.epost, docLabel);
}
});
test("repository docs advertise the daiso-product-search skill", () => {
const readme = read("README.md");
const install = read(path.join("docs", "install.md"));
const featureDocPath = path.join(repoRoot, "docs", "features", "daiso-product-search.md");
assert.ok(fs.existsSync(featureDocPath), "expected docs/features/daiso-product-search.md to exist");
assert.match(readme, /\| 다이소 상품 조회 \|/);
assert.match(readme, /\[다이소 상품 조회 가이드\]\(docs\/features\/daiso-product-search\.md\)/);
assert.match(install, /--skill daiso-product-search/);
});
test("daiso-product-search skill documents the official Daiso Mall lookup flow", () => {
const skillPath = path.join(repoRoot, "daiso-product-search", "SKILL.md");
const featureDoc = read(path.join("docs", "features", "daiso-product-search.md"));
assert.ok(fs.existsSync(skillPath), "expected daiso-product-search/SKILL.md to exist");
const skill = read(path.join("daiso-product-search", "SKILL.md"));
assert.match(skill, /^name: daiso-product-search$/m);
assert.match(skill, /다이소몰/i);
assert.match(skill, /매장명/);
assert.match(skill, /상품명|검색어/);
assert.match(skill, /https:\/\/www\.daisomall\.co\.kr\/api\/ms\/msg\/selStr/);
assert.match(skill, /https:\/\/www\.daisomall\.co\.kr\/ssn\/search\/SearchGoods/);
assert.match(skill, /https:\/\/www\.daisomall\.co\.kr\/api\/pd\/pdh\/selStrPkupStck/);
assert.match(skill, /공식 표면이 매장 내 진열 위치를 주지 않으면 재고 중심/);
assert.match(featureDoc, /SearchGoods/);
assert.match(featureDoc, /selStrPkupStck/);
});
test("daiso-product-search package exposes reusable store, product, and stock helpers", () => {
const pkg = require(path.join(repoRoot, "packages", "daiso-product-search", "src", "index.js"));
assert.equal(typeof pkg.searchStores, "function");
assert.equal(typeof pkg.searchProducts, "function");
assert.equal(typeof pkg.getStorePickupStock, "function");
assert.equal(typeof pkg.lookupStoreProductAvailability, "function");
});
test("daiso-product-search docs record the shipped feature and official sources", () => {
const roadmap = read(path.join("docs", "roadmap.md"));
const sources = read(path.join("docs", "sources.md"));
assert.match(roadmap, /다이소 상품 조회 스킬 출시/);
assert.match(sources, /https:\/\/www\.daisomall\.co\.kr\/api\/ms\/msg\/selStr/);
assert.match(sources, /https:\/\/www\.daisomall\.co\.kr\/ssn\/search\/SearchGoods/);
assert.match(sources, /https:\/\/www\.daisomall\.co\.kr\/api\/pd\/pdh\/selStrPkupStck/);
});
test("root pack:dry-run script covers all publishable workspaces", () => {
const packageJson = readJson("package.json");
assert.match(packageJson.scripts["pack:dry-run"], /workspace k-lotto/);
assert.match(packageJson.scripts["pack:dry-run"], /workspace daiso-product-search/);
assert.match(packageJson.scripts["pack:dry-run"], /workspace blue-ribbon-nearby/);
assert.match(packageJson.scripts["pack:dry-run"], /workspace kakao-bar-nearby/);
assert.match(packageJson.scripts["pack:dry-run"], /workspace kleague-results/);
});
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(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("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("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("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("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/);
});
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.1.0");
assert.equal(packageLock.packages["packages/toss-securities"].license, "MIT");
assert.equal(packageLock.packages["packages/toss-securities"].engines.node, ">=18");
});