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

243 lines
10 KiB
Python

"""Tests for migration from legacy flat layout to profile-based structure."""
import json
import os
from unittest.mock import patch
import pytest
from notebooklm.migration import ensure_profiles_dir, migrate_to_profiles
from notebooklm.paths import _reset_config_cache, set_active_profile
@pytest.fixture(autouse=True)
def _reset_profile_state():
"""Reset module-level profile state between tests."""
set_active_profile(None)
_reset_config_cache()
yield
set_active_profile(None)
_reset_config_cache()
def _clean_env():
env = os.environ.copy()
env.pop("NOTEBOOKLM_HOME", None)
env.pop("NOTEBOOKLM_PROFILE", None)
return env
class TestMigrateToProfiles:
def test_already_migrated(self, tmp_path):
"""Returns False if profiles/ already exists."""
(tmp_path / "profiles" / "default").mkdir(parents=True)
with patch.dict(os.environ, {"NOTEBOOKLM_HOME": str(tmp_path)}, clear=True):
assert migrate_to_profiles() is False
def test_fresh_install(self, tmp_path):
"""Creates profiles/ dir on fresh install (no legacy files)."""
with patch.dict(os.environ, {"NOTEBOOKLM_HOME": str(tmp_path)}, clear=True):
assert migrate_to_profiles() is True
assert (tmp_path / "profiles").exists()
def test_migrates_legacy_files(self, tmp_path):
"""Moves legacy files into profiles/default/."""
# Create legacy layout
(tmp_path / "storage_state.json").write_text('{"cookies":[]}')
(tmp_path / "context.json").write_text('{"notebook_id":"nb1"}')
(tmp_path / "browser_profile").mkdir()
(tmp_path / "browser_profile" / "data").write_text("chrome data")
with patch.dict(os.environ, {"NOTEBOOKLM_HOME": str(tmp_path)}, clear=True):
assert migrate_to_profiles() is True
default_dir = tmp_path / "profiles" / "default"
# Files moved to profile dir
assert (default_dir / "storage_state.json").exists()
assert json.loads((default_dir / "storage_state.json").read_text()) == {"cookies": []}
assert (default_dir / "context.json").exists()
assert (default_dir / "browser_profile" / "data").exists()
# Migration marker present
assert (default_dir / ".migration_complete").exists()
# Originals removed
assert not (tmp_path / "storage_state.json").exists()
assert not (tmp_path / "context.json").exists()
assert not (tmp_path / "browser_profile").exists()
def test_updates_config(self, tmp_path):
"""Sets default_profile in config.json."""
(tmp_path / "storage_state.json").write_text('{"cookies":[]}')
with patch.dict(os.environ, {"NOTEBOOKLM_HOME": str(tmp_path)}, clear=True):
migrate_to_profiles()
config = json.loads((tmp_path / "config.json").read_text())
assert config["default_profile"] == "default"
def test_preserves_existing_config(self, tmp_path):
"""Preserves existing config.json values during migration."""
(tmp_path / "storage_state.json").write_text('{"cookies":[]}')
(tmp_path / "config.json").write_text(json.dumps({"language": "ja"}))
with patch.dict(os.environ, {"NOTEBOOKLM_HOME": str(tmp_path)}, clear=True):
migrate_to_profiles()
config = json.loads((tmp_path / "config.json").read_text())
assert config["language"] == "ja"
assert config["default_profile"] == "default"
def test_idempotent(self, tmp_path):
"""Running twice is safe — second call is no-op."""
(tmp_path / "storage_state.json").write_text('{"cookies":[]}')
with patch.dict(os.environ, {"NOTEBOOKLM_HOME": str(tmp_path)}, clear=True):
assert migrate_to_profiles() is True
assert migrate_to_profiles() is False # Already migrated
def test_partial_legacy(self, tmp_path):
"""Works with only some legacy files present."""
(tmp_path / "storage_state.json").write_text('{"cookies":[]}')
# No context.json or browser_profile
with patch.dict(os.environ, {"NOTEBOOKLM_HOME": str(tmp_path)}, clear=True):
assert migrate_to_profiles() is True
default_dir = tmp_path / "profiles" / "default"
assert (default_dir / "storage_state.json").exists()
assert not (default_dir / "context.json").exists()
def test_interrupted_migration_retries(self, tmp_path):
"""Interrupted migration (marker + leftover legacy files) is retried."""
# Simulate crash after marker but before deletion (old buggy order)
(tmp_path / "storage_state.json").write_text('{"cookies":[]}')
default_dir = tmp_path / "profiles" / "default"
default_dir.mkdir(parents=True)
(default_dir / "storage_state.json").write_text('{"cookies":[]}')
(default_dir / ".migration_complete").write_text("migrated\n")
# Legacy file still at root — this simulates a crash
with patch.dict(os.environ, {"NOTEBOOKLM_HOME": str(tmp_path)}, clear=True):
# ensure_profiles_dir should detect leftover legacy files and re-migrate
ensure_profiles_dir()
# Legacy file should now be cleaned up
assert not (tmp_path / "storage_state.json").exists()
assert (default_dir / "storage_state.json").exists()
def test_marker_written_after_deletion(self, tmp_path):
"""Marker is written only after originals are deleted."""
(tmp_path / "storage_state.json").write_text('{"cookies":[]}')
with patch.dict(os.environ, {"NOTEBOOKLM_HOME": str(tmp_path)}, clear=True):
migrate_to_profiles()
default_dir = tmp_path / "profiles" / "default"
# Marker exists
assert (default_dir / ".migration_complete").exists()
# Original removed
assert not (tmp_path / "storage_state.json").exists()
def test_overwrites_stale_profile_when_legacy_is_newer(self, tmp_path):
"""Fresh login data at legacy root overwrites a stale profile copy.
Regression for the scenario where a previous migration left a profile
copy in place, and a subsequent `login` wrote fresh cookies to the
legacy root path. The migration must prefer the newer source.
"""
default_dir = tmp_path / "profiles" / "default"
default_dir.mkdir(parents=True)
stale_profile = default_dir / "storage_state.json"
stale_profile.write_text('{"cookies":["stale"]}')
os.utime(stale_profile, (1000, 1000))
fresh_legacy = tmp_path / "storage_state.json"
fresh_legacy.write_text('{"cookies":["fresh"]}')
os.utime(fresh_legacy, (2000, 2000))
with patch.dict(os.environ, {"NOTEBOOKLM_HOME": str(tmp_path)}, clear=True):
migrate_to_profiles()
# Profile copy now holds the fresh data, legacy file is gone
assert json.loads(stale_profile.read_text()) == {"cookies": ["fresh"]}
assert not fresh_legacy.exists()
def test_keeps_newer_profile_when_legacy_is_older(self, tmp_path):
"""An up-to-date profile copy is preserved over an older legacy file."""
default_dir = tmp_path / "profiles" / "default"
default_dir.mkdir(parents=True)
fresh_profile = default_dir / "storage_state.json"
fresh_profile.write_text('{"cookies":["fresh"]}')
os.utime(fresh_profile, (2000, 2000))
stale_legacy = tmp_path / "storage_state.json"
stale_legacy.write_text('{"cookies":["stale"]}')
os.utime(stale_legacy, (1000, 1000))
with patch.dict(os.environ, {"NOTEBOOKLM_HOME": str(tmp_path)}, clear=True):
migrate_to_profiles()
# Profile copy is preserved, legacy file is cleaned up
assert json.loads(fresh_profile.read_text()) == {"cookies": ["fresh"]}
assert not stale_legacy.exists()
class TestEnsureProfilesDir:
def test_creates_on_first_run(self, tmp_path):
"""Creates profiles directory on first invocation."""
with patch.dict(os.environ, {"NOTEBOOKLM_HOME": str(tmp_path)}, clear=True):
ensure_profiles_dir()
assert (tmp_path / "profiles").exists()
def test_noop_when_exists(self, tmp_path):
"""Does nothing if profiles/ already exists."""
(tmp_path / "profiles").mkdir()
with patch.dict(os.environ, {"NOTEBOOKLM_HOME": str(tmp_path)}, clear=True):
ensure_profiles_dir() # Should not raise
assert (tmp_path / "profiles").exists()
class TestExplicitAuthBypassesProfileSetup:
"""Verify that --storage and NOTEBOOKLM_AUTH_JSON don't require writable home."""
def test_storage_flag_skips_profile_setup(self, tmp_path):
"""CLI with --storage should not call ensure_profiles_dir."""
from click.testing import CliRunner
from notebooklm.notebooklm_cli import cli
# Make home read-only to prove profiles dir creation is skipped
ro_home = tmp_path / "ro_home"
ro_home.mkdir(mode=0o500)
storage_file = tmp_path / "storage.json"
storage_file.write_text('{"cookies": []}')
runner = CliRunner()
result = runner.invoke(
cli,
["--storage", str(storage_file), "status", "--paths"],
env={"NOTEBOOKLM_HOME": str(ro_home)},
)
# Should not crash with PermissionError from profiles dir creation
assert result.exit_code != 1 or "PermissionError" not in str(result.exception or "")
def test_auth_json_env_skips_profile_setup(self, tmp_path):
"""CLI with NOTEBOOKLM_AUTH_JSON should not call ensure_profiles_dir."""
from click.testing import CliRunner
from notebooklm.notebooklm_cli import cli
ro_home = tmp_path / "ro_home"
ro_home.mkdir(mode=0o500)
runner = CliRunner()
result = runner.invoke(
cli,
["status", "--paths"],
env={
"NOTEBOOKLM_HOME": str(ro_home),
"NOTEBOOKLM_AUTH_JSON": '{"cookies": [{"name": "SID", "value": "x", "domain": ".google.com"}, {"name": "__Secure-1PSIDTS", "value": "test_1psidts", "domain": ".google.com"}]}',
},
)
assert result.exit_code != 1 or "PermissionError" not in str(result.exception or "")