fix(prefetch/faz6): generic-model skip'i KALDIR — PSA'yı yanlış zehirliyordu
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Tespit: generic-model (model==brand) sadece Opel'i değil PSA'yı (Peugeot/Citroën) da kapsıyor; ama PSA generic-model PATLAMIYOR (~200 kat) ve PARÇA ALIYOR (512/290). Skip onları yanlışlıkla poison'lar + prefetch'i keserdi. Tek doğru guard = CATEGORY_CAP (marka-bağımsız; yalnız gerçek patlamada >3000 tetiklenir → Opel'i durdurur, küçük PSA'yı korur). isGenericModel + processInit gate kaldırıldı, spec sadeleşti (cap + scan-skip kaldı). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<void> };
|
||||
describe("poison guard (Faz 6: category cap — brand-agnostic anti-explosion)", () => {
|
||||
type PC = { processChildren: (j: unknown) => Promise<void> };
|
||||
|
||||
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[]) =>
|
||||
|
||||
@@ -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<void> {
|
||||
await this.redis.set(this.poisonKey(vehicleId), "1", POISON_TTL_S);
|
||||
await this.redis.del(`prefetch:scheduled:${vehicleId}`);
|
||||
|
||||
Reference in New Issue
Block a user