L16 Lotse-Chat für Monteure: Tests für Chat-Schleife und Bestätigen, Isolation und Smoke
Fake-Provider mit geskripteten Tool-Aufrufen: Zuordnung, Vorschlag ohne Schreibzugriff, Ausführung über Services, Blocker, Einmaligkeit/Ablauf/fremder Nutzer, Scope, Mandantentrennung, Datenminimierung, Einstellung aus, Budget- und Rundengrenze. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
// Shared fixture of the lane L16 tests (scripts/test-lotse-chat-*.ts): two zz test tenants with
|
||||
// technicians (assigned / not assigned), admin, tenant B, customers with contact data (for the data
|
||||
// minimisation checks) and work orders in different statuses. Not a test itself.
|
||||
|
||||
import { prisma, dbForTenant } from "../../src/server/db";
|
||||
import { ROLE_DEFS, type RoleKey } from "../../src/server/rbac";
|
||||
import { ServiceError, type ServiceCtx } from "../../src/server/services/context";
|
||||
|
||||
export let failures = 0;
|
||||
export const ok = (cond: boolean, msg: string) => {
|
||||
console.log(`${cond ? "✓" : "✗ FEHLER"} ${msg}`);
|
||||
if (!cond) failures++;
|
||||
};
|
||||
|
||||
export async function expectErr(fn: () => Promise<unknown>, code: ServiceError["code"], msg: string, reason?: string) {
|
||||
try {
|
||||
await fn();
|
||||
ok(false, `${msg} — kein Fehler (erwartet ${code}${reason ? `/${reason}` : ""})`);
|
||||
} catch (err) {
|
||||
if (!(err instanceof ServiceError)) return ok(false, `${msg} — ${(err as Error).message}`);
|
||||
const r = (err.details as { reason?: string } | undefined)?.reason;
|
||||
ok(err.code === code && (!reason || r === reason), `${msg} (${err.code}${r ? `/${r}` : ""})`);
|
||||
}
|
||||
}
|
||||
|
||||
export const ctxOf = (tenantId: string, userId: string, role: RoleKey): ServiceCtx => ({
|
||||
db: dbForTenant(tenantId),
|
||||
tenantId,
|
||||
userId,
|
||||
permissions: new Set<string>(ROLE_DEFS[role].permissions),
|
||||
});
|
||||
|
||||
export async function cleanupChatTenants(prefix: string) {
|
||||
const slugs = [`zz-${prefix}-a`, `zz-${prefix}-b`];
|
||||
const tenants = await prisma.tenant.findMany({ where: { slug: { in: slugs } }, select: { id: true } });
|
||||
const ids = tenants.map((t) => t.id);
|
||||
if (ids.length) {
|
||||
const w = { where: { tenantId: { in: ids } } };
|
||||
await prisma.lotseActionProposal.deleteMany(w);
|
||||
await prisma.lotseMessage.deleteMany(w);
|
||||
await prisma.lotseConversation.deleteMany(w);
|
||||
await prisma.notification.deleteMany(w);
|
||||
await prisma.mailLog.deleteMany(w);
|
||||
await prisma.aiGeneration.deleteMany(w);
|
||||
await prisma.signature.deleteMany(w);
|
||||
await prisma.report.deleteMany(w);
|
||||
await prisma.activityNote.deleteMany(w);
|
||||
await prisma.voiceNote.deleteMany(w);
|
||||
await prisma.materialUsage.deleteMany(w);
|
||||
await prisma.materialPlan.deleteMany(w);
|
||||
await prisma.timeEntry.deleteMany(w);
|
||||
await prisma.workSession.deleteMany(w);
|
||||
await prisma.photo.deleteMany(w);
|
||||
await prisma.checklistItem.deleteMany(w);
|
||||
await prisma.photoRequirement.deleteMany(w);
|
||||
await prisma.workOrderStatusChange.deleteMany(w);
|
||||
await prisma.workOrderAssignee.deleteMany(w);
|
||||
await prisma.syncOperation.deleteMany(w);
|
||||
await prisma.document.deleteMany(w);
|
||||
await prisma.workOrder.deleteMany(w);
|
||||
await prisma.site.deleteMany(w);
|
||||
await prisma.contact.deleteMany(w);
|
||||
await prisma.customer.deleteMany(w);
|
||||
await prisma.numberSequence.deleteMany(w);
|
||||
await prisma.auditLog.deleteMany(w);
|
||||
await prisma.tenantModule.deleteMany(w);
|
||||
await prisma.tenantSettings.deleteMany(w);
|
||||
await prisma.user.deleteMany(w);
|
||||
await prisma.tenant.deleteMany({ where: { id: { in: ids } } });
|
||||
}
|
||||
await prisma.identity.deleteMany({ where: { email: { endsWith: `@zz-${prefix}.test` } } });
|
||||
}
|
||||
|
||||
export async function createChatFixture(prefix: string) {
|
||||
await cleanupChatTenants(prefix);
|
||||
const domain = `@zz-${prefix}.test`;
|
||||
const tA = await prisma.tenant.create({ data: { name: `Lotse-Chat ${prefix} A`, slug: `zz-${prefix}-a` } });
|
||||
const tB = await prisma.tenant.create({ data: { name: `Lotse-Chat ${prefix} B`, slug: `zz-${prefix}-b` } });
|
||||
await prisma.tenantSettings.create({ data: { tenantId: tA.id, orgName: "Chat Test A GmbH", phone: "040 1234567", email: "buero@lotsechat-a.test", address: "Werftstraße 5, 20457 Hamburg" } });
|
||||
await prisma.tenantSettings.create({ data: { tenantId: tB.id, orgName: "Chat Test B" } });
|
||||
|
||||
const mkUser = async (tenantId: string, key: string, name: string) => {
|
||||
const identity = await prisma.identity.create({ data: { email: `${key}${domain}`, passwordHash: "x" } });
|
||||
return prisma.user.create({ data: { tenantId, identityId: identity.id, email: identity.email, name } });
|
||||
};
|
||||
const tech = await mkUser(tA.id, "tech", "Max Monteur");
|
||||
const tech2 = await mkUser(tA.id, "tech2", "Nora Nordmann");
|
||||
const outsider = await mkUser(tA.id, "outsider", "Otto Fremd");
|
||||
const admin = await mkUser(tA.id, "admin", "Anna Admin");
|
||||
const techB = await mkUser(tB.id, "techb", "Tom Bader");
|
||||
|
||||
const now = Date.now();
|
||||
const today = { plannedStart: new Date(now - 3_600_000), plannedEnd: new Date(now + 3 * 3_600_000) };
|
||||
const tomorrow = { plannedStart: new Date(now + 26 * 3_600_000), plannedEnd: new Date(now + 30 * 3_600_000) };
|
||||
|
||||
const customer = await prisma.customer.create({
|
||||
data: { tenantId: tA.id, firstName: "Erika", lastName: "Mustermann", phone: "0171 9876543", email: "erika@kunde.test", street: "Lindenallee", houseNumber: "7", postalCode: "22111", city: "Hamburg" },
|
||||
});
|
||||
const contact = await prisma.contact.create({ data: { tenantId: tA.id, customerId: customer.id, name: "Klaus Kontakt", phone: "040 555666", email: "klaus@kunde.test" } });
|
||||
const site = await prisma.site.create({
|
||||
data: { tenantId: tA.id, customerId: customer.id, name: "Haus Mustermann", street: "Lindenallee", houseNumber: "7", postalCode: "22111", city: "Hamburg", accessNotes: "Schlüsselkasten Code 4711, Hausmeister 0170 1112223" },
|
||||
});
|
||||
const company = await prisma.customer.create({ data: { tenantId: tA.id, companyName: "Müller Haustechnik GmbH", city: "Buxtehude" } });
|
||||
const site2 = await prisma.site.create({ data: { tenantId: tA.id, customerId: company.id, name: "Objekt Müller", city: "Buxtehude" } });
|
||||
|
||||
const order = async (number: string, data: Record<string, unknown>, assignees: string[]) => {
|
||||
const wo = await prisma.workOrder.create({ data: { tenantId: tA.id, number, ...(data as object) } as never });
|
||||
for (const userId of assignees) await prisma.workOrderAssignee.create({ data: { tenantId: tA.id, workOrderId: wo.id, userId } });
|
||||
return wo as unknown as { id: string; number: string; status: string };
|
||||
};
|
||||
const wo1 = await order(
|
||||
"A-LC-1",
|
||||
{ customerId: customer.id, siteId: site.id, contactId: contact.id, title: "Speicher tauschen", description: "Kundin unter 0171 9876543 oder erika@kunde.test erreichbar, Lindenallee 7.", status: "in_progress", signatureRequired: false, ...today },
|
||||
[tech.id, tech2.id],
|
||||
);
|
||||
const plan = await prisma.materialPlan.create({ data: { tenantId: tA.id, workOrderId: wo1.id, name: "Filter", plannedQuantity: 1, unit: "Stk" } });
|
||||
const wo2 = await order("A-LC-2", { customerId: company.id, siteId: site2.id, title: "Wartung Heizung", status: "assigned", signatureRequired: false, ...today }, [tech.id, tech2.id]);
|
||||
const wo3 = await order("A-LC-3", { customerId: company.id, siteId: site2.id, title: "Pumpe montieren", status: "in_progress", signatureRequired: false, ...tomorrow }, [tech.id]);
|
||||
await prisma.photoRequirement.create({ data: { tenantId: tA.id, workOrderId: wo3.id, key: "fertige_montage", label: "Fertige Montage" } });
|
||||
const wo4 = await order("A-LC-4", { customerId: company.id, siteId: site2.id, title: "Regler einstellen", status: "in_progress", signatureRequired: false, ...tomorrow }, [tech.id]);
|
||||
|
||||
const customerB = await prisma.customer.create({ data: { tenantId: tB.id, companyName: "Kunde B" } });
|
||||
const woB = await prisma.workOrder.create({ data: { tenantId: tB.id, number: "B-LC-1", customerId: customerB.id, title: "Auftrag B", status: "in_progress", ...today } });
|
||||
await prisma.workOrderAssignee.create({ data: { tenantId: tB.id, workOrderId: woB.id, userId: techB.id } });
|
||||
|
||||
return {
|
||||
tA,
|
||||
tB,
|
||||
users: { tech, tech2, outsider, admin, techB },
|
||||
ctx: {
|
||||
tech: ctxOf(tA.id, tech.id, "technician"),
|
||||
tech2: ctxOf(tA.id, tech2.id, "technician"),
|
||||
outsider: ctxOf(tA.id, outsider.id, "technician"),
|
||||
admin: ctxOf(tA.id, admin.id, "tenant-admin"),
|
||||
techB: ctxOf(tB.id, techB.id, "technician"),
|
||||
},
|
||||
customer,
|
||||
contact,
|
||||
site,
|
||||
plan,
|
||||
orders: { wo1, wo2, wo3, wo4, woB },
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user