k-skill/scripts/skill-docs.test.js
Jeffrey (Dongkyu) Kim 1e13cc10e1 Keep PR #19 CI stable without user Python site-packages
The validate job was failing because the KTX helper regression suite imported optional runtime dependencies directly from the module top level. GitHub Actions does not inherit the local user site-packages that made those imports succeed on the workstation, so the test suite died before it could exercise the helper logic.

This change makes the helper import-safe in minimal environments by deferring requests usage, providing lightweight fallbacks for optional Korail/Crypto imports during unit tests, and surfacing an explicit install command when the real runtime dependencies are actually needed. The docs now list pycryptodome alongside korail2, and the regression suite forces PYTHONNOUSERSITE=1 so CI keeps exercising the dependency-light path instead of accidentally relying on a developer machine.

Constraint: PR #19 must keep npm run ci green on GitHub Actions without assuming user-level Python packages
Constraint: The KTX helper still needs the real korail2 and pycryptodome packages for live reservation flows
Rejected: Installing ad-hoc Python packages in the CI workflow | hides the import-safety regression instead of fixing the helper/test contract
Rejected: Removing the Python regression suite from skill-docs coverage | would lose the guard on the train_id reservation flow
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep the KTX helper importable under PYTHONNOUSERSITE=1 and document every required runtime package in both the skill and install docs
Tested: PYTHONNOUSERSITE=1 python3 -m unittest discover -s scripts -p test_ktx_booking.py
Tested: node --test scripts/skill-docs.test.js
Tested: npm run ci
Not-tested: GitHub Actions validate rerun after push
2026-03-27 22:51:33 +09:00

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