update
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// Tests der reinen Module: gjs -m dev/tests/run.js
|
||||
import * as D from '../../desktop@marha.de/dates.js';
|
||||
import {continueList, renderMarkdown} from '../../desktop@marha.de/markdown.js';
|
||||
import {batches, fileStamp, planPull, planPush, stampToSeconds} from '../../desktop@marha.de/files.js';
|
||||
import {batches, concat, fileStamp, planPull, planPush, stampToSeconds, tailHash, uploadItem} from '../../desktop@marha.de/files.js';
|
||||
|
||||
let failed = 0;
|
||||
function eq(actual, expected, name) {
|
||||
@@ -75,9 +75,19 @@ const H = 3600, NOW = 1_800_000_000;
|
||||
eq(fileStamp(NOW), '2027-01-15T08:00:00.000Z', 'fileStamp');
|
||||
eq(stampToSeconds('2027-01-15T08:00:00.000Z'), NOW, 'stampToSeconds');
|
||||
const local = new Map([['a/s.jsonl', NOW - 2 * H], ['a/frisch.jsonl', NOW - 10], ['m/MEMORY.md', NOW - 3 * H]]);
|
||||
eq(planPush(local, {'m/MEMORY.md': fileStamp(NOW - 3 * H), 'weg.md': fileStamp(NOW - 5 * H), 'alt.md': null}, NOW, H),
|
||||
eq(planPush(local, {'m/MEMORY.md': {updated: fileStamp(NOW - 3 * H), size: 5, hash: 'x'}, 'weg.md': fileStamp(NOW - 5 * H), 'alt.md': null}, NOW, H),
|
||||
{upload: [{id: 'a/s.jsonl', updated: fileStamp(NOW - 2 * H)}], remove: ['weg.md']},
|
||||
'planPush: nur ruhende, geänderte Dateien hoch, Verschwundene als Tombstone, Gemeldete nicht nochmal');
|
||||
const enc = s => new TextEncoder().encode(s);
|
||||
eq(tailHash(enc('abc')), 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad', 'tailHash = SHA-256 bei kleinen Dateien');
|
||||
eq(tailHash(enc('x'.repeat(70000) + 'abc')), tailHash(enc('y'.repeat(70000) + 'x'.repeat(65533) + 'abc')), 'tailHash sieht nur die letzten 64 KB');
|
||||
const known = {updated: 'u', size: 4, hash: tailHash(enc('a\nb\n'))};
|
||||
eq(uploadItem('s', 'v', enc('a\nb\nc\n'), known), {id: 's', updated: 'v', base: {size: 4, hash: known.hash}, append: 'c\n'}, 'uploadItem: nur der Rest, wenn der Anfang stimmt');
|
||||
eq(uploadItem('s', 'v', enc('x\nb\nc\n'), known), {id: 's', updated: 'v', content: 'x\nb\nc\n'}, 'uploadItem: ganze Datei bei anderem Anfang');
|
||||
eq(uploadItem('s', 'v', enc('a\n'), known), {id: 's', updated: 'v', content: 'a\n'}, 'uploadItem: ganze Datei, wenn kürzer');
|
||||
eq(uploadItem('s', 'v', enc('a\nb\nc\n'), 'u'), {id: 's', updated: 'v', content: 'a\nb\nc\n'}, 'uploadItem: alter Stand ohne Größe → ganz');
|
||||
eq(uploadItem('s', 'v', new Uint8Array([0xff, 0xfe]), null), null, 'uploadItem: kein UTF-8 → nichts');
|
||||
eq(Array.from(concat(new Uint8Array([1, 2]), new Uint8Array([3]))), [1, 2, 3], 'concat');
|
||||
eq(planPull([
|
||||
{id: 'a/s.jsonl', updated: fileStamp(NOW - 1 * H), content: 'x'},
|
||||
{id: 'a/s.jsonl', updated: fileStamp(NOW - 3 * H), content: 'x'},
|
||||
|
||||
@@ -46,8 +46,7 @@ trap cleanup EXIT
|
||||
|
||||
echo "=== Server starten"
|
||||
rm -rf "$SERVER_DATA"
|
||||
# FILES_PAGE_BYTES winzig: Dateien kommen einzeln, der Client muss die Seiten nachholen
|
||||
(cd "$ROOT/backend" && DATA_DIR="$SERVER_DATA" SYNC_TOKEN=test FILES_PAGE_BYTES=10 .venv/bin/uvicorn app.main:app --port $PORT > /tmp/desktop-app-sync-server.log 2>&1 &)
|
||||
(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}'
|
||||
@@ -156,45 +155,45 @@ check "Lokal gelöschter Ordner auf dem Server als gelöscht" "$(folders | pytho
|
||||
|
||||
echo "=== Dateien: Claude-Sitzungen zwischen PCs"
|
||||
FILES=/tmp/desktop-app-nested/claude-projects
|
||||
# Alle Dateien vom Server, seitenweise wie der Client
|
||||
allfiles() { python3 - "$URL" <<'PY'
|
||||
import json, sys, urllib.request
|
||||
url, since, out = sys.argv[1], 0, []
|
||||
while True:
|
||||
req = urllib.request.Request(url + "/api/sync", data=json.dumps({"since": since, "files": []}).encode(), headers={"Authorization": "Bearer test", "Content-Type": "application/json"})
|
||||
d = json.load(urllib.request.urlopen(req))
|
||||
out += d["files"]; since = d["cursor"]
|
||||
if not d.get("more"): break
|
||||
print(json.dumps(out))
|
||||
PY
|
||||
}
|
||||
files() { allfiles | python3 -c 'import json,sys; print(sorted((f["id"], bool(f.get("deleted"))) for f in json.load(sys.stdin)))'; }
|
||||
SFILES=$SERVER_DATA/files
|
||||
files() { api -X POST "$URL/api/sync" -d '{"since": 0, "files": []}' | python3 -c 'import json,sys; print(sorted((f["id"], bool(f.get("deleted"))) for f in json.load(sys.stdin)["files"]))'; }
|
||||
same() { cmp -s "$SFILES/$1" "$FILES/$1" && echo gleich || echo verschieden; }
|
||||
# Lauf wie vom Timer: Ruhezeit gilt
|
||||
timercycle() { "$E" "$PRE ext._sync._filesCycle(); 'ok'" >/dev/null; sleep 3; }
|
||||
# Wie make sync-files, nur auf dem Bus der Test-Shell: erzwungen, wartet, bis der Lauf durch ist.
|
||||
filescycle() { DBUS_SESSION_BUS_ADDRESS=$(cat /tmp/desktop-app-nested/bus) gdbus call --session --dest org.gnome.Shell --object-path /de/marha/desktop --method de.marha.Desktop.SyncFiles | sed -e "s/^('//" -e "s/',)$//"; }
|
||||
mkdir -p "$FILES/-home-x/memory"
|
||||
echo '{"a":1}' > "$FILES/-home-x/alt.jsonl"; touch -d '2 hours ago' "$FILES/-home-x/alt.jsonl"
|
||||
echo '# Memo' > "$FILES/-home-x/memory/MEMORY.md"; touch -d '90 minutes ago' "$FILES/-home-x/memory/MEMORY.md"
|
||||
echo 'läuft noch' > "$FILES/-home-x/frisch.jsonl"
|
||||
printf '{"a":1}\n' > "$FILES/-home-x/alt.jsonl"; touch -d '2 hours ago' "$FILES/-home-x/alt.jsonl"
|
||||
printf '# Memo\n' > "$FILES/-home-x/memory/MEMORY.md"; touch -d '90 minutes ago' "$FILES/-home-x/memory/MEMORY.md"
|
||||
printf 'läuft noch\n' > "$FILES/-home-x/frisch.jsonl"
|
||||
timercycle
|
||||
check "Timer-Lauf: ruhende Dateien auf dem Server, frische nicht" "$(files)" "[('-home-x/alt.jsonl', False), ('-home-x/memory/MEMORY.md', False)]"
|
||||
check "Inhalt liegt als Datei auf dem Server" "$(same -home-x/alt.jsonl) $(same -home-x/memory/MEMORY.md)" "gleich gleich"
|
||||
check "Takt nach Übertragung wie beim Kalender" "$(val 'ext._sync.interval')" "1"
|
||||
check "make sync-files erzwingt auch die frische Datei" "$(filescycle)" "hoch 1, gelöscht gemeldet 0, runter 0, lokal gelöscht 0"
|
||||
check "Frische Datei jetzt auf dem Server" "$(files)" "[('-home-x/alt.jsonl', False), ('-home-x/frisch.jsonl', False), ('-home-x/memory/MEMORY.md', False)]"
|
||||
check "Normale Anfrage bekommt keine Dateien" "$(api -X POST "$URL/api/sync" -d '{"since": 0}' | python3 -c 'import json,sys; d=json.load(sys.stdin); print("files" in d, "events" in d)')" "False True"
|
||||
check "Stand der Dateien getrennt gemerkt" "$(val 'Object.keys(state().files.pushed).sort().join(",") + " " + (state().files.cursor > 0)')" "-home-x/alt.jsonl,-home-x/frisch.jsonl,-home-x/memory/MEMORY.md true"
|
||||
push '{"files": [{"id": "-home-y/s2.jsonl", "updated": "2026-09-01T00:00:00.000Z", "content": "vom anderen PC"}]}'
|
||||
check "Stand der Dateien mit Größe gemerkt" "$(val 'Object.keys(state().files.pushed).sort().join(",") + " " + (state().files.cursor > 0) + " " + state().files.pushed["-home-x/alt.jsonl"].size')" "-home-x/alt.jsonl,-home-x/frisch.jsonl,-home-x/memory/MEMORY.md true 8"
|
||||
printf '{"a":2}\n' >> "$FILES/-home-x/alt.jsonl"; touch -d '2 minutes ago' "$FILES/-home-x/alt.jsonl"
|
||||
timercycle
|
||||
check "Angehängte Zeile nach einer Minute Ruhe auf dem Server" "$(same -home-x/alt.jsonl) $(val 'state().files.pushed["-home-x/alt.jsonl"].size')" "gleich 16"
|
||||
printf 'neu\n' > "$FILES/-home-x/alt.jsonl"; touch -d '2 minutes ago' "$FILES/-home-x/alt.jsonl"
|
||||
timercycle
|
||||
check "Umgeschriebene Datei ganz ersetzt" "$(same -home-x/alt.jsonl) $(cat "$SFILES/-home-x/alt.jsonl")" "gleich neu"
|
||||
push '{"files": [{"id": "-home-y/s2.jsonl", "updated": "2026-09-01T00:00:00.000Z", "content": "vom anderen PC\n"}]}'
|
||||
check "Abgleich holt die Datei" "$(filescycle)" "hoch 0, gelöscht gemeldet 0, runter 1, lokal gelöscht 0"
|
||||
check "Datei vom Server geschrieben, Änderungszeit übernommen" "$(cat "$FILES/-home-y/s2.jsonl") $(stat -c %Y "$FILES/-home-y/s2.jsonl")" "vom anderen PC $(date -u -d 2026-09-01T00:00:00Z +%s)"
|
||||
echo '{"a":2}' > "$FILES/-home-x/alt.jsonl"; touch -d '70 minutes ago' "$FILES/-home-x/alt.jsonl"
|
||||
H=$(printf 'vom anderen PC\n' | sha256sum | cut -d' ' -f1)
|
||||
push '{"files": [{"id": "-home-y/s2.jsonl", "updated": "2026-09-01T01:00:00.000Z", "base": {"size": 15, "hash": "'$H'"}, "append": "zweite Zeile\n"}]}'
|
||||
check "Anhang vom Server kommt nur als Rest" "$(filescycle) | $(tr '\n' '|' < "$FILES/-home-y/s2.jsonl")" "hoch 0, gelöscht gemeldet 0, runter 1, lokal gelöscht 0 | vom anderen PC|zweite Zeile|"
|
||||
printf 'meins\n' > "$FILES/-home-x/alt.jsonl"; touch -d '90 seconds ago' "$FILES/-home-x/alt.jsonl"
|
||||
push '{"files": [{"id": "-home-x/alt.jsonl", "updated": "'$(date -u -d '3 hours ago' +%Y-%m-%dT%H:%M:%S.000Z)'", "content": "veraltet"}]}'
|
||||
filescycle >/dev/null
|
||||
check "Lokal neuer gewinnt und geht hoch" "$(allfiles | python3 -c 'import json,sys; print([f["content"].strip() for f in json.load(sys.stdin) if f["id"] == "-home-x/alt.jsonl"])')" "['{\"a\":2}']"
|
||||
check "Seiten: Einzelanfrage liefert nur ein Paket mit more" "$(api -X POST "$URL/api/sync" -d '{"since": 0, "files": []}' | python3 -c 'import json,sys; d=json.load(sys.stdin); print(len(d["files"]), d.get("more"))')" "1 True"
|
||||
timercycle
|
||||
check "Lokal neuer gewinnt und geht hoch" "$(cat "$SFILES/-home-x/alt.jsonl")" "meins"
|
||||
rm "$FILES/-home-x/memory/MEMORY.md"
|
||||
push '{"files": [{"id": "-home-y/s2.jsonl", "updated": "'$(now)'", "deleted": true}]}'
|
||||
check "Abgleich meldet Löschungen" "$(filescycle)" "hoch 0, gelöscht gemeldet 1, runter 0, lokal gelöscht 1"
|
||||
check "Gelöscht in beide Richtungen" "$(files | python3 -c 'import sys; print("MEMORY.md\x27, True" in sys.stdin.read())') $([ -e "$FILES/-home-y/s2.jsonl" ] && echo da || echo weg)" "True weg"
|
||||
check "Gelöscht in beide Richtungen, auch auf der Server-Platte" "$(files | python3 -c 'import sys; print("MEMORY.md\x27, True" in sys.stdin.read())') $([ -e "$FILES/-home-y/s2.jsonl" ] && echo da || echo weg) $([ -e "$SFILES/-home-x/memory/MEMORY.md" ] && echo da || echo weg)" "True weg weg"
|
||||
check "Gemeldete Löschung nicht nochmal" "$(val 'state().files.pushed["-home-x/memory/MEMORY.md"] === null && state().files.pushed["-home-y/s2.jsonl"] === null')" "true"
|
||||
check "Zweiter Lauf überträgt nichts" "$(filescycle)" "hoch 0, gelöscht gemeldet 0, runter 0, lokal gelöscht 0"
|
||||
check "Zweiter Lauf schickt nichts erneut" "$(files)" "[('-home-x/alt.jsonl', False), ('-home-x/frisch.jsonl', False), ('-home-x/memory/MEMORY.md', True), ('-home-y/s2.jsonl', True)]"
|
||||
|
||||
Reference in New Issue
Block a user