diff --git a/CHANGELOG.md b/CHANGELOG.md index 628843b59..05cf9c0a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to solstone (the Python package) will be documented in this Format adapted from [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), aligned with `cmo/brand/changelog-voice.md`. +## [0.6.5] - 2026-06-16 + +### Changed +- the local web interface no longer has its own password, login page, session cookie, or localhost-trust switch. once setup is complete, the local interface serves directly on the journal machine; linked devices continue to use their paired-device identity through link. + ## [0.6.4] - 2026-06-15 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 44b05c5ad..562956004 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -66,13 +66,12 @@ journal setup The source-checkout journal lives at `journal/` inside the repo unless you pass `--journal` or have already configured another path. -Configure API keys and the web password in `journal/config/journal.json`. This file is the only key configuration method for source-checkout development: +Configure API keys in `journal/config/journal.json`. This file is the only key configuration method for source-checkout development: ```bash mkdir -p journal/config cat > journal/config/journal.json << 'EOF' { - "convey": {}, "env": { "GOOGLE_API_KEY": "your-key-here" } @@ -81,11 +80,10 @@ EOF chmod 600 journal/config/journal.json ``` -Run `journal password set` to configure web authentication. Replace `your-key-here` with your Google AI API key. Optional provider keys can be added to the same `env` object: +Replace `your-key-here` with your Google AI API key. Optional provider keys can be added to the same `env` object: ```json { - "convey": {}, "env": { "GOOGLE_API_KEY": "your-gemini-key", "OPENAI_API_KEY": "your-openai-key", diff --git a/INSTALL.md b/INSTALL.md index 612a162be..ce7729392 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -63,7 +63,7 @@ journal setup this runs the setup readiness doctor battery, confirms the journal directory at `~/journal`, installs the local transcription model (~2.5 GB on linux), installs the `sol` skill for claude code, codex, and gemini, installs the journal-side `sol` and `journal` router skills so sol can tend the journal, and starts a background service (systemd on linux, launchd on macOS) listening on http://localhost:5015. -let your human know: **open http://localhost:5015 in a browser**. the first-run wizard walks them through setting their identity and connecting a gemini API key. an optional password can be configured later in settings → security. +let your human know: **open http://localhost:5015 in a browser**. the first-run wizard walks them through setting their identity and connecting a gemini API key. a `solstone[journal]` install bundles everything a journal host needs — PDF rendering, whisper, and the default CPU transcription stack are all included; `journal setup` downloads the transcription model. there are no separate à-la-carte extras to add. if the readiness doctor step (`sol doctor --readiness`) finds missing system libraries, it will tell you the exact install command to run for your platform. diff --git a/README.md b/README.md index 7d110b025..d69c4d9a0 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ journal setup want only the thin `sol` client — to talk to a journal running elsewhere? `uv tool install solstone` (no extras), or `uvx solstone` for an ephemeral one-shot. -then open http://localhost:5015 in a browser; the first-run wizard handles identity and the gemini API key. an optional password can be configured later in settings → security. +then open http://localhost:5015 in a browser; the first-run wizard handles identity and the gemini API key. see [INSTALL.md](INSTALL.md) for prerequisites, observer install, and troubleshooting; see [CONTRIBUTING.md](CONTRIBUTING.md) if you want to develop on solstone from a source checkout. diff --git a/docs/CONVEY.md b/docs/CONVEY.md index 2b290cf4a..2da4daa30 100644 --- a/docs/CONVEY.md +++ b/docs/CONVEY.md @@ -16,19 +16,9 @@ Run the server with: convey ``` -### Authentication - -Password authentication is configured from Settings → Security. For headless setups, use the CLI: - -```bash -journal password set -``` - -When a password is set, it is stored as a secure hash in `config/journal.json` under `convey.password_hash`. - ## Architecture -Convey uses an **app plugin system** where all functional views are implemented as independent apps in the `/apps/` directory. The core `solstone/convey/` package provides authentication, WebSocket communication, and the app loading infrastructure. +Convey uses an **app plugin system** where all functional views are implemented as independent apps in the `/apps/` directory. The core `solstone/convey/` package provides access gating, WebSocket communication, and the app loading infrastructure. ``` convey/ @@ -36,14 +26,11 @@ convey/ state.py - global state (journal_root) bridge.py - Callosum WebSocket bridge for real-time events utils.py - shared helpers (format_date, spawn_agent, etc.) - views/ - __init__.py - blueprint registration - home.py - authentication (login/logout) and root redirect + root.py - access gate, setup routes, and root redirect templates/ app.html - main app container template menu_bar.html - dynamic left sidebar menu status_pane.html - WebSocket status indicator - login.html - login page macros.html - Jinja macros static/ - shared CSS and JavaScript app.css - app system styles @@ -73,11 +60,9 @@ Browse `/apps/` to see available apps. ### Core Routes -The `solstone/convey/views/home.py` module provides essential routes: +The `solstone/convey/root.py` module provides essential routes: - `/` - Redirects to `/app/home/` -- `/login` - Authentication page -- `/logout` - Clear session and redirect to login - `/favicon.ico` - Serve favicon All functional views are accessed at `/app/{name}/` URLs. @@ -123,7 +108,7 @@ from a client-supplied field. Key-based ingest authenticates with an `Authorization: Bearer` header — never a key in the URL path — and derives its storage scope from the authenticated record; a scope id in the URL is an assertion to check against that record, not an input. Any key-authenticated route must be in -the `require_login` allowlist. +the `require_access` allowlist. **Pagination.** Use `parse_pagination_params()` (offset/limit, max 100) or a cursor; no list endpoint returns an unbounded full array. diff --git a/docs/design/providers-panel-consolidation.md b/docs/design/providers-panel-consolidation.md index e5d615e22..b90ad5a8b 100644 --- a/docs/design/providers-panel-consolidation.md +++ b/docs/design/providers-panel-consolidation.md @@ -181,7 +181,7 @@ Do not add persisted local active-model state in this lode. That would be a sele | Endpoint payload | New `solstone/apps/settings/tests/test_providers_payload_extended.py` | Use the `settings_client` pattern from `test_workspace_install_copy_template.py:15-26`; `GET /api/providers`; assert top-level `local` and `mlx` dicts exist; each includes the 7 `InstallStatus` fields and `install_state` is in the canonical vocabulary from `InstallState` (`install_state.py:11-19`). | | Visual smoke | New `solstone/apps/settings/tests/test_providers_panel_visual.py` | Mirror the werkzeug + pytest-playwright pattern in `test_workspace_qr_size.py:22-52`; navigate to `/app/settings/`; assert one panel renders five cards for `anthropic`, `openai`, `openhands`, `local`, `mlx`; assert each card badge text is from `INSTALL_COPY`; assert retired DOM ids are absent. Mark `@pytest.mark.integration`. | -Visual smoke data source: use a clean temporary settings journal with `setup.completed_at` and `convey.trust_localhost`, following `settings_client` (`test_workspace_install_copy_template.py:15-26`) rather than monkeypatching provider state. Clean fixture is preferred because it exercises the real route and template path. +Visual smoke data source: use a clean temporary settings journal with `setup.completed_at`, following `settings_client` (`test_workspace_install_copy_template.py:15-26`) rather than monkeypatching provider state. Clean fixture is preferred because it exercises the real route and template path. Playwright precondition: `pytest-playwright` is already in dev dependencies (`pyproject.toml:181-189`), and `make install` installs Chromium (`Makefile:54-58`). Running the visual smoke outside the installed dev env requires the same `playwright install chromium` precondition. diff --git a/solstone/apps/activities/tests/conftest.py b/solstone/apps/activities/tests/conftest.py index c81ea9c2d..ba1ad7e9a 100644 --- a/solstone/apps/activities/tests/conftest.py +++ b/solstone/apps/activities/tests/conftest.py @@ -28,7 +28,6 @@ def activities_env(tmp_path, monkeypatch): (config_dir / "journal.json").write_text( json.dumps( { - "convey": {"trust_localhost": True}, "setup": {"completed_at": 1700000000000}, } ) diff --git a/solstone/apps/activities/tests/test_routes.py b/solstone/apps/activities/tests/test_routes.py index 40e78768f..c651a9cfb 100644 --- a/solstone/apps/activities/tests/test_routes.py +++ b/solstone/apps/activities/tests/test_routes.py @@ -12,7 +12,7 @@ ROOT = Path(__file__).resolve().parents[4] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) -from tests._baseline_harness import make_logged_in_test_client +from tests._baseline_harness import make_test_client def test_day_activities_returns_collection_envelope(activities_env): @@ -50,7 +50,7 @@ def test_day_activities_empty_day_returns_empty_envelope(activities_env): def test_create_record_rejects_empty_title(activities_env): journal, facet, day, _day_path = activities_env(None) - client = make_logged_in_test_client(journal) + client = make_test_client(journal) response = client.post( f"/app/activities/api/day/{day}/records?facet={facet}", @@ -63,7 +63,7 @@ def test_create_record_rejects_empty_title(activities_env): def test_create_record_rejects_invalid_source(activities_env): journal, facet, day, _day_path = activities_env(None) - client = make_logged_in_test_client(journal) + client = make_test_client(journal) response = client.post( f"/app/activities/api/day/{day}/records?facet={facet}", diff --git a/solstone/apps/backup/tests/conftest.py b/solstone/apps/backup/tests/conftest.py index a021d9199..af87ad577 100644 --- a/solstone/apps/backup/tests/conftest.py +++ b/solstone/apps/backup/tests/conftest.py @@ -41,7 +41,6 @@ def backup_env(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): (config_dir / "journal.json").write_text( json.dumps( { - "convey": {"trust_localhost": True}, "setup": {"completed_at": 1700000000000}, }, indent=2, @@ -55,9 +54,6 @@ def backup_env(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): app = create_app(journal=str(journal)) app.config["TESTING"] = True client = app.test_client() - with client.session_transaction() as session: - session["logged_in"] = True - session.permanent = True return Env(journal=journal, client=client, app=app) return _create diff --git a/solstone/apps/backup/tests/test_flows.py b/solstone/apps/backup/tests/test_flows.py index 2caa99a07..6778ee16e 100644 --- a/solstone/apps/backup/tests/test_flows.py +++ b/solstone/apps/backup/tests/test_flows.py @@ -21,6 +21,7 @@ def _config_path(env) -> Path: def _write_config(env, payload: dict) -> None: + payload.setdefault("setup", {"completed_at": 1700000000000}) _config_path(env).write_text(json.dumps(payload), encoding="utf-8") diff --git a/solstone/apps/chat/tests/test_call.py b/solstone/apps/chat/tests/test_call.py index 1ccaefe95..025a7a2fe 100644 --- a/solstone/apps/chat/tests/test_call.py +++ b/solstone/apps/chat/tests/test_call.py @@ -20,7 +20,7 @@ import solstone.convey.sol_initiated.start as sol_start from solstone.apps.chat.call import app from solstone.convey.chat_stream import read_chat_events from solstone.think.convey_client import ConveyClient -from tests._baseline_harness import make_logged_in_test_client +from tests._baseline_harness import make_test_client FROZEN_MS = 1_700_000_000_000 FROZEN_DAY = datetime.fromtimestamp(FROZEN_MS / 1000).strftime("%Y%m%d") @@ -33,7 +33,13 @@ def journal(tmp_path, monkeypatch): config_path = tmp_path / "config" / "journal.json" config_path.parent.mkdir(parents=True, exist_ok=True) config_path.write_text( - json.dumps({"sol_voice": {"rate_floor_minutes": 0}}) + "\n", + json.dumps( + { + "setup": {"completed_at": 1700000000000}, + "sol_voice": {"rate_floor_minutes": 0}, + } + ) + + "\n", encoding="utf-8", ) return tmp_path @@ -42,7 +48,7 @@ def journal(tmp_path, monkeypatch): @pytest.fixture def runner(journal, monkeypatch): monkeypatch.setattr(sol_start, "now_ms", lambda: FROZEN_MS) - client = ConveyClient(session=make_logged_in_test_client(journal), base_url="") + client = ConveyClient(session=make_test_client(journal), base_url="") monkeypatch.setattr("solstone.apps.chat.call.get_client", lambda: client) return CliRunner() diff --git a/solstone/apps/chat/tests/test_liveness_chat_bar.py b/solstone/apps/chat/tests/test_liveness_chat_bar.py index 9252a0a02..d1d7894a0 100644 --- a/solstone/apps/chat/tests/test_liveness_chat_bar.py +++ b/solstone/apps/chat/tests/test_liveness_chat_bar.py @@ -20,7 +20,6 @@ def chat_client(tmp_path, monkeypatch): json.dumps( { "setup": {"completed_at": "2026-05-09T00:00:00Z"}, - "convey": {"trust_localhost": True}, } ) + "\n", @@ -30,9 +29,6 @@ def chat_client(tmp_path, monkeypatch): app = create_app(str(journal)) app.config["TESTING"] = True client = app.test_client() - with client.session_transaction() as session: - session["logged_in"] = True - session.permanent = True return client diff --git a/solstone/apps/chat/tests/test_liveness_live.py b/solstone/apps/chat/tests/test_liveness_live.py index 4fbfc081a..00d67fb77 100644 --- a/solstone/apps/chat/tests/test_liveness_live.py +++ b/solstone/apps/chat/tests/test_liveness_live.py @@ -19,7 +19,6 @@ def chat_client(tmp_path, monkeypatch): json.dumps( { "setup": {"completed_at": "2026-05-09T00:00:00Z"}, - "convey": {"trust_localhost": True}, } ) + "\n", @@ -29,9 +28,6 @@ def chat_client(tmp_path, monkeypatch): app = create_app(str(journal)) app.config["TESTING"] = True client = app.test_client() - with client.session_transaction() as session: - session["logged_in"] = True - session.permanent = True return client diff --git a/solstone/apps/chat/tests/test_origin_tag_live.py b/solstone/apps/chat/tests/test_origin_tag_live.py index bfa5cb318..5f10d1408 100644 --- a/solstone/apps/chat/tests/test_origin_tag_live.py +++ b/solstone/apps/chat/tests/test_origin_tag_live.py @@ -25,7 +25,6 @@ def chat_client(tmp_path, monkeypatch): json.dumps( { "setup": {"completed_at": "2026-05-09T00:00:00Z"}, - "convey": {"trust_localhost": True}, } ) + "\n", @@ -35,9 +34,6 @@ def chat_client(tmp_path, monkeypatch): app = create_app(str(journal)) app.config["TESTING"] = True client = app.test_client() - with client.session_transaction() as session: - session["logged_in"] = True - session.permanent = True return client diff --git a/solstone/apps/chat/tests/test_origin_tag_ssr.py b/solstone/apps/chat/tests/test_origin_tag_ssr.py index fd1f52cf8..e78a36b56 100644 --- a/solstone/apps/chat/tests/test_origin_tag_ssr.py +++ b/solstone/apps/chat/tests/test_origin_tag_ssr.py @@ -34,7 +34,6 @@ def chat_env(tmp_path, monkeypatch): json.dumps( { "setup": {"completed_at": "2026-05-09T00:00:00Z"}, - "convey": {"trust_localhost": True}, "identity": {"preferred": "Owner"}, "agent": {"name": "sol"}, } @@ -46,9 +45,6 @@ def chat_env(tmp_path, monkeypatch): app = create_app(str(journal)) app.config["TESTING"] = True client = app.test_client() - with client.session_transaction() as session: - session["logged_in"] = True - session.permanent = True return client, journal diff --git a/solstone/apps/chat/tests/test_routes.py b/solstone/apps/chat/tests/test_routes.py index b309b2fd4..a3f0deb87 100644 --- a/solstone/apps/chat/tests/test_routes.py +++ b/solstone/apps/chat/tests/test_routes.py @@ -49,9 +49,6 @@ def _make_env(journal, monkeypatch) -> ChatTestEnv: app = create_app(str(journal)) app.config["TESTING"] = True client = app.test_client() - with client.session_transaction() as session: - session["logged_in"] = True - session.permanent = True return ChatTestEnv(client=client, journal=journal) diff --git a/solstone/apps/curation/tests/conftest.py b/solstone/apps/curation/tests/conftest.py index 204e2c41b..b3cb2f16c 100644 --- a/solstone/apps/curation/tests/conftest.py +++ b/solstone/apps/curation/tests/conftest.py @@ -24,7 +24,6 @@ def curation_env(tmp_path, monkeypatch): (config_dir / "journal.json").write_text( json.dumps( { - "convey": {"trust_localhost": True}, "setup": {"completed_at": 1700000000000}, }, indent=2, diff --git a/solstone/apps/entities/tests/test_delete_journal_entity.py b/solstone/apps/entities/tests/test_delete_journal_entity.py index bd4df28a0..a333bf759 100644 --- a/solstone/apps/entities/tests/test_delete_journal_entity.py +++ b/solstone/apps/entities/tests/test_delete_journal_entity.py @@ -111,7 +111,7 @@ def test_cancel_delete_journal_entity_within_window_keeps_entity( ): entity_id = "cancel-delete-test" _create_journal_entity(entity_id) - monkeypatch.setattr("solstone.apps.entities.routes.ENTITY_DELETE_TTL", 0.2) + monkeypatch.setattr("solstone.apps.entities.routes.ENTITY_DELETE_TTL", 1.0) delete_response = client.delete(f"/app/entities/api/journal/entity/{entity_id}") pending_id = delete_response.get_json()["pending"] diff --git a/solstone/apps/entities/tests/test_merge.py b/solstone/apps/entities/tests/test_merge.py index 808bf4073..c2594928e 100644 --- a/solstone/apps/entities/tests/test_merge.py +++ b/solstone/apps/entities/tests/test_merge.py @@ -17,7 +17,7 @@ from solstone.apps.entities.call import app as entities_app from solstone.think.convey_client import ConveyClient from solstone.think.entities import merge as merge_mod from solstone.think.entities.journal import load_journal_entity -from tests._baseline_harness import make_logged_in_test_client +from tests._baseline_harness import make_test_client runner = CliRunner() STREAM = "test" @@ -28,7 +28,7 @@ def _entities_client(monkeypatch: pytest.MonkeyPatch) -> None: def client() -> ConveyClient: journal = Path(os.environ["SOLSTONE_JOURNAL"]) return ConveyClient( - session=make_logged_in_test_client(journal), + session=make_test_client(journal), base_url="", ) diff --git a/solstone/apps/facets/tests/test_call.py b/solstone/apps/facets/tests/test_call.py index 2c13ef2bf..e0f99a5dc 100644 --- a/solstone/apps/facets/tests/test_call.py +++ b/solstone/apps/facets/tests/test_call.py @@ -23,18 +23,19 @@ from solstone.think.facet_review_candidates import ( record_facet_candidate, ) from solstone.think.journal_io import LockTimeout -from tests._baseline_harness import make_logged_in_test_client +from tests._baseline_harness import make_test_client, mark_setup_complete @pytest.fixture def journal(tmp_path, monkeypatch): monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) + mark_setup_complete(tmp_path) return tmp_path @pytest.fixture def runner(journal, monkeypatch): - client = ConveyClient(session=make_logged_in_test_client(journal), base_url="") + client = ConveyClient(session=make_test_client(journal), base_url="") monkeypatch.setattr("solstone.apps.facets.call.get_client", lambda: client) return CliRunner() diff --git a/solstone/apps/health/tests/conftest.py b/solstone/apps/health/tests/conftest.py index 0704b33ae..9785bfaf9 100644 --- a/solstone/apps/health/tests/conftest.py +++ b/solstone/apps/health/tests/conftest.py @@ -27,7 +27,6 @@ def health_env(tmp_path, monkeypatch): config_file.write_text( json.dumps( { - "convey": {"trust_localhost": True}, "setup": {"completed_at": 1700000000000}, }, indent=2, diff --git a/solstone/apps/link/copy.py b/solstone/apps/link/copy.py index 638bd20ec..87f95c430 100644 --- a/solstone/apps/link/copy.py +++ b/solstone/apps/link/copy.py @@ -50,8 +50,6 @@ SUCCESS_VERIFY_NOTE_ANYWHERE = ( "fingerprint should match what it shows. didn't do this?" ) SUCCESS_REMOVE_LABEL = "that wasn't me — remove" -PAIR_WEB_PASSWORD_SETTINGS_LINK = "set a web password for this page in settings →" - # --- U4 first-run hero --- HERO_TITLE = "let's connect a device" HERO_BODY = ( diff --git a/solstone/apps/link/routes.py b/solstone/apps/link/routes.py index 9a1984136..12514e62c 100644 --- a/solstone/apps/link/routes.py +++ b/solstone/apps/link/routes.py @@ -115,7 +115,7 @@ from solstone.think.pairing.config import ( ) from solstone.think.services import operations, spl, spl_handoff from solstone.think.services import status as service_status -from solstone.think.utils import get_config, get_journal, now_ms +from solstone.think.utils import get_journal, now_ms logger = logging.getLogger(__name__) MANUAL_CODE_RE = re.compile(rf"^[0-9A-HJKMNP-TV-Z]{{{MANUAL_CODE_LEN}}}$") @@ -173,11 +173,6 @@ def _is_loopback_request() -> bool: return request.remote_addr in {"127.0.0.1", "::1"} -def _convey_password_is_set() -> bool: - password_hash = get_config().get("convey", {}).get("password_hash", "") - return bool(str(password_hash or "").strip()) - - def _read_link_connection_event() -> str | None: event = get_cached_state().get("link_connection") return event if isinstance(event, str) else None @@ -404,7 +399,6 @@ def api_status() -> Any: "enrolled": token_present, "relay_url": relay_url(), "ca_fingerprint": ca_fp, - "has_password": _convey_password_is_set(), "lan_accessible": lan_accessible, "posture": posture, "reachability": reachability, diff --git a/solstone/apps/link/tests/conftest.py b/solstone/apps/link/tests/conftest.py index b691b2612..8329f9da7 100644 --- a/solstone/apps/link/tests/conftest.py +++ b/solstone/apps/link/tests/conftest.py @@ -27,7 +27,6 @@ def link_env(tmp_path, monkeypatch): config_dir.mkdir(parents=True, exist_ok=True) config_file = config_dir / "journal.json" config = { - "convey": {"trust_localhost": True}, "setup": {"completed_at": 1700000000000}, } if posture is not None: diff --git a/solstone/apps/link/tests/test_api_status.py b/solstone/apps/link/tests/test_api_status.py index 083d473ac..b98c07b62 100644 --- a/solstone/apps/link/tests/test_api_status.py +++ b/solstone/apps/link/tests/test_api_status.py @@ -21,7 +21,6 @@ TOTP_SECRET = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ" def _write_config(env: Any, *, link: Any = None, include_link: bool = True) -> None: config: dict[str, Any] = { - "convey": {"trust_localhost": True}, "setup": {"completed_at": 1700000000000}, } if include_link: @@ -324,7 +323,6 @@ def test_back_compat_field_set(link_env, monkeypatch) -> None: "enrolled", "relay_url", "ca_fingerprint", - "has_password", "lan_accessible", "posture", "reachability", @@ -337,7 +335,6 @@ def test_back_compat_field_set(link_env, monkeypatch) -> None: assert isinstance(data["enrolled"], bool) assert isinstance(data["relay_url"], str) assert isinstance(data["ca_fingerprint"], str) or data["ca_fingerprint"] is None - assert isinstance(data["has_password"], bool) assert isinstance(data["lan_accessible"], bool) @@ -359,7 +356,6 @@ def test_api_status_unprovisioned(link_env, monkeypatch) -> None: "enrolled", "relay_url", "ca_fingerprint", - "has_password", "lan_accessible", "posture", "reachability", @@ -396,7 +392,6 @@ def test_cli_status_unprovisioned_does_not_write_state(tmp_path, monkeypatch) -> (config_dir / "journal.json").write_text( json.dumps( { - "convey": {"trust_localhost": True}, "setup": {"completed_at": 1700000000000}, }, indent=2, @@ -407,9 +402,6 @@ def test_cli_status_unprovisioned_does_not_write_state(tmp_path, monkeypatch) -> app = create_app(journal=str(journal)) app.config["TESTING"] = True test_client = app.test_client() - with test_client.session_transaction() as session: - session["logged_in"] = True - session.permanent = True client = ConveyClient(session=test_client, base_url="") monkeypatch.setattr(link_call, "get_client", lambda: client) @@ -426,19 +418,3 @@ def test_cli_status_unprovisioned_does_not_write_state(tmp_path, monkeypatch) -> ) assert "Home label: (not provisioned)" in result.stdout assert not (journal / "link" / "state.json").exists() - - -def test_status_reports_convey_password_state(link_env, monkeypatch) -> None: - env = link_env() - monkeypatch.setattr(link_routes, "_detect_lan_ip", lambda: "192.168.1.50") - - data = _get_status(env) - assert data["has_password"] is False - - config_path = env.journal / "config" / "journal.json" - config = json.loads(config_path.read_text("utf-8")) - config.setdefault("convey", {})["password_hash"] = "hashed" - config_path.write_text(json.dumps(config, indent=2), encoding="utf-8") - - data = _get_status(env) - assert data["has_password"] is True diff --git a/solstone/apps/link/tests/test_reach_copy.py b/solstone/apps/link/tests/test_reach_copy.py index ab7aa85fd..97a9ce160 100644 --- a/solstone/apps/link/tests/test_reach_copy.py +++ b/solstone/apps/link/tests/test_reach_copy.py @@ -147,13 +147,6 @@ def test_reach_shell_corrected_copy_is_locked() -> None: assert copy.PRIVATE_LINK_RETRY_CTA == "try again" -def test_pair_web_password_settings_link_is_locked() -> None: - assert ( - copy.PAIR_WEB_PASSWORD_SETTINGS_LINK - == "set a web password for this page in settings →" - ) - - def test_reach_shell_copy_stays_in_bounds() -> None: banned_terms = ( "sign in", @@ -174,7 +167,6 @@ def test_reach_shell_copy_stays_in_bounds() -> None: for value in [ *U2_COPY_VALUES, - copy.PAIR_WEB_PASSWORD_SETTINGS_LINK, ]: lowered = value.lower() for term in banned_terms: diff --git a/solstone/apps/link/tests/test_workspace_copy.py b/solstone/apps/link/tests/test_workspace_copy.py index 34cf255d9..05489e853 100644 --- a/solstone/apps/link/tests/test_workspace_copy.py +++ b/solstone/apps/link/tests/test_workspace_copy.py @@ -29,7 +29,6 @@ MODAL_COPY_VALUES = [ copy.SUCCESS_DONE, copy.SUCCESS_VERIFY_NOTE, copy.SUCCESS_REMOVE_LABEL, - copy.PAIR_WEB_PASSWORD_SETTINGS_LINK, ] diff --git a/solstone/apps/link/workspace.html b/solstone/apps/link/workspace.html index d45f2305c..80f7d6c21 100644 --- a/solstone/apps/link/workspace.html +++ b/solstone/apps/link/workspace.html @@ -228,7 +228,6 @@ {{ link_copy.REACH_HOME_ADDRESS_LABEL }}

-
data
-
@@ -2648,38 +2646,6 @@ button:focus:not(:focus-visible) { - -
-

security

-

{{ convey_copy.SETTINGS_SECURITY_DESC }}

-

{{ convey_copy.SETTINGS_SECURITY_REACH_HINT }}

- -
- -
-
- - trust localhost -
- -
- -
- -
- - -
- {{ convey_copy.SETTINGS_PASSWORD_HINT }} -
-
-
-

sync

@@ -3197,7 +3163,7 @@ setupModalAccessibility('customActivityModal'); setupModalAccessibility('cleanupModal'); // ========== NAVIGATION ========== -const VALID_SECTIONS = ['guide', 'profile', 'agent', 'apikeys', 'transcription', 'observer', 'vision', 'sol-voice', 'chat', 'security', 'sync', 'storage', 'support', 'facets', 'facet-appearance', 'facet-activities', 'facet-activity']; + const VALID_SECTIONS = ['guide', 'profile', 'agent', 'apikeys', 'transcription', 'observer', 'vision', 'sol-voice', 'chat', 'sync', 'storage', 'support', 'facets', 'facet-appearance', 'facet-activities', 'facet-activity']; function switchSection(sectionId, updateHash = true) { // Validate section exists @@ -3587,11 +3553,6 @@ function populateFields(config) { document.getElementById('field-transcribe-preserve').checked = transcribe.preserve_all || false; document.getElementById('field-transcribe-noise-upgrade').checked = transcribe.noise_upgrade !== false; - // Convey - const convey = config.convey || {}; - document.getElementById('field-trust-localhost').checked = convey.trust_localhost !== false; - renderConveyHostFields(config); - // Support settings const support = config.support || {}; document.getElementById('field-support-enabled').checked = support.enabled !== false; @@ -3984,18 +3945,6 @@ function readAutoSaveControlValue(el) { function applySavedConfigResult(result, runtimeEnv) { configData = result.config; configData.runtime_env = runtimeEnv; - renderConveyHostFields(configData); -} - -function renderConveyHostFields(config) { - const convey = config?.convey || {}; - const passwordInput = document.getElementById('field-password'); - - if (passwordInput) { - passwordInput.placeholder = convey.has_password - ? 'Password set (enter to change)' - : 'Enter password to protect web access'; - } } async function saveConfigValue(section, key, value) { @@ -4585,10 +4534,11 @@ document.getElementById('createPersonalBtn').onclick = async () => { }; // ========== PASSWORD/KEY TOGGLE ========== -// Handle all password toggle buttons (password field + API key fields) +// Handle all password toggle buttons for API key fields document.querySelectorAll('.password-toggle').forEach(btn => { btn.onclick = () => { - const targetId = btn.dataset.toggle || 'field-password'; + const targetId = btn.dataset.toggle; + if (!targetId) return; const input = document.getElementById(targetId); const icon = btn.querySelector('span'); if (input && icon) { diff --git a/solstone/apps/speakers/tests/conftest.py b/solstone/apps/speakers/tests/conftest.py index 525d5025e..101d05aa0 100644 --- a/solstone/apps/speakers/tests/conftest.py +++ b/solstone/apps/speakers/tests/conftest.py @@ -51,6 +51,12 @@ def speakers_env(tmp_path, monkeypatch): self.journal = journal_path monkeypatch.setenv("SOLSTONE_JOURNAL", str(journal_path)) monkeypatch.setenv("SOL_SKIP_SUPERVISOR_CHECK", "1") + config_path = journal_path / "config" / "journal.json" + config_path.parent.mkdir(parents=True, exist_ok=True) + config_path.write_text( + json.dumps({"setup": {"completed_at": 1700000000000}}) + "\n", + encoding="utf-8", + ) import solstone.think.utils as think_utils think_utils._journal_path_cache = None diff --git a/solstone/apps/stats/static/dashboard.js b/solstone/apps/stats/static/dashboard.js index 37326ffe6..056ac2873 100644 --- a/solstone/apps/stats/static/dashboard.js +++ b/solstone/apps/stats/static/dashboard.js @@ -976,12 +976,12 @@ const Dashboard = (function() { return { load: function(url) { fetch(url, { - credentials: 'same-origin' // Include cookies for authentication + credentials: 'same-origin' }) .then(response => { if (!response.ok) { if (response.status === 401 || response.redirected) { - // Redirected to login, reload the page + // Access changed while viewing the dashboard; reload the page. window.location.reload(); return; } diff --git a/solstone/apps/stats/tests/test_stats_data_route.py b/solstone/apps/stats/tests/test_stats_data_route.py index bebc9579b..837262aa0 100644 --- a/solstone/apps/stats/tests/test_stats_data_route.py +++ b/solstone/apps/stats/tests/test_stats_data_route.py @@ -19,7 +19,6 @@ def stats_client(tmp_path, monkeypatch): (config_dir / "journal.json").write_text( json.dumps( { - "convey": {"trust_localhost": True}, "setup": {"completed_at": 1700000000000}, }, indent=2, diff --git a/solstone/apps/stats/tests/test_workspace_template.py b/solstone/apps/stats/tests/test_workspace_template.py index 850fc247e..12e1d0de4 100644 --- a/solstone/apps/stats/tests/test_workspace_template.py +++ b/solstone/apps/stats/tests/test_workspace_template.py @@ -106,7 +106,6 @@ def stats_env(tmp_path, monkeypatch): config_file.write_text( json.dumps( { - "convey": {"trust_localhost": True}, "setup": {"completed_at": 1700000000000}, }, indent=2, diff --git a/solstone/apps/thinking/tests/test_local_bootstrap_routes.py b/solstone/apps/thinking/tests/test_local_bootstrap_routes.py index f778dec0b..5d2806261 100644 --- a/solstone/apps/thinking/tests/test_local_bootstrap_routes.py +++ b/solstone/apps/thinking/tests/test_local_bootstrap_routes.py @@ -37,7 +37,6 @@ def _client(journal_path): def _settings_config() -> dict: return { "setup": {"completed_at": "2026-05-09T00:00:00Z"}, - "convey": {"trust_localhost": True}, "providers": { "generate": {"provider": "google", "tier": 2, "backup": "anthropic"}, "cogitate": {"provider": "openai", "tier": 2, "backup": "anthropic"}, diff --git a/solstone/apps/thinking/tests/test_local_endpoint_routes.py b/solstone/apps/thinking/tests/test_local_endpoint_routes.py index 7dc5b6044..2ac88347b 100644 --- a/solstone/apps/thinking/tests/test_local_endpoint_routes.py +++ b/solstone/apps/thinking/tests/test_local_endpoint_routes.py @@ -37,7 +37,6 @@ def _write_config(journal_path: Path, config: dict[str, Any]) -> None: def _ready_settings_env(settings_env) -> tuple[Path, dict[str, Any]]: journal_path, config = settings_env() config["setup"] = {"completed_at": "2026-05-23T00:00:00Z"} - config.setdefault("convey", {})["trust_localhost"] = True _write_config(journal_path, config) return journal_path, config diff --git a/solstone/apps/thinking/tests/test_providers_payload_extended.py b/solstone/apps/thinking/tests/test_providers_payload_extended.py index 1f6c5f4a0..1c232e62d 100644 --- a/solstone/apps/thinking/tests/test_providers_payload_extended.py +++ b/solstone/apps/thinking/tests/test_providers_payload_extended.py @@ -42,7 +42,6 @@ def settings_client_with_journal(settings_env): def _settings_client_with_journal(settings_env): journal_path, config = settings_env() config["setup"] = {"completed_at": "2026-05-23T00:00:00Z"} - config.setdefault("convey", {})["trust_localhost"] = True (journal_path / "config" / "journal.json").write_text( json.dumps(config, indent=2) + "\n", encoding="utf-8", diff --git a/solstone/apps/thinking/tests/test_scout_routes.py b/solstone/apps/thinking/tests/test_scout_routes.py index 4c2d8effb..83ef0024f 100644 --- a/solstone/apps/thinking/tests/test_scout_routes.py +++ b/solstone/apps/thinking/tests/test_scout_routes.py @@ -32,6 +32,7 @@ def _read_config(journal: Path) -> dict: def _write_config(payload: dict) -> None: + payload.setdefault("setup", {"completed_at": 1700000000000}) write_journal_config(payload) @@ -48,9 +49,6 @@ def thinking_client(journal_copy: Path): app = create_app(journal=str(journal_copy.resolve())) app.config["TESTING"] = True client = app.test_client() - with client.session_transaction() as session: - session["logged_in"] = True - session.permanent = True return client diff --git a/solstone/apps/thinking/tests/test_workspace_html.py b/solstone/apps/thinking/tests/test_workspace_html.py index 4f2d7525b..ace56200e 100644 --- a/solstone/apps/thinking/tests/test_workspace_html.py +++ b/solstone/apps/thinking/tests/test_workspace_html.py @@ -18,7 +18,6 @@ STATIC = Path(__file__).resolve().parents[1] / "static" / "thinking.js" def test_workspace_renders_each_lane(settings_env): journal_path, config = settings_env() config["setup"] = {"completed_at": "2026-05-23T00:00:00Z"} - config.setdefault("convey", {})["trust_localhost"] = True (journal_path / "config" / "journal.json").write_text( json.dumps(config, indent=2) + "\n", encoding="utf-8", diff --git a/solstone/apps/timeline/tests/conftest.py b/solstone/apps/timeline/tests/conftest.py index 4e3ec54ce..d7dc41494 100644 --- a/solstone/apps/timeline/tests/conftest.py +++ b/solstone/apps/timeline/tests/conftest.py @@ -54,7 +54,6 @@ def timeline_env(tmp_path, monkeypatch): (journal / "config" / "journal.json").write_text( json.dumps( { - "convey": {"secret": "test-secret", "trust_localhost": True}, "setup": {"completed_at": 1700000000000}, } ) diff --git a/solstone/apps/timeline/tests/fixtures/day_top_dedup/journal/config/journal.json b/solstone/apps/timeline/tests/fixtures/day_top_dedup/journal/config/journal.json index 90a89f590..d497db418 100644 --- a/solstone/apps/timeline/tests/fixtures/day_top_dedup/journal/config/journal.json +++ b/solstone/apps/timeline/tests/fixtures/day_top_dedup/journal/config/journal.json @@ -1 +1 @@ -{"convey":{"secret":"test-secret","trust_localhost":true},"setup":{"completed_at":1700000000000}} +{"setup":{"completed_at":1700000000000}} \ No newline at end of file diff --git a/solstone/apps/timeline/tests/fixtures/day_top_empty_array/journal/config/journal.json b/solstone/apps/timeline/tests/fixtures/day_top_empty_array/journal/config/journal.json index 90a89f590..d497db418 100644 --- a/solstone/apps/timeline/tests/fixtures/day_top_empty_array/journal/config/journal.json +++ b/solstone/apps/timeline/tests/fixtures/day_top_empty_array/journal/config/journal.json @@ -1 +1 @@ -{"convey":{"secret":"test-secret","trust_localhost":true},"setup":{"completed_at":1700000000000}} +{"setup":{"completed_at":1700000000000}} \ No newline at end of file diff --git a/solstone/apps/timeline/tests/fixtures/empty_no_dir/journal/config/journal.json b/solstone/apps/timeline/tests/fixtures/empty_no_dir/journal/config/journal.json index 90a89f590..d497db418 100644 --- a/solstone/apps/timeline/tests/fixtures/empty_no_dir/journal/config/journal.json +++ b/solstone/apps/timeline/tests/fixtures/empty_no_dir/journal/config/journal.json @@ -1 +1 @@ -{"convey":{"secret":"test-secret","trust_localhost":true},"setup":{"completed_at":1700000000000}} +{"setup":{"completed_at":1700000000000}} \ No newline at end of file diff --git a/solstone/apps/timeline/tests/fixtures/empty_segments_no_rollup/journal/config/journal.json b/solstone/apps/timeline/tests/fixtures/empty_segments_no_rollup/journal/config/journal.json index 90a89f590..d497db418 100644 --- a/solstone/apps/timeline/tests/fixtures/empty_segments_no_rollup/journal/config/journal.json +++ b/solstone/apps/timeline/tests/fixtures/empty_segments_no_rollup/journal/config/journal.json @@ -1 +1 @@ -{"convey":{"secret":"test-secret","trust_localhost":true},"setup":{"completed_at":1700000000000}} +{"setup":{"completed_at":1700000000000}} \ No newline at end of file diff --git a/solstone/apps/timeline/tests/fixtures/hour_picks_empty/journal/config/journal.json b/solstone/apps/timeline/tests/fixtures/hour_picks_empty/journal/config/journal.json index 90a89f590..d497db418 100644 --- a/solstone/apps/timeline/tests/fixtures/hour_picks_empty/journal/config/journal.json +++ b/solstone/apps/timeline/tests/fixtures/hour_picks_empty/journal/config/journal.json @@ -1 +1 @@ -{"convey":{"secret":"test-secret","trust_localhost":true},"setup":{"completed_at":1700000000000}} +{"setup":{"completed_at":1700000000000}} \ No newline at end of file diff --git a/solstone/apps/timeline/tests/test_routes.py b/solstone/apps/timeline/tests/test_routes.py index cbda839ca..01181d88a 100644 --- a/solstone/apps/timeline/tests/test_routes.py +++ b/solstone/apps/timeline/tests/test_routes.py @@ -35,7 +35,6 @@ def empty_timeline_env(tmp_path: Path, monkeypatch): (journal / "config" / "journal.json").write_text( json.dumps( { - "convey": {"secret": "test-secret", "trust_localhost": True}, "setup": {"completed_at": 1700000000000}, } ) diff --git a/solstone/apps/tokens/tests/conftest.py b/solstone/apps/tokens/tests/conftest.py index 6d73bc1c7..e99457207 100644 --- a/solstone/apps/tokens/tests/conftest.py +++ b/solstone/apps/tokens/tests/conftest.py @@ -25,7 +25,6 @@ def tokens_env(tmp_path, monkeypatch): config_file.write_text( json.dumps( { - "convey": {"trust_localhost": True}, "setup": {"completed_at": 1700000000000}, }, indent=2, diff --git a/solstone/apps/transcripts/tests/test_call.py b/solstone/apps/transcripts/tests/test_call.py index 9364d7e15..f970a146b 100644 --- a/solstone/apps/transcripts/tests/test_call.py +++ b/solstone/apps/transcripts/tests/test_call.py @@ -9,18 +9,19 @@ from typer.testing import CliRunner from solstone.apps.transcripts.call import app from solstone.think.convey_client import ConveyClient -from tests._baseline_harness import make_logged_in_test_client +from tests._baseline_harness import make_test_client, mark_setup_complete @pytest.fixture def journal(tmp_path, monkeypatch): monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) + mark_setup_complete(tmp_path) return tmp_path @pytest.fixture def runner(journal, monkeypatch): - client = ConveyClient(session=make_logged_in_test_client(journal), base_url="") + client = ConveyClient(session=make_test_client(journal), base_url="") monkeypatch.setattr("solstone.apps.transcripts.call.get_client", lambda: client) return CliRunner() @@ -52,7 +53,7 @@ def _write_segment( def _route_markdown(journal, day: str, params: dict[str, str]) -> str: - client = ConveyClient(session=make_logged_in_test_client(journal), base_url="") + client = ConveyClient(session=make_test_client(journal), base_url="") return client.request("GET", f"/app/transcripts/api/read/{day}", params=params)[ "markdown" ] diff --git a/solstone/apps/transcripts/tests/test_serve_file.py b/solstone/apps/transcripts/tests/test_serve_file.py index c0f2caa47..d4966f156 100644 --- a/solstone/apps/transcripts/tests/test_serve_file.py +++ b/solstone/apps/transcripts/tests/test_serve_file.py @@ -30,7 +30,6 @@ def client(tmp_path, monkeypatch): (config_dir / "journal.json").write_text( json.dumps( { - "convey": {"trust_localhost": True}, "setup": {"completed_at": 1700000000000}, }, indent=2, diff --git a/solstone/convey/__init__.py b/solstone/convey/__init__.py index 4dc19b293..fab0bff29 100644 --- a/solstone/convey/__init__.py +++ b/solstone/convey/__init__.py @@ -31,61 +31,6 @@ def __getattr__(name: str): raise AttributeError(f"module {__name__!r} has no attribute {name!r}") -def _get_or_create_secret() -> str: - """Load convey.secret from journal.json, generating one if absent.""" - from solstone.think.utils import ensure_journal_config - - config = ensure_journal_config() - return config["convey"]["secret"] - - -def _migrate_password_hash() -> None: - """Migrate plaintext convey.password to hashed password_hash.""" - from werkzeug.security import generate_password_hash - - from solstone.think.journal_config import write_journal_config - from solstone.think.utils import get_config - - config = get_config() - convey = config.get("convey", {}) - - if "password_hash" in convey or "password" not in convey: - return - - plaintext = convey.pop("password") - if plaintext: - convey["password_hash"] = generate_password_hash(plaintext) - - config["convey"] = convey - write_journal_config(config) - - -def _migrate_setup_completed() -> None: - """Infer setup.completed_at and set trust_localhost for existing installs. - - Legacy migration: handles journals where password_hash was set via - 'journal password set' CLI before web onboarding existed. Web onboarding - now writes all config atomically in init_finalize(), so this path is - only reached for pre-existing journals. - """ - from solstone.think.journal_config import write_journal_config - from solstone.think.utils import get_config - - config = get_config() - - if not config.get("convey", {}).get("password_hash"): - return - if config.get("setup", {}).get("completed_at"): - return - - from solstone.think.utils import now_ms - - config.setdefault("setup", {})["completed_at"] = now_ms() - config.setdefault("convey", {})["trust_localhost"] = True - - write_journal_config(config) - - def install_identity_stamper(app: Flask) -> None: from flask import g, request @@ -155,16 +100,12 @@ def create_app(journal: str = "") -> Flask: ] ) - app.secret_key = _get_or_create_secret() - _migrate_password_hash() - _migrate_setup_completed() - app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(days=30) app.config["SEND_FILE_MAX_AGE_DEFAULT"] = timedelta(seconds=300) app.config.setdefault("SECURE_LISTENER_ENABLED", False) install_identity_stamper(app) install_request_id_stamper(app) - # Register root blueprint (login, logout, /, favicon) + # Register root blueprint (/, favicon) app.register_blueprint(root_bp) # Register config API blueprint diff --git a/solstone/convey/cli.py b/solstone/convey/cli.py index ee500ed59..0caf10646 100644 --- a/solstone/convey/cli.py +++ b/solstone/convey/cli.py @@ -19,19 +19,6 @@ from .bridge import start_bridge, stop_bridge logger = logging.getLogger(__name__) -def _resolve_config_password_hash() -> str: - """Return the configured Convey password hash from journal config.""" - from solstone.think.utils import get_config - - try: - config = get_config() - convey_config = config.get("convey", {}) - return convey_config.get("password_hash", "") - except Exception: - # Intended fail-closed-on-unreadable-config: no hash means no password auth. - return "" - - def _resolve_bind_host() -> str: """Return Convey's bind host — always loopback; :5015 is never network-exposed.""" return "127.0.0.1" @@ -90,13 +77,6 @@ def main() -> None: journal = get_journal() app = create_app(journal) - password = _resolve_config_password_hash() - if password: - logger.info("Password authentication enabled") - else: - logger.warning( - "no password configured; required only when localhost trust is disabled." - ) # Write port to health directory for discovery by other tools write_service_port("convey", args.port) diff --git a/solstone/convey/copy.py b/solstone/convey/copy.py index b13069b19..2db38f409 100644 --- a/solstone/convey/copy.py +++ b/solstone/convey/copy.py @@ -70,32 +70,17 @@ CONVEY_REPORT_MAILTO_BODY_PREFIX = ( ) CONVEY_REPORT_MAILTO_TRUNCATION_SUFFIX = "\n\n[full report is in your clipboard]\n" CONVEY_REPORT_BUTTON_LABEL = "Report this" -LOGIN_NO_PASSWORD_CONFIGURED = "no password is configured. set one in settings → security, or run 'journal password set' from a terminal on this machine." -SETTINGS_SECURITY_DESC = "password protection for the convey web interface." -SETTINGS_SECURITY_REACH_HINT = ( - "how your devices reach home — managing your connection, paired devices, " - "and reach-from-anywhere now lives in link →" -) -SETTINGS_PASSWORD_HINT = "used when localhost trust is off. not used otherwise." OBSERVER_CALLOSUM_LIVE_LABEL = "live" def format_convey_status( *, bind: str, - password: str, - trust_localhost: str, host_url: str, ) -> str: - """Return the locked five-line convey status block.""" + """Return the locked convey status block.""" - return ( - "convey\n" - f" bind: {bind}\n" - f" password: {password}\n" - f" trust localhost: {trust_localhost}\n" - f" host url: {host_url}" - ) + return f"convey\n bind: {bind}\n host url: {host_url}" __all__ = [ @@ -160,10 +145,6 @@ __all__ = [ "CONVEY_REPORT_SUCCESS_BODY", "CONVEY_REPORT_SUCCESS_BODY_NO_ID", "CONVEY_REPORT_TITLE", - "LOGIN_NO_PASSWORD_CONFIGURED", "OBSERVER_CALLOSUM_LIVE_LABEL", - "SETTINGS_PASSWORD_HINT", - "SETTINGS_SECURITY_DESC", - "SETTINGS_SECURITY_REACH_HINT", "format_convey_status", ] diff --git a/solstone/convey/reasons.py b/solstone/convey/reasons.py index ce1e0c951..67385ed71 100644 --- a/solstone/convey/reasons.py +++ b/solstone/convey/reasons.py @@ -157,11 +157,6 @@ BACKUP_UNAVAILABLE = Reason( "I couldn't start a backup because solstone's background service isn't running. Start it, then try again.", 503, ) -NETWORK_SECURITY_REQUIRES_PASSWORD = Reason( - "network_security_requires_password", - "I couldn't change localhost trust until a password is set.", - 400, -) LOCAL_REQUEST_ONLY = Reason( "local_request_only", "I couldn't register that observer because this endpoint serves local requests only.", diff --git a/solstone/convey/root.py b/solstone/convey/root.py index 334ebafa1..749fabe08 100644 --- a/solstone/convey/root.py +++ b/solstone/convey/root.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc -"""Root blueprint: authentication and core routes.""" +"""Root blueprint: access gate and core routes.""" from __future__ import annotations @@ -24,11 +24,9 @@ from flask import ( render_template, request, send_from_directory, - session, stream_with_context, url_for, ) -from werkzeug.security import check_password_hash, generate_password_hash from solstone.think.cluster import cluster_segments from solstone.think.journal_config import write_journal_config @@ -44,7 +42,6 @@ from .config import ( locked_modify_convey_config, seed_default_app_navigation, ) -from .copy import LOGIN_NO_PASSWORD_CONFIGURED from .reasons import INVALID_CONFIG_VALUE, PL_REVOKED from .secure_listener import get_authorized_clients from .utils import error_response, error_response_with_reason @@ -52,17 +49,6 @@ from .utils import error_response, error_response_with_reason logger = logging.getLogger(__name__) -def _get_password_hash() -> str: - """Get current password hash from config, reloading on each call.""" - try: - config = get_config() - convey_config = config.get("convey", {}) - return convey_config.get("password_hash", "") - except Exception: - # Intended fail-closed-on-unreadable-config: no hash means no password auth. - return "" - - def _is_setup_complete() -> bool: """Check if initial setup has been completed.""" try: @@ -73,17 +59,6 @@ def _is_setup_complete() -> bool: return False -def _check_basic_auth() -> bool: - """Check Basic Auth credentials against stored password hash.""" - auth = request.authorization - if not auth or auth.type != "basic": - return False - password_hash = _get_password_hash() - if not password_hash: - return False - return check_password_hash(password_hash, auth.password or "") - - bp = Blueprint( "root", __name__, @@ -93,7 +68,7 @@ bp = Blueprint( @bp.before_app_request -def require_login() -> Any: +def require_access() -> Any: if request.endpoint is None: return None @@ -102,7 +77,6 @@ def require_login() -> Any: "root.init_validate_provider", "root.init_observers", "root.init_finalize", - "root.login", "static", "root.static", "root.favicon", @@ -143,35 +117,11 @@ def require_login() -> Any: detail="paired device revoked", ) - # Session cookie - if session.get("logged_in"): - return None - - # Basic Auth (per-request, no session creation) - if _check_basic_auth(): - return None - # Check setup state - setup_complete = _is_setup_complete() - - # Opt-in localhost bypass (requires completed setup + trust_localhost flag) - if setup_complete: - config = get_config() - if config.get("convey", {}).get("trust_localhost", True): - remote_addr = request.remote_addr - is_localhost = remote_addr in ("127.0.0.1", "::1", "localhost") - proxy_headers = ( - request.headers.get("X-Forwarded-For") - or request.headers.get("X-Real-IP") - or request.headers.get("X-Forwarded-Host") - ) - if is_localhost and not proxy_headers: - return None - - # Not authenticated — redirect based on setup state - if not setup_complete: + if not _is_setup_complete(): return redirect(url_for("root.init")) - return redirect(url_for("root.login")) + + return None @bp.route("/sse/events", methods=["GET"], endpoint="callosum_sse") @@ -223,26 +173,6 @@ def callosum_sse() -> Response: ) -@bp.route("/login", methods=["GET", "POST"]) -def login() -> Any: - # Re-check password from config on each request - password_hash = _get_password_hash() - - # If no password is configured, show error page - if not password_hash: - error = LOGIN_NO_PASSWORD_CONFIGURED - return render_template("login.html", error=error, no_password=True) - - error = None - if request.method == "POST": - if check_password_hash(password_hash, request.form.get("password", "")): - session["logged_in"] = True - session.permanent = True - return redirect(url_for("root.index")) - error = "incorrect password. passwords are case-sensitive. if you've forgotten it, you can reset via journal password set on the command line." - return render_template("login.html", error=error, no_password=False) - - @bp.route("/init") def init() -> Any: if _is_setup_complete(): @@ -316,24 +246,10 @@ def init_observers() -> Any: def init_finalize() -> Any: data = request.get_json(silent=True) or {} - password = data.get("password") or "" - if password and len(password) < 8: - return error_response( - INVALID_CONFIG_VALUE, - detail="Password must be at least 8 characters", - ) - from solstone.think.utils import now_ms config = get_config() - convey_config = config.setdefault("convey", {}) - convey_config.pop("allow_network_access", None) - convey_update = { - "trust_localhost": True, - } - if password: - convey_update["password_hash"] = generate_password_hash(password) - convey_config.update(convey_update) + config.setdefault("convey", {}).pop("allow_network_access", None) config.setdefault("identity", {}).update( { k: v @@ -375,17 +291,9 @@ def init_finalize() -> Any: except Exception: logger.error("default app navigation seed convey-config PERSIST failed") - session["logged_in"] = True - session.permanent = True return jsonify({"success": True, "redirect": url_for("app:thinking.index")}) -@bp.route("/logout") -def logout() -> Any: - session.pop("logged_in", None) - return redirect(url_for("root.login")) - - @bp.route("/favicon.ico") def favicon() -> Any: """Serve the favicon from the project root.""" diff --git a/solstone/convey/static/tests/api.html b/solstone/convey/static/tests/api.html index 482f499de..b796b4ea6 100644 --- a/solstone/convey/static/tests/api.html +++ b/solstone/convey/static/tests/api.html @@ -78,9 +78,9 @@ ), error => error.serverMessage === 'Request failed (HTTP 400)'); await expectApiError('401 no redirect', () => withFetch( - async () => response({ status: 401, statusText: 'Unauthorized', body: '{"error":"login"}' }), + async () => response({ status: 401, statusText: 'Unauthorized', body: '{"error":"unauthorized"}' }), () => window.apiJson('/auth', { noAuthRedirect: true }) - ), error => error.status === 401 && error.serverMessage === 'login'); + ), error => error.status === 401 && error.serverMessage === 'unauthorized'); await expectApiError('403 no redirect', () => withFetch( async () => response({ status: 403, statusText: 'Forbidden', body: '{"message":"disabled"}' }), diff --git a/solstone/convey/templates/login.html b/solstone/convey/templates/login.html deleted file mode 100644 index d00716db9..000000000 --- a/solstone/convey/templates/login.html +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - sign in — solstone - - - -
- - sol - - - - - - - -

sign in to solstone

- {% if error %} -
-

{{ error }}

-
- {% endif %} - {% if not no_password %} -
- - - -
- {% endif %} -

your data stays on your machine

-
- - diff --git a/solstone/convey/tests/conftest.py b/solstone/convey/tests/conftest.py index 73cef1823..49c76f408 100644 --- a/solstone/convey/tests/conftest.py +++ b/solstone/convey/tests/conftest.py @@ -22,7 +22,6 @@ def convey_env(tmp_path, monkeypatch): config_file.write_text( json.dumps( { - "convey": {"trust_localhost": True}, "setup": {"completed_at": 1700000000000}, }, indent=2, @@ -59,7 +58,6 @@ def convey_env_setup_pending(tmp_path, monkeypatch): config_file.write_text( json.dumps( { - "convey": {"trust_localhost": True}, "setup": {}, }, indent=2, diff --git a/solstone/convey/tests/test_callosum_sse.py b/solstone/convey/tests/test_callosum_sse.py index f13c3fb30..7d4abe49f 100644 --- a/solstone/convey/tests/test_callosum_sse.py +++ b/solstone/convey/tests/test_callosum_sse.py @@ -71,16 +71,19 @@ def test_callosum_sse_success_headers(convey_env): resp.close() -def test_callosum_sse_unauthenticated_redirects_to_login(convey_env): +def test_callosum_sse_setup_complete_serves_with_proxy_header(convey_env): env = convey_env() resp = env.client.get( "/sse/events", headers={"X-Forwarded-For": "1.2.3.4"}, + buffered=False, ) - - assert resp.status_code == 302 - assert "/login" in resp.headers["Location"] + try: + assert resp.status_code == 200 + assert resp.content_type.startswith("text/event-stream") + finally: + resp.close() def test_callosum_sse_round_trip_payload(convey_env): diff --git a/solstone/convey/tests/test_request_id_header.py b/solstone/convey/tests/test_request_id_header.py index b07cc43e2..ad2e72737 100644 --- a/solstone/convey/tests/test_request_id_header.py +++ b/solstone/convey/tests/test_request_id_header.py @@ -19,7 +19,7 @@ def _assert_request_id_header(response) -> None: def test_request_id_header_on_success(convey_env) -> None: env = convey_env() - response = env.client.get("/login") + response = env.client.get("/") _assert_request_id_header(response) diff --git a/solstone/talent/journal/references/config.md b/solstone/talent/journal/references/config.md index 7c303a689..66f27d76b 100644 --- a/solstone/talent/journal/references/config.md +++ b/solstone/talent/journal/references/config.md @@ -40,20 +40,7 @@ This configuration helps meeting extraction identify the owner as a participant, ## Convey configuration -The `convey` block contains settings for the web application: - -```json -{ - "convey": { - "password_hash": " Security or journal password set>" - } -} -``` - -Fields: -- `password_hash` (string) – Hashed password for accessing the convey web application. Set via Settings → Security or `journal password set`. - -**UI Preferences:** The separate `config/convey.json` file stores UI/UX personalization (facet/app ordering, selected facet). All fields optional: +The separate `config/convey.json` file stores UI/UX personalization (facet/app ordering, selected facet). All fields optional: ```json { diff --git a/solstone/think/journal_default.json b/solstone/think/journal_default.json index 7462db9ff..ce8ffb7b7 100644 --- a/solstone/think/journal_default.json +++ b/solstone/think/journal_default.json @@ -65,9 +65,6 @@ }, "debug_show_throttled": false }, - "convey": { - "trust_localhost": true - }, "pairing": { "host_url": null }, diff --git a/solstone/think/password_cli.py b/solstone/think/password_cli.py deleted file mode 100644 index 7cfc30eea..000000000 --- a/solstone/think/password_cli.py +++ /dev/null @@ -1,60 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""CLI for setting the convey web UI password. - -Usage: - journal password set Set the convey password - journal password reset Reset the convey password (alias for set) -""" - -from __future__ import annotations - -import argparse -import getpass -import sys - -from werkzeug.security import generate_password_hash - -from solstone.think.journal_config import write_journal_config -from solstone.think.utils import get_config, require_solstone, setup_cli - - -def _set_password() -> None: - """Prompt for a password, hash it, and write to journal config.""" - password = getpass.getpass("Password: ") - confirm = getpass.getpass("Confirm password: ") - - if password != confirm: - print("Passwords do not match.", file=sys.stderr) - sys.exit(1) - - password_hash = generate_password_hash(password) - - config = get_config() - config.setdefault("convey", {})["password_hash"] = password_hash - config.get("convey", {}).pop("password", None) - - write_journal_config(config) - - print("Password set successfully.") - - -def main() -> None: - parser = argparse.ArgumentParser(description="Manage convey web UI password") - subparsers = parser.add_subparsers(dest="subcommand") - subparsers.add_parser("set", help="Set the convey password") - subparsers.add_parser("reset", help="Reset the convey password") - - args = setup_cli(parser) - require_solstone() - - if args.subcommand in ("set", "reset"): - _set_password() - else: - parser.print_help() - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/solstone/think/settings_cli.py b/solstone/think/settings_cli.py index fa710d41f..edb16c428 100644 --- a/solstone/think/settings_cli.py +++ b/solstone/think/settings_cli.py @@ -8,28 +8,17 @@ from __future__ import annotations import argparse import json import sys -from typing import Any from solstone.convey.cli import _resolve_bind_host from solstone.convey.copy import format_convey_status from solstone.think.pairing.config import get_host_url from solstone.think.service import DEFAULT_SERVICE_PORT from solstone.think.utils import ( - get_config, read_service_port, setup_cli, ) -def _trust_localhost_enabled(config: dict[str, Any]) -> bool: - return bool(config.get("convey", {}).get("trust_localhost", True)) - - -def _convey_password_is_set(config: dict[str, Any]) -> bool: - password_hash = config.get("convey", {}).get("password_hash", "") - return bool(str(password_hash or "").strip()) - - def _host_url_status_value() -> str: return get_host_url() @@ -38,17 +27,15 @@ def _convey_port() -> int: return read_service_port("convey") or DEFAULT_SERVICE_PORT -def _status_payload(config: dict[str, Any]) -> dict[str, Any]: +def _status_payload() -> dict[str, str]: return { "effective_host_url": get_host_url(), - "password_configured": _convey_password_is_set(config), } def _print_status(*, as_json: bool) -> None: - config = get_config() if as_json: - print(json.dumps(_status_payload(config), indent=2)) + print(json.dumps(_status_payload(), indent=2)) return bind_host = _resolve_bind_host() @@ -57,8 +44,6 @@ def _print_status(*, as_json: bool) -> None: format_convey_status( bind=f"{bind_host}:{port}", host_url=_host_url_status_value(), - password="set" if _convey_password_is_set(config) else "not set", - trust_localhost="yes" if _trust_localhost_enabled(config) else "no", ) ) diff --git a/solstone/think/sol_cli.py b/solstone/think/sol_cli.py index fe9142496..ec074ade0 100644 --- a/solstone/think/sol_cli.py +++ b/solstone/think/sol_cli.py @@ -99,7 +99,6 @@ COMMANDS: dict[str, Command] = { "install-models": Command("solstone.think.install_models", "service"), "install-provider": Command("solstone.think.install_provider", "service"), "skills": Command("solstone.think.skills_cli", "access"), - "password": Command("solstone.think.password_cli", "service"), "settings": Command("solstone.think.settings_cli", "service"), "streams": Command("solstone.think.streams", "service"), "segment": Command("solstone.think.segment", "service"), diff --git a/solstone/think/utils.py b/solstone/think/utils.py index 0736213d5..17ba32589 100644 --- a/solstone/think/utils.py +++ b/solstone/think/utils.py @@ -17,7 +17,6 @@ import logging import os import pwd import re -import secrets import socket import sys import time @@ -730,9 +729,8 @@ def _resolve_os_timezone() -> str: def ensure_journal_config() -> dict[str, Any]: """Materialize /config/journal.json and return its contents. - Idempotent after first creation, with one transitional exception: if the - file exists but lacks ``convey.secret``, the secret is backfilled. Identity - fields on an existing file are never modified. + Idempotent after first creation. Identity fields on an existing file are + never modified. """ from solstone.think.journal_config import write_journal_config @@ -744,9 +742,6 @@ def ensure_journal_config() -> dict[str, Any]: if config_path.exists(): with config_path.open(encoding="utf-8") as fh: config = json.load(fh) - if not config.get("convey", {}).get("secret"): - config.setdefault("convey", {})["secret"] = secrets.token_hex(32) - write_journal_config(config) return config if _default_config is None: @@ -771,7 +766,6 @@ def ensure_journal_config() -> dict[str, Any]: config["identity"]["name"] = full_name config["identity"]["preferred"] = login_name config["identity"]["timezone"] = timezone - config.setdefault("convey", {})["secret"] = secrets.token_hex(32) write_journal_config(config) return config diff --git a/tests/_baseline_harness.py b/tests/_baseline_harness.py index de8605658..d939f3dea 100644 --- a/tests/_baseline_harness.py +++ b/tests/_baseline_harness.py @@ -14,6 +14,7 @@ oracle. from __future__ import annotations import atexit +import json import os import shutil import subprocess @@ -136,14 +137,24 @@ def isolated_app_env(journal: Path) -> Iterator[Path]: os.environ["SOLSTONE_JOURNAL"] = prev_override -def make_logged_in_test_client(journal: Path): - """Create a Flask test client with an authenticated session.""" +def make_test_client(journal: Path): + """Create a Flask test client for an isolated journal.""" from solstone.convey import create_app app = create_app(journal=str(Path(journal).resolve())) app.config["TESTING"] = True client = app.test_client() - with client.session_transaction() as session: - session["logged_in"] = True - session.permanent = True return client + + +def mark_setup_complete(journal: Path, completed_at: int = 1700000000000) -> None: + """Mark a minimal test journal as past first-run setup.""" + + config_path = Path(journal) / "config" / "journal.json" + config_path.parent.mkdir(parents=True, exist_ok=True) + if config_path.exists(): + config = json.loads(config_path.read_text(encoding="utf-8")) + else: + config = {} + config["setup"] = {"completed_at": completed_at} + config_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8") diff --git a/tests/baselines/api/settings/config.json b/tests/baselines/api/settings/config.json index 0a41ae0f9..7cef39ee8 100644 --- a/tests/baselines/api/settings/config.json +++ b/tests/baselines/api/settings/config.json @@ -6,7 +6,6 @@ }, "convey": { "allow_network_access": false, - "has_password": true, "trust_localhost": true }, "describe": { diff --git a/tests/conftest.py b/tests/conftest.py index 093146638..c3fc17ce7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -158,11 +158,16 @@ def set_test_journal_path(monkeypatch, _isolate_os_environ): This ensures all tests have a valid SOLSTONE_JOURNAL without needing to explicitly set it in each test. """ + import solstone.think.utils as think_utils + monkeypatch.setenv( "SOLSTONE_JOURNAL", str(Path("tests/fixtures/journal").resolve()), ) monkeypatch.setenv("SOL_SKIP_SUPERVISOR_CHECK", "1") + think_utils._journal_path_cache = None + yield + think_utils._journal_path_cache = None @pytest.fixture(autouse=True) @@ -216,6 +221,9 @@ def journal_copy(tmp_path, monkeypatch): dst = tmp_path / "journal" copytree_tracked(src, dst) monkeypatch.setenv("SOLSTONE_JOURNAL", str(dst.resolve())) + import solstone.think.utils as think_utils + + think_utils._journal_path_cache = None return dst diff --git a/tests/link/certless_helpers.py b/tests/link/certless_helpers.py index b4cc29d4a..2e708079a 100644 --- a/tests/link/certless_helpers.py +++ b/tests/link/certless_helpers.py @@ -75,10 +75,8 @@ def write_config( journal: Path, *, link: dict[str, Any] | None = None, - trust_localhost: bool = True, ) -> None: config: dict[str, Any] = { - "convey": {"trust_localhost": trust_localhost}, "setup": {"completed_at": 1700000000000}, } if link is not None: diff --git a/tests/link/test_link_join_refusals.py b/tests/link/test_link_join_refusals.py index 87750e09c..bd92001ae 100644 --- a/tests/link/test_link_join_refusals.py +++ b/tests/link/test_link_join_refusals.py @@ -59,7 +59,7 @@ def _args() -> argparse.Namespace: ) -def test_pair_request_refuses_auth_bounce_html( +def test_pair_request_refuses_html_bounce( tmp_path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], @@ -68,8 +68,8 @@ def test_pair_request_refuses_auth_bounce_html( def fake_urlopen(request, **_kwargs): return _FakeResponse( - b"login", - url="http://receiver/login", + b"unexpected", + url="http://receiver/unexpected", content_type="text/html", ) @@ -80,7 +80,7 @@ def test_pair_request_refuses_auth_bounce_html( assert result == 1 err = capsys.readouterr().err assert "http://receiver/app/link/by-code" in err - assert "http://receiver/login" in err + assert "http://receiver/unexpected" in err assert "text/html" in err diff --git a/tests/link/test_local_endpoints_route.py b/tests/link/test_local_endpoints_route.py index 06911eb29..5efd8a072 100644 --- a/tests/link/test_local_endpoints_route.py +++ b/tests/link/test_local_endpoints_route.py @@ -28,7 +28,6 @@ def link_client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): (config_dir / "journal.json").write_text( json.dumps( { - "convey": {"trust_localhost": True}, "setup": {"completed_at": 1}, } ), @@ -80,8 +79,6 @@ def test_local_endpoints_returns_watcher_snapshot(link_client) -> None: def test_local_endpoints_non_loopback_404(link_client) -> None: - with link_client.session_transaction() as session: - session["logged_in"] = True response = link_client.get( "/app/link/local-endpoints", environ_base={"REMOTE_ADDR": "192.168.1.5"}, diff --git a/tests/test_activities_call_parity.py b/tests/test_activities_call_parity.py index 49bafa41a..50488fe48 100644 --- a/tests/test_activities_call_parity.py +++ b/tests/test_activities_call_parity.py @@ -14,7 +14,7 @@ from solstone.apps.activities.call import app from solstone.convey.reasons import ACTIVITIES_BUSY from solstone.think.convey_client import ConveyClient from solstone.think.journal_io import LockTimeout -from tests._baseline_harness import make_logged_in_test_client +from tests._baseline_harness import make_test_client DAY = "20260418" PREV_DAY = "20260417" @@ -36,7 +36,7 @@ def journal(tmp_path, monkeypatch): @pytest.fixture def runner(journal, monkeypatch): - client = ConveyClient(session=make_logged_in_test_client(journal), base_url="") + client = ConveyClient(session=make_test_client(journal), base_url="") monkeypatch.setattr("solstone.apps.activities.call.get_client", lambda: client) return CliRunner() @@ -52,7 +52,6 @@ def _seed_journal( (config_dir / "journal.json").write_text( json.dumps( { - "convey": {"trust_localhost": True}, "setup": {"completed_at": 1700000000000}, } ) diff --git a/tests/test_activities_cli_create.py b/tests/test_activities_cli_create.py index 51aef743c..2ab664c96 100644 --- a/tests/test_activities_cli_create.py +++ b/tests/test_activities_cli_create.py @@ -7,7 +7,7 @@ from typer.testing import CliRunner from solstone.apps.activities.call import app from solstone.think.convey_client import ConveyClient -from tests._baseline_harness import make_logged_in_test_client +from tests._baseline_harness import make_test_client runner = CliRunner() @@ -29,7 +29,6 @@ def _configure_cli_env(tmp_path, monkeypatch) -> None: (config_dir / "journal.json").write_text( json.dumps( { - "convey": {"trust_localhost": True}, "setup": {"completed_at": 1700000000000}, } ) @@ -51,7 +50,7 @@ def _configure_cli_env(tmp_path, monkeypatch) -> None: import solstone.think.utils as think_utils think_utils._journal_path_cache = None - client = ConveyClient(session=make_logged_in_test_client(tmp_path), base_url="") + client = ConveyClient(session=make_test_client(tmp_path), base_url="") monkeypatch.setattr("solstone.apps.activities.call.get_client", lambda: client) diff --git a/tests/test_api_baselines.py b/tests/test_api_baselines.py index ef46aa2f6..a04803f39 100644 --- a/tests/test_api_baselines.py +++ b/tests/test_api_baselines.py @@ -14,7 +14,7 @@ from tests._baseline_harness import ( FROZEN_DATE, FROZEN_TZ_OFFSET, isolated_app_env, - make_logged_in_test_client, + make_test_client, prepare_isolated_journal, ) from tests.conftest import _install_heavy_module_stubs @@ -64,7 +64,7 @@ def _baseline_journal(tmp_path_factory): @pytest.fixture(scope="module") def client(_baseline_journal): with isolated_app_env(_baseline_journal): - yield make_logged_in_test_client(_baseline_journal) + yield make_test_client(_baseline_journal) @pytest.fixture(scope="module") diff --git a/tests/test_app_import_journal.py b/tests/test_app_import_journal.py index 0fa412b18..3f8b5e700 100644 --- a/tests/test_app_import_journal.py +++ b/tests/test_app_import_journal.py @@ -19,7 +19,6 @@ def _temp_journal(monkeypatch, tmp_path): json.dumps( { "setup": {"completed_at": "2026-04-26T00:00:00Z"}, - "convey": {"trust_localhost": True}, } ), encoding="utf-8", diff --git a/tests/test_app_news.py b/tests/test_app_news.py index cc46aea00..072917976 100644 --- a/tests/test_app_news.py +++ b/tests/test_app_news.py @@ -20,9 +20,6 @@ def _make_client(journal: Path): app = create_app(str(journal)) app.config["TESTING"] = True client = app.test_client() - with client.session_transaction() as session: - session["logged_in"] = True - session.permanent = True return client diff --git a/tests/test_app_reflections.py b/tests/test_app_reflections.py index 8c4bc4745..bc62c35f0 100644 --- a/tests/test_app_reflections.py +++ b/tests/test_app_reflections.py @@ -31,9 +31,6 @@ def _make_client(journal: Path): app = create_app(str(journal)) app.config["TESTING"] = True client = app.test_client() - with client.session_transaction() as session: - session["logged_in"] = True - session.permanent = True return client diff --git a/tests/test_app_support.py b/tests/test_app_support.py index 550c23790..806fbab8b 100644 --- a/tests/test_app_support.py +++ b/tests/test_app_support.py @@ -49,10 +49,11 @@ def journal(tmp_path, monkeypatch): @pytest.fixture def cli(journal, monkeypatch): from solstone.think.convey_client import ConveyClient - from tests._baseline_harness import make_logged_in_test_client + from tests._baseline_harness import make_test_client, mark_setup_complete + mark_setup_complete(journal) client = ConveyClient( - session=make_logged_in_test_client(journal), + session=make_test_client(journal), base_url="", require_service=False, ) diff --git a/tests/test_awareness_call_parity.py b/tests/test_awareness_call_parity.py index 25596b6a3..149885299 100644 --- a/tests/test_awareness_call_parity.py +++ b/tests/test_awareness_call_parity.py @@ -13,7 +13,7 @@ from solstone.apps.awareness.call import app from solstone.convey.reasons import AWARENESS_BUSY from solstone.think.convey_client import ConveyClient from solstone.think.journal_io import LockTimeout -from tests._baseline_harness import make_logged_in_test_client +from tests._baseline_harness import make_test_client, mark_setup_complete FROZEN_MS = 1700000000000 FROZEN_ISO = "20260415T12:00:00" @@ -25,12 +25,13 @@ def journal(tmp_path, monkeypatch): # Env must point at the tmp journal so BOTH the seed helpers (append_log/ # update_state) and the in-process route handlers resolve get_journal() to it. monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) + mark_setup_complete(tmp_path) return tmp_path @pytest.fixture def runner(journal, monkeypatch): - client = ConveyClient(session=make_logged_in_test_client(journal), base_url="") + client = ConveyClient(session=make_test_client(journal), base_url="") monkeypatch.setattr("solstone.apps.awareness.call.get_client", lambda: client) return CliRunner() diff --git a/tests/test_awareness_routes.py b/tests/test_awareness_routes.py index 838585a67..580d7a2c5 100644 --- a/tests/test_awareness_routes.py +++ b/tests/test_awareness_routes.py @@ -3,9 +3,11 @@ from __future__ import annotations +import json + from solstone.apps import AppRegistry from solstone.think.awareness import append_log -from tests._baseline_harness import make_logged_in_test_client +from tests._baseline_harness import make_test_client PREFIX = "/app/awareness" @@ -28,7 +30,7 @@ def test_awareness_api_only_discovery_registers_blueprint_outside_menu(): def test_awareness_index_404(journal_copy): - client = make_logged_in_test_client(journal_copy) + client = make_test_client(journal_copy) response = client.get(f"{PREFIX}/") @@ -37,7 +39,13 @@ def test_awareness_index_404(journal_copy): def test_awareness_state_empty_journal_returns_empty_dict(tmp_path, monkeypatch): monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) - client = make_logged_in_test_client(tmp_path) + config_dir = tmp_path / "config" + config_dir.mkdir(parents=True, exist_ok=True) + (config_dir / "journal.json").write_text( + json.dumps({"setup": {"completed_at": 1}}), + encoding="utf-8", + ) + client = make_test_client(tmp_path) response = client.get(f"{PREFIX}/api/state") @@ -46,7 +54,7 @@ def test_awareness_state_empty_journal_returns_empty_dict(tmp_path, monkeypatch) def test_awareness_state_full_state_includes_journal(journal_copy): - client = make_logged_in_test_client(journal_copy) + client = make_test_client(journal_copy) response = client.get(f"{PREFIX}/api/state") @@ -55,7 +63,7 @@ def test_awareness_state_full_state_includes_journal(journal_copy): def test_awareness_state_known_section(journal_copy): - client = make_logged_in_test_client(journal_copy) + client = make_test_client(journal_copy) response = client.get(f"{PREFIX}/api/state?section=journal") @@ -64,7 +72,7 @@ def test_awareness_state_known_section(journal_copy): def test_awareness_state_unknown_section(journal_copy): - client = make_logged_in_test_client(journal_copy) + client = make_test_client(journal_copy) response = client.get(f"{PREFIX}/api/state?section=nope") @@ -73,7 +81,7 @@ def test_awareness_state_unknown_section(journal_copy): def test_awareness_imports_get_defaults(journal_copy): - client = make_logged_in_test_client(journal_copy) + client = make_test_client(journal_copy) response = client.get(f"{PREFIX}/api/imports") @@ -82,7 +90,7 @@ def test_awareness_imports_get_defaults(journal_copy): def test_awareness_imports_post_record(journal_copy): - client = make_logged_in_test_client(journal_copy) + client = make_test_client(journal_copy) response = client.post(f"{PREFIX}/api/imports", json={"record": "chatgpt"}) @@ -93,7 +101,7 @@ def test_awareness_imports_post_record(journal_copy): def test_awareness_imports_post_declined(journal_copy): - client = make_logged_in_test_client(journal_copy) + client = make_test_client(journal_copy) response = client.post(f"{PREFIX}/api/imports", json={"declined": True}) @@ -102,7 +110,7 @@ def test_awareness_imports_post_declined(journal_copy): def test_awareness_imports_post_nudge(journal_copy): - client = make_logged_in_test_client(journal_copy) + client = make_test_client(journal_copy) response = client.post(f"{PREFIX}/api/imports", json={"nudge": True}) @@ -111,7 +119,7 @@ def test_awareness_imports_post_nudge(journal_copy): def test_awareness_imports_post_multi_action_400(journal_copy): - client = make_logged_in_test_client(journal_copy) + client = make_test_client(journal_copy) response = client.post( f"{PREFIX}/api/imports", @@ -122,7 +130,7 @@ def test_awareness_imports_post_multi_action_400(journal_copy): def test_awareness_imports_post_zero_action_400(journal_copy): - client = make_logged_in_test_client(journal_copy) + client = make_test_client(journal_copy) response = client.post(f"{PREFIX}/api/imports", json={}) @@ -133,7 +141,7 @@ def test_awareness_log_collection_limit_and_kind_filter(journal_copy): append_log("observation", message="a") append_log("observation", message="b") append_log("nudge", message="c") - client = make_logged_in_test_client(journal_copy) + client = make_test_client(journal_copy) response = client.get(f"{PREFIX}/api/log?limit=2") @@ -152,7 +160,7 @@ def test_awareness_log_collection_limit_and_kind_filter(journal_copy): def test_awareness_log_day_param_uses_requested_day(journal_copy): append_log("observation", message="old", day="20260101") - client = make_logged_in_test_client(journal_copy) + client = make_test_client(journal_copy) response = client.get(f"{PREFIX}/api/log?day=20260101") @@ -168,7 +176,7 @@ def test_awareness_log_day_param_uses_requested_day(journal_copy): def test_awareness_log_post_creates_201(journal_copy): - client = make_logged_in_test_client(journal_copy) + client = make_test_client(journal_copy) response = client.post( f"{PREFIX}/api/log", @@ -182,7 +190,7 @@ def test_awareness_log_post_creates_201(journal_copy): def test_awareness_log_post_missing_kind_400(journal_copy): - client = make_logged_in_test_client(journal_copy) + client = make_test_client(journal_copy) response = client.post(f"{PREFIX}/api/log", json={}) @@ -190,7 +198,7 @@ def test_awareness_log_post_missing_kind_400(journal_copy): def test_awareness_log_post_empty_kind_400(journal_copy): - client = make_logged_in_test_client(journal_copy) + client = make_test_client(journal_copy) response = client.post(f"{PREFIX}/api/log", json={"kind": ""}) @@ -198,7 +206,7 @@ def test_awareness_log_post_empty_kind_400(journal_copy): def test_awareness_post_endpoints_no_body_400(journal_copy): - client = make_logged_in_test_client(journal_copy) + client = make_test_client(journal_copy) imports_response = client.post(f"{PREFIX}/api/imports") log_response = client.post(f"{PREFIX}/api/log") @@ -210,7 +218,7 @@ def test_awareness_post_endpoints_no_body_400(journal_copy): def test_awareness_post_endpoints_non_json_400(journal_copy): - client = make_logged_in_test_client(journal_copy) + client = make_test_client(journal_copy) imports_response = client.post( f"{PREFIX}/api/imports", diff --git a/tests/test_config_ingest.py b/tests/test_config_ingest.py index a139ad45d..f1443d12e 100644 --- a/tests/test_config_ingest.py +++ b/tests/test_config_ingest.py @@ -94,7 +94,6 @@ def _sample_config(): "allow_network_access": False, "password_hash": "secret_hash", "secret": "secret_value", - "trust_localhost": True, }, "setup": {"completed_at": 12345}, "providers": {"auth": "provider_auth_val", "key_validation": "val123"}, @@ -188,7 +187,7 @@ def test_config_staged(ingest_env): source = load_journal_source(env["key"]) assert response.status_code == 200 - assert body == {"staged": True, "skipped": False, "diff_fields": 12} + assert body == {"staged": True, "skipped": False, "diff_fields": 11} assert (state_dir / "source_config.json").exists() assert (state_dir / "diff.json").exists() assert "last_hash" in _read_json(state_dir / "state.json") diff --git a/tests/test_ensure_journal_config.py b/tests/test_ensure_journal_config.py index 084ef8a06..e70683fef 100644 --- a/tests/test_ensure_journal_config.py +++ b/tests/test_ensure_journal_config.py @@ -36,7 +36,7 @@ def test_ensure_journal_config_creates_file_with_os_defaults(tmp_path, monkeypat assert config["identity"]["name"] == "Test User" assert config["identity"]["preferred"] == "tester" assert config["identity"]["timezone"] == "America/Denver" - assert config["convey"]["secret"] + assert "convey" not in config def test_ensure_journal_config_is_idempotent(tmp_path, monkeypatch): @@ -98,9 +98,9 @@ def test_ensure_journal_config_timezone_resolver_failure_is_isolated( assert _config_path(tmp_path).exists() -def test_ensure_journal_config_backfills_secret_without_touching_identity( +def test_ensure_journal_config_reads_existing_config_without_touching_identity( tmp_path, monkeypatch -): +) -> None: monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) _mock_os(monkeypatch, identity=("OS User", "osuser"), timezone="America/New_York") config_path = _config_path(tmp_path) @@ -111,15 +111,12 @@ def test_ensure_journal_config_backfills_secret_without_touching_identity( "preferred": "Existing", "timezone": "UTC", }, - "convey": {"trust_localhost": True}, } config_path.write_text(json.dumps(staged), encoding="utf-8") config = utils.ensure_journal_config() - assert config["identity"] == staged["identity"] - assert config["convey"]["trust_localhost"] is True - assert config["convey"]["secret"] + assert config == staged def test_ensure_journal_config_raises_on_corrupt_existing_config_without_writing( diff --git a/tests/test_entities_call_parity.py b/tests/test_entities_call_parity.py index 41a1a3e3e..b34122a48 100644 --- a/tests/test_entities_call_parity.py +++ b/tests/test_entities_call_parity.py @@ -27,7 +27,7 @@ from solstone.think.entities.journal import load_journal_entity, save_journal_en from solstone.think.entities.observations import add_observation, save_observations from solstone.think.entities.review_candidates import save_candidates from solstone.think.journal_io import LockTimeout -from tests._baseline_harness import make_logged_in_test_client +from tests._baseline_harness import make_test_client runner = CliRunner() @@ -37,7 +37,7 @@ def _entities_client(monkeypatch: pytest.MonkeyPatch) -> None: def client() -> ConveyClient: journal = Path(os.environ["SOLSTONE_JOURNAL"]) return ConveyClient( - session=make_logged_in_test_client(journal), + session=make_test_client(journal), base_url="", ) @@ -123,7 +123,6 @@ def _ensure_config(journal: Path) -> None: (config_dir / "journal.json").write_text( json.dumps( { - "convey": {"trust_localhost": True}, "setup": {"completed_at": 1700000000000}, } ) diff --git a/tests/test_export.py b/tests/test_export.py index 948c09a05..ab45e6d3b 100644 --- a/tests/test_export.py +++ b/tests/test_export.py @@ -215,7 +215,6 @@ def _setup_config(tmp_path): "allow_network_access": False, "password_hash": "secret_hash", "secret": "secret_val", - "trust_localhost": True, }, "setup": {"completed_at": 12345}, "env": {"KEY": "val"}, @@ -1182,9 +1181,8 @@ class TestExportConfig: "json" ) or mock_session.post.call_args[1].get("json") posted_config = posted_data["config"] - assert posted_config["convey"] == { - "allow_network_access": False, - "trust_localhost": True, - } + assert posted_config["convey"] == {"allow_network_access": False} + assert "password_hash" not in posted_config["convey"] + assert "secret" not in posted_config["convey"] assert posted_config["setup"] == {"completed_at": 12345} assert posted_config["env"] == {"KEY": "val"} diff --git a/tests/test_export_integration.py b/tests/test_export_integration.py index 6cd3ab129..1bf2a1c65 100644 --- a/tests/test_export_integration.py +++ b/tests/test_export_integration.py @@ -278,7 +278,6 @@ def _setup_config(journal_root: Path) -> None: "retention": {"days": 30}, "convey": { "allow_network_access": False, - "trust_localhost": True, "secret": "shhh", }, "env": {"API_KEY": "xyz"}, diff --git a/tests/test_export_pl.py b/tests/test_export_pl.py index a30b25349..95c243e43 100644 --- a/tests/test_export_pl.py +++ b/tests/test_export_pl.py @@ -301,7 +301,6 @@ def test_export_dl_regression_config_url_headers_and_body( "convey": { "password_hash": "secret", "secret": "secret", - "trust_localhost": True, }, } (config_dir / "journal.json").write_text(json.dumps(config), encoding="utf-8") @@ -329,7 +328,7 @@ def test_export_dl_regression_config_url_headers_and_body( ) payload = mock_session.post.call_args.kwargs["json"]["config"] assert payload["identity"] == {"name": "Test"} - assert payload["convey"] == {"trust_localhost": True} + assert payload["convey"] == {} class _FakeInput(io.StringIO): diff --git a/tests/test_health_call_parity.py b/tests/test_health_call_parity.py index d04b8d2f7..e60eaf4bd 100644 --- a/tests/test_health_call_parity.py +++ b/tests/test_health_call_parity.py @@ -13,7 +13,7 @@ from typer.testing import CliRunner from solstone.think.convey_client import ConveyClient from solstone.think.surfaces import health as health_surface from solstone.think.tools.health import app -from tests._baseline_harness import make_logged_in_test_client +from tests._baseline_harness import make_test_client, mark_setup_complete from tests.test_surfaces_health import ( _clear_readiness_snapshot, _minimal_facet_tree, @@ -25,12 +25,13 @@ from tests.test_surfaces_health import ( @pytest.fixture def journal(tmp_path, monkeypatch): monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) + mark_setup_complete(tmp_path) return tmp_path @pytest.fixture def runner(journal, monkeypatch): - client = ConveyClient(session=make_logged_in_test_client(journal), base_url="") + client = ConveyClient(session=make_test_client(journal), base_url="") monkeypatch.setattr("solstone.think.tools.health.get_client", lambda: client) return CliRunner() diff --git a/tests/test_health_routes.py b/tests/test_health_routes.py index 0284f694a..e1cbd72b9 100644 --- a/tests/test_health_routes.py +++ b/tests/test_health_routes.py @@ -3,9 +3,11 @@ from __future__ import annotations +import json + from solstone.convey import create_app from solstone.think.surfaces import health as health_surface -from tests._baseline_harness import make_logged_in_test_client +from tests._baseline_harness import make_test_client from tests.test_surfaces_health import ( _clear_readiness_snapshot, _minimal_facet_tree, @@ -27,6 +29,16 @@ def _assert_error(response, status: int) -> dict: def _configure_journal(tmp_path, monkeypatch) -> None: monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) + config_dir = tmp_path / "config" + config_dir.mkdir(parents=True, exist_ok=True) + (config_dir / "journal.json").write_text( + json.dumps({"setup": {"completed_at": 1}}), + encoding="utf-8", + ) + + +def _configure_unset_journal(tmp_path, monkeypatch) -> None: + monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) def _freeze_health_surface(tmp_path, monkeypatch) -> None: @@ -47,7 +59,7 @@ def _freeze_health_surface(tmp_path, monkeypatch) -> None: def test_summary_returns_report_shape(tmp_path, monkeypatch): _configure_journal(tmp_path, monkeypatch) _freeze_health_surface(tmp_path, monkeypatch) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) response = client.get(f"{PREFIX}/summary?day=20260410") @@ -64,7 +76,7 @@ def test_summary_returns_report_shape(tmp_path, monkeypatch): def test_summary_and_full_identical_for_same_day(tmp_path, monkeypatch): _configure_journal(tmp_path, monkeypatch) _freeze_health_surface(tmp_path, monkeypatch) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) summary_response = client.get(f"{PREFIX}/summary?day=20260410") full_response = client.get(f"{PREFIX}/full?day=20260410") @@ -77,7 +89,7 @@ def test_summary_and_full_identical_for_same_day(tmp_path, monkeypatch): def test_none_field_survives_as_json_null(tmp_path, monkeypatch): _configure_journal(tmp_path, monkeypatch) _freeze_health_surface(tmp_path, monkeypatch) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) response = client.get(f"{PREFIX}/summary?day=20260410") @@ -87,7 +99,7 @@ def test_none_field_survives_as_json_null(tmp_path, monkeypatch): def test_malformed_day_returns_400(tmp_path, monkeypatch): _configure_journal(tmp_path, monkeypatch) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) data = _assert_error(client.get(f"{PREFIX}/summary?day=notaday"), 400) @@ -97,7 +109,7 @@ def test_malformed_day_returns_400(tmp_path, monkeypatch): def test_range_valid_window(tmp_path, monkeypatch): _configure_journal(tmp_path, monkeypatch) _freeze_health_surface(tmp_path, monkeypatch) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) response = client.get(f"{PREFIX}/range?day_from=20260404&day_to=20260410") @@ -108,7 +120,7 @@ def test_range_valid_window(tmp_path, monkeypatch): def test_range_omit_both_uses_default_window(tmp_path, monkeypatch): _configure_journal(tmp_path, monkeypatch) _freeze_health_surface(tmp_path, monkeypatch) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) response = client.get(f"{PREFIX}/range") @@ -118,7 +130,7 @@ def test_range_omit_both_uses_default_window(tmp_path, monkeypatch): def test_range_only_one_endpoint_returns_400(tmp_path, monkeypatch): _configure_journal(tmp_path, monkeypatch) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) data = _assert_error(client.get(f"{PREFIX}/range?day_from=20260404"), 400) @@ -128,7 +140,7 @@ def test_range_only_one_endpoint_returns_400(tmp_path, monkeypatch): def test_range_inverted_returns_400_distinct_detail(tmp_path, monkeypatch): _configure_journal(tmp_path, monkeypatch) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) data = _assert_error( client.get(f"{PREFIX}/range?day_from=20260410&day_to=20260404"), @@ -139,8 +151,8 @@ def test_range_inverted_returns_400_distinct_detail(tmp_path, monkeypatch): assert "day_from must be <= day_to" in data["detail"] -def test_health_requires_login(tmp_path, monkeypatch): - _configure_journal(tmp_path, monkeypatch) +def test_health_redirects_to_init_when_setup_incomplete(tmp_path, monkeypatch): + _configure_unset_journal(tmp_path, monkeypatch) app = create_app(journal=str(tmp_path)) app.config["TESTING"] = True client = app.test_client() @@ -148,3 +160,4 @@ def test_health_requires_login(tmp_path, monkeypatch): response = client.get(f"{PREFIX}/summary") assert response.status_code == 302 + assert "/init" in response.headers["Location"] diff --git a/tests/test_home_reflections.py b/tests/test_home_reflections.py index ab94a59f3..90ec6d0ac 100644 --- a/tests/test_home_reflections.py +++ b/tests/test_home_reflections.py @@ -12,9 +12,6 @@ def _make_client(journal_path: str): app = create_app(journal_path) app.config["TESTING"] = True client = app.test_client() - with client.session_transaction() as session: - session["logged_in"] = True - session.permanent = True return client diff --git a/tests/test_import_call.py b/tests/test_import_call.py index e01beebf3..6b7031671 100644 --- a/tests/test_import_call.py +++ b/tests/test_import_call.py @@ -20,7 +20,7 @@ from solstone.think.entities.journal import ( save_journal_entity, ) from solstone.think.entities.relationships import load_facet_relationship -from tests._baseline_harness import make_logged_in_test_client +from tests._baseline_harness import make_test_client, mark_setup_complete import_call = import_module("solstone.apps.import.call") import_resolve = import_module("solstone.apps.import.resolve") @@ -70,6 +70,7 @@ def import_env(tmp_path, monkeypatch): parents=True, exist_ok=True ) (tmp_path / "config").mkdir(parents=True, exist_ok=True) + mark_setup_complete(tmp_path) key = generate_key() source = _source(key=key) @@ -81,7 +82,7 @@ def import_env(tmp_path, monkeypatch): def _client() -> ConveyClient: journal = Path(os.environ["SOLSTONE_JOURNAL"]) return ConveyClient( - session=make_logged_in_test_client(journal), + session=make_test_client(journal), base_url="", ) @@ -483,7 +484,10 @@ def test_resolve_config_apply(import_env): ) _write_json( import_env["root"] / "config" / "journal.json", - {"identity": {"name": "Local User"}}, + { + "identity": {"name": "Local User"}, + "setup": {"completed_at": 1700000000000}, + }, ) result = runner.invoke( @@ -527,7 +531,11 @@ def test_resolve_config_keep(import_env): {"retention": {"days": 30}}, ) _write_json( - import_env["root"] / "config" / "journal.json", {"retention": {"days": 90}} + import_env["root"] / "config" / "journal.json", + { + "retention": {"days": 90}, + "setup": {"completed_at": 1700000000000}, + }, ) result = runner.invoke( @@ -573,7 +581,11 @@ def test_resolve_config_all_transferable(import_env): ) _write_json( import_env["root"] / "config" / "journal.json", - {"identity": {"name": "Local User"}, "retention": {"days": 90}}, + { + "identity": {"name": "Local User"}, + "retention": {"days": 90}, + "setup": {"completed_at": 1700000000000}, + }, ) result = runner.invoke( diff --git a/tests/test_init.py b/tests/test_init.py index 55ecf9904..c2dab18de 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc -import base64 import json import re from pathlib import Path @@ -32,11 +31,8 @@ def _make_empty_client(tmp_path, monkeypatch, *, timezone="America/Denver"): return app.test_client(), journal -def _remove_password(journal_dir): +def _clear_setup(journal_dir): config = _read_config(journal_dir) - config["convey"].pop("password_hash", None) - config["convey"].pop("password", None) - config["convey"].pop("trust_localhost", None) config.pop("setup", None) (journal_dir / "config" / "journal.json").write_text(json.dumps(config, indent=2)) @@ -68,7 +64,7 @@ def _save_test_observer( @pytest.fixture def fresh_client(journal_copy): - _remove_password(journal_copy) + _clear_setup(journal_copy) app = create_app(str(journal_copy)) app.config["TESTING"] = True return app.test_client() @@ -82,15 +78,15 @@ def configured_client(journal_copy): class TestInitDetection: - def test_redirects_to_init_when_no_password(self, fresh_client): - resp = fresh_client.get("/", headers={"X-Forwarded-For": "1.2.3.4"}) + def test_redirects_to_init_when_setup_incomplete(self, fresh_client): + resp = fresh_client.get("/") assert resp.status_code == 302 assert "/init" in resp.headers["Location"] - def test_redirects_to_login_when_password_exists(self, configured_client): - resp = configured_client.get("/", headers={"X-Forwarded-For": "1.2.3.4"}) + def test_setup_complete_serves_root(self, configured_client): + resp = configured_client.get("/") assert resp.status_code == 302 - assert "/login" in resp.headers["Location"] + assert resp.headers["Location"].endswith("/app/home/") def test_init_page_renders(self, fresh_client): resp = fresh_client.get("/init") @@ -251,7 +247,6 @@ class TestInitDetection: def test_init_retention_reflects_persisted_state(self, journal_copy): config = _read_config(journal_copy) config.pop("setup", None) - config["convey"].pop("password_hash", None) config["retention"] = {"raw_media": "days", "raw_media_days": 14} (journal_copy / "config" / "journal.json").write_text( json.dumps(config, indent=2) @@ -302,14 +297,13 @@ class TestInitDetection: assert config["identity"]["name"] == "OS User" assert config["identity"]["preferred"] == "osuser" assert config["identity"]["timezone"] == "America/Denver" - assert config["convey"]["secret"] + assert "convey" not in config assert b'value="OS User"' in resp.data assert b'value="osuser"' in resp.data def test_init_escapes_identity_values(self, journal_copy): config = _read_config(journal_copy) config.pop("setup", None) - config["convey"].pop("password_hash", None) config["identity"]["name"] = "" (journal_copy / "config" / "journal.json").write_text( json.dumps(config, indent=2) @@ -325,7 +319,6 @@ class TestInitDetection: def test_init_does_not_overwrite_existing_identity(self, journal_copy): config = _read_config(journal_copy) config.pop("setup", None) - config["convey"].pop("password_hash", None) config["identity"]["name"] = "Existing User" config["identity"]["preferred"] = "Existing" config["identity"]["timezone"] = "UTC" @@ -429,8 +422,8 @@ class TestInitObservers: assert isinstance(data["thresholds"]["active_ms"], int) assert isinstance(data["thresholds"]["stale_ms"], int) - def test_observers_no_password_required(self, fresh_client, monkeypatch): - """Observers endpoint works without password_hash set.""" + def test_observers_available_before_setup(self, fresh_client, monkeypatch): + """Observers endpoint works before setup completes.""" monkeypatch.setattr( "solstone.apps.observer.utils.list_observers", lambda: [], @@ -479,7 +472,7 @@ class TestInitObservers: assert observers[0]["elapsed_ms"] is None assert observers[0]["clock_skew"] is False - def test_init_observers_endpoint_parity(self, fresh_client): + def test_init_observers_endpoint_parity(self, fresh_client, journal_copy): current_now = now_ms() _save_test_observer( "aaaa0000", @@ -499,9 +492,12 @@ class TestInitObservers: created_at=30, last_seen=current_now - 600_000, ) - - with fresh_client.session_transaction() as sess: - sess["logged_in"] = True + config = _read_config(journal_copy) + config["setup"] = {"completed_at": current_now} + (journal_copy / "config" / "journal.json").write_text( + json.dumps(config, indent=2), + encoding="utf-8", + ) api_resp = fresh_client.get("/app/observer/api/list") init_resp = fresh_client.get("/init/observers") @@ -535,7 +531,6 @@ class TestInitFinalize: resp = fresh_client.post( "/init/finalize", json={ - "password": "securepass123", "name": "Jane Doe", "preferred": "Jane", "timezone": "America/Denver", @@ -549,12 +544,7 @@ class TestInitFinalize: assert data["redirect"] == "/app/thinking/" config = _read_config(journal_copy) - # Password - from werkzeug.security import check_password_hash - - assert check_password_hash(config["convey"]["password_hash"], "securepass123") assert "allow_network_access" not in config["convey"] - assert config["convey"]["trust_localhost"] is True # Identity assert config["identity"]["name"] == "Jane Doe" assert config["identity"]["preferred"] == "Jane" @@ -564,7 +554,7 @@ class TestInitFinalize: # Setup assert "completed_at" in config["setup"] - def test_finalize_no_password_succeeds(self, fresh_client, journal_copy): + def test_finalize_succeeds(self, fresh_client, journal_copy): resp = fresh_client.post( "/init/finalize", json={"name": "Jane"}, @@ -577,16 +567,6 @@ class TestInitFinalize: config = _read_config(journal_copy) assert "completed_at" in config["setup"] assert "allow_network_access" not in config["convey"] - assert config["convey"]["trust_localhost"] is True - assert "password_hash" not in config["convey"] - - def test_finalize_password_too_short(self, fresh_client): - resp = fresh_client.post( - "/init/finalize", - json={"password": "short"}, - content_type="application/json", - ) - assert resp.status_code == 400 def test_finalize_minimal(self, fresh_client, journal_copy): """Finalize with optional fields omitted.""" @@ -597,7 +577,6 @@ class TestInitFinalize: ) assert resp.status_code == 200 config = _read_config(journal_copy) - assert "password_hash" not in config["convey"] assert "completed_at" in config["setup"] # No gemini key written assert "GOOGLE_API_KEY" not in config.get("env", {}) @@ -646,25 +625,20 @@ class TestInitFinalize: assert config["identity"]["timezone"] == "America/Denver" assert "completed_at" in config["setup"] - def test_finalize_auto_login(self, fresh_client, journal_copy): + def test_finalize_completes_setup_access(self, fresh_client, journal_copy): response = fresh_client.post( "/init/finalize", json={}, content_type="application/json", ) assert response.get_json()["redirect"] == "/app/thinking/" - with fresh_client.session_transaction() as session: - assert session["logged_in"] is True - resp = fresh_client.get("/", headers={"X-Forwarded-For": "1.2.3.4"}) + resp = fresh_client.get("/") assert resp.status_code == 302 - location = resp.headers["Location"] - assert "/login" not in location - assert "/init" not in location + assert resp.headers["Location"].endswith("/app/home/") def test_finalize_no_early_config_write(self, fresh_client, journal_copy): - """Before finalize, config should have no password_hash or setup.""" + """Before finalize, config should have no setup.""" config = _read_config(journal_copy) - assert "password_hash" not in config.get("convey", {}) assert "setup" not in config or "completed_at" not in config.get("setup", {}) def test_post_init_redirect(self, fresh_client, journal_copy): @@ -722,189 +696,3 @@ class TestInitFinalize: assert resp.get_json()["reason_code"] == "corrupt_config" write_config.assert_not_called() assert config_path.read_bytes() == before - - -class TestRemovedEndpoints: - """Verify old endpoints no longer exist.""" - - def test_init_password_gone(self, fresh_client): - resp = fresh_client.post( - "/init/password", - json={"password": "securepass123"}, - content_type="application/json", - ) - assert resp.status_code in (404, 405) - - def test_init_identity_gone(self, fresh_client): - resp = fresh_client.post( - "/init/identity", - json={"name": "Jane"}, - content_type="application/json", - ) - assert resp.status_code in (404, 405) - - def test_init_provider_gone(self, fresh_client): - resp = fresh_client.post( - "/init/provider", - json={"key": "some-key"}, - content_type="application/json", - ) - assert resp.status_code in (404, 405) - - -class TestLocalhostBypass: - """Tests for the opt-in trust_localhost bypass.""" - - def test_localhost_fresh_install_redirects_to_init(self, fresh_client): - """Plain localhost with no config → redirect to /init.""" - resp = fresh_client.get("/") - assert resp.status_code == 302 - assert "/init" in resp.headers["Location"] - - def test_localhost_trust_bypass(self, journal_copy): - """Localhost + trust_localhost + setup.completed_at → pass through.""" - config = _read_config(journal_copy) - config["convey"]["trust_localhost"] = True - config["setup"] = {"completed_at": 1700000000000} - (journal_copy / "config" / "journal.json").write_text( - json.dumps(config, indent=2) - ) - app = create_app(str(journal_copy)) - app.config["TESTING"] = True - client = app.test_client() - resp = client.get("/") - assert resp.status_code == 302 - # Should redirect to home app, not login or init - assert "/login" not in resp.headers["Location"] - assert "/init" not in resp.headers["Location"] - - def test_localhost_trust_without_setup_redirects_to_init(self, journal_copy): - """trust_localhost set but no setup.completed_at → redirect to /init.""" - config = _read_config(journal_copy) - config["convey"]["trust_localhost"] = True - config.pop("setup", None) - config["convey"].pop("password_hash", None) - (journal_copy / "config" / "journal.json").write_text( - json.dumps(config, indent=2) - ) - app = create_app(str(journal_copy)) - app.config["TESTING"] = True - client = app.test_client() - resp = client.get("/") - assert resp.status_code == 302 - assert "/init" in resp.headers["Location"] - - def test_localhost_trust_disabled_redirects_to_login(self, journal_copy): - """Localhost + setup.completed_at + trust_localhost false → redirect to /login.""" - config = _read_config(journal_copy) - config["convey"]["trust_localhost"] = False - config["setup"] = {"completed_at": 1700000000000} - (journal_copy / "config" / "journal.json").write_text( - json.dumps(config, indent=2) - ) - app = create_app(str(journal_copy)) - app.config["TESTING"] = True - client = app.test_client() - resp = client.get("/") - assert resp.status_code == 302 - assert "/login" in resp.headers["Location"] - - def test_proxy_header_defeats_trust_localhost(self, configured_client): - """Proxy headers prevent trust_localhost bypass.""" - resp = configured_client.get("/", headers={"X-Forwarded-For": "1.2.3.4"}) - assert resp.status_code == 302 - assert "/login" in resp.headers["Location"] - - -class TestBasicAuth: - """Tests for Basic Auth support.""" - - def test_basic_auth_correct_password(self, configured_client): - """Basic Auth with correct password → authenticated.""" - creds = base64.b64encode(b":test123").decode() - resp = configured_client.get( - "/", - headers={ - "Authorization": f"Basic {creds}", - "X-Forwarded-For": "1.2.3.4", - }, - ) - assert resp.status_code == 302 - # Should redirect to home app, not login or init - assert "/login" not in resp.headers["Location"] - assert "/init" not in resp.headers["Location"] - - def test_basic_auth_wrong_password(self, configured_client): - """Basic Auth with wrong password → redirect to /login.""" - creds = base64.b64encode(b":wrongpassword").decode() - resp = configured_client.get( - "/", - headers={ - "Authorization": f"Basic {creds}", - "X-Forwarded-For": "1.2.3.4", - }, - ) - assert resp.status_code == 302 - assert "/login" in resp.headers["Location"] - - def test_basic_auth_no_session(self, configured_client): - """Basic Auth does not create a session — next request without header fails.""" - creds = base64.b64encode(b":test123").decode() - # First request with Basic Auth succeeds - resp1 = configured_client.get( - "/", - headers={ - "Authorization": f"Basic {creds}", - "X-Forwarded-For": "1.2.3.4", - }, - ) - assert "/login" not in resp1.headers["Location"] - - # Second request without Basic Auth → should redirect to login - resp2 = configured_client.get("/", headers={"X-Forwarded-For": "1.2.3.4"}) - assert resp2.status_code == 302 - assert "/login" in resp2.headers["Location"] - - -class TestSetupMigration: - """Tests for the _migrate_setup_completed migration. - - Legacy-only: handles journals where password was set via CLI before - web onboarding existed. New onboarding writes all config atomically. - """ - - def test_migration_writes_setup_and_trust(self, journal_copy): - """App startup with password_hash but no setup.completed_at writes both.""" - config = _read_config(journal_copy) - config.pop("setup", None) - config["convey"].pop("trust_localhost", None) - (journal_copy / "config" / "journal.json").write_text( - json.dumps(config, indent=2) - ) - - # create_app triggers the migration - create_app(str(journal_copy)) - - config = _read_config(journal_copy) - assert "completed_at" in config.get("setup", {}) - assert config["convey"].get("trust_localhost") is True - - def test_migration_idempotent(self, journal_copy): - """Running migration twice is a no-op.""" - config = _read_config(journal_copy) - config.pop("setup", None) - config["convey"].pop("trust_localhost", None) - (journal_copy / "config" / "journal.json").write_text( - json.dumps(config, indent=2) - ) - - # First run triggers migration - create_app(str(journal_copy)) - config1 = _read_config(journal_copy) - ts1 = config1["setup"]["completed_at"] - - # Second run should be a no-op - create_app(str(journal_copy)) - config2 = _read_config(journal_copy) - assert config2["setup"]["completed_at"] == ts1 - assert config2["convey"]["trust_localhost"] is True diff --git a/tests/test_ledger_call_parity.py b/tests/test_ledger_call_parity.py index 50a736fdd..1c50b7e7c 100644 --- a/tests/test_ledger_call_parity.py +++ b/tests/test_ledger_call_parity.py @@ -15,7 +15,7 @@ from solstone.convey.reasons import ACTIVITIES_BUSY from solstone.think.convey_client import ConveyClient from solstone.think.journal_io import LockTimeout from solstone.think.tools.ledger import app -from tests._baseline_harness import make_logged_in_test_client +from tests._baseline_harness import make_test_client, mark_setup_complete from tests.test_surfaces_ledger import ( _commitment, _decision, @@ -28,12 +28,13 @@ from tests.test_surfaces_ledger import ( @pytest.fixture def journal(tmp_path, monkeypatch): monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) + mark_setup_complete(tmp_path) return tmp_path @pytest.fixture def runner(journal, monkeypatch): - client = ConveyClient(session=make_logged_in_test_client(journal), base_url="") + client = ConveyClient(session=make_test_client(journal), base_url="") monkeypatch.setattr("solstone.think.tools.ledger.get_client", lambda: client) return CliRunner() diff --git a/tests/test_ledger_routes.py b/tests/test_ledger_routes.py index a9bd8fba3..cc2e3f0f6 100644 --- a/tests/test_ledger_routes.py +++ b/tests/test_ledger_routes.py @@ -3,8 +3,10 @@ from __future__ import annotations +import json + from solstone.convey import create_app -from tests._baseline_harness import make_logged_in_test_client +from tests._baseline_harness import make_test_client from tests.test_surfaces_ledger import ( _commitment, _minimal_facet_tree, @@ -26,6 +28,16 @@ def _assert_error(response, status: int) -> dict: def _configure_journal(tmp_path, monkeypatch) -> None: monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) + config_dir = tmp_path / "config" + config_dir.mkdir(parents=True, exist_ok=True) + (config_dir / "journal.json").write_text( + json.dumps({"setup": {"completed_at": 1}}), + encoding="utf-8", + ) + + +def _configure_unset_journal(tmp_path, monkeypatch) -> None: + monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) def _decision( @@ -54,7 +66,7 @@ def test_ledger_list_collection_envelope_and_bound(tmp_path, monkeypatch): _utc_ms(f"2026-04-10T09:{index:02d}:00Z"), commitments=[_commitment(action=f"action number {index}")], ) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) response = client.get(PREFIX) @@ -90,7 +102,7 @@ def test_ledger_list_counterparty_filter_narrows_total(tmp_path, monkeypatch): ) ], ) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) all_response = client.get(PREFIX) filtered_response = client.get(f"{PREFIX}?counterparty=Ravi") @@ -103,7 +115,7 @@ def test_ledger_list_counterparty_filter_narrows_total(tmp_path, monkeypatch): def test_ledger_list_bad_state_returns_invalid_request_value(tmp_path, monkeypatch): _configure_journal(tmp_path, monkeypatch) _minimal_facet_tree(tmp_path) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) data = _assert_error(client.get(f"{PREFIX}?state=bogus"), 400) @@ -113,7 +125,7 @@ def test_ledger_list_bad_state_returns_invalid_request_value(tmp_path, monkeypat def test_ledger_list_bad_sort_returns_invalid_request_value(tmp_path, monkeypatch): _configure_journal(tmp_path, monkeypatch) _minimal_facet_tree(tmp_path) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) data = _assert_error(client.get(f"{PREFIX}?sort=bogus"), 400) @@ -125,7 +137,7 @@ def test_ledger_list_bad_age_days_gte_returns_invalid_request_value( ): _configure_journal(tmp_path, monkeypatch) _minimal_facet_tree(tmp_path) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) data = _assert_error(client.get(f"{PREFIX}?age_days_gte=abc"), 400) @@ -135,7 +147,7 @@ def test_ledger_list_bad_age_days_gte_returns_invalid_request_value( def test_ledger_list_bad_closed_since_returns_invalid_day(tmp_path, monkeypatch): _configure_journal(tmp_path, monkeypatch) _minimal_facet_tree(tmp_path) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) notaday = _assert_error(client.get(f"{PREFIX}?closed_since=notaday"), 400) bad_month = _assert_error(client.get(f"{PREFIX}?closed_since=20261301"), 400) @@ -154,7 +166,7 @@ def test_ledger_decisions_collection_envelope_and_routing(tmp_path, monkeypatch) _utc_ms("2026-04-10T09:00:00Z"), decisions=[_decision(action="move launch review")], ) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) response = client.get(f"{PREFIX}/decisions") @@ -168,7 +180,7 @@ def test_ledger_decisions_collection_envelope_and_routing(tmp_path, monkeypatch) def test_ledger_decisions_bad_since_returns_invalid_day(tmp_path, monkeypatch): _configure_journal(tmp_path, monkeypatch) _minimal_facet_tree(tmp_path) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) data = _assert_error(client.get(f"{PREFIX}/decisions?since=bad"), 400) @@ -185,7 +197,7 @@ def test_ledger_get_item_returns_recursive_dataclass_dict(tmp_path, monkeypatch) _utc_ms("2026-04-10T09:00:00Z"), commitments=[_commitment()], ) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) item_id = client.get(PREFIX).get_json()["items"][0]["id"] response = client.get(f"{PREFIX}/{item_id}") @@ -200,7 +212,7 @@ def test_ledger_get_item_returns_recursive_dataclass_dict(tmp_path, monkeypatch) def test_ledger_get_item_missing_returns_ledger_item_not_found(tmp_path, monkeypatch): _configure_journal(tmp_path, monkeypatch) _minimal_facet_tree(tmp_path) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) data = _assert_error(client.get(f"{PREFIX}/does-not-exist"), 404) @@ -217,7 +229,7 @@ def test_ledger_close_flips_state(tmp_path, monkeypatch): _utc_ms("2026-04-10T09:00:00Z"), commitments=[_commitment()], ) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) item_id = client.get(PREFIX).get_json()["items"][0]["id"] closed_response = client.post(f"{PREFIX}/{item_id}/close", json={"note": "done"}) @@ -242,7 +254,7 @@ def test_ledger_close_decision_id_returns_ledger_item_not_found(tmp_path, monkey _utc_ms("2026-04-10T09:00:00Z"), decisions=[_decision()], ) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) decision_id = client.get(f"{PREFIX}/decisions").get_json()["items"][0]["id"] data = _assert_error( @@ -255,7 +267,7 @@ def test_ledger_close_decision_id_returns_ledger_item_not_found(tmp_path, monkey def test_ledger_close_no_body_returns_missing_request_body(tmp_path, monkeypatch): _configure_journal(tmp_path, monkeypatch) _minimal_facet_tree(tmp_path) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) data = _assert_error(client.post(f"{PREFIX}/does-not-exist/close"), 400) @@ -265,7 +277,7 @@ def test_ledger_close_no_body_returns_missing_request_body(tmp_path, monkeypatch def test_ledger_close_non_json_returns_invalid_json_request(tmp_path, monkeypatch): _configure_journal(tmp_path, monkeypatch) _minimal_facet_tree(tmp_path) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) data = _assert_error( client.post( @@ -282,7 +294,7 @@ def test_ledger_close_non_json_returns_invalid_json_request(tmp_path, monkeypatc def test_ledger_close_empty_note_returns_missing_required_field(tmp_path, monkeypatch): _configure_journal(tmp_path, monkeypatch) _minimal_facet_tree(tmp_path) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) data = _assert_error( client.post(f"{PREFIX}/does-not-exist/close", json={"note": " "}), 400 @@ -294,7 +306,7 @@ def test_ledger_close_empty_note_returns_missing_required_field(tmp_path, monkey def test_ledger_close_bad_as_state_returns_invalid_request_value(tmp_path, monkeypatch): _configure_journal(tmp_path, monkeypatch) _minimal_facet_tree(tmp_path) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) data = _assert_error( client.post( @@ -307,8 +319,8 @@ def test_ledger_close_bad_as_state_returns_invalid_request_value(tmp_path, monke assert data["reason_code"] == "invalid_request_value" -def test_ledger_requires_login(tmp_path, monkeypatch): - _configure_journal(tmp_path, monkeypatch) +def test_ledger_redirects_to_init_when_setup_incomplete(tmp_path, monkeypatch): + _configure_unset_journal(tmp_path, monkeypatch) app = create_app(journal=str(tmp_path)) app.config["TESTING"] = True client = app.test_client() @@ -316,3 +328,4 @@ def test_ledger_requires_login(tmp_path, monkeypatch): response = client.get(PREFIX) assert response.status_code == 302 + assert "/init" in response.headers["Location"] diff --git a/tests/test_link_call_parity.py b/tests/test_link_call_parity.py index 479d6bb46..27f3d67ee 100644 --- a/tests/test_link_call_parity.py +++ b/tests/test_link_call_parity.py @@ -18,7 +18,7 @@ from solstone.think.link.paths import ( authorized_clients_path, nonces_path, ) -from tests._baseline_harness import make_logged_in_test_client +from tests._baseline_harness import make_test_client PAIRED_AT = "2026-04-19T00:00:00Z" LAST_SEEN_AT = "2026-04-19T00:30:00Z" @@ -33,7 +33,6 @@ def journal(tmp_path, monkeypatch): (config_dir / "journal.json").write_text( json.dumps( { - "convey": {"trust_localhost": True}, "setup": {"completed_at": 1700000000000}, }, indent=2, @@ -45,7 +44,7 @@ def journal(tmp_path, monkeypatch): @pytest.fixture def runner(journal, monkeypatch): - client = ConveyClient(session=make_logged_in_test_client(journal), base_url="") + client = ConveyClient(session=make_test_client(journal), base_url="") monkeypatch.setattr("solstone.apps.link.call.get_client", lambda: client) return CliRunner() diff --git a/tests/test_password.py b/tests/test_password.py deleted file mode 100644 index dc776b0c0..000000000 --- a/tests/test_password.py +++ /dev/null @@ -1,136 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""Tests for password hashing: login, migration, and settings API.""" - -from __future__ import annotations - -import json -from pathlib import Path - -import pytest -from werkzeug.security import check_password_hash - -from solstone.convey import create_app -from tests.conftest import copytree_tracked - - -@pytest.fixture -def client(journal_copy): - app = create_app(str(journal_copy)) - app.config["TESTING"] = True - return app.test_client() - - -def _read_config(journal_dir): - return json.loads((journal_dir / "config" / "journal.json").read_text()) - - -class TestLogin: - def test_correct_password(self, client): - resp = client.post("/login", data={"password": "test123"}) - assert resp.status_code == 302 - - def test_wrong_password(self, client): - resp = client.post("/login", data={"password": "wrong"}) - assert resp.status_code == 200 - assert b"incorrect password. passwords are case-sensitive." in resp.data - - def test_no_password_configured(self, journal_copy): - config = _read_config(journal_copy) - config["convey"].pop("password_hash", None) - config["convey"].pop("password", None) - (journal_copy / "config" / "journal.json").write_text( - json.dumps(config, indent=2) - ) - app = create_app(str(journal_copy)) - app.config["TESTING"] = True - client = app.test_client() - resp = client.get("/login") - assert b"journal password set" in resp.data - - -class TestMigration: - def test_plaintext_migrated_to_hash(self, tmp_path, monkeypatch): - """Plaintext password is hashed and old key removed on app creation.""" - src = Path(__file__).resolve().parent / "fixtures" / "journal" - dst = tmp_path / "journal" - copytree_tracked(src, dst) - config_path = dst / "config" / "journal.json" - config = json.loads(config_path.read_text()) - config["convey"].pop("password_hash", None) - config["convey"]["password"] = "migrate-me" - config_path.write_text(json.dumps(config, indent=2)) - monkeypatch.setenv("SOLSTONE_JOURNAL", str(dst)) - - create_app(str(dst)) - - config = json.loads(config_path.read_text()) - assert "password" not in config["convey"] - assert "password_hash" in config["convey"] - assert check_password_hash(config["convey"]["password_hash"], "migrate-me") - - def test_empty_password_removed(self, tmp_path, monkeypatch): - """Empty plaintext password is removed, not hashed.""" - src = Path(__file__).resolve().parent / "fixtures" / "journal" - dst = tmp_path / "journal" - copytree_tracked(src, dst) - config_path = dst / "config" / "journal.json" - config = json.loads(config_path.read_text()) - config["convey"].pop("password_hash", None) - config["convey"]["password"] = "" - config_path.write_text(json.dumps(config, indent=2)) - monkeypatch.setenv("SOLSTONE_JOURNAL", str(dst)) - - create_app(str(dst)) - - config = json.loads(config_path.read_text()) - assert "password" not in config["convey"] - assert "password_hash" not in config["convey"] - - def test_already_migrated_skipped(self, journal_copy): - """If password_hash exists, migration is a no-op.""" - config_before = _read_config(journal_copy) - hash_before = config_before["convey"]["password_hash"] - - create_app(str(journal_copy)) - - config_after = _read_config(journal_copy) - assert config_after["convey"]["password_hash"] == hash_before - - -class TestSettingsAPI: - def test_get_config_strips_password(self, client): - """GET /app/settings/api/config must not return password or password_hash.""" - resp = client.get("/app/settings/api/config") - data = resp.get_json() - convey = data.get("convey", {}) - assert "password" not in convey - assert "password_hash" not in convey - assert convey.get("has_password") is True - - def test_put_hashes_password(self, client, journal_copy): - """PUT with convey.password hashes before writing to disk.""" - resp = client.put( - "/app/settings/api/config", - json={"section": "convey", "data": {"password": "new-secret"}}, - content_type="application/json", - ) - assert resp.status_code == 200 - config = _read_config(journal_copy) - assert "password" not in config["convey"] - assert check_password_hash(config["convey"]["password_hash"], "new-secret") - - def test_put_empty_password_skipped(self, client, journal_copy): - """PUT with empty password does not overwrite existing hash.""" - config_before = _read_config(journal_copy) - hash_before = config_before["convey"]["password_hash"] - - resp = client.put( - "/app/settings/api/config", - json={"section": "convey", "data": {"password": ""}}, - content_type="application/json", - ) - assert resp.status_code == 200 - config_after = _read_config(journal_copy) - assert config_after["convey"]["password_hash"] == hash_before diff --git a/tests/test_password_cli.py b/tests/test_password_cli.py deleted file mode 100644 index d63da7901..000000000 --- a/tests/test_password_cli.py +++ /dev/null @@ -1,99 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -"""Tests for the journal password CLI.""" - -from __future__ import annotations - -import json - -import pytest -from werkzeug.security import check_password_hash - -from solstone.think.password_cli import main - - -def _read_config(journal_dir): - return json.loads((journal_dir / "config" / "journal.json").read_text()) - - -def _mock_getpass(monkeypatch, *responses): - """Mock getpass.getpass to return successive responses.""" - it = iter(responses) - monkeypatch.setattr( - "solstone.think.password_cli.getpass.getpass", lambda prompt="": next(it) - ) - - -class TestSetPassword: - def test_set_writes_hash(self, journal_copy, monkeypatch, capsys): - monkeypatch.setattr("sys.argv", ["sol password", "set"]) - _mock_getpass(monkeypatch, "mypassword", "mypassword") - - main() - - config = _read_config(journal_copy) - assert config["convey"]["password_hash"].startswith("scrypt:") - assert check_password_hash(config["convey"]["password_hash"], "mypassword") - assert "Password set successfully." in capsys.readouterr().out - - def test_mismatch_rejected(self, journal_copy, monkeypatch, capsys): - monkeypatch.setattr("sys.argv", ["sol password", "set"]) - _mock_getpass(monkeypatch, "password1", "different") - - with pytest.raises(SystemExit) as exc_info: - main() - - assert exc_info.value.code == 1 - assert "Passwords do not match." in capsys.readouterr().err - - def test_plaintext_cleanup(self, journal_copy, monkeypatch): - # Seed a plaintext password - config_path = journal_copy / "config" / "journal.json" - config = json.loads(config_path.read_text()) - config["convey"]["password"] = "old-plaintext" - config_path.write_text(json.dumps(config, indent=2)) - - monkeypatch.setattr("sys.argv", ["sol password", "set"]) - _mock_getpass(monkeypatch, "newpass", "newpass") - - main() - - config = _read_config(journal_copy) - assert "password" not in config["convey"] - assert "password_hash" in config["convey"] - - def test_no_config_file(self, tmp_path, monkeypatch, capsys): - """Works when no journal.json exists yet.""" - monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) - monkeypatch.setattr("sys.argv", ["sol password", "set"]) - _mock_getpass(monkeypatch, "freshpass", "freshpass") - - main() - - config_path = tmp_path / "config" / "journal.json" - assert config_path.exists() - config = json.loads(config_path.read_text()) - assert check_password_hash(config["convey"]["password_hash"], "freshpass") - assert config_path.stat().st_mode & 0o777 == 0o600 - - def test_file_permissions(self, journal_copy, monkeypatch): - monkeypatch.setattr("sys.argv", ["sol password", "set"]) - _mock_getpass(monkeypatch, "securepass", "securepass") - - main() - - config_path = journal_copy / "config" / "journal.json" - assert config_path.stat().st_mode & 0o777 == 0o600 - - -class TestResetAlias: - def test_reset_writes_hash(self, journal_copy, monkeypatch, capsys): - monkeypatch.setattr("sys.argv", ["sol password", "reset"]) - _mock_getpass(monkeypatch, "resetpass", "resetpass") - - main() - - config = _read_config(journal_copy) - assert check_password_hash(config["convey"]["password_hash"], "resetpass") - assert "Password set successfully." in capsys.readouterr().out diff --git a/tests/test_profile_call_parity.py b/tests/test_profile_call_parity.py index ddcdddba1..02ad91ac0 100644 --- a/tests/test_profile_call_parity.py +++ b/tests/test_profile_call_parity.py @@ -13,7 +13,7 @@ from typer.testing import CliRunner from solstone.think.convey_client import ConveyClient from solstone.think.surfaces import profile as profile_surface from solstone.think.tools.profile import app -from tests._baseline_harness import make_logged_in_test_client +from tests._baseline_harness import make_test_client, mark_setup_complete from tests.test_surfaces_profile import ( _activity_record, _append_activity, @@ -29,12 +29,13 @@ from tests.test_surfaces_profile import ( @pytest.fixture def journal(tmp_path, monkeypatch): _configure_env(tmp_path, monkeypatch) + mark_setup_complete(tmp_path) return tmp_path @pytest.fixture def runner(journal, monkeypatch): - client = ConveyClient(session=make_logged_in_test_client(journal), base_url="") + client = ConveyClient(session=make_test_client(journal), base_url="") monkeypatch.setattr("solstone.think.tools.profile.get_client", lambda: client) return CliRunner() diff --git a/tests/test_profile_routes.py b/tests/test_profile_routes.py index 34578caa5..81e0dcc96 100644 --- a/tests/test_profile_routes.py +++ b/tests/test_profile_routes.py @@ -3,9 +3,11 @@ from __future__ import annotations +import json + from solstone.convey import create_app from solstone.think.surfaces import profile as profile_surface -from tests._baseline_harness import make_logged_in_test_client +from tests._baseline_harness import make_test_client from tests.test_surfaces_profile import ( _activity_record, _append_activity, @@ -30,8 +32,22 @@ def _assert_error(response, status: int) -> dict: return data -def _seed_ravi(tmp_path, monkeypatch) -> None: +def _configure_journal(tmp_path, monkeypatch) -> None: + monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) + config_dir = tmp_path / "config" + config_dir.mkdir(parents=True, exist_ok=True) + (config_dir / "journal.json").write_text( + json.dumps({"setup": {"completed_at": 1}}), + encoding="utf-8", + ) + + +def _configure_unset_journal(tmp_path, monkeypatch) -> None: monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) + + +def _seed_ravi(tmp_path, monkeypatch) -> None: + _configure_journal(tmp_path, monkeypatch) _minimal_facet_tree( tmp_path, journal_entities=({"id": "ravi", "name": "Ravi", "type": "Person"},), @@ -58,7 +74,7 @@ def _seed_ravi(tmp_path, monkeypatch) -> None: def test_profile_full_composed(tmp_path, monkeypatch): _seed_ravi(tmp_path, monkeypatch) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) response = client.get(f"{PROFILE_PREFIX}/Ravi") @@ -72,10 +88,10 @@ def test_profile_full_composed(tmp_path, monkeypatch): def test_profile_full_not_found(tmp_path, monkeypatch): - monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) + _configure_journal(tmp_path, monkeypatch) _minimal_facet_tree(tmp_path) monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260607") - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) data = _assert_error(client.get(f"{PROFILE_PREFIX}/missing"), 404) @@ -84,7 +100,7 @@ def test_profile_full_not_found(tmp_path, monkeypatch): def test_profile_brief(tmp_path, monkeypatch): _seed_ravi(tmp_path, monkeypatch) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) response = client.get(f"{PROFILE_PREFIX}/Ravi/brief") @@ -94,7 +110,7 @@ def test_profile_brief(tmp_path, monkeypatch): def test_profile_cadence(tmp_path, monkeypatch): _seed_ravi(tmp_path, monkeypatch) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) response = client.get(f"{PROFILE_PREFIX}/Ravi/cadence") mentions_response = client.get( @@ -108,7 +124,7 @@ def test_profile_cadence(tmp_path, monkeypatch): def test_profiles_active_collection_envelope(tmp_path, monkeypatch): _seed_ravi(tmp_path, monkeypatch) - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) response = client.get(f"{PROFILES_PREFIX}/active") @@ -123,18 +139,18 @@ def test_profiles_active_collection_envelope(tmp_path, monkeypatch): def test_profiles_active_bad_window_days_returns_invalid_request_value( tmp_path, monkeypatch ): - monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) + _configure_journal(tmp_path, monkeypatch) _minimal_facet_tree(tmp_path) monkeypatch.setattr(profile_surface, "_today_day", lambda: "20260607") - client = make_logged_in_test_client(tmp_path) + client = make_test_client(tmp_path) data = _assert_error(client.get(f"{PROFILES_PREFIX}/active?window_days=abc"), 400) assert data["reason_code"] == "invalid_request_value" -def test_profile_requires_login(tmp_path, monkeypatch): - monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) +def test_profile_redirects_to_init_when_setup_incomplete(tmp_path, monkeypatch): + _configure_unset_journal(tmp_path, monkeypatch) app = create_app(journal=str(tmp_path)) app.config["TESTING"] = True client = app.test_client() @@ -142,3 +158,4 @@ def test_profile_requires_login(tmp_path, monkeypatch): response = client.get(f"{PROFILE_PREFIX}/Ravi") assert response.status_code == 302 + assert "/init" in response.headers["Location"] diff --git a/tests/test_push_routes.py b/tests/test_push_routes.py index e10aca315..69b444ed8 100644 --- a/tests/test_push_routes.py +++ b/tests/test_push_routes.py @@ -132,12 +132,10 @@ def test_register_refuses_none_fingerprint_on_loopback(journal_copy): assert response.get_json()["reason_code"] == "push_request_invalid" -def test_register_refuses_none_fingerprint_on_session_auth(journal_copy): +def test_register_refuses_missing_paired_identity(journal_copy): app = create_app(str(journal_copy)) app.config["TESTING"] = True client = app.test_client() - with client.session_transaction() as session: - session["logged_in"] = True try: response = client.post( "/api/push/register", diff --git a/tests/test_settings_call_parity.py b/tests/test_settings_call_parity.py index 6cea6c927..f5f6c63b3 100644 --- a/tests/test_settings_call_parity.py +++ b/tests/test_settings_call_parity.py @@ -14,7 +14,7 @@ from typer.testing import CliRunner import solstone.apps.settings.call as settings_call import solstone.apps.settings.routes as settings_routes from solstone.think.convey_client import ConveyClient -from tests._baseline_harness import make_logged_in_test_client +from tests._baseline_harness import make_test_client runner = CliRunner() @@ -39,7 +39,7 @@ def _settings_client(journal_copy: Path, monkeypatch: pytest.MonkeyPatch) -> Non for key in API_ENV_KEYS: monkeypatch.delenv(key, raising=False) client = ConveyClient( - session=make_logged_in_test_client(journal_copy), + session=make_test_client(journal_copy), base_url="", ) monkeypatch.setattr(settings_call, "get_client", lambda: client) @@ -307,7 +307,7 @@ def test_identity_and_observer_setters(journal_copy: Path) -> None: } -def test_convey_status_host_url_and_trust_localhost( +def test_convey_status_host_url( journal_copy: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -316,8 +316,6 @@ def test_convey_status_host_url_and_trust_localhost( assert status.stdout == ( "convey\n" " bind: 127.0.0.1:5015\n" - " password: set\n" - " trust localhost: yes\n" " host url: http://localhost:5015\n" ) @@ -364,33 +362,3 @@ def test_convey_status_host_url_and_trust_localhost( "this needs an ip address — to reach home by name from anywhere, " "set up solstone private link\n" ) - - trust_enable = runner.invoke( - settings_call.app, ["convey", "trust-localhost", "enable"] - ) - assert trust_enable.exit_code == 0 - assert ( - trust_enable.stdout - == "localhost trust enabled. localhost requests skip the password.\n" - ) - - trust_disable = runner.invoke( - settings_call.app, ["convey", "trust-localhost", "disable"] - ) - assert trust_disable.exit_code == 0 - assert trust_disable.stdout == ( - "localhost trust disabled. localhost requests now require the password.\n" - ) - - config = _read_config(journal_copy) - config["convey"].pop("password_hash", None) - config["convey"].pop("password", None) - _write_config(journal_copy, config) - trust_refuse = runner.invoke( - settings_call.app, ["convey", "trust-localhost", "disable"] - ) - assert trust_refuse.exit_code == 1 - assert trust_refuse.stderr == ( - "error: disabling localhost trust requires a password (otherwise no " - "client could authenticate). set one first with: journal password set\n" - ) diff --git a/tests/test_settings_cli.py b/tests/test_settings_cli.py index a9ae5c80b..fc346ebfb 100644 --- a/tests/test_settings_cli.py +++ b/tests/test_settings_cli.py @@ -29,7 +29,6 @@ def test_status_json_does_not_require_running_stack( payload = json.loads(captured.out) assert payload == { "effective_host_url": "http://localhost:5015", - "password_configured": True, } assert captured.err == "" diff --git a/tests/test_settings_convey.py b/tests/test_settings_convey.py deleted file mode 100644 index 687ea8d39..000000000 --- a/tests/test_settings_convey.py +++ /dev/null @@ -1,90 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -# Copyright (c) 2026 sol pbc - -from __future__ import annotations - -import json -from pathlib import Path -from unittest.mock import patch - -from solstone.apps.settings.copy import ( - CONVEY_REFUSE_NO_PASSWORD_TRUST, -) -from solstone.convey import create_app - - -def _read_config(journal_dir: Path) -> dict: - return json.loads((journal_dir / "config" / "journal.json").read_text("utf-8")) - - -def _write_config(journal_dir: Path, payload: dict) -> None: - (journal_dir / "config" / "journal.json").write_text( - json.dumps(payload, indent=2) + "\n", - encoding="utf-8", - ) - - -def _clear_password(journal_dir: Path) -> None: - config = _read_config(journal_dir) - config["convey"].pop("password_hash", None) - config["convey"].pop("password", None) - _write_config(journal_dir, config) - - -def _settings_client(journal_dir: Path): - app = create_app(str(journal_dir)) - app.config["TESTING"] = True - return app.test_client() - - -def test_api_get_config_masks_password_without_effective_host_url(journal_copy): - client = _settings_client(journal_copy) - - response = client.get("/app/settings/api/config") - - assert response.status_code == 200 - payload = response.get_json() - assert payload["convey"]["allow_network_access"] is False - assert payload["convey"]["has_password"] is True - assert "password_hash" not in payload["convey"] - assert "pairing" not in payload - - -def test_api_put_corrupt_config_returns_reason_without_writing(journal_copy): - client = _settings_client(journal_copy) - with client.session_transaction() as sess: - sess["logged_in"] = True - config_path = journal_copy / "config" / "journal.json" - config_path.write_bytes(b"{ invalid json }") - before = config_path.read_bytes() - - with patch("solstone.apps.settings.routes.write_journal_config") as write_config: - response = client.put( - "/app/settings/api/config", - json={"section": "identity", "data": {"name": "Changed"}}, - content_type="application/json", - ) - - assert response.status_code == 500 - assert response.get_json()["reason_code"] == "corrupt_config" - write_config.assert_not_called() - assert config_path.read_bytes() == before - - -def test_api_put_trust_localhost_refuses_without_password(journal_copy): - _clear_password(journal_copy) - client = _settings_client(journal_copy) - - response = client.put( - "/app/settings/api/config", - json={"section": "convey", "data": {"trust_localhost": False}}, - content_type="application/json", - ) - - assert response.status_code == 400 - payload = response.get_json() - assert ( - payload["error"] == "I couldn't change localhost trust until a password is set." - ) - assert payload["reason_code"] == "network_security_requires_password" - assert payload["detail"] == CONVEY_REFUSE_NO_PASSWORD_TRUST diff --git a/tests/test_setup.py b/tests/test_setup.py index 8d7f62a81..2d08ec1d9 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -586,7 +586,7 @@ def test_step_journal_materializes_journal_config( assert config["identity"]["name"] == "Setup User" assert config["identity"]["preferred"] == "setup" assert config["identity"]["timezone"] == "America/Denver" - assert config["convey"]["secret"] + assert "convey" not in config assert str(config_path.resolve()) in result.paths diff --git a/tests/test_sol_call_parity.py b/tests/test_sol_call_parity.py index e71a6b553..bc2bff270 100644 --- a/tests/test_sol_call_parity.py +++ b/tests/test_sol_call_parity.py @@ -13,24 +13,25 @@ from typer.testing import CliRunner from solstone.apps.sol.call import app from solstone.think.convey_client import ConveyClient from solstone.think.journal_config import read_journal_config, write_journal_config -from tests._baseline_harness import make_logged_in_test_client +from tests._baseline_harness import make_test_client, mark_setup_complete @pytest.fixture def journal(tmp_path, monkeypatch): monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) + mark_setup_complete(tmp_path) return tmp_path @pytest.fixture def runner(journal, monkeypatch): - client = ConveyClient(session=make_logged_in_test_client(journal), base_url="") + client = ConveyClient(session=make_test_client(journal), base_url="") monkeypatch.setattr("solstone.apps.sol.call.get_client", lambda: client) return CliRunner() def test_set_name_updates_config(runner) -> None: - write_journal_config({}) + write_journal_config({"setup": {"completed_at": 1700000000000}}) result = runner.invoke(app, ["set-name", "aria", "--status", "chosen"]) @@ -45,11 +46,12 @@ def test_set_name_updates_config(runner) -> None: def test_reset_updates_agent(runner) -> None: write_journal_config( { + "setup": {"completed_at": 1700000000000}, "agent": { "name": "aria", "name_status": "chosen", "named_date": "2026-04-19", - } + }, } ) @@ -65,7 +67,7 @@ def test_reset_updates_agent(runner) -> None: def test_set_owner_name_only_and_bio(runner) -> None: - write_journal_config({}) + write_journal_config({"setup": {"completed_at": 1700000000000}}) name_only = runner.invoke(app, ["set-owner", "Jer"]) with_bio = runner.invoke(app, ["set-owner", "Jer", "--bio", "Building solstone"]) diff --git a/tests/test_speakers_call_parity.py b/tests/test_speakers_call_parity.py index 3986925ea..36159d2ee 100644 --- a/tests/test_speakers_call_parity.py +++ b/tests/test_speakers_call_parity.py @@ -17,18 +17,19 @@ from solstone.apps.speakers.call import app from solstone.convey.reasons import SPEAKER_LABELS_BUSY, SPEAKER_VOICEPRINT_BUSY from solstone.think.convey_client import ConveyClient from solstone.think.journal_io import LockTimeout -from tests._baseline_harness import make_logged_in_test_client +from tests._baseline_harness import make_test_client, mark_setup_complete @pytest.fixture def journal(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) + mark_setup_complete(tmp_path) return tmp_path @pytest.fixture def runner(journal: Path, monkeypatch: pytest.MonkeyPatch) -> CliRunner: - client = ConveyClient(session=make_logged_in_test_client(journal), base_url="") + client = ConveyClient(session=make_test_client(journal), base_url="") monkeypatch.setattr(speakers_call, "get_client", lambda: client) return CliRunner() diff --git a/tests/test_surfaces_health.py b/tests/test_surfaces_health.py index af7390ef5..db9273bc3 100644 --- a/tests/test_surfaces_health.py +++ b/tests/test_surfaces_health.py @@ -17,7 +17,7 @@ from solstone.convey.readiness_snapshot import unavailable_snapshot from solstone.think.convey_client import ConveyClient from solstone.think.pipeline_health import SegmentBacklog, SegmentCompletion from solstone.think.surfaces import health as health_surface -from tests._baseline_harness import make_logged_in_test_client +from tests._baseline_harness import make_test_client _RUNNER = CliRunner() _SPEC_POINTER = "cpo/specs/in-flight/consumer-surface-health.md" @@ -26,6 +26,12 @@ _SPEC_POINTER = "cpo/specs/in-flight/consumer-surface-health.md" def _configure_env(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) monkeypatch.setenv("SOL_SKIP_SUPERVISOR_CHECK", "1") + config_dir = tmp_path / "config" + config_dir.mkdir(parents=True, exist_ok=True) + (config_dir / "journal.json").write_text( + json.dumps({"setup": {"completed_at": 1}}), + encoding="utf-8", + ) def _set_now(monkeypatch: pytest.MonkeyPatch, value: datetime) -> None: @@ -278,7 +284,7 @@ def _neutral_readiness_snapshot() -> dict[str, object]: def _patch_health_cli_client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - client = ConveyClient(session=make_logged_in_test_client(tmp_path), base_url="") + client = ConveyClient(session=make_test_client(tmp_path), base_url="") monkeypatch.setattr("solstone.think.tools.health.get_client", lambda: client) diff --git a/tests/test_system_status.py b/tests/test_system_status.py index 13b17be5f..b8e95eb00 100644 --- a/tests/test_system_status.py +++ b/tests/test_system_status.py @@ -30,7 +30,6 @@ def client(tmp_path, monkeypatch): journal = tmp_path (journal / "config").mkdir(parents=True, exist_ok=True) config = { - "convey": {"password_hash": "", "trust_localhost": True}, "setup": {"completed_at": 1}, } (journal / "config" / "journal.json").write_text(json.dumps(config)) diff --git a/tests/test_thinking_call_parity.py b/tests/test_thinking_call_parity.py index bff2d8632..5a2d02f9c 100644 --- a/tests/test_thinking_call_parity.py +++ b/tests/test_thinking_call_parity.py @@ -16,7 +16,7 @@ import solstone.apps.thinking.routes as thinking_routes from solstone.apps.thinking import copy as thinking_copy from solstone.think.convey_client import ConveyClient from solstone.think.services import operations, scout, scout_handoff -from tests._baseline_harness import make_logged_in_test_client +from tests._baseline_harness import make_test_client runner = CliRunner() @@ -41,7 +41,7 @@ def _thinking_client(journal_copy: Path, monkeypatch: pytest.MonkeyPatch) -> Non for key in API_ENV_KEYS: monkeypatch.delenv(key, raising=False) client = ConveyClient( - session=make_logged_in_test_client(journal_copy), + session=make_test_client(journal_copy), base_url="", ) monkeypatch.setattr(thinking_call, "get_client", lambda: client) diff --git a/tests/verify_api.py b/tests/verify_api.py index 0c6e0e690..cb20e4a3b 100644 --- a/tests/verify_api.py +++ b/tests/verify_api.py @@ -22,7 +22,7 @@ try: FROZEN_DATE, FROZEN_TZ_OFFSET, isolated_app_env, - make_logged_in_test_client, + make_test_client, prepare_isolated_journal, ) except ModuleNotFoundError: @@ -30,7 +30,7 @@ except ModuleNotFoundError: FROZEN_DATE, FROZEN_TZ_OFFSET, isolated_app_env, - make_logged_in_test_client, + make_test_client, prepare_isolated_journal, ) @@ -699,7 +699,7 @@ def client_context( journal_path = prepare_isolated_journal(Path(tmpdir) / "journal") with freeze_time(FROZEN_DATE, tz_offset=FROZEN_TZ_OFFSET): with isolated_app_env(journal_path): - yield make_logged_in_test_client(journal_path), str(journal_path) + yield make_test_client(journal_path), str(journal_path) def main(argv: list[str] | None = None) -> int: