feat: Sprachnotiz in der Durchsicht, Info-Seite und Zähler (T7 L5 Tasks 20, 22)
Task 20: /inspections/new?voice=<id> zeigt die Sprachnotiz über dem Formular (Abgeschriebenes, Unklares, „Alle übernehmen“ mit Rückfrage) und Vorschläge unter den Feldern mit wörtlichem Zitat; nichts wird ohne Tipp übernommen, die Volkskarte wird nie angehakt. Bei ausgeschalteter Auswertung ist der Text offen zum Abtippen. Speichern schickt voiceCaptureId; bei 409 VOICE_CAPTURE_GONE genau ein neuer Versuch ohne Notiz mit demselben Idempotency-Key (online und aus der Warteschlange); VOICE_CAPTURE_COLONY_MISMATCH nie automatisch, sondern „Ohne Sprachnotiz speichern/senden“. Skalen aus lib/inspections/scales.ts mit Paritätstest zum Backend. Task 22: /settings/voice mit Modell und Fristen (bei ausgeschalteter Auswertung ausdrücklich: nichts geht an einen KI-Dienst), „Alle meine Sprachnotizen löschen“ auf Server und Gerät mit Anzahl; Systemkarte „Sprachnotizen“ (nur system:manage) mit Zählern und Kosten der letzten 7 Tage (Ruling 15); Service Worker cacht /api/v1/voice-captures nie, /voice und /voice/new offline erreichbar. Gates: Frontend lint, typecheck, 3106 Tests, build. Refs #27 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
51206bea4f
commit
ce922c8569
40 files changed
+3347
-45
No files matched your search
@@ -0,0 +1,61 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
INSPECTION_SCALE_DIRECTIONS,
|
||||
INSPECTION_SCALE_LABELS,
|
||||
type InspectionScaleField,
|
||||
} from "./scales";
|
||||
|
||||
interface ScaleFixture {
|
||||
field: string;
|
||||
storedOne: string;
|
||||
storedFive: string;
|
||||
anchors: Record<string, number>;
|
||||
}
|
||||
|
||||
// The one source both sides read: the backend prompt and this form.
|
||||
const FIXTURE_PATH = path.resolve(
|
||||
process.cwd(),
|
||||
"../ApiCore-Backend/test/fixtures/voice/inspection-scales.json",
|
||||
);
|
||||
|
||||
/** Which stored end is the good one - a fact about beekeeping, not derivable from the words. */
|
||||
const GOOD_STORED_END: Record<InspectionScaleField, 1 | 5> = {
|
||||
broodPattern: 5,
|
||||
swarmMood: 1,
|
||||
gentleness: 5,
|
||||
combBehavior: 5,
|
||||
};
|
||||
|
||||
// The frontend image builds without the backend folder: then there is nothing to compare.
|
||||
describe.skipIf(!existsSync(FIXTURE_PATH))("inspection scales", () => {
|
||||
const fixture = JSON.parse(readFileSync(FIXTURE_PATH, "utf8")) as ScaleFixture[];
|
||||
|
||||
it("scale directions equal the shared fixture", () => {
|
||||
expect(Object.keys(INSPECTION_SCALE_DIRECTIONS).sort()).toEqual(
|
||||
fixture.map((scale) => scale.field).sort(),
|
||||
);
|
||||
for (const scale of fixture) {
|
||||
const field = scale.field as InspectionScaleField;
|
||||
expect(INSPECTION_SCALE_LABELS[field], field).toEqual({
|
||||
lowLabel: scale.storedOne,
|
||||
highLabel: scale.storedFive,
|
||||
});
|
||||
expect(scale.anchors[scale.storedOne], field).toBe(1);
|
||||
expect(scale.anchors[scale.storedFive], field).toBe(5);
|
||||
expect(INSPECTION_SCALE_DIRECTIONS[field], field).toBe(
|
||||
GOOD_STORED_END[field] === 5 ? "higher-is-better" : "lower-is-better",
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("inspection scales (without the fixture)", () => {
|
||||
it("mirrors only the swarm mood, whose stored 1 is the calm end", () => {
|
||||
const mirrored = Object.entries(INSPECTION_SCALE_DIRECTIONS)
|
||||
.filter(([, direction]) => direction === "lower-is-better")
|
||||
.map(([field]) => field);
|
||||
expect(mirrored).toEqual(["swarmMood"]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user