fix: harden SessionStore — per-record timestamp handling, atomic writes, robust quarantine
This commit is contained in:
@@ -67,3 +67,59 @@ class TestSessionStore:
|
||||
|
||||
assert default_store_path().name == "sessions.json"
|
||||
assert "wabot" in str(default_store_path())
|
||||
|
||||
def test_naive_created_at_is_treated_as_utc_not_crash(self, tmp_path):
|
||||
# legacy/hand-written records may carry naive timestamps
|
||||
import json
|
||||
|
||||
path = tmp_path / "sessions.json"
|
||||
record = make_record()
|
||||
data = {"scraper1": {**record.__dict__, "created_at": "2999-01-01T00:00:00"}}
|
||||
path.write_text(json.dumps(data))
|
||||
store = SessionStore(path=path)
|
||||
assert store.get("scraper1") is not None # fresh: kept, assumed UTC
|
||||
|
||||
def test_garbage_created_at_evicts_only_that_record(self, tmp_path):
|
||||
import json
|
||||
|
||||
path = tmp_path / "sessions.json"
|
||||
good, bad = make_record(name="good"), make_record(name="bad")
|
||||
data = {
|
||||
"good": good.__dict__,
|
||||
"bad": {**bad.__dict__, "created_at": "not-a-date"},
|
||||
}
|
||||
path.write_text(json.dumps(data))
|
||||
store = SessionStore(path=path)
|
||||
assert store.names() == ["good"] # no crash, bad evicted, file rewritten
|
||||
assert "not-a-date" not in path.read_text()
|
||||
|
||||
def test_null_created_at_evicts_only_that_record(self, tmp_path):
|
||||
import json
|
||||
|
||||
path = tmp_path / "sessions.json"
|
||||
data = {"broken": {**make_record(name="broken").__dict__, "created_at": None}}
|
||||
path.write_text(json.dumps(data))
|
||||
assert SessionStore(path=path).names() == []
|
||||
|
||||
def test_unknown_record_fields_are_ignored_not_fatal(self, tmp_path):
|
||||
# a newer wabot may add fields; older versions must not quarantine the store
|
||||
import json
|
||||
|
||||
path = tmp_path / "sessions.json"
|
||||
data = {"scraper1": {**make_record().__dict__, "future_field": 42}}
|
||||
path.write_text(json.dumps(data))
|
||||
assert SessionStore(path=path).get("scraper1") is not None
|
||||
|
||||
def test_repeat_corruption_with_existing_bad_file_does_not_crash(self, tmp_path):
|
||||
path = tmp_path / "sessions.json"
|
||||
(tmp_path / "sessions.json.bad").write_text("old corruption")
|
||||
path.write_text("{ corrupt again")
|
||||
store = SessionStore(path=path)
|
||||
assert store.load() == {}
|
||||
store.save(make_record())
|
||||
assert store.get("scraper1") is not None
|
||||
|
||||
def test_write_is_atomic_no_tmp_leftover(self, tmp_path):
|
||||
store = SessionStore(path=tmp_path / "sessions.json")
|
||||
store.save(make_record())
|
||||
assert not (tmp_path / "sessions.json.tmp").exists()
|
||||
|
||||
Reference in New Issue
Block a user