← 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");
  });
});