@@ -39,13 +39,15 @@ class body, was silently rewritten by the Python compiler to
3939
4040import functools
4141import http .server
42+ import json
4243import shutil
4344import socketserver
4445import threading
4546from pathlib import Path
4647
4748import pytest
4849
50+
4951STATE_PY = (
5052 Path (__file__ ).resolve ().parents [2 ] / "mlsysim" / "mlsysim" / "labs" / "state.py"
5153)
@@ -205,3 +207,146 @@ def test_design_ledger_save_async_persists_in_real_indexeddb(served_dir):
205207 f"from #1985 / PR #1988. A mocked test cannot catch this; only a "
206208 f"real Pyodide + IndexedDB check like this one can."
207209 )
210+
211+
212+ def test_load_async_corrupt_record_sets_last_load_error (served_dir ):
213+ """A stored record exists but is corrupt JSON -- json.loads() raising
214+ is a Python-side failure independent of the JS resolve/reject shape,
215+ so last_load_error must be populated regardless of #1988's status."""
216+ from playwright .sync_api import sync_playwright
217+
218+ _ , port = served_dir
219+
220+ with sync_playwright () as p :
221+ browser = p .chromium .launch ()
222+ context = browser .new_context ()
223+ try :
224+ page = context .new_page ()
225+ init_errors : list [str ] = []
226+ page .on (
227+ "pageerror" ,
228+ lambda exc , errors = init_errors : errors .append (str (exc )),
229+ )
230+
231+ page .goto (f"http://127.0.0.1:{ port } /index.html" )
232+ page .wait_for_function (
233+ "window.__ready === true || window.__initError" , timeout = 30_000
234+ )
235+ init_error = page .evaluate ("window.__initError || null" )
236+ assert not init_error , f"Pyodide init failed: { init_error } "
237+ assert not init_errors , f"Uncaught page errors during init: { init_errors } "
238+
239+ page .evaluate (
240+ """
241+ () => new Promise((resolve) => {
242+ const req = indexedDB.deleteDatabase("mlsys_ledger_db");
243+ req.onsuccess = req.onerror = req.onblocked = () => resolve();
244+ })
245+ """
246+ )
247+
248+ # Seed a corrupt record directly at the storage layer.
249+ page .evaluate (
250+ """
251+ () => new Promise((resolve, reject) => {
252+ const req = indexedDB.open("mlsys_ledger_db", 1);
253+ req.onupgradeneeded = (e) => {
254+ const db = e.target.result;
255+ if (!db.objectStoreNames.contains("ledger")) {
256+ db.createObjectStore("ledger");
257+ }
258+ };
259+ req.onsuccess = (e) => {
260+ const db = e.target.result;
261+ const tx = db.transaction("ledger", "readwrite");
262+ tx.objectStore("ledger").put("{not valid json", "mlsys_design_ledger");
263+ tx.oncomplete = () => { db.close(); resolve(); };
264+ tx.onerror = () => { db.close(); reject(tx.error); };
265+ };
266+ req.onerror = () => reject(req.error);
267+ })
268+ """
269+ )
270+
271+ result = page .evaluate (
272+ """
273+ async () => {
274+ const pyodide = window.__pyodide;
275+ return await pyodide.runPythonAsync(`
276+ import json
277+ ledger = DesignLedger()
278+ await ledger.load_async()
279+ json.dumps({"error": ledger.last_load_error})
280+ `);
281+ }
282+ """
283+ )
284+ page .close ()
285+ finally :
286+ context .close ()
287+ browser .close ()
288+
289+ parsed = json .loads (result )
290+ assert parsed ["error" ] is not None , (
291+ "load_async() must surface a corrupt-JSON read failure via "
292+ "last_load_error instead of silently returning a blank LedgerState()."
293+ )
294+
295+
296+ def test_load_async_synchronous_indexeddb_open_throw_sets_last_load_error (served_dir ):
297+ """indexedDB.open() throwing synchronously must reject the Promise
298+ (per the Promise constructor spec) and propagate to last_load_error --
299+ true today even against the pre-#1988 resolve(null)-style onerror
300+ handlers, since this never reaches those handlers at all."""
301+ from playwright .sync_api import sync_playwright
302+
303+ _ , port = served_dir
304+
305+ with sync_playwright () as p :
306+ browser = p .chromium .launch ()
307+ context = browser .new_context ()
308+ try :
309+ page = context .new_page ()
310+ init_errors : list [str ] = []
311+ page .on (
312+ "pageerror" ,
313+ lambda exc , errors = init_errors : errors .append (str (exc )),
314+ )
315+
316+ page .goto (f"http://127.0.0.1:{ port } /index.html" )
317+ page .wait_for_function (
318+ "window.__ready === true || window.__initError" , timeout = 30_000
319+ )
320+ init_error = page .evaluate ("window.__initError || null" )
321+ assert not init_error , f"Pyodide init failed: { init_error } "
322+ assert not init_errors , f"Uncaught page errors during init: { init_errors } "
323+
324+ page .evaluate (
325+ """() => {
326+ window.indexedDB.open = () => {
327+ throw new Error('Simulated synchronous IndexedDB failure');
328+ };
329+ }"""
330+ )
331+
332+ result = page .evaluate (
333+ """
334+ async () => {
335+ const pyodide = window.__pyodide;
336+ return await pyodide.runPythonAsync(`
337+ import json
338+ ledger = DesignLedger()
339+ await ledger.load_async()
340+ json.dumps({"error": ledger.last_load_error})
341+ `);
342+ }
343+ """
344+ )
345+ page .close ()
346+ finally :
347+ context .close ()
348+ browser .close ()
349+
350+ parsed = json .loads (result )
351+ assert parsed ["error" ] is not None
352+ assert "Simulated synchronous IndexedDB failure" in parsed ["error" ]
0 commit comments