366 lines
16 KiB
JavaScript
366 lines
16 KiB
JavaScript
import { test } from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import {
|
||
customerChanges,
|
||
customerTicketStatus,
|
||
isInternalTicket,
|
||
customerOrder,
|
||
customerWorkEntries,
|
||
workTotalMinutes,
|
||
} from '../server/services/customerProjectView.js'
|
||
|
||
// Commit wie listRepoCommits() ihn liefert (giteaAdmin.js)
|
||
function commit(message, date = '2026-09-24T12:05:00Z') {
|
||
return {
|
||
sha: '88d3c37ab7b15337772cf59a5a9294ec0cc8cca4',
|
||
shortSha: '88d3c37',
|
||
message,
|
||
authorName: 'Claude',
|
||
date,
|
||
url: 'https://git.webklar.com/knso/ff-lack/commit/88d3c37ab7b15337772cf59a5a9294ec0cc8cca4',
|
||
}
|
||
}
|
||
|
||
const titles = (commits) => customerChanges(commits).map((c) => c.title)
|
||
|
||
/* --------------------------------------------------------- Änderungen --- */
|
||
|
||
test('Änderung: KI-Trailer fallen weg, Kürzel/Autor/Link werden nicht ausgeliefert', () => {
|
||
const changes = customerChanges([
|
||
commit('Öffnungszeiten für den Winter\n\nTeamfotos ausgetauscht\n\nCo-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>\n'),
|
||
])
|
||
assert.deepEqual(changes, [
|
||
{ date: '2026-09-24T12:05:00Z', title: 'Öffnungszeiten für den Winter', details: ['Teamfotos ausgetauscht'] },
|
||
])
|
||
})
|
||
|
||
test('Änderung: auch andere Modelle, Tools und Git-Trailer fallen weg', () => {
|
||
const [change] = customerChanges([
|
||
commit([
|
||
'Kontaktformular',
|
||
'',
|
||
'Co-authored-by: Copilot <copilot@github.com>',
|
||
'Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>',
|
||
'Signed-off-by: Kenso Grimm <kenso@webklar.com>',
|
||
'Change-Id: I8d2f0a1b',
|
||
'🤖 Generated with [Claude Code](https://claude.com/claude-code)',
|
||
'https://claude.ai/code/session_01AbCdEf',
|
||
].join('\n')),
|
||
])
|
||
assert.deepEqual(change.details, [])
|
||
})
|
||
|
||
test('Änderung: Präfixe wie feat(...)/fix: verschwinden, erster Buchstabe groß', () => {
|
||
assert.deepEqual(
|
||
titles([commit('feat(roadmaps): Ticket-Integration'), commit('fix: kaputten Link im Footer repariert', '2026-09-23T10:00:00Z')]),
|
||
['Ticket-Integration', 'Kaputten Link im Footer repariert']
|
||
)
|
||
})
|
||
|
||
test('Änderung: rein technische Commits werden ausgeblendet', () => {
|
||
assert.deepEqual(
|
||
titles([
|
||
commit('Update preview config from ticket system\n'),
|
||
commit('Preview-Config: unger-reisen.project.webklar.com\n'),
|
||
commit('Hosting: Preview aktivieren (automatisch)\n'),
|
||
commit('Migration: Preview auf Pipeline-Repo umgezogen (identica-kaiser)\n'),
|
||
commit("Merge branch 'main' of https://git.webklar.com/knso/lackwerk-kl\n"),
|
||
commit("Merge pull request 'Neue Startseite' (#3) from dev into main\n"),
|
||
]),
|
||
[]
|
||
)
|
||
})
|
||
|
||
test('Änderung: bekannte Automatik-Texte werden übersetzt', () => {
|
||
const day = (d) => `2026-09-${d}T08:00:00Z`
|
||
assert.deepEqual(
|
||
titles([
|
||
commit('Initial commit\n', day(10)),
|
||
commit('Automatisch erstellte Preview-Seite\n', day(11)),
|
||
commit('Anreicherung: Bilder, Logo, Text-Feinarbeit (Cloud-Routine)\n', day(12)),
|
||
commit('Google Fonts self-hosten (DSGVO): assets/fonts.css statt fonts.googleapis.com\n', day(13)),
|
||
commit('Website ff-lack – aus tamplate-lack, auf Fahrzeuglackierer Fruck – Altenglan angepasst\n', day(14)),
|
||
]),
|
||
[
|
||
'Projekt angelegt',
|
||
'Erste Vorschau der Website erstellt',
|
||
'Inhalte ergänzt: Bilder, Logo, Text-Feinarbeit',
|
||
'Schriftarten datenschutzkonform eingebunden (DSGVO)',
|
||
'Website erstellt und an den Betrieb angepasst',
|
||
]
|
||
)
|
||
})
|
||
|
||
test('Änderung: nichtssagende Titel werden zu "Website aktualisiert", echte Wörter bleiben', () => {
|
||
const day = (d) => `2026-09-${d}T08:00:00Z`
|
||
assert.deepEqual(
|
||
titles([
|
||
commit('fix\n', day(10)),
|
||
commit('upload\n', day(11)),
|
||
commit('pagev1\n', day(12)),
|
||
commit('webseite-version-01\n', day(13)),
|
||
commit('Teamseite\n', day(14)),
|
||
]),
|
||
['Website aktualisiert', 'Website aktualisiert', 'Website aktualisiert', 'Website aktualisiert', 'Teamseite']
|
||
)
|
||
})
|
||
|
||
test('Änderung: Revert wird verständlich, die "This reverts commit"-Zeile fällt weg', () => {
|
||
const [change] = customerChanges([
|
||
commit('Revert "Anreicherung: Bilder, Logo, Farbwelt, Texte (Cloud-Routine)"\n\nThis reverts commit 4ff6e7a0348c5553769fbff2ac4bc1e44817ff28.\n'),
|
||
])
|
||
assert.deepEqual(change, {
|
||
date: '2026-09-24T12:05:00Z',
|
||
title: '„Inhalte ergänzt: Bilder, Logo, Farbwelt, Texte“ zurückgenommen',
|
||
details: [],
|
||
})
|
||
})
|
||
|
||
test('Änderung: umbrochene Aufzählungen und Absätze werden wieder zusammengefügt', () => {
|
||
const [bullets] = customerChanges([
|
||
commit([
|
||
'Website stärker personalisiert',
|
||
'',
|
||
'- Echte Betriebsfotos statt Stock: Luftbild als Hero, Galerie',
|
||
' mit rotem Lackier-Fahrzeug, Farbtonanalyse-',
|
||
' Mischsystem',
|
||
'- Marken-Rot statt Blau',
|
||
'',
|
||
'Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>',
|
||
].join('\n')),
|
||
])
|
||
assert.deepEqual(bullets.details, [
|
||
'Echte Betriebsfotos statt Stock: Luftbild als Hero, Galerie mit rotem Lackier-Fahrzeug, Farbtonanalyse-Mischsystem',
|
||
'Marken-Rot statt Blau',
|
||
])
|
||
|
||
const [paragraphs] = customerChanges([
|
||
commit('Planboard\n\nDas Planboard war eine Hülle - sieben Spalten\nmit "Keine Termine".\n\nZweiter Absatz.\n'),
|
||
])
|
||
assert.deepEqual(paragraphs.details, ['Das Planboard war eine Hülle - sieben Spalten mit "Keine Termine".', 'Zweiter Absatz.'])
|
||
})
|
||
|
||
test('Änderung: Backticks aus Markdown verschwinden', () => {
|
||
assert.deepEqual(titles([commit('Kontakt: `info@autohaus-meier.de` ergänzt')]), ['Kontakt: info@autohaus-meier.de ergänzt'])
|
||
})
|
||
|
||
test('Änderung: gleiche Titel am selben Tag stehen nur einmal da, an verschiedenen Tagen zweimal', () => {
|
||
const changes = customerChanges([
|
||
commit('fix\n', '2026-09-24T15:00:00Z'),
|
||
commit('upload\n', '2026-09-24T09:00:00Z'),
|
||
commit('fix\n', '2026-09-20T09:00:00Z'),
|
||
])
|
||
assert.deepEqual(changes.map((c) => c.date), ['2026-09-24T15:00:00Z', '2026-09-20T09:00:00Z'])
|
||
})
|
||
|
||
/* ------------------------------------------------------ Ticketstatus --- */
|
||
|
||
test('Status: kein Status aus dem Ticketsystem kommt unübersetzt beim Kunden an', () => {
|
||
const woms = ['Open', 'Occupied', 'Assigned', 'Awaiting', 'Added Info', 'In Test', 'Halted', 'Aborted', 'Cancelled', 'Closed']
|
||
for (const status of woms) {
|
||
const { label, step } = customerTicketStatus(status)
|
||
assert.notEqual(label, status, status)
|
||
assert.ok(Number.isInteger(step) && step >= 0 && step <= 4, `${status}: step ${step}`)
|
||
}
|
||
})
|
||
|
||
test('Status: Fortschritt und Ton passen zur Bedeutung', () => {
|
||
assert.deepEqual(customerTicketStatus('Open'), { label: 'Eingegangen', tone: 'new', step: 1 })
|
||
assert.deepEqual(customerTicketStatus('Occupied'), { label: 'In Arbeit', tone: 'active', step: 2 })
|
||
assert.deepEqual(customerTicketStatus('Awaiting'), { label: 'Wartet auf deine Rückmeldung', tone: 'waiting', step: 2 })
|
||
assert.deepEqual(customerTicketStatus('In Test'), { label: 'Wird geprüft', tone: 'review', step: 3 })
|
||
assert.deepEqual(customerTicketStatus('Closed'), { label: 'Erledigt', tone: 'done', step: 4 })
|
||
assert.deepEqual(customerTicketStatus('Cancelled'), { label: 'Storniert', tone: 'stopped', step: 0 })
|
||
})
|
||
|
||
test('Status: leer heißt "Eingegangen", Unbekanntes wird nie roh durchgereicht', () => {
|
||
assert.equal(customerTicketStatus('').label, 'Eingegangen')
|
||
assert.equal(customerTicketStatus(undefined).label, 'Eingegangen')
|
||
assert.deepEqual(customerTicketStatus('Zugesagt'), { label: 'In Bearbeitung', tone: 'active', step: 2 })
|
||
})
|
||
|
||
/* ------------------------------------------------- Auftrag / Akquise --- */
|
||
|
||
test('Auftrag: Akquise-Tickets sind intern, auch wenn nur der Betreff so beginnt', () => {
|
||
assert.equal(isInternalTicket({ type: 'Akquise', topic: 'Akquise: Mover GmbH' }), true)
|
||
assert.equal(isInternalTicket({ type: 'akquise', topic: 'Lead' }), true)
|
||
assert.equal(isInternalTicket({ type: 'Webpage', topic: 'Akquise: Mover GmbH' }), true)
|
||
assert.equal(isInternalTicket({ type: 'Webpage', topic: 'Relaunch der Website' }), false)
|
||
})
|
||
|
||
test('Auftrag: Nummer, Betreff und übersetzter Status; intern -> null', () => {
|
||
assert.deepEqual(customerOrder({ woid: '100004', topic: 'Demo Webseite', status: 'Closed', type: 'Webpage' }), {
|
||
number: '100004',
|
||
topic: 'Demo Webseite',
|
||
status: { label: 'Erledigt', tone: 'done', step: 4 },
|
||
})
|
||
assert.equal(customerOrder({ woid: '100015', topic: 'Akquise: Mover GmbH', status: 'Open', type: 'Akquise' }), null)
|
||
assert.equal(customerOrder(null), null)
|
||
})
|
||
|
||
/* --------------------------------------------------- Arbeitseinträge --- */
|
||
|
||
// Worksheet wie gitWorksheet.js ihn beim Push anlegt
|
||
const gitSheet = {
|
||
serviceType: 'GIT',
|
||
employeeName: 'knso',
|
||
employeeShort: 'GIT',
|
||
totalTime: 0,
|
||
startTime: '',
|
||
startDate: '24.09.2026',
|
||
endDate: '24.09.2026',
|
||
endTime: '1405',
|
||
isComment: true,
|
||
details: [
|
||
'Git Push auf `knso/ff-lack` (Branch: main)',
|
||
'Commit: 88d3c37',
|
||
'',
|
||
'- [88d3c37](https://git.webklar.com/knso/ff-lack/commit/88d3c37ab7b15337772cf59a5a9294ec0cc8cca4): Hintergrundbilder leicht rot einfaerben (knso)',
|
||
' Dezenter roter Tint auf den Hintergrundbildern',
|
||
' Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>',
|
||
'- [1234567](https://git.webklar.com/knso/ff-lack/commit/1234567890abcdef1234567890abcdef12345678): Update preview config from ticket system (knso)',
|
||
'- [2ca14eb](https://git.webklar.com/knso/ff-lack/commit/2ca14ebd93d53c45dee69e1beb8e668c4ce2a016): Anreicherung: Bilder, Logo, Text-Feinarbeit (Cloud-Routine) (webklar-automation)',
|
||
].join('\n'),
|
||
}
|
||
|
||
test('Arbeit: Git-Push wird zu "Website aktualisiert" mit bereinigten Änderungstiteln', () => {
|
||
assert.deepEqual(customerWorkEntries([gitSheet], { includeUpdates: true }), [
|
||
{
|
||
kind: 'update',
|
||
label: 'Website aktualisiert',
|
||
date: '2026-09-24',
|
||
minutes: 0,
|
||
changes: ['Hintergrundbilder leicht rot einfaerben', 'Inhalte ergänzt: Bilder, Logo, Text-Feinarbeit'],
|
||
},
|
||
])
|
||
})
|
||
|
||
test('Arbeit: mehrere Pushes am selben Tag werden ein Eintrag, andere Tage bleiben getrennt', () => {
|
||
const push = (day, sha, title, totalTime = 0) => ({
|
||
...gitSheet,
|
||
endDate: day,
|
||
totalTime,
|
||
details: `Git Push auf \`knso/ff-lack\` (Branch: main)\nCommit: ${sha}\n\n- [${sha}](https://git.webklar.com/knso/ff-lack/commit/${sha}): ${title} (knso)`,
|
||
})
|
||
const entries = customerWorkEntries(
|
||
[
|
||
push('24.09.2026', 'aaaaaaa', 'Handy-Ansicht überarbeitet', 15),
|
||
push('24.09.2026', 'bbbbbbb', 'Trust-Icons repariert', 30),
|
||
push('23.09.2026', 'ccccccc', 'Anfahrt ergänzt'),
|
||
],
|
||
{ includeUpdates: true }
|
||
)
|
||
assert.deepEqual(entries, [
|
||
{ kind: 'update', label: 'Website aktualisiert', date: '2026-09-24', minutes: 45, changes: ['Handy-Ansicht überarbeitet', 'Trust-Icons repariert'] },
|
||
{ kind: 'update', label: 'Website aktualisiert', date: '2026-09-23', minutes: 0, changes: ['Anfahrt ergänzt'] },
|
||
])
|
||
})
|
||
|
||
test('Arbeit: Push mit nur technischen Commits verschwindet ganz', () => {
|
||
const onlyConfig = {
|
||
...gitSheet,
|
||
details: 'Git Push auf `knso/ff-lack` (Branch: main)\nCommit: 1234567\n\n- [1234567](https://git.webklar.com/x/y/commit/1234567): Update preview config from ticket system (knso)',
|
||
}
|
||
const noDetails = { ...gitSheet, details: 'Git Push auf `knso/ff-lack` (Branch: main)\nCommit: ?\n\n- (keine Commit-Details im Webhook)' }
|
||
assert.deepEqual(customerWorkEntries([onlyConfig, noDetails], { includeUpdates: true }), [])
|
||
})
|
||
|
||
test('Arbeit: ohne includeUpdates bleiben nur Git-Einträge mit nachgetragener Zeit', () => {
|
||
const withTime = { ...gitSheet, totalTime: 45, startTime: '1320' }
|
||
const entries = customerWorkEntries([gitSheet, withTime], { includeUpdates: false })
|
||
assert.deepEqual(entries.map((e) => [e.kind, e.minutes]), [['update', 45]])
|
||
})
|
||
|
||
test('Arbeit: manuelle Einträge bekommen deutsche Art, Person, Zeit und Text', () => {
|
||
const sheet = (serviceType, extra = {}) => ({
|
||
serviceType,
|
||
employeeName: 'Kenso Grimm',
|
||
totalTime: 150,
|
||
startDate: '20.09.2026',
|
||
endDate: '20.09.2026',
|
||
details: 'Website neu erstellt und als Vorschau bereitgestellt.',
|
||
isComment: false,
|
||
...extra,
|
||
})
|
||
assert.deepEqual(customerWorkEntries([sheet('Remote')]), [
|
||
{
|
||
kind: 'online',
|
||
label: 'Online erledigt',
|
||
date: '2026-09-20',
|
||
minutes: 150,
|
||
person: 'Kenso Grimm',
|
||
text: 'Website neu erstellt und als Vorschau bereitgestellt.',
|
||
},
|
||
])
|
||
assert.deepEqual(
|
||
customerWorkEntries([sheet('On Site'), sheet('Off Site'), sheet('Sonderwunsch')]).map((e) => [e.kind, e.label]),
|
||
[['onsite', 'Vor Ort bei dir'], ['office', 'In unserem Büro'], ['work', 'Arbeit am Projekt']]
|
||
)
|
||
})
|
||
|
||
test('Arbeit: interne Kommentare tauchen nicht auf', () => {
|
||
const comment = { serviceType: 'Remote', employeeName: 'Kenso Grimm', totalTime: 0, endDate: '20.09.2026', details: 'Roadmap-Knoten "Erstellen": Offen -> In Arbeit', isComment: true }
|
||
const commentType = { serviceType: 'COMMENT', employeeName: 'Kenso Grimm', totalTime: 0, endDate: '20.09.2026', details: 'intern', isComment: false }
|
||
assert.deepEqual(customerWorkEntries([comment, commentType], { includeUpdates: true }), [])
|
||
})
|
||
|
||
test('Arbeit: Einträge aus getCustomerActivity (date statt endDate, Zeit als Text) werden verstanden', () => {
|
||
const fromActivity = { serviceType: 'Remote', employeeName: 'Andrej Stevanovski', totalTime: '30', date: '18.09.2026', details: 'Demo-Version erstellt', isGit: false, pending: false }
|
||
assert.deepEqual(customerWorkEntries([fromActivity]), [
|
||
{ kind: 'online', label: 'Online erledigt', date: '2026-09-18', minutes: 30, person: 'Andrej Stevanovski', text: 'Demo-Version erstellt' },
|
||
])
|
||
})
|
||
|
||
/* ---------------------------------------------------- Unterstützung --- */
|
||
|
||
const andrej = { employeeId: 'andrej', name: 'Andrej Stevanovski', task: 'Farbkonzept' }
|
||
const nico = { employeeId: 'nico', name: 'Nico Rech', task: 'Texte' }
|
||
const workSheet = (extra = {}) => ({
|
||
serviceType: 'Remote', employeeName: 'Justin Klein', totalTime: 60, startDate: '30.09.2026', endDate: '30.09.2026',
|
||
details: 'Startseite umgebaut', isComment: false, ...extra,
|
||
})
|
||
|
||
test('Unterstützung: eigene Zeile mit Vorname, Tätigkeit und halber Zeit', () => {
|
||
const [entry] = customerWorkEntries([workSheet({ supporters: JSON.stringify([andrej]) })])
|
||
assert.equal(entry.minutes, 60)
|
||
assert.deepEqual(entry.supports, [{ name: 'Andrej', task: 'Farbkonzept', minutes: 30 }])
|
||
})
|
||
|
||
test('Unterstützung: ohne supporters kein supports-Feld (bisherige Form bleibt)', () => {
|
||
const [entry] = customerWorkEntries([workSheet()])
|
||
assert.equal('supports' in entry, false)
|
||
})
|
||
|
||
test('Unterstützung: zusammengefasste Git-Tage sammeln alle Unterstützungen', () => {
|
||
const push = (sha, title, sup) => ({
|
||
...gitSheet, endDate: '24.09.2026', totalTime: 30, startTime: '1300', isComment: false, supporters: JSON.stringify([sup]),
|
||
details: `Git Push auf \`knso/ff-lack\` (Branch: main)\nCommit: ${sha}\n\n- [${sha}](https://git.webklar.com/knso/ff-lack/commit/${sha}): ${title} (knso)`,
|
||
})
|
||
const entries = customerWorkEntries([push('aaaaaaa', 'Handy-Ansicht', andrej), push('bbbbbbb', 'Icons', nico)], { includeUpdates: false })
|
||
assert.equal(entries.length, 1)
|
||
assert.equal(entries[0].minutes, 60)
|
||
assert.deepEqual(entries[0].supports, [
|
||
{ name: 'Andrej', task: 'Farbkonzept', minutes: 15 },
|
||
{ name: 'Nico', task: 'Texte', minutes: 15 },
|
||
])
|
||
})
|
||
|
||
test('Unterstützung: Kommentar mit supporters erscheint nicht', () => {
|
||
assert.deepEqual(customerWorkEntries([workSheet({ isComment: true, supporters: JSON.stringify([andrej]) })]), [])
|
||
})
|
||
|
||
test('Unterstützung: kaputtes supporters -> Eintrag ohne Unterstützung', () => {
|
||
const [entry] = customerWorkEntries([workSheet({ supporters: 'kaputt' })])
|
||
assert.equal(entry.minutes, 60)
|
||
assert.equal('supports' in entry, false)
|
||
})
|
||
|
||
test('workTotalMinutes: Summe aller Schritte inkl. Unterstützung', () => {
|
||
const entries = customerWorkEntries([
|
||
workSheet({ supporters: JSON.stringify([andrej, nico]) }),
|
||
workSheet({ totalTime: 45 }),
|
||
])
|
||
assert.equal(workTotalMinutes(entries), 60 + 30 + 30 + 45)
|
||
assert.equal(workTotalMinutes([]), 0)
|
||
})
|