diff --git a/src/lib/agent/resolve.test.ts b/src/lib/agent/resolve.test.ts index 5c11cb7..ff3ca9c 100644 --- a/src/lib/agent/resolve.test.ts +++ b/src/lib/agent/resolve.test.ts @@ -1,5 +1,10 @@ import { describe, it, expect } from "vitest"; -import { resolveSite, type SiteRef } from "./resolve"; +import { + resolveSite, + resolveVehicle, + type SiteRef, + type VehicleRef, +} from "./resolve"; const SITES: SiteRef[] = [ { id: "aaa111", name: "The Hills", projectCode: "C26/09" }, @@ -67,4 +72,49 @@ describe("resolveSite", () => { const r = resolveSite("anything", []); expect(r.kind).toBe("none"); }); -}); \ No newline at end of file +}); +const VEHICLES: VehicleRef[] = [ + { id: "v1", description: "Center Point Grader", qrCodeId: "1626", projectCode: "C26/09" }, + { id: "v2", description: "Isuzu MY85HGGP", qrCodeId: "1936", projectCode: "C26/09" }, + { id: "v3", description: "TLB centre point", qrCodeId: "2201", projectCode: "C26/09" }, +]; + +describe("resolveVehicle", () => { + it("matches an exact record id", () => { + const r = resolveVehicle("v2", VEHICLES); + expect(r.kind).toBe("exact"); + if (r.kind === "exact") expect(r.vehicle.id).toBe("v2"); + }); + + it("matches a QR code", () => { + const r = resolveVehicle("1626", VEHICLES); + expect(r.kind).toBe("exact"); + if (r.kind === "exact") expect(r.vehicle.id).toBe("v1"); + }); + + it("matches a full description case-insensitively", () => { + const r = resolveVehicle("center point grader", VEHICLES); + expect(r.kind).toBe("exact"); + if (r.kind === "exact") expect(r.vehicle.id).toBe("v1"); + }); + + it("matches a unique substring", () => { + const r = resolveVehicle("isuzu", VEHICLES); + expect(r.kind).toBe("exact"); + if (r.kind === "exact") expect(r.vehicle.id).toBe("v2"); + }); + + it("reports ambiguity rather than guessing", () => { + const r = resolveVehicle("point", VEHICLES); + expect(r.kind).toBe("ambiguous"); + if (r.kind === "ambiguous") expect(r.matches.length).toBe(2); + }); + + it("returns none for no match", () => { + expect(resolveVehicle("helicopter", VEHICLES).kind).toBe("none"); + }); + + it("returns none for an empty query", () => { + expect(resolveVehicle(" ", VEHICLES).kind).toBe("none"); + }); +}); diff --git a/src/lib/agent/resolve.ts b/src/lib/agent/resolve.ts index 6c74542..8476ded 100644 --- a/src/lib/agent/resolve.ts +++ b/src/lib/agent/resolve.ts @@ -50,4 +50,54 @@ export function resolveSite(query: string, sites: SiteRef[]): ResolveSiteResult /** Human-readable disambiguation line for an ambiguous result. */ export function describeAmbiguity(matches: SiteRef[]): string { return matches.map((m) => `${m.name} (${m.projectCode})`).join(", "); +} + +export interface VehicleRef { + id: string; + description: string; + qrCodeId: string; + projectCode: string; +} + +export type ResolveVehicleResult = + | { kind: "exact"; vehicle: VehicleRef } + | { kind: "ambiguous"; matches: VehicleRef[] } + | { kind: "none" }; + +/** + * Free-text vehicle lookup. Mirrors resolveSite: id, then QR code, then + * exact description, then substring — and ambiguity is reported, never + * guessed. Picking the wrong vehicle silently attributes fuel spend to + * the wrong machine. + */ +export function resolveVehicle( + query: string, + vehicles: VehicleRef[], +): ResolveVehicleResult { + const q = query.trim().toLowerCase(); + if (!q) return { kind: "none" }; + + const byId = vehicles.find((v) => v.id === query.trim()); + if (byId) return { kind: "exact", vehicle: byId }; + + const byQr = vehicles.filter((v) => v.qrCodeId.toLowerCase() === q); + if (byQr.length === 1) return { kind: "exact", vehicle: byQr[0] }; + if (byQr.length > 1) return { kind: "ambiguous", matches: byQr }; + + const byDesc = vehicles.filter((v) => v.description.toLowerCase() === q); + if (byDesc.length === 1) return { kind: "exact", vehicle: byDesc[0] }; + if (byDesc.length > 1) return { kind: "ambiguous", matches: byDesc }; + + const bySub = vehicles.filter((v) => + v.description.toLowerCase().includes(q), + ); + if (bySub.length === 1) return { kind: "exact", vehicle: bySub[0] }; + if (bySub.length > 1) return { kind: "ambiguous", matches: bySub }; + + return { kind: "none" }; +} + +/** Human-readable disambiguation line for an ambiguous vehicle result. */ +export function describeVehicleAmbiguity(matches: VehicleRef[]): string { + return matches.map((m) => `${m.description} (QR ${m.qrCodeId})`).join(", "); } \ No newline at end of file