feat: Zuständigen-Engpass — Warnung, wenn eine Person mehr als die Hälfte des Pfads trägt (D71)

Trägt ein @name mehr als die Hälfte der offenen Arbeit des günstigsten
Pfads (Marginalkosten-Maß, D69) und tragen mindestens zwei Personen Last
auf der offenen Front, meldet die zeilenlose Warnung assigneeOverload die
mögliche Engstelle; die Pillen der Person wechseln an offenen Pfad-Knoten
auf die Warnfarbe. Knoten ohne Tags erben für die Rechnung vom nächsten
getaggten Vorfahren, mehrere Tags teilen sich den Beitrag.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-25 11:29:52 +02:00
co-authored by Claude Fable 5
parent d1da6a3529
commit 35b72fc178
10 changed files with 343 additions and 4 deletions
+18 -3
View File
@@ -1,6 +1,6 @@
import './style.css';
import { parse, setFoldMark, expandShortIds, shortIdClosed } from './parser.js';
import { computeCheapPlan, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus, atMostM, lineTargets } from './model.js';
import { computeCheapPlan, overloadedAssignee, freshProdSet, initialCollapsed, nodeKeys, effectiveStatus, atMostM, lineTargets } from './model.js';
import { esc, renderTreeHtml, TIP_RULE } from './render.js';
import { formatWarning, warningText } from './warnings.js';
import { padUrls } from './remote.js';
@@ -133,11 +133,16 @@ function render(){
/* Günstigster Pfad auf der Dependency Closure (D42): scheitert die exakte
Suche an zu vielen gekoppelten Gruppen, wird die gierige Schätzung
BENANNT statt stillschweigend geliefert (zeilenlose Warnung). */
let cheapSet = new Set();
let cheapSet = new Set(), overload = null;
if(cheapPathOn){
const plan = computeCheapPlan(roots);
cheapSet = plan.set;
if(!plan.exact) warnings = warnings.concat([{type: 'cheapApprox'}]);
/* Zuständigen-Engpass (SPEC §9/D71): Trägt eine Person mehr als die
Hälfte der offenen Pfad-Arbeit, wird das zeilenlos gemeldet und ihre
Pillen an offenen Pfad-Knoten warnfarben (overloadTag unten). */
overload = overloadedAssignee(roots, cheapSet);
if(overload) warnings = warnings.concat([{type: 'assigneeOverload', ...overload}]);
}
out.classList.toggle('cheap-on', cheapPathOn);
/* Faltung (D38): Anfangszustand aus den Textmarken (`!!!` holt sich mit
@@ -185,7 +190,8 @@ function render(){
lineTargetMap = lineTargets(roots, collapsedSet, showDiscarded);
const r = renderTreeHtml(roots, {t, showDiscarded, cheapPath: cheapPathOn, cheapSet, showIds,
freshSet, collapsedSet,
effStatus: effectiveStatus(roots)});
effStatus: effectiveStatus(roots),
overloadTag: overload ? overload.tag : null});
out.innerHTML = r.html;
warnings = warnings.concat(r.warnings);
}
@@ -2220,6 +2226,7 @@ const I18N = {
sizeConflictWarn:"Zeile {line}: Die Teilpakete übersteigen zusammen die angegebene Größe ({size}) — selbst in der günstigsten Lesart.",
sizeConflictTooltip:"Die Teilpakete übersteigen zusammen die angegebene Größe",
cheapApproxWarn:"Zu viele gekoppelte Alternativgruppen für die exakte Suche — der günstigste Pfad ist gierig geschätzt (je Gruppe lokal gewählt).",
assigneeOverloadWarn:"@{tag} trägt {share} % der offenen Arbeit auf dem günstigsten Pfad ({stations} von {total} Stationen) — mögliche Engstelle.",
st_idee:"Idee", st_geplant:"geplant", st_arbeit:"in Arbeit", st_durchstich:"Durchstich",
st_fertig:"fertig", st_prod:"in Produktion", st_highrisk:"High Risk", st_verworfen:"verworfen",
unknownStatusWarn:"Zeile {line}: unbekanntes Statuszeichen „{code}“ — als neutral dargestellt.",
@@ -2324,6 +2331,7 @@ const I18N = {
sizeConflictWarn:"Line {line}: the sub-packages together exceed the given size ({size}) — even in the most optimistic reading.",
sizeConflictTooltip:"The sub-packages together exceed the given size",
cheapApproxWarn:"Too many coupled alternative groups for the exact search — the cheapest path is a greedy estimate (chosen locally per group).",
assigneeOverloadWarn:"@{tag} carries {share}% of the open work on the cheapest path ({stations} of {total} stations) — a possible bottleneck.",
st_idee:"idea", st_geplant:"planned", st_arbeit:"in progress", st_durchstich:"walking skeleton",
st_fertig:"done", st_prod:"in production", st_highrisk:"high risk", st_verworfen:"discarded",
unknownStatusWarn:"Line {line}: unknown status code “{code}” — shown as neutral.",
@@ -2428,6 +2436,7 @@ const I18N = {
sizeConflictWarn:"Línea {line}: los subpaquetes juntos superan el tamaño indicado ({size}), incluso en la lectura más optimista.",
sizeConflictTooltip:"Los subpaquetes juntos superan el tamaño indicado",
cheapApproxWarn:"Demasiados grupos de alternativas acoplados para la búsqueda exacta — el camino más barato es una estimación voraz (elección local por grupo).",
assigneeOverloadWarn:"@{tag} lleva el {share} % del trabajo pendiente en el camino más barato ({stations} de {total} estaciones) — posible cuello de botella.",
st_idee:"idea", st_geplant:"planificado", st_arbeit:"en curso", st_durchstich:"prototipo funcional",
st_fertig:"terminado", st_prod:"en producción", st_highrisk:"alto riesgo", st_verworfen:"descartado",
unknownStatusWarn:"Línea {line}: código de estado desconocido «{code}» — mostrado como neutral.",
@@ -2532,6 +2541,7 @@ const I18N = {
sizeConflictWarn:"Ligne {line} : les sous-lots dépassent ensemble la taille indiquée ({size}), même dans la lecture la plus optimiste.",
sizeConflictTooltip:"Les sous-lots dépassent ensemble la taille indiquée",
cheapApproxWarn:"Trop de groupes dalternatives couplés pour la recherche exacte — le chemin le moins cher est une estimation gloutonne (choix local par groupe).",
assigneeOverloadWarn:"@{tag} porte {share} % du travail restant sur le chemin le moins cher ({stations} stations sur {total}) — goulot d’étranglement possible.",
st_idee:"idée", st_geplant:"planifié", st_arbeit:"en cours", st_durchstich:"squelette fonctionnel",
st_fertig:"terminé", st_prod:"en production", st_highrisk:"risque élevé", st_verworfen:"abandonné",
unknownStatusWarn:"Ligne {line} : code de statut inconnu « {code} » — affiché comme neutre.",
@@ -2636,6 +2646,7 @@ const I18N = {
sizeConflictWarn:"Wiersz {line}: podzadania razem przekraczają podany rozmiar ({size}) — nawet w najkorzystniejszym odczycie.",
sizeConflictTooltip:"Podzadania razem przekraczają podany rozmiar",
cheapApproxWarn:"Zbyt wiele sprzężonych grup alternatyw dla dokładnego wyszukiwania — najtańsza ścieżka jest oszacowana zachłannie (wybór lokalny w każdej grupie).",
assigneeOverloadWarn:"@{tag} niesie {share} % otwartej pracy na najtańszej ścieżce ({stations} z {total} stacji) — możliwe wąskie gardło.",
st_idee:"pomysł", st_geplant:"zaplanowane", st_arbeit:"w toku", st_durchstich:"działający szkielet",
st_fertig:"gotowe", st_prod:"w produkcji", st_highrisk:"wysokie ryzyko", st_verworfen:"odrzucone",
unknownStatusWarn:"Wiersz {line}: nieznany znak statusu „{code}” — pokazany jako neutralny.",
@@ -2740,6 +2751,7 @@ const I18N = {
sizeConflictWarn:"Строка {line}: подзадачи вместе превышают указанный размер ({size}) — даже при самой оптимистичной оценке.",
sizeConflictTooltip:"Подзадачи вместе превышают указанный размер",
cheapApproxWarn:"Слишком много связанных групп альтернатив для точного поиска — самый дешёвый путь оценён жадно (локальный выбор в каждой группе).",
assigneeOverloadWarn:"@{tag} несёт {share} % открытой работы на самом дешёвом пути ({stations} из {total} станций) — возможное узкое место.",
st_idee:"идея", st_geplant:"запланировано", st_arbeit:"в работе", st_durchstich:"сквозной прототип",
st_fertig:"готово", st_prod:"в эксплуатации", st_highrisk:"высокий риск", st_verworfen:"отклонено",
unknownStatusWarn:"Строка {line}: неизвестный код статуса «{code}» — показан как нейтральный.",
@@ -2844,6 +2856,7 @@ const I18N = {
sizeConflictWarn:"पंक्ति {line}: उप-पैकेज मिलकर दिए गए आकार ({size}) से बड़े हैं — सबसे आशावादी आकलन में भी।",
sizeConflictTooltip:"उप-पैकेज मिलकर दिए गए आकार से बड़े हैं",
cheapApproxWarn:"सटीक खोज के लिए बहुत सारे युग्मित विकल्प-समूह — सबसे सस्ता पथ लालची अनुमान है (प्रति समूह स्थानीय चयन)।",
assigneeOverloadWarn:"@{tag} सबसे सस्ते पथ पर खुले काम का {share}% उठाए हुए है ({total} में से {stations} स्टेशन) — संभावित अड़चन।",
st_idee:"विचार", st_geplant:"नियोजित", st_arbeit:"प्रगति पर", st_durchstich:"कार्यशील ढाँचा",
st_fertig:"पूर्ण", st_prod:"उत्पादन में", st_highrisk:"उच्च जोखिम", st_verworfen:"अस्वीकृत",
unknownStatusWarn:"पंक्ति {line}: अज्ञात स्थिति कोड „{code}“ — तटस्थ रूप में दिखाया गया।",
@@ -2948,6 +2961,7 @@ const I18N = {
sizeConflictWarn:"第 {line} 行:子项合计超出所标注的尺寸({size})——即使按最乐观的估算也是如此。",
sizeConflictTooltip:"子项合计超出所标注的尺寸",
cheapApproxWarn:"耦合的备选组过多,无法精确搜索——最便宜路径为贪心估计(每组就地选择)。",
assigneeOverloadWarn:"@{tag} 承担最便宜路径上 {share}% 的未完成工作({total} 个站点中的 {stations} 个)——可能的瓶颈。",
st_idee:"想法", st_geplant:"已计划", st_arbeit:"进行中", st_durchstich:"可运行骨架",
st_fertig:"已完成", st_prod:"已上线", st_highrisk:"高风险", st_verworfen:"已放弃",
unknownStatusWarn:"第 {line} 行:未知状态代码“{code}”——显示为中性。",
@@ -3052,6 +3066,7 @@ const I18N = {
sizeConflictWarn:"{line} 行目:サブパッケージの合計が指定サイズ({size})を超えています — 最も楽観的な見積もりでも。",
sizeConflictTooltip:"サブパッケージの合計が指定サイズを超えています",
cheapApproxWarn:"結合された選択肢グループが多すぎるため厳密探索は不可 — 最安パスは貪欲法による推定です(グループごとに局所選択)。",
assigneeOverloadWarn:"@{tag} が最安パスの未完了作業の {share}% を担っています(全 {total} 駅中 {stations} 駅)— ボトルネックの可能性。",
st_idee:"アイデア", st_geplant:"計画済み", st_arbeit:"作業中", st_durchstich:"ウォーキングスケルトン",
st_fertig:"完了", st_prod:"本番稼働", st_highrisk:"高リスク", st_verworfen:"破棄",
unknownStatusWarn:"{line} 行目: 不明なステータス記号「{code}」— 中立として表示。",
+53
View File
@@ -318,6 +318,59 @@ export function cheapCls(n, cheapSet, collapsed){
return hidesOpenCheap(n, cheapSet) ? 'cheap' : 'cheap cheap-leaf';
}
/* ---------- Zuständigen-Engpass (SPEC §7/§9, D71) ----------
Wer trägt wie viel der OFFENEN Arbeit des günstigsten Pfads? Jeder nötige
Knoten zählt mit seiner Marge — dem, was seine Größe über die nötigen
Teilpakete hinaus behauptet (dasselbe Maß wie der Belegungs-Vergleich in
`computeCheapPlan`, D69); Erledigtes hat `ownCost` 0 und trägt damit von
selbst nichts. Zuständig ist, wer auf der Zeile getaggt ist; ein Knoten
OHNE Tags erbt für diese Rechnung die Tags des nächsten Vorfahren mit Tags
(WBS-Lesart: Wer das Paket hat, hat die Teilpakete). Mehrere Tags teilen
sich den Beitrag zu gleichen Teilen. Beiträge ohne getaggten Vorfahren
gehen nur in die Gesamtsumme ein — sie verwässern die Anteile, warnen aber
nicht selbst. Stationen (die tiefsten offenen Pfad-Knoten, D46) werden für
den Meldungstext mitgezählt; ein Knoten mit mehreren Zuständigen zählt als
Station bei jedem von ihnen. */
export function assigneeLoads(roots, cheapSet){
const loads = new Map(), stations = new Map();
let total = 0, totalStations = 0;
const walk = (ns, inherited) => {
for(const n of ns){
const owners = n.tags && n.tags.length ? n.tags : inherited;
if(cheapSet.has(n)){
let kids = 0;
for(const k of pathChildren(n)) if(cheapSet.has(k)) kids += ownCost(k);
const m = Math.max(0, ownCost(n) - kids);
total += m;
if(m > 0) for(const tag of owners)
loads.set(tag, (loads.get(tag) || 0) + m / owners.length);
if(!isDone(n) && !hidesOpenCheap(n, cheapSet)){
totalStations++;
for(const tag of owners) stations.set(tag, (stations.get(tag) || 0) + 1);
}
}
walk(n.children, owners);
}
};
walk(roots, []);
return {loads, total, stations, totalStations};
}
/* Die Engpass-Entscheidung (SPEC §9, D71): gemeldet nur, wenn mindestens ZWEI
Personen Last auf der offenen Front tragen — ein Solo-Plan warnt nie, eine
Person mit allem ist dort keine Engstelle, sondern die Realität — und eine
davon MEHR ALS DIE HÄLFTE der gesamten offenen Pfad-Arbeit hält (strikt;
mehr als eine Person über der Hälfte kann es nicht geben). Die Schwelle ist
gesetzt, nicht hergeleitet (D64-Linie). */
export function overloadedAssignee(roots, cheapSet){
const {loads, total, stations, totalStations} = assigneeLoads(roots, cheapSet);
if(loads.size < 2 || total <= 0) return null;
let tag = null, load = 0;
loads.forEach((l, name) => { if(l > load){ load = l; tag = name; } });
if(load * 2 <= total) return null;
return {tag, share: Math.round(load / total * 100),
stations: stations.get(tag) || 0, totalStations};
}
/* ---------- Effektiver Status (SPEC §4/§9, D39) ----------
Fortschritts-Rang entlang der Ergebnis-Skala (D5). Außerhalb der Skala:
neutrale und verworfene Knoten zählen als 0 („nichts Anrechenbares"),
+9 -1
View File
@@ -13,6 +13,9 @@
collapsedSet, // optional: eingeklappte Knoten (Faltung, SPEC §9/D38)
effStatus, // optional: Map Knoten -> effektiver Status-Key, nur
// Diskrepanzen (effectiveStatus() in model.js, D39)
overloadTag, // optional: Person, die mehr als die Hälfte der offenen
// Pfad-Arbeit trägt (overloadedAssignee(), SPEC §9/D71) —
// ihre Pillen an offenen Pfad-Knoten werden warnfarben
} */
import { gateOf, needsBreakdown, visibleChildren, cheapCls, isDone, assumedSize } from './model.js';
@@ -190,8 +193,13 @@ function nodeHtml(n, extra, opts, fold){
als zweiten, dürftigeren Tooltip. Kein zweites Attribut mit demselben Text
— es ist dasselbe, nur unter anderem Namen. */
const title = ` data-tip="${attr(tip)}"`;
/* Zuständigen-Engpass (SPEC §9/D71): Die Pille der überlasteten Person
wechselt an OFFENEN Pfad-Knoten auf die Warnfarbe — dort liegt die Last.
Nur direkte Tags haben eine Pille; geerbte Zuständigkeit (die in die
Rechnung eingeht) hat nichts, das sich färben ließe. */
const hot = tag => opts.overloadTag === tag && opts.cheapSet.has(n) && !isDone(n);
const tagsHtml = n.tags && n.tags.length
? `<span class="tags" aria-hidden="true">${n.tags.map(tag => `<span class="tag">${esc(tag)}</span>`).join('')}</span>`
? `<span class="tags" aria-hidden="true">${n.tags.map(tag => `<span class="tag${hot(tag) ? ' overload' : ''}">${esc(tag)}</span>`).join('')}</span>`
: '';
/* Das implizite Größen-Badge macht eine KOSTENANNAHME sichtbar (D18) —
seit D66 die aus den Teilpaketen geschätzte Größe statt pauschal M. An
+4
View File
@@ -885,6 +885,10 @@
border-radius:999px;padding:1px 6px;line-height:1.4;
box-shadow:0 0 0 1.5px var(--card);
}
/* Zuständigen-Engpass (SPEC §9/D71): die Pille der Person, die mehr als die
Hälfte der offenen Pfad-Arbeit trägt — Warnfarbe wie das
Größen-Konflikt-Badge (D62); der Export liest die gemessenen Farben. */
.tag.overload{background:var(--warn);color:#fff;border-color:var(--warn)}
.ghost-node{
position:relative;margin-top:14px;
border:1.5px dashed var(--warn);
+8
View File
@@ -29,6 +29,11 @@
angegebene Größe des Elternknotens, selbst
in der günstigsten Bereichs-Lesart
(SPEC §5/D62); Zeile = Elternknoten
- assigneeOverload { tag, share, stations, totalStations }
— eine Person trägt mehr als die Hälfte der
offenen Arbeit des günstigsten Pfads
(SPEC §9/D71); zeilenlos — der Engpass
hat keine einzelne Zeile
- cheapApprox { } — günstigster Pfad nur gierig geschätzt:
zu viele gekoppelte Gruppen für die
exakte Suche (D42); zeilenlos
@@ -78,6 +83,9 @@ function build(w, t, esc){
return t('descStrayWarn', {line: w.line});
case 'sizeConflict':
return t('sizeConflictWarn', {line: w.line, size: esc(w.size)});
case 'assigneeOverload':
return t('assigneeOverloadWarn', {tag: esc(w.tag), share: w.share,
stations: w.stations, total: w.totalStations});
case 'cheapApprox':
return t('cheapApproxWarn');
case 'sourceLoad':
+143
View File
@@ -0,0 +1,143 @@
import { describe, it, expect } from 'vitest';
import { parse } from '../src/parser.js';
import { computeCheapSet, assigneeLoads, overloadedAssignee } from '../src/model.js';
import { renderTreeHtml } from '../src/render.js';
import { formatWarning } from '../src/warnings.js';
/* Zuständigen-Engpass (SPEC §7/§9, D71): Trägt eine Person mehr als die Hälfte
der offenen Arbeit des günstigsten Pfads (Marginalkosten-Maß, D69) und gibt
es mindestens zwei Personen mit Last auf der offenen Front, wird gewarnt.
Knoten ohne Tags erben für die Rechnung vom nächsten getaggten Vorfahren;
mehrere Tags teilen sich den Beitrag. Kostenskala: XS=1 S=2 M=3 L=4 XL=5. */
const roots = txt => parse(txt).roots;
const plan = txt => { const r = roots(txt); return { r, set: computeCheapSet(r) }; };
const t = (key, vars) => {
let s = key;
if(vars) for(const k in vars) s += ':' + vars[k];
return s;
};
describe('overloadedAssignee — die Engpass-Entscheidung', () => {
it('eine Person über der Hälfte, eine zweite auf der Front: Warnung', () => {
const { r, set } = plan(`[ ] Plan
- [ ] A (L) @anna
- [ ] B (XS) @ben`);
/* anna 4, ben 1, Wurzel geschätzt L mit Marge 0 ⇒ total 5, anna 80 % */
expect(overloadedAssignee(r, set)).toEqual(
{tag: 'anna', share: 80, stations: 1, totalStations: 2});
});
it('ein Solo-Plan warnt nie — auch bei 100 %', () => {
const { r, set } = plan(`[ ] Plan
- [ ] A (L) @anna
- [ ] B (S) @anna`);
expect(overloadedAssignee(r, set)).toBeNull();
});
it('genau die Hälfte reicht nicht — die Schwelle ist strikt', () => {
const { r, set } = plan(`[ ] Plan
- [ ] A (L) @anna
- [ ] B (L) @ben`);
expect(overloadedAssignee(r, set)).toBeNull();
});
it('Knoten ohne Tags erben vom nächsten Vorfahren mit Tags', () => {
const { r, set } = plan(`[ ] Plan
- [ ] Paket (L) @anna
- [ ] Teil (M)
- [ ] B (S) @ben`);
/* Teil (3) gehört per Vererbung anna; dazu Paket-Marge 1 ⇒ anna 4 von 6.
Ohne Vererbung wäre anna nur die Marge 1 und niemand über der Hälfte. */
const o = overloadedAssignee(r, set);
expect(o && o.tag).toBe('anna');
expect(o.share).toBe(67);
});
it('mehrere Tags einer Zeile teilen sich den Beitrag', () => {
const { r, set } = plan(`[ ] Plan
- [ ] A (L) @anna @ben
- [ ] C (S) @carla`);
/* A: je 2 für anna und ben; voll doppelt gezählt läge anna bei 4 von 6
und würde fälschlich gemeldet. */
expect(overloadedAssignee(r, set)).toBeNull();
});
it('Erledigtes trägt keine Last — die offene Front zählt', () => {
const { r, set } = plan(`[ ] Plan
- [x] Big (XL) @ben
- [ ] A (S) @anna
- [ ] B (XS) @ben`);
/* bens großer Brocken ist fertig (0); offen sind anna 2 und ben 1 */
const o = overloadedAssignee(r, set);
expect(o && o.tag).toBe('anna');
});
it('nicht zugewiesene Arbeit verwässert die Anteile, warnt aber nicht', () => {
const { r, set } = plan(`[ ] Plan (XXL)
- [ ] A (S) @anna
- [ ] B (XS) @ben`);
/* Wurzel-Marge 3 gehört niemandem ⇒ anna 2 von 6, unter der Hälfte */
expect(overloadedAssignee(r, set)).toBeNull();
});
it('leere Pfadmenge (Pfad aus): keine Aussage', () => {
const r = roots(`[ ] Plan
- [ ] A (L) @anna
- [ ] B (XS) @ben`);
expect(overloadedAssignee(r, new Set())).toBeNull();
});
});
describe('assigneeLoads — das Maß dahinter', () => {
it('Marginalkosten je Person, Summe ist der Pfadpreis', () => {
const { r, set } = plan(`[ ] Plan
- [ ] A (L) @anna
- [ ] B (XS) @ben`);
const { loads, total } = assigneeLoads(r, set);
expect(loads.get('anna')).toBe(4);
expect(loads.get('ben')).toBe(1);
expect(total).toBe(5);
});
it('Stationen zählen nur offene Blätter des Pfads', () => {
const { r, set } = plan(`[ ] Plan
- [x] Fertig (S) @anna
- [ ] Offen (S) @anna
- [ ] B (XS) @ben`);
const { stations, totalStations } = assigneeLoads(r, set);
expect(stations.get('anna')).toBe(1);
expect(totalStations).toBe(2);
});
});
describe('Darstellung', () => {
it('die Pille der überlasteten Person wird an offenen Pfad-Knoten markiert', () => {
const { r, set } = plan(`[ ] Plan
- [ ] A (L) @anna
- [x] Done (S) @anna
- [ ] B (XS) @ben`);
const { html } = renderTreeHtml(r, {t, showDiscarded: false, cheapPath: true,
cheapSet: set, overloadTag: 'anna'});
/* genau eine markierte Pille: am offenen A — nicht am erledigten Done,
nicht an bens Pille */
expect(html.match(/tag overload/g)).toHaveLength(1);
expect(html).toContain('<span class="tag overload">anna</span>');
expect(html).toContain('<span class="tag">ben</span>');
});
it('ohne overloadTag bleibt alles wie bisher', () => {
const { r, set } = plan(`[ ] Plan
- [ ] A (L) @anna
- [ ] B (XS) @ben`);
const { html } = renderTreeHtml(r, {t, showDiscarded: false, cheapPath: true,
cheapSet: set, overloadTag: null});
expect(html).not.toContain('overload');
});
it('die Warnung nennt Person, Anteil und Stationen', () => {
const msg = formatWarning(
{type: 'assigneeOverload', tag: 'anna', share: 80, stations: 1, totalStations: 2}, t);
expect(msg).toBe('assigneeOverloadWarn:anna:80:1:2');
});
});