Files
wehub-resource-sync 09e9f3545f
Test / Code Quality (push) Has been cancelled
Test / Test (macos-latest, Python 3.10) (push) Has been cancelled
Test / Test (macos-latest, Python 3.11) (push) Has been cancelled
Test / Test (macos-latest, Python 3.12) (push) Has been cancelled
Test / Test (macos-latest, Python 3.13) (push) Has been cancelled
Test / Test (macos-latest, Python 3.14) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.10) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.11) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.12) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.13) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.14) (push) Has been cancelled
Test / Test (windows-latest, Python 3.10) (push) Has been cancelled
Test / Test (windows-latest, Python 3.11) (push) Has been cancelled
Test / Test (windows-latest, Python 3.12) (push) Has been cancelled
Test / Test (windows-latest, Python 3.13) (push) Has been cancelled
Test / Test (windows-latest, Python 3.14) (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
dependency-audit / pip-audit (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:30:13 +08:00

218 lines
8.4 KiB
Python

"""Coverage gap tests for ``notebooklm.cli.context``.
Targets the error-handling branches of the context-file read/write/clear
helpers: corrupted-JSON and OS-error warnings, lock contention and
unavailability during ``clear``, and the locked-clear fast paths
(missing file race, corrupt JSON, non-dict payload, no-op when unchanged).
All tests pass an explicit ``context_path_fn`` so nothing touches the real
``~/.notebooklm/context.json``.
"""
from __future__ import annotations
import json
from pathlib import Path
from filelock import Timeout
from notebooklm.cli import context as ctx
def _path_fn(target: Path):
"""Build a ``context_path_fn`` that always returns ``target``."""
def _fn(*, storage_path=None):
return target
return _fn
# ---------------------------------------------------------------------------
# _get_context_value
# ---------------------------------------------------------------------------
class TestGetContextValue:
def test_invalid_shape_warns_and_returns_none(self, tmp_path, caplog):
target = tmp_path / "context.json"
target.write_text(json.dumps(["not", "an", "object"]), encoding="utf-8")
with caplog.at_level("WARNING"):
result = ctx._get_context_value("notebook_id", context_path_fn=_path_fn(target))
assert result is None
assert "invalid shape" in caplog.text
def test_corrupt_json_warns_and_returns_none(self, tmp_path, caplog):
target = tmp_path / "context.json"
target.write_text("{not json", encoding="utf-8")
with caplog.at_level("WARNING"):
result = ctx._get_context_value("notebook_id", context_path_fn=_path_fn(target))
assert result is None
assert "corrupted" in caplog.text
def test_os_error_warns_and_returns_none(self, tmp_path, caplog, monkeypatch):
target = tmp_path / "context.json"
target.write_text(json.dumps({"notebook_id": "nb_1"}), encoding="utf-8")
def _boom(*a, **kw):
raise OSError("read failed")
monkeypatch.setattr(Path, "read_text", _boom)
with caplog.at_level("WARNING"):
result = ctx._get_context_value("notebook_id", context_path_fn=_path_fn(target))
assert result is None
assert "Cannot read context file" in caplog.text
# ---------------------------------------------------------------------------
# _set_context_value
# ---------------------------------------------------------------------------
class TestSetContextValue:
def test_corrupt_json_warns(self, tmp_path, caplog, monkeypatch):
target = tmp_path / "context.json"
target.write_text(json.dumps({"notebook_id": "nb_1"}), encoding="utf-8")
def _boom(*a, **kw):
raise json.JSONDecodeError("bad", "doc", 0)
monkeypatch.setattr(ctx, "atomic_update_json", _boom)
with caplog.at_level("WARNING"):
ctx._set_context_value("conversation_id", "c1", context_path_fn=_path_fn(target))
assert "corrupted" in caplog.text
def test_os_error_warns(self, tmp_path, caplog, monkeypatch):
target = tmp_path / "context.json"
target.write_text(json.dumps({"notebook_id": "nb_1"}), encoding="utf-8")
def _boom(*a, **kw):
raise OSError("disk full")
monkeypatch.setattr(ctx, "atomic_update_json", _boom)
with caplog.at_level("WARNING"):
ctx._set_context_value("conversation_id", "c1", context_path_fn=_path_fn(target))
assert "Failed to write context file" in caplog.text
# ---------------------------------------------------------------------------
# _clear_context_file (lock-level outcomes)
# ---------------------------------------------------------------------------
class TestClearContextFileLockOutcomes:
def test_lock_timeout_returns_contended(self, tmp_path, caplog, monkeypatch):
target = tmp_path / "context.json"
target.write_text(json.dumps({"notebook_id": "nb_1"}), encoding="utf-8")
class _FakeLock:
def __init__(self, *a, **kw):
pass
def __enter__(self):
raise Timeout("lock-path")
def __exit__(self, *a):
return False
monkeypatch.setattr(ctx, "FileLock", _FakeLock)
with caplog.at_level("WARNING"):
status = ctx._clear_context_file(target, clear_account=False)
assert status == "contended"
assert "contended" in caplog.text
def test_lock_os_error_returns_unavailable(self, tmp_path, caplog, monkeypatch):
target = tmp_path / "context.json"
target.write_text(json.dumps({"notebook_id": "nb_1"}), encoding="utf-8")
class _FakeLock:
def __init__(self, *a, **kw):
pass
def __enter__(self):
raise OSError("lock unavailable")
def __exit__(self, *a):
return False
monkeypatch.setattr(ctx, "FileLock", _FakeLock)
with caplog.at_level("WARNING"):
status = ctx._clear_context_file(target, clear_account=False)
assert status == "unavailable"
assert "unavailable" in caplog.text
# ---------------------------------------------------------------------------
# _clear_context_file_locked
# ---------------------------------------------------------------------------
class TestClearContextFileLocked:
def test_missing_file_race_returns_unchanged(self, tmp_path):
target = tmp_path / "context.json" # never created
assert ctx._clear_context_file_locked(target, clear_account=False) == "unchanged"
def test_corrupt_json_unlinks_and_clears(self, tmp_path):
target = tmp_path / "context.json"
target.write_text("{not json", encoding="utf-8")
assert ctx._clear_context_file_locked(target, clear_account=False) == "cleared"
assert not target.exists()
def test_non_dict_payload_unlinks_and_clears(self, tmp_path):
target = tmp_path / "context.json"
target.write_text(json.dumps(["list", "payload"]), encoding="utf-8")
assert ctx._clear_context_file_locked(target, clear_account=False) == "cleared"
assert not target.exists()
def test_account_preserved_writes_and_clears(self, tmp_path):
target = tmp_path / "context.json"
target.write_text(
json.dumps({"account": {"email": "a@b.c"}, "notebook_id": "nb_1"}),
encoding="utf-8",
)
assert ctx._clear_context_file_locked(target, clear_account=False) == "cleared"
data = json.loads(target.read_text(encoding="utf-8"))
assert data == {"account": {"email": "a@b.c"}}
def test_only_account_field_is_noop_unchanged(self, tmp_path):
# data == original after clearing non-account fields → "unchanged".
target = tmp_path / "context.json"
original = {"account": {"email": "a@b.c"}}
target.write_text(json.dumps(original), encoding="utf-8")
assert ctx._clear_context_file_locked(target, clear_account=False) == "unchanged"
assert json.loads(target.read_text(encoding="utf-8")) == original
def test_os_error_during_clear_returns_unavailable(self, tmp_path, caplog, monkeypatch):
target = tmp_path / "context.json"
target.write_text(json.dumps({"notebook_id": "nb_1"}), encoding="utf-8")
real_read_text = Path.read_text
def _boom(self, *a, **kw):
if self == target:
raise OSError("read failed mid-clear")
return real_read_text(self, *a, **kw)
monkeypatch.setattr(Path, "read_text", _boom)
with caplog.at_level("WARNING"):
status = ctx._clear_context_file_locked(target, clear_account=False)
assert status == "unavailable"
assert "unavailable" in caplog.text
# ---------------------------------------------------------------------------
# clear_context public wrapper (sanity over "cleared" mapping)
# ---------------------------------------------------------------------------
class TestClearContextWrapper:
def test_returns_true_when_cleared(self, tmp_path):
target = tmp_path / "context.json"
target.write_text(json.dumps({"notebook_id": "nb_1"}), encoding="utf-8")
assert ctx.clear_context(clear_account=True, context_path_fn=_path_fn(target)) is True
assert not target.exists()
def test_returns_false_when_missing(self, tmp_path):
target = tmp_path / "context.json"
assert ctx.clear_context(context_path_fn=_path_fn(target)) is False