aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_photo_sync.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests/test_photo_sync.py')
-rw-r--r--packages/meshbay-hub/tests/test_photo_sync.py69
1 files changed, 65 insertions, 4 deletions
diff --git a/packages/meshbay-hub/tests/test_photo_sync.py b/packages/meshbay-hub/tests/test_photo_sync.py
index 98f49ba..266a111 100644
--- a/packages/meshbay-hub/tests/test_photo_sync.py
+++ b/packages/meshbay-hub/tests/test_photo_sync.py
@@ -57,9 +57,13 @@ doc.visibilityState = 'visible';
globalThis.window = win;
globalThis.document = doc;
globalThis.localStorage = { getItem() { return null; }, setItem() {}, removeItem() {} };
-globalThis.fetch = async (url) => {
- fake.log.push(['fetch', url]);
- return { ok: true, blob: async () => new Blob([new Uint8Array(4)]) };
+globalThis.fetch = async (url, opts) => {
+ const range = opts && opts.headers && opts.headers.Range;
+ fake.log.push(['fetch', url, range || null]);
+ const m = /bytes=(\d+)-(\d+)/.exec(range || '');
+ const n = m ? Number(m[2]) - Number(m[1]) + 1 : 4;
+ return { ok: true, status: m ? 206 : 200, arrayBuffer: async () => new ArrayBuffer(n),
+ blob: async () => new Blob([new Uint8Array(n)]) };
};
const NOW = Date.now();
@@ -95,6 +99,12 @@ fake.transport = {
},
async uploadFile(file, opts) {
fake.log.push(['upload', opts.dir, file.name, opts.tr]);
+ if (fake.readChunk) {
+ for (let at = 0; at < file.size; at += fake.readChunk) {
+ const buf = await file.slice(at, at + fake.readChunk).arrayBuffer();
+ fake.log.push(['read', at, buf.byteLength]);
+ }
+ }
if (fake.uploadError) { const e = new Error('refused'); e.code = fake.uploadError; throw e; }
if (fake.afterUpload) fake.afterUpload();
return { stored_as: file.name, dir: opts.dir };
@@ -102,7 +112,9 @@ fake.transport = {
};
win.meshbay = { photoSync: {
status: async () => fake.status,
- plan: async () => { fake.log.push(['plan']); return { items: fake.items }; },
+ plan: async () => { fake.log.push(['plan']); return { items: fake.items, manifest: !!fake.manifest }; },
+ manifest: async () => ({ item: fake.manifest || null }),
+ manifestSent: async (token) => { fake.log.push(['manifest-sent', token]); fake.manifest = null; return true; },
sent: async (token, dir, name) => { fake.log.push(['sent', token, dir, name]); return true; },
completed: async () => {
fake.log.push(['completed']);
@@ -164,6 +176,20 @@ def test_a_due_run_sends_newest_first_and_records_each(tmp_path):
"the screen-off keep-alive was not released after the run")
+def test_a_file_is_read_from_the_phone_a_chunk_at_a_time(tmp_path):
+ """A video of gigabytes is never whole in the page: each slice the upload
+ reads is its own ranged request to the phone."""
+ log = _run(_start("""
+fake.items = [{ token: 'v1', name: 'clip.mp4', dir: 'Media/Photos/Bob/2026/2026-10', size: 25,
+ edited: false, video: true }];
+fake.readChunk = 10;
+"""), tmp_path)
+ assert _of(log, "fetch") == [["/photosync/v1", "bytes=0-9"], ["/photosync/v1", "bytes=10-19"],
+ ["/photosync/v1", "bytes=20-24"]]
+ assert _of(log, "read") == [[0, 10], [10, 10], [20, 5]]
+ assert ["sent", "v1", "Media/Photos/Bob/2026/2026-10", "clip.mp4"] in log
+
+
def test_only_missing_folders_are_made_and_parents_first(tmp_path):
log = _run(_start(), tmp_path)
assert _of(log, "mkdir") == [["Media/Photos/Bob/2026/2026-10"]]
@@ -286,6 +312,41 @@ def test_another_account_on_the_phone_sends_nothing(tmp_path):
assert ["plan"] not in log
+# ── the manifest ────────────────────────────────────────────────────────────
+
+MANIFEST = """
+fake.manifest = { token: 'mf', name: 'manifest-2026-10-10-090000.jsonl',
+ dir: 'Media/Photos/Bob/bob-photos/meshbay-manifest', size: 7 };
+"""
+
+
+def test_a_run_ends_with_the_manifest_of_what_it_sent(tmp_path):
+ log = _run(_start(MANIFEST), tmp_path)
+ uploads = _of(log, "upload")
+ assert uploads[-1][:2] == ["Media/Photos/Bob/bob-photos/meshbay-manifest",
+ "manifest-2026-10-10-090000.jsonl"]
+ assert ["mkdir", "Media/Photos/Bob/bob-photos/meshbay-manifest"] in log
+ assert log.index(["manifest-sent", "mf"]) < log.index(["completed"])
+
+
+def test_a_manifest_left_by_an_interrupted_run_goes_on_its_own(tmp_path):
+ log = _run(_start(MANIFEST + "fake.items = [];"), tmp_path)
+ assert [u[1] for u in _of(log, "upload")] == ["manifest-2026-10-10-090000.jsonl"]
+ assert ["manifest-sent", "mf"] in log and ["completed"] in log
+
+
+def test_a_manifest_the_node_refused_is_kept_for_the_next_run(tmp_path):
+ log = _run(_start(MANIFEST + """
+const real = fake.transport.uploadFile;
+fake.transport.uploadFile = async (file, opts) => {
+ if (file.name.startsWith('manifest-')) { const e = new Error('full'); e.code = 'disk_full'; throw e; }
+ return real(file, opts);
+};"""), tmp_path)
+ assert _of(log, "manifest-sent") == []
+ assert _of(log, "failed") == [["disk_full"]]
+ assert ["completed"] not in log
+
+
# ── the account's own group, at every run ───────────────────────────────────
@pytest.mark.parametrize("setup", [