mirror of
https://github.com/NomaDamas/k-skill.git
synced 2026-09-14 16:37:18 +08:00
d0ffe5219d
fix(korean-privacy-terms): install.sh 0바이트 템플릿 경고 + 한글 약관 누락 문서화 (#660)
4887 lines
232 KiB
JavaScript
4887 lines
232 KiB
JavaScript
// allow: SIZE_OK - Repository-wide documentation invariants remain one catalog-oriented suite.
|
|
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 { resolvePython, venvPython } = require("./ci-run");
|
|
|
|
const repoRoot = path.join(__dirname, "..");
|
|
|
|
function read(relativePath) {
|
|
// CLI-managed skills keep their authored content in skill.json (frontmatter)
|
|
// + instruction.md; the committed SKILL.md is a generated CLI stub. Content
|
|
// tests keep asserting against the logical document.
|
|
const skillMatch = relativePath.match(/^([a-z0-9-]+)[\\/]SKILL\.md$/);
|
|
if (skillMatch) {
|
|
const manifestPath = path.join(repoRoot, skillMatch[1], "skill.json");
|
|
if (fs.existsSync(manifestPath)) {
|
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
const instruction = fs.readFileSync(path.join(repoRoot, skillMatch[1], "instruction.md"), "utf8");
|
|
return `---\n${manifest.frontmatter.trim()}\n---\n\n${instruction}`;
|
|
}
|
|
}
|
|
|
|
return fs.readFileSync(path.join(repoRoot, relativePath), "utf8");
|
|
}
|
|
|
|
function readRaw(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 trackedTextFiles() {
|
|
return childProcess
|
|
.execFileSync("git", ["ls-files"], { cwd: repoRoot, encoding: "utf8" })
|
|
.split("\n")
|
|
.filter(Boolean)
|
|
.filter((relativePath) => fs.existsSync(path.join(repoRoot, relativePath)))
|
|
.filter((relativePath) => {
|
|
if (relativePath.startsWith(".omo/") || relativePath.startsWith(".private/")) return false;
|
|
if (relativePath.includes("/fixtures/")) return false;
|
|
return (
|
|
relativePath.endsWith(".md") ||
|
|
relativePath.endsWith(".mdx") ||
|
|
relativePath.endsWith(".txt") ||
|
|
relativePath.endsWith(".sh") ||
|
|
relativePath.endsWith(".js") ||
|
|
relativePath.endsWith(".yml") ||
|
|
relativePath.endsWith(".yaml")
|
|
);
|
|
});
|
|
}
|
|
|
|
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"));
|
|
const { listNodeTestFiles, toPosix } = require("./ci-paths");
|
|
|
|
assert.equal(packageJson.scripts.test, "node scripts/run-tests.js");
|
|
assert.ok(
|
|
listNodeTestFiles().some((file) => toPosix(file) === "scripts/skill-docs.test.js"),
|
|
"run-tests.js should glob scripts/skill-docs.test.js",
|
|
);
|
|
});
|
|
|
|
test("root CI glob runners pick up helper tests without a package.json hand list", () => {
|
|
const packageJson = readJson("package.json");
|
|
const {
|
|
listJsSyntaxCheckFiles,
|
|
listPythonCompileFiles,
|
|
listPythonTestJobs,
|
|
listRootPythonTestModules,
|
|
toPosix,
|
|
} = require("./ci-paths");
|
|
|
|
assert.equal(packageJson.scripts.lint, "node scripts/run-lint.js");
|
|
assert.equal(packageJson.scripts.test, "node scripts/run-tests.js");
|
|
assert.equal(packageJson.scripts["pack:dry-run"], "node scripts/pack-dry-run.js");
|
|
assert.equal(readJson(path.join("packages", "k-skill-proxy", "package.json")).scripts.lint, "node scripts/check-js.js");
|
|
|
|
const jsFiles = listJsSyntaxCheckFiles().map(toPosix);
|
|
assert.ok(jsFiles.includes("scripts/ci-paths.js"));
|
|
assert.ok(jsFiles.includes("lck-analytics/scripts/sync-oracle.js"));
|
|
|
|
const pyFiles = listPythonCompileFiles().map(toPosix);
|
|
assert.ok(pyFiles.includes("scripts/seoul_density.py"));
|
|
assert.ok(pyFiles.includes("gov-overseas-trip-report/scripts/gov_overseas_trip_report.py"));
|
|
assert.ok(pyFiles.includes("popbill/scripts/popbill_cli.py"));
|
|
assert.ok(!pyFiles.some((file) => file.startsWith("packages/k-skill-cli/skills/")));
|
|
|
|
const rootModules = listRootPythonTestModules();
|
|
assert.ok(rootModules.includes("scripts.test_seoul_density"));
|
|
assert.ok(rootModules.includes("scripts.test_myrealtrip_mcp"));
|
|
assert.ok(!rootModules.includes("scripts.test_store_longevity_mirror"));
|
|
|
|
const jobLabels = listPythonTestJobs().map((job) => job.label).join("\n");
|
|
assert.match(jobLabels, /gov-overseas-trip-report\/tests/);
|
|
assert.match(jobLabels, /popbill\/tests\/run_tests\.py/);
|
|
assert.match(jobLabels, /jobkorea-talent-search\/scripts/);
|
|
assert.match(jobLabels, /myrealtrip-search\/scripts/);
|
|
assert.doesNotMatch(jobLabels, /k-skill-cli\/skills/);
|
|
assert.doesNotMatch(jobLabels, /test_store_longevity_mirror/);
|
|
assert.equal(
|
|
packageJson.scripts["test:store-longevity-mirror"],
|
|
"node scripts/run-store-longevity-mirror-test.js",
|
|
);
|
|
});
|
|
|
|
test("CI path discovery surfaces directory read failures", () => {
|
|
const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "k-skill-ci-paths-"));
|
|
const filePath = path.join(fixtureRoot, "not-a-directory");
|
|
fs.writeFileSync(filePath, "fixture");
|
|
const { walkFiles } = require("./ci-paths");
|
|
|
|
assert.throws(
|
|
() => walkFiles(filePath, () => true),
|
|
(error) => error?.code === "ENOTDIR",
|
|
);
|
|
});
|
|
|
|
test("Node syntax checks reject an invalid file after a valid file", () => {
|
|
const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "k-skill-node-check-"));
|
|
const validPath = path.join(fixtureRoot, "valid.js");
|
|
const invalidPath = path.join(fixtureRoot, "invalid.js");
|
|
fs.writeFileSync(validPath, "\"use strict\";\n");
|
|
fs.writeFileSync(invalidPath, "const broken = ;\n");
|
|
|
|
const modulePath = JSON.stringify(path.join(repoRoot, "scripts", "ci-run.js"));
|
|
const result = childProcess.spawnSync(
|
|
process.execPath,
|
|
[
|
|
"-e",
|
|
`require(${modulePath}).runNodeSyntaxChecks(${JSON.stringify([validPath, invalidPath])})`,
|
|
],
|
|
{ cwd: repoRoot, encoding: "utf8" },
|
|
);
|
|
|
|
assert.notEqual(result.status, 0);
|
|
assert.match(result.stderr, /SyntaxError/);
|
|
});
|
|
|
|
// Skills that have migrated to the @nomadamas/k-skill CLI adapter keep their
|
|
// source in skill.json/instruction.md and publish a generated stub SKILL.md.
|
|
function cliManagedSkills() {
|
|
return fs
|
|
.readdirSync(repoRoot, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => entry.name)
|
|
.filter((name) => fs.existsSync(path.join(repoRoot, name, "skill.json")))
|
|
.sort();
|
|
}
|
|
|
|
test("every top-level skill is a generated CLI stub", () => {
|
|
const skillDirs = fs
|
|
.readdirSync(repoRoot, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => entry.name)
|
|
.filter((name) => fs.existsSync(path.join(repoRoot, name, "SKILL.md")))
|
|
.sort();
|
|
|
|
assert.equal(skillDirs.length, 125);
|
|
|
|
for (const skillName of skillDirs) {
|
|
const skill = readRaw(path.join(skillName, "SKILL.md"));
|
|
assert.match(skill, /k-skill:cli-stub/, `${skillName} must be a generated CLI stub`);
|
|
assert.match(
|
|
skill,
|
|
new RegExp(`npx -y @nomadamas/k-skill@0 instruct ${escapeRegex(skillName)}`),
|
|
`${skillName} stub must invoke the pinned-major CLI`,
|
|
);
|
|
assert.doesNotMatch(
|
|
skill,
|
|
/^## Runtime contract \(required\)$/m,
|
|
`${skillName} must not embed the retired portable runtime contract; CLI templates own that`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("CLI-managed skills keep helpers in the skill directory instead of root bundle[]", () => {
|
|
for (const skillName of cliManagedSkills()) {
|
|
const manifest = JSON.parse(read(path.join(skillName, "skill.json")));
|
|
assert.equal(
|
|
manifest.bundle,
|
|
undefined,
|
|
`${skillName}/skill.json must not use legacy root bundle[]; put helpers in ${skillName}/scripts/`,
|
|
);
|
|
}
|
|
|
|
assert.ok(fs.existsSync(path.join(repoRoot, "railway-timetable", "scripts", "railway_timetable.py")));
|
|
assert.ok(fs.existsSync(path.join(repoRoot, "fine-dust-location", "scripts", "fine_dust.py")));
|
|
assert.ok(fs.existsSync(path.join(repoRoot, "k-skill-setup", "scripts", "check-setup.sh")));
|
|
});
|
|
|
|
test("every CLI-managed skill has a feature guide advertised from README", () => {
|
|
const readme = read("README.md");
|
|
const aliases = {
|
|
"k-skill-setup": "docs/setup.md",
|
|
};
|
|
|
|
for (const skillName of cliManagedSkills()) {
|
|
const alias = aliases[skillName];
|
|
const relative = alias || path.posix.join("docs", "features", `${skillName}.md`);
|
|
assert.ok(
|
|
fs.existsSync(path.join(repoRoot, ...relative.split("/"))),
|
|
alias
|
|
? `${skillName} may omit docs/features only when ${alias} exists`
|
|
: `expected docs/features/${skillName}.md`,
|
|
);
|
|
assert.ok(
|
|
readme.includes(`](${relative})`),
|
|
`README should link ${relative} for ${skillName}`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("CLI-managed skills keep source, stub safety floor, and bundled copies aligned", () => {
|
|
const cliManaged = cliManagedSkills();
|
|
|
|
assert.ok(cliManaged.length >= 5, "expected at least the five pilot skills to be CLI-managed");
|
|
|
|
const { SAFETY_FLOOR } = require("./generate-skill-stubs.js");
|
|
|
|
for (const skillName of cliManaged) {
|
|
const stub = readRaw(path.join(skillName, "SKILL.md"));
|
|
const manifest = readJson(path.join(skillName, "skill.json"));
|
|
|
|
assert.equal(manifest.name, skillName, `${skillName}/skill.json name must match the directory`);
|
|
assert.ok(
|
|
fs.existsSync(path.join(repoRoot, skillName, "instruction.md")),
|
|
`${skillName} must keep its instruction.md source`,
|
|
);
|
|
assert.ok(stub.includes(SAFETY_FLOOR), `${skillName} stub must keep the static safety floor`);
|
|
assert.match(stub, /clarify|approval/, `${skillName} stub must state the approval boundary`);
|
|
|
|
for (const relative of ["skill.json", "instruction.md"]) {
|
|
const source = readRaw(path.join(skillName, relative));
|
|
const bundled = readRaw(path.join("packages", "k-skill-cli", "skills", skillName, relative));
|
|
|
|
assert.equal(bundled, source, `packages/k-skill-cli/skills/${skillName}/${relative} must match the source`);
|
|
}
|
|
}
|
|
|
|
// Staleness gates: regenerating and re-syncing must be no-ops.
|
|
childProcess.execFileSync("node", [path.join(__dirname, "generate-skill-stubs.js"), "--check"], { cwd: repoRoot });
|
|
childProcess.execFileSync(
|
|
"node",
|
|
[path.join(__dirname, "migrate-cli-asset-instructions.js"), "--check"],
|
|
{ cwd: repoRoot },
|
|
);
|
|
childProcess.execFileSync("node", [path.join(__dirname, "sync-cli-skills.js"), "--check"], { cwd: repoRoot });
|
|
});
|
|
|
|
test("trademark-reviewed skills publish the combined legal disclaimer", () => {
|
|
const reviewedSkills = cliManagedSkills().filter((skillName) =>
|
|
fs.existsSync(path.join(repoRoot, skillName, "references", "DISCLAIMER.md")),
|
|
);
|
|
|
|
assert.equal(reviewedSkills.length, 43, "expected the complete trademark-reviewed skill set");
|
|
|
|
for (const skillName of reviewedSkills) {
|
|
const disclaimerPath = path.join(skillName, "references", "DISCLAIMER.md");
|
|
const bundledDisclaimerPath = path.join(
|
|
"packages",
|
|
"k-skill-cli",
|
|
"skills",
|
|
skillName,
|
|
"references",
|
|
"DISCLAIMER.md",
|
|
);
|
|
|
|
assert.ok(fs.existsSync(path.join(repoRoot, disclaimerPath)), `${skillName} must publish DISCLAIMER.md`);
|
|
|
|
const disclaimer = readRaw(disclaimerPath);
|
|
const stub = readRaw(path.join(skillName, "SKILL.md"));
|
|
|
|
assert.ok(
|
|
fs.existsSync(path.join(repoRoot, skillName, "references", "TRADEMARK-LEGAL-STATEMENT.md")),
|
|
`${skillName} must keep a skill-specific trademark statement`,
|
|
);
|
|
assert.match(disclaimer, /공식 기능|공식 지원/);
|
|
assert.match(disclaimer, /제휴|협업/);
|
|
assert.match(disclaimer, /기능|조회 대상|호환 대상/);
|
|
assert.match(disclaimer, /2005도1637/);
|
|
assert.match(disclaimer, /2021도1533/);
|
|
assert.match(disclaimer, /정보통신망법 제48조/);
|
|
assert.match(disclaimer, /저작권법 제93조/);
|
|
assert.match(disclaimer, /개인.*조회/);
|
|
assert.match(disclaimer, /조직적.*크롤링|대량.*수집/);
|
|
assert.match(disclaimer, /영업.*방해|서비스.*(?:장애|방해)/);
|
|
assert.match(disclaimer, /접근통제|차단.*회피/);
|
|
|
|
assert.equal(
|
|
readRaw(bundledDisclaimerPath),
|
|
disclaimer,
|
|
`packages/k-skill-cli/skills/${skillName}/references/DISCLAIMER.md must match the source`,
|
|
);
|
|
assert.match(stub, /^## Legal disclaimer \(required\)$/m);
|
|
assert.match(
|
|
stub,
|
|
new RegExp(`read ${escapeRegex(skillName)} references/DISCLAIMER\\.md`),
|
|
`${skillName} stub must require reading its disclaimer`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("actionable skills publish a Dolshoi action path", () => {
|
|
const cliManaged = new Set(cliManagedSkills());
|
|
const actionableSkills = [
|
|
"bunjang-search",
|
|
"corporate-registration-consulting",
|
|
"coupang-product-search",
|
|
"court-auction-notice-search",
|
|
"court-payment-order-assistant",
|
|
"d2b-notice-search",
|
|
"daangn-cars-search",
|
|
"daangn-jobs-search",
|
|
"daangn-realty-search",
|
|
"daangn-used-goods-search",
|
|
"daiso-product-search",
|
|
"danawa-price-search",
|
|
"donation-place-search",
|
|
"ev-charger-nearby",
|
|
"ev-subsidy-status",
|
|
"express-bus-booking",
|
|
"flight-ticket-search",
|
|
"foresttrip-vacancy",
|
|
"g2b-order-plan-search",
|
|
"hola-poke-yeoksam",
|
|
"intercity-bus-booking",
|
|
"iros-registry-automation",
|
|
"job-posting-match",
|
|
"jobkorea-talent-search",
|
|
"kakao-bar-nearby",
|
|
"kakao-map",
|
|
"kakaotalk-mac",
|
|
"keris-academic-search",
|
|
"kopis-performance-search",
|
|
"korean-cinema-search",
|
|
"korean-jangbu-for",
|
|
"korean-marathon-schedule",
|
|
"korean-scholarship-search",
|
|
"kr-whois-lookup",
|
|
"kstartup-search",
|
|
"lh-notice-search",
|
|
"library-book-search",
|
|
"market-kurly-search",
|
|
"myrealtrip-search",
|
|
"naver-ad-performance",
|
|
"naver-shopping-search",
|
|
"nhis-care-checkup-search",
|
|
"nts-business-registration",
|
|
"nts-tax-delinquency",
|
|
"ohou-today-deal",
|
|
"olive-young-search",
|
|
"popbill",
|
|
"s2b-notice-search",
|
|
"saramin-talent-search",
|
|
"sh-notice-search",
|
|
"subway-lost-property",
|
|
"ticket-availability",
|
|
"toss-investment",
|
|
];
|
|
|
|
for (const skillName of actionableSkills) {
|
|
if (cliManaged.has(skillName)) continue; // action path now lives in the CLI-assembled instructions
|
|
|
|
const skill = read(path.join(skillName, "SKILL.md"));
|
|
|
|
assert.match(skill, /^## Dolshoi action path$/m, `${skillName} should document its Dolshoi action path`);
|
|
assert.match(skill, /clarify/, `${skillName} should document irreversible-action approval`);
|
|
}
|
|
});
|
|
|
|
test("runtime action audit covers every top-level skill exactly once", () => {
|
|
const audit = findSection(read("docs/runtime-action-audit.md"), "## Complete catalog");
|
|
const skillDirs = fs
|
|
.readdirSync(repoRoot, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => entry.name)
|
|
.filter((name) => fs.existsSync(path.join(repoRoot, name, "SKILL.md")))
|
|
.sort();
|
|
|
|
for (const skillName of skillDirs) {
|
|
const matches = audit.match(new RegExp(`^\\| \\\`${escapeRegex(skillName)}\\\` \\|`, "gm")) ?? [];
|
|
|
|
assert.equal(matches.length, 1, `${skillName} must appear exactly once in the runtime action audit`);
|
|
}
|
|
});
|
|
|
|
test("runtime action audit modes and totals match skill manifests", () => {
|
|
const audit = read("docs/runtime-action-audit.md");
|
|
const rows = new Map(
|
|
[...audit.matchAll(/^\| `([^`]+)` \| `([^`]+)` \|/gm)].map((match) => [match[1], match[2]]),
|
|
);
|
|
const modes = [
|
|
"commerce",
|
|
"booking",
|
|
"submission",
|
|
"recruiting",
|
|
"account",
|
|
"legal",
|
|
"operations",
|
|
"local",
|
|
"lookup",
|
|
];
|
|
const counts = Object.fromEntries(modes.map((mode) => [mode, 0]));
|
|
|
|
for (const [skillName, auditMode] of rows) {
|
|
const manifest = readJson(path.join(skillName, "skill.json"));
|
|
const manifestModes = manifest.profiles
|
|
.map((profile) => profile.replace(/^action:/, ""))
|
|
.filter((profile) => modes.includes(profile));
|
|
|
|
assert.deepEqual(manifestModes, [auditMode], `${skillName} audit mode must match its manifest`);
|
|
counts[auditMode] += 1;
|
|
}
|
|
|
|
assert.match(audit, new RegExp(`top-level \`SKILL\\.md\` ${rows.size}개`));
|
|
for (const mode of modes) {
|
|
assert.match(audit, new RegExp(`\\*\\*${mode}\\*\\* \\(${counts[mode]}\\):`));
|
|
}
|
|
});
|
|
|
|
test("README advertises OpenClaw among the supported coding agents", () => {
|
|
const readme = read("README.md");
|
|
|
|
assert.match(
|
|
readme,
|
|
/Claude Code, Codex, OpenCode, OpenClaw\/ClawHub 등 각종 코딩 에이전트 지원합니다\./,
|
|
);
|
|
});
|
|
|
|
test("repository publishes Korean contribution guidance for external contributors", () => {
|
|
const contributingPath = path.join(repoRoot, "CONTRIBUTING.md");
|
|
|
|
assert.ok(fs.existsSync(contributingPath), "expected CONTRIBUTING.md to exist");
|
|
|
|
const contributing = read("CONTRIBUTING.md");
|
|
|
|
assert.match(contributing, /^# 기여 가이드$/m);
|
|
assert.match(contributing, /PR 코멘트, 이슈, 리뷰 등 모든 소통은 한국어로 진행/);
|
|
assert.match(contributing, /PR의 대상 브랜치는 반드시 `dev`/);
|
|
assert.match(contributing, /`main` 브랜치로 PR을 만들 수 있는 사람은 `@vkehfdl1`뿐/);
|
|
assert.match(contributing, /스킬을 추가하거나 변경할 때는 관련 기능 문서와 `README\.md`의 표/);
|
|
assert.match(contributing, /npm 패키지를 수정할 때는 Changesets/);
|
|
assert.match(contributing, /Changeset 파일의 존재 여부를 테스트로 검증하지 않는다/);
|
|
assert.match(contributing, /`package\.json`과 `package-lock\.json`의 `version` 필드를 테스트에서 고정하지 않는다/);
|
|
assert.match(contributing, /`name`, `license`, `engines\.node`, workspace link metadata/);
|
|
assert.match(contributing, /현재 구현이 registry token 기반인 경우에도 신규 또는 재설계 흐름은 trusted publishing\/OIDC를 우선/);
|
|
assert.match(contributing, /신규 proxy route는 upstream이 API key를 요구하는 무료 API인 경우에만 `k-skill-proxy` 경유를 검토/);
|
|
assert.match(contributing, /인증 없이 동작하는 공개 read-only endpoint는 기본적으로 사용자 머신에서 직접 호출/);
|
|
assert.doesNotMatch(contributing, /무료 API이고 1일 리미트가 충분한 경우/);
|
|
assert.match(contributing, /유료 API/);
|
|
assert.match(contributing, /`k-skill-proxy`를 타지 않도록 설계/);
|
|
assert.match(contributing, /릴리스나 패키징 관련 변경은 `npm run ci`/);
|
|
assert.match(contributing, /scripts\/run-\*\.js/);
|
|
assert.match(contributing, /`~\/\.claude\/skills\/<skill-name>`/);
|
|
assert.match(contributing, /`~\/\.agents\/skills\/<skill-name>`/);
|
|
assert.match(contributing, /gpu01/);
|
|
assert.match(contributing, /systemd/);
|
|
assert.match(contributing, /gpu01 app directory의 `\.env`/);
|
|
assert.match(contributing, /`main`에 머지된 뒤에만 프로덕션에 반영/);
|
|
});
|
|
|
|
test("README links to the contribution guide", () => {
|
|
const readme = read("README.md");
|
|
|
|
assert.match(readme, /\[기여 가이드\]\(CONTRIBUTING\.md\)/);
|
|
});
|
|
|
|
|
|
test("repository docs advertise Daangn read-only search skills", () => {
|
|
const readme = read("README.md");
|
|
const sources = read(path.join("docs", "sources.md"));
|
|
const skills = [
|
|
["daangn-used-goods-search", "당근 중고거래 검색"],
|
|
["daangn-realty-search", "당근부동산 검색"],
|
|
["daangn-jobs-search", "당근알바 검색"],
|
|
["daangn-cars-search", "당근중고차 검색"],
|
|
];
|
|
|
|
assert.match(sources, /www\.daangn\.com\/kr\/api\/v1\/regions\/keyword/);
|
|
assert.match(sources, /realty\.daangn\.com\/articles/);
|
|
|
|
for (const [skillName, label] of skills) {
|
|
const skill = read(path.join(skillName, "SKILL.md"));
|
|
const featureDocPath = path.join(repoRoot, "docs", "features", `${skillName}.md`);
|
|
const featureDoc = read(path.join("docs", "features", `${skillName}.md`));
|
|
|
|
assert.ok(fs.existsSync(featureDocPath), `expected docs/features/${skillName}.md to exist`);
|
|
assert.ok(
|
|
readme.includes(`| [${label}](docs/features/${skillName}.md) | \`${skillName}\``),
|
|
`README should advertise ${skillName}`,
|
|
);
|
|
assert.ok(
|
|
readme.includes(`](docs/features/${skillName}.md)`),
|
|
`README should link docs/features/${skillName}.md`,
|
|
);
|
|
assert.match(skill, /kr\/api\/v1\/regions\/keyword/);
|
|
assert.match(featureDoc, /kr\/api\/v1\/regions\/keyword/);
|
|
assert.match(featureDoc, /(로그인|채팅|구매|문의|지원).*자동화/);
|
|
}
|
|
});
|
|
|
|
test("hwp skill documents kordoc-based parsing and supported operations", () => {
|
|
const skillPath = path.join(repoRoot, "hwp", "SKILL.md");
|
|
|
|
assert.ok(fs.existsSync(skillPath), "expected hwp/SKILL.md to exist");
|
|
|
|
const skill = read(path.join("hwp", "SKILL.md"));
|
|
|
|
assert.match(skill, /^name: hwp$/m);
|
|
assert.match(skill, /\bkordoc\b/);
|
|
assert.doesNotMatch(skill, /@ohah\/hwpjs/);
|
|
assert.doesNotMatch(skill, /\bhwp-mcp\b/);
|
|
assert.match(skill, /JSON/i);
|
|
assert.match(skill, /Markdown/i);
|
|
assert.match(skill, /image/i);
|
|
assert.match(skill, /(batch|배치)/i);
|
|
assert.match(skill, /HWPX/i);
|
|
assert.match(skill, /(역변환|되돌려)/);
|
|
assert.match(skill, /(비교|compare)/i);
|
|
assert.match(skill, /pdfjs-dist/);
|
|
assert.match(skill, /(extractFormFields|양식 필드)/);
|
|
assert.doesNotMatch(skill, /fillForm/);
|
|
assert.doesNotMatch(skill, /kordoc fill/);
|
|
assert.doesNotMatch(skill, /kordoc mcp/);
|
|
});
|
|
|
|
test("hwp docs match the published kordoc install and runtime contract", () => {
|
|
const skill = read(path.join("hwp", "SKILL.md"));
|
|
const featureDoc = read(path.join("docs", "features", "hwp.md"));
|
|
const install = read(path.join("docs", "install.md"));
|
|
const readme = read("README.md");
|
|
const sources = read(path.join("docs", "sources.md"));
|
|
|
|
assert.match(skill, /npx --yes --package kordoc --package pdfjs-dist kordoc .* -o .*\.md/);
|
|
assert.match(skill, /markdownToHwpx/);
|
|
assert.match(skill, /extractFormFields/);
|
|
assert.match(skill, /npm init -y/);
|
|
assert.match(skill, /npm install kordoc pdfjs-dist/);
|
|
assert.doesNotMatch(skill, /^\s*npx kordoc\b/m);
|
|
assert.doesNotMatch(skill, /export NODE_PATH/);
|
|
assert.match(featureDoc, /npx --yes --package kordoc --package pdfjs-dist kordoc .* --format json/);
|
|
assert.match(featureDoc, /markdownToHwpx/);
|
|
assert.match(featureDoc, /(extractFormFields|양식 필드)/);
|
|
assert.match(featureDoc, /npx --yes --package kordoc --package pdfjs-dist kordoc watch/);
|
|
assert.match(featureDoc, /npm init -y/);
|
|
assert.match(featureDoc, /npm install kordoc pdfjs-dist/);
|
|
assert.doesNotMatch(featureDoc, /^\s*npx kordoc\b/m);
|
|
assert.doesNotMatch(featureDoc, /export NODE_PATH/);
|
|
assert.match(featureDoc, /npm install -g kordoc pdfjs-dist/);
|
|
assert.doesNotMatch(featureDoc, /선택적으로 `pdfjs-dist`/);
|
|
assert.doesNotMatch(featureDoc, /kordoc fill/);
|
|
assert.doesNotMatch(featureDoc, /kordoc mcp/);
|
|
assert.doesNotMatch(featureDoc, /fillForm/);
|
|
assert.match(install, /npm install -g kordoc pdfjs-dist /);
|
|
assert.match(install, /HWP Node API 예시는 전역 `NODE_PATH` 대신 로컬 프로젝트에 `npm install kordoc pdfjs-dist` 후 실행/);
|
|
assert.match(install, /`kordoc` CLI를 일회성으로만 쓸 때는 `npx --yes --package kordoc --package pdfjs-dist kordoc \.\.\.` 형태를 사용한다\./);
|
|
assert.match(readme, /\| \[HWP 문서 조회\/변환\]\(docs\/features\/hwp\.md\) \| .*양식 필드 추출.*Markdown→HWPX 역변환/);
|
|
assert.doesNotMatch(readme, /\| \[HWP 문서 조회\/변환\]\(docs\/features\/hwp\.md\) \| .*양식 채우기/);
|
|
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 문서 조회\/변환\]\(docs\/features\/hwp\.md\) \|/);
|
|
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 아카이브 검색\]\(docs\/features\/kakaotalk-mac\.md\) \|/);
|
|
assert.match(readme, /\[카카오톡 Mac 아카이브 검색\]\(docs\/features\/kakaotalk-mac\.md\)/);
|
|
assert.match(install, /--skill kakaotalk-mac/);
|
|
});
|
|
|
|
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, /\| \[근처 공중화장실 찾기\]\(docs\/features\/public-restroom-nearby\.md\) \|/);
|
|
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("repository docs advertise the animal pharmacy search skill", () => {
|
|
const readme = read("README.md");
|
|
const install = read(path.join("docs", "install.md"));
|
|
const sources = read(path.join("docs", "sources.md"));
|
|
const featureDocPath = path.join(repoRoot, "docs", "features", "animal-pharmacy-search.md");
|
|
const skillPath = path.join(repoRoot, "animal-pharmacy-search", "SKILL.md");
|
|
|
|
assert.ok(fs.existsSync(featureDocPath), "expected docs/features/animal-pharmacy-search.md to exist");
|
|
assert.ok(fs.existsSync(skillPath), "expected animal-pharmacy-search/SKILL.md to exist");
|
|
assert.match(readme, /\| \[동물약국·동물용의약품 취급 약국 조회\]\(docs\/features\/animal-pharmacy-search\.md\) \|/);
|
|
assert.match(
|
|
readme,
|
|
/\[동물약국·동물용의약품 취급 약국 조회\]\(docs\/features\/animal-pharmacy-search\.md\)/,
|
|
);
|
|
assert.match(install, /--skill animal-pharmacy-search/);
|
|
assert.match(sources, /https:\/\/hkmedi\.co\.kr\/pharmacy-mcp/);
|
|
});
|
|
|
|
test("animal pharmacy docs preserve the private distributor provenance boundary", () => {
|
|
const skill = read(path.join("animal-pharmacy-search", "SKILL.md"));
|
|
const featureDoc = read(path.join("docs", "features", "animal-pharmacy-search.md"));
|
|
|
|
for (const doc of [skill, featureDoc]) {
|
|
assert.match(doc, /홍익메디케어/);
|
|
assert.match(doc, /최근 6개월/);
|
|
assert.match(doc, /구매 이력|구매이력/);
|
|
assert.match(doc, /재고.*보장|보장.*재고/);
|
|
assert.match(doc, /전화.*확인|확인.*전화/);
|
|
assert.match(doc, /민간|private/i);
|
|
assert.match(doc, /진단|처방/);
|
|
assert.match(doc, /find_animal_pharmacies/);
|
|
assert.match(doc, /search_product/);
|
|
assert.match(doc, /find_pharmacies_by_product/);
|
|
}
|
|
});
|
|
|
|
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 경기 분석\]\(docs\/features\/lck-analytics\.md\) \|/);
|
|
assert.match(readme, /\[LCK 경기 분석\]\(docs\/features\/lck-analytics\.md\)/);
|
|
assert.match(install, /--skill lck-analytics/);
|
|
assert.match(install, /npm install -g .*lck-analytics/);
|
|
});
|
|
|
|
test("lck-analytics docs and skill credit the original author and reference repo", () => {
|
|
const skill = read(path.join("lck-analytics", "SKILL.md"));
|
|
const featureDoc = read(path.join("docs", "features", "lck-analytics.md"));
|
|
const packageReadme = read(path.join("packages", "lck-analytics", "README.md"));
|
|
const sources = read(path.join("docs", "sources.md"));
|
|
|
|
for (const doc of [skill, featureDoc, packageReadme]) {
|
|
assert.match(doc, /jerjangmin/);
|
|
assert.match(doc, /https:\/\/github\.com\/jerjangmin\/share\/tree\/main\/SKILL\/lck-analytics/);
|
|
assert.match(doc, /Riot|LoL Esports|Oracle(?:'s)? Elixir/i);
|
|
}
|
|
|
|
assert.match(sources, /https:\/\/github\.com\/jerjangmin\/share\/tree\/main\/SKILL\/lck-analytics/);
|
|
});
|
|
|
|
test("repository docs advertise the korean-humanizer skill and credit im-not-ai and happy-nut", () => {
|
|
const readme = read("README.md");
|
|
const skill = read(path.join("korean-humanizer", "SKILL.md"));
|
|
const featureDoc = read(path.join("docs", "features", "korean-humanizer.md"));
|
|
const taxonomy = read(path.join("korean-humanizer", "references", "ai-tell-taxonomy.md"));
|
|
|
|
assert.match(readme, /\| \[한국어 AI 윤문\]\(docs\/features\/korean-humanizer\.md\) \| `korean-humanizer` \|/);
|
|
assert.match(readme, /\[한국어 AI 윤문\]\(docs\/features\/korean-humanizer\.md\)/);
|
|
|
|
for (const doc of [skill, featureDoc, taxonomy]) {
|
|
assert.match(doc, /https:\/\/github\.com\/epoko77-ai\/im-not-ai/);
|
|
}
|
|
for (const doc of [skill, featureDoc]) {
|
|
assert.match(doc, /happy-nut/);
|
|
}
|
|
assert.match(skill, /S1|S2|S3/);
|
|
assert.match(skill, /references\/ai-tell-taxonomy\.md/);
|
|
});
|
|
|
|
test("repository docs advertise the korean-spell-check skill and usage constraints", () => {
|
|
const readme = read("README.md");
|
|
const install = read(path.join("docs", "install.md"));
|
|
const featureDocPath = path.join(repoRoot, "docs", "features", "korean-spell-check.md");
|
|
const skillPath = path.join(repoRoot, "korean-spell-check", "SKILL.md");
|
|
const featureDoc = read(path.join("docs", "features", "korean-spell-check.md"));
|
|
const skill = read(path.join("korean-spell-check", "SKILL.md"));
|
|
const sources = read(path.join("docs", "sources.md"));
|
|
const roadmap = read(path.join("docs", "roadmap.md"));
|
|
|
|
assert.ok(fs.existsSync(featureDocPath), "expected docs/features/korean-spell-check.md to exist");
|
|
assert.ok(fs.existsSync(skillPath), "expected korean-spell-check/SKILL.md to exist");
|
|
assert.match(readme, /\| \[한국어 맞춤법 검사\]\(docs\/features\/korean-spell-check\.md\) \|/);
|
|
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,
|
|
/@nomadamas\/k-skill@0 exec korean-spell-check 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, /\| \[의약품 안전 체크\]\(docs\/features\/mfds-drug-safety\.md\) \|/);
|
|
assert.match(readme, /\| \[식품 안전 체크\]\(docs\/features\/mfds-food-safety\.md\) \|/);
|
|
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, /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("seoul subway docs default to the hosted proxy when KSKILL_PROXY_BASE_URL is unset", () => {
|
|
const readme = read("README.md");
|
|
const setup = read(path.join("docs", "setup.md"));
|
|
const install = read(path.join("docs", "install.md"));
|
|
const security = read(path.join("docs", "security-and-secrets.md"));
|
|
const setupSkill = read(path.join("k-skill-setup", "SKILL.md"));
|
|
const skill = read(path.join("seoul-subway-arrival", "SKILL.md"));
|
|
const featureDoc = read(path.join("docs", "features", "seoul-subway-arrival.md"));
|
|
const proxyDoc = read(path.join("docs", "features", "k-skill-proxy.md"));
|
|
const proxyReadme = read(path.join("packages", "k-skill-proxy", "README.md"));
|
|
const secretsExample = read(path.join("examples", "secrets.env.example"));
|
|
|
|
assert.match(readme, /\| \[서울 지하철 도착정보 조회\]\(docs\/features\/seoul-subway-arrival\.md\) \|/);
|
|
assert.match(setup, /\| 서울 지하철 도착정보 조회 \| 사용자 시크릿 불필요 \(기본 hosted proxy 사용, 운영자만 `SEOUL_OPEN_API_KEY`\) \|/);
|
|
assert.match(install, /--skill seoul-subway-arrival/);
|
|
|
|
for (const doc of [skill, featureDoc]) {
|
|
assert.match(doc, /KSKILL_PROXY_BASE_URL/);
|
|
assert.match(doc, /\/v1\/seoul-subway\/arrival/);
|
|
assert.match(doc, /사용자가 .*OpenAPI key.*직접.*필요(가|는)? 없다|개인 API key 없이/i);
|
|
assert.match(doc, /비우면 기본 hosted `https:\/\/k-skill-proxy\.nomadamas\.org`|없으면 기본 hosted proxy/i);
|
|
assert.doesNotMatch(doc, /SEOUL_OPEN_API_KEY/);
|
|
assert.doesNotMatch(doc, /swopenAPI\.seoul\.go\.kr\/api\/subway\/\$\{SEOUL_OPEN_API_KEY\}/);
|
|
assert.doesNotMatch(doc, /필수: self-host|반드시 명시|먼저 확보|public hosted route rollout 전/);
|
|
}
|
|
|
|
assert.match(proxyDoc, /GET \/v1\/seoul-subway\/arrival/);
|
|
assert.match(proxyDoc, /KSKILL_PROXY_BASE_URL/);
|
|
assert.match(proxyDoc, /unset\/empty|비워 두면/);
|
|
assert.match(proxyDoc, /https:\/\/k-skill-proxy\.nomadamas\.org/);
|
|
assert.match(proxyDoc, /KSKILL_PROXY_BASE_URL=https:\/\/your-proxy\.example\.com/);
|
|
assert.match(proxyDoc, /self-host|alternate proxy/i);
|
|
assert.match(proxyDoc, /override/i);
|
|
assert.match(proxyDoc, /SEOUL_OPEN_API_KEY/);
|
|
assert.match(proxyReadme, /GET \/v1\/seoul-subway\/arrival/);
|
|
assert.match(proxyReadme, /SEOUL_OPEN_API_KEY/);
|
|
assert.match(security, /KSKILL_PROXY_BASE_URL/);
|
|
assert.match(security, /서울 지하철.*한국 날씨.*기본 hosted proxy|기본 hosted proxy.*서울 지하철.*한국 날씨/i);
|
|
assert.match(setupSkill, /서울 지하철: 사용자 시크릿 불필요 \(기본 hosted proxy 사용, 운영자만 `SEOUL_OPEN_API_KEY`\)/);
|
|
assert.doesNotMatch(secretsExample, /SEOUL_OPEN_API_KEY/);
|
|
assert.doesNotMatch(secretsExample, /^KSKILL_PROXY_BASE_URL=https:\/\/your-proxy\.example\.com$/m);
|
|
assert.match(secretsExample, /^(#\s*)?KSKILL_PROXY_BASE_URL=$|^#\s*KSKILL_PROXY_BASE_URL=https:\/\/your-proxy\.example\.com$/m);
|
|
assert.doesNotMatch(secretsExample, /KSKILL_PROXY_BASE_URL=https:\/\/k-skill-proxy\.nomadamas\.org/);
|
|
});
|
|
|
|
test("repository docs advertise the korea-weather skill across the documented surfaces", () => {
|
|
const readme = read("README.md");
|
|
const install = read(path.join("docs", "install.md"));
|
|
const roadmap = read(path.join("docs", "roadmap.md"));
|
|
const sources = read(path.join("docs", "sources.md"));
|
|
const featureDocPath = path.join(repoRoot, "docs", "features", "korea-weather.md");
|
|
|
|
assert.ok(fs.existsSync(featureDocPath), "expected docs/features/korea-weather.md to exist");
|
|
assert.match(readme, /\| \[한국 날씨 조회\]\(docs\/features\/korea-weather\.md\) \|/);
|
|
assert.match(readme, /\[한국 날씨 조회\]\(docs\/features\/korea-weather\.md\)/);
|
|
assert.match(install, /--skill korea-weather/);
|
|
assert.match(roadmap, /한국 날씨 조회 스킬 출시/);
|
|
assert.match(sources, /기상청 단기예보 조회서비스: https:\/\/www\.data\.go\.kr\/data\/15084084\/openapi\.do/);
|
|
});
|
|
|
|
test("korea-weather docs route short-term forecast calls through the proxy without requiring a user API key", () => {
|
|
const skillPath = path.join(repoRoot, "korea-weather", "SKILL.md");
|
|
|
|
assert.ok(fs.existsSync(skillPath), "expected korea-weather/SKILL.md to exist");
|
|
|
|
const skill = read(path.join("korea-weather", "SKILL.md"));
|
|
const featureDoc = read(path.join("docs", "features", "korea-weather.md"));
|
|
const proxyDoc = read(path.join("docs", "features", "k-skill-proxy.md"));
|
|
const proxyReadme = read(path.join("packages", "k-skill-proxy", "README.md"));
|
|
|
|
assert.match(skill, /^name: korea-weather$/m);
|
|
assert.match(skill, /^description: .*날씨.*기상청.*프록시.*$/m);
|
|
|
|
for (const doc of [skill, featureDoc]) {
|
|
assert.match(doc, /\/v1\/korea-weather\/forecast/);
|
|
assert.match(doc, /기상청.*단기예보|단기예보.*기상청/);
|
|
assert.match(doc, /사용자가 .*API key.*직접.*필요(가|는)? 없다|개인 API key 없이/i);
|
|
assert.match(doc, /nx|ny|위도|경도/u);
|
|
assert.match(doc, /TMP|SKY|PTY|POP/);
|
|
assert.match(doc, /KSKILL_PROXY_BASE_URL|k-skill-proxy\.nomadamas\.org/);
|
|
assert.match(doc, /비우면 기본 hosted `https:\/\/k-skill-proxy\.nomadamas\.org`|없으면 기본 hosted proxy/i);
|
|
assert.doesNotMatch(doc, /필수: self-host|반드시 명시|먼저 확보|public hosted route rollout 전/);
|
|
assert.doesNotMatch(doc, /KMA_OPEN_API_KEY=.*사용자/);
|
|
}
|
|
|
|
assert.match(proxyDoc, /GET \/v1\/korea-weather\/forecast/);
|
|
assert.match(proxyDoc, /KMA_OPEN_API_KEY/);
|
|
assert.match(proxyReadme, /GET \/v1\/korea-weather\/forecast/);
|
|
assert.match(proxyReadme, /KMA_OPEN_API_KEY/);
|
|
});
|
|
|
|
test("hosted proxy docs keep self-host overrides inactive and demonstrate resolver fallback", () => {
|
|
const setup = read(path.join("docs", "setup.md"));
|
|
const security = read(path.join("docs", "security-and-secrets.md"));
|
|
const setupSkill = read(path.join("k-skill-setup", "SKILL.md"));
|
|
const secretsExample = read(path.join("examples", "secrets.env.example"));
|
|
const subwaySkill = read(path.join("seoul-subway-arrival", "SKILL.md"));
|
|
const weatherSkill = read(path.join("korea-weather", "SKILL.md"));
|
|
const subwayFeatureDoc = read(path.join("docs", "features", "seoul-subway-arrival.md"));
|
|
const weatherFeatureDoc = read(path.join("docs", "features", "korea-weather.md"));
|
|
const proxyDoc = read(path.join("docs", "features", "k-skill-proxy.md"));
|
|
|
|
for (const doc of [setup, security, setupSkill, secretsExample]) {
|
|
assert.doesNotMatch(doc, /^KSKILL_PROXY_BASE_URL=https:\/\/your-proxy\.example\.com$/m);
|
|
}
|
|
|
|
for (const doc of [setup, security, setupSkill, secretsExample]) {
|
|
assert.match(
|
|
doc,
|
|
/^(#\s*)?KSKILL_PROXY_BASE_URL=$|^#\s*KSKILL_PROXY_BASE_URL=https:\/\/your-proxy\.example\.com$/m,
|
|
);
|
|
}
|
|
|
|
for (const doc of [subwaySkill, weatherSkill, subwayFeatureDoc, weatherFeatureDoc]) {
|
|
assert.match(doc, /BASE="\$\{KSKILL_PROXY_BASE_URL:-https:\/\/k-skill-proxy\.nomadamas\.org\}"/);
|
|
assert.match(doc, /curl -fsS --get "\$\{BASE\}/);
|
|
assert.doesNotMatch(doc, /curl -fsS --get 'https:\/\/k-skill-proxy\.nomadamas\.org/);
|
|
}
|
|
|
|
assert.match(proxyDoc, /BASE="\$\{KSKILL_PROXY_BASE_URL:-https:\/\/k-skill-proxy\.nomadamas\.org\}"/);
|
|
for (const endpoint of ["seoul-subway/arrival", "korea-weather/forecast"]) {
|
|
assert.match(proxyDoc, new RegExp(`curl -fsS --get "\\$\\{BASE\\}/v1/${endpoint}"`));
|
|
assert.doesNotMatch(proxyDoc, /http:\/\/127\.0\.0\.1:\d+/);
|
|
}
|
|
});
|
|
|
|
test("proxy deployment script and docs stay aligned with gpu01 automation", () => {
|
|
const deployScript = read(path.join("scripts", "deploy-k-skill-proxy-gpu01.sh"));
|
|
const agents = read("AGENTS.md");
|
|
const deployDoc = read(path.join("docs", "deploy-k-skill-proxy.md"));
|
|
const featureDoc = read(path.join("docs", "features", "k-skill-proxy.md"));
|
|
const packageReadme = read(path.join("packages", "k-skill-proxy", "README.md"));
|
|
const dockerfile = read(path.join("packages", "k-skill-proxy", "Dockerfile"));
|
|
const dockerignore = read(".dockerignore");
|
|
const npmReleaseWorkflow = read(path.join(".github", "workflows", "release-npm.yml"));
|
|
const proxyPackage = JSON.parse(read(path.join("packages", "k-skill-proxy", "package.json")));
|
|
|
|
assert.match(deployScript, /DEPLOY_REF="\$\{KSKILL_PROXY_DEPLOY_REF:-origin\/main\}"/);
|
|
assert.match(deployScript, /KSKILL_PROXY_ENV_FILE/);
|
|
assert.match(deployScript, /KSKILL_PROXY_DEPLOY_ENVIRONMENT:-production/);
|
|
assert.match(deployScript, /KSKILL_PROXY_DEPLOY_HOST:-\$\(hostname -s\)/);
|
|
assert.match(deployScript, /export XDG_RUNTIME_DIR=/);
|
|
assert.match(deployScript, /export DBUS_SESSION_BUS_ADDRESS=/);
|
|
assert.match(deployScript, /is_gpu01_production/);
|
|
assert.match(deployScript, /marker-multi-gpu-server-01/);
|
|
assert.match(deployScript, /ensure_production_services "\$DEPLOY_ENVIRONMENT" "\$DEPLOY_HOST"/);
|
|
assert.match(
|
|
deployScript,
|
|
/install -m 0755 "\$REPO_DIR\/scripts\/deploy-k-skill-proxy-gpu01\.sh" "\$APP_DIR\/deploy-k-skill-proxy-gpu01\.sh"/,
|
|
);
|
|
assert.match(deployScript, /ensure_gpu01_production_defaults "\$ENV_FILE" "\$DEPLOY_ENVIRONMENT" "\$DEPLOY_HOST"/);
|
|
assert.match(deployScript, /npm --prefix "\$REPO_DIR" run test --workspace k-skill-proxy/);
|
|
assert.match(deployScript, /tar -C "\$APP_DIR" -czf "\$backup"/);
|
|
assert.match(deployScript, /systemctl --user restart "\$SERVICE_NAME"/);
|
|
assert.match(deployScript, /health_check http:\/\/127\.0\.0\.1:8080\/health/);
|
|
assert.match(deployScript, /health_check https:\/\/k-skill-proxy\.nomadamas\.org\/health/);
|
|
assert.match(deployScript, /printf '%s\\n' "\$target_sha" > "\$APP_DIR\/deployed-sha"/);
|
|
assert.match(dockerfile, /COPY package\.json package-lock\.json/);
|
|
assert.match(dockerfile, /npm ci --omit=dev --workspace k-skill-proxy/);
|
|
assert.match(dockerignore, /^gha-creds-\*\.json$/m);
|
|
assert.doesNotMatch(npmReleaseWorkflow, /^\s+workflow_dispatch:/m);
|
|
assert.equal(proxyPackage.engines.node, ">=20");
|
|
|
|
for (const doc of [agents, deployDoc, featureDoc, packageReadme]) {
|
|
assert.match(doc, /gpu01/i);
|
|
assert.match(doc, /main/i);
|
|
}
|
|
|
|
assert.match(agents, /scripts\/deploy-k-skill-proxy-gpu01\.sh/);
|
|
assert.match(deployDoc, /systemd/i);
|
|
assert.match(deployDoc, /flock/);
|
|
assert.match(deployDoc, /rollback/i);
|
|
assert.match(deployDoc, /deployed-sha/);
|
|
assert.match(deployDoc, /KSKILL_PROXY_TRUST_PROXY_HOPS=1/);
|
|
assert.match(deployDoc, /Cloudflare Tunnel/);
|
|
assert.match(
|
|
deployDoc,
|
|
/KSKILL_PROXY_DEPLOY_HOST=gpu01 flock -n \/tmp\/k-skill-proxy-deploy\.lock/,
|
|
);
|
|
assert.match(deployDoc, /marker-multi-gpu-server-01/);
|
|
const setupScript = read(path.join("infra", "k-skill-proxy-dashboard", "setup-gpu01.sh"));
|
|
assert.match(setupScript, /k-skill-proxy-tunnel\.service/);
|
|
const proxyUnit = read(
|
|
path.join("infra", "k-skill-proxy-dashboard", "systemd", "k-skill-proxy.service"),
|
|
);
|
|
assert.match(proxyUnit, /^Restart=always$/m);
|
|
assert.match(proxyUnit, /^StartLimitIntervalSec=0$/m);
|
|
assert.match(packageReadme, /KSKILL_PROXY_TRUST_PROXY_HOPS/);
|
|
assert.match(deployScript, /KSKILL_PROXY_TRUST_PROXY_HOPS/);
|
|
});
|
|
|
|
test("proxy deployment defaults trust-proxy only for gpu01 production", () => {
|
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "k-skill-proxy-deploy-env-"));
|
|
const deployScript = path.join(repoRoot, "scripts", "deploy-k-skill-proxy-gpu01.sh");
|
|
const runDefault = (envFile, deployEnvironment, deployHost) =>
|
|
childProcess.execFileSync(
|
|
"bash",
|
|
[
|
|
"-c",
|
|
'KSKILL_PROXY_DEPLOY_LIB_ONLY=1 source "$1"; ensure_gpu01_production_defaults "$2" "$3" "$4"',
|
|
"bash",
|
|
deployScript,
|
|
envFile,
|
|
deployEnvironment,
|
|
deployHost,
|
|
],
|
|
{ encoding: "utf8" },
|
|
);
|
|
|
|
const productionEnv = path.join(tmpDir, "production.env");
|
|
const realHostnameEnv = path.join(tmpDir, "real-hostname.env");
|
|
const stagingEnv = path.join(tmpDir, "staging.env");
|
|
const otherHostEnv = path.join(tmpDir, "other-host.env");
|
|
const explicitEnv = path.join(tmpDir, "explicit.env");
|
|
for (const envFile of [productionEnv, realHostnameEnv, stagingEnv, otherHostEnv]) {
|
|
fs.writeFileSync(envFile, "KSKILL_PROXY_RATE_LIMIT_MAX=60\n");
|
|
}
|
|
fs.writeFileSync(explicitEnv, "KSKILL_PROXY_TRUST_PROXY_HOPS=2\n");
|
|
|
|
runDefault(productionEnv, "production", "gpu01");
|
|
runDefault(realHostnameEnv, "production", "marker-multi-gpu-server-01");
|
|
runDefault(stagingEnv, "staging", "gpu01");
|
|
runDefault(otherHostEnv, "production", "developer-mac");
|
|
runDefault(explicitEnv, "production", "gpu01");
|
|
|
|
assert.match(fs.readFileSync(productionEnv, "utf8"), /^KSKILL_PROXY_TRUST_PROXY_HOPS=1$/m);
|
|
assert.match(fs.readFileSync(realHostnameEnv, "utf8"), /^KSKILL_PROXY_TRUST_PROXY_HOPS=1$/m);
|
|
assert.doesNotMatch(fs.readFileSync(stagingEnv, "utf8"), /KSKILL_PROXY_TRUST_PROXY_HOPS/);
|
|
assert.doesNotMatch(fs.readFileSync(otherHostEnv, "utf8"), /KSKILL_PROXY_TRUST_PROXY_HOPS/);
|
|
assert.match(fs.readFileSync(explicitEnv, "utf8"), /^KSKILL_PROXY_TRUST_PROXY_HOPS=2$/m);
|
|
});
|
|
|
|
test("proxy deploy watchdog starts dead services on the real gpu01 hostname", () => {
|
|
const deployScript = path.join(repoRoot, "scripts", "deploy-k-skill-proxy-gpu01.sh");
|
|
const mockRoot = fs.mkdtempSync(path.join(os.tmpdir(), "k-skill-proxy-watchdog-"));
|
|
const mockBin = path.join(mockRoot, "bin");
|
|
fs.mkdirSync(mockBin);
|
|
fs.writeFileSync(
|
|
path.join(mockBin, "systemctl"),
|
|
[
|
|
"#!/usr/bin/env bash",
|
|
"set -euo pipefail",
|
|
'STATE_DIR="${MOCK_SYSTEMCTL_STATE:?}"',
|
|
'echo "$*" >> "$STATE_DIR/calls.log"',
|
|
'cmd=""',
|
|
"args=()",
|
|
"while [[ $# -gt 0 ]]; do",
|
|
' case "$1" in',
|
|
" --user|--no-legend|--quiet|--no-pager) shift ;;",
|
|
" *)",
|
|
' if [[ -z "$cmd" ]]; then cmd="$1"; shift',
|
|
' else args+=("$1"); shift',
|
|
" fi",
|
|
" ;;",
|
|
" esac",
|
|
"done",
|
|
'unit="${args[0]:-}"',
|
|
'case "$cmd" in',
|
|
" list-unit-files)",
|
|
' if [[ -f "$STATE_DIR/installed/$unit" ]]; then echo "$unit enabled"; fi',
|
|
" exit 0",
|
|
" ;;",
|
|
" is-active)",
|
|
' if [[ -f "$STATE_DIR/active/$unit" ]]; then exit 0; fi',
|
|
" exit 3",
|
|
" ;;",
|
|
" start)",
|
|
' mkdir -p "$STATE_DIR/active"',
|
|
' touch "$STATE_DIR/active/$unit"',
|
|
' echo "$unit" >> "$STATE_DIR/started.log"',
|
|
" exit 0",
|
|
" ;;",
|
|
" *)",
|
|
' echo "unexpected: $cmd" >&2',
|
|
" exit 99",
|
|
" ;;",
|
|
"esac",
|
|
"",
|
|
].join("\n"),
|
|
{ mode: 0o755 },
|
|
);
|
|
|
|
const runWatchdog = (host, { installed = [], active = [] } = {}) => {
|
|
const state = fs.mkdtempSync(path.join(mockRoot, "state-"));
|
|
fs.mkdirSync(path.join(state, "installed"));
|
|
fs.mkdirSync(path.join(state, "active"));
|
|
fs.writeFileSync(path.join(state, "calls.log"), "");
|
|
fs.writeFileSync(path.join(state, "started.log"), "");
|
|
for (const unit of installed) {
|
|
fs.writeFileSync(path.join(state, "installed", unit), "");
|
|
}
|
|
for (const unit of active) {
|
|
fs.writeFileSync(path.join(state, "active", unit), "");
|
|
}
|
|
childProcess.execFileSync(
|
|
"bash",
|
|
[
|
|
"-c",
|
|
'export PATH="$1:$PATH"; export MOCK_SYSTEMCTL_STATE="$2"; KSKILL_PROXY_DEPLOY_LIB_ONLY=1 source "$3"; ensure_production_services production "$4"',
|
|
"bash",
|
|
mockBin,
|
|
state,
|
|
deployScript,
|
|
host,
|
|
],
|
|
{ encoding: "utf8" },
|
|
);
|
|
const startedRaw = fs.readFileSync(path.join(state, "started.log"), "utf8").trim();
|
|
return {
|
|
started: startedRaw === "" ? [] : startedRaw.split("\n"),
|
|
calls: fs.readFileSync(path.join(state, "calls.log"), "utf8"),
|
|
};
|
|
};
|
|
|
|
const watched = [
|
|
"k-skill-proxy.service",
|
|
"k-skill-proxy-tunnel.service",
|
|
"k-skill-proxy-loki.service",
|
|
"k-skill-proxy-promtail.service",
|
|
"k-skill-proxy-grafana.service",
|
|
];
|
|
|
|
assert.deepEqual(runWatchdog("developer-mac", { installed: watched }).started, []);
|
|
assert.deepEqual(
|
|
runWatchdog("marker-multi-gpu-server-01", { installed: watched }).started.sort(),
|
|
[...watched].sort(),
|
|
);
|
|
assert.deepEqual(
|
|
runWatchdog("gpu01", {
|
|
installed: watched,
|
|
active: watched.filter((unit) => unit !== "k-skill-proxy-tunnel.service"),
|
|
}).started,
|
|
["k-skill-proxy-tunnel.service"],
|
|
);
|
|
assert.deepEqual(runWatchdog("gpu01", { installed: [] }).started, []);
|
|
});
|
|
|
|
test("kakaotalk-mac skill documents katok archive search usage", () => {
|
|
const skillPath = path.join(repoRoot, "kakaotalk-mac", "SKILL.md");
|
|
const featureDocPath = path.join(repoRoot, "docs", "features", "kakaotalk-mac.md");
|
|
|
|
assert.ok(fs.existsSync(skillPath), "expected kakaotalk-mac/SKILL.md to exist");
|
|
assert.ok(fs.existsSync(featureDocPath), "expected docs/features/kakaotalk-mac.md to exist");
|
|
|
|
const skill = read(path.join("kakaotalk-mac", "SKILL.md"));
|
|
const featureDoc = read(path.join("docs", "features", "kakaotalk-mac.md"));
|
|
|
|
assert.match(skill, /^name: kakaotalk-mac$/m);
|
|
|
|
for (const doc of [skill, featureDoc]) {
|
|
assert.match(doc, /\bkatok\b/);
|
|
assert.match(doc, /macOS/i);
|
|
assert.match(doc, /KakaoTalk/i);
|
|
assert.match(doc, /Full Disk Access/i);
|
|
assert.match(doc, /katok doctor --json/);
|
|
assert.match(doc, /katok permissions macos/);
|
|
assert.match(doc, /katok sync --source macos --json/);
|
|
assert.match(doc, /katok index --json/);
|
|
assert.match(doc, /katok search keyword/);
|
|
assert.match(doc, /katok search bm25/);
|
|
assert.match(doc, /katok search semantic/);
|
|
assert.match(doc, /katok chunk get/);
|
|
assert.match(doc, /katok chunk context/);
|
|
assert.match(doc, /katok chunk parent/);
|
|
assert.match(doc, /(no|never|do not|don't|not).{0,80}((direct|raw).{0,40}(DB|database).{0,40}read|directly read.{0,40}(DB|database))/i);
|
|
assert.match(doc, /(no|never|do not|don't|not).{0,80}(auth|authentication).{0,40}caches?/i);
|
|
assert.match(doc, /(no|never|do not|don't|not).{0,80}decryption material/i);
|
|
assert.doesNotMatch(doc, /kakaocli/);
|
|
assert.doesNotMatch(doc, /python3 scripts\/kakaotalk_mac\.py/);
|
|
assert.doesNotMatch(doc, /send --me/);
|
|
assert.doesNotMatch(doc, /delete-last/);
|
|
assert.doesNotMatch(doc, /confirm before sending/i);
|
|
assert.doesNotMatch(doc, /SQLCipher key/i);
|
|
}
|
|
});
|
|
|
|
test("repository docs advertise the unified railway read-only lookup skill as supported", () => {
|
|
const readme = read("README.md");
|
|
const install = read(path.join("docs", "install.md"));
|
|
const featureDocPath = path.join(repoRoot, "docs", "features", "railway-timetable.md");
|
|
|
|
assert.ok(fs.existsSync(featureDocPath), "expected docs/features/railway-timetable.md to exist");
|
|
assert.match(readme, /\| \[철도 통합 시간표 조회\]\(docs\/features\/railway-timetable\.md\) \|/);
|
|
assert.match(readme, /\[철도 통합 시간표 조회\]\(docs\/features\/railway-timetable\.md\)/);
|
|
assert.match(readme, /조회 전용/);
|
|
assert.match(install, /--skill railway-timetable/);
|
|
});
|
|
|
|
test("railway-timetable docs enforce Korail integrated read-only lookup", () => {
|
|
const skillPath = path.join(repoRoot, "railway-timetable", "SKILL.md");
|
|
const helperPath = path.join(repoRoot, "railway-timetable", "scripts", "railway_timetable.py");
|
|
|
|
assert.ok(fs.existsSync(skillPath), "expected railway-timetable/SKILL.md to exist");
|
|
assert.ok(fs.existsSync(helperPath), "expected railway-timetable/scripts/railway_timetable.py to exist");
|
|
|
|
const instruction = read(path.join("railway-timetable", "instruction.md"));
|
|
const manifest = JSON.parse(read(path.join("railway-timetable", "skill.json")));
|
|
const stub = read(path.join("railway-timetable", "SKILL.md"));
|
|
const featureDoc = read(path.join("docs", "features", "railway-timetable.md"));
|
|
const helper = read(path.join("railway-timetable", "scripts", "railway_timetable.py"));
|
|
|
|
assert.match(stub, /^name: railway-timetable$/m);
|
|
assert.deepEqual(manifest.profiles, ["lookup"]);
|
|
|
|
for (const doc of [instruction, featureDoc]) {
|
|
assert.match(doc, /@nomadamas\/k-skill@0 exec railway-timetable scripts\/railway_timetable\.py --\s*\\?\s+search/);
|
|
assert.match(doc, /조회 전용/);
|
|
assert.match(doc, /KTX/);
|
|
assert.match(doc, /코레일/);
|
|
assert.match(doc, /예약.*(?:결제|취소).*실행하지 않는다|예약.*결제.*취소.*없/);
|
|
assert.doesNotMatch(doc, /--train-id|--try-waiting/);
|
|
}
|
|
|
|
assert.match(helper, /ktx_backend/);
|
|
assert.match(helper, /def build_parser/);
|
|
assert.doesNotMatch(helper, /reserve|cancel|payment|login/);
|
|
});
|
|
|
|
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, /\| \[긱뉴스 조회\]\(docs\/features\/geeknews-search\.md\) \|/);
|
|
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, /@nomadamas\/k-skill@0 exec geeknews-search scripts\/geeknews_search\.py -- list/);
|
|
assert.match(doc, /@nomadamas\/k-skill@0 exec geeknews-search scripts\/geeknews_search\.py -- search/);
|
|
assert.match(doc, /@nomadamas\/k-skill@0 exec geeknews-search 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, /\| \[지하철 분실물 조회\]\(docs\/features\/subway-lost-property\.md\) \|/);
|
|
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, /@nomadamas\/k-skill@0 exec subway-lost-property 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, /\| \[우편번호 검색\]\(docs\/features\/zipcode-search\.md\) \|/);
|
|
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, /@nomadamas\/k-skill@0 exec zipcode-search scripts\/zipcode_search\.py --/);
|
|
assert.doesNotMatch(doc, /(?:python3|\.) 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, /\| \[택배 배송조회\]\(docs\/features\/delivery-tracking\.md\) \|/);
|
|
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, /\| \[다이소 상품 조회\]\(docs\/features\/daiso-product-search\.md\) \|/);
|
|
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 ev-subsidy-status across documented surfaces", () => {
|
|
const readme = read("README.md");
|
|
const install = read(path.join("docs", "install.md"));
|
|
const sources = read(path.join("docs", "sources.md"));
|
|
const featureDocPath = path.join(repoRoot, "docs", "features", "ev-subsidy-status.md");
|
|
const skillPath = path.join(repoRoot, "ev-subsidy-status", "SKILL.md");
|
|
|
|
assert.ok(fs.existsSync(featureDocPath), "expected docs/features/ev-subsidy-status.md to exist");
|
|
assert.ok(fs.existsSync(skillPath), "expected ev-subsidy-status/SKILL.md to exist");
|
|
assert.match(readme, /\| \[전기차 보조금 현황 조회\]\(docs\/features\/ev-subsidy-status\.md\) \| `ev-subsidy-status` \|/);
|
|
assert.match(readme, /\[전기차 보조금 현황 조회\]\(docs\/features\/ev-subsidy-status\.md\)/);
|
|
assert.match(install, /--skill ev-subsidy-status/);
|
|
assert.match(install, /npm install -g .*ev-subsidy-status/);
|
|
assert.match(sources, /환경부 무공해차 통합누리집 구매보조금 지급현황: https:\/\/ev\.or\.kr\/nportal\/buySupprt\/initSubsidyPaymentCheckAction\.do/);
|
|
assert.match(sources, /환경부 무공해차 통합누리집 모델별 보조금: https:\/\/ev\.or\.kr\/nportal\/buySupprt\/psPopupLocalCarModelPrice\.do/);
|
|
});
|
|
|
|
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, /\| \[마켓컬리 상품 조회\]\(docs\/features\/market-kurly-search\.md\) \|/);
|
|
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, /\| \[올리브영 검색\]\(docs\/features\/olive-young-search\.md\) \|/);
|
|
assert.match(readme, /\[올리브영 검색\]\(docs\/features\/olive-young-search\.md\)/);
|
|
assert.match(install, /--skill olive-young-search/);
|
|
assert.match(install, /npm install -g .* daiso/);
|
|
assert.match(roadmap, /올리브영 검색 스킬 출시/);
|
|
assert.match(sources, /https:\/\/github\.com\/hmmhmmhm\/daiso-mcp/);
|
|
assert.match(sources, /https:\/\/www\.npmjs\.com\/package\/daiso/);
|
|
assert.match(sources, /https:\/\/mcp\.aka\.page\/api\/oliveyoung\/stores/);
|
|
assert.match(sources, /https:\/\/mcp\.aka\.page\/api\/oliveyoung\/products/);
|
|
assert.match(sources, /https:\/\/mcp\.aka\.page\/api\/oliveyoung\/inventory/);
|
|
});
|
|
|
|
test("olive-young install docs warn about intermittent public endpoint failures and direct users to retry or clone fallback", () => {
|
|
const install = read(path.join("docs", "install.md"));
|
|
const quickstart = findSection(install, "### `olive-young-search` upstream CLI quickstart");
|
|
|
|
assert.match(install, /olive-young-search/);
|
|
assert.match(install, /5xx\/503/);
|
|
assert.match(install, /재시도|retry/i);
|
|
assert.match(install, /clone fallback|git clone https:\/\/github\.com\/hmmhmmhm\/daiso-mcp\.git/i);
|
|
assertOliveYoungCloneFallbackShorthand(quickstart, "olive-young install quickstart");
|
|
assertOliveYoungCloneFallbackCommands(quickstart, "olive-young install quickstart");
|
|
});
|
|
|
|
test("olive-young-search skill documents the upstream daiso CLI flow for stores, products, and inventory", () => {
|
|
const skillPath = path.join(repoRoot, "olive-young-search", "SKILL.md");
|
|
const featureDoc = read(path.join("docs", "features", "olive-young-search.md"));
|
|
|
|
assert.ok(fs.existsSync(skillPath), "expected olive-young-search/SKILL.md to exist");
|
|
|
|
const skill = read(path.join("olive-young-search", "SKILL.md"));
|
|
const featureTop = findSection(featureDoc, "## 가장 중요한 규칙");
|
|
const featureFallback = findSection(featureDoc, "## 원본 저장소 clone fallback");
|
|
const skillFallback = findSection(skill, "## Fallback: clone the original repository and run the same CLI locally");
|
|
|
|
assert.match(skill, /^name: olive-young-search$/m);
|
|
assert.match(skill, /^description: .*올리브영.*매장.*상품.*재고.*$/m);
|
|
|
|
for (const doc of [skill, featureDoc]) {
|
|
assert.match(doc, /hmmhmmhm\/daiso-mcp/);
|
|
assert.match(doc, /https:\/\/github\.com\/hmmhmmhm\/daiso-mcp/);
|
|
assert.match(doc, /npm install -g daiso|npx --yes daiso|npx daiso/);
|
|
assert.match(doc, /git clone https:\/\/github\.com\/hmmhmmhm\/daiso-mcp\.git/);
|
|
assert.match(doc, /npm install/);
|
|
assert.match(doc, /npm run build/);
|
|
assert.match(doc, /MCP 서버를 .*직접 설치.*않고.*CLI/u);
|
|
assert.match(doc, /매장 검색/);
|
|
assert.match(doc, /상품 검색/);
|
|
assert.match(doc, /재고 확인/);
|
|
assert.match(doc, /\/api\/oliveyoung\/stores/);
|
|
assert.match(doc, /\/api\/oliveyoung\/products/);
|
|
assert.match(doc, /\/api\/oliveyoung\/inventory/);
|
|
assert.match(doc, /vendoring 하지 않/);
|
|
}
|
|
|
|
assertOliveYoungCloneFallbackShorthand(featureTop, "olive-young feature guide shorthand");
|
|
|
|
for (const fallbackDoc of [featureFallback, skillFallback]) {
|
|
assertOliveYoungCloneFallbackCommands(fallbackDoc, "olive-young clone fallback docs");
|
|
}
|
|
});
|
|
|
|
test("repository docs advertise the korean-cinema-search skill across the documented surfaces", () => {
|
|
const readme = read("README.md");
|
|
const install = read(path.join("docs", "install.md"));
|
|
const roadmap = read(path.join("docs", "roadmap.md"));
|
|
const sources = read(path.join("docs", "sources.md"));
|
|
const featureDocPath = path.join(repoRoot, "docs", "features", "korean-cinema-search.md");
|
|
const skillPath = path.join(repoRoot, "korean-cinema-search", "SKILL.md");
|
|
|
|
assert.ok(fs.existsSync(featureDocPath), "expected docs/features/korean-cinema-search.md to exist");
|
|
assert.ok(fs.existsSync(skillPath), "expected korean-cinema-search/SKILL.md to exist");
|
|
assert.match(readme, /\| \[영화관 검색\]\(docs\/features\/korean-cinema-search\.md\) \|/);
|
|
assert.match(readme, /\[영화관 검색\]\(docs\/features\/korean-cinema-search\.md\)/);
|
|
assert.match(install, /--skill korean-cinema-search/);
|
|
assert.match(install, /--playDate <YYYYMMDD>/);
|
|
assert.match(install, /5xx|retry|재시도/);
|
|
assert.match(install, /git clone https:\/\/github\.com\/hmmhmmhm\/daiso-mcp\.git && cd daiso-mcp && npm install && npm run build/);
|
|
assert.match(install, /node dist\/bin\.js get \/api\/cgv\/timetable --keyword 강남 --playDate <YYYYMMDD> --json/);
|
|
assert.match(roadmap, /영화관 검색 스킬 출시/);
|
|
assert.match(sources, /https:\/\/github\.com\/hmmhmmhm\/daiso-mcp/);
|
|
assert.match(sources, /https:\/\/www\.npmjs\.com\/package\/daiso/);
|
|
assert.match(sources, /https:\/\/mcp\.aka\.page\/api\/cgv\/theaters/);
|
|
assert.match(sources, /https:\/\/mcp\.aka\.page\/api\/megabox\/theaters/);
|
|
assert.match(sources, /https:\/\/mcp\.aka\.page\/api\/lottecinema\/theaters/);
|
|
});
|
|
|
|
test("korean-cinema-search skill documents the upstream daiso CLI flow for Korean cinemas", () => {
|
|
const skillPath = path.join(repoRoot, "korean-cinema-search", "SKILL.md");
|
|
const featureDoc = read(path.join("docs", "features", "korean-cinema-search.md"));
|
|
|
|
assert.ok(fs.existsSync(skillPath), "expected korean-cinema-search/SKILL.md to exist");
|
|
|
|
const skill = read(path.join("korean-cinema-search", "SKILL.md"));
|
|
|
|
assert.match(skill, /^name: korean-cinema-search$/m);
|
|
assert.match(skill, /^description: .*CGV.*메가박스.*롯데시네마.*$/m);
|
|
|
|
for (const doc of [skill, featureDoc]) {
|
|
assert.match(doc, /hmmhmmhm\/daiso-mcp/);
|
|
assert.match(doc, /npm install -g daiso|npx --yes daiso|npx daiso/);
|
|
assert.match(doc, /MCP 서버를 .*직접 설치.*않고.*CLI/u);
|
|
assert.match(doc, /Asia\/Seoul|KST/);
|
|
assert.match(doc, /YYYYMMDD/);
|
|
assert.match(doc, /--playDate <YYYYMMDD>/);
|
|
assert.match(doc, /\/api\/cgv\/movies --keyword 강남 --playDate <YYYYMMDD> --json/);
|
|
assert.match(doc, /\/api\/cgv\/timetable --keyword 강남 --playDate <YYYYMMDD> --json/);
|
|
assert.match(doc, /\/api\/megabox\/seats --keyword 코엑스 --playDate <YYYYMMDD> --limit 10 --json/);
|
|
assert.match(doc, /\/api\/lottecinema\/seats --keyword 월드타워 --playDate <YYYYMMDD> --limit 10 --json/);
|
|
assert.match(doc, /CGV/);
|
|
assert.match(doc, /메가박스/);
|
|
assert.match(doc, /롯데시네마/);
|
|
assert.match(doc, /\/api\/cgv\/theaters/);
|
|
assert.match(doc, /\/api\/cgv\/movies/);
|
|
assert.match(doc, /\/api\/cgv\/timetable/);
|
|
assert.match(doc, /\/api\/megabox\/theaters/);
|
|
assert.match(doc, /\/api\/megabox\/movies/);
|
|
assert.match(doc, /\/api\/megabox\/seats/);
|
|
assert.match(doc, /\/api\/lottecinema\/theaters/);
|
|
assert.match(doc, /\/api\/lottecinema\/movies/);
|
|
assert.match(doc, /\/api\/lottecinema\/seats/);
|
|
assert.match(doc, /예매|결제/);
|
|
}
|
|
|
|
assert.match(featureDoc, /CGV_UPSTREAM_UNAVAILABLE/);
|
|
assert.match(featureDoc, /봇 차단|접속 제한/);
|
|
assert.match(featureDoc, /CAPTCHA.*우회|접근 제한 우회/);
|
|
assert.match(featureDoc, /https:\/\/www\.cgv\.co\.kr/);
|
|
assert.match(featureDoc, /브라우저 사용 스킬/);
|
|
assert.match(featureDoc, /로그인·예매·결제[\s\S]*진행하지 않는다/);
|
|
assert.match(featureDoc, /공식 웹사이트에서도[\s\S]*우회하지/);
|
|
});
|
|
|
|
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, /\| \[번개장터 검색\]\(docs\/features\/bunjang-search\.md\) \|/);
|
|
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, /\| \[쿠팡 상품 검색\]\(docs\/features\/coupang-product-search\.md\) \|/);
|
|
assert.match(readme, /\[쿠팡 상품 검색\]\(docs\/features\/coupang-product-search\.md\)/);
|
|
assert.match(install, /--skill coupang-product-search/);
|
|
});
|
|
|
|
test("coupang-product-search skill and docs use the hosted proxy route", () => {
|
|
const skillPath = path.join(repoRoot, "coupang-product-search", "SKILL.md");
|
|
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");
|
|
|
|
const skill = read(path.join("coupang-product-search", "SKILL.md"));
|
|
|
|
for (const doc of [skill, featureDoc]) {
|
|
assert.match(doc, /\/v1\/coupang\/products\/search/);
|
|
assert.match(doc, /COUPANG_ACCESS_KEY/);
|
|
assert.match(doc, /COUPANG_SECRET_KEY/);
|
|
assert.match(doc, /로켓배송/);
|
|
assert.match(doc, /(파트너스|어필리에이트|affiliate)/i);
|
|
assert.doesNotMatch(doc, /a\.retn\.kr\/v1\/public\/assist/);
|
|
assert.doesNotMatch(doc, /local:\/\/coupang-mcp/);
|
|
}
|
|
|
|
assert.match(featureDoc, /proxy 서버/);
|
|
assert.match(featureDoc, /HMAC/i);
|
|
assert.match(featureDoc, /caller/);
|
|
assert.match(featureDoc, /키를 볼 수도, 전달할 필요도 없다/);
|
|
assert.match(sources, /developers\.coupangcorp\.com/);
|
|
});
|
|
|
|
test("coupang-product-search generated stub carries the affiliate disclosure", () => {
|
|
const stub = readRaw(path.join("coupang-product-search", "SKILL.md"));
|
|
assert.match(stub, /Affiliate disclosure \(required\)/);
|
|
assert.match(stub, /쿠팡 파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있습니다\./);
|
|
});
|
|
|
|
test("repository docs advertise the ohou-today-deal skill", () => {
|
|
const readme = read("README.md");
|
|
const install = read(path.join("docs", "install.md"));
|
|
const roadmap = read(path.join("docs", "roadmap.md"));
|
|
const sources = read(path.join("docs", "sources.md"));
|
|
const featureDocPath = path.join(repoRoot, "docs", "features", "ohou-today-deal.md");
|
|
const skillPath = path.join(repoRoot, "ohou-today-deal", "SKILL.md");
|
|
const helperPath = path.join(repoRoot, "ohou-today-deal", "scripts", "ohou_today_deal.py");
|
|
|
|
assert.ok(fs.existsSync(featureDocPath), "expected docs/features/ohou-today-deal.md to exist");
|
|
assert.ok(fs.existsSync(skillPath), "expected ohou-today-deal/SKILL.md to exist");
|
|
assert.ok(fs.existsSync(helperPath), "expected ohou-today-deal helper script to exist");
|
|
assert.match(readme, /\| \[오늘의집 오늘의딜 조회\]\(docs\/features\/ohou-today-deal\.md\) \| `ohou-today-deal` \|/);
|
|
assert.match(readme, /\[오늘의집 오늘의딜 조회\]\(docs\/features\/ohou-today-deal\.md\)/);
|
|
assert.match(install, /--skill ohou-today-deal/);
|
|
assert.match(roadmap, /오늘의집 오늘의딜 조회 스킬 출시/);
|
|
assert.match(sources, /ohou\.se\/commerces\/today_deals/);
|
|
assert.match(sources, /store\.ohou\.se\/today_deals/);
|
|
});
|
|
|
|
test("ohou-today-deal docs lock the public Next data read-only workflow", () => {
|
|
const skill = read(path.join("ohou-today-deal", "SKILL.md"));
|
|
const featureDoc = read(path.join("docs", "features", "ohou-today-deal.md"));
|
|
const helper = read(path.join("ohou-today-deal", "scripts", "ohou_today_deal.py"));
|
|
const sources = read(path.join("docs", "sources.md"));
|
|
|
|
for (const doc of [skill, featureDoc]) {
|
|
assert.match(doc, /https:\/\/ohou\.se\/commerces\/today_deals/);
|
|
assert.match(doc, /https:\/\/store\.ohou\.se\/today_deals/);
|
|
assert.match(doc, /__NEXT_DATA__/);
|
|
assert.match(doc, /today-deal-feed/);
|
|
assert.match(doc, /ohou_today_deal\.py -- list/);
|
|
assert.match(doc, /(로그인|API key|API 키).*(불필요|없음)|(불필요|없음).*(로그인|API key|API 키)/);
|
|
assert.match(doc, /(구매|장바구니|결제).*자동화.*(하지 않는다|하지 말고|제외)/);
|
|
}
|
|
|
|
assert.match(helper, /DEFAULT_URL = "https:\/\/ohou\.se\/commerces\/today_deals"/);
|
|
assert.match(helper, /__NEXT_DATA__/);
|
|
assert.match(helper, /today-deal-feed/);
|
|
assert.match(helper, /special-today-deal-feed/);
|
|
assert.match(
|
|
helper,
|
|
/k-skill-ohou-today-deal\/1\.0 \(\+https:\/\/github\.com\/NomaDamas\/k-skill\)/,
|
|
);
|
|
assert.match(sources, /ohou\.se\/commerces\/today_deals/);
|
|
assert.match(sources, /store\.ohou\.se\/today_deals/);
|
|
});
|
|
|
|
test("root pack:dry-run script covers all publishable workspaces", () => {
|
|
const packageJson = readJson("package.json");
|
|
const { listPublishableWorkspaces } = require("./ci-paths");
|
|
const publishableWorkspaces = listPublishableWorkspaces();
|
|
|
|
assert.equal(packageJson.scripts["pack:dry-run"], "node scripts/pack-dry-run.js");
|
|
assert.ok(publishableWorkspaces.includes("donation-place-search"));
|
|
assert.ok(publishableWorkspaces.includes("@nomadamas/k-skill"));
|
|
assert.ok(!publishableWorkspaces.includes("k-skill-proxy"));
|
|
});
|
|
|
|
test("README main capability table advertises the donation-place-search skill", () => {
|
|
const readme = read("README.md");
|
|
const tableSection = readme.slice(
|
|
readme.indexOf("## 어떤 걸 할 수 있나"),
|
|
readme.indexOf("## Claude Code 플러그인으로 설치"),
|
|
);
|
|
|
|
assert.match(tableSection, /기부처 조회/);
|
|
assert.match(tableSection, /`donation-place-search`/);
|
|
assert.match(tableSection, /docs\/features\/donation-place-search\.md/);
|
|
});
|
|
|
|
test("donation-place-search install docs include the skill and npm helper", () => {
|
|
const install = read(path.join("docs", "install.md"));
|
|
|
|
assert.match(install, /--skill donation-place-search/);
|
|
assert.match(install, /npm install -g .*donation-place-search/);
|
|
});
|
|
|
|
test("donation-place-search docs describe 1365 links as best-effort verification assists", () => {
|
|
const skill = read(path.join("donation-place-search", "SKILL.md"));
|
|
const packageReadme = read(path.join("packages", "donation-place-search", "README.md"));
|
|
const featureDoc = read(path.join("docs", "features", "donation-place-search.md"));
|
|
const packageJson = readJson(path.join("packages", "donation-place-search", "package.json"));
|
|
|
|
for (const doc of [skill, packageReadme, featureDoc, packageJson.description]) {
|
|
assert.match(doc, /best-effort|보조|assist/i);
|
|
assert.doesNotMatch(doc, /candidate-verified|후보별 검증 완료/);
|
|
}
|
|
});
|
|
|
|
test("naming-house docs and package preserve the 작명소 contract", () => {
|
|
const readme = read("README.md");
|
|
const install = read(path.join("docs", "install.md"));
|
|
const sources = read(path.join("docs", "sources.md"));
|
|
const skill = read(path.join("naming-house", "SKILL.md"));
|
|
const featureDoc = read(path.join("docs", "features", "naming-house.md"));
|
|
const packageReadme = read(path.join("packages", "naming-house", "README.md"));
|
|
const packageJson = readJson(path.join("packages", "naming-house", "package.json"));
|
|
|
|
assert.match(readme, /\| \[작명소\]\(docs\/features\/naming-house\.md\) \| `naming-house` \|/);
|
|
assert.match(readme, /docs\/features\/naming-house\.md/);
|
|
assert.match(install, /--skill naming-house/);
|
|
assert.match(install, /npm install -g naming-house/);
|
|
assert.equal(packageJson.name, "naming-house");
|
|
assert.equal(packageJson.dependencies.hanja, "^1.1.5");
|
|
assert.equal(packageJson.dependencies["korean-stroke"], "^1.1.5");
|
|
assert.match(sources, /hanja/);
|
|
assert.match(sources, /korean-stroke/);
|
|
|
|
for (const doc of [skill, featureDoc, packageReadme]) {
|
|
assert.match(doc, /hanja/);
|
|
assert.match(doc, /korean-stroke/);
|
|
assert.match(doc, /saju-fortune/);
|
|
assert.match(doc, /음력|lunar/);
|
|
assert.match(doc, /한자|Hanja/);
|
|
assert.match(doc, /법적|legal|인명용 한자/);
|
|
assert.match(doc, /MCP|proxy|로컬 또는 전역 npm package/);
|
|
}
|
|
});
|
|
|
|
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 경기 결과 조회\]\(docs\/features\/kbl-results\.md\) \|/);
|
|
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리그 경기 결과 조회\]\(docs\/features\/kleague-results\.md\) \|/);
|
|
assert.match(readme, /\[K리그 경기 결과 조회\]\(docs\/features\/kleague-results\.md\)/);
|
|
assert.match(install, /--skill kleague-results/);
|
|
assert.match(roadmap, /K리그 경기 결과 조회 스킬 출시/);
|
|
assert.match(sources, /K League 일정\/결과 JSON: https:\/\/www\.kleague\.com\/getScheduleList\.do/);
|
|
assert.match(sources, /K League 팀 순위 JSON: https:\/\/www\.kleague\.com\/record\/teamRank\.do/);
|
|
});
|
|
|
|
test("kleague-results skill documents the official JSON flow for date, team, and standings lookups", () => {
|
|
const skillPath = path.join(repoRoot, "kleague-results", "SKILL.md");
|
|
|
|
assert.ok(fs.existsSync(skillPath), "expected kleague-results/SKILL.md to exist");
|
|
|
|
const skill = read(path.join("kleague-results", "SKILL.md"));
|
|
const featureDoc = read(path.join("docs", "features", "kleague-results.md"));
|
|
|
|
assert.match(skill, /^name: kleague-results$/m);
|
|
assert.match(skill, /^description: .*케이리그.*경기 결과.*순위.*$/m);
|
|
|
|
for (const doc of [skill, featureDoc]) {
|
|
assert.match(doc, /YYYY-MM-DD/);
|
|
assert.match(doc, /K리그1|K리그2/);
|
|
assert.match(doc, /FC서울|서울 이랜드|팀 코드/);
|
|
assert.match(doc, /https:\/\/www\.kleague\.com\/getScheduleList\.do/);
|
|
assert.match(doc, /https:\/\/www\.kleague\.com\/record\/teamRank\.do/);
|
|
assert.match(doc, /공식 JSON|공식 API|공식 표면/u);
|
|
assert.match(doc, /현재 순위|standings/i);
|
|
assert.match(doc, /kleague-results|K리그 결과 조회/u);
|
|
}
|
|
});
|
|
|
|
test("kleague-results package exports reusable results and standings helpers", () => {
|
|
const pkg = require(path.join(repoRoot, "packages", "kleague-results", "src", "index.js"));
|
|
|
|
assert.equal(typeof pkg.getMatchResults, "function");
|
|
assert.equal(typeof pkg.getStandings, "function");
|
|
assert.equal(typeof pkg.getKLeagueSummary, "function");
|
|
});
|
|
|
|
test("kleague-results package README stays aligned with the official K League JSON lookup flow", () => {
|
|
const packageReadme = read(path.join("packages", "kleague-results", "README.md"));
|
|
|
|
assert.match(packageReadme, /공식 K리그 JSON 엔드포인트/u);
|
|
assert.match(packageReadme, /getScheduleList\.do/);
|
|
assert.match(packageReadme, /teamRank\.do/);
|
|
assert.match(packageReadme, /getKLeagueSummary/);
|
|
assert.match(packageReadme, /FC서울/);
|
|
});
|
|
|
|
test("unsupported naver map and blue ribbon skills are removed from the repository", () => {
|
|
const readme = read("README.md");
|
|
const install = read(path.join("docs", "install.md"));
|
|
const sources = read(path.join("docs", "sources.md"));
|
|
const cleanerInstruction = read(path.join("k-skill-cleaner", "instruction.md"));
|
|
assert.ok(!fs.existsSync(path.join(repoRoot, "blue-ribbon-nearby", "SKILL.md")));
|
|
assert.ok(!fs.existsSync(path.join(repoRoot, "naver-map-route", "SKILL.md")));
|
|
assert.ok(!fs.existsSync(path.join(repoRoot, "docs", "features", "blue-ribbon-nearby.md")));
|
|
assert.ok(!fs.existsSync(path.join(repoRoot, "docs", "features", "naver-map-route.md")));
|
|
assert.ok(!fs.existsSync(path.join(repoRoot, "packages", "blue-ribbon-nearby", "package.json")));
|
|
assert.doesNotMatch(cleanerInstruction, /blue-ribbon-nearby|naver-map-route/);
|
|
|
|
for (const doc of [readme, install, sources]) {
|
|
assert.doesNotMatch(doc, /blue-ribbon-nearby|naver-map-route/);
|
|
assert.doesNotMatch(doc, /근처 블루리본 맛집|네이버맵 길찾기|네이버맵 자동차 길찾기/);
|
|
}
|
|
});
|
|
|
|
test("retired local QA daemon is removed from the repository", () => {
|
|
assert.ok(!fs.existsSync(path.join(repoRoot, "tools", "k-skill-qa-bot")));
|
|
});
|
|
|
|
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, /\| \[근처 술집 조회\]\(docs\/features\/kakao-bar-nearby\.md\) \|/);
|
|
assert.match(readme, /\[근처 술집 조회\]\(docs\/features\/kakao-bar-nearby\.md\)/);
|
|
assert.match(install, /--skill kakao-bar-nearby/);
|
|
assert.match(roadmap, /근처 술집 조회 스킬 출시/);
|
|
assert.match(sources, /카카오맵 근처 술집 후보 검색: https:\/\/dapi\.kakao\.com\/v2\/local\/search\/keyword\.json/);
|
|
assert.match(sources, /카카오맵 장소 상세 페이지: https:\/\/place\.map\.kakao\.com\/<id>/);
|
|
});
|
|
|
|
test("kakao-bar-nearby documents official API search and detail-page handoff", () => {
|
|
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, /k-skill-proxy\.nomadamas\.org(?:\/v1\/kakao-map\/search\/keyword)?/);
|
|
assert.match(doc, /https:\/\/place\.map\.kakao\.com\/<id>/);
|
|
assert.match(doc, /detailLookup/);
|
|
assert.match(doc, /영업 중|영업전|영업 상태/);
|
|
assert.match(doc, /메뉴/);
|
|
assert.match(doc, /단체석|좌석 옵션|인원 수용/);
|
|
assert.match(doc, /전화번호/);
|
|
assert.match(doc, /kakao-bar-nearby|근처 술집 조회/u);
|
|
assert.match(doc, /panel3.*기본 검색 경로로 사용하지/u);
|
|
}
|
|
});
|
|
|
|
test("kakao-bar-nearby package README stays aligned with official search and handoff", () => {
|
|
const packageReadme = read(path.join("packages", "kakao-bar-nearby", "README.md"));
|
|
|
|
assert.match(packageReadme, /현재 위치를 먼저 물어본다/u);
|
|
assert.match(packageReadme, /Kakao Developers 공식 Local API/);
|
|
assert.match(packageReadme, /k-skill-proxy\.nomadamas\.org/);
|
|
assert.match(packageReadme, /https:\/\/place\.map\.kakao\.com\//);
|
|
assert.match(packageReadme, /detailLookup/);
|
|
assert.match(packageReadme, /searchNearbyBarsByLocationQuery/);
|
|
});
|
|
|
|
test("kakao-bar-nearby feature doc keeps unresolved detail fields explicit", () => {
|
|
const featureDoc = read(path.join("docs", "features", "kakao-bar-nearby.md"));
|
|
|
|
assert.match(featureDoc, /"isOpenNow": null/);
|
|
assert.match(featureDoc, /"openStatus": null/);
|
|
assert.match(featureDoc, /"menuSamples": \[\]/);
|
|
assert.match(featureDoc, /"status": "required"/);
|
|
assert.match(featureDoc, /"source": "kakao-local-rest-api"/);
|
|
});
|
|
|
|
test("kakao-bar-nearby package README directs enrichment through returned place links", () => {
|
|
const packageReadme = read(path.join("packages", "kakao-bar-nearby", "README.md"));
|
|
|
|
assert.match(packageReadme, /"fetchedPanels": 0/);
|
|
assert.match(packageReadme, /"url": "https:\/\/place\.map\.kakao\.com\/\.\.\."/);
|
|
assert.match(packageReadme, /실제로 확인한 값만/u);
|
|
});
|
|
|
|
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, /\| \[사용자 위치 미세먼지 조회\]\(docs\/features\/fine-dust-location\.md\) \|/);
|
|
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,
|
|
/@nomadamas\/k-skill@0 exec fine-dust-location 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,
|
|
/@nomadamas\/k-skill@0 exec fine-dust-location scripts\/fine_dust\.py --/,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("fine-dust helper python regression tests pass", () => {
|
|
const result = childProcess.spawnSync(
|
|
resolvePython(),
|
|
["-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-investment 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-investment.md");
|
|
|
|
assert.ok(fs.existsSync(featureDocPath), "expected docs/features/toss-investment.md to exist");
|
|
assert.match(readme, /\| \[토스증권 조회\]\(docs\/features\/toss-investment\.md\) \|/);
|
|
assert.match(readme, /\[토스증권 조회\]\(docs\/features\/toss-investment\.md\)/);
|
|
assert.match(install, /--skill toss-investment/);
|
|
assert.match(roadmap, /토스증권 조회 스킬 출시/);
|
|
assert.match(sources, /토스증권 공식 Open API 문서: https:\/\/developers\.tossinvest\.com\/docs/);
|
|
});
|
|
|
|
test("toss-investment skill source and bundled metadata stay synchronized", () => {
|
|
const sourceManifest = readJson(path.join("toss-investment", "skill.json"));
|
|
const bundledManifest = readJson(
|
|
path.join("packages", "k-skill-cli", "skills", "toss-investment", "skill.json")
|
|
);
|
|
|
|
assert.deepEqual(bundledManifest, sourceManifest);
|
|
assert.deepEqual(sourceManifest.profiles, ["vault", "action:account"]);
|
|
});
|
|
|
|
test("toss-investment package exposes only official read-only helpers", () => {
|
|
const pkg = require(path.join(repoRoot, "packages", "toss-investment", "src", "index.js"));
|
|
const official = require(path.join(
|
|
repoRoot,
|
|
"packages",
|
|
"toss-investment",
|
|
"src",
|
|
"official-client.js"
|
|
));
|
|
|
|
assert.deepEqual(Object.keys(pkg).sort(), Object.keys(official).sort());
|
|
assert.equal(typeof pkg.issueAccessToken, "function");
|
|
assert.equal(typeof pkg.getPrices, "function");
|
|
assert.equal(typeof pkg.getHoldings, "function");
|
|
assert.equal(typeof pkg.getBuyingPower, "function");
|
|
assert.equal(typeof pkg.listOfficialAccounts, "function");
|
|
assert.equal(typeof pkg.TossApiError, "function");
|
|
assert.equal(typeof pkg.TossCredentialsError, "function");
|
|
|
|
// Read-only safety contract: no order mutation helpers.
|
|
assert.equal(pkg.placeOrder, undefined);
|
|
assert.equal(pkg.modifyOrder, undefined);
|
|
assert.equal(pkg.cancelOrder, undefined);
|
|
});
|
|
|
|
test("toss-investment package metadata excludes the retired fallback", () => {
|
|
const packageJson = readJson(path.join("packages", "toss-investment", "package.json"));
|
|
const parserPath = path.join(repoRoot, "packages", "toss-investment", "src", "parse.js");
|
|
|
|
assert.equal(packageJson.name, "toss-investment");
|
|
assert.ok(packageJson.keywords.includes("openapi"));
|
|
assert.ok(!packageJson.keywords.includes("tossctl"));
|
|
assert.equal(fs.existsSync(parserPath), false);
|
|
});
|
|
|
|
test("pack:dry-run includes the toss-investment workspace", () => {
|
|
const packageJson = JSON.parse(read("package.json"));
|
|
const { listPublishableWorkspaces } = require("./ci-paths");
|
|
|
|
assert.equal(packageJson.scripts["pack:dry-run"], "node scripts/pack-dry-run.js");
|
|
assert.ok(listPublishableWorkspaces().includes("toss-investment"));
|
|
});
|
|
|
|
test("toss-investment pack dry-run ships only the official client surface", () => {
|
|
const packResult = JSON.parse(
|
|
childProcess.execFileSync("npm", ["pack", "--workspace", "toss-investment", "--json", "--dry-run"], {
|
|
cwd: repoRoot,
|
|
encoding: "utf8"
|
|
}),
|
|
);
|
|
|
|
const files = packResult[0]?.files?.map((entry) => entry.path).sort() || [];
|
|
assert.deepEqual(files, [
|
|
"README.md",
|
|
"package.json",
|
|
"src/index.js",
|
|
"src/official-client.js"
|
|
]);
|
|
});
|
|
|
|
test("package-lock captures the toss-investment workspace metadata for npm ci", () => {
|
|
const packageLock = readJson("package-lock.json");
|
|
|
|
assert.deepEqual(packageLock.packages[""].workspaces, ["packages/*"]);
|
|
assert.deepEqual(packageLock.packages["node_modules/toss-investment"], {
|
|
resolved: "packages/toss-investment",
|
|
link: true,
|
|
});
|
|
assert.equal(packageLock.packages["packages/toss-investment"].license, "MIT");
|
|
assert.equal(packageLock.packages["packages/toss-investment"].engines.node, ">=18");
|
|
});
|
|
|
|
test("repository docs advertise the korean-law-search skill via k-skill-proxy", () => {
|
|
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, /\| \[한국 법령 검색\]\(docs\/features\/korean-law-search\.md\) \|/);
|
|
assert.match(readme, /\[한국 법령 검색\]\(docs\/features\/korean-law-search\.md\)/);
|
|
assert.match(readme, /\| \[한국 법령 검색\]\(docs\/features\/korean-law-search\.md\) \|/);
|
|
assert.match(install, /--skill korean-law-search/);
|
|
assert.match(install, /k-skill-proxy\.nomadamas\.org/);
|
|
assert.match(install, /운영자만 proxy 서버에 `LAW_OC`/);
|
|
assert.match(setup, /한국 법령 검색은 기본 hosted proxy/);
|
|
assert.match(setup, /self-host proxy 운영자만 서버 환경변수 `LAW_OC`/);
|
|
assert.match(featureDoc, /\/v1\/korean-law\/search/);
|
|
assert.match(featureDoc, /\/v1\/korean-law\/detail/);
|
|
assert.match(setupSkill, /한국 법령 검색은 기본 hosted proxy/);
|
|
assert.match(setupSkill, /운영자만 서버 환경변수 `LAW_OC`/);
|
|
|
|
for (const doc of [setup, security, setupSkill]) {
|
|
assert.match(doc, /LAW_OC/);
|
|
assert.match(doc, /k-skill-proxy/);
|
|
}
|
|
|
|
assert.match(sources, /korean-law-mcp: https:\/\/github\.com\/chrisryugj\/korean-law-mcp/);
|
|
assert.match(roadmap, /한국 법령 검색 스킬 출시/);
|
|
|
|
for (const doc of [readme, install, setup, security, setupSkill, sources, featureDoc]) {
|
|
assert.doesNotMatch(doc, /법망|beopmang/i);
|
|
assert.doesNotMatch(doc, /api\.beopmang\.org/);
|
|
}
|
|
});
|
|
|
|
test("korean-law-search skill is proxy-first and drops the 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, /k-skill-proxy\.nomadamas\.org/);
|
|
assert.match(doc, /\/v1\/korean-law\/search/);
|
|
assert.match(doc, /\/v1\/korean-law\/detail/);
|
|
assert.match(doc, /target=law/);
|
|
assert.match(doc, /target=prec/);
|
|
assert.match(doc, /open\.law\.go\.kr/);
|
|
assert.match(doc, /github\.com\/chrisryugj\/korean-law-mcp/);
|
|
assert.match(doc, /KSKILL_PROXY_BASE_URL/);
|
|
assert.match(doc, /LAW_OC/);
|
|
assert.doesNotMatch(doc, /법망|beopmang/i);
|
|
assert.doesNotMatch(doc, /api\.beopmang\.org/);
|
|
assert.doesNotMatch(doc, /npm install -g korean-law-mcp/);
|
|
assert.doesNotMatch(doc, /packages\/korean-law-search/);
|
|
assert.doesNotMatch(doc, /python-packages\/korean-law-search/);
|
|
}
|
|
|
|
assert.match(doneSection, /target=prec/);
|
|
assert.match(doneSection, /target=ordin/);
|
|
|
|
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, /\| \[조선왕조실록 검색\]\(docs\/features\/joseon-sillok-search\.md\) \|/);
|
|
assert.match(readme, /\[조선왕조실록 검색\]\(docs\/features\/joseon-sillok-search\.md\)/);
|
|
assert.match(install, /--skill joseon-sillok-search/);
|
|
assert.match(
|
|
install,
|
|
/@nomadamas\/k-skill@0 exec joseon-sillok-search 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,
|
|
/@nomadamas\/k-skill@0 exec joseon-sillok-search 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(resolvePython(), ["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-heritage-search skill and official API", () => {
|
|
const readme = read("README.md");
|
|
const install = read(path.join("docs", "install.md"));
|
|
const featureDocPath = path.join(repoRoot, "docs", "features", "korean-heritage-search.md");
|
|
const featureDoc = read(path.join("docs", "features", "korean-heritage-search.md"));
|
|
const skillPath = path.join(repoRoot, "korean-heritage-search", "SKILL.md");
|
|
const skill = read(path.join("korean-heritage-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 heritage feature documentation to exist");
|
|
assert.ok(fs.existsSync(skillPath), "expected korean-heritage-search/SKILL.md to exist");
|
|
assert.match(readme, /\| \[국가유산 검색·행사 조회\]\(docs\/features\/korean-heritage-search\.md\) \|/);
|
|
assert.match(readme, /\[국가유산 검색·행사 조회\]\(docs\/features\/korean-heritage-search\.md\)/);
|
|
assert.match(install, /--skill korean-heritage-search/);
|
|
assert.match(skill, /SearchKindOpenapiList\.do/);
|
|
assert.match(skill, /SearchKindOpenapiDt\.do/);
|
|
assert.match(skill, /selectEventListOpenapi\.do/);
|
|
assert.match(featureDoc, /ccbaMnm1/);
|
|
assert.match(featureDoc, /ccbaKdcd/);
|
|
assert.match(sources, /https:\/\/www\.khs\.go\.kr\/cha\/SearchKindOpenapiList\.do/);
|
|
assert.match(roadmap, /한국 국가유산 검색·행사 조회 스킬 출시/);
|
|
});
|
|
|
|
test("korean-heritage-search install payload includes the documented helper commands", () => {
|
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "korean-heritage-"));
|
|
const installedSkillPath = path.join(tempRoot, "korean-heritage-search");
|
|
const bundledHelperPath = path.join(installedSkillPath, "scripts", "korean_heritage_search.py");
|
|
|
|
try {
|
|
fs.cpSync(path.join(repoRoot, "korean-heritage-search"), installedSkillPath, { recursive: true });
|
|
assert.ok(fs.existsSync(bundledHelperPath), "expected bundled heritage helper to exist");
|
|
const pythonCommand = process.platform === "win32" ? "python" : "python3";
|
|
const helpText = childProcess.execFileSync(pythonCommand, ["scripts/korean_heritage_search.py", "--help"], {
|
|
|
|
cwd: installedSkillPath,
|
|
encoding: "utf8",
|
|
});
|
|
|
|
assert.match(helpText, /Search official Korean heritage records and events/);
|
|
assert.match(helpText, /search/);
|
|
assert.match(helpText, /detail/);
|
|
assert.match(helpText, /events/);
|
|
} 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, /\| \[한국 특허 정보 검색\]\(docs\/features\/korean-patent-search\.md\) \|/);
|
|
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,
|
|
/@nomadamas\/k-skill@0 exec korean-patent-search 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,
|
|
/@nomadamas\/k-skill@0 exec korean-patent-search 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(resolvePython(), ["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, /\| \[한국 부동산 실거래가 조회\]\(docs\/features\/real-estate-search\.md\) \|/);
|
|
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, /\| \[장학금 검색 및 조회\]\(docs\/features\/korean-scholarship-search\.md\) \|/);
|
|
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(resolvePython(), [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(resolvePython(), [plannerPath, "--help"], {
|
|
cwd: repoRoot,
|
|
encoding: "utf8",
|
|
});
|
|
assert.match(plannerHelpText, /nationwide/i);
|
|
assert.match(plannerHelpText, /school-name/);
|
|
|
|
const filtered = JSON.parse(
|
|
childProcess.execFileSync(
|
|
resolvePython(),
|
|
[
|
|
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(
|
|
resolvePython(),
|
|
[
|
|
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(
|
|
resolvePython(),
|
|
[
|
|
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(
|
|
resolvePython(),
|
|
[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(
|
|
resolvePython(),
|
|
[
|
|
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, new RegExp(["Cloudflare", "Tunnel"].join(" "), "i"));
|
|
assert.doesNotMatch(doc, /launchd/i);
|
|
assert.doesNotMatch(doc, /docker compose/i);
|
|
}
|
|
});
|
|
|
|
test("repository docs advertise the housing-official-price skill and public direct realtyprice workflow", () => {
|
|
const readme = read("README.md");
|
|
const featureDocPath = path.join(repoRoot, "docs", "features", "housing-official-price.md");
|
|
const featureDoc = read(path.join("docs", "features", "housing-official-price.md"));
|
|
const skillPath = path.join(repoRoot, "housing-official-price", "SKILL.md");
|
|
const skill = read(path.join("housing-official-price", "SKILL.md"));
|
|
const sources = read(path.join("docs", "sources.md"));
|
|
const packageReadme = read(path.join("packages", "housing-official-price", "README.md"));
|
|
const packageJson = readJson(path.join("packages", "housing-official-price", "package.json"));
|
|
const rootPackageJson = readJson("package.json");
|
|
const realLookingFixturePattern = new RegExp([
|
|
"\\uB798\\uBBF8\\uC548",
|
|
"\\uB204\\uC0C1\\uB3D9",
|
|
["119", "98121"].join(""),
|
|
["11110", "10900"].join(""),
|
|
].join("|"), "u");
|
|
|
|
assert.ok(fs.existsSync(featureDocPath), "expected docs/features/housing-official-price.md to exist");
|
|
assert.ok(fs.existsSync(skillPath), "expected housing-official-price/SKILL.md to exist");
|
|
|
|
assert.match(readme, /\| \[한국 주택 공시가격 조회\]\(docs\/features\/housing-official-price\.md\) \| `housing-official-price` \|/);
|
|
assert.match(readme, /\[한국 주택 공시가격 조회\]\(docs\/features\/housing-official-price\.md\)/);
|
|
|
|
assert.equal(packageJson.name, "housing-official-price");
|
|
assert.equal(packageJson.license, "MIT");
|
|
assert.ok(packageJson.files.includes("src"), "package should ship runtime src");
|
|
assert.ok(packageJson.files.includes("README.md"), "package should ship README");
|
|
assert.equal(rootPackageJson.scripts["pack:dry-run"], "node scripts/pack-dry-run.js");
|
|
assert.ok(require("./ci-paths").listPublishableWorkspaces().includes("housing-official-price"));
|
|
|
|
for (const doc of [skill, featureDoc, packageReadme]) {
|
|
assert.match(doc, /realtyprice\.kr/);
|
|
assert.match(doc, /public web data surface|공개 웹 데이터 표면/);
|
|
assert.match(doc, /not a documented OpenAPI|공식 문서화된 OpenAPI가 아님/);
|
|
assert.match(doc, /lookupIndividualHousePriceByPnu/);
|
|
assert.match(doc, /searchApartmentCandidates/);
|
|
assert.match(doc, /lookupApartmentOfficialPrice/);
|
|
assert.match(doc, /modelMap/);
|
|
assert.match(doc, /UPSTREAM_TIMEOUT/);
|
|
assert.match(doc, /UPSTREAM_AMBIGUOUS_EMPTY/);
|
|
assert.match(doc, /9999999999199999999/);
|
|
assert.doesNotMatch(doc, /k-skill-proxy\.nomadamas\.org/);
|
|
assert.doesNotMatch(doc, realLookingFixturePattern);
|
|
}
|
|
|
|
for (const doc of [skill, featureDoc]) {
|
|
assert.match(doc, /gongsijiga-search[\s\S]*land official price|gongsijiga-search[\s\S]*개별공시지가/u);
|
|
assert.match(doc, /real-estate-search[\s\S]*transaction data|real-estate-search[\s\S]*실거래/u);
|
|
assert.match(doc, /building-register-search[\s\S]*register metadata|building-register-search[\s\S]*대장/u);
|
|
}
|
|
|
|
assert.match(sources, /realtyprice\.kr[\s\S]*hpindividual\/search\.htm/);
|
|
assert.match(sources, /realtyprice\.kr[\s\S]*m\/town\/search\.do/);
|
|
});
|
|
|
|
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, /\| \[한국 주식 정보 조회\]\(docs\/features\/korean-stock-search\.md\) \|/);
|
|
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,
|
|
/@nomadamas\/k-skill@0 exec korean-spell-check 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, /\| \[한국어 글자 수 세기\]\(docs\/features\/korean-character-count\.md\) \|/);
|
|
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,
|
|
/@nomadamas\/k-skill@0 exec korean-character-count 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,
|
|
/@nomadamas\/k-skill@0 exec korean-character-count 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, /\| \[근처 가장 싼 주유소 찾기\]\(docs\/features\/cheap-gas-nearby\.md\) \|/);
|
|
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, /\| \[한강 수위 정보 조회\]\(docs\/features\/han-river-water-level\.md\) \|/);
|
|
assert.match(readme, /\[한강 수위 정보 조회\]\(docs\/features\/han-river-water-level\.md\)/);
|
|
assert.match(install, /--skill han-river-water-level/);
|
|
|
|
for (const doc of [skill, featureDoc]) {
|
|
assert.match(doc, /\/v1\/han-river\/water-level/);
|
|
assert.match(doc, /stationName|station_code|stationCode/);
|
|
assert.match(doc, /수위|유량/);
|
|
assert.match(doc, /candidate_stations|ambiguous_station/);
|
|
assert.match(doc, /KSKILL_PROXY_BASE_URL/);
|
|
}
|
|
|
|
assert.match(featureDoc, /HRFCO_OPEN_API_KEY/);
|
|
|
|
assert.match(skill, /기본적으로 `https:\/\/k-skill-proxy\.nomadamas\.org\/v1\/han-river\/water-level`/);
|
|
assert.doesNotMatch(featureDoc, /기본 hosted 조회:/);
|
|
|
|
for (const doc of [proxyDoc, proxyReadme]) {
|
|
assert.match(doc, /\/v1\/han-river\/water-level/);
|
|
assert.match(doc, /HRFCO_OPEN_API_KEY/);
|
|
assert.match(doc, /waterlevel\/info\.json/);
|
|
assert.match(doc, /waterlevel\/list\/10M/);
|
|
}
|
|
|
|
assert.match(setup, /한강 수위 정보 조회 \| 사용자 시크릿 불필요/);
|
|
assert.match(setup, /한강 수위.*기본 hosted p/i);
|
|
assert.match(security, /KSKILL_PROXY_BASE_URL.*서울 지하철.*한국 날씨.*기본 hosted proxy|서울 지하철.*한국 날씨.*KSKILL_PROXY_BASE_URL.*기본 hosted proxy/);
|
|
assert.match(sources, /hrfco\.go\.kr\/web\/openapiPage\/reference\.do/);
|
|
assert.match(sources, /api\.hrfco\.go\.kr/);
|
|
assert.match(roadmap, /한강 수위 정보 조회 스킬 출시/);
|
|
});
|
|
|
|
test("repository docs advertise the MFDS drug and food safety skills", () => {
|
|
const readme = read("README.md");
|
|
const install = read(path.join("docs", "install.md"));
|
|
const sources = read(path.join("docs", "sources.md"));
|
|
const drugFeatureDocPath = path.join(repoRoot, "docs", "features", "mfds-drug-safety.md");
|
|
const foodFeatureDocPath = path.join(repoRoot, "docs", "features", "mfds-food-safety.md");
|
|
const drugSkillPath = path.join(repoRoot, "mfds-drug-safety", "SKILL.md");
|
|
const foodSkillPath = path.join(repoRoot, "mfds-food-safety", "SKILL.md");
|
|
|
|
assert.equal(fs.existsSync(drugFeatureDocPath), true);
|
|
assert.equal(fs.existsSync(foodFeatureDocPath), true);
|
|
assert.equal(fs.existsSync(drugSkillPath), true);
|
|
assert.equal(fs.existsSync(foodSkillPath), true);
|
|
assert.match(readme, /\| \[의약품 안전 체크\]\(docs\/features\/mfds-drug-safety\.md\) \|/);
|
|
assert.match(readme, /\| \[식품 안전 체크\]\(docs\/features\/mfds-food-safety\.md\) \|/);
|
|
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,
|
|
/@nomadamas\/k-skill@0 exec mfds-drug-safety 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,
|
|
/@nomadamas\/k-skill@0 exec mfds-food-safety 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, /\| \[올라포케 역삼 포케\]\(docs\/features\/hola-poke-yeoksam\.md\) \|/);
|
|
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, /\| \[도서관 도서 조회\]\(docs\/features\/library-book-search\.md\) \|/);
|
|
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, /\| \[한국 개인정보처리방침·이용약관 자동 생성\]\(docs\/features\/korean-privacy-terms\.md\) \|/);
|
|
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 install.sh warns on zero-byte upstream templates (#660)", () => {
|
|
const install = fs.readFileSync(
|
|
path.join(repoRoot, "korean-privacy-terms", "scripts", "install.sh"),
|
|
"utf8",
|
|
);
|
|
|
|
assert.match(
|
|
install,
|
|
/find\s+"?\$\{?CLONE_DIR\}?[\s\S]*-name\s+'\*\.tmpl'[\s\S]*-size 0/,
|
|
"install.sh must scan the pinned upstream checkout for zero-byte .tmpl templates",
|
|
);
|
|
assert.match(
|
|
install,
|
|
/0바이트 템플릿/,
|
|
"install.sh must warn in Korean when a zero-byte template is found",
|
|
);
|
|
});
|
|
|
|
test("korean-privacy-terms docs record the zero-byte Korean terms template limitation (#660)", () => {
|
|
const instruction = read(path.join("korean-privacy-terms", "instruction.md"));
|
|
const featureDoc = read(path.join("docs", "features", "korean-privacy-terms.md"));
|
|
|
|
for (const [label, doc] of [["instruction.md", instruction], ["docs/features/korean-privacy-terms.md", featureDoc]]) {
|
|
assert.match(doc, /terms-of-service\.ko\.mdx\.tmpl/, `${label} must name the missing template`);
|
|
assert.match(doc, /0바이트/, `${label} must record that the template is zero bytes`);
|
|
}
|
|
|
|
assert.match(
|
|
instruction,
|
|
/Failure modes[\s\S]*terms-of-service\.ko\.mdx\.tmpl/,
|
|
"instruction.md must document the limitation under Failure modes",
|
|
);
|
|
});
|
|
|
|
test("korean-privacy-terms bundles the Apache-2.0 LICENSE per §4(a) redistribution requirement", () => {
|
|
const licensePath = path.join(repoRoot, "korean-privacy-terms", "LICENSE.upstream");
|
|
|
|
assert.ok(
|
|
fs.existsSync(licensePath),
|
|
"expected korean-privacy-terms/LICENSE.upstream to exist (Apache-2.0 §4(a) requires redistributors to give recipients a copy of this License)",
|
|
);
|
|
|
|
const license = fs.readFileSync(licensePath, "utf8");
|
|
|
|
assert.match(license, /Apache License/);
|
|
assert.match(license, /Version 2\.0, January 2004/);
|
|
assert.match(license, /http:\/\/www\.apache\.org\/licenses\/LICENSE-2\.0/);
|
|
assert.match(license, /TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION/);
|
|
assert.match(license, /Redistribution\. You may reproduce and distribute/);
|
|
assert.match(license, /END OF TERMS AND CONDITIONS/);
|
|
assert.match(license, /APPENDIX: How to apply the Apache License/);
|
|
assert.match(license, /Copyright 2026 kimlawtech/);
|
|
|
|
const skill = read(path.join("korean-privacy-terms", "SKILL.md"));
|
|
const featureDoc = read(path.join("docs", "features", "korean-privacy-terms.md"));
|
|
|
|
assert.match(
|
|
skill,
|
|
/LICENSE\.upstream/,
|
|
"SKILL.md Notes section must link to LICENSE.upstream so §4(a) is satisfied even before install.sh runs",
|
|
);
|
|
assert.match(
|
|
featureDoc,
|
|
/LICENSE\.upstream/,
|
|
"docs/features/korean-privacy-terms.md must reference LICENSE.upstream",
|
|
);
|
|
});
|
|
|
|
test("korean-privacy-terms feature doc documents the thin-wrapper install flow and legal disclaimer", () => {
|
|
const featureDoc = read(path.join("docs", "features", "korean-privacy-terms.md"));
|
|
|
|
assert.match(featureDoc, /kimlawtech\/korean-privacy-terms/);
|
|
assert.match(featureDoc, /Apache-2\.0/);
|
|
assert.match(featureDoc, /~\/\.claude\/skills\/korean-privacy-terms/);
|
|
assert.match(featureDoc, /~\/\.agents\/skills\/korean-privacy-terms/);
|
|
assert.match(featureDoc, /scripts\/install\.sh/);
|
|
assert.match(featureDoc, /scripts\/upstream\.pin/);
|
|
assert.match(featureDoc, /참고용 초안/);
|
|
assert.match(featureDoc, /법률 자문/);
|
|
assert.match(featureDoc, /변호사 검토/);
|
|
assert.match(featureDoc, /2026\.9\.11/);
|
|
assert.match(featureDoc, /Next\.js/);
|
|
});
|
|
|
|
test("repository docs advertise the korean-jangbu-for thin-wrapper skill", () => {
|
|
const readme = read("README.md");
|
|
const install = read(path.join("docs", "install.md"));
|
|
const sources = read(path.join("docs", "sources.md"));
|
|
const featureDocPath = path.join(repoRoot, "docs", "features", "korean-jangbu-for.md");
|
|
const skillPath = path.join(repoRoot, "korean-jangbu-for", "SKILL.md");
|
|
|
|
assert.ok(fs.existsSync(featureDocPath), "expected docs/features/korean-jangbu-for.md to exist");
|
|
assert.ok(fs.existsSync(skillPath), "expected korean-jangbu-for/SKILL.md to exist");
|
|
assert.match(readme, /\| \[한국 사업자 장부 자동화\]\(docs\/features\/korean-jangbu-for\.md\) \|/);
|
|
assert.match(readme, /\[한국 사업자 장부 자동화\]\(docs\/features\/korean-jangbu-for\.md\)/);
|
|
assert.match(install, /--skill korean-jangbu-for/);
|
|
assert.match(sources, /https:\/\/github\.com\/kimlawtech\/korean-jangbu-for/);
|
|
assert.match(sources, /Apache-2\.0/);
|
|
assert.match(sources, /kimlawtech/);
|
|
assert.match(sources, /SpeciAI/);
|
|
});
|
|
|
|
test("korean-jangbu-for skill cites upstream author and enforces accounting/tax disclaimers", () => {
|
|
const skillPath = path.join(repoRoot, "korean-jangbu-for", "SKILL.md");
|
|
|
|
assert.ok(fs.existsSync(skillPath), "expected korean-jangbu-for/SKILL.md to exist");
|
|
|
|
const skill = read(path.join("korean-jangbu-for", "SKILL.md"));
|
|
|
|
assert.match(skill, /^name: korean-jangbu-for$/m);
|
|
assert.match(skill, /^license: Apache-2\.0$/m);
|
|
assert.match(skill, /^description: .*장부.*사업자.*$/m);
|
|
assert.match(skill, /https:\/\/github\.com\/kimlawtech\/korean-jangbu-for/);
|
|
assert.match(skill, /@kimlawtech/);
|
|
assert.match(skill, /SpeciAI/);
|
|
assert.match(skill, /Apache-2\.0/);
|
|
assert.match(skill, /참고용 초안/);
|
|
assert.match(skill, /공식 회계감사/);
|
|
assert.match(skill, /세무신고/);
|
|
assert.match(skill, /세무사 검토/);
|
|
assert.match(skill, /공인회계사/);
|
|
assert.match(skill, /CODEF/);
|
|
assert.match(skill, /BYOK/);
|
|
assert.match(skill, /~\/\.claude\/skills\/korean-jangbu-for/);
|
|
assert.match(skill, /~\/\.agents\/skills\/korean-jangbu-for/);
|
|
assert.match(skill, /scripts\/install\.sh/);
|
|
assert.match(skill, /scripts\/upstream\.pin/);
|
|
assert.match(skill, /DISCLAIMER\.md/);
|
|
assert.match(skill, /LICENSE\.upstream/);
|
|
assert.doesNotMatch(skill, /AskUserQuestion/);
|
|
});
|
|
|
|
test("korean-jangbu-for preserves upstream attribution, disclaimer, and Apache license", () => {
|
|
const noticePath = path.join(repoRoot, "korean-jangbu-for", "NOTICE");
|
|
const disclaimerPath = path.join(repoRoot, "korean-jangbu-for", "DISCLAIMER.md");
|
|
const licensePath = path.join(repoRoot, "korean-jangbu-for", "LICENSE.upstream");
|
|
|
|
assert.ok(fs.existsSync(noticePath), "expected korean-jangbu-for/NOTICE to exist");
|
|
assert.ok(fs.existsSync(disclaimerPath), "expected korean-jangbu-for/DISCLAIMER.md to exist");
|
|
assert.ok(fs.existsSync(licensePath), "expected korean-jangbu-for/LICENSE.upstream to exist");
|
|
|
|
const notice = read(path.join("korean-jangbu-for", "NOTICE"));
|
|
const disclaimer = read(path.join("korean-jangbu-for", "DISCLAIMER.md"));
|
|
const license = read(path.join("korean-jangbu-for", "LICENSE.upstream"));
|
|
|
|
assert.match(notice, /korean-jangbu-for/);
|
|
assert.match(notice, /Copyright 2026 kimlawtech/);
|
|
assert.match(notice, /SpeciAI/);
|
|
assert.match(notice, /@kimlawtech/);
|
|
assert.match(notice, /https:\/\/github\.com\/kimlawtech\/korean-jangbu-for/);
|
|
|
|
assert.match(disclaimer, /참고용 초안/);
|
|
assert.match(disclaimer, /공식 회계감사/);
|
|
assert.match(disclaimer, /세무신고/);
|
|
assert.match(disclaimer, /세무사 검토 필수/);
|
|
assert.match(disclaimer, /공인회계사 감사 필수/);
|
|
assert.match(disclaimer, /법적 효력/);
|
|
|
|
assert.match(license, /Apache License/);
|
|
assert.match(license, /Version 2\.0, January 2004/);
|
|
assert.match(license, /END OF TERMS AND CONDITIONS/);
|
|
assert.match(license, /Copyright 2026 kimlawtech \(SpeciAI\)/);
|
|
});
|
|
|
|
test("korean-jangbu-for ships an install.sh wrapper and a pinned upstream SHA", () => {
|
|
const pinPath = path.join(repoRoot, "korean-jangbu-for", "scripts", "upstream.pin");
|
|
const installPath = path.join(repoRoot, "korean-jangbu-for", "scripts", "install.sh");
|
|
|
|
assert.ok(fs.existsSync(pinPath), "expected korean-jangbu-for/scripts/upstream.pin to exist");
|
|
assert.ok(fs.existsSync(installPath), "expected korean-jangbu-for/scripts/install.sh to exist");
|
|
|
|
const pin = read(path.join("korean-jangbu-for", "scripts", "upstream.pin")).trim();
|
|
|
|
assert.match(pin, /^[0-9a-f]{40}$/, "upstream.pin must contain a single 40-char git SHA");
|
|
assert.notStrictEqual(pin, "0".repeat(40), "upstream.pin must not be a placeholder all-zero SHA");
|
|
|
|
const install = read(path.join("korean-jangbu-for", "scripts", "install.sh"));
|
|
|
|
assert.match(install, /^#!\/(?:usr\/bin\/env bash|bin\/bash)/m);
|
|
assert.match(install, /set -euo pipefail/);
|
|
assert.match(install, /~\/\.claude\/skills\/korean-jangbu-for/);
|
|
assert.match(install, /~\/\.agents\/skills\/korean-jangbu-for/);
|
|
assert.match(install, /https:\/\/github\.com\/kimlawtech\/korean-jangbu-for\.git/);
|
|
assert.match(install, /git clone --filter=blob:none/);
|
|
assert.match(install, /upstream\.pin/);
|
|
assert.match(install, /verify\.sh/);
|
|
assert.match(install, /Re-run this wrapper installer after upstream runtime install/);
|
|
|
|
const stat = fs.statSync(installPath);
|
|
assert.ok((stat.mode & 0o111) !== 0, "install.sh must be executable");
|
|
});
|
|
|
|
test("korean-jangbu-for installer registers upstream subskills for Claude and agents", () => {
|
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "korean-jangbu-for-install-"));
|
|
const homeDir = path.join(tmpDir, "home");
|
|
const upstreamDir = path.join(tmpDir, "upstream");
|
|
const installPath = path.join(repoRoot, "korean-jangbu-for", "scripts", "install.sh");
|
|
const upstreamSubskills = [
|
|
"jangbu-connect",
|
|
"jangbu-dash",
|
|
"jangbu-import",
|
|
"jangbu-jongso",
|
|
"jangbu-tag",
|
|
"jangbu-tax",
|
|
];
|
|
|
|
fs.mkdirSync(path.join(upstreamDir, "skills"), { recursive: true });
|
|
fs.mkdirSync(homeDir, { recursive: true });
|
|
|
|
for (const skillName of ["korean-jangbu-for", ...upstreamSubskills]) {
|
|
const skillDir = path.join(upstreamDir, "skills", skillName);
|
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(skillDir, "SKILL.md"),
|
|
`---\nname: ${skillName}\n---\n\n# ${skillName}\n`,
|
|
);
|
|
}
|
|
|
|
fs.mkdirSync(path.join(upstreamDir, "scripts"), { recursive: true });
|
|
fs.writeFileSync(path.join(upstreamDir, "scripts", "verify.sh"), "#!/usr/bin/env bash\nexit 0\n");
|
|
|
|
childProcess.execFileSync("git", ["init"], { cwd: upstreamDir, stdio: "ignore" });
|
|
childProcess.execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: upstreamDir });
|
|
childProcess.execFileSync("git", ["config", "user.name", "Test"], { cwd: upstreamDir });
|
|
childProcess.execFileSync("git", ["add", "."], { cwd: upstreamDir });
|
|
childProcess.execFileSync("git", ["commit", "-m", "seed upstream skills"], { cwd: upstreamDir, stdio: "ignore" });
|
|
const upstreamSha = childProcess.execFileSync("git", ["rev-parse", "HEAD"], { cwd: upstreamDir, encoding: "utf8" }).trim();
|
|
|
|
fs.mkdirSync(path.join(homeDir, ".claude", "skills"), { recursive: true });
|
|
fs.symlinkSync(
|
|
path.join(upstreamDir, "skills", "korean-jangbu-for"),
|
|
path.join(homeDir, ".claude", "skills", "korean-jangbu-for"),
|
|
);
|
|
|
|
childProcess.execFileSync("bash", [installPath], {
|
|
cwd: repoRoot,
|
|
env: {
|
|
...process.env,
|
|
HOME: homeDir,
|
|
KOREAN_JANGBU_FOR_UPSTREAM_REPO: upstreamDir,
|
|
KOREAN_JANGBU_FOR_UPSTREAM_SHA: upstreamSha,
|
|
},
|
|
stdio: "pipe",
|
|
});
|
|
|
|
for (const root of [".claude", ".agents"]) {
|
|
const skillRoot = path.join(homeDir, root, "skills");
|
|
|
|
assert.ok(
|
|
fs.existsSync(path.join(skillRoot, "korean-jangbu-for", "upstream", "skills", "jangbu-connect", "SKILL.md")),
|
|
`${root} should keep the pinned upstream checkout nested under the wrapper`,
|
|
);
|
|
assert.match(
|
|
fs.readFileSync(path.join(skillRoot, "korean-jangbu-for", "SKILL.md"), "utf8"),
|
|
/@kimlawtech|k-skill:cli-stub/,
|
|
`${root} should keep the korean-jangbu-for wrapper policy (or CLI stub) at the top level`,
|
|
);
|
|
assert.ok(
|
|
!fs.lstatSync(path.join(skillRoot, "korean-jangbu-for")).isSymbolicLink(),
|
|
`${root} should replace conflicting upstream korean-jangbu-for symlinks with a wrapper directory`,
|
|
);
|
|
|
|
for (const requiredPath of [
|
|
"scripts/install.sh",
|
|
"scripts/upstream.pin",
|
|
"LICENSE.upstream",
|
|
"DISCLAIMER.md",
|
|
"NOTICE",
|
|
]) {
|
|
assert.ok(
|
|
fs.existsSync(path.join(skillRoot, "korean-jangbu-for", requiredPath)),
|
|
`${root} should install wrapper support payload ${requiredPath}`,
|
|
);
|
|
}
|
|
|
|
for (const skillName of upstreamSubskills) {
|
|
const installedSubskillPath = path.join(skillRoot, skillName, "SKILL.md");
|
|
assert.ok(
|
|
fs.existsSync(installedSubskillPath),
|
|
`${root} should register upstream subskill ${skillName} as a top-level discoverable skill`,
|
|
);
|
|
|
|
const installedSubskill = fs.readFileSync(installedSubskillPath, "utf8");
|
|
assert.match(installedSubskill, /https:\/\/github\.com\/kimlawtech\/korean-jangbu-for/);
|
|
assert.match(installedSubskill, /@kimlawtech/);
|
|
assert.match(installedSubskill, /SpeciAI/);
|
|
assert.match(installedSubskill, /Apache-2\.0/);
|
|
assert.match(installedSubskill, /참고용 초안/);
|
|
assert.match(installedSubskill, /공식 회계감사/);
|
|
assert.match(installedSubskill, /세무신고/);
|
|
}
|
|
}
|
|
|
|
for (const installedRoot of [".claude", ".agents"]) {
|
|
childProcess.execFileSync("bash", [path.join(homeDir, installedRoot, "skills", "korean-jangbu-for", "scripts", "install.sh")], {
|
|
cwd: repoRoot,
|
|
env: {
|
|
...process.env,
|
|
HOME: homeDir,
|
|
KOREAN_JANGBU_FOR_UPSTREAM_REPO: upstreamDir,
|
|
KOREAN_JANGBU_FOR_UPSTREAM_SHA: upstreamSha,
|
|
},
|
|
stdio: "pipe",
|
|
});
|
|
}
|
|
});
|
|
|
|
test("korean-jangbu-for installer preflights promoted subskill collisions before home writes", () => {
|
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "korean-jangbu-for-collision-"));
|
|
const homeDir = path.join(tmpDir, "home");
|
|
const upstreamDir = path.join(tmpDir, "upstream");
|
|
const installPath = path.join(repoRoot, "korean-jangbu-for", "scripts", "install.sh");
|
|
const upstreamSubskills = [
|
|
"jangbu-connect",
|
|
"jangbu-dash",
|
|
"jangbu-import",
|
|
"jangbu-jongso",
|
|
"jangbu-tag",
|
|
"jangbu-tax",
|
|
];
|
|
|
|
for (const skillName of upstreamSubskills) {
|
|
fs.mkdirSync(path.join(upstreamDir, "skills", skillName), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(upstreamDir, "skills", skillName, "SKILL.md"),
|
|
`---\nname: ${skillName}\n---\n\n# ${skillName}\n`,
|
|
);
|
|
}
|
|
|
|
fs.mkdirSync(path.join(upstreamDir, "scripts"), { recursive: true });
|
|
fs.writeFileSync(path.join(upstreamDir, "scripts", "verify.sh"), "#!/usr/bin/env bash\nexit 0\n");
|
|
|
|
childProcess.execFileSync("git", ["init"], { cwd: upstreamDir, stdio: "ignore" });
|
|
childProcess.execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: upstreamDir });
|
|
childProcess.execFileSync("git", ["config", "user.name", "Test"], { cwd: upstreamDir });
|
|
childProcess.execFileSync("git", ["add", "."], { cwd: upstreamDir });
|
|
childProcess.execFileSync("git", ["commit", "-m", "seed upstream skills"], { cwd: upstreamDir, stdio: "ignore" });
|
|
const upstreamSha = childProcess.execFileSync("git", ["rev-parse", "HEAD"], { cwd: upstreamDir, encoding: "utf8" }).trim();
|
|
|
|
const unrelatedSkillDir = path.join(homeDir, ".agents", "skills", "jangbu-tax");
|
|
fs.mkdirSync(unrelatedSkillDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(unrelatedSkillDir, "SKILL.md"),
|
|
"---\nname: jangbu-tax\n---\n\n# user-authored jangbu-tax\n",
|
|
);
|
|
|
|
assert.throws(
|
|
() =>
|
|
childProcess.execFileSync("bash", [installPath], {
|
|
cwd: repoRoot,
|
|
env: {
|
|
...process.env,
|
|
HOME: homeDir,
|
|
KOREAN_JANGBU_FOR_UPSTREAM_REPO: upstreamDir,
|
|
KOREAN_JANGBU_FOR_UPSTREAM_SHA: upstreamSha,
|
|
},
|
|
stdio: "pipe",
|
|
}),
|
|
/refusing to overwrite unrelated skill/,
|
|
);
|
|
|
|
assert.match(
|
|
fs.readFileSync(path.join(unrelatedSkillDir, "SKILL.md"), "utf8"),
|
|
/user-authored jangbu-tax/,
|
|
"unrelated existing subskill should be preserved after installer refusal",
|
|
);
|
|
|
|
for (const root of [".claude", ".agents"]) {
|
|
const skillRoot = path.join(homeDir, root, "skills");
|
|
assert.ok(
|
|
!fs.existsSync(path.join(skillRoot, "korean-jangbu-for")),
|
|
`${root} should not create the wrapper directory after a promoted-subskill preflight failure`,
|
|
);
|
|
|
|
for (const skillName of upstreamSubskills.filter((name) => name !== "jangbu-tax")) {
|
|
assert.ok(
|
|
!fs.existsSync(path.join(skillRoot, skillName)),
|
|
`${root} should not create promoted subskill ${skillName} after a promoted-subskill preflight failure`,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("korean-jangbu-for feature doc documents source-first use and mandatory attribution", () => {
|
|
const featureDoc = read(path.join("docs", "features", "korean-jangbu-for.md"));
|
|
|
|
assert.match(featureDoc, /kimlawtech\/korean-jangbu-for/);
|
|
assert.match(featureDoc, /https:\/\/github\.com\/kimlawtech\/korean-jangbu-for/);
|
|
assert.match(featureDoc, /@kimlawtech/);
|
|
assert.match(featureDoc, /SpeciAI/);
|
|
assert.match(featureDoc, /Apache-2\.0/);
|
|
assert.match(featureDoc, /~\/\.claude\/skills\/korean-jangbu-for/);
|
|
assert.match(featureDoc, /~\/\.agents\/skills\/korean-jangbu-for/);
|
|
assert.match(featureDoc, /scripts\/install\.sh/);
|
|
assert.match(featureDoc, /scripts\/upstream\.pin/);
|
|
assert.match(featureDoc, /CODEF/);
|
|
assert.match(featureDoc, /BYOK/);
|
|
assert.match(featureDoc, /세무사 검토/);
|
|
assert.match(featureDoc, /공인회계사/);
|
|
});
|
|
|
|
test("corporate-registration-consulting skill covers court registry workflow, tax pitfalls, and rhwp automation", () => {
|
|
const skillPath = path.join(repoRoot, "corporate-registration-consulting", "SKILL.md");
|
|
const featureDocPath = path.join(repoRoot, "docs", "features", "corporate-registration-consulting.md");
|
|
|
|
assert.ok(fs.existsSync(skillPath), "expected corporate-registration-consulting/SKILL.md to exist");
|
|
assert.ok(fs.existsSync(featureDocPath), "expected corporate-registration-consulting feature doc to exist");
|
|
const documentPackTemplatePath = path.join(
|
|
"corporate-registration-consulting",
|
|
"templates",
|
|
"incorporation-document-pack.md",
|
|
);
|
|
const officialFormSourcesPath = path.join(
|
|
"corporate-registration-consulting",
|
|
"templates",
|
|
"official-form-sources.md",
|
|
);
|
|
const officialPromoterHwpPath = path.join(
|
|
"corporate-registration-consulting",
|
|
"templates",
|
|
"official",
|
|
"form-65-1-stock-company-incorporation-promoter.hwp",
|
|
);
|
|
const officialSubscriptionHwpPath = path.join(
|
|
"corporate-registration-consulting",
|
|
"templates",
|
|
"official",
|
|
"form-65-2-stock-company-incorporation-subscription.hwp",
|
|
);
|
|
const officialFillMapPath = path.join(
|
|
"corporate-registration-consulting",
|
|
"templates",
|
|
"official",
|
|
"form-65-1-fill-map.json",
|
|
);
|
|
const officialSourceManifestPath = path.join(
|
|
"corporate-registration-consulting",
|
|
"templates",
|
|
"official",
|
|
"source-manifest.json",
|
|
);
|
|
const officialFillScriptPath = path.join(
|
|
"corporate-registration-consulting",
|
|
"scripts",
|
|
"fill_official_hwp.py",
|
|
);
|
|
const attachmentHwpDir = path.join("corporate-registration-consulting", "templates", "attachment-hwp");
|
|
const attachmentHwpArtifacts = [
|
|
"articles-of-incorporation.hwp",
|
|
"standard-articles-startup-moj.hwp",
|
|
"share-issuance-consent.hwp",
|
|
"share-subscription.hwp",
|
|
"founder-meeting-minutes.hwp",
|
|
"founder-meeting-period-shortening-consent.hwp",
|
|
"shareholder-register.hwp",
|
|
"inspection-report.hwp",
|
|
"officer-acceptance-director-ceo.hwp",
|
|
"officer-acceptance-auditor.hwp",
|
|
"board-minutes.hwp",
|
|
"corporate-seal-report.hwp",
|
|
"power-of-attorney.hwp",
|
|
].map((fileName) => path.join(attachmentHwpDir, fileName));
|
|
|
|
assert.ok(
|
|
fs.existsSync(path.join(repoRoot, documentPackTemplatePath)),
|
|
"expected an incorporation document pack template artifact",
|
|
);
|
|
assert.ok(
|
|
fs.existsSync(path.join(repoRoot, officialFormSourcesPath)),
|
|
"expected an official form sources artifact",
|
|
);
|
|
for (const artifactPath of [officialPromoterHwpPath, officialSubscriptionHwpPath, officialFillMapPath, officialSourceManifestPath, officialFillScriptPath, ...attachmentHwpArtifacts, path.join(attachmentHwpDir, "source-manifest.json")]) {
|
|
assert.ok(fs.existsSync(path.join(repoRoot, artifactPath)), `expected ${artifactPath} to exist`);
|
|
}
|
|
|
|
const documentPackTemplate = read(documentPackTemplatePath);
|
|
const officialFormSources = read(officialFormSourcesPath);
|
|
const officialFillMap = JSON.parse(read(officialFillMapPath));
|
|
const officialSourceManifest = JSON.parse(read(officialSourceManifestPath));
|
|
const attachmentSourceManifest = JSON.parse(read(path.join(attachmentHwpDir, "source-manifest.json")));
|
|
for (const hwpPath of [officialPromoterHwpPath, officialSubscriptionHwpPath, ...attachmentHwpArtifacts]) {
|
|
const hwpMagic = fs.readFileSync(path.join(repoRoot, hwpPath)).subarray(0, 8).toString("hex");
|
|
assert.equal(hwpMagic, "d0cf11e0a1b11ae1", `${hwpPath} should be an OLE HWP file`);
|
|
}
|
|
const skill = read(path.join("corporate-registration-consulting", "SKILL.md"));
|
|
const featureDoc = read(path.join("docs", "features", "corporate-registration-consulting.md"));
|
|
const readme = read("README.md");
|
|
const install = read(path.join("docs", "install.md"));
|
|
const sources = read(path.join("docs", "sources.md"));
|
|
|
|
assert.match(skill, /^---\nname: corporate-registration-consulting\n/);
|
|
assert.match(skill, /참고용/);
|
|
assert.match(skill, /법률 자문/);
|
|
assert.match(skill, /변호사|법무사|세무사/);
|
|
assert.match(skill, /상호/);
|
|
assert.match(skill, /정관/);
|
|
assert.match(skill, /잔고증명|주금납입/);
|
|
assert.match(skill, /취임승낙서/);
|
|
assert.match(skill, /조사보고서/);
|
|
assert.match(skill, /등록면허세/);
|
|
assert.match(skill, /과밀억제권역/);
|
|
assert.match(skill, /지방세법 제28조/);
|
|
assert.match(skill, /소프트웨어/);
|
|
assert.match(skill, /주민등록번호/);
|
|
assert.match(skill, /마스킹/);
|
|
assert.match(skill, /에이전트는[\s\S]*로그인[\s\S]*전자서명[\s\S]*세금 납부[\s\S]*등기 제출[\s\S]*수행하지 않는다/);
|
|
assert.match(skill, /사용자 사칭[\s\S]*수행하지 않는다/);
|
|
assert.match(skill, /최종 법률 판단[\s\S]*수행하지 않는다/);
|
|
assert.match(skill, /최종 세무 판단[\s\S]*수행하지 않는다/);
|
|
assert.match(skill, /모집설립/);
|
|
assert.match(skill, /모집설립은 일반적이지[\s\S]*기본 플로우에서는 제외/);
|
|
assert.match(skill, /저장된 발기설립 양식과 첨부서류 양식/);
|
|
assert.match(skill, /등기신청양식/);
|
|
assert.match(skill, /첨부서면예시/);
|
|
assert.match(skill, /상업등기신청서의 양식에 관한 예규/);
|
|
assert.match(skill, /official-form-sources\.md/);
|
|
assert.match(skill, /form-65-1-stock-company-incorporation-promoter\.hwp/);
|
|
assert.match(skill, /fill_official_hwp\.py/);
|
|
assert.match(skill, /form-65-1-fill-map\.json/);
|
|
assert.match(skill, /기본 산출물은 실제 HWP 파일/);
|
|
assert.match(skill, /Markdown만 반환하지 말고/);
|
|
assert.match(skill, /dummy[\s\S]*지양|dummy[\s\S]*남기지 않는다/);
|
|
assert.match(skill, /반드시 작성할 문서와 저장된 양식 경로/);
|
|
assert.match(skill, /저장해 둔 HWP 양식을 우선 활용|저장된 HWP 양식을 우선 활용/);
|
|
assert.match(skill, /에이전트가 매번 공식 양식을 새로 찾게 하지 말고/);
|
|
assert.match(skill, /정관은 최대한 저장된 표준정관을 그대로 따른다/);
|
|
assert.match(skill, /정관[\s\S]*제2조[\s\S]*업태·종목/);
|
|
assert.match(skill, /정관[\s\S]*맨 마지막[\s\S]*작성일자[\s\S]*서명|정관[\s\S]*맨 마지막[\s\S]*날짜[\s\S]*기명날인/);
|
|
assert.match(skill, /간인/);
|
|
assert.match(skill, /법인인감/);
|
|
assert.match(skill, /templates\/attachment-hwp\//);
|
|
assert.match(skill, /자리표시자/);
|
|
assert.match(skill, /articles-of-incorporation\.hwp/);
|
|
assert.match(skill, /founder-meeting-minutes\.hwp/);
|
|
assert.match(skill, /share-subscription\.hwp/);
|
|
assert.match(skill, /inspection-report\.hwp/);
|
|
assert.match(skill, /officer-acceptance-director-ceo\.hwp/);
|
|
assert.match(skill, /rhwp-edit/);
|
|
assert.match(skill, /k-skill-rhwp/);
|
|
assert.match(skill, /본문[\s\S]*replace-all|replace-all[\s\S]*본문/);
|
|
assert.match(skill, /replace-all[\s\S]*shortcut/);
|
|
assert.match(skill, /한 장 한 장[\s\S]*순차/);
|
|
assert.match(skill, /set-cell-text/);
|
|
assert.match(skill, /info.*list-paragraphs/);
|
|
assert.match(skill, /render|kordoc/);
|
|
assert.match(skill, /표.*셀/);
|
|
assert.match(skill, /레포.*밖|레포.*외부|저장소.*밖|저장소.*외부/);
|
|
assert.doesNotMatch(skill, /\.\/out\/court-form-filled\.hwp/);
|
|
assert.match(skill, /등기신청수수료 영수필확인서/);
|
|
assert.match(skill, /등기이사[\s\S]*인감증명서 또는 본인서명사실확인서/);
|
|
assert.match(skill, /등기이사[\s\S]*주민등록초본\/등본/);
|
|
assert.match(skill, /상법 제291조/);
|
|
assert.match(skill, /명의개서대리인/);
|
|
assert.match(skill, /현물출자[\s\S]*재산인수/);
|
|
assert.match(skill, /인허가/);
|
|
assert.match(skill, /정관 공증|의사록 인증/);
|
|
assert.match(skill, /법인명/);
|
|
assert.match(skill, /이사/);
|
|
assert.match(skill, /주소/);
|
|
assert.match(skill, /쉬운 말/);
|
|
assert.match(skill, /사용자 결정/);
|
|
assert.match(documentPackTemplate, /취임승낙서/);
|
|
assert.match(documentPackTemplate, /인감신고서/);
|
|
assert.match(documentPackTemplate, /등록면허세/);
|
|
assert.match(documentPackTemplate, /등기신청수수료 영수필확인서/);
|
|
assert.match(documentPackTemplate, /등기이사[\s\S]*인감증명서 또는 본인서명사실확인서/);
|
|
assert.match(documentPackTemplate, /등기이사[\s\S]*주민등록초본\/등본/);
|
|
assert.match(documentPackTemplate, /명의개서대리인/);
|
|
assert.match(documentPackTemplate, /현물출자[\s\S]*재산인수/);
|
|
assert.match(documentPackTemplate, /인허가/);
|
|
assert.match(documentPackTemplate, /정관 공증|의사록 인증/);
|
|
assert.match(documentPackTemplate, /개인정보|민감정보/);
|
|
assert.match(documentPackTemplate, /양식별 수정 위치/);
|
|
assert.match(documentPackTemplate, /간인/);
|
|
assert.match(documentPackTemplate, /법인인감/);
|
|
assert.match(documentPackTemplate, /레포.*커밋/);
|
|
assert.match(documentPackTemplate, /\{\{INSPECTION_CONCLUSION_AFTER_USER_OR_EXPERT_REVIEW\}\}/);
|
|
assert.doesNotMatch(documentPackTemplate, /중대한 흠이 없음을 보고합니다/);
|
|
assert.match(officialFormSources, /인터넷등기소/);
|
|
assert.match(officialFormSources, /등기신청양식/);
|
|
assert.match(officialFormSources, /첨부서면예시/);
|
|
assert.match(officialFormSources, /상업등기신청서의 양식에 관한 예규/);
|
|
assert.match(officialFormSources, /양식 제65-1호/);
|
|
assert.match(officialFormSources, /양식 제65-2호/);
|
|
assert.match(officialFormSources, /이미 저장된 HWP 양식 경로/);
|
|
assert.match(officialFormSources, /새 양식을 찾는 것부터 시작하지 말고/);
|
|
assert.match(officialFormSources, /모집설립[\s\S]*기본 플로우에서는 사용하지 않음/);
|
|
assert.match(officialFormSources, /HWP\/HWPX\/PDF/);
|
|
assert.match(officialFormSources, /templates\/official/);
|
|
assert.match(officialFormSources, /form-65-1-stock-company-incorporation-promoter\.hwp/);
|
|
assert.match(officialFormSources, /form-65-2-stock-company-incorporation-subscription\.hwp/);
|
|
assert.match(officialFormSources, /fill_official_hwp\.py/);
|
|
assert.match(officialFormSources, /templates\/attachment-hwp\//);
|
|
assert.match(officialFormSources, /실제 공개 배포 첨부서류 HWP 양식 묶음/);
|
|
assert.match(officialFormSources, /자리표시자/);
|
|
assert.match(officialFormSources, /articles-of-incorporation\.hwp/);
|
|
assert.match(officialFormSources, /corporate-seal-report\.hwp/);
|
|
assert.match(officialFormSources, /최신본을 다시 확인|최신 예규/);
|
|
assert.match(officialFormSources, /replace-all[\s\S]*shortcut/);
|
|
assert.match(officialFormSources, /간인/);
|
|
assert.match(officialFormSources, /법인인감/);
|
|
assert.match(officialFormSources, /등기신청수수료 영수필확인서/);
|
|
assert.match(officialFormSources, /등기이사[\s\S]*인감증명서 또는 본인서명사실확인서/);
|
|
assert.match(officialFormSources, /등기이사[\s\S]*주민등록초본\/등본/);
|
|
assert.match(officialFormSources, /상업등기규칙 제129조/);
|
|
assert.match(officialFormSources, /명의개서대리인/);
|
|
assert.match(officialFormSources, /현물출자[\s\S]*재산인수/);
|
|
assert.match(officialFormSources, /인허가/);
|
|
assert.match(officialFormSources, /정관 공증|의사록 인증/);
|
|
assert.equal(officialFillMap.fields.company_name.cell, 21);
|
|
assert.equal(officialFillMap.fields.head_office_address.cell, 23);
|
|
assert.equal(officialFillMap.fields.capital_krw.cell, 33);
|
|
assert.equal(officialSourceManifest.source.adm_rul_seq, "2200000106061");
|
|
assert.match(officialSourceManifest.files[0].sha256, /^[a-f0-9]{64}$/);
|
|
assert.match(attachmentSourceManifest.note, /sanitized to placeholders|자리표시자/);
|
|
|
|
assert.match(featureDoc, /법인등기 신청 컨설팅/);
|
|
assert.match(featureDoc, /정관/);
|
|
assert.match(featureDoc, /등록면허세/);
|
|
assert.match(featureDoc, /과밀억제권역/);
|
|
assert.match(featureDoc, /조세특례제한법 제6조/);
|
|
assert.match(featureDoc, /지방세법 제28조/);
|
|
assert.match(featureDoc, /에이전트는[\s\S]*로그인[\s\S]*전자서명[\s\S]*세금 납부[\s\S]*등기 제출[\s\S]*지원하지 않는다|에이전트는[\s\S]*로그인[\s\S]*전자서명[\s\S]*세금 납부[\s\S]*등기 제출[\s\S]*수행하지 않는다/);
|
|
assert.match(featureDoc, /사용자 사칭[\s\S]*(지원하지 않는다|수행하지 않는다|사용자가 직접 또는 전문가)/);
|
|
assert.match(featureDoc, /최종 법률 판단[\s\S]*(지원하지 않는다|수행하지 않는다|사용자가 직접 또는 전문가)/);
|
|
assert.match(featureDoc, /최종 세무 판단[\s\S]*(지원하지 않는다|수행하지 않는다|사용자가 직접 또는 전문가)/);
|
|
assert.match(featureDoc, /개인정보|민감정보/);
|
|
assert.match(featureDoc, /자리표시자/);
|
|
assert.match(featureDoc, /최종 산출물은 실제 `?\.hwp`? 사본/);
|
|
assert.match(featureDoc, /저장된 HWP 양식 사본/);
|
|
assert.match(featureDoc, /표 셀|set-cell-text/);
|
|
assert.match(featureDoc, /모집설립/);
|
|
assert.match(featureDoc, /모집설립은 일반적이지[\s\S]*기본 플로우/);
|
|
assert.match(featureDoc, /저장된 양식 경로/);
|
|
assert.match(featureDoc, /필수 문서와 양식/);
|
|
assert.match(featureDoc, /replace-all/);
|
|
assert.match(featureDoc, /replace-all[\s\S]*shortcut/);
|
|
assert.match(featureDoc, /한 장 한 장[\s\S]*순차/);
|
|
assert.match(featureDoc, /표 셀|set-cell-text/);
|
|
assert.match(featureDoc, /간인/);
|
|
assert.match(featureDoc, /법인인감/);
|
|
assert.match(featureDoc, /등기신청수수료 영수필확인서/);
|
|
assert.match(featureDoc, /등기이사[\s\S]*인감증명서\/본인서명사실확인서|등기이사[\s\S]*인감증명서 또는 본인서명사실확인서/);
|
|
assert.match(featureDoc, /등기이사[\s\S]*주민등록초본\/등본/);
|
|
assert.match(featureDoc, /상법 제291조/);
|
|
assert.match(featureDoc, /명의개서대리인/);
|
|
assert.match(featureDoc, /현물출자[\s\S]*재산인수/);
|
|
assert.match(featureDoc, /인허가/);
|
|
assert.match(featureDoc, /정관 공증|의사록 인증/);
|
|
assert.match(featureDoc, /업태·종목/);
|
|
assert.match(featureDoc, /표.*셀/);
|
|
assert.match(featureDoc, /세금 납부/);
|
|
assert.match(featureDoc, /실제 제출은 사용자가 직접/);
|
|
assert.match(featureDoc, /인터넷등기소|온라인법인설립시스템/);
|
|
assert.match(featureDoc, /form-65-1-stock-company-incorporation-promoter\.hwp/);
|
|
assert.doesNotMatch(featureDoc, /양식 제65-2호/);
|
|
assert.match(featureDoc, /templates\/official\/\*\.hwp|templates\/official\//);
|
|
assert.match(featureDoc, /templates\/attachment-hwp\//);
|
|
assert.match(featureDoc, /templates\/attachment-hwp\/\*\.hwp|첨부/);
|
|
assert.match(featureDoc, /참고용/);
|
|
|
|
assert.match(readme, /\| \[법인등기 신청 컨설팅\]\(docs\/features\/corporate-registration-consulting\.md\) \|/);
|
|
assert.match(readme, /docs\/features\/corporate-registration-consulting\.md/);
|
|
assert.match(install, /--skill corporate-registration-consulting/);
|
|
assert.match(sources, /corporate-registration-consulting/);
|
|
assert.match(sources, /startbiz\.go\.kr/);
|
|
assert.match(sources, /law\.go\.kr/);
|
|
});
|
|
|
|
test("iros-registry-automation skill documents safe IROS registry certificate automation and upstream credit", () => {
|
|
const skillPath = path.join(repoRoot, "iros-registry-automation", "SKILL.md");
|
|
const featureDocPath = path.join(repoRoot, "docs", "features", "iros-registry-automation.md");
|
|
const upstreamPinPath = path.join(repoRoot, "iros-registry-automation", "scripts", "upstream.pin");
|
|
|
|
assert.ok(fs.existsSync(skillPath), "expected iros-registry-automation/SKILL.md to exist");
|
|
assert.ok(fs.existsSync(featureDocPath), "expected iros-registry-automation feature doc to exist");
|
|
assert.ok(fs.existsSync(upstreamPinPath), "expected iros-registry-automation/scripts/upstream.pin to exist");
|
|
|
|
const skill = read(path.join("iros-registry-automation", "SKILL.md"));
|
|
const featureDoc = read(path.join("docs", "features", "iros-registry-automation.md"));
|
|
const upstreamPin = read(path.join("iros-registry-automation", "scripts", "upstream.pin")).trim();
|
|
const readme = read("README.md");
|
|
const install = read(path.join("docs", "install.md"));
|
|
const sources = read(path.join("docs", "sources.md"));
|
|
const roadmap = read(path.join("docs", "roadmap.md"));
|
|
|
|
assert.match(upstreamPin, /^[0-9a-f]{40}$/, "upstream pin should be a reviewed 40-character Git SHA");
|
|
|
|
assert.match(skill, /^---\nname: iros-registry-automation\n/);
|
|
assert.match(skill, /등기부등본|등기사항증명서/);
|
|
assert.match(skill, /법인/);
|
|
assert.match(skill, /부동산/);
|
|
assert.match(skill, /인터넷등기소|IROS/);
|
|
assert.match(skill, /TouchEn nxKey/);
|
|
assert.match(skill, /Playwright|Chromium/);
|
|
assert.match(skill, /로그인[\s\S]*수동|사용자.*직접.*로그인/);
|
|
assert.match(skill, /결제[\s\S]*수동|사용자.*직접.*결제/);
|
|
assert.match(skill, /카드|공동인증서|간편인증/);
|
|
assert.match(skill, /페이지당\s*10건|10건/);
|
|
assert.match(skill, /장바구니/);
|
|
assert.match(skill, /열람/);
|
|
assert.match(skill, /저장|다운로드/);
|
|
assert.match(skill, /법인등록번호/);
|
|
assert.match(skill, /상호명/);
|
|
assert.match(skill, /주소/);
|
|
assert.match(skill, /개인정보|민감정보/);
|
|
assert.match(skill, /법률 자문|참고용/);
|
|
assert.match(skill, /challengekim/);
|
|
assert.match(skill, /https:\/\/github\.com\/challengekim\/iros-registry-automation/);
|
|
assert.match(skill, /MIT/);
|
|
assert.match(skill, /원 저작자|원저작자|upstream|참고 구현/);
|
|
assert.match(skill, new RegExp(`git checkout ${upstreamPin}`), "skill install flow should check out the pinned upstream SHA");
|
|
assert.match(skill, /scripts\/upstream\.pin/, "skill should document where the reviewed upstream pin lives");
|
|
assert.match(skill, /pin update|핀 업데이트|업스트림 핀|review/i, "skill should require review before updating the pin");
|
|
assert.match(skill, /\$workdir\/corp-input\.json/, "skill should put real corporate inputs under the private workdir");
|
|
assert.match(
|
|
skill,
|
|
/\$workdir\/companies-input\.json[\s\S]*(iros_download\.py|결제 후|열람|저장)/,
|
|
"skill should prepare the company-name list required by iros_download.py before the download flow after payment",
|
|
);
|
|
assert.match(
|
|
skill,
|
|
/"excel_path":\s*str\(workdir \/ "customer-list\.xlsx"\)/,
|
|
"skill should redirect upstream customer Excel input to the private workdir",
|
|
);
|
|
assert.match(skill, /\$workdir\/downloads/, "skill should point save_dir/download output at the private workdir");
|
|
assert.match(skill, /config\.json[\s\S]*save_dir[\s\S]*\$workdir/, "skill should wire config.json save_dir to the private workdir");
|
|
assert.match(skill, /upstream repo `data\/`|upstream `data\/`|data\/.*실제/, "skill should warn not to use upstream data/ for real inputs");
|
|
|
|
for (const doc of [featureDoc, sources]) {
|
|
assert.match(doc, /challengekim/);
|
|
assert.match(doc, /https:\/\/github\.com\/challengekim\/iros-registry-automation/);
|
|
assert.match(doc, /인터넷등기소|IROS|iros\.go\.kr/);
|
|
}
|
|
|
|
assert.match(featureDoc, new RegExp(`git checkout ${upstreamPin}`), "feature doc install flow should check out the pinned upstream SHA");
|
|
assert.match(featureDoc, /scripts\/upstream\.pin/, "feature doc should document where the reviewed upstream pin lives");
|
|
assert.match(featureDoc, /pin update|핀 업데이트|업스트림 핀|review/i, "feature doc should require review before updating the pin");
|
|
assert.match(featureDoc, /로그인[\s\S]*수동|사용자.*직접.*로그인/);
|
|
assert.match(featureDoc, /결제[\s\S]*수동|사용자.*직접.*결제/);
|
|
assert.match(featureDoc, /법인[\s\S]*장바구니[\s\S]*열람[\s\S]*저장/);
|
|
assert.match(featureDoc, /부동산[\s\S]*장바구니[\s\S]*수동/);
|
|
assert.match(featureDoc, /TouchEn nxKey/);
|
|
assert.match(featureDoc, /페이지당\s*10건|10건/);
|
|
assert.match(featureDoc, /i?ros_cart_by_corpnum\.py|법인등록번호 기반/);
|
|
assert.match(featureDoc, /i?ros_cart_realty\.py|부동산 장바구니/);
|
|
assert.match(featureDoc, /저장소 밖|레포 밖|커밋하지/);
|
|
assert.match(featureDoc, /\$workdir\/corp-input\.json/, "feature doc should put real corporate inputs under the private workdir");
|
|
assert.match(
|
|
featureDoc,
|
|
/\$workdir\/companies-input\.json[\s\S]*(iros_download\.py|결제 후|열람|저장)/,
|
|
"feature doc should prepare the company-name list required by iros_download.py before the download flow after payment",
|
|
);
|
|
assert.match(
|
|
featureDoc,
|
|
/"excel_path":\s*str\(workdir \/ "customer-list\.xlsx"\)/,
|
|
"feature doc should redirect upstream customer Excel input to the private workdir",
|
|
);
|
|
assert.match(featureDoc, /\$workdir\/downloads/, "feature doc should point save_dir/download output at the private workdir");
|
|
assert.match(featureDoc, /config\.json[\s\S]*save_dir[\s\S]*\$workdir/, "feature doc should wire config.json save_dir to the private workdir");
|
|
assert.match(featureDoc, /upstream repo `data\/`|upstream `data\/`|data\/.*실제/, "feature doc should warn not to use upstream data/ for real inputs");
|
|
assert.doesNotMatch(featureDoc, /결제.*자동화.*지원/);
|
|
|
|
assert.match(readme, /\| \[등기부등본 자동화\]\(docs\/features\/iros-registry-automation\.md\) \| `iros-registry-automation` \|/);
|
|
assert.match(readme, /docs\/features\/iros-registry-automation\.md/);
|
|
assert.match(install, /--skill iros-registry-automation/);
|
|
assert.match(roadmap, /등기부등본 자동화 스킬 출시/);
|
|
});
|
|
|
|
test("rhwp-edit skill pins the k-skill-rhwp CLI as the editing engine and disclaims kordoc/rhwp-advanced routing", () => {
|
|
const skill = read(path.join("rhwp-edit", "SKILL.md"));
|
|
|
|
assert.match(skill, /^---\nname: rhwp-edit\n/);
|
|
assert.match(skill, /k-skill-rhwp/);
|
|
assert.match(skill, /@rhwp\/core/);
|
|
assert.match(skill, /hwp\/SKILL\.md/);
|
|
assert.match(skill, /rhwp-advanced\/SKILL\.md/);
|
|
assert.match(skill, /insert-text/);
|
|
assert.match(skill, /delete-text/);
|
|
assert.match(skill, /replace-all/);
|
|
assert.match(skill, /create-table/);
|
|
assert.match(skill, /set-cell-text/);
|
|
assert.match(skill, /create-blank/);
|
|
assert.match(skill, /#196/);
|
|
assert.match(skill, /본문 문단만/, "SKILL.md must document body-only scope for search/replace-all");
|
|
assert.match(skill, /set-cell-text/, "SKILL.md must reference set-cell-text for cell content workflow");
|
|
assert.match(skill, /non-overlapping|개행|문단 경계/, "SKILL.md must document replace-all edge cases");
|
|
assert.match(
|
|
skill,
|
|
/UTF-?16|U\+0130|İ|case[ -]?fold/i,
|
|
"SKILL.md must disclose the case-insensitive UTF-16 length-drift guard (Unicode follow-up)"
|
|
);
|
|
});
|
|
|
|
test("rhwp-advanced skill pins the upstream rhwp Rust CLI debug/dump/convert surface", () => {
|
|
const skill = read(path.join("rhwp-advanced", "SKILL.md"));
|
|
|
|
assert.match(skill, /^---\nname: rhwp-advanced\n/);
|
|
assert.match(skill, /cargo install rhwp/);
|
|
assert.match(skill, /export-svg/);
|
|
assert.match(skill, /--debug-overlay/);
|
|
assert.match(skill, /\brhwp dump\b/);
|
|
assert.match(skill, /dump-pages/);
|
|
assert.match(skill, /ir-diff/);
|
|
assert.match(skill, /thumbnail/);
|
|
assert.match(skill, /\brhwp convert\b/);
|
|
assert.match(skill, /편집 서브커맨드[가는]? (없다|부재|제공하지 않는다|않는다)/);
|
|
assert.match(skill, /rhwp-edit/);
|
|
});
|
|
|
|
test("rhwp feature docs, README, install, roadmap, and sources are wired for the new skills", () => {
|
|
const readme = read("README.md");
|
|
const install = read(path.join("docs", "install.md"));
|
|
const roadmap = read(path.join("docs", "roadmap.md"));
|
|
const sources = read(path.join("docs", "sources.md"));
|
|
const editDoc = read(path.join("docs", "features", "rhwp-edit.md"));
|
|
const advancedDoc = read(path.join("docs", "features", "rhwp-advanced.md"));
|
|
|
|
assert.match(readme, /\| \[HWP 문서 편집\]\(docs\/features\/rhwp-edit\.md\) \|/);
|
|
assert.match(readme, /\| \[HWP 레이아웃·IR 디버깅\]\(docs\/features\/rhwp-advanced\.md\) \|/);
|
|
assert.match(readme, /\[HWP 문서 편집\]\(docs\/features\/rhwp-edit\.md\)/);
|
|
assert.match(readme, /\[HWP 레이아웃·IR 디버깅\]\(docs\/features\/rhwp-advanced\.md\)/);
|
|
|
|
assert.match(install, /--skill rhwp-edit/);
|
|
assert.match(install, /--skill rhwp-advanced/);
|
|
|
|
assert.match(roadmap, /rhwp-edit/);
|
|
assert.match(roadmap, /rhwp-advanced/);
|
|
assert.match(roadmap, /#155/);
|
|
|
|
assert.match(sources, /edwardkim\/rhwp/);
|
|
assert.match(sources, /@rhwp\/core/);
|
|
assert.match(sources, /issues\/196/);
|
|
|
|
assert.match(editDoc, /k-skill-rhwp/);
|
|
assert.match(editDoc, /insert-text/);
|
|
assert.match(editDoc, /create-table/);
|
|
assert.match(editDoc, /#196/);
|
|
assert.match(
|
|
editDoc,
|
|
/본문\S* 문단만|본문 \(body\) 문단만|body paragraphs only/,
|
|
"rhwp-edit feature doc must disclose search/replace-all body-only scope"
|
|
);
|
|
assert.match(
|
|
editDoc,
|
|
/UTF-?16|U\+0130|İ|case[ -]?fold/i,
|
|
"rhwp-edit feature doc must disclose the case-insensitive UTF-16 length-drift guard (Unicode follow-up)"
|
|
);
|
|
|
|
assert.match(advancedDoc, /cargo install rhwp/);
|
|
assert.match(advancedDoc, /export-svg/);
|
|
assert.match(advancedDoc, /ir-diff/);
|
|
assert.match(advancedDoc, /편집/);
|
|
});
|
|
|
|
test("k-skill-rhwp package ships CLI bin, WASM-init shim, and minor semver changeset", () => {
|
|
const packagePath = path.join(repoRoot, "packages", "k-skill-rhwp", "package.json");
|
|
assert.ok(fs.existsSync(packagePath), "expected packages/k-skill-rhwp/package.json");
|
|
const pkg = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
|
|
assert.equal(pkg.name, "k-skill-rhwp");
|
|
assert.ok(pkg.bin && pkg.bin["k-skill-rhwp"] === "bin/k-skill-rhwp.js", "expected bin mapping");
|
|
assert.ok(pkg.dependencies && pkg.dependencies["@rhwp/core"], "expected @rhwp/core dependency");
|
|
assert.ok(pkg.engines && /\^|>=\s*1[89]/.test(pkg.engines.node || ""), "expected Node 18+");
|
|
assert.ok(
|
|
fs.existsSync(path.join(repoRoot, "packages", "k-skill-rhwp", "src", "wasm-init.js")),
|
|
"expected src/wasm-init.js"
|
|
);
|
|
assert.ok(
|
|
fs.existsSync(path.join(repoRoot, "packages", "k-skill-rhwp", "bin", "k-skill-rhwp.js")),
|
|
"expected bin/k-skill-rhwp.js"
|
|
);
|
|
|
|
});
|
|
|
|
const README_SKILL_NAME_COLUMN_MAPPING = [
|
|
["철도 통합 시간표 조회", "railway-timetable"],
|
|
["카카오톡 Mac 아카이브 검색", "kakaotalk-mac"],
|
|
["서울 지하철 도착정보 조회", "seoul-subway-arrival"],
|
|
["지하철 분실물 조회", "subway-lost-property"],
|
|
["긱뉴스 조회", "geeknews-search"],
|
|
["한국 날씨 조회", "korea-weather"],
|
|
["사용자 위치 미세먼지 조회", "fine-dust-location"],
|
|
["한강 수위 정보 조회", "han-river-water-level"],
|
|
["한국 법령 검색", "korean-law-search"],
|
|
["법인등기 신청 컨설팅", "corporate-registration-consulting"],
|
|
["한국 개인정보처리방침·이용약관 자동 생성", "korean-privacy-terms"],
|
|
["한국 부동산 실거래가 조회", "real-estate-search"],
|
|
["한국 주택 공시가격 조회", "housing-official-price"],
|
|
["LH 청약 공고문 조회", "lh-notice-search"],
|
|
["장학금 검색 및 조회", "korean-scholarship-search"],
|
|
["생활쓰레기 배출정보 조회", "household-waste-info"],
|
|
["학교 급식 식단 조회", "k-schoollunch-menu"],
|
|
["도서관 도서 조회", "library-book-search"],
|
|
["의약품 안전 체크", "mfds-drug-safety"],
|
|
["식품 안전 체크", "mfds-food-safety"],
|
|
["한국 주식 정보 조회", "korean-stock-search"],
|
|
["금감원 DART 전자공시 조회", "k-dart"],
|
|
["조선왕조실록 검색", "joseon-sillok-search"],
|
|
["국가유산 검색·행사 조회", "korean-heritage-search"],
|
|
["한국 특허 정보 검색", "korean-patent-search"],
|
|
["근처 가장 싼 주유소 찾기", "cheap-gas-nearby"],
|
|
["근처 공중화장실 찾기", "public-restroom-nearby"],
|
|
["근처 공영주차장 찾기", "parking-lot-search"],
|
|
["KBO 경기 결과 조회", "kbo-results"],
|
|
["KBL 경기 결과 조회", "kbl-results"],
|
|
["K리그 경기 결과 조회", "kleague-results"],
|
|
["LCK 경기 분석", "lck-analytics"],
|
|
["토스증권 조회", "toss-investment"],
|
|
["로또 당첨 확인", "lotto-results"],
|
|
["HWP 문서 조회/변환", "hwp"],
|
|
["HWP 문서 편집", "rhwp-edit"],
|
|
["HWP 레이아웃·IR 디버깅", "rhwp-advanced"],
|
|
["근처 술집 조회", "kakao-bar-nearby"],
|
|
["우편번호 검색", "zipcode-search"],
|
|
["다이소 상품 조회", "daiso-product-search"],
|
|
["마켓컬리 상품 조회", "market-kurly-search"],
|
|
["올리브영 검색", "olive-young-search"],
|
|
["영화관 검색", "korean-cinema-search"],
|
|
["올라포케 역삼 포케", "hola-poke-yeoksam"],
|
|
["택배 배송조회", "delivery-tracking"],
|
|
["쿠팡 상품 검색", "coupang-product-search"],
|
|
["번개장터 검색", "bunjang-search"],
|
|
["한국어 맞춤법 검사", "korean-spell-check"],
|
|
["네이버 블로그 리서치", "naver-blog-research"],
|
|
["네이버 쇼핑 가격비교", "naver-shopping-search"],
|
|
["네이버 뉴스 검색", "naver-news-search"],
|
|
["한국일보 뉴스 조회", "hankookilbo-news"],
|
|
["한국어 글자 수 세기", "korean-character-count"],
|
|
["한국어 유행어 글쓰기", "korean-slang-writing"],
|
|
["K-스킬 클리너", "k-skill-cleaner"],
|
|
];
|
|
|
|
test("README skill table header advertises the new 스킬 이름 column (issue #165)", () => {
|
|
const readme = read("README.md");
|
|
|
|
assert.match(
|
|
readme,
|
|
/\| 할 수 있는 일 \| 스킬 이름 \| 설명 \|\n\| --- \| --- \| --- \|/,
|
|
"expected the 어떤 걸 할 수 있나 table header to include 스킬 이름 between 할 수 있는 일 and 설명 with a 5-column separator",
|
|
);
|
|
});
|
|
|
|
test("README skill table includes inline-code skill names for every documented row (issue #165)", () => {
|
|
const readme = read("README.md");
|
|
|
|
assert.ok(
|
|
README_SKILL_NAME_COLUMN_MAPPING.some(([, skillName]) => skillName === "k-skill-cleaner"),
|
|
"expected k-skill-cleaner to be covered by the central README skill-name mapping fixture",
|
|
);
|
|
|
|
for (const [label, skillName] of README_SKILL_NAME_COLUMN_MAPPING) {
|
|
const escapedLabel = escapeRegex(label);
|
|
const escapedName = escapeRegex(skillName);
|
|
|
|
assert.match(
|
|
readme,
|
|
new RegExp(`\\| \\[${escapedLabel}\\]\\([^)]+\\) \\| \`${escapedName}\` \\|`),
|
|
`expected README row "${label}" to surface skill name \`${skillName}\` as the second column`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("removed blue ribbon package is not part of npm workspaces or pack dry run", () => {
|
|
const packageJson = readJson("package.json");
|
|
const packageLock = readJson("package-lock.json");
|
|
const { listPublishableWorkspaces } = require("./ci-paths");
|
|
|
|
assert.equal(packageJson.scripts["pack:dry-run"], "node scripts/pack-dry-run.js");
|
|
assert.ok(!listPublishableWorkspaces().includes("blue-ribbon-nearby"));
|
|
assert.ok(!fs.existsSync(path.join(repoRoot, "packages", "blue-ribbon-nearby", "package.json")));
|
|
assert.ok(!Object.hasOwn(packageLock.packages, "packages/blue-ribbon-nearby"));
|
|
assert.ok(!Object.hasOwn(packageLock.packages, "node_modules/blue-ribbon-nearby"));
|
|
});
|
|
|
|
test("README skill table skill-name column entries match real on-disk skill directories (issue #165)", () => {
|
|
const allEntries = README_SKILL_NAME_COLUMN_MAPPING.map(([, skillName]) => skillName);
|
|
|
|
for (const skillName of allEntries) {
|
|
const skillFile = path.join(repoRoot, skillName, "SKILL.md");
|
|
|
|
assert.ok(
|
|
fs.existsSync(skillFile),
|
|
`expected ${skillName}/SKILL.md to exist on disk so the README table never advertises a non-existent skill identifier`,
|
|
);
|
|
|
|
const frontmatterMatch = read(path.join(skillName, "SKILL.md")).match(/^name:\s*(\S+)\s*$/m);
|
|
|
|
assert.ok(frontmatterMatch, `expected ${skillName}/SKILL.md to declare a name in frontmatter`);
|
|
assert.equal(
|
|
frontmatterMatch[1].replace(/^"|"$/g, ""),
|
|
skillName,
|
|
`expected ${skillName}/SKILL.md frontmatter name to match the directory name (validate-skills.sh invariant)`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("repository docs advertise the k-skill-cleaner skill and agent usage sources", () => {
|
|
const readme = read("README.md");
|
|
const install = read(path.join("docs", "install.md"));
|
|
const featureDocPath = path.join(repoRoot, "docs", "features", "k-skill-cleaner.md");
|
|
const skillPath = path.join(repoRoot, "k-skill-cleaner", "SKILL.md");
|
|
const skillLocalHelperPath = path.join(repoRoot, "k-skill-cleaner", "scripts", "k_skill_cleaner.py");
|
|
|
|
assert.ok(fs.existsSync(skillPath), "expected k-skill-cleaner/SKILL.md to exist");
|
|
assert.ok(fs.existsSync(skillLocalHelperPath), "expected k-skill-cleaner/scripts/k_skill_cleaner.py to be included in standalone skill installs");
|
|
assert.ok(fs.existsSync(featureDocPath), "expected docs/features/k-skill-cleaner.md to exist");
|
|
|
|
const skill = read(path.join("k-skill-cleaner", "SKILL.md"));
|
|
const featureDoc = read(path.join("docs", "features", "k-skill-cleaner.md"));
|
|
|
|
assert.match(skill, /^name: k-skill-cleaner$/m);
|
|
assert.match(skill, /Claude Code/);
|
|
assert.match(skill, /Codex/);
|
|
assert.match(skill, /OpenCode/);
|
|
assert.match(skill, /OpenClaw\/ClawHub/);
|
|
assert.match(skill, /Hermes Agent/);
|
|
assert.match(
|
|
skill,
|
|
/@nomadamas\/k-skill@0 exec k-skill-cleaner scripts\/k_skill_cleaner\.py --/,
|
|
);
|
|
assert.match(skill, /--days 90/);
|
|
assert.match(
|
|
featureDoc,
|
|
/@nomadamas\/k-skill@0 exec k-skill-cleaner scripts\/k_skill_cleaner\.py --/,
|
|
);
|
|
assert.match(featureDoc, /--days 90/);
|
|
assert.match(featureDoc, /인터뷰/);
|
|
assert.match(featureDoc, /트리거 횟수/);
|
|
assert.match(readme, /\| \[K-스킬 클리너\]\(docs\/features\/k-skill-cleaner\.md\) \| `k-skill-cleaner` \|/);
|
|
assert.match(readme, /\[K-스킬 클리너\]\(docs\/features\/k-skill-cleaner\.md\)/);
|
|
assert.match(install, /--skill k-skill-cleaner/);
|
|
});
|
|
|
|
test("court auction Workflow C docs preserve the PGJ151 safety contract", () => {
|
|
const featureDoc = read(path.join("docs", "features", "court-auction-notice-search.md"));
|
|
|
|
assert.match(
|
|
featureDoc,
|
|
/물건 자유 조건검색[\s\S]*PGJ151F00/,
|
|
"Workflow C docs should name the PGJ151F00 warmup path for property search",
|
|
);
|
|
assert.match(
|
|
featureDoc,
|
|
/pageSize[\s\S]*`10`\/`20`\/`50`\/`100`/,
|
|
"Workflow C docs should keep pageSize aligned with the observed PGJ151 dropdown values",
|
|
);
|
|
assert.doesNotMatch(
|
|
featureDoc,
|
|
/세션 cookie\([^\n]+\)는 `GET \/pgj\/index\.on\?w2xPath=\/pgj\/ui\/pgj100\/PGJ143M01\.xml&pgjId=143M01` 으로 사전에 한 번 받아둡니다\./,
|
|
"Workflow C docs should not imply every endpoint warms up through PGJ143M01 only",
|
|
);
|
|
});
|
|
|
|
test("court auction pending changeset does not publish stale fallback or pageSize guidance", () => {
|
|
const changesetDir = path.join(repoRoot, ".changeset");
|
|
if (!fs.existsSync(changesetDir)) return;
|
|
|
|
const pendingCourtAuctionChangesets = fs
|
|
.readdirSync(changesetDir)
|
|
.filter((entry) => entry.endsWith(".md"))
|
|
.map((entry) => read(path.join(".changeset", entry)))
|
|
.filter((contents) => contents.includes('"court-auction-notice-search"') && contents.includes("searchProperties()"));
|
|
|
|
for (const contents of pendingCourtAuctionChangesets) {
|
|
assert.doesNotMatch(
|
|
contents,
|
|
/direct HTTP call returns `BLOCKED` or `UPSTREAM_ERROR` 400/,
|
|
"Changeset must not tell users that confirmed BLOCKED/ipcheck=false auto-falls back by default",
|
|
);
|
|
assert.doesNotMatch(
|
|
contents,
|
|
/`pageSize` is capped at 100/,
|
|
"Changeset must not describe pageSize as an arbitrary 1..100 cap",
|
|
);
|
|
assert.match(
|
|
contents,
|
|
/`UPSTREAM_ERROR` 400/,
|
|
"Changeset should still document the raw-HTTP WAF-style 400 fallback path",
|
|
);
|
|
assert.match(
|
|
contents,
|
|
/`BLOCKED`[\s\S]*`fallbackOnBlocked:true`/,
|
|
"Changeset should document that confirmed BLOCKED retry is explicit opt-in",
|
|
);
|
|
assert.match(
|
|
contents,
|
|
/`10`\/`20`\/`50`\/`100`/,
|
|
"Changeset should document the exact PGJ151 pageSize allowlist",
|
|
);
|
|
}
|
|
});
|
|
|
|
test("building register public docs use standard PNU land-category digits", () => {
|
|
const docs = [
|
|
["feature guide", read(path.join("docs", "features", "building-register-search.md"))],
|
|
["proxy feature guide", read(path.join("docs", "features", "k-skill-proxy.md"))],
|
|
["proxy package README", read(path.join("packages", "k-skill-proxy", "README.md"))],
|
|
];
|
|
|
|
for (const [label, doc] of docs) {
|
|
assert.doesNotMatch(doc, /1168010100001230004/, `${label} should not publish a PNU with land category 0`);
|
|
assert.match(doc, /1168010100101230004/, `${label} should publish a valid normal-land PNU example`);
|
|
}
|
|
|
|
const featureGuide = docs[0][1];
|
|
assert.match(featureGuide, /토지구분\(1\)/);
|
|
assert.match(featureGuide, /`1`\(일반 토지\)[\s\S]*`platGbCd=0`/);
|
|
assert.match(featureGuide, /`2`\(산\)[\s\S]*`platGbCd=1`/);
|
|
});
|