From 363a53bc6fffe5e43544d664e2b744a7c8151830 Mon Sep 17 00:00:00 2001 From: Jer Miller Date: Sun, 19 Jul 2026 23:33:17 -0600 Subject: [PATCH] feat(indexer): explain zero-edge full rescans at the command boundary Emit the zero-edge full-rescan hint on stdout from both the Python CLI (solstone/think/indexer/cli.py) and the native solstone-core binary (core/crates/solstone-core/src/main.rs). Suppress the hint by invocation shape: --rebuild-edges, --reset, plain --rescan, or any nonzero edge inserts. Change scan_journal to return a ScanReport dataclass instead of a bare bool, updating every consuming call site directly without a compatibility shim. --- core/crates/solstone-core/src/main.rs | 8 ++ core/crates/solstone-core/tests/indexer.rs | 91 ++++++++++++-- solstone/apps/speakers/tests/test_edges.py | 2 +- solstone/think/indexer/__init__.py | 2 + solstone/think/indexer/cli.py | 18 ++- solstone/think/indexer/journal.py | 17 ++- tests/test_backup_integration.py | 5 +- tests/test_backup_restore.py | 19 +-- tests/test_index_edges.py | 20 +-- tests/test_indexer_cli.py | 134 ++++++++++++++++++++- tests/test_indexer_native_seam.py | 35 ++++++ tests/test_journal_index.py | 16 +-- 12 files changed, 322 insertions(+), 45 deletions(-) diff --git a/core/crates/solstone-core/src/main.rs b/core/crates/solstone-core/src/main.rs index a9fae3b58..89dbddc04 100644 --- a/core/crates/solstone-core/src/main.rs +++ b/core/crates/solstone-core/src/main.rs @@ -19,6 +19,7 @@ use solstone_core_journal::{ const EXIT_USAGE: u8 = 64; const EXIT_UNAVAILABLE: u8 = 69; const EXIT_TEMPFAIL: u8 = 75; +const ZERO_EDGE_HINT: &str = "Zero edges indexed: edges are talent-derived, and the --rescan-full edge phase remains modification-time incremental — run journal indexer --rebuild-edges to force full edge re-extraction."; struct JournalPathLine { label: &'static str, @@ -149,6 +150,13 @@ fn run_indexer(options: IndexerOptions) -> ExitCode { for warning in report.warnings { eprintln!("warning: {warning}"); } + let should_emit_zero_edge_hint = options.rescan_full + && !options.rebuild_edges + && !options.reset + && report.edge_rows_inserted == 0; + if should_emit_zero_edge_hint { + println!("{ZERO_EDGE_HINT}"); + } return ExitCode::SUCCESS; } Err(error) => { diff --git a/core/crates/solstone-core/tests/indexer.rs b/core/crates/solstone-core/tests/indexer.rs index 07fc1cda4..9f738d74c 100644 --- a/core/crates/solstone-core/tests/indexer.rs +++ b/core/crates/solstone-core/tests/indexer.rs @@ -1,12 +1,14 @@ // SPDX-License-Identifier: AGPL-3.0-only // Copyright (c) 2026 sol pbc -use std::process::Command; +use std::process::{Command, Output}; use std::time::{SystemTime, UNIX_EPOCH}; use std::{env, fs, path::Path}; use solstone_core_indexer_store::db::open_index; +const EXPECTED_ZERO_EDGE_HINT: &str = "Zero edges indexed: edges are talent-derived, and the --rescan-full edge phase remains modification-time incremental — run journal indexer --rebuild-edges to force full edge re-extraction."; + fn bin() -> &'static str { env!("CARGO_BIN_EXE_solstone-core") } @@ -26,6 +28,16 @@ fn write(root: &Path, rel: &str, text: &str) { fs::write(path, text).expect("write test file"); } +fn run_indexer(root: &Path, args: &[&str]) -> Output { + Command::new(bin()) + .arg("indexer") + .arg("--journal") + .arg(root) + .args(args) + .output() + .expect("solstone-core should execute") +} + fn seed_edge_entity(root: &Path, entity_id: &str, name: &str) { write( root, @@ -67,15 +79,13 @@ fn indexer_rescan_full_succeeds_for_tiny_journal() { "# Flow\n\nindexed", ); - let output = Command::new(bin()) - .arg("indexer") - .arg("--journal") - .arg(&root) - .arg("--rescan-full") - .output() - .expect("solstone-core should execute"); + let output = run_indexer(&root, &["--rescan-full"]); assert_eq!(output.status.code(), Some(0)); + assert_eq!( + String::from_utf8(output.stdout).expect("stdout should be utf-8"), + format!("{EXPECTED_ZERO_EDGE_HINT}\n") + ); assert_eq!( String::from_utf8(output.stderr).expect("stderr should be utf-8"), "" @@ -84,6 +94,71 @@ fn indexer_rescan_full_succeeds_for_tiny_journal() { fs::remove_dir_all(root).expect("cleanup success root"); } +#[test] +fn indexer_rescan_full_suppresses_zero_edge_hint_for_nonzero_rebuild_and_reset_cases() { + let nonzero_root = temp_path("nonzero-edge-no-hint"); + seed_edge_entity(&nonzero_root, "alice", "Alice Edge"); + seed_edge_entity(&nonzero_root, "bob", "Bob Edge"); + write( + &nonzero_root, + "facets/work/entities/20260717.jsonl", + r#"{"name":"Alice Edge","segments":["s1"]} +{"name":"Bob Edge","segments":["s1"]} +"#, + ); + + let output = run_indexer(&nonzero_root, &["--rescan-full"]); + + assert_eq!(output.status.code(), Some(0)); + assert_eq!( + String::from_utf8(output.stdout).expect("stdout should be utf-8"), + "" + ); + assert_eq!( + String::from_utf8(output.stderr).expect("stderr should be utf-8"), + "" + ); + fs::remove_dir_all(nonzero_root).expect("cleanup nonzero root"); + + let rebuild_root = temp_path("rebuild-suppression"); + write( + &rebuild_root, + "chronicle/20260717/talents/flow.md", + "# Flow\n\nindexed", + ); + let output = run_indexer(&rebuild_root, &["--rebuild-edges", "--rescan-full"]); + + assert_eq!(output.status.code(), Some(0)); + assert_eq!( + String::from_utf8(output.stdout).expect("stdout should be utf-8"), + "" + ); + assert_eq!( + String::from_utf8(output.stderr).expect("stderr should be utf-8"), + "" + ); + fs::remove_dir_all(rebuild_root).expect("cleanup rebuild root"); + + let reset_root = temp_path("reset-suppression"); + write( + &reset_root, + "chronicle/20260717/talents/flow.md", + "# Flow\n\nindexed", + ); + let output = run_indexer(&reset_root, &["--reset", "--rescan-full"]); + + assert_eq!(output.status.code(), Some(0)); + assert_eq!( + String::from_utf8(output.stdout).expect("stdout should be utf-8"), + "" + ); + assert_eq!( + String::from_utf8(output.stderr).expect("stderr should be utf-8"), + "" + ); + fs::remove_dir_all(reset_root).expect("cleanup reset root"); +} + #[test] fn indexer_scan_edge_failure_warns_and_exits_zero() { let root = temp_path("scan-edge-failure"); diff --git a/solstone/apps/speakers/tests/test_edges.py b/solstone/apps/speakers/tests/test_edges.py index 0895b2017..d90c8b2ab 100644 --- a/solstone/apps/speakers/tests/test_edges.py +++ b/solstone/apps/speakers/tests/test_edges.py @@ -467,7 +467,7 @@ def test_ac10_scan_journal_full_is_content_hash_idempotent(edge_journal): ) _write_transcript(edge_journal, ["Cora Candidate is named.", "No mention here."]) - assert scan_journal(str(edge_journal), full=True) is True + assert scan_journal(str(edge_journal), full=True).changed is True conn, _ = get_journal_index(str(edge_journal)) first_hash = edges_content_hash(conn) conn.close() diff --git a/solstone/think/indexer/__init__.py b/solstone/think/indexer/__init__.py index f8d27b2f5..1c61de672 100644 --- a/solstone/think/indexer/__init__.py +++ b/solstone/think/indexer/__init__.py @@ -11,6 +11,7 @@ from .cli import main # Import from journal (unified index) from .journal import ( + ScanReport, get_journal_index, index_file, reset_journal_index, @@ -24,6 +25,7 @@ from .journal import ( # All public functions and constants __all__ = [ # Journal (unified index) + "ScanReport", "get_journal_index", "index_file", "reset_journal_index", diff --git a/solstone/think/indexer/cli.py b/solstone/think/indexer/cli.py index 6dc9884ed..ec5df57a5 100644 --- a/solstone/think/indexer/cli.py +++ b/solstone/think/indexer/cli.py @@ -21,6 +21,12 @@ from .native_seam import maybe_run_native_indexer logger = logging.getLogger(__name__) +ZERO_EDGE_HINT = ( + "Zero edges indexed: edges are talent-derived, and the --rescan-full edge phase " + "remains modification-time incremental — run journal indexer --rebuild-edges to " + "force full edge re-extraction." +) + def _format_count_column( items: list[tuple[str, int]], total: int, top_n: int @@ -207,9 +213,17 @@ def main() -> int | None: except (ValueError, FileNotFoundError) as e: parser.error(str(e)) elif args.rescan or args.rescan_full: - changed = scan_journal(journal, verbose=args.verbose, full=args.rescan_full) - if changed: + report = scan_journal(journal, verbose=args.verbose, full=args.rescan_full) + if report.changed: logger.info("indexer journal rescan ok") + should_emit_zero_edge_hint = ( + args.rescan_full + and not args.rebuild_edges + and not args.reset + and report.edge_rows_inserted == 0 + ) + if should_emit_zero_edge_hint: + print(ZERO_EDGE_HINT) if args.query is not None: query_kwargs: dict[str, Any] = {} diff --git a/solstone/think/indexer/journal.py b/solstone/think/indexer/journal.py index 5240b511e..d4b9765c9 100644 --- a/solstone/think/indexer/journal.py +++ b/solstone/think/indexer/journal.py @@ -20,6 +20,7 @@ import os import re import sqlite3 import time +from dataclasses import dataclass from datetime import date, datetime, timedelta from pathlib import Path from typing import Any, Iterable @@ -63,6 +64,13 @@ DB_NAME = "journal.sqlite" ENTITY_SEARCH_WATERMARK_MTIME_PATH = "entity_search:__mtime__" ENTITY_SEARCH_WATERMARK_COUNT_PATH = "entity_search:__count__" + +@dataclass(frozen=True) +class ScanReport: + changed: bool + edge_rows_inserted: int + + # Schema for the unified journal index SCHEMA = [ "CREATE TABLE IF NOT EXISTS files(path TEXT PRIMARY KEY, mtime INTEGER)", @@ -599,7 +607,7 @@ def _index_entity_search_chunks(conn: sqlite3.Connection) -> int: return count -def scan_journal(journal: str, verbose: bool = False, full: bool = False) -> bool: +def scan_journal(journal: str, verbose: bool = False, full: bool = False) -> ScanReport: """Scan and index journal content. Args: @@ -609,7 +617,7 @@ def scan_journal(journal: str, verbose: bool = False, full: bool = False) -> boo YYYYMMDD directories (before today) for lighter incremental scans. Returns: - True if any files were indexed or removed + Report with whether any rows changed and how many edge rows were inserted. """ conn, db_path = get_journal_index(journal) journal_path = Path(journal) @@ -805,7 +813,10 @@ def scan_journal(journal: str, verbose: bool = False, full: bool = False) -> boo ) conn.close() - return bool(to_index or removed or entity_changed or edge_changed) + return ScanReport( + changed=bool(to_index or removed or entity_changed or edge_changed), + edge_rows_inserted=edge_rows_inserted, + ) # Compiled patterns for temporal extraction (checked against unquoted text only) diff --git a/tests/test_backup_integration.py b/tests/test_backup_integration.py index 3c8850d0b..84f776ece 100644 --- a/tests/test_backup_integration.py +++ b/tests/test_backup_integration.py @@ -30,6 +30,7 @@ from solstone.think.backup.readiness import ( from solstone.think.backup.repo import init_repository from solstone.think.backup.runner import run_restic from solstone.think.backup.state import BACKUP_DEFAULTS, get_destination +from solstone.think.indexer.journal import ScanReport from tests.helpers.journal_config import seed_journal_config RESTIC_BIN = shutil.which("restic") @@ -238,9 +239,9 @@ def test_backup_restore_rotation_teardown_real_local_round_trip( scan_calls: list[tuple[str, dict[str, Any]]] = [] - def fake_scan_journal(journal: str, **kwargs: Any) -> bool: + def fake_scan_journal(journal: str, **kwargs: Any) -> ScanReport: scan_calls.append((journal, kwargs)) - return True + return ScanReport(changed=True, edge_rows_inserted=0) monkeypatch.setattr(restore, "scan_journal", fake_scan_journal) monkeypatch.setenv("SOLSTONE_JOURNAL", str(restored_journal)) diff --git a/tests/test_backup_restore.py b/tests/test_backup_restore.py index d6ef42874..d699a1d54 100644 --- a/tests/test_backup_restore.py +++ b/tests/test_backup_restore.py @@ -14,6 +14,7 @@ from solstone.think.backup import restore from solstone.think.backup.destination import Destination from solstone.think.backup.hosted import HostedBinding, HostedCredentials from solstone.think.backup.runner import ResticResult +from solstone.think.indexer.journal import ScanReport def _config_path(journal: Path) -> Path: @@ -132,11 +133,11 @@ def test_restore_success_normalizes_key_assembles_env_and_reindexes( order.append("set_recovery_key_confirmed") assert value is True - def fake_scan_journal(journal: str, **kwargs: Any) -> bool: + def fake_scan_journal(journal: str, **kwargs: Any) -> ScanReport: order.append("scan_journal") assert journal == str(tmp_path) assert kwargs == {"full": True} - return True + return ScanReport(changed=True, edge_rows_inserted=0) monkeypatch.setattr(restore, "ensure_restic", lambda: Path("/restic")) monkeypatch.setattr(restore, "run_restic", fake_run_restic) @@ -370,11 +371,11 @@ def test_restore_check_failure_reports_degraded_and_keeps_side_effects( order.append("set_recovery_key_confirmed") assert value is True - def fake_scan_journal(journal: str, **kwargs: Any) -> bool: + def fake_scan_journal(journal: str, **kwargs: Any) -> ScanReport: order.append("scan_journal") assert journal == str(tmp_path) assert kwargs == {"full": True} - return True + return ScanReport(changed=True, edge_rows_inserted=0) monkeypatch.setattr(restore, "ensure_restic", lambda: Path("/restic")) monkeypatch.setattr(restore, "run_restic", fake_run_restic) @@ -429,7 +430,11 @@ def test_restore_missing_daily_key_is_not_resumable( monkeypatch.setattr(restore, "set_recovery_key", lambda key: None) monkeypatch.setattr(restore, "set_recovery_key_confirmed", lambda confirmed: None) monkeypatch.setattr(restore, "get_backup_config", lambda: {"daily_key": None}) - monkeypatch.setattr(restore, "scan_journal", lambda journal, **kwargs: True) + monkeypatch.setattr( + restore, + "scan_journal", + lambda journal, **kwargs: ScanReport(changed=True, edge_rows_inserted=0), + ) result = restore.restore_journal(_destination(), "A" * 64) @@ -481,11 +486,11 @@ def test_restore_operated_success_persists_mode_and_key_without_destination( assert value is True real_set_recovery_key_confirmed(value) - def fake_scan_journal(journal: str, **kwargs: Any) -> bool: + def fake_scan_journal(journal: str, **kwargs: Any) -> ScanReport: order.append("scan_journal") assert journal == str(tmp_path) assert kwargs == {"full": True} - return True + return ScanReport(changed=True, edge_rows_inserted=0) monkeypatch.setattr(restore, "ensure_restic", lambda: Path("/restic")) monkeypatch.setattr(restore, "ensure_rclone", lambda: Path("/rclone")) diff --git a/tests/test_index_edges.py b/tests/test_index_edges.py index 796adb0f4..ec96ec3db 100644 --- a/tests/test_index_edges.py +++ b/tests/test_index_edges.py @@ -383,7 +383,7 @@ def test_discover_edge_files_keeps_structural_sources_with_chronicle_root( def test_scan_indexes_edges_and_second_scan_is_zero_delta(edges_journal): - assert scan_journal(str(edges_journal), full=True) is True + assert scan_journal(str(edges_journal), full=True).changed is True conn = _conn(edges_journal) schema_version = conn.execute( @@ -477,7 +477,7 @@ def test_scan_indexes_edges_and_second_scan_is_zero_delta(edges_journal): first_hash = edges_content_hash(conn) conn.close() - assert scan_journal(str(edges_journal), full=True) is False + assert scan_journal(str(edges_journal), full=True).changed is False conn = _conn(edges_journal) assert edges_content_hash(conn) == first_hash conn.close() @@ -495,7 +495,7 @@ def test_touching_one_edge_file_replaces_only_that_path(edges_journal): _write_jsonl(path, records) _bump_mtime(path) - assert scan_journal(str(edges_journal), full=True) is True + assert scan_journal(str(edges_journal), full=True).changed is True conn = _conn(edges_journal) after = _source_counts(conn) assert after["participation"] == 6 @@ -514,7 +514,7 @@ def test_deleted_edge_source_removes_rows_and_ledger(edges_journal): path = edges_journal / "facets" / "edges-events" / "events" / "20260430.jsonl" path.unlink() - assert scan_journal(str(edges_journal), full=True) is True + assert scan_journal(str(edges_journal), full=True).changed is True conn = _conn(edges_journal) assert ( conn.execute( @@ -640,7 +640,7 @@ def test_pretty_documents_json_edges_via_index_file_and_scan_journal(edges_journ ) conn.close() - assert scan_journal(str(edges_journal), full=True) is True + assert scan_journal(str(edges_journal), full=True).changed is True conn = _conn(edges_journal) assert ( conn.execute( @@ -806,7 +806,7 @@ def test_new_source_shapes_support_index_file_replacement_and_deletion( _replace_new_source_fixture(shape, path) _bump_mtime(path) - assert scan_journal(str(edges_journal), full=True) is True + assert scan_journal(str(edges_journal), full=True).changed is True conn = _conn(edges_journal) assert ( conn.execute("SELECT count(*) FROM edges WHERE path=?", (rel,)).fetchone()[0] @@ -815,7 +815,7 @@ def test_new_source_shapes_support_index_file_replacement_and_deletion( conn.close() path.unlink() - assert scan_journal(str(edges_journal), full=True) is True + assert scan_journal(str(edges_journal), full=True).changed is True conn = _conn(edges_journal) assert ( conn.execute("SELECT count(*) FROM edges WHERE path=?", (rel,)).fetchone()[0] @@ -845,7 +845,7 @@ def test_malformed_new_source_fails_without_suppressing_sibling( caplog.clear() caplog.set_level(logging.ERROR, logger="solstone.think.indexer.edges") - assert scan_journal(str(edges_journal), full=True) is True + assert scan_journal(str(edges_journal), full=True).changed is True assert f"Skipping edge extraction for {bad_rel}" in caplog.text conn = _conn(edges_journal) @@ -1012,7 +1012,7 @@ def test_non_date_activity_edge_source_fails_without_poisoning_sibling( assert f"Skipping edge extraction for {bad_rel}" in caplog.text caplog.clear() - assert scan_journal(str(edges_journal), full=True) is True + assert scan_journal(str(edges_journal), full=True).changed is True conn = _conn(edges_journal) assert ( @@ -1200,7 +1200,7 @@ def test_schema_version_migration_preserves_chunks_and_files( assert table_content_hash(conn, "files", FILE_COLUMNS) == files_hash conn.close() - assert scan_journal(str(edges_journal), full=True) is True + assert scan_journal(str(edges_journal), full=True).changed is True conn = _conn(edges_journal) assert conn.execute("SELECT count(*) FROM edges").fetchone()[0] == 24 conn.close() diff --git a/tests/test_indexer_cli.py b/tests/test_indexer_cli.py index b776fa4d1..15a2d25de 100644 --- a/tests/test_indexer_cli.py +++ b/tests/test_indexer_cli.py @@ -12,6 +12,13 @@ import pytest import solstone.think.utils as think_utils from solstone.think.indexer import cli as indexer_cli +from solstone.think.indexer.journal import ScanReport + +EXPECTED_ZERO_EDGE_HINT = ( + "Zero edges indexed: edges are talent-derived, and the --rescan-full edge phase " + "remains modification-time incremental — run journal indexer --rebuild-edges to " + "force full edge re-extraction." +) def test_module_entrypoint_propagates_main_return(monkeypatch): @@ -39,6 +46,120 @@ def _run_indexer_cli(monkeypatch, journal: Path, args: list[str]) -> None: indexer_cli.main() +def test_rescan_full_zero_edge_rows_prints_hint(tmp_path, monkeypatch, capsys): + journal = tmp_path / "journal" + journal.mkdir() + scans: list[tuple[str, bool, bool]] = [] + + def scan_journal( + journal_arg: str, + *, + verbose: bool = False, + full: bool = False, + ) -> ScanReport: + scans.append((journal_arg, verbose, full)) + return ScanReport(changed=True, edge_rows_inserted=0) + + monkeypatch.setattr(indexer_cli, "scan_journal", scan_journal) + + _run_indexer_cli(monkeypatch, journal, ["--rescan-full"]) + + captured = capsys.readouterr() + assert scans == [(str(journal), False, True)] + assert captured.out == EXPECTED_ZERO_EDGE_HINT + "\n" + assert captured.err == "" + + +def test_rescan_full_nonzero_edge_rows_suppresses_hint(tmp_path, monkeypatch, capsys): + journal = tmp_path / "journal" + journal.mkdir() + + def scan_journal( + _journal_arg: str, + *, + verbose: bool = False, + full: bool = False, + ) -> ScanReport: + assert verbose is False + assert full is True + return ScanReport(changed=True, edge_rows_inserted=1) + + monkeypatch.setattr(indexer_cli, "scan_journal", scan_journal) + + _run_indexer_cli(monkeypatch, journal, ["--rescan-full"]) + + captured = capsys.readouterr() + assert captured.out == "" + assert captured.err == "" + + +def test_rebuild_edges_rescan_full_suppresses_zero_edge_hint( + tmp_path, + monkeypatch, + capsys, +): + journal = tmp_path / "journal" + journal.mkdir() + calls: list[tuple[str, str] | tuple[str, str, bool, bool]] = [] + + def rebuild_edges(journal_arg: str) -> dict[str, int]: + calls.append(("rebuild_edges", journal_arg)) + return {"files": 0, "rows": 0, "drops": 0, "failed": 0} + + def scan_journal( + journal_arg: str, + *, + verbose: bool = False, + full: bool = False, + ) -> ScanReport: + calls.append(("scan", journal_arg, verbose, full)) + return ScanReport(changed=True, edge_rows_inserted=0) + + monkeypatch.setattr(indexer_cli, "rebuild_edges", rebuild_edges) + monkeypatch.setattr(indexer_cli, "scan_journal", scan_journal) + + _run_indexer_cli(monkeypatch, journal, ["--rebuild-edges", "--rescan-full"]) + + captured = capsys.readouterr() + assert calls == [ + ("rebuild_edges", str(journal)), + ("scan", str(journal), False, True), + ] + assert captured.out == "" + assert captured.err == "" + + +def test_reset_rescan_full_suppresses_zero_edge_hint(tmp_path, monkeypatch, capsys): + journal = tmp_path / "journal" + journal.mkdir() + calls: list[tuple[str, str] | tuple[str, str, bool, bool]] = [] + + def reset_journal_index(journal_arg: str) -> None: + calls.append(("reset", journal_arg)) + + def scan_journal( + journal_arg: str, + *, + verbose: bool = False, + full: bool = False, + ) -> ScanReport: + calls.append(("scan", journal_arg, verbose, full)) + return ScanReport(changed=True, edge_rows_inserted=0) + + monkeypatch.setattr(indexer_cli, "reset_journal_index", reset_journal_index) + monkeypatch.setattr(indexer_cli, "scan_journal", scan_journal) + + _run_indexer_cli(monkeypatch, journal, ["--reset", "--rescan-full"]) + + captured = capsys.readouterr() + assert calls == [ + ("reset", str(journal)), + ("scan", str(journal), False, True), + ] + assert captured.out == "" + assert captured.err == "" + + def test_rescan_file_does_not_create_root_task_log(tmp_path, monkeypatch): journal = tmp_path / "journal" journal.mkdir() @@ -151,9 +272,9 @@ def test_native_decline_fallback_runs_python_write_blocks(tmp_path, monkeypatch) *, verbose: bool = False, full: bool = False, - ) -> bool: + ) -> ScanReport: calls.append(("scan", journal_arg, verbose, full)) - return True + return ScanReport(changed=True, edge_rows_inserted=0) monkeypatch.setattr( indexer_cli, "maybe_run_native_indexer", fallback_after_native_decline @@ -196,9 +317,14 @@ def test_rescan_leaves_existing_root_task_log_unchanged( root_log.write_bytes(original) scans: list[tuple[str, bool, bool]] = [] - def scan_journal(journal_arg: str, *, verbose: bool = False, full: bool = False): + def scan_journal( + journal_arg: str, + *, + verbose: bool = False, + full: bool = False, + ) -> ScanReport: scans.append((journal_arg, verbose, full)) - return True + return ScanReport(changed=True, edge_rows_inserted=1) monkeypatch.setattr(indexer_cli, "scan_journal", scan_journal) diff --git a/tests/test_indexer_native_seam.py b/tests/test_indexer_native_seam.py index ac1cee20c..29960d35b 100644 --- a/tests/test_indexer_native_seam.py +++ b/tests/test_indexer_native_seam.py @@ -192,6 +192,41 @@ def test_rust_prefers_rescan_full_when_both_scan_flags_are_set() -> None: ] +@pytest.mark.parametrize( + ("overrides", "expected_flags"), + [ + ({"rescan_full": True}, ["--rescan-full"]), + ({"rescan": True}, ["--rescan"]), + ( + {"rebuild_edges": True, "rescan_full": True}, + ["--rebuild-edges", "--rescan-full"], + ), + ({"reset": True, "rescan_full": True}, ["--reset", "--rescan-full"]), + ], +) +def test_rust_zero_edge_hint_suppression_options_are_forwarded_in_native_argv( + overrides: dict[str, Any], + expected_flags: list[str], +) -> None: + # The seam inherits native stdio, so direct Rust tests assert hint output. + result, python_calls, native_argvs = _route( + _args(**overrides), + config={"core": {"indexer": "rust"}}, + ) + + assert result == 0 + assert python_calls == [] + assert native_argvs == [ + [ + "/tmp/bin/solstone-core", + "indexer", + "--journal", + "/tmp/journal", + *expected_flags, + ] + ] + + def test_rust_rescan_file_normalizes_chronicle_prefixed_relative_to_absolute( tmp_path: Path, ) -> None: diff --git a/tests/test_journal_index.py b/tests/test_journal_index.py index 154f9bec7..70c9b5c88 100644 --- a/tests/test_journal_index.py +++ b/tests/test_journal_index.py @@ -460,7 +460,7 @@ def test_scan_journal(journal_fixture): """Test scanning journal creates index.""" from solstone.think.indexer.journal import scan_journal - changed = scan_journal(str(journal_fixture), verbose=True) + changed = scan_journal(str(journal_fixture), verbose=True).changed assert changed is True # Index file should exist @@ -786,11 +786,11 @@ def test_index_caching(journal_fixture): from solstone.think.indexer.journal import scan_journal # First scan indexes files - changed = scan_journal(str(journal_fixture)) + changed = scan_journal(str(journal_fixture)).changed assert changed is True # Second scan should be a no-op (all cached) - changed = scan_journal(str(journal_fixture)) + changed = scan_journal(str(journal_fixture)).changed assert changed is False @@ -819,7 +819,7 @@ def test_scan_journal_full_mode(journal_fixture): from solstone.think.indexer.journal import scan_journal, search_journal # Full scan should include everything - changed = scan_journal(str(journal_fixture), full=True) + changed = scan_journal(str(journal_fixture), full=True).changed assert changed is True # Should find content from historical day @@ -1437,7 +1437,7 @@ def test_light_scan_removes_deleted_facet_content(journal_fixture): events_file.unlink() # Light rescan should detect the deletion (facet content is in scope) - changed = scan_journal(str(journal_fixture), full=False) + changed = scan_journal(str(journal_fixture), full=False).changed assert changed is True # Event should no longer be searchable @@ -1474,7 +1474,7 @@ def test_light_scan_removes_deleted_today_segment(tmp_path, monkeypatch): output_file.unlink() # Light rescan should detect the deletion - changed = scan_journal(str(journal), full=False) + changed = scan_journal(str(journal), full=False).changed assert changed is True # Content should no longer be searchable @@ -1508,7 +1508,7 @@ def test_light_scan_preserves_historical_content(tmp_path, monkeypatch): output_file.unlink() # Light rescan should NOT remove the historical content (out of scope) - changed = scan_journal(str(journal), full=False) + changed = scan_journal(str(journal), full=False).changed # No changes because the historical path is out of scope assert changed is False @@ -1543,7 +1543,7 @@ def test_full_scan_removes_historical_content(tmp_path, monkeypatch): output_file.unlink() # Full rescan SHOULD remove the historical content - changed = scan_journal(str(journal), full=True) + changed = scan_journal(str(journal), full=True).changed assert changed is True # Content should no longer be searchable -- 2.51.2