93 lines
4.8 KiB
TypeScript
93 lines
4.8 KiB
TypeScript
import assert from "node:assert/strict"
|
|
import test from "node:test"
|
|
import {
|
|
buildSuggestionMail,
|
|
buildEntitySuggestionRequest,
|
|
dismissEntitySuggestion,
|
|
selectCandidates,
|
|
shouldRetryWithLegacyAiModel,
|
|
validateSuggestions,
|
|
type EntityCandidate,
|
|
} from "../src/modules/email/email.entity-suggestions"
|
|
|
|
const customer: EntityCandidate = { entityType: "customers", entityId: 1, entityName: "Muster GmbH", entityTypeLabel: "Kunde", email: "kontakt@muster.de" }
|
|
const project: EntityCandidate = { entityType: "projects", entityId: 1, entityName: "Umbau", entityTypeLabel: "Projekt", number: "P-2026-123" }
|
|
const suggestion = { entityType: "customers", entityId: 1, confidence: "high", reason: "Absender stimmt überein." }
|
|
|
|
test("rejects invented IDs and types outside the supplied tenant candidates, and removes duplicates", () => {
|
|
assert.deepEqual(validateSuggestions({ suggestions: [suggestion, suggestion,
|
|
{ ...suggestion, entityId: 99 }, { ...suggestion, entityType: "vendors" },
|
|
] }, [customer]), [suggestion])
|
|
})
|
|
|
|
test("keeps distinct entity types with the same numeric ID", () => {
|
|
assert.equal(validateSuggestions({ suggestions: [suggestion, { ...suggestion, entityType: "projects" }] }, [customer, project]).length, 2)
|
|
})
|
|
|
|
test("empty analysis is valid; malformed confidence and empty evidence are not suggestions", () => {
|
|
assert.deepEqual(validateSuggestions({ suggestions: [] }, [customer]), [])
|
|
assert.deepEqual(validateSuggestions({ suggestions: [{ ...suggestion, reason: " " }] }, [customer]), [])
|
|
assert.throws(() => validateSuggestions({ suggestions: [{ ...suggestion, confidence: "low" }] }, [customer]))
|
|
assert.throws(() => validateSuggestions({ suggestions: "bad response" }, [customer]))
|
|
})
|
|
|
|
test("prioritizes matching addresses and project references even in large tenants", () => {
|
|
const unrelated = Array.from({ length: 200 }, (_, id) => ({ ...customer, entityId: id + 10, email: null, entityName: `Unrelated ${id}` }))
|
|
const result = selectCandidates([...unrelated, project, customer], JSON.stringify({ from: [{ address: "kontakt@muster.de" }], subject: "Anfrage P-2026-123" }))
|
|
assert.equal(result.length, 150)
|
|
assert.equal(result[0], customer)
|
|
assert.equal(result[1], project)
|
|
})
|
|
|
|
test("does not treat a partial address as an exact match", () => {
|
|
const exact = { ...customer, entityId: 2, email: "abc-kontakt@muster.de" }
|
|
assert.equal(selectCandidates([customer, exact], "abc-kontakt@muster.de")[0], exact)
|
|
})
|
|
|
|
|
|
test("recipient and CC headers neither reach the model nor affect candidate ranking", () => {
|
|
const ownCustomer = { ...customer, entityId: 999, entityName: "Mailbox Owner", email: "owner@example.org" }
|
|
const message = { subject: "Anfrage P-2026-123", from: [{ address: "kontakt@muster.de" }], body: { text: "Bitte Angebot erstellen." } }
|
|
const withoutRecipients = buildSuggestionMail(message)
|
|
const withRecipients = buildSuggestionMail({ ...message,
|
|
to: [{ name: ownCustomer.entityName, address: ownCustomer.email }],
|
|
cc: [{ name: "Muster GmbH", address: "cc@example.org" }],
|
|
})
|
|
assert.deepEqual(withRecipients, withoutRecipients)
|
|
assert.equal("to" in withRecipients, false)
|
|
assert.equal("cc" in withRecipients, false)
|
|
const candidates = [ownCustomer, customer, project]
|
|
const ranked = selectCandidates(candidates, JSON.stringify(withRecipients))
|
|
assert.deepEqual(ranked, selectCandidates(candidates, JSON.stringify(withoutRecipients)))
|
|
assert.equal(ranked[0], customer)
|
|
assert.equal(ranked[1], project)
|
|
})
|
|
|
|
test("dismisses only the selected entity suggestion", () => {
|
|
const projectSuggestion = { ...suggestion, entityType: "projects" as const, reason: "Projektnummer stimmt überein." }
|
|
assert.deepEqual(
|
|
dismissEntitySuggestion([suggestion, projectSuggestion], "customers", 1),
|
|
[projectSuggestion],
|
|
)
|
|
assert.deepEqual(dismissEntitySuggestion(null, "customers", 1), [])
|
|
})
|
|
|
|
test("uses Luna by default and omits reasoning effort for the legacy fallback", () => {
|
|
const request = buildEntitySuggestionRequest(buildSuggestionMail({ subject: "Test" }), [customer])
|
|
assert.equal(request.model, "gpt-5.6-luna")
|
|
assert.equal(request.reasoning_effort, "none")
|
|
|
|
const fallback = buildEntitySuggestionRequest(buildSuggestionMail({ subject: "Test" }), [customer], "gpt-4o-mini")
|
|
assert.equal(fallback.model, "gpt-4o-mini")
|
|
assert.equal("reasoning_effort" in fallback, false)
|
|
})
|
|
|
|
test("retries model compatibility errors without retrying auth, rate-limit, or configuration errors", () => {
|
|
for (const status of [400, 404, 422, 500, 502]) {
|
|
assert.equal(shouldRetryWithLegacyAiModel({ status }), true)
|
|
}
|
|
for (const status of [401, 403, 429, 503]) {
|
|
assert.equal(shouldRetryWithLegacyAiModel({ status }), false)
|
|
}
|
|
})
|