#!/bin/bash # Sync-Test: lokaler Server plus headless Shell, Änderungen in beide Richtungen: dev/tests/sync.sh DIR=$(cd "$(dirname "$0")/.." && pwd) ROOT=$(cd "$DIR/.." && pwd) E="$DIR/eval.sh" OUT=/tmp/desktop-app-nested/out.txt PORT=8041 URL="http://localhost:$PORT" SERVER_DATA=/tmp/desktop-app-sync-server AUTH="Authorization: Bearer test" FAILED=0 PRE='const ext = Main.extensionManager.lookup("desktop@marha.de").stateObj; const notes = ext._root.get_child_at_index(0); const todos = ext._root.get_child_at_index(1).get_child_at_index(1); const store = ext._store; const read = p => new TextDecoder().decode(GLib.file_get_contents(p)[1]); const exists = p => GLib.file_test(p, GLib.FileTest.EXISTS); const state = () => JSON.parse(read(store.dir + "/sync-state.json")); const St = imports.gi.St; const labels = a => { const out = []; const walk = n => { if (n instanceof St.Label && n.text) out.push(n.text); n.get_children().forEach(walk); }; walk(a); return out; }; ' val() { "$E" "$PRE GLib.file_set_contents('$OUT', String($1)); 'w'" | grep -v "^OK '\"w\"')$" cat "$OUT" } api() { curl -s -H "$AUTH" -H "Content-Type: application/json" "$@"; } now() { date -u +%Y-%m-%dT%H:%M:%S.000Z; } push() { api -X POST "$URL/api/sync" -d "$1" >/dev/null; } check() { local name=$1 actual=$2 expected=$3 if [ "$actual" == "$expected" ]; then echo "ok $name" else echo "FAIL $name"; echo " ist: $actual"; echo " soll: $expected" FAILED=$((FAILED + 1)) fi } cleanup() { kill $SERVER_PID 2>/dev/null "$DIR/stop.sh" pkill -f "^gnome-shell --(nested|headless)" 2>/dev/null wait 2>/dev/null } trap cleanup EXIT echo "=== Server starten" rm -rf "$SERVER_DATA" (cd "$ROOT/backend" && DATA_DIR="$SERVER_DATA" SYNC_TOKEN=test .venv/bin/uvicorn app.main:app --port $PORT > /tmp/desktop-app-sync-server.log 2>&1 &) for i in $(seq 1 30); do curl -s "$URL/api/health" >/dev/null && break; sleep 0.5; done SERVER_PID=$(pgrep -f "uvicorn app.main:app --port $PORT") check "Server läuft" "$(curl -s $URL/api/health)" '{"ok":true}' echo "=== Shell mit Sync starten" SYNC_URL="$URL" "$DIR/restart.sh" headless >/dev/null check "Sync aktiv" "$(val 'ext._sync.active')" "true" echo "=== Lokal → Server" "$E" "$PRE store.saveEvent({title: 'Zahnarzt', date: '2026-09-21', start: '09:00', end: '10:00', endDate: null}); store.saveTodo({title: 'Einkaufen', date: null, time: null}); 'ok'" >/dev/null sleep 3 check "Termin auf dem Server" "$(api "$URL/api/events" | python3 -c 'import json,sys; print([e["title"] for e in json.load(sys.stdin)])')" "['Zahnarzt']" check "Todo auf dem Server" "$(api "$URL/api/todos" | python3 -c 'import json,sys; print([t["title"] for t in json.load(sys.stdin)])')" "['Einkaufen']" check "Stand gemerkt" "$(val 'state().cursor > 0')" "true" echo "=== Server → lokal" push '{"todos": [{"id": "r1", "updated": "2026-09-19T10:00:00.000Z", "title": "Vom Handy", "date": null, "time": null, "done": false}], "notes": [{"id": "Remote/Idee.md", "updated": "2026-09-19T10:00:00.000Z", "content": "# Vom Server"}]}' sleep 3 check "Todo vom Server in der Liste" "$(val 'store.sortedTodos().map(t => t.title).sort().join(",")')" "Einkaufen,Vom Handy" check "Todo vom Server im Panel" "$(val 'labels(todos).includes("Vom Handy")')" "true" check "Notiz vom Server als Datei" "$(val 'exists(store.notesDir + "/Remote/Idee.md") + " " + JSON.stringify(read(store.notesDir + "/Remote/Idee.md"))')" 'true "# Vom Server"' check "Notiz vom Server im Baum" "$(val 'labels(notes).includes("▸ Remote")')" "true" check "Übernommene Notiz wird nicht zurückgeschickt" "$(val 'state().pushed.notes["Remote/Idee.md"]')" "2026-09-19T10:00:00.000Z" echo "=== Konflikt: neuester gewinnt" push '{"todos": [{"id": "r1", "updated": "2026-09-19T09:00:00.000Z", "title": "Älter", "date": null, "time": null, "done": false}]}' sleep 3 check "Älterer Stand verliert" "$(val 'store.todos.find(t => t.id === "r1").title')" "Vom Handy" push '{"todos": [{"id": "r1", "updated": "'$(now)'", "title": "Neuer", "date": null, "time": null, "done": false}]}' sleep 3 check "Neuerer Stand gewinnt" "$(val 'store.todos.find(t => t.id === "r1").title')" "Neuer" sleep 1 "$E" "$PRE store.toggleTodo('r1'); 'ok'" >/dev/null sleep 3 check "Lokale Änderung ist neuer und landet auf dem Server" "$(api "$URL/api/todos" | python3 -c 'import json,sys; print([t["done"] for t in json.load(sys.stdin) if t["id"] == "r1"])')" "[True]" push '{"todos": [{"id": "r1", "updated": "2026-09-19T09:30:00.000Z", "title": "Verspätet", "date": null, "time": null, "done": false}]}' sleep 3 check "Verspäteter älterer Stand ändert nichts" "$(val 'store.todos.find(t => t.id === "r1").title + " " + store.todos.find(t => t.id === "r1").done')" "Neuer true" echo "=== Löschen in beide Richtungen" "$E" "$PRE store.removeTodo(store.todos.find(t => t.title === 'Einkaufen').id); store.deleteNote(store.notesDir + '/Remote/Idee.md'); 'ok'" >/dev/null sleep 3 check "Gelöschtes Todo auf dem Server weg" "$(api "$URL/api/todos" | python3 -c 'import json,sys; print(sorted(t["title"] for t in json.load(sys.stdin)))')" "['Neuer']" check "Gelöschte Notiz auf dem Server weg" "$(api "$URL/api/notes" | python3 -c 'import json,sys; print([n["id"] for n in json.load(sys.stdin)])')" "[]" "$E" "$PRE store.createNote(store.notesDir, 'Lokal'); store.writeNote(store.notesDir + '/Lokal.md', 'lokal'); 'ok'" >/dev/null sleep 3 check "Lokale Notiz auf dem Server" "$(api "$URL/api/notes" | python3 -c 'import json,sys; print([(n["id"], n["content"]) for n in json.load(sys.stdin)])')" "[('Lokal.md', 'lokal')]" push '{"notes": [{"id": "Lokal.md", "updated": "'$(now)'", "deleted": true}]}' sleep 3 check "Vom Server gelöschte Notiz lokal weg" "$(val 'exists(store.notesDir + "/Lokal.md")')" "false" echo "=== Offene Notiz bleibt unangetastet" "$E" "$PRE store.createNote(store.notesDir, 'Offen'); store.writeNote(store.notesDir + '/Offen.md', 'meins'); notes._select({type: 'note', path: store.notesDir + '/Offen.md', name: 'Offen'}); 'ok'" >/dev/null sleep 3 "$E" "$PRE notes._startEdit(); 'ok'" >/dev/null CURSOR=$(val 'state().cursor') push '{"notes": [{"id": "Offen.md", "updated": "'$(now)'", "content": "vom server"}]}' sleep 3 check "Datei während des Tippens unverändert" "$(val 'read(store.notesDir + "/Offen.md") + " " + (state().cursor === '"$CURSOR"')')" "meins true" "$E" "$PRE notes._stopEdit(); 'ok'" >/dev/null sleep 3 check "Nach dem Schließen übernommen" "$(val 'read(store.notesDir + "/Offen.md") + " " + notes._content.text')" "vom server vom server" echo "=== Umbenennen" sleep 1 "$E" "$PRE store.renameNote(store.notesDir + '/Offen.md', 'Neu'); 'ok'" >/dev/null sleep 3 check "Umbenannt auf dem Server" "$(api "$URL/api/notes" | python3 -c 'import json,sys; print([n["id"] for n in json.load(sys.stdin)])')" "['Neu.md']" echo "=== Server weg und wieder da" kill $SERVER_PID; sleep 1 "$E" "$PRE store.saveTodo({title: 'Offline', date: null, time: null}); 'ok'" >/dev/null sleep 3 (cd "$ROOT/backend" && DATA_DIR="$SERVER_DATA" SYNC_TOKEN=test .venv/bin/uvicorn app.main:app --port $PORT >> /tmp/desktop-app-sync-server.log 2>&1 &) for i in $(seq 1 30); do curl -s "$URL/api/health" >/dev/null && break; sleep 0.5; done SERVER_PID=$(pgrep -f "uvicorn app.main:app --port $PORT") sleep 4 check "Offline-Änderung nachgeliefert" "$(api "$URL/api/todos" | python3 -c 'import json,sys; print(sorted(t["title"] for t in json.load(sys.stdin)))')" "['Neuer', 'Offline']" check "Keine JS-Fehler" "$(grep -c 'JS ERROR' /tmp/desktop-app-nested.log)" "0" echo if [ $FAILED -eq 0 ]; then echo "alle ok"; else echo "$FAILED FEHLER"; grep -n "JS ERROR" -A5 /tmp/desktop-app-nested.log | head -30; grep "desktop-app sync" /tmp/desktop-app-nested.log | tail -5; fi