feat(FN-285): fix EMEX parallel decode race — always await second promise
Some checks failed
Sync dev → Gitea / Mirror dev to Gitea (push) Has been cancelled

Commits merged:
- fix(FN-285): fix EMEX parallel decode race — always await second promise

Files changed:
apps/api/src/vehicles/vehicles.service.spec.ts | 63 ++++++++++++++++++++++++++
 apps/api/src/vehicles/vehicles.service.ts      |  5 +-
 2 files changed, 66 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-285
This commit is contained in:
Fusion
2026-05-13 06:56:10 +00:00
parent b0edb079d9
commit 80596a4ceb
2 changed files with 66 additions and 2 deletions

View File

@@ -375,6 +375,69 @@ describe("VehiclesService", () => {
expect(result).toEqual(savedVehicle);
expect(vinApiService.decodeVin).toHaveBeenCalledWith("WBAPH5C55BA123456");
});
it("should use EMEX when it returns a vehicle (parallel decode)", async () => {
vi.mocked(isValidVin).mockReturnValue(true);
const emexVehicle = {
brand: "BMW",
model: "320i",
year: 2020,
engineCode: "N20",
engineType: null,
engineVolume: null,
transmission: "Auto",
bodyType: "Sedan",
driveType: null,
raw: {},
};
let selectCall = 0;
let insertCall = 0;
const savedVehicle = { id: "v-emex", vin: "WBAPH5C55BA123456", source: "emex" };
const db = {
select: vi.fn().mockImplementation(() => {
selectCall++;
const chain: Record<string, any> = {};
chain.from = vi.fn().mockReturnValue(chain);
chain.innerJoin = vi.fn().mockReturnValue(chain);
chain.where = vi.fn().mockReturnValue(chain);
chain.limit = vi.fn().mockImplementation(() => {
if (selectCall === 1) return []; // no cache
if (selectCall === 2) return [{ id: "b1", name: "BMW" }]; // brand
if (selectCall === 3) return [{ brandCount: 0 }]; // subscription (unlimited)
return [];
});
return chain;
}),
insert: vi.fn().mockImplementation(() => {
insertCall++;
const insertChain: Record<string, any> = {};
insertChain.values = vi.fn().mockReturnValue(insertChain);
insertChain.returning = vi.fn().mockImplementation(() => {
if (insertCall === 1) return [savedVehicle]; // vehicles upsert
return [];
});
insertChain.onConflictDoNothing = vi.fn().mockReturnValue(insertChain);
insertChain.onConflictDoUpdate = vi.fn().mockReturnValue(insertChain);
return insertChain;
}),
};
const { service, partsCatalogsService, emexService } = createService(db);
// PCAT returns 0 cars — not definitive, falls through to EMEX check
partsCatalogsService.decodeVin.mockResolvedValue(null);
// EMEX returns a single vehicle with a known brand
emexService.decodeVinOrCandidates.mockResolvedValue({
type: "vehicle",
vehicle: emexVehicle,
});
const result = await service.decodeVin("WBAPH5C55BA123456", "u1");
expect(result).toEqual(savedVehicle);
expect(partsCatalogsService.decodeVin).toHaveBeenCalled();
expect(emexService.decodeVinOrCandidates).toHaveBeenCalledWith("WBAPH5C55BA123456");
});
});
describe("getHistory", () => {

View File

@@ -558,9 +558,10 @@ export class VehiclesService {
if (!earlyPcatDefinitive && !earlyEmexDefinitive) {
// First completer wasn't definitive — wait for the other one.
if (earlyWinner.kind === "pcat" && !emexResolved) {
// Always await the other promise (if already resolved, returns immediately).
if (earlyWinner.kind === "pcat") {
emexResult = await emexTimedPromise;
} else if (earlyWinner.kind === "emex" && !pcatResolved) {
} else if (earlyWinner.kind === "emex") {
pcatResult = await pcatPromise;
}
if (signal?.aborted) return null;