5a048488ea
- aktualisierung.py: Backup-Ordner verliert sein manifest.json beim Sichern, sonst laedt HA ihn nach einem Neustart als zweite audi_dashboard-Integration (Ursache des raetselhaften "gleicher Pfad"-Umbenennungs-Fehlers). - service.ts/audi-dashboard-app.js: Uebersicht faellt jetzt wie Service schon auf die Fahrzeugmeldung zurueck, wenn kein Servicebucheintrag existiert; eigenes (kuerzeres) Oelwechsel-Intervall wird jetzt auch ohne Servicebuch- eintrag naeherungsweise aus der Herstellermeldung hochgerechnet, statt weiterhin unveraendert die Herstellervorgabe zu zeigen. - Sicherheit neu gestaltet: vier Sammelzeilen (Fahrzeug verriegelt / Tueren und Klappen geschlossen / Fenster und Dach geschlossen / Kein Licht) statt einer flachen Liste, mit neuen Tuerschloss-, Dach- und Standlicht-Sensor- rollen, Kreis-Haekchen-Symbolen und einer Detailseite je Tuer/Motorhaube/ Kofferraum in beiden Frontends. Details, Verifikation und Sensor-Rollen in AGENTS.md (Abschnitte J/M/N). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
350 lines
14 KiB
TypeScript
350 lines
14 KiB
TypeScript
import { describe, expect, it } from "vitest"
|
|
|
|
import {
|
|
inspektionPrognose,
|
|
kmProTagAusFahrten,
|
|
letzterOelwechsel,
|
|
meldungsPrognose,
|
|
meldungsPrognoseEigenesIntervall,
|
|
naechsterService,
|
|
oelMeldungsPrognose,
|
|
oelwechselPrognose,
|
|
} from "./service"
|
|
import type { Fahrzeug } from "./profilAdapter"
|
|
|
|
function fahrzeug(odo: number, modus = "hersteller"): Fahrzeug {
|
|
return {
|
|
fin: "",
|
|
details: "",
|
|
erstzulassung: "",
|
|
hu: "",
|
|
odo,
|
|
odoBekannt: true,
|
|
tankPct: 0,
|
|
reichweite: null,
|
|
batteriespannung: null,
|
|
gesichert: null,
|
|
sicherheitscheck: [],
|
|
zuendung: null,
|
|
standortLat: null,
|
|
standortLon: null,
|
|
standortZeit: null,
|
|
oelwechselFaelligTs: null,
|
|
oelwechselFaelligKm: null,
|
|
inspektionFaelligTs: null,
|
|
inspektionFaelligKm: null,
|
|
smartdeal: { aktiv: false, laeuftAb: null },
|
|
oel: { modus, km: 15000, monate: 12, herstellerKm: 30000, herstellerMonate: 24 },
|
|
versicherung: {},
|
|
steuer: { faellig: "" },
|
|
reifen: { aktiv: "Sommer", anzugsmoment: null, wechsel: {}, sommer: {}, winter: {} },
|
|
service: {},
|
|
technik: {},
|
|
ausstattung: [],
|
|
}
|
|
}
|
|
|
|
const BUCH = [
|
|
{ datum: "2026-02-10", km: 40000, art: "Ölwechsel" },
|
|
{ datum: "2025-03-05", km: 25000, art: "Inspektion" },
|
|
]
|
|
|
|
describe("letzterOelwechsel", () => {
|
|
it("findet den jüngsten Ölwechsel und ignoriert andere Arten", () => {
|
|
expect(letzterOelwechsel(BUCH)?.datum).toBe("2026-02-10")
|
|
})
|
|
|
|
it("erkennt auch die Schreibweise ohne Umlaut", () => {
|
|
expect(letzterOelwechsel([{ datum: "2026-01-01", km: 1, art: "Oelwechsel" }])).not.toBeNull()
|
|
})
|
|
|
|
it("liefert null, wenn nie einer eingetragen wurde", () => {
|
|
expect(letzterOelwechsel([{ datum: "2026-01-01", km: 1, art: "Inspektion" }])).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe("oelwechselPrognose", () => {
|
|
const jetzt = new Date("2026-08-11T12:00:00")
|
|
|
|
it("rechnet ab dem letzten Ölwechsel, nicht ab dem ersten Buchungseintrag", () => {
|
|
// Seit 10.02. sind 182 Tage vergangen, 6000 km gefahren -> rund 33 km/Tag.
|
|
// Ziel ist 40000 + 30000 = 70000, offen sind also 24000 km.
|
|
const p = oelwechselPrognose(fahrzeug(46000), BUCH, jetzt)
|
|
expect(p).not.toBeNull()
|
|
expect(p!.restKm).toBe(24000)
|
|
expect(p!.kmProTag).toBeGreaterThan(30)
|
|
expect(p!.kmProTag).toBeLessThan(36)
|
|
})
|
|
|
|
it("deckelt auf das Zeitlimit, wenn wenig gefahren wurde", () => {
|
|
// Nur 100 km seit dem Wechsel: die Kilometerprognose läge Jahre entfernt,
|
|
// das Zeitlimit von 24 Monaten greift vorher.
|
|
const p = oelwechselPrognose(fahrzeug(40100), BUCH, jetzt)
|
|
expect(p!.durchZeitlimit).toBe(true)
|
|
expect(p!.datum).toBe("2028-02-10")
|
|
})
|
|
|
|
it("meldet Fälligkeit sofort, wenn das Intervall überschritten ist", () => {
|
|
const p = oelwechselPrognose(fahrzeug(75000), BUCH, jetzt)
|
|
expect(p!.restKm).toBe(0)
|
|
expect(p!.datum).toBe("2026-08-11")
|
|
})
|
|
|
|
it("nutzt bei eigenem Intervall den kleineren der beiden Werte", () => {
|
|
// modus "eigen": min(15000, 30000) = 15000 -> Ziel 55000, offen 9000.
|
|
const p = oelwechselPrognose(fahrzeug(46000, "eigen"), BUCH, jetzt)
|
|
expect(p!.restKm).toBe(9000)
|
|
})
|
|
|
|
it("liefert nichts ohne bekannten Kilometerstand", () => {
|
|
const ohne = { ...fahrzeug(0), odoBekannt: false }
|
|
expect(oelwechselPrognose(ohne, BUCH, jetzt)).toBeNull()
|
|
})
|
|
|
|
it("liefert nichts, wenn nie ein Ölwechsel eingetragen wurde", () => {
|
|
expect(oelwechselPrognose(fahrzeug(46000), [], jetzt)).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe("inspektionPrognose", () => {
|
|
const jetzt = new Date("2026-08-11T12:00:00")
|
|
|
|
it("rechnet mit dem festen Intervall 30.000 km / 24 Monate, nicht dem Ölwechsel-Intervall", () => {
|
|
// Letzte Inspektion 05.03.2025 bei 25000 km -> Ziel 55000. Bei 46000 km
|
|
// sind das 9000 km offen, unabhängig vom oel-Feld des Fahrzeugs (15000/12).
|
|
const p = inspektionPrognose(fahrzeug(46000), BUCH, jetzt)
|
|
expect(p).not.toBeNull()
|
|
expect(p!.restKm).toBe(9000)
|
|
})
|
|
|
|
it("ignoriert den Ölwechsel-Eintrag bei der Suche nach der letzten Inspektion", () => {
|
|
expect(inspektionPrognose(fahrzeug(46000), [BUCH[0]!], jetzt)).toBeNull()
|
|
})
|
|
|
|
it("liefert nichts, wenn nie eine Inspektion eingetragen wurde", () => {
|
|
expect(inspektionPrognose(fahrzeug(46000), [], jetzt)).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe("naechsterService", () => {
|
|
const jetzt = new Date("2026-08-11T12:00:00")
|
|
|
|
it("wählt über Ölwechsel und Inspektion hinweg das zeitlich frühere Datum", () => {
|
|
// Beide unabhängig mit derselben Formel berechnet, statt ein erwartetes
|
|
// Datum von Hand vorzurechnen - so bleibt der Test robust gegen spätere
|
|
// Änderungen an den Intervallen, prüft aber trotzdem die Auswahllogik.
|
|
const oel = oelwechselPrognose(fahrzeug(46000), BUCH, jetzt)
|
|
const insp = inspektionPrognose(fahrzeug(46000), BUCH, jetzt)
|
|
const erwarteteArt = oel!.datum <= insp!.datum ? "Ölwechsel" : "Inspektion"
|
|
|
|
const s = naechsterService(fahrzeug(46000), BUCH, [], jetzt)
|
|
expect(s).not.toBeNull()
|
|
expect(s!.art).toBe(erwarteteArt)
|
|
expect(s!.datum).toBe(erwarteteArt === "Ölwechsel" ? oel!.datum : insp!.datum)
|
|
})
|
|
|
|
it("nimmt die Hauptuntersuchung auf, obwohl sie keine eigene Prognose hat", () => {
|
|
const f = { ...fahrzeug(46000), hu: "2026-09-01" }
|
|
const s = naechsterService(f, [], [], jetzt)
|
|
expect(s).not.toBeNull()
|
|
expect(s!.art).toBe("Hauptuntersuchung")
|
|
expect(s!.restKm).toBeNull()
|
|
expect(s!.datum).toBe("2026-09-01")
|
|
})
|
|
|
|
it("liefert nichts ohne Servicebuch und ohne gepflegte Hauptuntersuchung", () => {
|
|
expect(naechsterService(fahrzeug(46000), [], [], jetzt)).toBeNull()
|
|
})
|
|
|
|
it("fällt ohne Servicebucheintrag auf die vom Fahrzeug gemeldete Ölwechsel-Fälligkeit zurück", () => {
|
|
// Ein frisch eingerichtetes Fahrzeug mit zugeordnetem Sensor, aber noch
|
|
// ganz ohne Servicebuch, muss die Meldung trotzdem zeigen - siehe
|
|
// AGENTS.md, Abschnitt H, "no inspektion and ölwechsel shown ...".
|
|
const f = { ...fahrzeug(46000), oelwechselFaelligTs: "2027-03-14T00:00:00+00:00", oelwechselFaelligKm: 11750 }
|
|
const s = naechsterService(f, [], [], jetzt)
|
|
expect(s).not.toBeNull()
|
|
expect(s!.art).toBe("Ölwechsel")
|
|
expect(s!.datum).toBe("2027-03-14")
|
|
expect(s!.restKm).toBe(11750)
|
|
})
|
|
|
|
it("fällt ohne Servicebucheintrag auf die vom Fahrzeug gemeldete Inspektions-Fälligkeit zurück", () => {
|
|
const f = { ...fahrzeug(46000), inspektionFaelligTs: "2027-01-01T00:00:00+00:00", inspektionFaelligKm: 5000 }
|
|
const s = naechsterService(f, [], [], jetzt)
|
|
expect(s).not.toBeNull()
|
|
expect(s!.art).toBe("Inspektion")
|
|
expect(s!.datum).toBe("2027-01-01")
|
|
expect(s!.restKm).toBe(5000)
|
|
})
|
|
|
|
it("bevorzugt die eigene Prognose gegenüber der Fahrzeugmeldung, wenn beide vorliegen", () => {
|
|
// Servicebucheintrag vorhanden -> oelwechselPrognose() greift; die
|
|
// Meldung (weit in der Zukunft) darf sie nicht verdrängen.
|
|
const f = { ...fahrzeug(46000), oelwechselFaelligTs: "2099-01-01T00:00:00+00:00", oelwechselFaelligKm: 99999 }
|
|
const oel = oelwechselPrognose(f, BUCH, jetzt)
|
|
expect(oel).not.toBeNull()
|
|
const s = naechsterService(f, BUCH, [], jetzt)
|
|
expect(s!.datum).not.toBe("2099-01-01")
|
|
})
|
|
})
|
|
|
|
describe("kmProTagAusFahrten", () => {
|
|
const jetzt = new Date("2026-08-11T12:00:00")
|
|
|
|
it("rechnet aus abgeschlossenen Fahrten seit der ältesten die km/Tag-Rate", () => {
|
|
// Exakt 30 Tage vor jetzt (gleiche Uhrzeit), 900 km insgesamt -> 30 km/Tag.
|
|
const fahrten = [
|
|
{ ts_start: "2026-07-12T12:00:00", distance_km: 400, status: "abgeschlossen" },
|
|
{ ts_start: "2026-08-01T08:00:00", distance_km: 500, status: "abgeschlossen" },
|
|
]
|
|
expect(kmProTagAusFahrten(fahrten, jetzt)).toBeCloseTo(30, 5)
|
|
})
|
|
|
|
it("ignoriert offene Fahrten und Fahrten ohne bekannte Distanz", () => {
|
|
const fahrten = [
|
|
{ ts_start: "2026-07-12T08:00:00", distance_km: 400, status: "abgeschlossen" },
|
|
{ ts_start: "2026-08-10T08:00:00", distance_km: null, status: "abgeschlossen" },
|
|
{ ts_start: "2026-08-11T08:00:00", distance_km: 9999, status: "offen" },
|
|
]
|
|
// Nur die erste Fahrt zählt -> dieselbe Rate wie ohne die beiden anderen.
|
|
expect(kmProTagAusFahrten(fahrten, jetzt)).toBe(
|
|
kmProTagAusFahrten([fahrten[0]!], jetzt),
|
|
)
|
|
})
|
|
|
|
it("liefert nichts ohne Fahrten oder ohne belastbaren Zeitraum", () => {
|
|
expect(kmProTagAusFahrten([], jetzt)).toBeNull()
|
|
expect(kmProTagAusFahrten([{ ts_start: jetzt.toISOString(), distance_km: 5, status: "abgeschlossen" }], jetzt))
|
|
.toBeNull()
|
|
})
|
|
})
|
|
|
|
describe("meldungsPrognose", () => {
|
|
const jetzt = new Date("2026-08-11T12:00:00")
|
|
// 30 km/Tag, siehe kmProTagAusFahrten-Tests oben.
|
|
const fahrten = [
|
|
{ ts_start: "2026-07-12T12:00:00", distance_km: 400, status: "abgeschlossen" },
|
|
{ ts_start: "2026-08-01T08:00:00", distance_km: 500, status: "abgeschlossen" },
|
|
]
|
|
|
|
it("rechnet die Restkilometer mit der Fahrleistung aus dem Fahrtenlog auf ein Datum hoch", () => {
|
|
// 300 km Rest bei 30 km/Tag -> 10 Tage.
|
|
const p = meldungsPrognose("2099-01-01T00:00:00+00:00", 300, fahrten, jetzt)
|
|
expect(p).not.toBeNull()
|
|
expect(p!.datum).toBe("2026-08-21")
|
|
expect(p!.durchZeitlimit).toBe(false)
|
|
})
|
|
|
|
it("deckelt auf die vom Fahrzeug gemeldete Zeitgrenze, wenn zu wenig gefahren wird", () => {
|
|
// 30000 km Rest bei 30 km/Tag läge Jahre in der Zukunft - die gemeldete
|
|
// Zeitgrenze (fünf Tage später) greift zuerst.
|
|
const p = meldungsPrognose("2026-08-16T00:00:00+00:00", 30000, fahrten, jetzt)
|
|
expect(p).not.toBeNull()
|
|
expect(p!.datum).toBe("2026-08-16")
|
|
expect(p!.durchZeitlimit).toBe(true)
|
|
})
|
|
|
|
it("fällt ohne Fahrtenlog auf die reine Zeitgrenze der Meldung zurück", () => {
|
|
const p = meldungsPrognose("2027-01-01T00:00:00+00:00", 5000, [], jetzt)
|
|
expect(p).not.toBeNull()
|
|
expect(p!.datum).toBe("2027-01-01")
|
|
expect(p!.durchZeitlimit).toBe(true)
|
|
})
|
|
|
|
it("liefert nichts ohne jede brauchbare Angabe", () => {
|
|
expect(meldungsPrognose(null, null, fahrten, jetzt)).toBeNull()
|
|
})
|
|
|
|
it("gibt die Restkilometer mit zurück", () => {
|
|
const p = meldungsPrognose("2027-01-01T00:00:00+00:00", 5000, [], jetzt)
|
|
expect(p!.restKm).toBe(5000)
|
|
})
|
|
})
|
|
|
|
describe("meldungsPrognoseEigenesIntervall", () => {
|
|
const jetzt = new Date("2026-08-11T12:00:00")
|
|
// 30 km/Tag, siehe kmProTagAusFahrten-Tests oben.
|
|
const fahrten = [
|
|
{ ts_start: "2026-07-12T12:00:00", distance_km: 400, status: "abgeschlossen" },
|
|
{ ts_start: "2026-08-01T08:00:00", distance_km: 500, status: "abgeschlossen" },
|
|
]
|
|
|
|
it("rechnet Restkilometer und Zeitgrenze auf das eigene, kürzere Intervall um", () => {
|
|
// Herstellermeldung: 25.000 km / 24 Monate Rest bis zum 2027-08-11 -
|
|
// daraus ergibt sich ein angenommener letzter Service vor 2025-08-11.
|
|
// Eigenes Intervall (15.000 km / 12 Monate) ab demselben Punkt: Zeit-
|
|
// grenze 2026-08-11 (heute), Restkilometer 25000 - 30000 + 15000 = 10000.
|
|
const p = meldungsPrognoseEigenesIntervall(
|
|
"2027-08-11T00:00:00+00:00", 25000, 30000, 24, 15000, 12, fahrten, jetzt,
|
|
)
|
|
expect(p).not.toBeNull()
|
|
expect(p!.restKm).toBe(10000)
|
|
// 10.000 km bei 30 km/Tag läge weit in der Zukunft - die zurückgerechnete
|
|
// eigene Zeitgrenze (heute) greift zuerst.
|
|
expect(p!.durchZeitlimit).toBe(true)
|
|
expect(p!.datum).toBe("2026-08-11")
|
|
})
|
|
|
|
it("deckelt die Restkilometer bei null, wenn das eigene Intervall rechnerisch schon überschritten ist", () => {
|
|
// Erst 10.000 von 30.000 km seit der Herstellerfälligkeit gefahren -
|
|
// das kürzere eigene Intervall (15.000 km) ist rechnerisch längst fällig.
|
|
const p = meldungsPrognoseEigenesIntervall(
|
|
"2027-08-11T00:00:00+00:00", 10000, 30000, 24, 15000, 12, fahrten, jetzt,
|
|
)
|
|
expect(p).not.toBeNull()
|
|
expect(p!.restKm).toBe(0)
|
|
expect(p!.durchZeitlimit).toBe(false)
|
|
})
|
|
|
|
it("fällt ohne Fahrtenlog auf die zurückgerechnete eigene Zeitgrenze zurück", () => {
|
|
const p = meldungsPrognoseEigenesIntervall(
|
|
"2027-08-11T00:00:00+00:00", 25000, 30000, 24, 15000, 12, [], jetzt,
|
|
)
|
|
expect(p).not.toBeNull()
|
|
expect(p!.datum).toBe("2026-08-11")
|
|
expect(p!.durchZeitlimit).toBe(true)
|
|
})
|
|
|
|
it("liefert nichts ohne bekannte Herstellerintervall-Länge", () => {
|
|
expect(
|
|
meldungsPrognoseEigenesIntervall("2027-08-11T00:00:00+00:00", 25000, null, 24, 15000, 12, fahrten, jetzt),
|
|
).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe("oelMeldungsPrognose", () => {
|
|
const jetzt = new Date("2026-08-11T12:00:00")
|
|
const fahrten = [
|
|
{ ts_start: "2026-07-12T12:00:00", distance_km: 400, status: "abgeschlossen" },
|
|
{ ts_start: "2026-08-01T08:00:00", distance_km: 500, status: "abgeschlossen" },
|
|
]
|
|
|
|
it("übernimmt bei Herstellervorgabe unverändert die reine Fahrzeugmeldung", () => {
|
|
const f = fahrzeug(10000, "hersteller")
|
|
f.oelwechselFaelligTs = "2027-08-11T00:00:00+00:00"
|
|
f.oelwechselFaelligKm = 25000
|
|
const p = oelMeldungsPrognose(f, fahrten, jetzt)
|
|
expect(p!.restKm).toBe(25000)
|
|
expect(p!.datum).toBe("2027-08-11")
|
|
})
|
|
|
|
it("rechnet bei eigenem Intervall auf dessen kürzere Vorgabe um", () => {
|
|
const f = fahrzeug(10000, "eigen")
|
|
f.oelwechselFaelligTs = "2027-08-11T00:00:00+00:00"
|
|
f.oelwechselFaelligKm = 25000
|
|
const p = oelMeldungsPrognose(f, fahrten, jetzt)
|
|
expect(p!.restKm).toBe(10000)
|
|
expect(p!.datum).toBe("2026-08-11")
|
|
})
|
|
|
|
it("fällt bei eigenem Intervall ohne Herstellerwerte auf die reine Meldung zurück", () => {
|
|
const f = fahrzeug(10000, "eigen")
|
|
f.oel.herstellerKm = null
|
|
f.oelwechselFaelligTs = "2027-08-11T00:00:00+00:00"
|
|
f.oelwechselFaelligKm = 25000
|
|
const p = oelMeldungsPrognose(f, fahrten, jetzt)
|
|
expect(p!.restKm).toBe(25000)
|
|
expect(p!.datum).toBe("2027-08-11")
|
|
})
|
|
})
|