Pin delivery-tracking doc samples to checked fixtures

The review follow-up showed the docs regression only proved that the two
markdown copies matched each other. This change adds a checked-in fixture
for the verified CJ and 우체국 public outputs, then requires both docs to
match that fixture exactly while scanning the full parsed samples for TEL,
phone-like strings, and raw sensitive field names.

Constraint: Must keep CI offline while tying docs to verified smoke-test invoices
Constraint: Existing PR #13 already publishes dated CJ and 우체국 public samples
Rejected: Keep shape-only assertions | shared-but-wrong markdown drift would still pass CI
Confidence: high
Scope-risk: narrow
Directive: Refresh scripts/fixtures/delivery-tracking-public-samples.json only after rerunning live smoke verification for the documented invoices
Tested: node --test scripts/skill-docs.test.js; npm run ci; python3 /tmp/cj_verify.py; npx --yes skills add . --list
Not-tested: python3 /tmp/epost_verify.py (service.epost.go.kr connection timed out repeatedly on 2026-03-27 from this environment)
This commit is contained in:
Jeffrey (Dongkyu) Kim 2026-03-27 03:39:31 +09:00
commit cfa76eccf3
2 changed files with 77 additions and 52 deletions

View file

@ -0,0 +1,51 @@
{
"cj": {
"carrier": "cj",
"invoice": "1234567890",
"status_code": "91",
"status": "배달완료",
"timestamp": "2026-03-21 12:22:13",
"location": "경기광주오포",
"event_count": 3,
"recent_events": [
{
"timestamp": "2026-03-10 03:01:45",
"location": "청원HUB",
"status_code": "44",
"status": "상품이동중"
},
{
"timestamp": "2026-03-21 10:53:19",
"location": "경기광주오포",
"status_code": "82",
"status": "배송출발"
},
{
"timestamp": "2026-03-21 12:22:13",
"location": "경기광주오포",
"status_code": "91",
"status": "배달완료"
}
]
},
"epost": {
"carrier": "epost",
"invoice": "1234567890123",
"status": "배달완료",
"timestamp": "2025.12.04 15:13",
"location": "제주우편집중국",
"event_count": 2,
"recent_events": [
{
"timestamp": "2025.12.04 15:13",
"location": "제주우편집중국",
"status": "배달준비"
},
{
"timestamp": "2025.12.04 15:13",
"location": "제주우편집중국",
"status": "배달완료"
}
]
}
}

View file

@ -9,6 +9,10 @@ function read(relativePath) {
return fs.readFileSync(path.join(repoRoot, relativePath), "utf8");
}
function readJson(relativePath) {
return JSON.parse(read(relativePath));
}
function extractQuotedEntries(block, indent) {
return block
.split("\n")
@ -43,6 +47,21 @@ function findJsonFenceAfterLabel(doc, label) {
return JSON.parse(match[1]);
}
function assertSanitizedPublicOutput(output, label) {
const serialized = JSON.stringify(output);
assert.doesNotMatch(serialized, /\bTEL\b/i, `${label} must not leak TEL fragments`);
assert.doesNotMatch(
serialized,
/\d{2,4}[.\-]\d{3,4}[.\-]\d{4}/,
`${label} must not leak phone-number-like strings anywhere in the published sample`,
);
assert.doesNotMatch(serialized, /crgNm/, `${label} must not leak CJ assignee/source fields`);
assert.doesNotMatch(serialized, /sender/i, `${label} must not leak sender fields`);
assert.doesNotMatch(serialized, /receiver/i, `${label} must not leak receiver fields`);
assert.doesNotMatch(serialized, /delivered_to/i, `${label} must not leak delivered_to fields`);
}
test("root npm test script includes the skill docs regression suite", () => {
const packageJson = JSON.parse(read("package.json"));
@ -339,6 +358,9 @@ test("delivery-tracking published examples lock a shared normalized non-PII sche
});
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 공개 출력 예시");
@ -348,56 +370,8 @@ test("delivery-tracking docs publish aligned sample normalized outputs for both
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(Object.keys(cjSkillOutput), [
"carrier",
"invoice",
"status_code",
"status",
"timestamp",
"location",
"event_count",
"recent_events",
]);
assert.equal(cjSkillOutput.carrier, "cj");
assert.equal(cjSkillOutput.invoice, "1234567890");
assert.equal(typeof cjSkillOutput.status, "string");
assert.equal(typeof cjSkillOutput.timestamp, "string");
assert.equal(typeof cjSkillOutput.location, "string");
assert.equal(typeof cjSkillOutput.event_count, "number");
assert.ok(Array.isArray(cjSkillOutput.recent_events));
assert.ok(cjSkillOutput.recent_events.length > 0 && cjSkillOutput.recent_events.length <= 3);
for (const event of cjSkillOutput.recent_events) {
assert.deepEqual(Object.keys(event), ["timestamp", "location", "status_code", "status"]);
}
assert.deepEqual(Object.keys(epostSkillOutput), [
"carrier",
"invoice",
"status",
"timestamp",
"location",
"event_count",
"recent_events",
]);
assert.equal(epostSkillOutput.carrier, "epost");
assert.equal(epostSkillOutput.invoice, "1234567890123");
assert.equal(typeof epostSkillOutput.status, "string");
assert.equal(typeof epostSkillOutput.timestamp, "string");
assert.equal(typeof epostSkillOutput.location, "string");
assert.equal(typeof epostSkillOutput.event_count, "number");
assert.ok(Array.isArray(epostSkillOutput.recent_events));
assert.ok(epostSkillOutput.recent_events.length > 0 && epostSkillOutput.recent_events.length <= 3);
for (const event of epostSkillOutput.recent_events) {
assert.deepEqual(Object.keys(event), ["timestamp", "location", "status"]);
assert.ok(!JSON.stringify(event).includes("TEL"));
}
for (const output of [cjSkillOutput, epostSkillOutput]) {
const serialized = JSON.stringify(output);
assert.ok(!serialized.includes("crgNm"));
assert.ok(!serialized.includes("sender"));
assert.ok(!serialized.includes("receiver"));
assert.ok(!serialized.includes("delivered_to"));
}
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");
});