Files
ApiCore/ApiCore-Frontend/src/lib/offline/db.spec.ts
T
Nick SzaunigandClaude Opus 5.5 e614ded967
Backend Image / build-and-push (push) Successful in 7m6s
Frontend Image / build-and-push (push) Successful in 15m27s
Manifests / validate (push) Successful in 16s
feat: Sprachnotizen in Synchronisierung, Warteschlange und Abmelden (T7 L5 Task 18)
Wartende Sprachnotizen werden nach Einträgen und Fotos hochgeladen, je
Notiz ein fester Idempotency-Key; endgültige 4xx markieren die Notiz
mit festem deutschem Text, 401/408/429/5xx/Netz warten auf den nächsten
Lauf, „wird bearbeitet“ wird übersprungen. Warteschlangen-Sheet mit
Abschnitt „Sprachnotizen“ (Verwerfen je Notiz nur für den Besitzer,
alle verwerfen mit Rückfrage, Hinweis bei zu vielen offenen Notizen),
Offline-Band zählt nur wartende Notizen, Abmelden fragt bei wartenden
Notizen nach. Zählen liest keine Audiodaten.

Gates: Frontend lint, typecheck, 2886 Tests.

Refs #27

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-16 11:10:14 +02:00

696 lines
25 KiB
TypeScript

import "fake-indexeddb/auto";
import { IDBFactory } from "fake-indexeddb";
import { openDB } from "idb";
import { beforeEach, describe, expect, it, vi } from "vitest";
type OfflineDbModule = typeof import("./db");
const DB_NAME = "apicore-offline";
const VERSION_2_STORES = [
"pending-inspections",
"pending-treatments",
"pending-feedings",
"pending-varroa-measurements",
"pending-swarm-control-checks",
"pending-harvests",
];
const INSPECTION = { colonyId: "colony-1", date: "2026-10-01T00:00:00.000Z" };
const TREATMENT = { colonyId: "colony-1", date: "2026-10-01T00:00:00.000Z", product: "Oxuvar" };
const FEEDING = {
colonyId: "colony-1",
date: "2026-10-01T00:00:00.000Z",
feedType: "FEED_SYRUP" as const,
amount: 2,
unit: "kg",
};
const REJECTION = {
status: 400,
message: "Volk V-03 ist beendet.",
at: "2026-10-13T09:00:00.000Z",
};
function photo(id: string, inspectionIdempotencyKey: string, ownerUserId = "user-1") {
return {
id,
sessionId: "session-1",
ownerUserId,
blob: new Blob(["jpeg"]),
fileName: `${id}.jpg`,
createdAt: "2026-10-13T08:00:00.000Z",
state: "waiting-for-inspection",
inspectionIdempotencyKey,
uploadIdempotencyKey: `upload-${id}`,
};
}
/** A fresh module, so nothing is cached from the previous test's database. */
async function loadDb(): Promise<OfflineDbModule> {
vi.resetModules();
return import("./db");
}
/** The queue exactly as the previous release (DB version 2) left it on a phone. */
async function createVersion2Queue(entry: object): Promise<void> {
const db = await openDB(DB_NAME, 2, {
upgrade(database) {
for (const store of VERSION_2_STORES) {
database.createObjectStore(store, { keyPath: "id", autoIncrement: true });
}
},
});
await db.add("pending-inspections", entry);
db.close();
}
/** A device that last opened the app with DB v3 (before plant reports). */
async function createVersion3Queue(inspection: object): Promise<void> {
const db = await openDB(DB_NAME, 3, {
upgrade(database) {
for (const store of [...VERSION_2_STORES, "pending-bulk"]) {
database.createObjectStore(store, {
keyPath: "id",
autoIncrement: true,
});
}
const photos = database.createObjectStore("pending-photos", {
keyPath: "id",
});
photos.createIndex("sessionId", "sessionId");
photos.createIndex("inspectionIdempotencyKey", "inspectionIdempotencyKey");
photos.createIndex("ownerUserId", "ownerUserId");
database.createObjectStore("offline-packs", { keyPath: "apiaryId" });
database.createObjectStore("meta");
},
});
await db.add("pending-inspections", inspection);
db.close();
}
describe("offline queue database", () => {
beforeEach(() => {
globalThis.indexedDB = new IDBFactory();
});
it.each([
["a fresh install (0)", 0],
["version 1", 1],
])("opening from %s creates the plant report store at version 6", async (_name, from) => {
if (from === 1) {
const old = await openDB(DB_NAME, 1, {
upgrade(database) {
database.createObjectStore("pending-inspections", {
keyPath: "id",
autoIncrement: true,
});
},
});
await old.add("pending-inspections", {
input: INSPECTION,
queuedAt: "2026-10-01T08:00:00.000Z",
});
old.close();
}
const db = await loadDb();
await db.queuePhenologyReport(
{ phase: "APFEL", date: "2026-04-20" },
"plant-key-0000000003",
"user-1",
);
const upgraded = await openDB(DB_NAME);
expect(upgraded.version).toBe(6);
expect([...upgraded.objectStoreNames]).toContain("pending-phenology-reports");
expect(await upgraded.count("pending-phenology-reports")).toBe(1);
expect(await upgraded.count("pending-inspections")).toBe(from === 1 ? 1 : 0);
upgraded.close();
});
it("db upgrade 3 -> 4 keeps queued inspections and adds the plant report queue", async () => {
await createVersion3Queue({
input: INSPECTION,
idempotencyKey: "key-from-v3",
ownerUserId: "user-1",
queuedAt: "2026-10-01T08:00:00.000Z",
});
const db = await loadDb();
const entries = await db.getPendingEntries("pending-inspections");
expect(entries).toEqual([
{
id: 1,
input: INSPECTION,
idempotencyKey: "key-from-v3",
ownerUserId: "user-1",
queuedAt: "2026-10-01T08:00:00.000Z",
},
]);
await db.queuePhenologyReport(
{ phase: "SALWEIDE", date: "2026-03-20", apiaryId: null },
"key-plant-0000000001",
"user-1",
);
expect(await db.getPendingEntries("pending-phenology-reports")).toEqual([
expect.objectContaining({
input: { phase: "SALWEIDE", date: "2026-03-20", apiaryId: null },
idempotencyKey: "key-plant-0000000001",
ownerUserId: "user-1",
}),
]);
expect(await db.summarizeQueue("user-1")).toEqual({
own: 2,
failed: 0,
foreign: 0,
});
expect((await db.listQueue("user-1")).map((item) => item.label)).toContain("Pflanzenmeldung");
const upgraded = await openDB(DB_NAME);
expect(upgraded.version).toBe(6);
upgraded.close();
});
it("upgrading v2 → v6 keeps already queued inspections", async () => {
await createVersion2Queue({
input: INSPECTION,
idempotencyKey: "key-from-v2",
queuedAt: "2026-10-01T08:00:00.000Z",
});
const db = await loadDb();
const entries = await db.getPendingEntries("pending-inspections");
expect(entries).toEqual([
{
id: 1,
input: INSPECTION,
idempotencyKey: "key-from-v2",
queuedAt: "2026-10-01T08:00:00.000Z",
},
]);
// Entries from before owners existed still count as the user's own.
expect(await db.summarizeQueue("user-1")).toEqual({ own: 1, failed: 0, foreign: 0 });
const upgraded = await openDB(DB_NAME);
expect(upgraded.version).toBe(6);
expect([...upgraded.objectStoreNames]).toEqual(
expect.arrayContaining([
...VERSION_2_STORES,
"pending-bulk",
"pending-photos",
"offline-packs",
"meta",
"pending-phenology-reports",
"pending-hive-changes",
]),
);
expect([...upgraded.transaction("pending-photos").store.indexNames].sort()).toEqual([
"inspectionIdempotencyKey",
"ownerUserId",
"sessionId",
]);
upgraded.close();
});
it("db upgrade 4 -> 5 keeps every store and entry and adds the hive change queue", async () => {
const old = await openDB(DB_NAME, 4, {
upgrade(database) {
for (const store of [...VERSION_2_STORES, "pending-bulk", "pending-phenology-reports"]) {
database.createObjectStore(store, { keyPath: "id", autoIncrement: true });
}
database.createObjectStore("pending-photos", { keyPath: "id" });
database.createObjectStore("offline-packs", { keyPath: "apiaryId" });
database.createObjectStore("meta");
},
});
await old.add("pending-inspections", { input: INSPECTION, queuedAt: "2026-10-01T08:00:00.000Z" });
await old.add("pending-phenology-reports", { input: {}, queuedAt: "2026-10-01T09:00:00.000Z" });
old.close();
const db = await loadDb();
await db.queueHiveChange(
{ colonyId: "colony-1", occurredOn: "2026-10-15", source: "CARD", ops: [] },
"key-h",
"user-1",
);
const [hive] = await db.getPendingEntries("pending-hive-changes");
expect(hive).toMatchObject({ idempotencyKey: "key-h", ownerUserId: "user-1" });
const upgraded = await openDB(DB_NAME);
expect(upgraded.version).toBe(6);
expect(await upgraded.count("pending-inspections")).toBe(1);
expect(await upgraded.count("pending-phenology-reports")).toBe(1);
expect([...upgraded.objectStoreNames]).toEqual(
expect.arrayContaining(["pending-bulk", "pending-photos", "offline-packs", "meta", "pending-hive-changes"]),
);
upgraded.close();
});
it("upgrading to the new version keeps queued inspections, photos and phenology reports", async () => {
const old = await openDB(DB_NAME, 5, {
upgrade(database) {
for (const store of [
...VERSION_2_STORES,
"pending-bulk",
"pending-phenology-reports",
"pending-hive-changes",
]) {
database.createObjectStore(store, { keyPath: "id", autoIncrement: true });
}
const photos = database.createObjectStore("pending-photos", { keyPath: "id" });
photos.createIndex("sessionId", "sessionId");
photos.createIndex("inspectionIdempotencyKey", "inspectionIdempotencyKey");
photos.createIndex("ownerUserId", "ownerUserId");
database.createObjectStore("offline-packs", { keyPath: "apiaryId" });
database.createObjectStore("meta");
},
});
await old.add("pending-inspections", { input: INSPECTION, ownerUserId: "user-1", queuedAt: "2026-10-01T08:00:00.000Z" });
await old.add("pending-phenology-reports", { input: {}, ownerUserId: "user-1", queuedAt: "2026-10-01T09:00:00.000Z" });
await old.add("pending-hive-changes", { input: {}, ownerUserId: "user-1", queuedAt: "2026-10-01T10:00:00.000Z" });
await old.put("pending-photos", photo("p1", "key-1"));
old.close();
const db = await loadDb();
expect(await db.getPendingEntries("pending-inspections")).toHaveLength(1);
const upgraded = await openDB(DB_NAME);
expect(upgraded.version).toBe(6);
expect(await upgraded.count("pending-phenology-reports")).toBe(1);
expect(await upgraded.count("pending-hive-changes")).toBe(1);
expect(await upgraded.count("pending-photos")).toBe(1);
expect([...upgraded.objectStoreNames]).toContain("pending-voice");
expect([...upgraded.transaction("pending-voice").store.indexNames]).toEqual(["ownerUserId"]);
upgraded.close();
});
it("clearQueueOf removes one owner's voice notes and keeps everyone else's", async () => {
const db = await loadDb();
await db.getDb();
const raw = await openDB(DB_NAME);
const note = (id: string, ownerUserId: string) => ({
id,
ownerUserId,
blob: new Blob(["x"]),
mime: "audio/webm;codecs=opus",
durationSec: 5,
colonyId: "c-1",
inspectionDate: "2026-10-16",
createdAt: "2026-10-16T08:00:00.000Z",
uploadIdempotencyKey: `u-${id}`,
state: "recorded",
});
await raw.put("pending-voice", note("v1", "user-1") as never);
await raw.put("pending-voice", note("v2", "user-2") as never);
await db.clearQueueOf("user-1");
expect(await raw.getAllKeys("pending-voice")).toEqual(["v2"]);
raw.close();
});
it("lists a hive change under its own name and marks inspections that carry one", async () => {
const db = await loadDb();
await db.queueHiveChange(
{ colonyId: "colony-1", occurredOn: "2026-10-15", source: "CARD", ops: [] },
"key-h",
"user-1",
);
await db.queueInspection({ ...INSPECTION, hiveChange: { ops: [] } }, "key-i", "user-1");
await db.queueInspection(INSPECTION, "key-j", "user-1");
const items = await db.listQueue("user-1");
expect(items.map((item) => [item.label, item.hasHiveChange ?? false]).sort()).toEqual([
["Beute-Änderung", true],
["Durchsicht", false],
["Durchsicht", true],
]);
});
it("retryInspectionWithoutHiveChange strips the hive change, clears the rejection and keeps the key", async () => {
const db = await loadDb();
await db.queueInspection({ ...INSPECTION, hiveChange: { ops: [] } }, "key-i", "user-1");
const [entry] = await db.getPendingEntries("pending-inspections");
await db.markFailed("pending-inspections", entry.id, { ...REJECTION, status: 403 });
expect((await db.listQueue("user-1"))[0].canResendWithoutHiveChange).toBe(true);
await db.retryInspectionWithoutHiveChange(entry.id);
const [after] = await db.getPendingEntries("pending-inspections");
expect(after.input).toEqual(INSPECTION);
expect(after.lastError).toBeUndefined();
expect(after.idempotencyKey).toBe("key-i");
});
it("offers „ohne Beute-Änderung“ only for a 403 or a hive conflict, not for any other rejection", async () => {
const db = await loadDb();
await db.queueInspection({ ...INSPECTION, hiveChange: { ops: [] } }, "k1", "user-1");
await db.queueInspection({ ...INSPECTION, hiveChange: { ops: [] } }, "k2", "user-1");
const [plain, conflict] = await db.getPendingEntries("pending-inspections");
await db.markFailed("pending-inspections", plain.id, REJECTION);
await db.markFailed("pending-inspections", conflict.id, {
status: 409,
message: "Konflikt",
details: { code: "HIVE_CONFLICT" },
at: REJECTION.at,
});
const items = await db.listQueue("user-1");
expect(items.map((item) => item.canResendWithoutHiveChange ?? false)).toEqual([false, true]);
});
it("summarizeQueue separates own, failed and foreign entries", async () => {
const db = await loadDb();
await db.queueInspection(INSPECTION, "key-1", "user-1");
await db.queueTreatment(TREATMENT, "key-2", "user-1");
await db.queueFeeding(FEEDING, "key-3", "user-2");
const [treatment] = await db.getPendingEntries("pending-treatments");
await db.markFailed("pending-treatments", treatment.id, REJECTION);
expect(await db.summarizeQueue("user-1")).toEqual({ own: 1, failed: 1, foreign: 1 });
expect(await db.summarizeQueue("user-2")).toEqual({ own: 1, failed: 0, foreign: 2 });
});
it("summarizeQueue counts a photo waiting on its own like an entry, never a draft or one riding with its inspection", async () => {
const db = await loadDb();
await db.queueInspection(INSPECTION, "inspection-key", "user-1");
const raw = await openDB(DB_NAME);
const ready = { ...photo("ready", "inspection-key"), state: "ready", inspectionId: "srv-1" };
await raw.put("pending-photos", ready);
await raw.put("pending-photos", { ...ready, id: "rejected", lastError: REJECTION });
await raw.put("pending-photos", { ...ready, id: "foreign", ownerUserId: "user-2" });
await raw.put("pending-photos", photo("rides-along", "inspection-key"));
await raw.put("pending-photos", { ...photo("draft", "unused"), state: "draft" });
raw.close();
// The inspection itself, plus the ready photo; the rejected one separately.
expect(await db.summarizeQueue("user-1")).toEqual({ own: 2, failed: 1, foreign: 1 });
});
it("toQueueError keeps the server's status, message and body", async () => {
const db = await loadDb();
const { ApiError } = await import("@/lib/auth/api-client");
const body = { statusCode: 413, message: "Datei zu groß" };
const error = db.toQueueError(new ApiError(413, "Datei zu groß", body));
expect(error).toEqual({ status: 413, message: "Datei zu groß", details: body, at: expect.any(String) });
});
it("stores each entry with its key and owner", async () => {
const db = await loadDb();
await db.queueVarroaMeasurement(
{ colonyId: "colony-1", date: "2026-10-01T00:00:00.000Z", method: "WASHING_METHOD" as const },
"key-1",
"user-1",
);
const [entry] = await db.getPendingEntries("pending-varroa-measurements");
expect(entry).toMatchObject({ idempotencyKey: "key-1", ownerUserId: "user-1" });
expect(Date.parse(entry.queuedAt)).not.toBeNaN();
});
it("lists only the current user's entries, oldest first, with the server's message", async () => {
const db = await loadDb();
await db.queueInspection(INSPECTION, "key-1", "user-1");
await db.queueFeeding(FEEDING, "key-2", "user-2");
await db.queueTreatment(TREATMENT, "key-3", "user-1");
const [treatment] = await db.getPendingEntries("pending-treatments");
await db.markFailed("pending-treatments", treatment.id, REJECTION);
const items = await db.listQueue("user-1");
expect(items).toEqual([
expect.objectContaining({ store: "pending-inspections", label: "Durchsicht" }),
expect.objectContaining({
store: "pending-treatments",
id: treatment.id,
label: "Behandlung",
lastError: REJECTION,
}),
]);
});
it("discardQueueItem deletes exactly that entry", async () => {
const db = await loadDb();
await db.queueInspection(INSPECTION, "key-1", "user-1");
await db.queueInspection(INSPECTION, "key-2", "user-1");
const [first] = await db.listQueue("user-1");
await db.discardQueueItem(first);
const remaining = await db.getPendingEntries("pending-inspections");
expect(remaining.map((entry) => entry.idempotencyKey)).toEqual(["key-2"]);
});
it("discarding a queued inspection discards its photos and no others", async () => {
const db = await loadDb();
await db.queueInspection(INSPECTION, "inspection-key", "user-1");
const raw = await openDB(DB_NAME);
await raw.put("pending-photos", photo("p1", "inspection-key"));
await raw.put("pending-photos", photo("p2", "other-inspection-key"));
const [item] = await db.listQueue("user-1");
expect(item.photoCount).toBe(1);
await db.discardQueueItem(item);
expect(await raw.getAllKeys("pending-photos")).toEqual(["p2"]);
expect(await db.getPendingEntries("pending-inspections")).toEqual([]);
raw.close();
});
it("clearQueueOf removes one owner's entries and photos and keeps everyone else's", async () => {
const db = await loadDb();
await db.queueInspection(INSPECTION, "key-1", "user-1");
await db.queueFeeding(FEEDING, "key-2", "user-1");
await db.queueInspection(INSPECTION, "key-3", "user-2");
const raw = await openDB(DB_NAME);
await raw.put("pending-photos", photo("p1", "key-1", "user-1"));
await raw.put("pending-photos", photo("p3", "key-3", "user-2"));
await db.clearQueueOf("user-1");
expect(await db.summarizeQueue("user-1")).toEqual({ own: 0, failed: 0, foreign: 1 });
const [left] = await db.getPendingEntries("pending-inspections");
expect(left.idempotencyKey).toBe("key-3");
expect(await raw.getAllKeys("pending-photos")).toEqual(["p3"]);
raw.close();
});
});
describe("bulk entries in the offline queue", () => {
const BULK_TREATMENT = {
date: "2026-10-14T00:00:00.000Z",
product: "Oxuvar",
batchNumber: "Ch-123",
colonyIds: ["c-1", "c-2", "c-3"],
};
const BULK_VARROA = {
date: "2026-10-14T00:00:00.000Z",
method: "NATURAL_MITE_FALL",
entries: [
{ colonyId: "c-1", miteCount: 4 },
{ colonyId: "c-3", miteCount: 9 },
],
};
const INVALID_COLONIES = {
status: 400,
message: "Einige Völker sind nicht (mehr) erfassbar",
details: { invalidColonyIds: ["c-3"] },
at: "2026-10-14T09:00:00.000Z",
};
beforeEach(() => {
globalThis.indexedDB = new IDBFactory();
});
async function onlyBulkEntry() {
const raw = await openDB(DB_NAME);
const [entry] = await raw.getAll("pending-bulk");
raw.close();
return entry;
}
it("queueBulk stores exactly one entry with kind, input, key and owner", async () => {
const db = await loadDb();
await db.queueBulk("treatments", BULK_TREATMENT, "bulk-key-0000000001", "user-1");
expect(await onlyBulkEntry()).toEqual({
id: 1,
kind: "treatments",
input: BULK_TREATMENT,
idempotencyKey: "bulk-key-0000000001",
ownerUserId: "user-1",
queuedAt: expect.any(String),
});
expect(await db.summarizeQueue("user-1")).toEqual({ own: 1, failed: 0, foreign: 0 });
});
it("lists a bulk entry under its own name", async () => {
const db = await loadDb();
await db.queueBulk("varroa", BULK_VARROA, "bulk-key-0000000001", "user-1");
const [item] = await db.listQueue("user-1");
expect(item).toMatchObject({
store: "pending-bulk",
label: "Varroamessung für mehrere Völker",
});
expect(item.resendWithout).toBeUndefined();
});
it("names the rejected colonies by code from the loaded packs", async () => {
const db = await loadDb();
await db.queueBulk("treatments", BULK_TREATMENT, "bulk-key-0000000001", "user-1");
const raw = await openDB(DB_NAME);
await raw.put("offline-packs", {
apiaryId: "apiary-1",
ownerUserId: "user-1",
savedAt: "2026-10-14T08:00:00.000Z",
pack: {
apiary: { id: "apiary-1", name: "Waldrand" },
colonies: [
{ id: "c-1", code: "V-1", apiaryId: "apiary-1", status: "ACTIVE" },
{ id: "c-3", code: "V-3", apiaryId: "apiary-1", status: "DISSOLVED" },
],
},
});
raw.close();
const entry = await onlyBulkEntry();
await db.markFailed("pending-bulk", entry.id, INVALID_COLONIES);
const [item] = await db.listQueue("user-1");
expect(item.resendWithout).toEqual({ colonyIds: ["c-3"], codes: ["V-3"] });
});
it("offers no resend when every colony of the entry was rejected", async () => {
const db = await loadDb();
await db.queueBulk("treatments", { ...BULK_TREATMENT, colonyIds: ["c-3"] }, "bulk-key-0000000001", "user-1");
const entry = await onlyBulkEntry();
await db.markFailed("pending-bulk", entry.id, INVALID_COLONIES);
const [item] = await db.listQueue("user-1");
expect(item.resendWithout).toBeUndefined();
});
it("retryBulkWithout resends the remaining colonies with the same key and clears the rejection", async () => {
const db = await loadDb();
await db.queueBulk("treatments", BULK_TREATMENT, "bulk-key-0000000001", "user-1");
const entry = await onlyBulkEntry();
await db.markFailed("pending-bulk", entry.id, INVALID_COLONIES);
await db.retryBulkWithout(entry.id, ["c-3"]);
expect(await onlyBulkEntry()).toEqual({
...entry,
input: { ...BULK_TREATMENT, colonyIds: ["c-1", "c-2"] },
lastError: undefined,
});
expect((await onlyBulkEntry()).idempotencyKey).toBe("bulk-key-0000000001");
});
it("retryBulkWithout drops a varroa colony with its own count", async () => {
const db = await loadDb();
await db.queueBulk("varroa", BULK_VARROA, "bulk-key-0000000001", "user-1");
const entry = await onlyBulkEntry();
await db.retryBulkWithout(entry.id, ["c-3"]);
expect((await onlyBulkEntry()).input).toEqual({
...BULK_VARROA,
entries: [{ colonyId: "c-1", miteCount: 4 }],
});
});
it("retryBulkWithout removes an entry that would be left without colonies", async () => {
const db = await loadDb();
await db.queueBulk("treatments", { ...BULK_TREATMENT, colonyIds: ["c-3"] }, "bulk-key-0000000001", "user-1");
const entry = await onlyBulkEntry();
await db.retryBulkWithout(entry.id, ["c-3"]);
expect(await onlyBulkEntry()).toBeUndefined();
});
it("discardQueueItem deletes a bulk entry", async () => {
const db = await loadDb();
await db.queueBulk("feedings", BULK_TREATMENT, "bulk-key-0000000001", "user-1");
const [item] = await db.listQueue("user-1");
await db.discardQueueItem(item);
expect(await onlyBulkEntry()).toBeUndefined();
});
});
describe("voice notes in the offline queue", () => {
beforeEach(() => {
globalThis.indexedDB = new IDBFactory();
});
function note(id: string, ownerUserId: string, lastError?: typeof REJECTION) {
return {
id,
ownerUserId,
blob: new Blob(["audio"]),
mime: "audio/webm;codecs=opus",
durationSec: 30,
colonyId: "colony-1",
inspectionDate: "2026-10-16",
createdAt: "2026-10-16T08:00:00.000Z",
uploadIdempotencyKey: `upload-${id}`,
state: "recorded" as const,
...(lastError ? { lastError } : {}),
};
}
async function storeNotes() {
const db = await loadDb();
await db.queueInspection(INSPECTION, "key-1", "user-1");
const raw = await openDB(DB_NAME);
await raw.put("pending-voice", note("v1", "user-1"));
await raw.put("pending-voice", note("v2", "user-1", REJECTION));
await raw.put("pending-voice", note("v3", "user-2"));
raw.close();
return db;
}
it("summarizeQueue counts waiting, rejected and foreign voice notes", async () => {
const db = await storeNotes();
expect(await db.summarizeQueue("user-1")).toEqual({
own: 2,
failed: 1,
foreign: 1,
voice: 2,
voiceWaiting: 1,
});
expect(await db.summarizeQueue("user-2")).toEqual({
own: 1,
failed: 0,
foreign: 3,
voice: 1,
voiceWaiting: 1,
});
});
it("listQueue lists the user's voice notes and never another account's", async () => {
const db = await storeNotes();
const items = (await db.listQueue("user-1")).filter((item) => item.store === "pending-voice");
expect(items.map((item) => item.id)).toEqual(["v1", "v2"]);
expect(items[0]).toMatchObject({
label: "Sprachnotiz",
voice: { colonyId: "colony-1", inspectionDate: "2026-10-16", durationSec: 30 },
});
expect(items[1].lastError).toMatchObject({ status: 400 });
});
});