Bis hierher galt eine Zweiteilung: Zustand im Deploymentcenter, Begruendung in docs/ROADMAP.md, verbunden ueber doc_anchor. Die Beschreibung der uebernommenen Eintraege sagte es woertlich - "Die Begruendung steht weiterhin in der Datei - hier steht nur der Zustand." Zwei Folgen, die erst im Betrieb sichtbar wurden: 1. Die Roadmap-Tafel kann die Begruendung nicht zeigen. Sie sieht die Datenbank; die Datei liegt auf einem Entwicklungsrechner. Im Seitenpanel stand deshalb nur doc_anchor als nicht anklickbarer Text mit dem Hinweis, er zeige auf etwas, das das WebUI nie zu sehen bekommt. 2. Schwerer: Die Beschreibung von dc_roadmap wies Agenten an, vor dem erneuten Vorschlagen eines zurueckgestellten Vorhabens "diesen Absatz" zu lesen. Ein Agent in polytrader oder clawddotnet kann ihn nicht lesen - er liegt in einem fremden Repository. Die Anweisung war nicht befolgbar. Dazu kam, dass cli/roadmap.php --check PHP braucht und auf dem Windows-Arbeitsplatz keines liegt: Die Haelfte der Pflegemaschinerie, die die Zweiteilung zusammenhalten sollte, lief dort nie. == Was sich aendert == bugtracker_items und roadmap_epics tragen rationale (Migration 020). Ein EIGENES Feld neben description, nicht statt dessen: description sagt, was gemeldet wurde, und kommt oft von einer fremden Maschine; rationale sagt, warum wir uns so entschieden haben. Zusammengelegt haette jede redaktionelle Ueberarbeitung den urspruenglichen Wortlaut der Meldung ueberschrieben, also die Grundlage, auf der entschieden wurde. Der Verlauf vermerkt nur, DASS die Begruendung sich geaendert hat und um wie viele Zeichen. Der ganze Absatz ein zweites Mal im Verlauf haette ihn nach der fuenften Ueberarbeitung unlesbar gemacht. == Der Weg herauf, einmalig == ?action=import_rationale liest eine mitgeschickte Roadmap-Datei. Drei Formen werden erkannt: Bereichsabschnitte, Vorhabensabschnitte und Tabellenzeilen. Die dritte ist kein Beiwerk - der Abschnitt "Verworfen" fuehrt seine zwoelf Ablehnungen als Tabelle, und ohne sie bliebe ausgerechnet das nicht am Item auffindbar, was am haeufigsten gebraucht wird: warum etwas schon einmal abgelehnt wurde. Ohne overwrite=true bleibt stehen, was im DC schon steht. == Der Weg herunter, dauerhaft == Ein zweites Markerpaar dc:rationale neben dc:roadmap. Bewusst ein zweites und keine Erweiterung des ersten: Zwischen den beiden steht in jeder gewachsenen Roadmap-Datei Handgeschriebenes - Einleitung, Leitlinie, Statuszeichen -, und ein einziger Block haette das beim naechsten Erzeugen verschluckt. ?action=render_file setzt beide Bloecke serverseitig ein, damit die Markersuche nur einmal umgesetzt ist. Sie hat eine Feinheit, die zweimal umgesetzt zweimal verschieden waere: Ein im Fliesstext ERWAEHNTER Marker darf nicht gefunden werden - die Einleitung dieser Datei erwaehnt jetzt beide. == Werkzeuge == scripts/dc.py roadmap-import und roadmap-write, in Python und nicht in cli/roadmap.php - sonst waeren sie auf dem Arbeitsplatz wieder nicht aufrufbar. roadmap-write behaelt die Zeilenenden der Datei bei. == Bestand == 35 Vorhaben und 6 Bereiche aus docs/ROADMAP.md uebernommen, 476 Zeilen handgepflegte Prosa durch den erzeugten Block ersetzt. Gegengeprueft auf Wortebene: Aus dem Fliesstext fehlt nichts, die einzigen Abweichungen sind die ersetzten Ueberschriften und die alte Einleitung. Kaertchen ohne hinterlegte Begruendung tragen auf der Tafel eine dezente Marke - erledigte nicht, dort waere sie ein Vorwurf ohne Adressaten. MCP-Adapter auf 1.3.0: dc_roadmap_set nimmt rationale, und die Beschreibung von dc_roadmap verweist nicht mehr auf eine Datei, die der Agent nicht erreichen kann. Geprueft: 7 neue Testfaelle, Suite gesamt 179 von 179. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
525 lines
21 KiB
Python
525 lines
21 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Roadmap-Tafel und zweite Stufe der Anmeldung.
|
|
|
|
Die Tafel wird ueber ihre Struktur geprueft und nicht ueber ihr Aussehen:
|
|
Der Server liefert Spalten, Spuren und Zustaende fertig aus, und genau das
|
|
ist der Vertrag, auf den sich WebUI, CLI und ein spaeterer Export stuetzen.
|
|
|
|
Die WebAuthn-Zeremonie selbst laeuft hier nicht mit. Sie braucht einen
|
|
Authenticator, der wirklich unterschreibt - dafuer gibt es ein eigenes
|
|
Werkzeug ausserhalb der Suite, weil es eine Krypto-Bibliothek verlangt und
|
|
diese Suite mit der Standardbibliothek auskommen soll. Was hier geprueft
|
|
wird, ist der Rahmen: dass die Endpunkte ohne Zwischenzustand nichts
|
|
herausgeben und dass ein sitzungsloser Aufruf abgewiesen wird.
|
|
"""
|
|
|
|
import time
|
|
|
|
from harness import (test, erwarte, erwarte_status, erwarte_erfolg,
|
|
erwarte_fehler, ueberspringen)
|
|
|
|
PROJEKT = 'myapp' # existiert nach dem Zuruecksetzen, siehe sql/schema.sql
|
|
|
|
|
|
def _item_id(resp):
|
|
for name in ('item_id', 'id'):
|
|
wert = resp.feld(name)
|
|
if isinstance(wert, int) and wert > 0:
|
|
return wert
|
|
return resp.feld('item', 'id')
|
|
|
|
|
|
def _melden(ctx, titel, ref):
|
|
return ctx.client.post('/api/bugtracker/v1/report', body={
|
|
'project_slug': PROJEKT,
|
|
'title': titel,
|
|
'description': 'Angelegt von tests/e2e (Tafel). Darf jederzeit geloescht werden.',
|
|
'severity': 'medium',
|
|
'environment': 'testing',
|
|
'client_ref': ref,
|
|
})
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Aufbau der Tafel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@test('tafel', 'Tafel liefert Spalten, Spuren und Kaertchen')
|
|
def _(ctx):
|
|
r = ctx.client.get('/api/bugtracker/v1/roadmap?action=board&project=' + PROJEKT)
|
|
erwarte_erfolg(r)
|
|
|
|
for feld in ('phases', 'lanes', 'items', 'stats', 'meta'):
|
|
erwarte(r.feld(feld) is not None, "Feld '%s' fehlt in der Antwort" % feld)
|
|
|
|
ctx['tafel'] = r.json
|
|
|
|
|
|
@test('tafel', 'Die beiden festen Randspalten sind immer da')
|
|
def _(ctx):
|
|
"""
|
|
"Fundament" links und "Wird ueberlegt" rechts haengen am Status und nicht
|
|
am Rang. Sie muessen auch dann stehen, wenn dazwischen gar nichts ist -
|
|
sonst haette ein frisches Projekt keine Tafel, sondern eine leere Flaeche.
|
|
"""
|
|
tafel = ctx.get('tafel')
|
|
if tafel is None:
|
|
ueberspringen('tafel')
|
|
|
|
schluessel = [p['key'] for p in tafel['phases']]
|
|
erwarte(schluessel[0] == 'fundament', "Erste Spalte ist '%s'" % schluessel[0])
|
|
erwarte(schluessel[-1] == 'ueberlegt', "Letzte Spalte ist '%s'" % schluessel[-1])
|
|
|
|
|
|
@test('tafel', 'Geratene Spaltengrenzen sind als solche gekennzeichnet')
|
|
def _(ctx):
|
|
"""
|
|
Ohne gepflegte roadmap_phases schneidet die Tafel selbst. Das darf sie -
|
|
aber sie muss es sagen. Eine geratene Grenze, die wie eine Entscheidung
|
|
aussieht, ist schlimmer als gar keine.
|
|
"""
|
|
tafel = ctx.get('tafel')
|
|
if tafel is None:
|
|
ueberspringen('tafel')
|
|
|
|
erwarte('phases_guessed' in tafel['meta'],
|
|
"meta.phases_guessed fehlt - die Tafel sagt nicht, woher ihre Grenzen kommen")
|
|
|
|
|
|
@test('tafel', 'Erledigte Vorhaben werden gekappt')
|
|
def _(ctx):
|
|
"""
|
|
Bei ueber hundert erledigten Eintraegen erschlaegt die Fundamentspalte
|
|
die ganze Tafel. done_limit muss greifen, und die Antwort muss die
|
|
Gesamtzahl trotzdem nennen - sonst sieht gekappt wie vollstaendig aus.
|
|
"""
|
|
r = ctx.client.get('/api/bugtracker/v1/roadmap?action=board&project=%s&done_limit=1' % PROJEKT)
|
|
erwarte_erfolg(r)
|
|
|
|
meta = r.feld('meta', default={})
|
|
erwarte('done_total' in meta and 'done_shown' in meta,
|
|
"meta nennt done_shown/done_total nicht")
|
|
erwarte(meta['done_shown'] <= 1,
|
|
"done_limit=1 zeigt %s erledigte Eintraege" % meta['done_shown'])
|
|
|
|
|
|
@test('tafel', 'Kaertchen tragen ihren Zustand und ihre Spalte')
|
|
def _(ctx):
|
|
tafel = ctx.get('tafel')
|
|
if tafel is None:
|
|
ueberspringen('tafel')
|
|
|
|
spalten = {p['key'] for p in tafel['phases']}
|
|
erlaubt = {'done', 'verification', 'working', 'deferred', 'idea', 'open'}
|
|
|
|
for karte in tafel['items']:
|
|
erwarte(karte['state'] in erlaubt,
|
|
"Unbekannter Zustand '%s' an #%s" % (karte['state'], karte['id']))
|
|
erwarte(karte['phase'] in spalten,
|
|
"Karte #%s zeigt auf die Spalte '%s', die es nicht gibt"
|
|
% (karte['id'], karte['phase']))
|
|
|
|
|
|
@test('tafel', 'Nachpruefung wird nicht als erledigt gezeichnet')
|
|
def _(ctx):
|
|
"""
|
|
Der Fall, den die Bastelloesung im Projekt clawddotnet von Hand als
|
|
"Status pruefen" danebenschreiben musste: Ein Fix, der nicht getragen
|
|
hat, ist keine Erledigung.
|
|
"""
|
|
r = _melden(ctx, 'Tafel-Testfall: wartet auf Nachpruefung', 'e2e-tafel-verify-%d' % time.time())
|
|
erwarte_status(r, 200, 201)
|
|
item = _item_id(r)
|
|
|
|
ctx.client.post('/api/bugtracker/v1/roadmap?action=set',
|
|
body={'id': item, 'epic': 'T', 'roadmap_rank': 10})
|
|
ctx.client.post('/api/bugtracker/v1/manage/?action=status',
|
|
body={'id': item, 'status': 'awaiting_verification'})
|
|
|
|
r = ctx.client.get('/api/bugtracker/v1/roadmap?action=board&project=' + PROJEKT)
|
|
erwarte_erfolg(r)
|
|
|
|
treffer = [k for k in r.feld('items', default=[]) if k['id'] == item]
|
|
erwarte(treffer, "Item #%d taucht auf der Tafel nicht auf" % item)
|
|
erwarte(treffer[0]['state'] == 'verification',
|
|
"Zustand ist '%s' statt 'verification'" % treffer[0]['state'])
|
|
|
|
ctx['tafel_item'] = item
|
|
|
|
|
|
@test('tafel', 'Vorhaben ohne Rang landen in "Wird ueberlegt"')
|
|
def _(ctx):
|
|
"""
|
|
Ein Vorhaben ohne Rang ist nicht eingeordnet. Es ans Ende einer Spalte zu
|
|
sortieren liesse es eingeordnet aussehen.
|
|
"""
|
|
r = _melden(ctx, 'Tafel-Testfall: noch nicht eingeordnet', 'e2e-tafel-ohne-%d' % time.time())
|
|
erwarte_status(r, 200, 201)
|
|
item = _item_id(r)
|
|
|
|
ctx.client.post('/api/bugtracker/v1/roadmap?action=set', body={'id': item, 'epic': 'T'})
|
|
|
|
r = ctx.client.get('/api/bugtracker/v1/roadmap?action=board&project=' + PROJEKT)
|
|
treffer = [k for k in r.feld('items', default=[]) if k['id'] == item]
|
|
erwarte(treffer, "Item #%d taucht auf der Tafel nicht auf" % item)
|
|
erwarte(treffer[0]['phase'] == 'ueberlegt',
|
|
"Ungereihtes Item steht in Spalte '%s'" % treffer[0]['phase'])
|
|
|
|
|
|
@test('tafel', 'Tafel verlangt ein einzelnes Projekt')
|
|
def _(ctx):
|
|
"""
|
|
Die Phasen werden aus roadmap_rank geschnitten, und der Rang gilt je
|
|
Projekt. Zwei Projekte uebereinanderzulegen ergaebe Spalten, die in
|
|
keinem von beiden stimmen.
|
|
"""
|
|
r = ctx.client.get('/api/bugtracker/v1/roadmap?action=board&project=all')
|
|
erwarte_fehler(r, http=400)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Einsortieren - der Befund aus #177
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@test('tafel', 'rank_between setzt den Eintrag zwischen seine Nachbarn')
|
|
def _(ctx):
|
|
stempel = int(time.time())
|
|
ids = []
|
|
for i, rang in enumerate((100, 200, 300)):
|
|
r = _melden(ctx, 'Tafel-Testfall: Rangnachbar %d' % i, 'e2e-tafel-rang-%d-%d' % (stempel, i))
|
|
erwarte_status(r, 200, 201)
|
|
item = _item_id(r)
|
|
ctx.client.post('/api/bugtracker/v1/roadmap?action=set',
|
|
body={'id': item, 'epic': 'T', 'roadmap_rank': rang})
|
|
ids.append(item)
|
|
|
|
r = ctx.client.post('/api/bugtracker/v1/roadmap?action=rank_between',
|
|
body={'id': ids[2], 'prev_id': ids[0], 'next_id': ids[1]})
|
|
erwarte_erfolg(r)
|
|
|
|
rang = r.feld('rank')
|
|
erwarte(100 < rang < 200, "Rang %s liegt nicht zwischen 100 und 200" % rang)
|
|
erwarte(r.feld('renumbered') is False,
|
|
"Ohne Not wurde neu nummeriert - das ist genau das, wovor #177 warnt")
|
|
|
|
ctx['tafel_ids'] = ids
|
|
|
|
|
|
@test('tafel', 'Kein Platz: einmal neu nummerieren, und es steht in der Antwort')
|
|
def _(ctx):
|
|
"""
|
|
Zwischen zwei direkt benachbarten Raengen ist kein Platz. Dann darf neu
|
|
nummeriert werden - aber nicht stillschweigend.
|
|
"""
|
|
ids = ctx.get('tafel_ids')
|
|
if not ids:
|
|
ueberspringen('tafel_ids')
|
|
|
|
# Zwei Eintraege direkt nebeneinander setzen
|
|
ctx.client.post('/api/bugtracker/v1/roadmap?action=set',
|
|
body={'id': ids[0], 'roadmap_rank': 500})
|
|
ctx.client.post('/api/bugtracker/v1/roadmap?action=set',
|
|
body={'id': ids[1], 'roadmap_rank': 501})
|
|
|
|
r = ctx.client.post('/api/bugtracker/v1/roadmap?action=rank_between',
|
|
body={'id': ids[2], 'prev_id': ids[0], 'next_id': ids[1]})
|
|
erwarte_erfolg(r)
|
|
erwarte(r.feld('renumbered') is True,
|
|
"Zwischen 500 und 501 war Platz? renumbered=%s" % r.feld('renumbered'))
|
|
|
|
# Und danach liegt der Eintrag wirklich dazwischen.
|
|
r = ctx.client.get('/api/bugtracker/v1/roadmap?action=board&project=' + PROJEKT)
|
|
raenge = {k['id']: k['rank'] for k in r.feld('items', default=[])}
|
|
erwarte(raenge.get(ids[0]) < raenge.get(ids[2]) < raenge.get(ids[1]),
|
|
"Nach dem Neunummerieren: %s / %s / %s"
|
|
% (raenge.get(ids[0]), raenge.get(ids[2]), raenge.get(ids[1])))
|
|
|
|
|
|
@test('tafel', 'move tauscht mit dem projektweiten Nachbarn (#177)')
|
|
def _(ctx):
|
|
"""
|
|
Der Nachbar wird projektweit gesucht, nicht im eigenen Bereich. Zuvor
|
|
sprang ein Eintrag bei einem Klick ueber beliebig viele fremde Vorhaben
|
|
hinweg, obwohl die Geste "eine Position" hiess.
|
|
"""
|
|
r = ctx.client.get('/api/bugtracker/v1/roadmap?action=board&project=' + PROJEKT)
|
|
erwarte_erfolg(r)
|
|
|
|
gereiht = sorted(
|
|
[k for k in r.feld('items', default=[]) if k['rank'] is not None and k['state'] != 'done'],
|
|
key=lambda k: (k['rank'], k['id'])
|
|
)
|
|
if len(gereiht) < 3:
|
|
ueberspringen('zu wenige gereihte Eintraege')
|
|
|
|
mitte, davor = gereiht[1], gereiht[0]
|
|
|
|
r = ctx.client.post('/api/bugtracker/v1/roadmap?action=move',
|
|
body={'id': mitte['id'], 'direction': 'up'})
|
|
erwarte_erfolg(r)
|
|
|
|
r = ctx.client.get('/api/bugtracker/v1/roadmap?action=board&project=' + PROJEKT)
|
|
raenge = {k['id']: k['rank'] for k in r.feld('items', default=[])}
|
|
|
|
erwarte(raenge.get(mitte['id']) == davor['rank'] and raenge.get(davor['id']) == mitte['rank'],
|
|
"Raenge nicht getauscht: #%s %s->%s, #%s %s->%s"
|
|
% (mitte['id'], mitte['rank'], raenge.get(mitte['id']),
|
|
davor['id'], davor['rank'], raenge.get(davor['id'])))
|
|
|
|
|
|
@test('tafel', 'Themenbereich traegt Farbe und Hervorhebung')
|
|
def _(ctx):
|
|
r = ctx.client.post('/api/bugtracker/v1/roadmap?action=epic', body={
|
|
'project_slug': PROJEKT, 'epic': 'T', 'title': 'Tafel-Testbereich',
|
|
'summary': 'Von tests/e2e angelegt.', 'sort_order': 900,
|
|
'color': '#F6465D', 'highlight': 1,
|
|
})
|
|
erwarte_erfolg(r)
|
|
|
|
r = ctx.client.get('/api/bugtracker/v1/roadmap?action=board&project=' + PROJEKT)
|
|
spuren = {l['epic']: l for l in r.feld('lanes', default=[])}
|
|
erwarte('T' in spuren, "Spur 'T' fehlt auf der Tafel")
|
|
erwarte(spuren['T']['highlight'] is True, "Hervorhebung nicht uebernommen")
|
|
erwarte(spuren['T']['color'] == '#F6465D', "Farbe ist '%s'" % spuren['T']['color'])
|
|
|
|
|
|
@test('tafel', 'Ein Aufruf ohne diese Felder loescht Farbe und Hervorhebung nicht')
|
|
def _(ctx):
|
|
"""
|
|
Das bestehende Formular im WebUI kennt color und highlight nicht. Wuerde
|
|
ein Aufruf ohne sie die Werte zuruecksetzen, waere jede Titelkorrektur
|
|
ein stiller Verlust.
|
|
"""
|
|
r = ctx.client.post('/api/bugtracker/v1/roadmap?action=epic', body={
|
|
'project_slug': PROJEKT, 'epic': 'T', 'title': 'Tafel-Testbereich (umbenannt)',
|
|
'sort_order': 900,
|
|
})
|
|
erwarte_erfolg(r)
|
|
|
|
r = ctx.client.get('/api/bugtracker/v1/roadmap?action=board&project=' + PROJEKT)
|
|
spuren = {l['epic']: l for l in r.feld('lanes', default=[])}
|
|
erwarte(spuren.get('T', {}).get('highlight') is True,
|
|
"Hervorhebung ging bei einem Aufruf ohne das Feld verloren")
|
|
erwarte(spuren.get('T', {}).get('color') == '#F6465D',
|
|
"Farbe ging bei einem Aufruf ohne das Feld verloren")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Zweite Stufe der Anmeldung
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@test('tafel', 'WebAuthn-Endpunkt gibt ohne offene Anmeldung nichts heraus')
|
|
def _(ctx):
|
|
"""
|
|
login_options laeuft im Zwischenzustand zwischen Passwort und zweitem
|
|
Faktor. Ohne diesen Zustand darf der Endpunkt nicht verraten, ob es ein
|
|
Konto gibt oder welche Schluessel dazu hinterlegt sind.
|
|
"""
|
|
r = ctx.client.post('/api/auth/v1/?action=login_options', body={}, token=None)
|
|
erwarte_status(r, 401)
|
|
erwarte(r.feld('error', 'code') == 'no_pending_login',
|
|
"Fehlercode ist '%s'" % r.feld('error', 'code'))
|
|
|
|
|
|
@test('tafel', 'Registrieren verlangt eine Anmeldung')
|
|
def _(ctx):
|
|
r = ctx.client.post('/api/auth/v1/?action=register_options', body={}, token=None)
|
|
erwarte_status(r, 401)
|
|
|
|
|
|
@test('tafel', 'Unbekannte Zeremonie wird abgewiesen')
|
|
def _(ctx):
|
|
r = ctx.client.post('/api/auth/v1/?action=rundgang', body={}, token=None)
|
|
erwarte_status(r, 404)
|
|
erwarte(isinstance(r.feld('error', 'available'), list),
|
|
"Die Absage nennt die moeglichen Aktionen nicht")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Begruendungen
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@test('tafel', 'Begruendung laesst sich setzen und kommt zurueck')
|
|
def _(ctx):
|
|
r = _melden(ctx, 'Tafel-Testfall: mit Begruendung', 'e2e-tafel-grund-%d' % time.time())
|
|
erwarte_status(r, 200, 201)
|
|
item = _item_id(r)
|
|
|
|
text = 'Warum so und nicht anders:\n\nWeil die Alternative teurer waere.'
|
|
r = ctx.client.post('/api/bugtracker/v1/roadmap?action=set',
|
|
body={'id': item, 'epic': 'T', 'roadmap_rank': 700, 'rationale': text})
|
|
erwarte_erfolg(r)
|
|
|
|
r = ctx.client.get('/api/bugtracker/v1/manage/?action=get&id=%d' % item)
|
|
erwarte_erfolg(r)
|
|
erwarte(r.feld('item', 'rationale') == text,
|
|
"Begruendung kam anders zurueck: %r" % r.feld('item', 'rationale'))
|
|
erwarte(r.feld('item', 'rationale_updated_at') is not None,
|
|
"rationale_updated_at wurde nicht gesetzt")
|
|
|
|
ctx['tafel_grund_item'] = item
|
|
|
|
|
|
@test('tafel', 'Der Verlauf traegt nicht den ganzen Begruendungstext')
|
|
def _(ctx):
|
|
"""
|
|
Eine Begruendung kann einige tausend Zeichen lang sein. Stuende sie bei
|
|
jeder Ueberarbeitung noch einmal im Verlauf, waere der Verlauf nach der
|
|
fuenften unlesbar - und damit als Werkzeug wertlos.
|
|
"""
|
|
item = ctx.get('tafel_grund_item')
|
|
if not item:
|
|
ueberspringen('tafel_grund_item')
|
|
|
|
langer_text = 'Sehr ausfuehrliche Begruendung. ' * 40
|
|
r = ctx.client.post('/api/bugtracker/v1/roadmap?action=set',
|
|
body={'id': item, 'rationale': langer_text})
|
|
erwarte_erfolg(r)
|
|
|
|
r = ctx.client.get('/api/bugtracker/v1/manage/?action=get&id=%d' % item)
|
|
verlauf = ' '.join(k.get('comment', '') for k in r.feld('item', 'comments', default=[]))
|
|
|
|
erwarte('Sehr ausfuehrliche Begruendung. Sehr ausfuehrliche' not in verlauf,
|
|
"Der ganze Text steht im Verlauf")
|
|
erwarte('Begruendung ueberarbeitet' in verlauf,
|
|
"Die Aenderung wurde gar nicht vermerkt")
|
|
|
|
|
|
@test('tafel', 'Die Tafel meldet, ob eine Begruendung hinterlegt ist')
|
|
def _(ctx):
|
|
item = ctx.get('tafel_grund_item')
|
|
if not item:
|
|
ueberspringen('tafel_grund_item')
|
|
|
|
r = ctx.client.get('/api/bugtracker/v1/roadmap?action=board&project=' + PROJEKT)
|
|
erwarte_erfolg(r)
|
|
|
|
karten = {k['id']: k for k in r.feld('items', default=[])}
|
|
erwarte(item in karten, "Item #%d fehlt auf der Tafel" % item)
|
|
erwarte(karten[item].get('has_rationale') is True,
|
|
"has_rationale ist %r" % karten[item].get('has_rationale'))
|
|
|
|
ohne = [k for k in karten.values() if k.get('has_rationale') is False]
|
|
erwarte(ohne, "Kein einziges Kaertchen ohne Begruendung - der Fall wird nicht geprueft")
|
|
|
|
|
|
@test('tafel', 'Einlesen erkennt Absaetze UND Tabellenzeilen')
|
|
def _(ctx):
|
|
"""
|
|
Der Abschnitt "Verworfen" fuehrt seine Ablehnungen als Tabelle, nicht als
|
|
Absaetze. Bliebe die Tabelle beim Bereich stecken, waere ausgerechnet das
|
|
nicht am Item auffindbar, was am haeufigsten gebraucht wird: warum etwas
|
|
schon einmal abgelehnt wurde.
|
|
"""
|
|
stempel = int(time.time())
|
|
ids = []
|
|
for i in range(2):
|
|
r = _melden(ctx, 'Tafel-Testfall: Einlesen %d' % i, 'e2e-tafel-imp-%d-%d' % (stempel, i))
|
|
erwarte_status(r, 200, 201)
|
|
item = _item_id(r)
|
|
ctx.client.post('/api/bugtracker/v1/roadmap?action=set', body={'id': item, 'epic': 'T'})
|
|
ids.append(item)
|
|
|
|
datei = "\n".join([
|
|
"# Fahrplan", "",
|
|
"## T - Tafel-Testbereich", "",
|
|
"Dieser Bereich traegt seine eigene Begruendung.", "",
|
|
"### [T] #%d - Erster" % ids[0], "",
|
|
"Der Absatz zum ersten Vorhaben.", "",
|
|
"## Verworfen", "",
|
|
"| | Vorschlag | Warum nicht |",
|
|
"|---|---|---|",
|
|
"| #%d | **Zweiter** | Der Aufwand stand in keinem Verhaeltnis. |" % ids[1],
|
|
"",
|
|
])
|
|
|
|
r = ctx.client.post('/api/bugtracker/v1/roadmap?action=import_rationale',
|
|
body={'project_slug': PROJEKT, 'content': datei})
|
|
erwarte_erfolg(r)
|
|
|
|
r = ctx.client.get('/api/bugtracker/v1/manage/?action=get&id=%d' % ids[0])
|
|
erwarte(r.feld('item', 'rationale') == 'Der Absatz zum ersten Vorhaben.',
|
|
"Absatz nicht uebernommen: %r" % r.feld('item', 'rationale'))
|
|
|
|
r = ctx.client.get('/api/bugtracker/v1/manage/?action=get&id=%d' % ids[1])
|
|
erwarte(r.feld('item', 'rationale') == 'Der Aufwand stand in keinem Verhaeltnis.',
|
|
"Tabellenzeile nicht uebernommen: %r" % r.feld('item', 'rationale'))
|
|
|
|
ctx['tafel_imp_ids'] = ids
|
|
|
|
|
|
@test('tafel', 'Ein zweiter Lauf ueberschreibt nichts ohne Ansage')
|
|
def _(ctx):
|
|
"""
|
|
Wer eine Begruendung im Deploymentcenter ueberarbeitet hat, soll sie nicht
|
|
beim naechsten Einlesen stillschweigend auf den Stand der Datei
|
|
zurueckgesetzt bekommen.
|
|
"""
|
|
ids = ctx.get('tafel_imp_ids')
|
|
if not ids:
|
|
ueberspringen('tafel_imp_ids')
|
|
|
|
ueberarbeitet = 'Im Deploymentcenter ueberarbeitet.'
|
|
ctx.client.post('/api/bugtracker/v1/roadmap?action=set',
|
|
body={'id': ids[0], 'rationale': ueberarbeitet})
|
|
|
|
datei = "\n".join([
|
|
"## T - Tafel-Testbereich", "",
|
|
"### [T] #%d - Erster" % ids[0], "",
|
|
"Der alte Absatz aus der Datei.", "",
|
|
])
|
|
|
|
r = ctx.client.post('/api/bugtracker/v1/roadmap?action=import_rationale',
|
|
body={'project_slug': PROJEKT, 'content': datei})
|
|
erwarte_erfolg(r)
|
|
|
|
r = ctx.client.get('/api/bugtracker/v1/manage/?action=get&id=%d' % ids[0])
|
|
erwarte(r.feld('item', 'rationale') == ueberarbeitet,
|
|
"Der Stand der Datei hat gewonnen: %r" % r.feld('item', 'rationale'))
|
|
|
|
# Mit Ansage darf er gewinnen.
|
|
r = ctx.client.post('/api/bugtracker/v1/roadmap?action=import_rationale',
|
|
body={'project_slug': PROJEKT, 'content': datei, 'overwrite': True})
|
|
erwarte_erfolg(r)
|
|
|
|
r = ctx.client.get('/api/bugtracker/v1/manage/?action=get&id=%d' % ids[0])
|
|
erwarte(r.feld('item', 'rationale') == 'Der alte Absatz aus der Datei.',
|
|
"overwrite=true hat nicht gewirkt: %r" % r.feld('item', 'rationale'))
|
|
|
|
|
|
@test('tafel', 'Die Datei wird erzeugt, Handgeschriebenes bleibt stehen')
|
|
def _(ctx):
|
|
"""
|
|
Zwei erzeugte Bloecke, und alles ausserhalb bleibt unberuehrt. Genau das
|
|
ist die Zusage, auf die sich jeder verlaesst, der in dieser Datei noch
|
|
etwas von Hand schreibt.
|
|
"""
|
|
datei = "\n".join([
|
|
"# Fahrplan", "",
|
|
"Diese Einleitung ist von Hand und soll das ueberleben.", "",
|
|
"<!-- dc:rationale stand=alt items=0 projekt=x -->",
|
|
"Alter erzeugter Inhalt, der ersetzt gehoert.",
|
|
"<!-- /dc:rationale -->", "",
|
|
"## Pflege dieser Datei", "",
|
|
"Auch dieser Abschnitt ist von Hand.", "",
|
|
])
|
|
|
|
r = ctx.client.post('/api/bugtracker/v1/roadmap?action=render_file',
|
|
body={'project_slug': PROJEKT, 'content': datei})
|
|
erwarte_erfolg(r)
|
|
|
|
neu = r.feld('markdown', default='')
|
|
|
|
erwarte('Diese Einleitung ist von Hand' in neu, "Die Einleitung wurde ueberschrieben")
|
|
erwarte('Auch dieser Abschnitt ist von Hand' in neu, "Der Fussabschnitt wurde ueberschrieben")
|
|
erwarte('Alter erzeugter Inhalt' not in neu, "Der alte Block steht noch da")
|
|
erwarte(neu.count('<!-- dc:rationale') == 1, "Der Begruendungsblock steht mehrfach")
|
|
erwarte(r.feld('changed') is True, "changed meldet keine Aenderung")
|
|
|
|
|
|
@test('tafel', 'render verlangt einen bekannten Teil')
|
|
def _(ctx):
|
|
r = ctx.client.get('/api/bugtracker/v1/roadmap?action=render&project=%s&part=quatsch' % PROJEKT)
|
|
erwarte_fehler(r, http=400)
|