diff --git a/apps/api/src/jobs/prefetch-worker.service.spec.ts b/apps/api/src/jobs/prefetch-worker.service.spec.ts index 8d53202..9ff1372 100644 --- a/apps/api/src/jobs/prefetch-worker.service.spec.ts +++ b/apps/api/src/jobs/prefetch-worker.service.spec.ts @@ -224,25 +224,9 @@ describe("PrefetchWorkerService — fast lane (lifo) + backlog gating", () => { }); }); - describe("poison guards (Faz 6: generic-model + category cap)", () => { - type PI = { processInit: (j: unknown) => Promise }; + describe("poison guard (Faz 6: category cap — brand-agnostic anti-explosion)", () => { type PC = { processChildren: (j: unknown) => Promise }; - it("processInit skips + marks poison for a generic-model vehicle (model == brand)", async () => { - const { service, queue, redis } = makeDeps({ - waiting: 0, - limitResults: [[{ id: "op1", brandName: "Opel", model: "Opel" }]], // vehicle lookup - }); - await (service as never as PI).processInit({ - data: { vehicleId: "op1", source: "pl24" }, - } as never); - const poisonSet = redis.set.mock.calls.some((c) => - String(c[0]).includes("prefetch:poison:op1"), - ); - expect(poisonSet).toBe(true); - expect(queue.add).not.toHaveBeenCalled(); // tree never drilled - }); - it("processChildren marks poison once progress.total exceeds CATEGORY_CAP", async () => { const { service, queue, redis } = makeDeps({ waiting: 0, limitResults: [] }); redis.getJson.mockImplementation(async (...a: unknown[]) => diff --git a/apps/api/src/jobs/prefetch-worker.service.ts b/apps/api/src/jobs/prefetch-worker.service.ts index 78aefc9..2d37d3f 100644 --- a/apps/api/src/jobs/prefetch-worker.service.ts +++ b/apps/api/src/jobs/prefetch-worker.service.ts @@ -275,7 +275,7 @@ export class PrefetchWorkerService implements OnModuleInit, OnModuleDestroy { await checkCooldown(this.redis, source); checkTimeWindow(source); - // Already flagged as poison (over-cap / generic model on a prior run) — skip. + // Already flagged as poison (tree exceeded CATEGORY_CAP on a prior run) — skip. if (await this.redis.exists(this.poisonKey(vehicleId))) { this.logger.debug(`[prefetch] Skip poison vehicle=${vehicleId}`); return; @@ -283,7 +283,7 @@ export class PrefetchWorkerService implements OnModuleInit, OnModuleDestroy { // Verify vehicle still exists const [vehicle] = await this.db - .select({ id: vehicles.id, brandName: vehicles.brandName, model: vehicles.model }) + .select({ id: vehicles.id }) .from(vehicles) .where(eq(vehicles.id, vehicleId)) .limit(1); @@ -293,19 +293,6 @@ export class PrefetchWorkerService implements OnModuleInit, OnModuleDestroy { return; } - // Generic-model guard: when VIN decode fails to resolve the model it stays - // equal to the brand (e.g. model "Opel" for brand "Opel") and the catalog - // fetch lands on the brand's ROOT tree — the whole model universe, hundreds - // of thousands of part-less categories. Don't drill it; mark poison until the - // decode is fixed. A real model is always more specific than the brand. - if (this.isGenericModel(vehicle.brandName, vehicle.model)) { - this.logger.warn( - `[prefetch] Skip generic-model vehicle=${vehicleId} (brand=${vehicle.brandName}, model=${vehicle.model}) — marking poison`, - ); - await this.markPoison(vehicleId); - return; - } - await initProgress(this.redis, vehicleId); // resets progress.total — the cap's tree-size proxy // Get all top-level categories for this vehicle @@ -823,19 +810,11 @@ export class PrefetchWorkerService implements OnModuleInit, OnModuleDestroy { return `prefetch:poison:${vehicleId}`; } - /** - * True when VIN decode failed to resolve a model: it stays equal to the brand - * (case-insensitive) or is empty. Such a vehicle can't be catalog-scoped and - * fetches the brand's whole root tree — see CATEGORY_CAP. - */ - private isGenericModel(brandName: string | null, model: string | null): boolean { - const m = (model ?? "").trim(); - if (!m) return true; - return m.toLowerCase() === (brandName ?? "").trim().toLowerCase(); - } - - /** Flag a vehicle as poison (generic-model / over-cap) so init/scan skip it, - * and clear its in-flight + progress state. */ + /** Flag a vehicle as poison (its tree blew past CATEGORY_CAP — a generic + * root-catalog explosion from a decode that didn't resolve the model) so + * init/scan skip it, and clear its in-flight + progress state. Brand-agnostic: + * only an actual explosion trips it, so small legit generic-model PSA vehicles + * (which stay ~200 categories and DO yield parts) are never affected. */ private async markPoison(vehicleId: string): Promise { await this.redis.set(this.poisonKey(vehicleId), "1", POISON_TTL_S); await this.redis.del(`prefetch:scheduled:${vehicleId}`);