← back to Homesonspec
collectors/chafin-communities/src/selftest.test.ts
165 lines
import { readFileSync, readdirSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { validatePayload } from "@homesonspec/schemas";
import { chafinCommunitiesAdapter, parseCommunityPage } from "./index";
/**
* Bounded, read-only self-test for the Chafin Communities adapter. Runs
* `parseCommunityPage` + `chafinCommunitiesAdapter.extract()` over 8 fixtures
* captured (rate-limited ~1 req/4s, honest UA) from chafincommunities.com on
* 2026-08-30 across 6 Metro-Atlanta GA counties / 8 distinct communities, and
* proves:
* - facts-only per-field coverage (price may legitimately be null on Pending
* homes; geo/sqft are legitimately absent from this DOM section),
* - every per-home street address AND mlsid (builderInventoryId) is DISTINCT
* across the whole set (proves genuine per-home listings, not a repeated
* community/floor-plan aggregate),
* - every emitted community + inventory_home record validates against the
* published schema.
*
* NB: unlike Goodall (one page == one detail home), Chafin publishes its
* available-home inventory INLINE on each community page — so each fixture is a
* community page carrying N home listings, and each yields 1 community record +
* N inventory_home records.
*/
const HERE = dirname(fileURLToPath(import.meta.url));
const FX = join(HERE, "..", "fixtures", "homes");
function urlMap(): Record<string, string> {
const tsv = readFileSync(join(FX, "_urls.tsv"), "utf8");
const out: Record<string, string> = {};
for (const line of tsv.split(/\r?\n/)) {
const [file, url] = line.split("\t");
if (file && url) out[file.trim()] = url.trim();
}
return out;
}
describe("chafin-communities adapter — coverage + per-home + schema", () => {
const files = readdirSync(FX)
.filter((f) => f.endsWith(".html"))
.sort();
const urls = urlMap();
it("parses per-home facts with high coverage and distinct addresses + ids", () => {
let n = 0;
const cov = {
price: 0,
beds: 0,
baths: 0,
sqft: 0,
address: 0,
status: 0,
id: 0,
geo: 0,
};
const addresses = new Set<string>();
const ids = new Set<string>();
for (const file of files) {
const html = readFileSync(join(FX, file), "utf8");
const url = urls[file] ?? `fixture://${file}`;
const parsed = parseCommunityPage(html, url);
expect(parsed, `parseCommunityPage returned null for ${file}`).not.toBeNull();
for (const h of parsed!.homes) {
n++;
if (h.street) {
cov.address++;
addresses.add(`${h.street}, ${h.city ?? ""}, ${h.state ?? ""} ${h.zip ?? ""}`);
}
if (h.builderInventoryId) {
cov.id++;
ids.add(h.builderInventoryId);
}
if (h.price != null) cov.price++;
if (h.beds != null) cov.beds++;
if (h.baths != null) cov.baths++;
if (h.status) cov.status++;
// sqft + geo are legitimately ABSENT from the available-homes DOM
// section — asserted at 0, an honest null (not a parse failure).
}
}
// eslint-disable-next-line no-console
console.log(
`\n[chafin-communities self-test] homes=${n} distinctAddresses=${addresses.size} distinctIds=${ids.size}\n` +
` price=${pct(cov.price, n)} beds=${pct(cov.beds, n)} baths=${pct(cov.baths, n)} ` +
`address=${pct(cov.address, n)} id=${pct(cov.id, n)} status=${pct(cov.status, n)} ` +
`sqft=${pct(cov.sqft, n)} geo=${pct(cov.geo, n)}`,
);
// Enough homes to be a meaningful test.
expect(n).toBeGreaterThanOrEqual(40);
// Genuinely per-home: one distinct street address AND one distinct mlsid
// per parsed home (proves real inventory listings, not a repeated aggregate).
expect(addresses.size).toBe(n);
expect(ids.size).toBe(n);
// Facts always present on every listing: address, mlsid, beds, baths, status.
expect(cov.address).toBe(n);
expect(cov.id).toBe(n);
expect(cov.beds).toBe(n);
expect(cov.baths).toBe(n);
expect(cov.status).toBe(n);
// Price is published for Active/available homes but NOT for Pending ones —
// an honest null. Assert coverage without requiring 100%.
expect(cov.price / n).toBeGreaterThanOrEqual(0.5);
// sqft + geo are honest nulls for this source's inventory DOM: assert we did
// NOT fabricate them (coverage stays 0), but do NOT fail on the null.
expect(cov.sqft).toBe(0);
expect(cov.geo).toBe(0);
});
it("emits schema-valid community + inventory_home records", () => {
let homes = 0;
let communities = 0;
for (const file of files) {
const html = readFileSync(join(FX, file), "utf8");
const url = urls[file] ?? `fixture://${file}`;
const page = {
url,
retrievedAt: new Date().toISOString(),
contentType: "text/html",
body: Buffer.from(html),
contentHash: "test",
};
const { records, errors } = chafinCommunitiesAdapter.extract(page);
expect(errors).toEqual([]);
for (const rec of records) {
if (rec.entityType === "inventory_home") {
homes++;
// Guard the DEDUPE KEY, not just the parsed struct: every home here
// has a real street (address coverage is 100%), so the canonical
// address hint must be that street — never a "Lot X" fallback. This
// is the assertion that catches the ?? / ?: precedence bug that made
// every home collide under "Lot X" (TK-10487 contrarian catch).
const canonAddr = rec.canonicalHints?.address ?? "";
expect(canonAddr, `canonicalHints.address fell back to "${canonAddr}" @ ${url}`).not.toMatch(
/^Lot\s/i,
);
expect(canonAddr).toMatch(/\d/); // a real street carries a house number
}
if (rec.entityType === "community") communities++;
const result = validatePayload(rec);
if (!result.ok) {
// eslint-disable-next-line no-console
console.error(`validation failed for ${rec.entityType} @ ${url}`, result);
}
expect(result.ok).toBe(true);
}
}
expect(communities).toBe(files.length);
expect(homes).toBeGreaterThanOrEqual(40);
});
});
function pct(a: number, b: number): string {
return b ? `${Math.round((100 * a) / b)}%` : "n/a";
}