7b6619133f
Dokumentation: - MASTER-PROMPT.md uppdaterad till v2.0 (SaaS/AI-wrapper, roller, multi-tenant, onboarding, beta-fokus). - Nya moduler: kommunikationsmodell (tal in/text ut, Push-to-Talk, redigering före skick), Live Share (behörighetsstyrd realtidslänk, versionsmärkta exporter), verifierade checklistor (bevis och kontext per kontrolltyp). Implementation i MVP:t: - Push-to-Talk-röstinmatning via webbläsarens taligenkänning (sv-SE): lyssnar endast efter aktivt tryck, röd indikator med realtids- transkript, texten hamnar i redigerbart fält och skickas aldrig automatiskt. Döljs i webbläsare utan talstöd. - Verifierade checklistor: varje kontroll i metodikerna har minimikrav (foto/mätvärde/kommentar); fotokontroller verifieras med bild och mätkontroller kan inte markeras verifierade utan värde. - Versionsmärkt JSON-export ur händelseloggen; exporten loggas själv som händelse med format, version och användare. Verifierat: 12 vitest-tester gröna, produktionsbygge ok, utökat Playwright-röktest genom hela flödet inkl. fotokravet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
175 lines
7.0 KiB
TypeScript
175 lines
7.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { Arende, Handelse } from "../domain";
|
|
import { nyLoggPost } from "../domain";
|
|
import { VIBRATION_METODIK, nastaSteg, valjMetodik } from "../metodik";
|
|
import { brief, ejKontrollerat, tidsfordelning } from "../projektioner";
|
|
|
|
// Hjälpare: bygger ett ärende ur en lista (tidpunkt, händelse) —
|
|
// precis som produktionskoden är alla vyer rena funktioner av loggen.
|
|
function byggArende(poster: [string, Handelse][]): Arende {
|
|
return {
|
|
id: "test",
|
|
nummer: 1,
|
|
skapad: poster[0]?.[0] ?? "2026-08-02T08:00:00Z",
|
|
handelser: poster.map(([tidpunkt, handelse]) => nyLoggPost("Anna", handelse, tidpunkt)),
|
|
};
|
|
}
|
|
|
|
const OBJEKT: Handelse = {
|
|
typ: "objekt_identifierat",
|
|
objekt: {
|
|
typ: "Fordon",
|
|
identifierare: "ABC123",
|
|
identifieringsmetod: "Registreringsnummer",
|
|
beskrivning: "Volvo XC60 D4 2019",
|
|
kund: "Anders Svensson",
|
|
},
|
|
};
|
|
|
|
describe("verifierade checklistor", () => {
|
|
it("varje kontroll i metodikerna har ett definierat minimikrav", async () => {
|
|
const { GENERISK_METODIK } = await import("../metodik");
|
|
for (const metodik of [VIBRATION_METODIK, GENERISK_METODIK]) {
|
|
for (const steg of metodik.steg) {
|
|
for (const kontroll of steg.kontroller ?? []) {
|
|
expect(kontroll.krav, `${metodik.id}/${steg.id}/${kontroll.id}`).toBeDefined();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("metodikval", () => {
|
|
it("väljer vibrationsmetodiken när felbeskrivningen nämner vibration", () => {
|
|
expect(valjMetodik("Bilen vibrerar runt 88 km/h").id).toBe("vibration");
|
|
expect(valjMetodik("Motorn startar inte").id).toBe("generisk");
|
|
});
|
|
});
|
|
|
|
describe("nastaSteg", () => {
|
|
it("börjar med första symptomfrågan och går vidare i metodikens ordning", () => {
|
|
const arende = byggArende([
|
|
["2026-08-02T08:03:00Z", OBJEKT],
|
|
["2026-08-02T08:05:00Z", { typ: "felbeskrivning", text: "Bilen vibrerar runt 88 km/h" }],
|
|
]);
|
|
const steg = nastaSteg(arende, VIBRATION_METODIK);
|
|
expect(steg.klart).toBe(false);
|
|
expect(steg.steg.id).toBe("symptom");
|
|
expect(steg.fraga?.id).toBe("hastighetsberoende");
|
|
});
|
|
|
|
it("hoppar till nästa obesvarade fråga när en besvarats", () => {
|
|
const arende = byggArende([
|
|
["2026-08-02T08:03:00Z", OBJEKT],
|
|
[
|
|
"2026-08-02T08:06:00Z",
|
|
{ typ: "fraga_besvarad", stegId: "symptom", frageId: "hastighetsberoende", fraga: "…", svar: "Ja" },
|
|
],
|
|
]);
|
|
expect(nastaSteg(arende, VIBRATION_METODIK).fraga?.id).toBe("var");
|
|
});
|
|
|
|
it("är klart när alla frågor och kontroller är dokumenterade", () => {
|
|
const poster: [string, Handelse][] = [["2026-08-02T08:00:00Z", OBJEKT]];
|
|
for (const steg of VIBRATION_METODIK.steg) {
|
|
for (const fraga of steg.fragor ?? []) {
|
|
poster.push([
|
|
"2026-08-02T08:10:00Z",
|
|
{ typ: "fraga_besvarad", stegId: steg.id, frageId: fraga.id, fraga: fraga.text, svar: "Ja" },
|
|
]);
|
|
}
|
|
for (const kontroll of steg.kontroller ?? []) {
|
|
poster.push([
|
|
"2026-08-02T08:20:00Z",
|
|
{ typ: "kontroll_utford", stegId: steg.id, kontrollId: kontroll.id, text: kontroll.text },
|
|
]);
|
|
}
|
|
}
|
|
expect(nastaSteg(byggArende(poster), VIBRATION_METODIK).klart).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("ejKontrollerat", () => {
|
|
it("krymper när kontroller utförs och härleds helt ur loggen", () => {
|
|
const utan = byggArende([["2026-08-02T08:00:00Z", OBJEKT]]);
|
|
const alla = ejKontrollerat(utan, VIBRATION_METODIK);
|
|
expect(alla).toContain("Kontrollera lufttryck");
|
|
|
|
const med = byggArende([
|
|
["2026-08-02T08:00:00Z", OBJEKT],
|
|
[
|
|
"2026-08-02T08:15:00Z",
|
|
{ typ: "kontroll_utford", stegId: "kontroller", kontrollId: "lufttryck", text: "Kontrollera lufttryck" },
|
|
],
|
|
]);
|
|
const kvar = ejKontrollerat(med, VIBRATION_METODIK);
|
|
expect(kvar).not.toContain("Kontrollera lufttryck");
|
|
expect(kvar.length).toBe(alla.length - 1);
|
|
});
|
|
});
|
|
|
|
describe("tidsfordelning", () => {
|
|
it("attribuerar intervall till aktiv kategori och räknar inte paus i totalen", () => {
|
|
const arende = byggArende([
|
|
["2026-08-02T08:00:00Z", OBJEKT],
|
|
// 30 min aktiv felsökning (standardkategori)
|
|
["2026-08-02T08:30:00Z", { typ: "kategori_byte", kategori: "paus" }],
|
|
// 15 min paus
|
|
["2026-08-02T08:45:00Z", { typ: "kategori_byte", kategori: "provkorning" }],
|
|
// 20 min provkörning
|
|
["2026-08-02T09:05:00Z", { typ: "arende_avslutat" }],
|
|
]);
|
|
const { totalMs, perKategori } = tidsfordelning(arende);
|
|
expect(perKategori.aktiv_felsokning).toBe(30 * 60000);
|
|
expect(perKategori.paus).toBe(15 * 60000);
|
|
expect(perKategori.provkorning).toBe(20 * 60000);
|
|
expect(totalMs).toBe(50 * 60000);
|
|
});
|
|
|
|
it("räknar pågående tid fram till 'nu' för öppna ärenden", () => {
|
|
const arende = byggArende([["2026-08-02T08:00:00Z", OBJEKT]]);
|
|
const { totalMs } = tidsfordelning(arende, "2026-08-02T08:10:00Z");
|
|
expect(totalMs).toBe(10 * 60000);
|
|
});
|
|
});
|
|
|
|
describe("brief", () => {
|
|
const arende = byggArende([
|
|
["2026-08-02T08:03:00Z", OBJEKT],
|
|
["2026-08-02T08:05:00Z", { typ: "felbeskrivning", text: "Bilen vibrerar runt 88 km/h" }],
|
|
[
|
|
"2026-08-02T08:11:00Z",
|
|
{ typ: "kontroll_utford", stegId: "kontroller", kontrollId: "lufttryck", text: "Kontrollera lufttryck", resultat: "2,4 bar" },
|
|
],
|
|
["2026-08-02T08:18:00Z", { typ: "matvarde", beskrivning: "Matningsspänning", varde: "13,9", enhet: "V" }],
|
|
["2026-08-02T08:20:00Z", { typ: "observation", text: "Höger framdäck visar ojämnt slitage" }],
|
|
["2026-08-02T08:22:00Z", { typ: "hypotes", text: "Obalans i höger framhjul", niva: "lag" }],
|
|
]);
|
|
|
|
it("sammanställer utfört, observerat, ej kontrollerat och nästa steg ur loggen", () => {
|
|
const b = brief(arende, VIBRATION_METODIK, "2026-08-02T08:30:00Z");
|
|
expect(b.objekt?.identifierare).toBe("ABC123");
|
|
expect(b.felbeskrivning).toBe("Bilen vibrerar runt 88 km/h");
|
|
expect(b.utfordaKontroller.map((k) => k.text)).toContain("Kontrollera lufttryck");
|
|
expect(b.observationer).toContain("Höger framdäck visar ojämnt slitage");
|
|
expect(b.observationer).toContain("Matningsspänning: 13,9 V");
|
|
expect(b.ejKontrollerat).toContain("Kontrollera hjulbalansering");
|
|
expect(b.ejKontrollerat).not.toContain("Kontrollera lufttryck");
|
|
expect(b.rekommenderatNastaSteg[0]).toContain("hastighetsberoende");
|
|
expect(b.totalArbetstid).toBe("27 min");
|
|
});
|
|
|
|
it("skiljer hypoteser från fakta och markerar felorsak som ej verifierad", () => {
|
|
const b = brief(arende, VIBRATION_METODIK, "2026-08-02T08:30:00Z");
|
|
expect(b.hypoteser).toEqual([{ text: "Obalans i höger framhjul", niva: "lag" }]);
|
|
expect(b.observationer).not.toContain("Obalans i höger framhjul");
|
|
expect(b.tillforlitlighet.some((r) => r.niva === "medel" && /inte verifierad/.test(r.text))).toBe(true);
|
|
});
|
|
|
|
it("är regenererbar: samma logg ger alltid samma brief", () => {
|
|
const b1 = brief(arende, VIBRATION_METODIK, "2026-08-02T08:30:00Z");
|
|
const b2 = brief(arende, VIBRATION_METODIK, "2026-08-02T08:30:00Z");
|
|
expect(b2).toEqual(b1);
|
|
});
|
|
});
|