personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge branch 'hopper-4jxalne6-tracked-copy-fixtures'

+113 -176
+37
tests/conftest.py
··· 2 2 # Copyright (c) 2026 sol pbc 3 3 4 4 import importlib 5 + import os 6 + import shutil 7 + import subprocess 5 8 import sys 6 9 import types 10 + from pathlib import Path 7 11 from unittest.mock import Mock 8 12 9 13 import numpy as np ··· 445 449 monkeypatch.setitem(sys.modules, "google.genai.errors", errors_mod) 446 450 447 451 return DummyChat 452 + 453 + 454 + def copytree_tracked(src, dst): 455 + """Copy only git-tracked files from src to dst, preserving directory structure.""" 456 + src = Path(src) 457 + dst = Path(dst) 458 + result = subprocess.run( 459 + ["git", "ls-files", "."], 460 + cwd=str(src), 461 + capture_output=True, 462 + text=True, 463 + check=True, 464 + ) 465 + for rel in result.stdout.splitlines(): 466 + if not rel: 467 + continue 468 + src_file = src / rel 469 + dst_file = dst / rel 470 + dst_file.parent.mkdir(parents=True, exist_ok=True) 471 + if src_file.is_symlink(): 472 + os.symlink(os.readlink(src_file), dst_file) 473 + else: 474 + shutil.copy2(src_file, dst_file) 475 + 476 + 477 + @pytest.fixture 478 + def journal_copy(tmp_path, monkeypatch): 479 + """Copy git-tracked journal fixtures to tmp_path and set the override env var.""" 480 + src = Path("tests/fixtures/journal") 481 + dst = tmp_path / "journal" 482 + copytree_tracked(src, dst) 483 + monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(dst)) 484 + return dst
+4 -4
tests/test_call.py
··· 4 4 """Tests for think/call.py CLI dispatcher and app discovery.""" 5 5 6 6 import json 7 + from pathlib import Path 7 8 8 9 import pytest 9 10 import typer ··· 11 12 12 13 from think.call import call_app 13 14 from think.utils import resolve_sol_day, resolve_sol_facet, resolve_sol_segment 15 + from tests.conftest import copytree_tracked 14 16 15 17 runner = CliRunner() 16 18 ··· 279 281 280 282 def test_journal_news_write(self, tmp_path, monkeypatch): 281 283 """News --write saves content from stdin.""" 282 - import shutil 283 - 284 284 # Copy fixtures to tmp so we can write 285 285 journal = tmp_path / "journal" 286 - shutil.copytree( 287 - "tests/fixtures/journal/facets/work", journal / "facets" / "work" 286 + copytree_tracked( 287 + Path("tests/fixtures/journal/facets/work"), journal / "facets" / "work" 288 288 ) 289 289 monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) 290 290 # Clear cached journal path
+2 -7
tests/test_cluster_full.py
··· 3 3 4 4 import importlib 5 5 import os 6 - import shutil 7 6 from pathlib import Path 8 7 9 8 from think.utils import day_path 9 + from tests.conftest import copytree_tracked 10 10 11 11 FIXTURES = Path("tests/fixtures") 12 12 ··· 15 15 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(tmp_path) 16 16 dest = day_path("20240101") 17 17 src = FIXTURES / "journal" / "20240101" 18 - # Copy contents from fixture to the day_path created directory 19 - for item in src.iterdir(): 20 - if item.is_dir(): 21 - shutil.copytree(item, dest / item.name, dirs_exist_ok=True) 22 - else: 23 - shutil.copy2(item, dest / item.name) 18 + copytree_tracked(src, dest) 24 19 return dest 25 20 26 21
+6 -29
tests/test_dream_dry_run.py
··· 4 4 """Tests for dream --dry-run.""" 5 5 6 6 import importlib 7 - import shutil 8 - from pathlib import Path 9 - 10 - FIXTURES = Path("tests/fixtures") 11 7 12 8 13 - def copy_journal(tmp_path: Path) -> Path: 14 - src = FIXTURES / "journal" 15 - dest = tmp_path / "journal" 16 - shutil.copytree(src, dest, symlinks=True) 17 - return dest 18 - 19 - 20 - def test_dry_run_daily(tmp_path, monkeypatch, capsys): 9 + def test_dry_run_daily(journal_copy, capsys): 21 10 """Dry-run daily mode prints prompts without spawning agents.""" 22 11 mod = importlib.import_module("think.dream") 23 - journal = copy_journal(tmp_path) 24 - monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) 25 12 26 13 mod.dry_run("20240101") 27 14 ··· 33 20 assert "Total:" in out 34 21 35 22 36 - def test_dry_run_segment(tmp_path, monkeypatch, capsys): 23 + def test_dry_run_segment(journal_copy, capsys): 37 24 """Dry-run segment mode skips pre/post phases.""" 38 25 mod = importlib.import_module("think.dream") 39 - journal = copy_journal(tmp_path) 40 - monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) 41 26 42 27 mod.dry_run("20240101", segment="120000_300") 43 28 ··· 48 33 assert "Post-phase" not in out 49 34 50 35 51 - def test_dry_run_segments_lists_all(tmp_path, monkeypatch, capsys): 36 + def test_dry_run_segments_lists_all(journal_copy, capsys): 52 37 """Dry-run --segments lists discovered segments.""" 53 38 mod = importlib.import_module("think.dream") 54 - journal = copy_journal(tmp_path) 55 - monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) 56 39 57 40 mod.dry_run("20240101", segments=True) 58 41 ··· 60 43 assert "segments" in out.lower() 61 44 62 45 63 - def test_dry_run_flush(tmp_path, monkeypatch, capsys): 46 + def test_dry_run_flush(journal_copy, capsys): 64 47 """Dry-run --flush shows flush-eligible agents.""" 65 48 mod = importlib.import_module("think.dream") 66 - journal = copy_journal(tmp_path) 67 - monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) 68 49 69 50 mod.dry_run("20240101", flush=True, segment="120000_300") 70 51 ··· 72 53 assert "flush" in out.lower() 73 54 74 55 75 - def test_dry_run_shows_refresh(tmp_path, monkeypatch, capsys): 56 + def test_dry_run_shows_refresh(journal_copy, capsys): 76 57 """Dry-run indicates refresh mode in header.""" 77 58 mod = importlib.import_module("think.dream") 78 - journal = copy_journal(tmp_path) 79 - monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) 80 59 81 60 mod.dry_run("20240101", refresh=True) 82 61 ··· 84 63 assert "(refresh)" in out 85 64 86 65 87 - def test_dry_run_no_callosum(tmp_path, monkeypatch, capsys): 66 + def test_dry_run_no_callosum(journal_copy, monkeypatch, capsys): 88 67 """Dry-run works without callosum connection.""" 89 68 mod = importlib.import_module("think.dream") 90 - journal = copy_journal(tmp_path) 91 - monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) 92 69 93 70 # Save and clear _callosum to verify dry_run doesn't create one 94 71 prev = mod._callosum
+4 -19
tests/test_dream_full.py
··· 4 4 """Tests for the dream module unified priority system.""" 5 5 6 6 import importlib 7 - import shutil 8 - from pathlib import Path 9 - 10 - FIXTURES = Path("tests/fixtures") 11 7 12 8 13 - def copy_journal(tmp_path: Path) -> Path: 14 - src = FIXTURES / "journal" 15 - dest = tmp_path / "journal" 16 - shutil.copytree(src, dest, symlinks=True) 17 - return dest 18 - 19 - 20 - def test_main_runs_with_mocked_prompts(tmp_path, monkeypatch): 9 + def test_main_runs_with_mocked_prompts(journal_copy, monkeypatch): 21 10 """Test that main() runs pre/post phases and prompts by priority.""" 22 11 mod = importlib.import_module("think.dream") 23 - journal = copy_journal(tmp_path) 24 - monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) 25 12 26 13 commands_run = [] 27 14 prompts_run = False ··· 61 48 assert any("--rescan" in cmd for cmd in indexer_cmds) 62 49 63 50 64 - def test_segment_mode_skips_pre_post_phases(tmp_path, monkeypatch): 51 + def test_segment_mode_skips_pre_post_phases(journal_copy, monkeypatch): 65 52 """Test that segment mode skips sense and journal-stats.""" 66 53 mod = importlib.import_module("think.dream") 67 - journal = copy_journal(tmp_path) 54 + journal = journal_copy 68 55 69 56 # Create segment directory 70 57 segment_dir = journal / "20240101" / "default" / "120000_300" 71 58 segment_dir.mkdir(parents=True, exist_ok=True) 72 - 73 - monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) 74 59 75 60 commands_run = [] 76 61 ··· 100 85 assert not any(c[1] == "journal-stats" for c in commands_run if len(c) > 1) 101 86 102 87 103 - def test_priority_validation_required(tmp_path, monkeypatch): 88 + def test_priority_validation_required(): 104 89 """Test that get_talent_configs raises error for scheduled prompts without priority.""" 105 90 from think.talent import get_talent_configs 106 91
+2 -7
tests/test_generate_full.py
··· 13 13 import io 14 14 import json 15 15 import os 16 - import shutil 17 16 from pathlib import Path 18 17 19 18 from think.utils import day_path 19 + from tests.conftest import copytree_tracked 20 20 21 21 FIXTURES = Path("tests/fixtures") 22 22 ··· 25 25 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(tmp_path) 26 26 dest = day_path("20240101") 27 27 src = FIXTURES / "journal" / "20240101" 28 - # Copy contents from fixture to the day_path created directory 29 - for item in src.iterdir(): 30 - if item.is_dir(): 31 - shutil.copytree(item, dest / item.name, dirs_exist_ok=True) 32 - else: 33 - shutil.copy2(item, dest / item.name) 28 + copytree_tracked(src, dest) 34 29 return dest 35 30 36 31
+2 -7
tests/test_generate_scan_day.py
··· 3 3 4 4 import importlib 5 5 import os 6 - import shutil 7 6 from pathlib import Path 8 7 9 8 from think.utils import day_path 9 + from tests.conftest import copytree_tracked 10 10 11 11 FIXTURES = Path("tests/fixtures") 12 12 ··· 15 15 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(tmp_path) 16 16 dest = day_path("20240101") 17 17 src = FIXTURES / "journal" / "20240101" 18 - # Copy contents from fixture to the day_path created directory 19 - for item in src.iterdir(): 20 - if item.is_dir(): 21 - shutil.copytree(item, dest / item.name, dirs_exist_ok=True) 22 - else: 23 - shutil.copy2(item, dest / item.name) 18 + copytree_tracked(src, dest) 24 19 agents_dir = dest / "agents" 25 20 agents_dir.mkdir(exist_ok=True) # Allow existing directory 26 21 (agents_dir / "flow.md").write_text("done")
+20 -31
tests/test_init.py
··· 1 1 import json 2 - import shutil 3 - from pathlib import Path 4 2 5 3 import pytest 6 4 7 5 from convey import create_app 8 6 9 7 10 - @pytest.fixture 11 - def journal_dir(tmp_path, monkeypatch): 12 - src = Path(__file__).resolve().parent / "fixtures" / "journal" 13 - dst = tmp_path / "journal" 14 - shutil.copytree(src, dst, symlinks=True) 15 - monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(dst)) 16 - return dst 17 - 18 - 19 8 def _read_config(journal_dir): 20 9 return json.loads((journal_dir / "config" / "journal.json").read_text()) 21 10 ··· 28 17 29 18 30 19 @pytest.fixture 31 - def fresh_client(journal_dir): 32 - _remove_password(journal_dir) 33 - app = create_app(str(journal_dir)) 20 + def fresh_client(journal_copy): 21 + _remove_password(journal_copy) 22 + app = create_app(str(journal_copy)) 34 23 app.config["TESTING"] = True 35 24 return app.test_client() 36 25 37 26 38 27 @pytest.fixture 39 - def configured_client(journal_dir): 40 - app = create_app(str(journal_dir)) 28 + def configured_client(journal_copy): 29 + app = create_app(str(journal_copy)) 41 30 app.config["TESTING"] = True 42 31 return app.test_client() 43 32 ··· 64 53 65 54 66 55 class TestInitPassword: 67 - def test_save_password(self, fresh_client, journal_dir): 56 + def test_save_password(self, fresh_client, journal_copy): 68 57 resp = fresh_client.post( 69 58 "/init/password", 70 59 json={"password": "securepass123"}, ··· 73 62 assert resp.status_code == 200 74 63 data = resp.get_json() 75 64 assert data["success"] is True 76 - config = _read_config(journal_dir) 65 + config = _read_config(journal_copy) 77 66 assert "password_hash" in config["convey"] 78 67 from werkzeug.security import check_password_hash 79 68 80 69 assert check_password_hash(config["convey"]["password_hash"], "securepass123") 81 70 82 - def test_password_too_short(self, fresh_client, journal_dir): 71 + def test_password_too_short(self, fresh_client, journal_copy): 83 72 resp = fresh_client.post( 84 73 "/init/password", 85 74 json={"password": "short"}, 86 75 content_type="application/json", 87 76 ) 88 77 assert resp.status_code == 400 89 - config = _read_config(journal_dir) 78 + config = _read_config(journal_copy) 90 79 assert "password_hash" not in config.get("convey", {}) 91 80 92 81 def test_password_already_set(self, configured_client): ··· 99 88 100 89 101 90 class TestInitIdentity: 102 - def test_save_identity(self, fresh_client, journal_dir): 91 + def test_save_identity(self, fresh_client, journal_copy): 103 92 fresh_client.post( 104 93 "/init/password", 105 94 json={"password": "securepass123"}, ··· 111 100 content_type="application/json", 112 101 ) 113 102 assert resp.status_code == 200 114 - config = _read_config(journal_dir) 103 + config = _read_config(journal_copy) 115 104 assert config["identity"]["name"] == "Jane Doe" 116 105 assert config["identity"]["preferred"] == "Jane" 117 106 ··· 125 114 126 115 127 116 class TestInitProvider: 128 - def test_save_provider_key(self, fresh_client, journal_dir, monkeypatch): 117 + def test_save_provider_key(self, fresh_client, journal_copy, monkeypatch): 129 118 fresh_client.post( 130 119 "/init/password", 131 120 json={"password": "securepass123"}, ··· 144 133 data = resp.get_json() 145 134 assert data["success"] is True 146 135 assert data["validation"]["valid"] is True 147 - config = _read_config(journal_dir) 136 + config = _read_config(journal_copy) 148 137 assert config["env"]["GOOGLE_API_KEY"] == "test-api-key-123" 149 138 150 - def test_provider_validation_failure(self, fresh_client, journal_dir, monkeypatch): 139 + def test_provider_validation_failure(self, fresh_client, journal_copy, monkeypatch): 151 140 fresh_client.post( 152 141 "/init/password", 153 142 json={"password": "securepass123"}, ··· 166 155 data = resp.get_json() 167 156 assert data["success"] is True 168 157 assert data["validation"]["valid"] is False 169 - config = _read_config(journal_dir) 158 + config = _read_config(journal_copy) 170 159 assert config["env"]["GOOGLE_API_KEY"] == "bad-key" 171 160 172 161 ··· 175 164 resp = fresh_client.get("/init/observers") 176 165 assert resp.status_code == 403 177 166 178 - def test_observers_returns_list(self, fresh_client, journal_dir, monkeypatch): 167 + def test_observers_returns_list(self, fresh_client, journal_copy, monkeypatch): 179 168 fresh_client.post( 180 169 "/init/password", 181 170 json={"password": "securepass123"}, ··· 221 210 222 211 223 212 class TestInitFinalize: 224 - def test_finalize_sets_session_and_config(self, fresh_client, journal_dir): 213 + def test_finalize_sets_session_and_config(self, fresh_client, journal_copy): 225 214 fresh_client.post( 226 215 "/init/password", 227 216 json={"password": "securepass123"}, ··· 236 225 data = resp.get_json() 237 226 assert data["success"] is True 238 227 assert data["redirect"] == "/" 239 - config = _read_config(journal_dir) 228 + config = _read_config(journal_copy) 240 229 assert config["setup"]["coding_agent"] == "claude-code" 241 230 assert "completed_at" in config["setup"] 242 231 243 - def test_finalize_auto_login(self, fresh_client, journal_dir): 232 + def test_finalize_auto_login(self, fresh_client, journal_copy): 244 233 fresh_client.post( 245 234 "/init/password", 246 235 json={"password": "securepass123"}, ··· 257 246 assert "/login" not in location 258 247 assert "/init" not in location 259 248 260 - def test_post_init_redirect(self, fresh_client, journal_dir): 249 + def test_post_init_redirect(self, fresh_client, journal_copy): 261 250 fresh_client.post( 262 251 "/init/password", 263 252 json={"password": "securepass123"},
+3 -6
tests/test_journal_index.py
··· 11 11 12 12 from think.indexer import sanitize_fts_query 13 13 from think.indexer.journal import get_journal_index, search_journal 14 + from tests.conftest import copytree_tracked 14 15 15 16 16 17 class TestSanitizeFtsQuery: ··· 989 990 990 991 def test_scan_entities_deletion(tmp_path): 991 992 """Verify entity rows are removed when source file is deleted.""" 992 - import shutil 993 - 994 993 src = Path("tests/fixtures/journal") 995 994 dst = tmp_path / "journal" 996 - shutil.copytree(src, dst, symlinks=True) 995 + copytree_tracked(src, dst) 997 996 j = str(dst) 998 997 999 998 from think.indexer.journal import scan_journal ··· 1138 1137 1139 1138 def test_scan_signals_deletion(tmp_path): 1140 1139 """Verify signal rows are removed when source file is deleted.""" 1141 - import shutil 1142 - 1143 1140 src = Path("tests/fixtures/journal") 1144 1141 dst = tmp_path / "journal" 1145 - shutil.copytree(src, dst, symlinks=True) 1142 + copytree_tracked(src, dst) 1146 1143 j = str(dst) 1147 1144 1148 1145 from think.indexer.journal import scan_journal
+2 -6
tests/test_output_hooks.py
··· 13 13 import io 14 14 import json 15 15 import os 16 - import shutil 17 16 from pathlib import Path 18 17 19 18 from think.agents import _apply_template_vars 20 19 from think.talent import load_post_hook, load_pre_hook 21 20 from think.utils import day_path 21 + from tests.conftest import copytree_tracked 22 22 23 23 FIXTURES = Path("tests/fixtures") 24 24 ··· 27 27 os.environ["_SOLSTONE_JOURNAL_OVERRIDE"] = str(tmp_path) 28 28 dest = day_path("20240101") 29 29 src = FIXTURES / "journal" / "20240101" 30 - for item in src.iterdir(): 31 - if item.is_dir(): 32 - shutil.copytree(item, dest / item.name, dirs_exist_ok=True) 33 - else: 34 - shutil.copy2(item, dest / item.name) 30 + copytree_tracked(src, dest) 35 31 return dest 36 32 37 33
+18 -28
tests/test_password.py
··· 6 6 from __future__ import annotations 7 7 8 8 import json 9 - import shutil 10 9 from pathlib import Path 11 10 12 11 import pytest 13 12 from werkzeug.security import check_password_hash 14 13 15 14 from convey import create_app 15 + from tests.conftest import copytree_tracked 16 16 17 17 18 18 @pytest.fixture 19 - def journal_dir(tmp_path, monkeypatch): 20 - """Copy test fixture to temp dir for mutation tests.""" 21 - src = Path(__file__).resolve().parent / "fixtures" / "journal" 22 - dst = tmp_path / "journal" 23 - shutil.copytree(src, dst, symlinks=True) 24 - monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(dst)) 25 - return dst 26 - 27 - 28 - @pytest.fixture 29 - def client(journal_dir): 30 - app = create_app(str(journal_dir)) 19 + def client(journal_copy): 20 + app = create_app(str(journal_copy)) 31 21 app.config["TESTING"] = True 32 22 return app.test_client() 33 23 ··· 46 36 assert resp.status_code == 200 47 37 assert b"Invalid password" in resp.data 48 38 49 - def test_no_password_configured(self, journal_dir, monkeypatch): 50 - config = _read_config(journal_dir) 39 + def test_no_password_configured(self, journal_copy): 40 + config = _read_config(journal_copy) 51 41 config["convey"].pop("password_hash", None) 52 42 config["convey"].pop("password", None) 53 - (journal_dir / "config" / "journal.json").write_text( 43 + (journal_copy / "config" / "journal.json").write_text( 54 44 json.dumps(config, indent=2) 55 45 ) 56 - app = create_app(str(journal_dir)) 46 + app = create_app(str(journal_copy)) 57 47 app.config["TESTING"] = True 58 48 client = app.test_client() 59 49 resp = client.get("/login") ··· 65 55 """Plaintext password is hashed and old key removed on app creation.""" 66 56 src = Path(__file__).resolve().parent / "fixtures" / "journal" 67 57 dst = tmp_path / "journal" 68 - shutil.copytree(src, dst, symlinks=True) 58 + copytree_tracked(src, dst) 69 59 config_path = dst / "config" / "journal.json" 70 60 config = json.loads(config_path.read_text()) 71 61 config["convey"].pop("password_hash", None) ··· 84 74 """Empty plaintext password is removed, not hashed.""" 85 75 src = Path(__file__).resolve().parent / "fixtures" / "journal" 86 76 dst = tmp_path / "journal" 87 - shutil.copytree(src, dst, symlinks=True) 77 + copytree_tracked(src, dst) 88 78 config_path = dst / "config" / "journal.json" 89 79 config = json.loads(config_path.read_text()) 90 80 config["convey"].pop("password_hash", None) ··· 98 88 assert "password" not in config["convey"] 99 89 assert "password_hash" not in config["convey"] 100 90 101 - def test_already_migrated_skipped(self, journal_dir): 91 + def test_already_migrated_skipped(self, journal_copy): 102 92 """If password_hash exists, migration is a no-op.""" 103 - config_before = _read_config(journal_dir) 93 + config_before = _read_config(journal_copy) 104 94 hash_before = config_before["convey"]["password_hash"] 105 95 106 - create_app(str(journal_dir)) 96 + create_app(str(journal_copy)) 107 97 108 - config_after = _read_config(journal_dir) 98 + config_after = _read_config(journal_copy) 109 99 assert config_after["convey"]["password_hash"] == hash_before 110 100 111 101 ··· 119 109 assert "password_hash" not in convey 120 110 assert convey.get("has_password") is True 121 111 122 - def test_put_hashes_password(self, client, journal_dir): 112 + def test_put_hashes_password(self, client, journal_copy): 123 113 """PUT with convey.password hashes before writing to disk.""" 124 114 resp = client.put( 125 115 "/app/settings/api/config", ··· 127 117 content_type="application/json", 128 118 ) 129 119 assert resp.status_code == 200 130 - config = _read_config(journal_dir) 120 + config = _read_config(journal_copy) 131 121 assert "password" not in config["convey"] 132 122 assert check_password_hash(config["convey"]["password_hash"], "new-secret") 133 123 134 - def test_put_empty_password_skipped(self, client, journal_dir): 124 + def test_put_empty_password_skipped(self, client, journal_copy): 135 125 """PUT with empty password does not overwrite existing hash.""" 136 - config_before = _read_config(journal_dir) 126 + config_before = _read_config(journal_copy) 137 127 hash_before = config_before["convey"]["password_hash"] 138 128 139 129 resp = client.put( ··· 142 132 content_type="application/json", 143 133 ) 144 134 assert resp.status_code == 200 145 - config_after = _read_config(journal_dir) 135 + config_after = _read_config(journal_copy) 146 136 assert config_after["convey"]["password_hash"] == hash_before
+10 -22
tests/test_password_cli.py
··· 6 6 from __future__ import annotations 7 7 8 8 import json 9 - import shutil 10 - from pathlib import Path 11 9 12 10 import pytest 13 11 from werkzeug.security import check_password_hash ··· 15 13 from think.password_cli import main 16 14 17 15 18 - @pytest.fixture 19 - def journal_dir(tmp_path, monkeypatch): 20 - """Copy test fixture to temp dir for mutation tests.""" 21 - src = Path(__file__).resolve().parent / "fixtures" / "journal" 22 - dst = tmp_path / "journal" 23 - shutil.copytree(src, dst, symlinks=True) 24 - monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(dst)) 25 - return dst 26 - 27 - 28 16 def _read_config(journal_dir): 29 17 return json.loads((journal_dir / "config" / "journal.json").read_text()) 30 18 ··· 36 24 37 25 38 26 class TestSetPassword: 39 - def test_set_writes_hash(self, journal_dir, monkeypatch, capsys): 27 + def test_set_writes_hash(self, journal_copy, monkeypatch, capsys): 40 28 monkeypatch.setattr("sys.argv", ["sol password", "set"]) 41 29 _mock_getpass(monkeypatch, "mypassword", "mypassword") 42 30 43 31 main() 44 32 45 - config = _read_config(journal_dir) 33 + config = _read_config(journal_copy) 46 34 assert config["convey"]["password_hash"].startswith("scrypt:") 47 35 assert check_password_hash(config["convey"]["password_hash"], "mypassword") 48 36 assert "Password set successfully." in capsys.readouterr().out 49 37 50 - def test_mismatch_rejected(self, journal_dir, monkeypatch, capsys): 38 + def test_mismatch_rejected(self, journal_copy, monkeypatch, capsys): 51 39 monkeypatch.setattr("sys.argv", ["sol password", "set"]) 52 40 _mock_getpass(monkeypatch, "password1", "different") 53 41 ··· 57 45 assert exc_info.value.code == 1 58 46 assert "Passwords do not match." in capsys.readouterr().err 59 47 60 - def test_plaintext_cleanup(self, journal_dir, monkeypatch): 48 + def test_plaintext_cleanup(self, journal_copy, monkeypatch): 61 49 # Seed a plaintext password 62 - config_path = journal_dir / "config" / "journal.json" 50 + config_path = journal_copy / "config" / "journal.json" 63 51 config = json.loads(config_path.read_text()) 64 52 config["convey"]["password"] = "old-plaintext" 65 53 config_path.write_text(json.dumps(config, indent=2)) ··· 69 57 70 58 main() 71 59 72 - config = _read_config(journal_dir) 60 + config = _read_config(journal_copy) 73 61 assert "password" not in config["convey"] 74 62 assert "password_hash" in config["convey"] 75 63 ··· 87 75 assert check_password_hash(config["convey"]["password_hash"], "freshpass") 88 76 assert config_path.stat().st_mode & 0o777 == 0o600 89 77 90 - def test_file_permissions(self, journal_dir, monkeypatch): 78 + def test_file_permissions(self, journal_copy, monkeypatch): 91 79 monkeypatch.setattr("sys.argv", ["sol password", "set"]) 92 80 _mock_getpass(monkeypatch, "securepass", "securepass") 93 81 94 82 main() 95 83 96 - config_path = journal_dir / "config" / "journal.json" 84 + config_path = journal_copy / "config" / "journal.json" 97 85 assert config_path.stat().st_mode & 0o777 == 0o600 98 86 99 87 100 88 class TestResetAlias: 101 - def test_reset_writes_hash(self, journal_dir, monkeypatch, capsys): 89 + def test_reset_writes_hash(self, journal_copy, monkeypatch, capsys): 102 90 monkeypatch.setattr("sys.argv", ["sol password", "reset"]) 103 91 _mock_getpass(monkeypatch, "resetpass", "resetpass") 104 92 105 93 main() 106 94 107 - config = _read_config(journal_dir) 95 + config = _read_config(journal_copy) 108 96 assert check_password_hash(config["convey"]["password_hash"], "resetpass") 109 97 assert "Password set successfully." in capsys.readouterr().out
+3 -10
tests/test_validate_key.py
··· 4 4 from __future__ import annotations 5 5 6 6 import json 7 - import shutil 8 - from pathlib import Path 9 7 from unittest.mock import Mock, patch 10 8 11 9 import pytest ··· 19 17 20 18 21 19 @pytest.fixture 22 - def settings_client(tmp_path, monkeypatch): 23 - src = Path(__file__).resolve().parent / "fixtures" / "journal" 24 - journal = tmp_path / "journal" 25 - shutil.copytree(src, journal, symlinks=True) 26 - monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) 27 - 28 - app = create_app(str(journal)) 20 + def settings_client(journal_copy): 21 + app = create_app(str(journal_copy)) 29 22 app.config["TESTING"] = True 30 - return app.test_client(), journal 23 + return app.test_client(), journal_copy 31 24 32 25 33 26 def test_validate_key_anthropic_success():