k-skill/packages/court-auction-notice-search/test/normalize.test.js
Jeffrey (Dongkyu) Kim d7aca1bcbe
Merge dev into main (#197)
* fix(toss-securities): clarify session expiry and quote 403 handling

* Clarify toss empty-output session expiry

Portfolio and watchlist reads can exit successfully with empty payloads when the stored Toss session has expired. The empty-output path now verifies the session before JSON parsing and only promotes confirmed invalid auth doctor data into TossSessionExpiredError.

Constraint: Scope is limited to toss-securities issue #126 follow-up on PR #192

Rejected: Treat auth doctor execution failures as expired sessions | unsupported or failing doctor output is inconclusive without parsed session.valid=false

Confidence: high

Scope-risk: narrow

Directive: Keep empty-result session expiry classification tied to explicit auth doctor confirmation

Tested: npm run test --workspace toss-securities; npm run lint --workspace toss-securities; npm run ci; manual mock tossctl blank stdout invalid/inconclusive doctor checks

* Avoid false session-expiry labels for validation errors

The toss wrapper now treats bare validation_error text as an upstream command failure instead of a session-expired signal. Structured auth doctor JSON remains the source of truth for empty portfolio/watchlist invalid-session promotion, while known stored-session-invalid stderr still maps to TossSessionExpiredError.\n\nConstraint: PR #192 follow-up must stay scoped to issue #126 toss-securities behavior.\nRejected: Keep validation_error in the global regex | it mislabels auth doctor transport failures and quote 403 validation errors as session expiry.\nConfidence: high\nScope-risk: narrow\nDirective: Do not broaden the free-text session classifier without regressions for auth doctor and quote upstream validation failures.\nTested: npm run lint --workspace toss-securities; npm run test --workspace toss-securities; npm run ci; manual mock tossctl validation_error checks; architect verification CLEAR\nNot-tested: Live tossctl network/auth session against real Toss upstream

* Align court auction lookup with monthly site search (#196)

The court auction notice page posts a YYYYMM search key from its 조회 button and returns a month of rows. Keep day inputs as a compatibility filter over the monthly response and normalize the current nested detail payload shape.

Constraint: courtauction.go.kr has no public API and blocks bursty automated calls.

Rejected: querying every day independently | the upstream search surface is month-based and day calls return false empty results.

Confidence: high

Scope-risk: narrow

Directive: Preserve the site-observed YYYYMM notice search contract unless the PGJ143M01 XHR changes again.

Tested: npm --workspace packages/court-auction-notice-search test; npm run ci; live 서울중앙지방법원 2026-05 notice/detail smoke lookup.

Not-tested: PR CI after push.

Co-authored-by: OmX <omx@oh-my-codex.dev>

* Guide crawler skills toward reusable discovery (#195)

* chore: version packages

* Guide crawler skills toward reusable discovery

Constraint: User requested insane-search-style guidance for future crawling k-skills without unrelated implementation changes.
Rejected: Adding crawler code or a standalone template | too broad for a docs guidance change and risks dependency creep.
Confidence: high
Scope-risk: narrow
Directive: Keep site-specific access details inside individual skills after a site-agnostic discovery pass.
Tested: npm run ci
Not-tested: Live crawler behavior; documentation-only change.

* Clarify crawler skill discovery guidance

Constraint: Crawling k-skills need site-dependent recipes, but should derive them through a reusable discovery pass.
Rejected: Leaving guidance only in docs/adding-a-skill.md | AGENTS.md and CLAUDE.md also guide future agents.
Confidence: high
Scope-risk: narrow
Directive: Use site-agnostic discovery to find, then explicitly package, the target site's stable access path.
Tested: npm run ci
Not-tested: Live crawler behavior; documentation-only change.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Ground corporate registration guidance in official form sources

Keep the consulting skill focused on draft/checklist support while pointing users to current IROS and law.go.kr form sources for submission-ready artifacts.

Constraint: official registry forms can change outside the repository and must be re-downloaded at use time

Rejected: committing copied official HWP/HWPX/PDF forms | they would become stale and risk misleading users

Confidence: high

Scope-risk: narrow

Directive: do not treat Markdown templates as substitutes for official registry submission forms

Tested: npm test

* Ground incorporation drafting in real HWP forms

Bundle official court incorporation forms plus public startup incorporation attachments, and make rhwp-filled HWP outputs the default drafting path for the corporate-registration skill. Replace the listed-company articles reference with a startup-suitable Ministry of Justice stock-company form and record source manifests for bundled binaries.

Constraint: user requires actual sourced HWP templates, not generated placeholder binaries.
Rejected: markdown-only drafting | it cannot produce submission-shaped Korean registry forms.
Rejected: listed-company standard articles as the default reference | it is mismatched for typical startup incorporation.
Confidence: high
Scope-risk: moderate
Directive: keep bundled HWP forms source-backed, sanitized, and edited only through copied working files.
Tested: node --test scripts/skill-docs.test.js; npm run lint; k-skill-rhwp info on bundled HWP files; kordoc conversion spot checks.
Not-tested: manual opening every HWP in Hancom Office and live registry submission.
Co-authored-by: OmX <omx@oh-my-codex.dev>

* Streamline corporate registration forms workflow

Prioritize saved HWP forms for ordinary stock-company promoter incorporations, make required court-registry receipts and director identity certificates explicit, and remove the redundant markdown articles template so the skill stays HWP-first.

Constraint: 법원등기소 기준 체크리스트 must include fee receipts, director seal/signature certificates, and resident-record documents.

Rejected: Keeping a separate markdown articles template | duplicated the stored HWP articles workflow and encouraged non-HWP drafting.

Confidence: high

Scope-risk: narrow

Directive: Keep corporate-registration-consulting focused on stored HWP form copies and explicit issued-document checklists.

Tested: node --test --test-name-pattern 'corporate-registration-consulting' scripts/skill-docs.test.js; node --check scripts/skill-docs.test.js; ./scripts/validate-skills.sh; git diff --check

Not-tested: Full npm run ci was not run because this is a skill documentation/template refactor, not release or package automation.

---------

Co-authored-by: galvaomica <galvaomica@galvaomicaui-MacBookAir.local>
Co-authored-by: OmX <omx@oh-my-codex.dev>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-02 23:51:59 +09:00

188 lines
7.4 KiB
JavaScript

"use strict";
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const {
normalizeNoticeListResponse,
normalizeNoticeDetailResponse,
normalizeCaseDetailResponse,
normalizeCourtCodesResponse,
parseAmount,
formatYmd,
formatHm
} = require("../src/normalize");
const fixturesDir = path.join(__dirname, "fixtures");
function loadFixture(name) {
return JSON.parse(fs.readFileSync(path.join(fixturesDir, name), "utf8"));
}
const noticesEmpty = loadFixture("notices-empty.json");
const noticesSample = loadFixture("notices-sample.json");
const noticeDetailSample = loadFixture("notice-detail-sample.json");
const caseFoundSample = loadFixture("case-found-sample.json");
const caseNotFoundSample = loadFixture("case-not-found.json");
const courtsSample = loadFixture("courts-sample.json");
test("parseAmount strips commas/won/spaces and rejects non-numeric", () => {
assert.equal(parseAmount("1,500,000,000"), 1500000000);
assert.equal(parseAmount("1,500,000,000원"), 1500000000);
assert.equal(parseAmount(" 850000000 "), 850000000);
assert.equal(
parseAmount('<img src="/images/number_01.gif" alt="첫번째"> 9,600,000<br>입찰시간(10:00)<br>'),
9600000
);
assert.equal(parseAmount(""), null);
assert.equal(parseAmount("-"), null);
assert.equal(parseAmount("not-a-number"), null);
assert.equal(parseAmount(null), null);
assert.equal(parseAmount(undefined), null);
});
test("formatYmd inserts hyphens for 8-digit dates and passes through otherwise", () => {
assert.equal(formatYmd("20260427"), "2026-04-27");
assert.equal(formatYmd("2026.04.27"), "2026.04.27");
assert.equal(formatYmd(""), null);
assert.equal(formatYmd(null), null);
});
test("formatHm formats 3-4 digit times into HH:MM", () => {
assert.equal(formatHm("1000"), "10:00");
assert.equal(formatHm("930"), "09:30");
assert.equal(formatHm(""), null);
assert.equal(formatHm(null), null);
});
test("normalizeNoticeListResponse returns 0 items for an empty payload", () => {
const result = normalizeNoticeListResponse(noticesEmpty, {
requestedDate: "2026-04-30",
requestedCourtCode: "",
requestedBidType: null
});
assert.equal(result.count, 0);
assert.deepEqual(result.items, []);
assert.equal(result.requestedDate, "2026-04-30");
});
test("normalizeNoticeListResponse normalizes auction notice rows with English keys + raw passthrough", () => {
const result = normalizeNoticeListResponse(noticesSample);
assert.equal(result.count, 2);
const first = result.items[0];
assert.equal(first.noticeId, "REAL_ID_2026042701");
assert.equal(first.courtCode, "B000210");
assert.equal(first.courtName, "서울중앙지방법원");
assert.equal(first.judgeDeptCode, "ENC_jdbn1");
assert.equal(first.judgeDeptName, "경매1계");
assert.equal(first.bidTypeCode, "000331");
assert.equal(first.bidTypeName, "기일입찰");
assert.equal(first.saleDate, "2026-04-27");
assert.equal(first.bidStartDate, "2026-04-27");
assert.equal(first.bidEndDate, "2026-04-27");
assert.equal(first.salePlace, "서울중앙지방법원 경매법정 (4별관 211호)");
assert.deepEqual(first.saleTimes, ["10:00", "14:00"]);
assert.equal(first.correctionCount, 0);
assert.equal(first.cancellationCount, 0);
assert.equal(first.raw.dspslRealId, "REAL_ID_2026042701");
const second = result.items[1];
assert.equal(second.bidTypeCode, "000332");
assert.equal(second.bidTypeName, "기간입찰");
assert.equal(second.bidStartDate, "2026-04-20");
assert.equal(second.bidEndDate, "2026-04-24");
assert.equal(second.bidPeriodLabel, "20260420 ~ 20260424");
});
test("normalizeNoticeListResponse can strip raw passthrough on demand", () => {
const result = normalizeNoticeListResponse(noticesSample, { includeRaw: false });
for (const item of result.items) {
assert.equal(item.raw, undefined);
}
});
test("normalizeNoticeDetailResponse extracts 사건번호/용도/주소/감정평가/최저매각가/매각장소", () => {
const result = normalizeNoticeDetailResponse(noticeDetailSample);
assert.equal(result.count, 2);
assert.equal(result.notice.salePlace, "서울중앙지방법원 경매법정 (4별관 211호)");
assert.equal(result.notice.saleDate, "2026-04-27");
assert.equal(result.notice.bidTypeCode, "000331");
assert.equal(result.notice.bidTypeName, "기일입찰");
const first = result.items[0];
assert.equal(first.caseNumber, "2024타경100001");
assert.equal(first.itemSeq, "1");
assert.equal(first.usage, "아파트");
assert.equal(
first.address,
"서울특별시 강남구 역삼동 123-4 OO아파트 101동 502호"
);
assert.equal(first.appraisedPrice, 1500000000);
assert.equal(first.minimumSalePrice, 1200000000);
assert.equal(first.remarks, "토지·건물 일괄매각");
assert.equal(first.raw.csNo, "2024타경100001");
});
test("normalizeNoticeDetailResponse handles empty payload gracefully", () => {
const result = normalizeNoticeDetailResponse({ status: 200, data: {} });
assert.equal(result.count, 0);
assert.deepEqual(result.items, []);
});
test("normalizeCourtCodesResponse returns code/name/branchName triples", () => {
const result = normalizeCourtCodesResponse(courtsSample);
assert.equal(result.count, 9);
assert.equal(result.items[0].code, "B000210");
assert.equal(result.items[0].name, "서울중앙지방법원");
assert.equal(result.items[0].branchName, "서울중앙지방법원");
});
test("normalizeCaseDetailResponse marks status:204 / null dma_csBasInf as found:false", () => {
const result = normalizeCaseDetailResponse(caseNotFoundSample);
assert.equal(result.found, false);
assert.equal(result.status, 204);
assert.match(result.message, /조회 되는 사건번호 정보가 없습니다/);
assert.equal(result.caseInfo, null);
assert.deepEqual(result.items, []);
});
test("normalizeCaseDetailResponse extracts case basic info, items, schedule, claim deadline", () => {
const result = normalizeCaseDetailResponse(caseFoundSample);
assert.equal(result.found, true);
assert.equal(result.status, 200);
assert.equal(result.caseInfo.caseNumber, "2024타경100001");
assert.equal(result.caseInfo.courtCode, "B000210");
assert.equal(result.caseInfo.caseName, "부동산임의경매");
assert.equal(result.caseInfo.caseReceiptDate, "2024-03-15");
assert.equal(result.caseInfo.caseStartDate, "2024-03-20");
assert.equal(result.caseInfo.claimAmount, 500000000);
assert.equal(result.caseInfo.judgeDeptName, "경매1계");
assert.equal(result.items.length, 1);
assert.equal(
result.items[0].address,
"서울특별시 강남구 역삼동 123-4 OO아파트 101동 502호"
);
assert.equal(result.items[0].claimDeadlineDate, "2024-06-15");
assert.equal(result.schedule.length, 2);
assert.equal(result.schedule[0].saleDate, "2026-04-27");
assert.equal(result.schedule[0].minimumSalePrice, 1200000000);
assert.equal(result.schedule[0].appraisedPrice, 1500000000);
assert.equal(result.schedule[0].resultCode, "유찰");
assert.equal(result.schedule[1].minimumSalePrice, 960000000);
assert.equal(result.schedule[1].resultCode, null);
assert.deepEqual(result.claimDeadline, {
deadlineDate: "2024-06-15",
announcementDate: "2024-05-01"
});
});
test("normalizeCaseDetailResponse strips raw when includeRaw=false", () => {
const result = normalizeCaseDetailResponse(caseFoundSample, { includeRaw: false });
assert.equal(result.raw, undefined);
});