← back to Homesonspec
apps/workers/src/pipeline.itest.ts
172 lines
import { beforeAll, describe, expect, it } from "vitest";
import { prisma } from "@homesonspec/database";
import { meridianHomesAdapter } from "@homesonspec/collector-meridian-homes";
import { publishStagedRecord } from "@homesonspec/publisher";
import { runPipeline, validateStage } from "./pipeline";
/**
* Integration: the full fixture pipeline against homesonspec_test.
* Proves raw ≠ published separation, verdict gating, idempotence,
* community auto-creation from staged records, and review-queue flow.
*/
async function resetDb() {
// Truncate in FK-safe order.
const tables = [
"ValidationEvent", "ReviewItem", "SourceEvidence", "StagedRecord", "RawSnapshot",
"ChangeEvent", "CorrectionReport", "Lead", "Incentive", "InventoryHome", "Lot",
"SalesOffice", "FloorPlan", "Community", "Division", "SourceRegistry", "Builder",
];
for (const table of tables) {
await prisma.$executeRawUnsafe(`TRUNCATE TABLE "${table}" CASCADE`);
}
}
beforeAll(async () => {
expect(process.env.DATABASE_URL).toMatch(/homesonspec_test/);
await resetDb();
// Minimal FK targets: builder + fixture source. NO community — the
// pipeline must create it from the staged community record.
const builder = await prisma.builder.create({
data: { slug: "meridian-homes", name: "Meridian Homes", isDemo: true },
});
await prisma.sourceRegistry.create({
data: {
key: "meridian-homes-fixtures",
name: "Meridian Homes (fixture pages)",
builderId: builder.id,
collectionMethod: "FIXTURE",
mediaRights: "NONE",
},
});
});
describe("fixture pipeline end-to-end", () => {
it("publishes community + homes + incentives from fixtures", async () => {
const summary = await runPipeline(meridianHomesAdapter);
expect(summary.pages).toBe(5);
expect(summary.staged).toBe(6);
expect(summary.published).toBe(6);
expect(summary.rejected).toBe(0);
expect(summary.errors).toEqual([]);
// Community was CREATED by the publish stage from the staged record.
const community = await prisma.community.findFirst({ where: { name: "Cedar Bend" } });
expect(community).not.toBeNull();
expect(community!.city).toBe("Leander");
expect(community!.state).toBe("TX");
expect(community!.isDemo).toBe(true);
const homes = await prisma.inventoryHome.findMany({ where: { status: "PUBLISHED" } });
expect(homes).toHaveLength(3);
for (const home of homes) {
expect(home.verificationLabel).toBe("DEMONSTRATION"); // FIXTURE method
expect(home.isDemo).toBe(true);
}
// Null-price home stays null — never guessed.
const silverleaf = homes.find((h) => h.street === "124 Silverleaf Pass");
expect(silverleaf?.price).toBeNull();
// Evidence re-pointed at published entities.
const evidence = await prisma.sourceEvidence.count({
where: { entityType: "INVENTORY_HOME", entityId: { not: null } },
});
expect(evidence).toBeGreaterThan(0);
});
it("is idempotent — unchanged bytes stage nothing new", async () => {
const summary = await runPipeline(meridianHomesAdapter);
expect(summary.changed).toBe(0);
expect(summary.staged).toBe(0);
expect(await prisma.inventoryHome.count()).toBe(3);
});
it("publish stage refuses non-validated records (only-writer invariant)", async () => {
const source = await prisma.sourceRegistry.findUniqueOrThrow({
where: { key: "meridian-homes-fixtures" },
});
const staged = await prisma.stagedRecord.create({
data: {
sourceId: source.id,
entityType: "INVENTORY_HOME",
canonicalKey: "not-validated-key",
payload: {},
hints: { builderSlug: "meridian-homes", communityName: "Cedar Bend" },
extractorVersion: "test",
status: "EXTRACTED",
},
});
await expect(publishStagedRecord(staged.id)).rejects.toThrow(/refusing to publish/);
expect(await prisma.inventoryHome.count()).toBe(3); // nothing leaked
});
it("big price change → needs_review + ReviewItem; approval publishes + ChangeEvent", async () => {
const source = await prisma.sourceRegistry.findUniqueOrThrow({
where: { key: "meridian-homes-fixtures" },
});
const juniper = await prisma.inventoryHome.findFirstOrThrow({
where: { street: "118 Juniper Draw" },
});
const fv = (value: string | number | null, raw: string | null) => ({
value, raw, evidenceText: raw, sourceUrl: "fixture://test", confidence: value === null ? 0 : 1,
});
// Same canonical identity, price dropped 20%.
const newPrice = Math.round(Number(juniper.price) * 0.8);
const staged = await prisma.stagedRecord.create({
data: {
sourceId: source.id,
entityType: "INVENTORY_HOME",
canonicalKey: juniper.canonicalKey,
payload: {
street: fv(juniper.street, juniper.street),
city: fv("Leander", "Leander"),
state: fv("TX", "TX"),
zip: fv("78641", "78641"),
price: fv(newPrice, `$${newPrice}`),
beds: fv(4, "4 Beds"),
bathsTotal: fv(3, "3 Baths"),
sqft: fv(2415, "2,415 sq ft"),
lat: fv(30.5791, "30.5791"),
lon: fv(-97.8519, "-97.8519"),
},
hints: {
builderSlug: "meridian-homes",
communityName: "Cedar Bend",
address: juniper.street,
lotNumber: "118",
builderInventoryId: "MH-1118",
lat: 30.5791,
lon: -97.8519,
planName: "The Juniper",
},
extractorVersion: "test",
status: "EXTRACTED",
},
});
const { verdict } = await validateStage(staged.id, "meridian-homes-fixtures");
expect(verdict).toBe("needs_review");
const review = await prisma.reviewItem.findFirst({
where: { stagedRecordId: staged.id, status: "OPEN" },
});
expect(review).not.toBeNull();
expect(review!.reason).toMatch(/price.big-change/);
// Price unchanged until a human approves.
const before = await prisma.inventoryHome.findUniqueOrThrow({ where: { id: juniper.id } });
expect(Number(before.price)).toBe(Number(juniper.price));
// Human approval → publish updates the home + records a ChangeEvent.
await prisma.stagedRecord.update({ where: { id: staged.id }, data: { status: "APPROVED" } });
await publishStagedRecord(staged.id);
const after = await prisma.inventoryHome.findUniqueOrThrow({ where: { id: juniper.id } });
expect(Number(after.price)).toBe(newPrice);
const change = await prisma.changeEvent.findFirst({
where: { entityType: "INVENTORY_HOME", entityId: juniper.id, field: "price" },
});
expect(change).not.toBeNull();
expect(change!.newValue).toBe(String(newPrice));
});
});