← back to Homesonspec
packages/validation/src/rules.test.ts
194 lines
import { describe, expect, it } from "vitest";
import { runValidation } from "./engine";
import {
ALL_RULES,
bigPriceChange,
fkIntegrity,
incentiveExpirationOrLabel,
latLonInStateRule,
noDuplicateActiveAddress,
plausibleBaths,
plausibleBeds,
priceWithinBounds,
sqftPositive,
stateZipAgreement,
} from "./rules";
import type { RuleContext, RuleDb, StagedCandidate } from "./types";
function fv<T>(value: T | null, raw?: string): {
value: T | null; raw: string | null; evidenceText: string | null; sourceUrl: string; confidence: number;
} {
return {
value,
raw: raw ?? (value === null ? null : String(value)),
evidenceText: raw ?? null,
sourceUrl: "https://fixtures.local/page",
confidence: value === null ? 0 : 1,
};
}
function makeHome(fields: Record<string, ReturnType<typeof fv>> = {}): StagedCandidate {
return {
entityType: "inventory_home",
canonicalKey: "test-key-1",
canonicalHints: {
builderSlug: "meridian-homes",
communityName: "Cedar Bend",
address: "123 Oak St",
lotNumber: "42",
},
fields: {
street: fv("123 Oak St"),
city: fv("Austin"),
state: fv("TX"),
zip: fv("78701"),
price: fv(449_990, "$449,990"),
beds: fv(4, "4 Beds"),
bathsTotal: fv(2.5),
sqft: fv(2450),
lat: fv(30.3123),
lon: fv(-97.7123),
...fields,
},
};
}
const stubDb: RuleDb = {
activeHomeExistsAtAddress: async () => false,
publishedPriceForCanonicalKey: async () => null,
communityExists: async () => true,
builderExists: async () => true,
};
const ctx: RuleContext = { db: stubDb, sourceKey: "test", now: new Date("2026-07-22T00:00:00Z") };
describe("price.within-bounds", () => {
it("passes in-bounds and null price (never guessed)", async () => {
expect((await priceWithinBounds.check(makeHome(), ctx)).passed).toBe(true);
expect((await priceWithinBounds.check(makeHome({ price: fv<number>(null) }), ctx)).passed).toBe(true);
});
it("fails out-of-bounds", async () => {
expect((await priceWithinBounds.check(makeHome({ price: fv(12_000) }), ctx)).passed).toBe(false);
expect((await priceWithinBounds.check(makeHome({ price: fv(50_000_000) }), ctx)).passed).toBe(false);
});
});
describe("geo.state-zip-agreement", () => {
it("passes agreeing pair and unknown prefix (cannot verify ≠ fail)", async () => {
expect((await stateZipAgreement.check(makeHome(), ctx)).passed).toBe(true);
expect((await stateZipAgreement.check(makeHome({ zip: fv("99999") }), ctx)).passed).toBe(true);
});
it("fails disagreeing pair", async () => {
expect((await stateZipAgreement.check(makeHome({ zip: fv("85001") }), ctx)).passed).toBe(false);
});
});
describe("geo.latlon-in-state", () => {
it("passes coords inside the state", async () => {
expect((await latLonInStateRule.check(makeHome(), ctx)).passed).toBe(true);
});
it("fails coords outside the state (e.g. Phoenix coords on a TX record)", async () => {
expect(
(await latLonInStateRule.check(makeHome({ lat: fv(33.4484), lon: fv(-112.074) }), ctx)).passed,
).toBe(false);
});
});
describe("home.plausible-beds / baths / sqft", () => {
it("pass plausible values", async () => {
expect((await plausibleBeds.check(makeHome(), ctx)).passed).toBe(true);
expect((await plausibleBaths.check(makeHome(), ctx)).passed).toBe(true);
expect((await sqftPositive.check(makeHome(), ctx)).passed).toBe(true);
});
it("fail implausible values", async () => {
expect((await plausibleBeds.check(makeHome({ beds: fv(45) }), ctx)).passed).toBe(false);
expect((await plausibleBaths.check(makeHome({ bathsTotal: fv(30) }), ctx)).passed).toBe(false);
expect((await sqftPositive.check(makeHome({ sqft: fv(0) }), ctx)).passed).toBe(false);
});
});
describe("dup.no-active-address", () => {
it("fails when another active record holds the same normalized address in the same zip", async () => {
const dupCtx: RuleContext = {
...ctx,
db: { ...stubDb, activeHomeExistsAtAddress: async () => true },
};
expect((await noDuplicateActiveAddress.check(makeHome(), dupCtx)).passed).toBe(false);
expect((await noDuplicateActiveAddress.check(makeHome(), ctx)).passed).toBe(true);
});
it("passes the zip through so identical street strings in other cities don't collide", async () => {
let seenZip: string | null = "unset";
const capturingCtx: RuleContext = {
...ctx,
db: {
...stubDb,
activeHomeExistsAtAddress: async (_addr, zip) => {
seenZip = zip;
return false;
},
},
};
await noDuplicateActiveAddress.check(makeHome(), capturingCtx);
expect(seenZip).toBe("78701");
});
});
describe("ref.fk-integrity", () => {
it("fails when builder or community is unknown", async () => {
const noBuilder: RuleContext = { ...ctx, db: { ...stubDb, builderExists: async () => false } };
const noCommunity: RuleContext = { ...ctx, db: { ...stubDb, communityExists: async () => false } };
expect((await fkIntegrity.check(makeHome(), noBuilder)).passed).toBe(false);
expect((await fkIntegrity.check(makeHome(), noCommunity)).passed).toBe(false);
expect((await fkIntegrity.check(makeHome(), ctx)).passed).toBe(true);
});
});
describe("price.big-change", () => {
it("flags >15% movement for review; small moves pass", async () => {
const withPrev = (prev: number): RuleContext => ({
...ctx,
db: { ...stubDb, publishedPriceForCanonicalKey: async () => prev },
});
expect((await bigPriceChange.check(makeHome(), withPrev(300_000))).passed).toBe(false); // 449990 vs 300k = +50%
expect((await bigPriceChange.check(makeHome(), withPrev(445_000))).passed).toBe(true); // ~1%
expect((await bigPriceChange.check(makeHome(), ctx)).passed).toBe(true); // no prior
});
});
describe("incentive.expiration-or-label", () => {
const incentive = (expiresAt: string | null): StagedCandidate => ({
entityType: "incentive",
canonicalKey: "inc-1",
canonicalHints: { builderSlug: "meridian-homes" },
fields: { title: fv("Closing cost credit"), expiresAt: fv(expiresAt) },
});
it("passes a parseable date and a null (maps to evergreen label at publish)", async () => {
expect((await incentiveExpirationOrLabel.check(incentive("2026-09-30"), ctx)).passed).toBe(true);
expect((await incentiveExpirationOrLabel.check(incentive(null), ctx)).passed).toBe(true);
});
it("fails an unparseable date", async () => {
expect((await incentiveExpirationOrLabel.check(incentive("soon-ish"), ctx)).passed).toBe(false);
});
});
describe("runValidation verdict folding", () => {
it("clean record → publishable", async () => {
const { verdict, events } = await runValidation(makeHome(), ALL_RULES, ctx);
expect(verdict).toBe("publishable");
expect(events.length).toBeGreaterThanOrEqual(9);
});
it("failed error rule → rejected", async () => {
const { verdict } = await runValidation(makeHome({ price: fv(12_000) }), ALL_RULES, ctx);
expect(verdict).toBe("rejected");
});
it("failed review rule (big price change) → needs_review", async () => {
const reviewCtx: RuleContext = {
...ctx,
db: { ...stubDb, publishedPriceForCanonicalKey: async () => 300_000 },
};
const { verdict } = await runValidation(makeHome(), ALL_RULES, reviewCtx);
expect(verdict).toBe("needs_review");
});
});