feat: Aufnahmeregeln fuer Integrationen als reine Logik
Was aus einem Fund wird, entscheidet fuer alle Integrationen dieselbe Stelle: anlegen, aktualisieren, schliessen oder nichts tun. Gefuehrt werden nur Titel, Notiz, Verweis und Faelligkeit. Bereich, Projekt, Prioritaet und geschaetzter Aufwand gehoeren der Planung — wer ein Ticket einsortiert und schaetzt, will nicht, dass der naechste Lauf das wieder wegraeumt. Dazu die Regeln des Spiegels: Aufbau der Sendung und ihre Frist. Ohne Frist liesse sich ein abgefangenes Paket beliebig oft erneut einspielen. 21 Tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7f8f505126
commit
e4d3547457
@@ -0,0 +1,188 @@
|
||||
import { describe, expect, test } from 'vitest'
|
||||
import {
|
||||
alsTitel,
|
||||
sendungFrisch,
|
||||
spiegelSendung,
|
||||
TITEL_MAX,
|
||||
uebernahmeEntscheiden,
|
||||
type Bestand,
|
||||
type Fund,
|
||||
} from '@/lib/integration'
|
||||
|
||||
function fund(teile: Partial<Fund> = {}): Fund {
|
||||
return { ref: 'T-1', titel: 'Drucker klemmt', ...teile }
|
||||
}
|
||||
|
||||
function bestand(teile: Partial<Bestand> = {}): Bestand {
|
||||
return {
|
||||
titel: 'Drucker klemmt',
|
||||
notiz: null,
|
||||
quelleUrl: null,
|
||||
faelligAm: null,
|
||||
prioritaet: 3,
|
||||
aufwandMinuten: null,
|
||||
erledigt: false,
|
||||
...teile,
|
||||
}
|
||||
}
|
||||
|
||||
describe('uebernahmeEntscheiden', () => {
|
||||
test('legt an, was es hier noch nicht gibt', () => {
|
||||
expect(uebernahmeEntscheiden(fund(), null)).toEqual({ tun: 'anlegen' })
|
||||
})
|
||||
|
||||
test('legt nichts an, was drüben schon erledigt ist', () => {
|
||||
expect(uebernahmeEntscheiden(fund({ erledigt: true }), null)).toEqual({
|
||||
tun: 'nichts',
|
||||
grund: 'schon-erledigt',
|
||||
})
|
||||
})
|
||||
|
||||
test('erkennt den unveränderten Fall', () => {
|
||||
expect(uebernahmeEntscheiden(fund(), bestand())).toEqual({
|
||||
tun: 'nichts',
|
||||
grund: 'unveraendert',
|
||||
})
|
||||
})
|
||||
|
||||
test('übernimmt einen geänderten Titel', () => {
|
||||
const entscheidung = uebernahmeEntscheiden(
|
||||
fund({ titel: 'Drucker klemmt weiterhin' }),
|
||||
bestand(),
|
||||
)
|
||||
expect(entscheidung).toEqual({
|
||||
tun: 'aktualisieren',
|
||||
felder: { titel: 'Drucker klemmt weiterhin' },
|
||||
})
|
||||
})
|
||||
|
||||
test('übernimmt eine neue Fälligkeit', () => {
|
||||
const faelligAm = new Date('2026-04-01T00:00:00.000Z')
|
||||
expect(uebernahmeEntscheiden(fund({ faelligAm }), bestand())).toEqual({
|
||||
tun: 'aktualisieren',
|
||||
felder: { faelligAm },
|
||||
})
|
||||
})
|
||||
|
||||
test('vergleicht Datumswerte nach Zeitpunkt, nicht nach Objekt', () => {
|
||||
const faelligAm = new Date('2026-04-01T00:00:00.000Z')
|
||||
const entscheidung = uebernahmeEntscheiden(
|
||||
fund({ faelligAm: new Date(faelligAm.getTime()) }),
|
||||
bestand({ faelligAm }),
|
||||
)
|
||||
expect(entscheidung).toEqual({ tun: 'nichts', grund: 'unveraendert' })
|
||||
})
|
||||
|
||||
test('rührt Priorität und Aufwand nicht an', () => {
|
||||
// Beides wird hier geschätzt, nicht drüben. Ein Abgleich, der die
|
||||
// Schätzung wegräumt, macht die Wochenplanung kaputt.
|
||||
const entscheidung = uebernahmeEntscheiden(
|
||||
fund({ prioritaet: 1, aufwandMinuten: 30 }),
|
||||
bestand({ prioritaet: 4, aufwandMinuten: 90 }),
|
||||
)
|
||||
expect(entscheidung).toEqual({ tun: 'nichts', grund: 'unveraendert' })
|
||||
})
|
||||
|
||||
test('schließt, was drüben erledigt wurde', () => {
|
||||
expect(uebernahmeEntscheiden(fund({ erledigt: true }), bestand())).toEqual({
|
||||
tun: 'schliessen',
|
||||
})
|
||||
})
|
||||
|
||||
test('schließt nicht zweimal', () => {
|
||||
expect(
|
||||
uebernahmeEntscheiden(fund({ erledigt: true }), bestand({ erledigt: true })),
|
||||
).toEqual({ tun: 'nichts', grund: 'schon-erledigt' })
|
||||
})
|
||||
|
||||
test('holt eine hier erledigte Aufgabe zurück, wenn sie drüben wieder offen ist', () => {
|
||||
const entscheidung = uebernahmeEntscheiden(fund(), bestand({ erledigt: true }))
|
||||
// Das Ursprungssystem führt den Zustand; wer dort neu öffnet, meint es so.
|
||||
expect(entscheidung).toEqual({ tun: 'nichts', grund: 'unveraendert' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('alsTitel', () => {
|
||||
test('lässt kurze Texte in Ruhe', () => {
|
||||
expect(alsTitel('Angebot schreiben')).toEqual({
|
||||
titel: 'Angebot schreiben',
|
||||
rest: null,
|
||||
})
|
||||
})
|
||||
|
||||
test('macht aus Zeilenumbrüchen Leerzeichen', () => {
|
||||
expect(alsTitel('Angebot\n\nschreiben ')).toEqual({
|
||||
titel: 'Angebot schreiben',
|
||||
rest: null,
|
||||
})
|
||||
})
|
||||
|
||||
test('kürzt lange Texte und hebt den Rest auf', () => {
|
||||
const lang = `${'wort '.repeat(60)}ende`
|
||||
const { titel, rest } = alsTitel(lang)
|
||||
expect(titel.length).toBeLessThanOrEqual(TITEL_MAX + 1)
|
||||
expect(titel.endsWith('…')).toBe(true)
|
||||
expect(rest).not.toBeNull()
|
||||
expect(rest?.endsWith('ende')).toBe(true)
|
||||
})
|
||||
|
||||
test('trennt nicht mitten im Wort, wenn eine Fuge in der Nähe ist', () => {
|
||||
const lang = `${'a'.repeat(60)} ${'b'.repeat(80)}`
|
||||
const { titel } = alsTitel(lang)
|
||||
expect(titel).toBe(`${'a'.repeat(60)}…`)
|
||||
})
|
||||
|
||||
test('trennt hart, wenn es keine Fuge gibt', () => {
|
||||
const { titel } = alsTitel('x'.repeat(200))
|
||||
expect(titel).toBe(`${'x'.repeat(TITEL_MAX)}…`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendungFrisch', () => {
|
||||
const jetzt = new Date('2026-03-17T08:20:00.000Z')
|
||||
|
||||
test('nimmt an, was gerade erst losgeschickt wurde', () => {
|
||||
expect(sendungFrisch(new Date('2026-03-17T08:18:00.000Z'), jetzt)).toBe(true)
|
||||
})
|
||||
|
||||
test('lehnt ein altes Paket ab', () => {
|
||||
expect(sendungFrisch(new Date('2026-03-17T08:10:00.000Z'), jetzt)).toBe(false)
|
||||
})
|
||||
|
||||
test('lehnt auch ein Paket aus der Zukunft ab', () => {
|
||||
// Eine verstellte Uhr auf der Gegenseite ist genauso verdächtig.
|
||||
expect(sendungFrisch(new Date('2026-03-17T08:40:00.000Z'), jetzt)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('spiegelSendung', () => {
|
||||
test('nimmt ein vollständiges Paket an', () => {
|
||||
const gelesen = spiegelSendung.safeParse({
|
||||
gesendetAm: '2026-03-17T08:20:00.000Z',
|
||||
belegungen: [
|
||||
{
|
||||
ref: 'zb-1',
|
||||
beginn: '2026-03-17T09:00:00.000Z',
|
||||
ende: '2026-03-17T10:00:00.000Z',
|
||||
},
|
||||
],
|
||||
aufgaben: [{ ref: 'a-1', titel: 'Angebot schreiben' }],
|
||||
})
|
||||
expect(gelesen.success).toBe(true)
|
||||
expect(gelesen.success && gelesen.data.aufgaben[0]?.erledigt).toBe(false)
|
||||
})
|
||||
|
||||
test('weist ein Paket ohne Zeitstempel ab', () => {
|
||||
const gelesen = spiegelSendung.safeParse({ belegungen: [], aufgaben: [] })
|
||||
expect(gelesen.success).toBe(false)
|
||||
})
|
||||
|
||||
test('weist eine Belegung mit kaputtem Datum ab', () => {
|
||||
const gelesen = spiegelSendung.safeParse({
|
||||
gesendetAm: '2026-03-17T08:20:00.000Z',
|
||||
belegungen: [{ ref: 'zb-1', beginn: 'morgen', ende: 'später' }],
|
||||
aufgaben: [],
|
||||
})
|
||||
expect(gelesen.success).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user