fix: guard IndexedDB private work cleanup (Closes #1000)
This commit is contained in:
1 parent
8148545295
commit
a2c91e0296
10 files changed
+195
-9
No files matched your search
@@ -84,7 +84,7 @@ def test_release_artifact_bootstraps_mobile_home_and_returns_from_insights(
|
||||
page.locator("#open-device-setup").click()
|
||||
expect(page.locator("#device-setup-sheet")).to_be_visible()
|
||||
expect(page.locator("#device-storage-heading")).to_have_text("Private device storage")
|
||||
expect(page.locator("#device-storage-detail")).to_contain_text("private work stores")
|
||||
expect(page.locator("#device-storage-detail")).to_contain_text("private work records")
|
||||
for selector in ("#clear-device-caches", "#clear-private-device-data"):
|
||||
bounds = page.locator(selector).bounding_box()
|
||||
assert bounds and bounds["height"] >= 44
|
||||
|
||||
@@ -16,6 +16,7 @@ class Target {
|
||||
}
|
||||
const summary=new Target(), detail=new Target(), clearCachesButton=new Target(), clearAllButton=new Target();
|
||||
let purgeCalls=0, purgeError='';
|
||||
let inventory={recordCount:0, unavailable:false};
|
||||
const localStorage={
|
||||
values:new Map([['stackchain.draft.1','private'],['other.preference','keep']]),
|
||||
get length(){return this.values.size;},
|
||||
@@ -29,6 +30,7 @@ const caches={
|
||||
const controller=createDeviceStorage({
|
||||
summary, detail, clearCachesButton, clearAllButton, localStorage, caches,
|
||||
privateDatabases:['stackchain-background-outbox-v1','stackchain-offline-work-v2'],
|
||||
inspectPrivateDatabases:async()=>inventory,
|
||||
storageManager:{estimate:async()=>({usage:2 * 1024 * 1024, quota:10 * 1024 * 1024})},
|
||||
clearPrivateDeviceData:async()=>{purgeCalls++;if(purgeError)throw new Error(purgeError);},
|
||||
});
|
||||
@@ -47,7 +49,7 @@ process.stdout.write(JSON.stringify({summary:summary.textContent,detail:detail.t
|
||||
|
||||
assert result == {
|
||||
"summary": "2 MB of 10 MB browser storage used.",
|
||||
"detail": "1 private browser item · 2 private work stores · 1 cached app copy",
|
||||
"detail": "1 private browser item · 0 private work records · 1 cached app copy",
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +69,7 @@ process.stdout.write(JSON.stringify({
|
||||
"caches": ["other-app"],
|
||||
"storage": ["stackchain.draft.1", "other.preference"],
|
||||
"status": "Cached app copies cleared. Private work was kept.",
|
||||
"detail": "1 private browser item · 2 private work stores · 0 cached app copies",
|
||||
"detail": "1 private browser item · 0 private work records · 0 cached app copies",
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +79,7 @@ await controller.start();
|
||||
await clearAllButton.dispatch('click');
|
||||
const warning={calls:purgeCalls,label:clearAllButton.textContent,status:summary.textContent};
|
||||
await clearAllButton.dispatch('click');
|
||||
process.stdout.write(JSON.stringify({warning,calls:purgeCalls,status:summary.textContent}));
|
||||
process.stdout.write(JSON.stringify({warning,calls:purgeCalls,status:summary.textContent,detail:detail.textContent}));
|
||||
""")
|
||||
|
||||
assert result == {
|
||||
@@ -88,6 +90,49 @@ process.stdout.write(JSON.stringify({warning,calls:purgeCalls,status:summary.tex
|
||||
},
|
||||
"calls": 1,
|
||||
"status": "All Stackchain private data was cleared from this device.",
|
||||
"detail": "0 private browser items · 0 private work records · 0 cached app copies",
|
||||
}
|
||||
|
||||
|
||||
def test_indexeddb_only_work_requires_confirmation_before_purge():
|
||||
result = run_scenario("""
|
||||
localStorage.values.clear();
|
||||
inventory={recordCount:3, unavailable:false};
|
||||
await controller.start();
|
||||
await clearAllButton.dispatch('click');
|
||||
process.stdout.write(JSON.stringify({
|
||||
calls:purgeCalls,
|
||||
label:clearAllButton.textContent,
|
||||
status:summary.textContent,
|
||||
detail:detail.textContent,
|
||||
}));
|
||||
""")
|
||||
|
||||
assert result == {
|
||||
"calls": 0,
|
||||
"label": "Confirm: clear private work",
|
||||
"status": "Private drafts or queued work may not be synced. Press confirm to clear them from this device.",
|
||||
"detail": "0 private browser items · 3 private work records · 1 cached app copy",
|
||||
}
|
||||
|
||||
|
||||
def test_unavailable_indexeddb_inventory_requires_confirmation_before_purge():
|
||||
result = run_scenario("""
|
||||
localStorage.values.clear();
|
||||
inventory={recordCount:0, unavailable:true};
|
||||
await controller.start();
|
||||
await clearAllButton.dispatch('click');
|
||||
process.stdout.write(JSON.stringify({
|
||||
calls:purgeCalls,
|
||||
label:clearAllButton.textContent,
|
||||
detail:detail.textContent,
|
||||
}));
|
||||
""")
|
||||
|
||||
assert result == {
|
||||
"calls": 0,
|
||||
"label": "Confirm: clear private work",
|
||||
"detail": "0 private browser items · private work status unknown · 1 cached app copy",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
MODULE = Path(__file__).parents[1] / "frontend" / "private-data-inventory.js"
|
||||
|
||||
|
||||
def run_inventory(databases: dict[str, list[int]], registered: list[str]) -> dict:
|
||||
script = r"""
|
||||
const createInspector = require(__MODULE__);
|
||||
const databases = __DATABASES__;
|
||||
const indexedDB = {
|
||||
async databases() { return Object.keys(databases).map(name => ({name})); },
|
||||
open(name) {
|
||||
const request = {};
|
||||
queueMicrotask(() => {
|
||||
const stores = databases[name];
|
||||
const db = {
|
||||
objectStoreNames: stores.map((_records, index) => `store-${index}`),
|
||||
transaction(names) {
|
||||
return {
|
||||
objectStore(storeName) {
|
||||
return {
|
||||
count() {
|
||||
const countRequest = {};
|
||||
const index = Number(storeName.split('-')[1]);
|
||||
queueMicrotask(() => {
|
||||
countRequest.result = stores[index];
|
||||
countRequest.onsuccess?.();
|
||||
});
|
||||
return countRequest;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
close() {},
|
||||
};
|
||||
request.result = db;
|
||||
request.onsuccess?.();
|
||||
});
|
||||
return request;
|
||||
},
|
||||
};
|
||||
createInspector(indexedDB)(__REGISTERED__)
|
||||
.then(result => process.stdout.write(JSON.stringify(result)))
|
||||
.catch(error => { console.error(error); process.exit(1); });
|
||||
"""
|
||||
script = (
|
||||
script.replace("__MODULE__", json.dumps(str(MODULE)))
|
||||
.replace("__DATABASES__", json.dumps(databases))
|
||||
.replace("__REGISTERED__", json.dumps(registered))
|
||||
)
|
||||
completed = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||
assert completed.returncode == 0, completed.stderr
|
||||
return json.loads(completed.stdout)
|
||||
|
||||
|
||||
def test_inventory_counts_records_without_creating_absent_registered_databases():
|
||||
result = run_inventory(
|
||||
{"private-a": [2, 1], "other-app": [99]},
|
||||
["private-a", "private-empty", "private-missing"],
|
||||
)
|
||||
|
||||
assert result == {"recordCount": 3, "unavailable": False}
|
||||
|
||||
|
||||
def test_inventory_is_conservatively_unavailable_when_enumeration_is_unsupported():
|
||||
script = f"""
|
||||
const createInspector = require({json.dumps(str(MODULE))});
|
||||
createInspector({{}})(['private-a'])
|
||||
.then(result => process.stdout.write(JSON.stringify(result)))
|
||||
.catch(error => {{ console.error(error.message); process.exit(1); }});
|
||||
"""
|
||||
completed = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||
|
||||
assert completed.returncode == 0, completed.stderr
|
||||
assert json.loads(completed.stdout) == {"recordCount": 0, "unavailable": True}
|
||||
@@ -44,3 +44,12 @@ def test_every_private_data_purge_context_consumes_the_shared_registry():
|
||||
|
||||
html = (FRONTEND / "index.html").read_text()
|
||||
assert html.index('static/private-data-registry.js') < html.index('static/session.js')
|
||||
|
||||
|
||||
def test_private_inventory_loads_after_registry_and_before_storage_controller():
|
||||
html = (FRONTEND / "index.html").read_text()
|
||||
registry = html.index('static/private-data-registry.js')
|
||||
inventory = html.index('static/private-data-inventory.js')
|
||||
controller = html.index('static/device-storage.js')
|
||||
|
||||
assert registry < inventory < controller
|
||||
@@ -1055,6 +1055,7 @@ def test_install_precaches_complete_subpath_scoped_app_shell():
|
||||
"/dashboard/static/mobile-insights.js",
|
||||
"/dashboard/static/mobile-app-shortcuts.js",
|
||||
"/dashboard/static/install-app.js",
|
||||
"/dashboard/static/private-data-inventory.js",
|
||||
"/dashboard/static/private-device-data.js",
|
||||
"/dashboard/static/device-storage.js",
|
||||
"/dashboard/static/mobile-device-setup.js",
|
||||
|
||||
Reference in new issue
Block a user