diff --git a/Makefile b/Makefile index 10cfcff46..e6bbadb7f 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # solstone Makefile # Python-based AI-driven desktop journaling toolkit -.PHONY: install uninstall test test-apps test-app test-only test-integration test-integration-only test-all format ci clean clean-install coverage watch versions update update-prices pre-commit skills dev all sail sandbox sandbox-stop install-pinchtab verify-browser update-browser-baselines review verify-api update-api-baselines install-service uninstall-service +.PHONY: install uninstall test test-apps test-app test-only test-integration test-integration-only test-all format format-check ci clean clean-install coverage watch versions update update-prices pre-commit skills dev all sail sandbox sandbox-stop install-pinchtab verify-browser update-browser-baselines review verify-api update-api-baselines install-service uninstall-service # Default target - install package in editable mode all: install @@ -291,8 +291,12 @@ PYTEST := $(VENV_BIN)/pytest RUFF := $(VENV_BIN)/ruff MYPY := $(VENV_BIN)/mypy +# Check formatting without modifying files — gates `make test` +format-check: .installed + @$(RUFF) format --check . || { echo "Run 'make format' to fix formatting"; exit 1; } + # Run core tests (excluding integration and app tests) -test: .installed +test: .installed format-check @echo "Running core tests..." $(TEST_ENV) $(PYTEST) tests/ -q --cov=. --ignore=tests/integration diff --git a/apps/entities/call.py b/apps/entities/call.py index 50445486e..460edfa18 100644 --- a/apps/entities/call.py +++ b/apps/entities/call.py @@ -154,7 +154,7 @@ def move_entity( src_obs = load_observations(from_facet, entity_name) dst_obs = load_observations(to_facet, entity_name) - + existing_keys = {(o["content"], o.get("observed_at")) for o in dst_obs} merged = list(dst_obs) + [ o @@ -386,7 +386,9 @@ def add_aka( return # Validate uniqueness across all entities in facet - entities = load_entities(facet, day=None, include_detached=True, include_blocked=True) + entities = load_entities( + facet, day=None, include_detached=True, include_blocked=True + ) conflict = validate_aka_uniqueness( aka_value, entities, exclude_entity_name=resolved_name diff --git a/apps/entities/talent/entity_observer.py b/apps/entities/talent/entity_observer.py index c6c4ac38b..681a63fd9 100644 --- a/apps/entities/talent/entity_observer.py +++ b/apps/entities/talent/entity_observer.py @@ -51,7 +51,9 @@ def post_process(result: str, context: dict) -> str | None: if not isinstance(observations, dict) or not observations: return None - valid_entity_ids = {entity.get("id") for entity in load_entities(facet) if entity.get("id")} + valid_entity_ids = { + entity.get("id") for entity in load_entities(facet) if entity.get("id") + } for entity_id, items in observations.items(): if entity_id not in valid_entity_ids: @@ -72,7 +74,9 @@ def post_process(result: str, context: dict) -> str | None: if not content: continue if content.lower() in existing: - logger.debug("Skipping duplicate observation for %s: %s", entity_id, content[:60]) + logger.debug( + "Skipping duplicate observation for %s: %s", entity_id, content[:60] + ) continue add_observation(facet, entity_id, content, day) existing.add(content.lower()) diff --git a/apps/graph/routes.py b/apps/graph/routes.py index 5d8d88052..0cdc8b5bc 100644 --- a/apps/graph/routes.py +++ b/apps/graph/routes.py @@ -112,9 +112,7 @@ def api_graph(): # Map edge entity names to node IDs node_ids = {n["id"] for n in nodes} - name_to_id = _build_name_to_node_id( - conn, node_ids, since=since, facet=facet - ) + name_to_id = _build_name_to_node_id(conn, node_ids, since=since, facet=facet) edges = [] for e in explicit_edges + co_occurrence_edges: from_id = name_to_id.get(e["from_name"], e["from_name"]) diff --git a/apps/home/routes.py b/apps/home/routes.py index 208fa7532..a4ac79fde 100644 --- a/apps/home/routes.py +++ b/apps/home/routes.py @@ -556,7 +556,9 @@ def _collect_skills() -> list[dict[str, Any]]: summary = meta.get("activity_type", "") typical_time = meta.get("typical_time", "") if typical_time: - summary = f"{summary} · {typical_time}" if summary else typical_time + summary = ( + f"{summary} · {typical_time}" if summary else typical_time + ) observations = meta.get("observations", 0) last_seen_str = meta.get("last_seen", "") diff --git a/apps/import/call.py b/apps/import/call.py index 7e20104cd..cb17206bc 100644 --- a/apps/import/call.py +++ b/apps/import/call.py @@ -92,7 +92,9 @@ def _write_config(config: dict) -> None: os.chmod(config_path, 0o600) -def merge_entity_fields(target: EntityDict, source: EntityDict) -> tuple[EntityDict, list[str]]: +def merge_entity_fields( + target: EntityDict, source: EntityDict +) -> tuple[EntityDict, list[str]]: merged: EntityDict = dict(target) pre_merge_snapshot = dict(merged) @@ -204,7 +206,9 @@ def _parse_jsonl_text(source_data: str) -> list[dict[str, Any]]: except json.JSONDecodeError as exc: raise ValueError(f"Invalid JSONL at line {line_number}: {exc.msg}") from exc if not isinstance(item, dict): - raise ValueError(f"Invalid JSONL at line {line_number}: item must be an object") + raise ValueError( + f"Invalid JSONL at line {line_number}: item must be an object" + ) items.append(item) return items @@ -283,7 +287,9 @@ def _resolve_config_field(state_dir: Path, field: str, action: str) -> None: @app.command("list-staged") def list_staged( source: str = typer.Option(..., "--source", help="Import source name."), - area: str | None = typer.Option(None, "--area", help="Area: entities, facets, or config."), + area: str | None = typer.Option( + None, "--area", help="Area: entities, facets, or config." + ), ) -> None: _, _, state_dir = _resolve_source(source) @@ -337,7 +343,9 @@ def resolve_entity( source_id: str = typer.Argument(help="Source entity ID."), action: str = typer.Argument(help="Action: merge, create, or skip."), source: str = typer.Option(..., "--source", help="Import source name."), - target: str | None = typer.Option(None, "--target", help="Target entity ID for merge."), + target: str | None = typer.Option( + None, "--target", help="Target entity ID for merge." + ), ) -> None: _, _, state_dir = _resolve_source(source) @@ -408,7 +416,9 @@ def resolve_entity( if reason == "id_collision" or journal_entity_path(final_id).exists(): allocated = _allocate_slug(str(created_entity.get("name", ""))) if allocated is None: - _fail(f"Unable to allocate a slug for '{created_entity.get('name', '')}'.") + _fail( + f"Unable to allocate a slug for '{created_entity.get('name', '')}'." + ) final_id = allocated created_entity["id"] = final_id @@ -450,7 +460,9 @@ def resolve_entity( @app.command("resolve-facet") def resolve_facet( - staged_file: str = typer.Argument(help="Staged file path relative to facets/staged/."), + staged_file: str = typer.Argument( + help="Staged file path relative to facets/staged/." + ), action: str = typer.Argument(help="Action: apply or skip."), source: str = typer.Option(..., "--source", help="Import source name."), ) -> None: @@ -505,7 +517,9 @@ def resolve_facet( id_map = entities_state.get("id_map", {}) source_entity_id = str(payload.get("source_entity_id", "")) if source_entity_id not in id_map: - _fail(f"Entity {source_entity_id} has no mapping yet. Run entity review first.") + _fail( + f"Entity {source_entity_id} has no mapping yet. Run entity review first." + ) source_path = str(payload.get("source_path", "")) source_data = str(payload.get("source_data", "")) @@ -544,9 +558,15 @@ def resolve_facet( merged_observations.append(item) save_observations(facet_name, entity_id, merged_observations) elif file_type in {"detected_entities", "activity_records"}: - existing_items = _parse_jsonl_text(target_path.read_text(encoding="utf-8")) if target_path.exists() else [] + existing_items = ( + _parse_jsonl_text(target_path.read_text(encoding="utf-8")) + if target_path.exists() + else [] + ) existing_ids = {item.get("id") for item in existing_items} - new_items = [item for item in remapped_data if item.get("id") not in existing_ids] + new_items = [ + item for item in remapped_data if item.get("id") not in existing_ids + ] _append_jsonl_items(target_path, new_items) else: _fail(f"Unsupported staged facet file type '{file_type}'.") @@ -569,7 +589,8 @@ def resolve_facet( target_path = Path(get_journal()) / "facets" / facet_name / "facet.json" target_path.parent.mkdir(parents=True, exist_ok=True) target_path.write_text( - json.dumps(payload.get("source_content"), indent=2, ensure_ascii=False) + "\n", + json.dumps(payload.get("source_content"), indent=2, ensure_ascii=False) + + "\n", encoding="utf-8", ) staged_path.unlink() @@ -603,7 +624,9 @@ def resolve_config( @app.command("resolve-config-all") def resolve_config_all( source: str = typer.Option(..., "--source", help="Import source name."), - category: str = typer.Option(..., "--category", help="Category: transferable or preference."), + category: str = typer.Option( + ..., "--category", help="Category: transferable or preference." + ), ) -> None: _, _, state_dir = _resolve_source(source) diff --git a/apps/import/facet_ingest.py b/apps/import/facet_ingest.py index 335bd5c38..778c568a2 100644 --- a/apps/import/facet_ingest.py +++ b/apps/import/facet_ingest.py @@ -74,7 +74,9 @@ def _parse_path(path_str: str, file_type: str) -> tuple[PurePosixPath, dict[str, if file_type == "entity_relationship": if len(parts) != 3 or parts[0] != "entities" or parts[2] != "entity.json": - raise ValueError("entity_relationship path must be entities//entity.json") + raise ValueError( + "entity_relationship path must be entities//entity.json" + ) return path, {"entity_id": parts[1]} if file_type == "entity_observations": @@ -89,7 +91,11 @@ def _parse_path(path_str: str, file_type: str) -> tuple[PurePosixPath, dict[str, return path, {"entity_id": parts[1]} if file_type == "detected_entities": - if len(parts) != 2 or parts[0] != "entities" or not _DAY_JSONL_RE.match(parts[1]): + if ( + len(parts) != 2 + or parts[0] != "entities" + or not _DAY_JSONL_RE.match(parts[1]) + ): raise ValueError("detected_entities path must be entities/YYYYMMDD.jsonl") return path, {"day_file": parts[1]} @@ -99,12 +105,20 @@ def _parse_path(path_str: str, file_type: str) -> tuple[PurePosixPath, dict[str, return path, {} if file_type == "activity_records": - if len(parts) != 2 or parts[0] != "activities" or not _DAY_JSONL_RE.match(parts[1]): + if ( + len(parts) != 2 + or parts[0] != "activities" + or not _DAY_JSONL_RE.match(parts[1]) + ): raise ValueError("activity_records path must be activities/YYYYMMDD.jsonl") return path, {"day_file": parts[1]} if file_type == "activity_output": - if len(parts) < 4 or parts[0] != "activities" or not re.match(r"^\d{8}$", parts[1]): + if ( + len(parts) < 4 + or parts[0] != "activities" + or not re.match(r"^\d{8}$", parts[1]) + ): raise ValueError( "activity_output path must be activities/YYYYMMDD//..." ) @@ -116,7 +130,11 @@ def _parse_path(path_str: str, file_type: str) -> tuple[PurePosixPath, dict[str, return path, {"day_file": parts[1]} if file_type == "calendar": - if len(parts) != 2 or parts[0] != "calendar" or not _DAY_JSONL_RE.match(parts[1]): + if ( + len(parts) != 2 + or parts[0] != "calendar" + or not _DAY_JSONL_RE.match(parts[1]) + ): raise ValueError("calendar path must be calendar/YYYYMMDD.jsonl") return path, {"day_file": parts[1]} @@ -152,7 +170,9 @@ def _parse_jsonl_bytes(raw_bytes: bytes) -> list[dict[str, Any]]: except json.JSONDecodeError as exc: raise ValueError(f"Invalid JSONL at line {line_number}: {exc.msg}") from exc if not isinstance(value, dict): - raise ValueError(f"Invalid JSONL at line {line_number}: item must be an object") + raise ValueError( + f"Invalid JSONL at line {line_number}: item must be an object" + ) items.append(value) return items @@ -166,7 +186,11 @@ def _check_unmapped_entities( unmapped: list[str] = [] def add(entity_id: str) -> None: - if entity_id and _remap_entity_id(entity_id, id_map) is None and entity_id not in unmapped: + if ( + entity_id + and _remap_entity_id(entity_id, id_map) is None + and entity_id not in unmapped + ): unmapped.append(entity_id) if file_type in {"entity_relationship", "entity_observations"}: @@ -200,7 +224,9 @@ def _stage_unmapped_entity( entity_id: str, source_data: str, ) -> Path: - target_path = staged_dir / facet_name / file_type / _sanitize_stage_name(relative_path) + target_path = ( + staged_dir / facet_name / file_type / _sanitize_stage_name(relative_path) + ) target_path.parent.mkdir(parents=True, exist_ok=True) payload = { "reason": "unmapped_entity", @@ -226,7 +252,9 @@ def _stage_facet_json_conflict( source_content: Any, target_content: Any, ) -> Path: - target_path = staged_dir / facet_name / "facet_json" / _sanitize_stage_name(relative_path) + target_path = ( + staged_dir / facet_name / "facet_json" / _sanitize_stage_name(relative_path) + ) target_path.parent.mkdir(parents=True, exist_ok=True) payload = { "reason": "facet_json_conflict", @@ -258,7 +286,10 @@ def _merge_facet_json( source_content = _parse_json_bytes(raw_bytes) if not target_path.exists() or new_facet: _write_bytes(target_path, raw_bytes) - return {"status": "written", "reason": "new_facet" if new_facet else "overlap_merged"} + return { + "status": "written", + "reason": "new_facet" if new_facet else "overlap_merged", + } target_content = json.loads(target_path.read_text(encoding="utf-8")) if target_content == source_content: @@ -294,7 +325,10 @@ def _merge_entity_relationship( merged_relationship = {**source_relationship, **target_relationship} save_facet_relationship(facet_name, entity_id, merged_relationship) - return {"status": "written", "reason": "new_facet" if new_facet else "overlap_merged"} + return { + "status": "written", + "reason": "new_facet" if new_facet else "overlap_merged", + } def _merge_observations( @@ -307,7 +341,8 @@ def _merge_observations( source_observations = _parse_jsonl_bytes(raw_bytes) target_observations = [] if new_facet else load_observations(facet_name, entity_id) seen = { - (item.get("content", ""), item.get("observed_at")) for item in target_observations + (item.get("content", ""), item.get("observed_at")) + for item in target_observations } merged_observations = list(target_observations) for item in source_observations: @@ -318,7 +353,10 @@ def _merge_observations( merged_observations.append(item) save_observations(facet_name, entity_id, merged_observations) - return {"status": "written", "reason": "new_facet" if new_facet else "overlap_merged"} + return { + "status": "written", + "reason": "new_facet" if new_facet else "overlap_merged", + } def _merge_detected_entities( @@ -337,7 +375,10 @@ def _merge_detected_entities( continue new_items.append(item) _append_jsonl(target_path, new_items) - return {"status": "written", "reason": "new_facet" if new_facet else "overlap_merged"} + return { + "status": "written", + "reason": "new_facet" if new_facet else "overlap_merged", + } def _merge_activity_config( @@ -351,7 +392,10 @@ def _merge_activity_config( existing_ids = {item.get("id") for item in target_items} new_items = [item for item in source_items if item.get("id") not in existing_ids] _append_jsonl(target_path, new_items) - return {"status": "written", "reason": "new_facet" if new_facet else "overlap_merged"} + return { + "status": "written", + "reason": "new_facet" if new_facet else "overlap_merged", + } def _merge_activity_records( @@ -365,7 +409,10 @@ def _merge_activity_records( existing_ids = {item.get("id") for item in target_items} new_items = [item for item in source_items if item.get("id") not in existing_ids] _append_jsonl(target_path, new_items) - return {"status": "written", "reason": "new_facet" if new_facet else "overlap_merged"} + return { + "status": "written", + "reason": "new_facet" if new_facet else "overlap_merged", + } def _merge_activity_output( @@ -378,7 +425,10 @@ def _merge_activity_output( if output_dir.exists(): return {"status": "skipped", "reason": "output_dir_exists"} _write_bytes(target_path, raw_bytes) - return {"status": "written", "reason": "new_facet" if new_facet else "overlap_merged"} + return { + "status": "written", + "reason": "new_facet" if new_facet else "overlap_merged", + } def _merge_todos( @@ -396,7 +446,10 @@ def _merge_todos( if (item["text"], item.get("created_at")) not in seen ] _append_jsonl(target_path, new_items) - return {"status": "written", "reason": "new_facet" if new_facet else "overlap_merged"} + return { + "status": "written", + "reason": "new_facet" if new_facet else "overlap_merged", + } def _merge_calendar( @@ -409,12 +462,13 @@ def _merge_calendar( target_items = [] if new_facet else _read_jsonl(target_path) seen = {(item["title"], item.get("start")) for item in target_items} new_items = [ - item - for item in source_items - if (item["title"], item.get("start")) not in seen + item for item in source_items if (item["title"], item.get("start")) not in seen ] _append_jsonl(target_path, new_items) - return {"status": "written", "reason": "new_facet" if new_facet else "overlap_merged"} + return { + "status": "written", + "reason": "new_facet" if new_facet else "overlap_merged", + } def _merge_news( @@ -426,7 +480,10 @@ def _merge_news( if target_path.exists(): return {"status": "skipped", "reason": "news_exists"} _write_bytes(target_path, raw_bytes) - return {"status": "written", "reason": "new_facet" if new_facet else "overlap_merged"} + return { + "status": "written", + "reason": "new_facet" if new_facet else "overlap_merged", + } def _merge_logs( @@ -437,7 +494,10 @@ def _merge_logs( ) -> dict[str, Any]: source_items = _parse_jsonl_bytes(raw_bytes) _append_jsonl(target_path, source_items) - return {"status": "written", "reason": "new_facet" if new_facet else "overlap_merged"} + return { + "status": "written", + "reason": "new_facet" if new_facet else "overlap_merged", + } def _remap_entity_ids( @@ -503,9 +563,9 @@ def _remap_entity_ids( def _serialize_jsonl(items: list[dict[str, Any]]) -> bytes: if not items: return b"" - return "".join(json.dumps(item, ensure_ascii=False) + "\n" for item in items).encode( - "utf-8" - ) + return "".join( + json.dumps(item, ensure_ascii=False) + "\n" for item in items + ).encode("utf-8") def process_facet( @@ -575,7 +635,9 @@ def process_facet( parsed_data = _parse_json_bytes(raw_bytes) if file_type in _ENTITY_FILE_TYPES: - unmapped = _check_unmapped_entities(parsed_data, id_map, file_type, path_info) + unmapped = _check_unmapped_entities( + parsed_data, id_map, file_type, path_info + ) if unmapped: staged_path = _stage_unmapped_entity( staged_dir, @@ -659,7 +721,9 @@ def process_facet( elif file_type == "todos": merge_result = _merge_todos(target_path, raw_bytes, new_facet=new_facet) elif file_type == "calendar": - merge_result = _merge_calendar(target_path, raw_bytes, new_facet=new_facet) + merge_result = _merge_calendar( + target_path, raw_bytes, new_facet=new_facet + ) elif file_type == "news": merge_result = _merge_news(target_path, raw_bytes, new_facet=new_facet) elif file_type == "logs": diff --git a/apps/import/ingest.py b/apps/import/ingest.py index 61dcfa3a3..610092511 100644 --- a/apps/import/ingest.py +++ b/apps/import/ingest.py @@ -31,7 +31,7 @@ from think.entities.journal import ( save_journal_entity, ) from think.entities.matching import find_matching_entity -from think.utils import DEFAULT_STREAM +from think.utils import DEFAULT_STREAM, day_path from .journal_sources import ( get_state_directory, @@ -204,7 +204,7 @@ def register_ingest_routes(bp) -> None: original_segment_key = segment_key arc_key = f"{stream}/{segment_key}" - day_dir = journal_root / day + day_dir = day_path(day) stream_dir = day_dir / stream segment_dir = stream_dir / segment_key action = "copied" @@ -396,7 +396,9 @@ def register_ingest_routes(bp) -> None: ) continue - match = find_matching_entity(entity_data["name"], list(target_entities.values())) + match = find_matching_entity( + entity_data["name"], list(target_entities.values()) + ) if match is not None and match.is_high_confidence: target_id = str(match["id"]) @@ -404,7 +406,10 @@ def register_ingest_routes(bp) -> None: pre_merge_snapshot = dict(target_entity) aka_by_lower: dict[str, str] = {} - for values in (target_entity.get("aka", []), entity_data.get("aka", [])): + for values in ( + target_entity.get("aka", []), + entity_data.get("aka", []), + ): if not isinstance(values, list): continue for value in values: @@ -414,7 +419,9 @@ def register_ingest_routes(bp) -> None: if key not in aka_by_lower: aka_by_lower[key] = str(value) if aka_by_lower: - target_entity["aka"] = sorted(aka_by_lower.values(), key=str.lower) + target_entity["aka"] = sorted( + aka_by_lower.values(), key=str.lower + ) merged_emails: list[str] = [] seen_emails: set[str] = set() @@ -439,7 +446,9 @@ def register_ingest_routes(bp) -> None: source_created = entity_data.get("created_at") target_created = target_entity.get("created_at") if source_created is not None and target_created is not None: - target_entity["created_at"] = min(source_created, target_created) + target_entity["created_at"] = min( + source_created, target_created + ) elif source_created is not None: target_entity["created_at"] = source_created @@ -516,7 +525,8 @@ def register_ingest_routes(bp) -> None: "staged_at": datetime.now(timezone.utc).isoformat(), } (staged_dir / f"{source_id}.json").write_text( - json.dumps(staged_payload, indent=2, ensure_ascii=False) + "\n", + json.dumps(staged_payload, indent=2, ensure_ascii=False) + + "\n", encoding="utf-8", ) staged += 1 @@ -543,7 +553,8 @@ def register_ingest_routes(bp) -> None: "staged_at": datetime.now(timezone.utc).isoformat(), } (staged_dir / f"{source_id}.json").write_text( - json.dumps(staged_payload, indent=2, ensure_ascii=False) + "\n", + json.dumps(staged_payload, indent=2, ensure_ascii=False) + + "\n", encoding="utf-8", ) staged += 1 @@ -585,7 +596,9 @@ def register_ingest_routes(bp) -> None: entity_state["received"][source_id] = content_hash except Exception as exc: - entity_id = entity_data.get("id", "") if isinstance(entity_data, dict) else "" + entity_id = ( + entity_data.get("id", "") if isinstance(entity_data, dict) else "" + ) errors.append({"entity_id": entity_id, "error": str(exc)}) _write_state_atomic(state_path, entity_state) @@ -682,13 +695,17 @@ def register_ingest_routes(bp) -> None: normalized_files: list[dict[str, str]] = [] for file_idx, file_meta in enumerate(files): if not isinstance(file_meta, dict): - return jsonify({"error": "Facet file metadata must be an object"}), 400 + return jsonify( + {"error": "Facet file metadata must be an object"} + ), 400 path_value = file_meta.get("path") type_value = file_meta.get("type") if not isinstance(path_value, str) or not isinstance(type_value, str): return ( - jsonify({"error": "Facet file metadata must include path and type"}), + jsonify( + {"error": "Facet file metadata must include path and type"} + ), 400, ) @@ -731,9 +748,9 @@ def register_ingest_routes(bp) -> None: if written_facets: source = g.journal_source source.setdefault("stats", {}) - source["stats"]["facets_received"] = ( - source["stats"].get("facets_received", 0) + len(written_facets) - ) + source["stats"]["facets_received"] = source["stats"].get( + "facets_received", 0 + ) + len(written_facets) save_journal_source(source) return jsonify( diff --git a/apps/observer/tests/test_observer_client.py b/apps/observer/tests/test_observer_client.py index dee95199c..a7441ec40 100644 --- a/apps/observer/tests/test_observer_client.py +++ b/apps/observer/tests/test_observer_client.py @@ -108,7 +108,9 @@ def test_auto_registration(mock_session, mock_config, mock_journal, tmp_path): assert result.success is True assert client._key == "registered-key" - assert mock_session.post.call_args_list[0][0][0].endswith("/app/observer/api/create") + assert mock_session.post.call_args_list[0][0][0].endswith( + "/app/observer/api/create" + ) config = json.loads((mock_journal / "config" / "journal.json").read_text()) assert config["observe"]["observer"]["key"] == "registered-key" diff --git a/apps/photos/call.py b/apps/photos/call.py index 61b991d1e..9c47971d8 100644 --- a/apps/photos/call.py +++ b/apps/photos/call.py @@ -73,7 +73,9 @@ def sync( if not matched: return - conn.execute("DELETE FROM entity_signals WHERE signal_type='photo_cooccurrence'") + conn.execute( + "DELETE FROM entity_signals WHERE signal_type='photo_cooccurrence'" + ) signal_count = 0 for cluster in clusters: diff --git a/apps/photos/reader.py b/apps/photos/reader.py index 0001a0961..538f0a5d8 100644 --- a/apps/photos/reader.py +++ b/apps/photos/reader.py @@ -7,6 +7,7 @@ import sqlite3 def read_face_clusters(db_path: str) -> list[dict]: conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) try: + def resolve_table(preferred: str, fallback: str) -> str: for table_name in (preferred, fallback): row = conn.execute( diff --git a/apps/photos/tests/test_call.py b/apps/photos/tests/test_call.py index 88530cebc..b718a5d08 100644 --- a/apps/photos/tests/test_call.py +++ b/apps/photos/tests/test_call.py @@ -15,7 +15,9 @@ runner = CliRunner() def _create_photos_db( - db_path: Path, people: list[tuple[int, str | None]], faces: list[tuple[int, int, int]] + db_path: Path, + people: list[tuple[int, str | None]], + faces: list[tuple[int, int, int]], ) -> None: conn = sqlite3.connect(db_path) try: @@ -217,7 +219,9 @@ class TestPhotosSync: from think.indexer.journal import get_entity_strength results = get_entity_strength() - alice = next((r for r in results if r.get("entity_id") == "alice_johnson"), None) + alice = next( + (r for r in results if r.get("entity_id") == "alice_johnson"), None + ) assert alice is not None assert "photo_count" in alice assert alice["photo_count"] == 2 @@ -258,6 +262,8 @@ class TestPhotosSync: monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal_dir)) monkeypatch.setattr(sys, "platform", "darwin") - result = runner.invoke(call_app, ["photos", "sync", "--library", str(photos_db)]) + result = runner.invoke( + call_app, ["photos", "sync", "--library", str(photos_db)] + ) assert result.exit_code == 0 assert "Found 1 named face clusters." in result.output diff --git a/apps/settings/call.py b/apps/settings/call.py index 31da92cc7..804eafbce 100644 --- a/apps/settings/call.py +++ b/apps/settings/call.py @@ -242,9 +242,8 @@ def keys_validate() -> None: key_validation[provider] = result providers_config = config.get("providers", {}) - if ( - providers_config.get("google_backend") == "vertex" - and providers_config.get("vertex_credentials") + if providers_config.get("google_backend") == "vertex" and providers_config.get( + "vertex_credentials" ): result = validate_vertex_credentials(providers_config["vertex_credentials"]) result["timestamp"] = datetime.now(timezone.utc).isoformat() @@ -286,7 +285,9 @@ def providers_show() -> None: for provider in providers_list } vertex_creds_path = providers_config.get("vertex_credentials") - vertex_creds_configured = bool(vertex_creds_path and Path(vertex_creds_path).exists()) + vertex_creds_configured = bool( + vertex_creds_path and Path(vertex_creds_path).exists() + ) provider_status = build_provider_status(providers_list, vertex_creds_configured) result = { "providers": providers_list, @@ -307,7 +308,9 @@ def providers_set_generate( backup: str | None = typer.Option(None, "--backup", help="Backup provider."), ) -> None: """Set generate provider defaults.""" - typer.echo(json.dumps(_set_provider_type("generate", provider, tier, backup), indent=2)) + typer.echo( + json.dumps(_set_provider_type("generate", provider, tier, backup), indent=2) + ) @providers_app.command("set-cogitate") @@ -317,7 +320,9 @@ def providers_set_cogitate( backup: str | None = typer.Option(None, "--backup", help="Backup provider."), ) -> None: """Set cogitate provider defaults.""" - typer.echo(json.dumps(_set_provider_type("cogitate", provider, tier, backup), indent=2)) + typer.echo( + json.dumps(_set_provider_type("cogitate", provider, tier, backup), indent=2) + ) @providers_app.command("set-auth") diff --git a/apps/settings/routes.py b/apps/settings/routes.py index b64a1bbfc..687b31931 100644 --- a/apps/settings/routes.py +++ b/apps/settings/routes.py @@ -472,9 +472,7 @@ def get_providers() -> Any: except Exception: pass - provider_status = build_provider_status( - providers_list, vertex_creds_configured - ) + provider_status = build_provider_status(providers_list, vertex_creds_configured) return jsonify( { diff --git a/apps/settings/tests/test_call.py b/apps/settings/tests/test_call.py index 5352cf987..63e0f1c8a 100644 --- a/apps/settings/tests/test_call.py +++ b/apps/settings/tests/test_call.py @@ -77,7 +77,9 @@ class TestKeysClear: def test_keys_clear(self, settings_env): tmp_path, _config = settings_env() - result = runner.invoke(call_app, ["settings", "keys", "clear", "GOOGLE_API_KEY"]) + result = runner.invoke( + call_app, ["settings", "keys", "clear", "GOOGLE_API_KEY"] + ) assert result.exit_code == 0 saved = json.loads((tmp_path / "config" / "journal.json").read_text()) @@ -281,7 +283,9 @@ class TestVertexCredentials: encoding="utf-8", ) - with patch("think.providers.google.validate_vertex_credentials") as mock_validate: + with patch( + "think.providers.google.validate_vertex_credentials" + ) as mock_validate: result = runner.invoke( call_app, [ diff --git a/apps/speakers/bootstrap.py b/apps/speakers/bootstrap.py index c75101d46..a9743ddb7 100644 --- a/apps/speakers/bootstrap.py +++ b/apps/speakers/bootstrap.py @@ -113,11 +113,15 @@ def _save_voiceprints_batch( existing_meta_strings = data["metadata"] existing_meta_dicts = [json.loads(m) for m in existing_meta_strings] except (FileNotFoundError, ValueError, np.lib.npyio.NpzFile) as e: - logger.warning(f"Failed to load existing voiceprints for {entity_id} from {npz_path}: {e}. Starting fresh.") + logger.warning( + f"Failed to load existing voiceprints for {entity_id} from {npz_path}: {e}. Starting fresh." + ) existing_emb = np.empty((0, 256), dtype=np.float32) existing_meta_dicts = [] - except Exception as e: # Catch other potential errors during loading - logger.error(f"Unexpected error loading existing voiceprints for {entity_id} from {npz_path}: {e}") + except Exception as e: # Catch other potential errors during loading + logger.error( + f"Unexpected error loading existing voiceprints for {entity_id} from {npz_path}: {e}" + ) raise else: existing_emb = np.empty((0, 256), dtype=np.float32) @@ -134,11 +138,13 @@ def _save_voiceprints_batch( if new_emb_list: new_emb_np = np.vstack(new_emb_list) combined_emb = ( - np.vstack([existing_emb, new_emb_np]) if len(existing_emb) > 0 else new_emb_np + np.vstack([existing_emb, new_emb_np]) + if len(existing_emb) > 0 + else new_emb_np ) # Combine the metadata dictionaries combined_meta_dicts = existing_meta_dicts + new_meta_dicts - else: # Should not happen if new_items is not empty, but for safety + else: # Should not happen if new_items is not empty, but for safety combined_emb = existing_emb combined_meta_dicts = existing_meta_dicts @@ -146,11 +152,11 @@ def _save_voiceprints_batch( try: # Import the utility function from apps.speakers.voiceprint_io import save_voiceprints_safely - + save_voiceprints_safely( npz_path=npz_path, embeddings=combined_emb, - metadata=combined_meta_dicts # Pass metadata as a list of dicts + metadata=combined_meta_dicts, # Pass metadata as a list of dicts ) return len(new_items) except Exception as e: @@ -882,9 +888,7 @@ def link_import(name: str, entity_id: str) -> dict[str, Any]: others = [e for eid, e in all_entities.items() if eid != entity_id] conflict = find_matching_entity(name, others) if conflict: - return { - "error": f"Name '{name}' conflicts with entity '{conflict['id']}'" - } + return {"error": f"Name '{name}' conflicts with entity '{conflict['id']}'"} existing_aka = set(entity.get("aka", [])) already_present = name in existing_aka diff --git a/apps/speakers/voiceprint_io.py b/apps/speakers/voiceprint_io.py index a77f6c384..9027542f3 100644 --- a/apps/speakers/voiceprint_io.py +++ b/apps/speakers/voiceprint_io.py @@ -20,7 +20,9 @@ import numpy as np logger = logging.getLogger(__name__) -def save_voiceprints_safely(npz_path: Path, embeddings: np.ndarray, metadata: dict) -> None: +def save_voiceprints_safely( + npz_path: Path, embeddings: np.ndarray, metadata: dict +) -> None: """ Safely saves voiceprint data to an NPZ file with file locking and integrity check. @@ -60,7 +62,9 @@ def save_voiceprints_safely(npz_path: Path, embeddings: np.ndarray, metadata: di tmp_path.rename(npz_path) else: # This should ideally not happen if np.savez_compressed succeeded - raise FileNotFoundError(f"Temporary voiceprint file not found: {tmp_path}") + raise FileNotFoundError( + f"Temporary voiceprint file not found: {tmp_path}" + ) # --- Integrity Check --- try: @@ -70,9 +74,13 @@ def save_voiceprints_safely(npz_path: Path, embeddings: np.ndarray, metadata: di # For now, assume standard numpy savz_compressed data. with np.load(npz_path, allow_pickle=False) as data: # Basic check: ensure expected keys exist - if 'embeddings' not in data or 'metadata' not in data: - raise ValueError("Missing 'embeddings' or 'metadata' keys in loaded NPZ.") - logger.info(f"Successfully wrote and verified voiceprint file: {npz_path}") + if "embeddings" not in data or "metadata" not in data: + raise ValueError( + "Missing 'embeddings' or 'metadata' keys in loaded NPZ." + ) + logger.info( + f"Successfully wrote and verified voiceprint file: {npz_path}" + ) except (FileNotFoundError, ValueError, np.lib.npyio.NpzFile) as e: logger.error( @@ -95,7 +103,9 @@ def save_voiceprints_safely(npz_path: Path, embeddings: np.ndarray, metadata: di try: tmp_path.unlink() except OSError as rm_err: - logger.error(f"Failed to clean up temporary file {tmp_path}: {rm_err}") + logger.error( + f"Failed to clean up temporary file {tmp_path}: {rm_err}" + ) raise e # Re-raise the original exception finally: diff --git a/apps/transcripts/routes.py b/apps/transcripts/routes.py index 4a0254a5a..a46027ba6 100644 --- a/apps/transcripts/routes.py +++ b/apps/transcripts/routes.py @@ -99,7 +99,9 @@ def transcript_day_data(day: str) -> Any: return error_response("Day not found", 404) audio_ranges, screen_ranges, segments = scan_day(day) - return jsonify({"audio": audio_ranges, "screen": screen_ranges, "segments": segments}) + return jsonify( + {"audio": audio_ranges, "screen": screen_ranges, "segments": segments} + ) @transcripts_bp.route("/api/serve_file//") diff --git a/convey/apps.py b/convey/apps.py index 630f8d4f4..8b19dabc4 100644 --- a/convey/apps.py +++ b/convey/apps.py @@ -226,16 +226,12 @@ def _resolve_placeholder(awareness_current: dict, day_count: int) -> str: return attention.placeholder_text imports = awareness_current.get("imports", {}) if not imports.get("has_imported") and day_count < 3: - return ( - "Bring in past conversations, calendar, or notes to give me context..." - ) + return "Bring in past conversations, calendar, or notes to give me context..." if awareness_current.get("journal", {}).get("first_daily_ready"): if day_count < 2: return "Your first daily analysis is ready — ask me what I found..." if day_count >= 7: - return ( - "Ask me about your day, search your journal, or explore insights..." - ) + return "Ask me about your day, search your journal, or explore insights..." return "Your daily analysis is ready — ask about today or anything in your journal..." return "Capture is running — your first daily analysis will be ready soon..." diff --git a/convey/system.py b/convey/system.py index e05a3e419..225b6c9da 100644 --- a/convey/system.py +++ b/convey/system.py @@ -102,9 +102,7 @@ def _get_capture_health() -> dict[str, Any]: observers = list_observers() # Filter to active (non-revoked, enabled) observers active = [ - o - for o in observers - if not o.get("revoked", False) and o.get("enabled", True) + o for o in observers if not o.get("revoked", False) and o.get("enabled", True) ] if not active: diff --git a/observe/describe.py b/observe/describe.py index a7d6d197f..ece1b5999 100644 --- a/observe/describe.py +++ b/observe/describe.py @@ -310,13 +310,13 @@ class VideoProcessor: except av.error.InvalidDataError as e: logger.error( f"Invalid video data error for {self.video_path}: {e}. Skipping video.", - exc_info=True + exc_info=True, ) return [] except Exception as e: logger.error( f"Unexpected error processing video {self.video_path}: {e}", - exc_info=True + exc_info=True, ) raise return self.qualified_frames diff --git a/observe/observer_cli.py b/observe/observer_cli.py index 3bc9e5c86..2b799372d 100644 --- a/observe/observer_cli.py +++ b/observe/observer_cli.py @@ -438,7 +438,9 @@ def main() -> None: sub.add_parser("list", help="List all registered observers") # rename - p_rename = sub.add_parser("rename", help="Rename an observer (affects future streams)") + p_rename = sub.add_parser( + "rename", help="Rename an observer (affects future streams)" + ) p_rename.add_argument("identifier", help="Observer name or key prefix") p_rename.add_argument("new_name", help="New name for the observer") diff --git a/observe/observer_client.py b/observe/observer_client.py index 141237236..ee512b2ee 100644 --- a/observe/observer_client.py +++ b/observe/observer_client.py @@ -206,11 +206,13 @@ class ObserverClient: data["platform"] = self._platform if meta: data["meta"] = json.dumps(meta) - + headers = {} if self._key: headers["Authorization"] = f"Bearer {self._key}" - logger.debug(f"Sending Authorization header: Bearer {self._key[:8]}...") + logger.debug( + f"Sending Authorization header: Bearer {self._key[:8]}..." + ) response = self._session.post( url, diff --git a/tests/test_activity_state_machine.py b/tests/test_activity_state_machine.py index 66156c125..e93cf488e 100644 --- a/tests/test_activity_state_machine.py +++ b/tests/test_activity_state_machine.py @@ -483,8 +483,15 @@ class TestCompletedRecordFields: sm.update(_sense(content_type="meeting"), "090500_300", "20260304") rec = sm.get_completed_activities()[0] - required = {"id", "activity", "segments", "level_avg", "description", - "active_entities", "created_at"} + required = { + "id", + "activity", + "segments", + "level_avg", + "description", + "active_entities", + "created_at", + } assert required.issubset(rec.keys()) # No internal _fields should leak assert not any(k.startswith("_") for k in rec.keys()) @@ -497,7 +504,9 @@ class TestCompletedRecordFields: {"type": "Person", "name": "Alice", "context": "dev"}, {"type": "Tool", "name": "VSCode", "context": "editor"}, ] - sm.update(_sense(content_type="coding", entities=entities), "090000_300", "20260304") + sm.update( + _sense(content_type="coding", entities=entities), "090000_300", "20260304" + ) sm.update(_sense(content_type="meeting"), "090500_300", "20260304") rec = sm.get_completed_activities()[0] diff --git a/tests/test_agents_check.py b/tests/test_agents_check.py index 511c6ef61..e54025edf 100644 --- a/tests/test_agents_check.py +++ b/tests/test_agents_check.py @@ -418,7 +418,9 @@ def test_all_skip_exits_zero(tmp_path, monkeypatch): monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) - monkeypatch.setattr(agents, "_check_generate", lambda *_args: ("skip", "not configured")) + monkeypatch.setattr( + agents, "_check_generate", lambda *_args: ("skip", "not configured") + ) async def mock_check_cogitate(*_args): return "skip", "not configured" @@ -458,7 +460,9 @@ def test_mix_skip_and_fail_exits_one(tmp_path, monkeypatch): monkeypatch.setattr("think.providers.PROVIDER_REGISTRY", fake_registry) monkeypatch.setattr("think.models.PROVIDER_DEFAULTS", fake_defaults) monkeypatch.setattr(agents, "get_journal", lambda: str(tmp_path)) - monkeypatch.setattr(agents, "_check_generate", lambda *_args: ("skip", "not configured")) + monkeypatch.setattr( + agents, "_check_generate", lambda *_args: ("skip", "not configured") + ) async def mock_check_cogitate(*_args): return "fail", "FAIL: broken" @@ -527,7 +531,9 @@ def test_skipped_count_in_summary(tmp_path, monkeypatch): assert exc_info.value.code == 0 payload = json.loads((tmp_path / "health" / "agents.json").read_text()) summary = payload["summary"] - assert summary["total"] == summary["passed"] + summary["skipped"] + summary["failed"] + assert ( + summary["total"] == summary["passed"] + summary["skipped"] + summary["failed"] + ) assert summary["passed"] == 6 assert summary["skipped"] == 6 assert summary["failed"] == 0 diff --git a/tests/test_app_sol.py b/tests/test_app_sol.py index 549c1b469..4d40c3b62 100644 --- a/tests/test_app_sol.py +++ b/tests/test_app_sol.py @@ -303,7 +303,5 @@ class TestApiOutputFile: def test_missing_file_returns_404(self, agents_client): """Non-existent file returns 404.""" - resp = agents_client.get( - "/app/sol/api/output/20260214/agents/nonexistent.md" - ) + resp = agents_client.get("/app/sol/api/output/20260214/agents/nonexistent.md") assert resp.status_code == 404 diff --git a/tests/test_awareness.py b/tests/test_awareness.py index 76e4b15e7..b3913c968 100644 --- a/tests/test_awareness.py +++ b/tests/test_awareness.py @@ -185,6 +185,7 @@ class TestJournalState: assert state["journal"]["first_daily_ready"] is True assert state["journal"]["first_daily_ready_at"] == "20260308T14:00:00" + class TestComputeThickness: """Tests for compute_thickness().""" diff --git a/tests/test_chat_context.py b/tests/test_chat_context.py index a21d485a5..5d5bb0577 100644 --- a/tests/test_chat_context.py +++ b/tests/test_chat_context.py @@ -94,7 +94,9 @@ def test_chat_context_awareness_error_graceful(monkeypatch): """Awareness failures still return the full template var shape.""" monkeypatch.setattr("think.conversation.build_memory_context", lambda **kw: "") monkeypatch.setattr("think.routines.get_routine_state", lambda: []) - monkeypatch.setattr("think.routines.get_config", lambda: {"_meta": {"suggestions": {}}}) + monkeypatch.setattr( + "think.routines.get_config", lambda: {"_meta": {"suggestions": {}}} + ) monkeypatch.setattr( "think.utils.get_config", lambda: {"agent": {"name": "aria", "name_status": "default"}}, @@ -182,7 +184,9 @@ def test_chat_context_routines_error_graceful(monkeypatch): "think.routines.get_routine_state", lambda: (_ for _ in ()).throw(RuntimeError("boom")), ) - monkeypatch.setattr("think.routines.get_config", lambda: {"_meta": {"suggestions": {}}}) + monkeypatch.setattr( + "think.routines.get_config", lambda: {"_meta": {"suggestions": {}}} + ) monkeypatch.setattr( "think.utils.get_config", lambda: {"agent": {"name": "aria", "name_status": "default"}}, diff --git a/tests/test_convey_apps.py b/tests/test_convey_apps.py index f1ee8619b..e183cd22b 100644 --- a/tests/test_convey_apps.py +++ b/tests/test_convey_apps.py @@ -34,6 +34,7 @@ def _run_triage(): assert response.status_code == 200 return mock_spawn + # --- Placeholder resolution --- diff --git a/tests/test_dream_activity.py b/tests/test_dream_activity.py index 1195beb11..439440ee5 100644 --- a/tests/test_dream_activity.py +++ b/tests/test_dream_activity.py @@ -458,8 +458,14 @@ class TestActivityPersistence: class TestActivityPersistenceRoundTrip: """Full round-trip: state machine → append → load → field verification.""" - def _sense(self, content_type="coding", density="active", facets=None, - summary="Working.", entities=None): + def _sense( + self, + content_type="coding", + density="active", + facets=None, + summary="Working.", + entities=None, + ): if facets is None: facets = [{"facet": "work", "activity": content_type, "level": "high"}] return { @@ -486,7 +492,9 @@ class TestActivityPersistenceRoundTrip: sm.update(self._sense(content_type="coding"), "090500_300", "20260304") sm.update(self._sense(content_type="coding"), "091000_300", "20260304") # End via type change - changes = sm.update(self._sense(content_type="meeting"), "091500_300", "20260304") + changes = sm.update( + self._sense(content_type="meeting"), "091500_300", "20260304" + ) ended = [c for c in changes if c.get("state") == "ended"] assert len(ended) == 1 @@ -546,7 +554,9 @@ class TestActivityPersistenceRoundTrip: sm = ActivityStateMachine() sm.update(self._sense(content_type="coding"), "090000_300", "20260304") - changes = sm.update(self._sense(content_type="meeting"), "090500_300", "20260304") + changes = sm.update( + self._sense(content_type="meeting"), "090500_300", "20260304" + ) ended = [c for c in changes if c.get("state") == "ended"] rec = sm.get_completed_activities()[0] @@ -572,7 +582,9 @@ class TestActivityPersistenceRoundTrip: sm = ActivityStateMachine() # Activity 1 ends sm.update(self._sense(content_type="coding"), "090000_300", "20260304") - changes1 = sm.update(self._sense(content_type="meeting"), "090500_300", "20260304") + changes1 = sm.update( + self._sense(content_type="meeting"), "090500_300", "20260304" + ) facet_by_id = { c["id"]: c.get("_facet", "__") for c in changes1 @@ -583,7 +595,9 @@ class TestActivityPersistenceRoundTrip: append_activity_record(facet_by_id[rec["id"]], "20260304", rec) # Activity 2 continues (no ending) - changes2 = sm.update(self._sense(content_type="meeting"), "091000_300", "20260304") + changes2 = sm.update( + self._sense(content_type="meeting"), "091000_300", "20260304" + ) # No ended changes in this update facet_by_id2 = { c["id"]: c.get("_facet", "__") @@ -613,9 +627,13 @@ class TestActivityPersistenceRoundTrip: ] sm = ActivityStateMachine() sm.update( - self._sense(content_type="coding", entities=entities, - summary="Pair programming with Alice"), - "090000_300", "20260304", + self._sense( + content_type="coding", + entities=entities, + summary="Pair programming with Alice", + ), + "090000_300", + "20260304", ) sm.update(self._sense(content_type="meeting"), "090500_300", "20260304") @@ -641,8 +659,9 @@ class TestActivityPersistenceRoundTrip: monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", tmpdir) sm = ActivityStateMachine() - sm.update(self._sense(content_type="coding", facets=[]), - "090000_300", "20260304") + sm.update( + self._sense(content_type="coding", facets=[]), "090000_300", "20260304" + ) changes = sm.update(self._sense(density="idle"), "090500_300", "20260304") facet_by_id = { @@ -725,21 +744,35 @@ class TestCreatedAtRoutesCompat: sm = ActivityStateMachine() sm.update( { - "density": "active", "content_type": "coding", - "activity_summary": "test", "entities": [], - "facets": [{"facet": "work", "activity": "coding", "level": "high"}], - "meeting_detected": False, "speakers": [], "recommend": {}, + "density": "active", + "content_type": "coding", + "activity_summary": "test", + "entities": [], + "facets": [ + {"facet": "work", "activity": "coding", "level": "high"} + ], + "meeting_detected": False, + "speakers": [], + "recommend": {}, }, - "090000_300", "20260304", + "090000_300", + "20260304", ) sm.update( { - "density": "active", "content_type": "meeting", - "activity_summary": "standup", "entities": [], - "facets": [{"facet": "work", "activity": "meeting", "level": "medium"}], - "meeting_detected": True, "speakers": [], "recommend": {}, + "density": "active", + "content_type": "meeting", + "activity_summary": "standup", + "entities": [], + "facets": [ + {"facet": "work", "activity": "meeting", "level": "medium"} + ], + "meeting_detected": True, + "speakers": [], + "recommend": {}, }, - "090500_300", "20260304", + "090500_300", + "20260304", ) rec = sm.get_completed_activities()[0] append_activity_record("work", "20260304", rec) @@ -774,26 +807,41 @@ class TestCreatedAtRoutesCompat: sm = ActivityStateMachine() sm.update( { - "density": "active", "content_type": "coding", - "activity_summary": "first", "entities": [], - "facets": [{"facet": "work", "activity": "coding", "level": "high"}], - "meeting_detected": False, "speakers": [], "recommend": {}, + "density": "active", + "content_type": "coding", + "activity_summary": "first", + "entities": [], + "facets": [ + {"facet": "work", "activity": "coding", "level": "high"} + ], + "meeting_detected": False, + "speakers": [], + "recommend": {}, }, - "090000_300", "20260304", + "090000_300", + "20260304", ) changes1 = sm.update( { - "density": "active", "content_type": "meeting", - "activity_summary": "second", "entities": [], - "facets": [{"facet": "work", "activity": "meeting", "level": "medium"}], - "meeting_detected": True, "speakers": [], "recommend": {}, + "density": "active", + "content_type": "meeting", + "activity_summary": "second", + "entities": [], + "facets": [ + {"facet": "work", "activity": "meeting", "level": "medium"} + ], + "meeting_detected": True, + "speakers": [], + "recommend": {}, }, - "090500_300", "20260304", + "090500_300", + "20260304", ) # Persist first completed facet_by_id = { c["id"]: c.get("_facet", "__") - for c in changes1 if c.get("state") == "ended" + for c in changes1 + if c.get("state") == "ended" } for rec in sm.get_completed_activities(): if rec["id"] in facet_by_id: @@ -804,16 +852,24 @@ class TestCreatedAtRoutesCompat: changes2 = sm.update( { - "density": "active", "content_type": "coding", - "activity_summary": "third", "entities": [], - "facets": [{"facet": "work", "activity": "coding", "level": "high"}], - "meeting_detected": False, "speakers": [], "recommend": {}, + "density": "active", + "content_type": "coding", + "activity_summary": "third", + "entities": [], + "facets": [ + {"facet": "work", "activity": "coding", "level": "high"} + ], + "meeting_detected": False, + "speakers": [], + "recommend": {}, }, - "091000_300", "20260304", + "091000_300", + "20260304", ) facet_by_id2 = { c["id"]: c.get("_facet", "__") - for c in changes2 if c.get("state") == "ended" + for c in changes2 + if c.get("state") == "ended" } for rec in sm.get_completed_activities(): if rec["id"] in facet_by_id2: diff --git a/tests/test_entity_ingest.py b/tests/test_entity_ingest.py index acc543768..40fb13629 100644 --- a/tests/test_entity_ingest.py +++ b/tests/test_entity_ingest.py @@ -442,7 +442,9 @@ def test_key_prefix_mismatch(ingest_env): def test_stats_update(ingest_env): env = ingest_env - save_journal_entity({"id": "alice_johnson", "name": "Alice Johnson", "type": "Person"}) + save_journal_entity( + {"id": "alice_johnson", "name": "Alice Johnson", "type": "Person"} + ) response = _post_entities( env["client"], diff --git a/tests/test_entity_observer_context.py b/tests/test_entity_observer_context.py index bf748d873..94389ed55 100644 --- a/tests/test_entity_observer_context.py +++ b/tests/test_entity_observer_context.py @@ -72,7 +72,9 @@ def test_assemble_observer_context_with_fixture_data(): assert result assert "Juliet Capulet" in result assert "Knowledge Graph" in result - assert "Prepared revenue projections for Verona Platform board presentation" in result + assert ( + "Prepared revenue projections for Verona Platform board presentation" in result + ) def test_assemble_observer_context_no_kg(tmp_path): @@ -125,7 +127,14 @@ def test_assemble_observer_context_observations_sliced(tmp_path): _attach_entity(tmp_path, facet, entity_id, "Alice Johnson") _write_jsonl( tmp_path / "facets" / facet / "entities" / f"{day}.jsonl", - [{"id": entity_id, "type": "Person", "name": "Alice Johnson", "description": ""}], + [ + { + "id": entity_id, + "type": "Person", + "name": "Alice Johnson", + "description": "", + } + ], ) _write_jsonl( tmp_path / _obs_path(facet, entity_id), @@ -283,7 +292,10 @@ def test_post_process_deduplicates_existing(tmp_path): "observations": { "alice_johnson": [ {"content": "Prefers morning meetings", "reasoning": "dupe"}, - {"content": "Expert in distributed systems", "reasoning": "new"}, + { + "content": "Expert in distributed systems", + "reasoning": "new", + }, ] }, "skipped": [], diff --git a/tests/test_facet_ingest.py b/tests/test_facet_ingest.py index ece09ab45..1b323d915 100644 --- a/tests/test_facet_ingest.py +++ b/tests/test_facet_ingest.py @@ -72,9 +72,9 @@ def ingest_env(journal_env): }, "received": {}, } - ( - get_state_directory(key_prefix) / "entities" / "state.json" - ).write_text(json.dumps(entity_state, indent=2), encoding="utf-8") + (get_state_directory(key_prefix) / "entities" / "state.json").write_text( + json.dumps(entity_state, indent=2), encoding="utf-8" + ) app = Flask(__name__) app.config["TESTING"] = True @@ -136,7 +136,9 @@ def _read_log(key_prefix: str) -> list[dict]: ] -def _read_staged(key_prefix: str, facet: str, file_type: str, relative_path: str) -> dict: +def _read_staged( + key_prefix: str, facet: str, file_type: str, relative_path: str +) -> dict: staged_name = relative_path.replace("/", "__") + ".staged.json" staged_path = ( get_state_directory(key_prefix) @@ -154,9 +156,9 @@ def _json_bytes(data: dict) -> bytes: def _jsonl_bytes(items: list[dict]) -> bytes: - return "".join(json.dumps(item, ensure_ascii=False) + "\n" for item in items).encode( - "utf-8" - ) + return "".join( + json.dumps(item, ensure_ascii=False) + "\n" for item in items + ).encode("utf-8") def _read_json(path: Path) -> dict: @@ -289,11 +291,17 @@ def test_new_facet_all_types(ingest_env): { "name": "personal", "files": [ - {"path": "facet.json", "type": "facet_json", "content": _json_bytes({"title": "Personal"})}, + { + "path": "facet.json", + "type": "facet_json", + "content": _json_bytes({"title": "Personal"}), + }, { "path": "entities/same_entity/entity.json", "type": "entity_relationship", - "content": _json_bytes({"description": "Close contact", "attached_at": 100}), + "content": _json_bytes( + {"description": "Close contact", "attached_at": 100} + ), }, { "path": "entities/same_entity/observations.jsonl", @@ -359,7 +367,9 @@ def test_new_facet_all_types(ingest_env): ] metadata, file_map = _build_request(facets) - response = _post_facets(env["client"], env["key"], env["key_prefix"], metadata, file_map) + response = _post_facets( + env["client"], env["key"], env["key_prefix"], metadata, file_map + ) assert response.status_code == 200 assert response.get_json() == { @@ -380,16 +390,40 @@ def test_new_facet_all_types(ingest_env): assert _read_jsonl_file( facet_root / "entities" / "same_entity" / "observations.jsonl" ) == [{"content": "Likes tea", "observed_at": 1}] - assert _read_jsonl_file(facet_root / "entities" / "20260305.jsonl")[0]["id"] == "same_entity" - assert _read_jsonl_file(facet_root / "activities" / "activities.jsonl")[0]["id"] == "coding" - assert _read_jsonl_file(facet_root / "activities" / "20260305.jsonl")[0]["id"] == "coding_093000_300" assert ( - facet_root / "activities" / "20260305" / "coding_093000_300" / "session_review.md" + _read_jsonl_file(facet_root / "entities" / "20260305.jsonl")[0]["id"] + == "same_entity" + ) + assert ( + _read_jsonl_file(facet_root / "activities" / "activities.jsonl")[0]["id"] + == "coding" + ) + assert ( + _read_jsonl_file(facet_root / "activities" / "20260305.jsonl")[0]["id"] + == "coding_093000_300" + ) + assert ( + facet_root + / "activities" + / "20260305" + / "coding_093000_300" + / "session_review.md" ).read_text(encoding="utf-8") == "# Session\n" - assert _read_jsonl_file(facet_root / "todos" / "20260305.jsonl")[0]["text"] == "Ship it" - assert _read_jsonl_file(facet_root / "calendar" / "20260305.jsonl")[0]["title"] == "Standup" - assert (facet_root / "news" / "20260305.md").read_text(encoding="utf-8") == "# News\n" - assert _read_jsonl_file(facet_root / "logs" / "20260305.jsonl")[0]["event"] == "ingested" + assert ( + _read_jsonl_file(facet_root / "todos" / "20260305.jsonl")[0]["text"] + == "Ship it" + ) + assert ( + _read_jsonl_file(facet_root / "calendar" / "20260305.jsonl")[0]["title"] + == "Standup" + ) + assert (facet_root / "news" / "20260305.md").read_text( + encoding="utf-8" + ) == "# News\n" + assert ( + _read_jsonl_file(facet_root / "logs" / "20260305.jsonl")[0]["event"] + == "ingested" + ) source = load_journal_source(env["key"]) assert source["stats"]["facets_received"] == 1 @@ -397,7 +431,9 @@ def test_new_facet_all_types(ingest_env): def test_existing_facet_merge_entity_relationship(ingest_env): env = ingest_env - target_path = env["root"] / "facets" / "work" / "entities" / "same_entity" / "entity.json" + target_path = ( + env["root"] / "facets" / "work" / "entities" / "same_entity" / "entity.json" + ) _write_json( target_path, {"entity_id": "same_entity", "description": "Keep target", "attached_at": 200}, @@ -410,13 +446,17 @@ def test_existing_facet_merge_entity_relationship(ingest_env): { "path": "entities/same_entity/entity.json", "type": "entity_relationship", - "content": _json_bytes({"description": "Source desc", "last_seen": 999}), + "content": _json_bytes( + {"description": "Source desc", "last_seen": 999} + ), } ], } ] metadata, file_map = _build_request(facets) - response = _post_facets(env["client"], env["key"], env["key_prefix"], metadata, file_map) + response = _post_facets( + env["client"], env["key"], env["key_prefix"], metadata, file_map + ) assert response.status_code == 200 assert response.get_json()["merged"] == 1 @@ -431,7 +471,12 @@ def test_existing_facet_merge_entity_relationship(ingest_env): def test_existing_facet_merge_observations(ingest_env): env = ingest_env target_path = ( - env["root"] / "facets" / "work" / "entities" / "same_entity" / "observations.jsonl" + env["root"] + / "facets" + / "work" + / "entities" + / "same_entity" + / "observations.jsonl" ) _write_jsonl( target_path, @@ -459,7 +504,9 @@ def test_existing_facet_merge_observations(ingest_env): } ] metadata, file_map = _build_request(facets) - response = _post_facets(env["client"], env["key"], env["key_prefix"], metadata, file_map) + response = _post_facets( + env["client"], env["key"], env["key_prefix"], metadata, file_map + ) assert response.status_code == 200 assert response.get_json()["merged"] == 1 @@ -493,7 +540,9 @@ def test_existing_facet_merge_detected_entities(ingest_env): } ] metadata, file_map = _build_request(facets) - response = _post_facets(env["client"], env["key"], env["key_prefix"], metadata, file_map) + response = _post_facets( + env["client"], env["key"], env["key_prefix"], metadata, file_map + ) assert response.status_code == 200 assert response.get_json()["merged"] == 1 @@ -524,11 +573,16 @@ def test_existing_facet_merge_activity_config(ingest_env): } ] metadata, file_map = _build_request(facets) - response = _post_facets(env["client"], env["key"], env["key_prefix"], metadata, file_map) + response = _post_facets( + env["client"], env["key"], env["key_prefix"], metadata, file_map + ) assert response.status_code == 200 assert response.get_json()["merged"] == 1 - assert [item["id"] for item in _read_jsonl_file(target_path)] == ["coding", "meeting"] + assert [item["id"] for item in _read_jsonl_file(target_path)] == [ + "coding", + "meeting", + ] def test_existing_facet_merge_activity_records(ingest_env): @@ -548,8 +602,16 @@ def test_existing_facet_merge_activity_records(ingest_env): "type": "activity_records", "content": _jsonl_bytes( [ - {"id": "coding_1", "activity": "coding", "active_entities": ["same_entity"]}, - {"id": "coding_2", "activity": "coding", "active_entities": ["source_entity"]}, + { + "id": "coding_1", + "activity": "coding", + "active_entities": ["same_entity"], + }, + { + "id": "coding_2", + "activity": "coding", + "active_entities": ["source_entity"], + }, ] ), } @@ -557,7 +619,9 @@ def test_existing_facet_merge_activity_records(ingest_env): } ] metadata, file_map = _build_request(facets) - response = _post_facets(env["client"], env["key"], env["key_prefix"], metadata, file_map) + response = _post_facets( + env["client"], env["key"], env["key_prefix"], metadata, file_map + ) assert response.status_code == 200 assert response.get_json()["merged"] == 1 @@ -593,7 +657,9 @@ def test_existing_facet_merge_activity_output_skip(ingest_env): } ] metadata, file_map = _build_request(facets) - response = _post_facets(env["client"], env["key"], env["key_prefix"], metadata, file_map) + response = _post_facets( + env["client"], env["key"], env["key_prefix"], metadata, file_map + ) assert response.status_code == 200 assert response.get_json()["skipped"] == 1 @@ -617,7 +683,9 @@ def test_existing_facet_merge_activity_output_copy(ingest_env): } ] metadata, file_map = _build_request(facets) - response = _post_facets(env["client"], env["key"], env["key_prefix"], metadata, file_map) + response = _post_facets( + env["client"], env["key"], env["key_prefix"], metadata, file_map + ) target_file = ( env["root"] @@ -656,7 +724,9 @@ def test_existing_facet_merge_todos(ingest_env): } ] metadata, file_map = _build_request(facets) - response = _post_facets(env["client"], env["key"], env["key_prefix"], metadata, file_map) + response = _post_facets( + env["client"], env["key"], env["key_prefix"], metadata, file_map + ) assert response.status_code == 200 assert response.get_json()["merged"] == 1 @@ -689,7 +759,9 @@ def test_existing_facet_merge_calendar(ingest_env): } ] metadata, file_map = _build_request(facets) - response = _post_facets(env["client"], env["key"], env["key_prefix"], metadata, file_map) + response = _post_facets( + env["client"], env["key"], env["key_prefix"], metadata, file_map + ) assert response.status_code == 200 assert response.get_json()["merged"] == 1 @@ -718,7 +790,9 @@ def test_existing_facet_merge_news_skip(ingest_env): } ] metadata, file_map = _build_request(facets) - response = _post_facets(env["client"], env["key"], env["key_prefix"], metadata, file_map) + response = _post_facets( + env["client"], env["key"], env["key_prefix"], metadata, file_map + ) assert response.status_code == 200 assert response.get_json()["skipped"] == 1 @@ -742,7 +816,9 @@ def test_existing_facet_merge_news_copy(ingest_env): } ] metadata, file_map = _build_request(facets) - response = _post_facets(env["client"], env["key"], env["key_prefix"], metadata, file_map) + response = _post_facets( + env["client"], env["key"], env["key_prefix"], metadata, file_map + ) target_path = env["root"] / "facets" / "work" / "news" / "20260305.md" assert response.status_code == 200 @@ -768,7 +844,9 @@ def test_existing_facet_merge_logs(ingest_env): } ] metadata, file_map = _build_request(facets) - response = _post_facets(env["client"], env["key"], env["key_prefix"], metadata, file_map) + response = _post_facets( + env["client"], env["key"], env["key_prefix"], metadata, file_map + ) assert response.status_code == 200 assert response.get_json()["merged"] == 1 @@ -789,13 +867,21 @@ def test_entity_id_remapping(ingest_env): { "path": "entities/source_entity/observations.jsonl", "type": "entity_observations", - "content": _jsonl_bytes([{"content": "Knows Rust", "observed_at": 1}]), + "content": _jsonl_bytes( + [{"content": "Knows Rust", "observed_at": 1}] + ), }, { "path": "entities/20260305.jsonl", "type": "detected_entities", "content": _jsonl_bytes( - [{"id": "source_entity", "name": "Source Entity", "type": "Person"}] + [ + { + "id": "source_entity", + "name": "Source Entity", + "type": "Person", + } + ] ), }, { @@ -815,7 +901,9 @@ def test_entity_id_remapping(ingest_env): } ] metadata, file_map = _build_request(facets) - response = _post_facets(env["client"], env["key"], env["key_prefix"], metadata, file_map) + response = _post_facets( + env["client"], env["key"], env["key_prefix"], metadata, file_map + ) facet_root = env["root"] / "facets" / "work" assert response.status_code == 200 @@ -823,10 +911,13 @@ def test_entity_id_remapping(ingest_env): assert (facet_root / "entities" / "target_entity" / "entity.json").exists() assert (facet_root / "entities" / "target_entity" / "observations.jsonl").exists() assert not (facet_root / "entities" / "source_entity").exists() - assert _read_jsonl_file(facet_root / "entities" / "20260305.jsonl")[0]["id"] == "target_entity" - assert _read_jsonl_file(facet_root / "activities" / "20260305.jsonl")[0]["active_entities"] == [ - "target_entity" - ] + assert ( + _read_jsonl_file(facet_root / "entities" / "20260305.jsonl")[0]["id"] + == "target_entity" + ) + assert _read_jsonl_file(facet_root / "activities" / "20260305.jsonl")[0][ + "active_entities" + ] == ["target_entity"] def test_unmapped_entity_staging(ingest_env): @@ -845,7 +936,9 @@ def test_unmapped_entity_staging(ingest_env): } ] metadata, file_map = _build_request(facets) - response = _post_facets(env["client"], env["key"], env["key_prefix"], metadata, file_map) + response = _post_facets( + env["client"], env["key"], env["key_prefix"], metadata, file_map + ) assert response.status_code == 200 assert response.get_json() == { @@ -855,7 +948,9 @@ def test_unmapped_entity_staging(ingest_env): "staged": 1, "errors": [], } - staged = _read_staged(env["key_prefix"], "work", "entity_relationship", "entities/unknown/entity.json") + staged = _read_staged( + env["key_prefix"], "work", "entity_relationship", "entities/unknown/entity.json" + ) assert staged["reason"] == "unmapped_entity" assert staged["source_entity_id"] == "unknown" assert staged["source_path"] == "entities/unknown/entity.json" @@ -877,7 +972,9 @@ def test_staged_then_retry(ingest_env): } ] metadata, file_map = _build_request(facets) - first = _post_facets(env["client"], env["key"], env["key_prefix"], metadata, file_map) + first = _post_facets( + env["client"], env["key"], env["key_prefix"], metadata, file_map + ) assert first.status_code == 200 assert first.get_json()["staged"] == 1 @@ -888,7 +985,9 @@ def test_staged_then_retry(ingest_env): state_path.write_text(json.dumps(entity_state), encoding="utf-8") metadata, file_map = _build_request(facets) - second = _post_facets(env["client"], env["key"], env["key_prefix"], metadata, file_map) + second = _post_facets( + env["client"], env["key"], env["key_prefix"], metadata, file_map + ) assert second.status_code == 200 body = second.get_json() @@ -916,7 +1015,9 @@ def test_facet_json_conflict_staging(ingest_env): } ] metadata, file_map = _build_request(facets) - response = _post_facets(env["client"], env["key"], env["key_prefix"], metadata, file_map) + response = _post_facets( + env["client"], env["key"], env["key_prefix"], metadata, file_map + ) assert response.status_code == 200 assert response.get_json()["staged"] == 1 @@ -937,10 +1038,14 @@ def test_idempotent(ingest_env): } ] metadata, file_map = _build_request(facets) - first = _post_facets(env["client"], env["key"], env["key_prefix"], metadata, file_map) + first = _post_facets( + env["client"], env["key"], env["key_prefix"], metadata, file_map + ) metadata, file_map = _build_request(facets) - second = _post_facets(env["client"], env["key"], env["key_prefix"], metadata, file_map) + second = _post_facets( + env["client"], env["key"], env["key_prefix"], metadata, file_map + ) assert first.status_code == 200 assert second.status_code == 200 @@ -971,7 +1076,9 @@ def test_error_isolation(ingest_env): } ] metadata, file_map = _build_request(facets) - response = _post_facets(env["client"], env["key"], env["key_prefix"], metadata, file_map) + response = _post_facets( + env["client"], env["key"], env["key_prefix"], metadata, file_map + ) assert response.status_code == 200 body = response.get_json() @@ -995,13 +1102,13 @@ def test_error_isolation_across_facets(ingest_env): }, { "name": "good", - "files": [ - {"path": "news/20260305.md", "type": "news", "content": b"ok\n"} - ], + "files": [{"path": "news/20260305.md", "type": "news", "content": b"ok\n"}], }, ] metadata, file_map = _build_request(facets) - response = _post_facets(env["client"], env["key"], env["key_prefix"], metadata, file_map) + response = _post_facets( + env["client"], env["key"], env["key_prefix"], metadata, file_map + ) assert response.status_code == 200 body = response.get_json() @@ -1042,7 +1149,9 @@ def test_stats_update(ingest_env): }, ] metadata, file_map = _build_request(facets) - response = _post_facets(env["client"], env["key"], env["key_prefix"], metadata, file_map) + response = _post_facets( + env["client"], env["key"], env["key_prefix"], metadata, file_map + ) source = load_journal_source(env["key"]) assert response.status_code == 200 @@ -1063,7 +1172,9 @@ def test_state_manifest(ingest_env): } ] metadata, file_map = _build_request(facets) - response = _post_facets(env["client"], env["key"], env["key_prefix"], metadata, file_map) + response = _post_facets( + env["client"], env["key"], env["key_prefix"], metadata, file_map + ) assert response.status_code == 200 assert _read_state(env["key_prefix"]) == { @@ -1085,7 +1196,9 @@ def test_decision_log(ingest_env): } ] metadata, file_map = _build_request(facets) - response = _post_facets(env["client"], env["key"], env["key_prefix"], metadata, file_map) + response = _post_facets( + env["client"], env["key"], env["key_prefix"], metadata, file_map + ) assert response.status_code == 200 entries = _read_log(env["key_prefix"]) @@ -1114,7 +1227,9 @@ def test_error_logged_to_decision_log(ingest_env): } ] metadata, file_map = _build_request(facets) - response = _post_facets(env["client"], env["key"], env["key_prefix"], metadata, file_map) + response = _post_facets( + env["client"], env["key"], env["key_prefix"], metadata, file_map + ) assert response.status_code == 200 assert len(response.get_json()["errors"]) == 1 diff --git a/tests/test_import_call.py b/tests/test_import_call.py index 646e13cd7..40f75eb5e 100644 --- a/tests/test_import_call.py +++ b/tests/test_import_call.py @@ -60,7 +60,9 @@ def import_env(tmp_path, monkeypatch): monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(tmp_path)) think.utils._journal_path_cache = None clear_journal_entity_cache() - (tmp_path / "apps" / "import" / "journal_sources").mkdir(parents=True, exist_ok=True) + (tmp_path / "apps" / "import" / "journal_sources").mkdir( + parents=True, exist_ok=True + ) (tmp_path / "config").mkdir(parents=True, exist_ok=True) key = generate_key() @@ -79,7 +81,9 @@ def import_env(tmp_path, monkeypatch): def _write_json(path: Path, data: dict) -> None: path.parent.mkdir(parents=True, exist_ok=True) - path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + path.write_text( + json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" + ) def _read_json(path: Path) -> dict: @@ -102,7 +106,9 @@ def _write_entity_state(key_prefix: str, state: dict) -> None: def test_list_staged_empty_state(import_env): - result = runner.invoke(call_app, ["import", "list-staged", "--source", "test-source"]) + result = runner.invoke( + call_app, ["import", "list-staged", "--source", "test-source"] + ) assert result.exit_code == 0 assert result.stdout.strip() == "" @@ -118,8 +124,14 @@ def test_list_staged_with_staged_entities(import_env): _write_json( staged_path, { - "source_entity": {"id": "test-entity", "name": "Test Entity", "type": "Tool"}, - "match_candidates": [{"id": "target-id", "name": "Target Entity", "tier": 8}], + "source_entity": { + "id": "test-entity", + "name": "Test Entity", + "type": "Tool", + }, + "match_candidates": [ + {"id": "target-id", "name": "Target Entity", "tier": 8} + ], "reason": "low_confidence_match", "staged_at": "2026-04-14T00:00:00+00:00", }, @@ -137,8 +149,14 @@ def test_list_staged_with_staged_entities(import_env): "area": "entities", "source_id": "test-entity", "reason": "low_confidence_match", - "source_entity": {"id": "test-entity", "name": "Test Entity", "type": "Tool"}, - "match_candidates": [{"id": "target-id", "name": "Target Entity", "tier": 8}], + "source_entity": { + "id": "test-entity", + "name": "Test Entity", + "type": "Tool", + }, + "match_candidates": [ + {"id": "target-id", "name": "Target Entity", "tier": 8} + ], "staged_at": "2026-04-14T00:00:00+00:00", } ] @@ -179,8 +197,15 @@ def test_list_staged_with_config_diff(import_env): def test_list_staged_with_staged_facets(import_env): - staged_file = "personal/entity_relationship/entities__source_entity__entity.json.staged.json" - staged_path = get_state_directory(import_env["key_prefix"]) / "facets" / "staged" / staged_file + staged_file = ( + "personal/entity_relationship/entities__source_entity__entity.json.staged.json" + ) + staged_path = ( + get_state_directory(import_env["key_prefix"]) + / "facets" + / "staged" + / staged_file + ) _write_json( staged_path, { @@ -188,7 +213,9 @@ def test_list_staged_with_staged_facets(import_env): "source_entity_id": "source_entity", "explanation": "Entity 'source_entity' has no mapping in entities/state.json id_map", "source_path": "entities/source_entity/entity.json", - "source_data": json.dumps({"entity_id": "source_entity"}, ensure_ascii=False, indent=2) + "source_data": json.dumps( + {"entity_id": "source_entity"}, ensure_ascii=False, indent=2 + ) + "\n", "staged_at": "2026-04-14T00:00:00+00:00", }, @@ -211,7 +238,9 @@ def test_list_staged_with_staged_facets(import_env): "source_entity_id": "source_entity", "explanation": "Entity 'source_entity' has no mapping in entities/state.json id_map", "source_path": "entities/source_entity/entity.json", - "source_data": json.dumps({"entity_id": "source_entity"}, ensure_ascii=False, indent=2) + "source_data": json.dumps( + {"entity_id": "source_entity"}, ensure_ascii=False, indent=2 + ) + "\n", "staged_at": "2026-04-14T00:00:00+00:00", } @@ -246,7 +275,9 @@ def test_resolve_entity_merge(import_env): "emails": ["alice@new.com"], "created_at": 1000, }, - "match_candidates": [{"id": "target-id", "name": "Alice Johnson", "tier": 8}], + "match_candidates": [ + {"id": "target-id", "name": "Alice Johnson", "tier": 8} + ], "reason": "low_confidence_match", "staged_at": "2026-04-14T00:00:00+00:00", }, @@ -274,16 +305,22 @@ def test_resolve_entity_merge(import_env): assert merged["emails"] == ["alice@old.com", "alice@new.com"] assert merged["created_at"] == 1000 - state = _read_json(get_state_directory(import_env["key_prefix"]) / "entities" / "state.json") + state = _read_json( + get_state_directory(import_env["key_prefix"]) / "entities" / "state.json" + ) assert state["id_map"]["test-entity"] == "target-id" - log_entries = _read_log(get_state_directory(import_env["key_prefix"]) / "entities" / "log.jsonl") + log_entries = _read_log( + get_state_directory(import_env["key_prefix"]) / "entities" / "log.jsonl" + ) assert log_entries[-1]["action"] == "resolved_merge" assert log_entries[-1]["resolved_by"] == "talent" def test_resolve_entity_create(import_env): - save_journal_entity({"id": "test-entity", "name": "Occupied Entity", "type": "Tool"}) + save_journal_entity( + {"id": "test-entity", "name": "Occupied Entity", "type": "Tool"} + ) staged_path = ( get_state_directory(import_env["key_prefix"]) / "entities" @@ -293,8 +330,14 @@ def test_resolve_entity_create(import_env): _write_json( staged_path, { - "source_entity": {"id": "test-entity", "name": "Fresh Entity", "type": "Tool"}, - "match_candidates": [{"id": "test-entity", "name": "Occupied Entity", "tier": None}], + "source_entity": { + "id": "test-entity", + "name": "Fresh Entity", + "type": "Tool", + }, + "match_candidates": [ + {"id": "test-entity", "name": "Occupied Entity", "tier": None} + ], "reason": "id_collision", "staged_at": "2026-04-14T00:00:00+00:00", }, @@ -302,7 +345,14 @@ def test_resolve_entity_create(import_env): result = runner.invoke( call_app, - ["import", "resolve-entity", "test-entity", "create", "--source", "test-source"], + [ + "import", + "resolve-entity", + "test-entity", + "create", + "--source", + "test-source", + ], ) assert result.exit_code == 0 @@ -311,7 +361,9 @@ def test_resolve_entity_create(import_env): assert created is not None assert created["name"] == "Fresh Entity" - state = _read_json(get_state_directory(import_env["key_prefix"]) / "entities" / "state.json") + state = _read_json( + get_state_directory(import_env["key_prefix"]) / "entities" / "state.json" + ) assert state["id_map"]["test-entity"] == "fresh_entity" @@ -347,7 +399,14 @@ def test_resolve_entity_create_principal_conflict(import_env): result = runner.invoke( call_app, - ["import", "resolve-entity", "new-principal", "create", "--source", "test-source"], + [ + "import", + "resolve-entity", + "new-principal", + "create", + "--source", + "test-source", + ], ) assert result.exit_code == 0 @@ -366,7 +425,11 @@ def test_resolve_entity_skip(import_env): _write_json( staged_path, { - "source_entity": {"id": "test-entity", "name": "Skip Entity", "type": "Tool"}, + "source_entity": { + "id": "test-entity", + "name": "Skip Entity", + "type": "Tool", + }, "match_candidates": [], "reason": "principal_conflict", "staged_at": "2026-04-14T00:00:00+00:00", @@ -382,7 +445,9 @@ def test_resolve_entity_skip(import_env): assert not staged_path.exists() assert load_journal_entity("test-entity") is None - log_entries = _read_log(get_state_directory(import_env["key_prefix"]) / "entities" / "log.jsonl") + log_entries = _read_log( + get_state_directory(import_env["key_prefix"]) / "entities" / "log.jsonl" + ) assert log_entries[-1]["action"] == "resolved_skip" assert log_entries[-1]["resolved_by"] == "talent" @@ -403,11 +468,21 @@ def test_resolve_config_apply(import_env): get_state_directory(import_env["key_prefix"]) / "config" / "source_config.json", {"identity": {"name": "Remote User"}}, ) - _write_json(import_env["root"] / "config" / "journal.json", {"identity": {"name": "Local User"}}) + _write_json( + import_env["root"] / "config" / "journal.json", + {"identity": {"name": "Local User"}}, + ) result = runner.invoke( call_app, - ["import", "resolve-config", "identity.name", "apply", "--source", "test-source"], + [ + "import", + "resolve-config", + "identity.name", + "apply", + "--source", + "test-source", + ], ) assert result.exit_code == 0 @@ -415,7 +490,9 @@ def test_resolve_config_apply(import_env): assert journal_config["identity"]["name"] == "Remote User" assert not diff_path.exists() - log_entries = _read_log(get_state_directory(import_env["key_prefix"]) / "config" / "log.jsonl") + log_entries = _read_log( + get_state_directory(import_env["key_prefix"]) / "config" / "log.jsonl" + ) assert log_entries[-1]["action"] == "config_field_applied" assert log_entries[-1]["resolved_by"] == "talent" @@ -436,11 +513,20 @@ def test_resolve_config_keep(import_env): get_state_directory(import_env["key_prefix"]) / "config" / "source_config.json", {"retention": {"days": 30}}, ) - _write_json(import_env["root"] / "config" / "journal.json", {"retention": {"days": 90}}) + _write_json( + import_env["root"] / "config" / "journal.json", {"retention": {"days": 90}} + ) result = runner.invoke( call_app, - ["import", "resolve-config", "retention.days", "keep", "--source", "test-source"], + [ + "import", + "resolve-config", + "retention.days", + "keep", + "--source", + "test-source", + ], ) assert result.exit_code == 0 @@ -504,8 +590,15 @@ def test_resolve_facet_apply_unmapped_entity(import_env): import_env["key_prefix"], {"id_map": {"source_entity": "target_entity"}, "received": {}}, ) - staged_file = "personal/entity_relationship/entities__source_entity__entity.json.staged.json" - staged_path = get_state_directory(import_env["key_prefix"]) / "facets" / "staged" / staged_file + staged_file = ( + "personal/entity_relationship/entities__source_entity__entity.json.staged.json" + ) + staged_path = ( + get_state_directory(import_env["key_prefix"]) + / "facets" + / "staged" + / staged_file + ) _write_json( staged_path, { @@ -535,7 +628,9 @@ def test_resolve_facet_apply_unmapped_entity(import_env): assert relationship["entity_id"] == "target_entity" assert relationship["description"] == "imported relationship" - log_entries = _read_log(get_state_directory(import_env["key_prefix"]) / "facets" / "log.jsonl") + log_entries = _read_log( + get_state_directory(import_env["key_prefix"]) / "facets" / "log.jsonl" + ) assert log_entries[-1]["action"] == "resolved_apply" assert log_entries[-1]["resolved_by"] == "talent" @@ -544,7 +639,12 @@ def test_resolve_facet_apply_facet_json_conflict(import_env): target_path = import_env["root"] / "facets" / "personal" / "facet.json" _write_json(target_path, {"title": "Local"}) staged_file = "personal/facet_json/facet.json.staged.json" - staged_path = get_state_directory(import_env["key_prefix"]) / "facets" / "staged" / staged_file + staged_path = ( + get_state_directory(import_env["key_prefix"]) + / "facets" + / "staged" + / staged_file + ) _write_json( staged_path, { @@ -564,15 +664,24 @@ def test_resolve_facet_apply_facet_json_conflict(import_env): assert not staged_path.exists() assert _read_json(target_path) == {"title": "Remote"} - log_entries = _read_log(get_state_directory(import_env["key_prefix"]) / "facets" / "log.jsonl") + log_entries = _read_log( + get_state_directory(import_env["key_prefix"]) / "facets" / "log.jsonl" + ) assert log_entries[-1]["action"] == "resolved_apply" assert log_entries[-1]["item_id"] == "personal/facet.json" assert log_entries[-1]["resolved_by"] == "talent" def test_resolve_facet_unmapped_entity_fails_without_mapping(import_env): - staged_file = "personal/entity_relationship/entities__source_entity__entity.json.staged.json" - staged_path = get_state_directory(import_env["key_prefix"]) / "facets" / "staged" / staged_file + staged_file = ( + "personal/entity_relationship/entities__source_entity__entity.json.staged.json" + ) + staged_path = ( + get_state_directory(import_env["key_prefix"]) + / "facets" + / "staged" + / staged_file + ) _write_json( staged_path, { @@ -580,7 +689,9 @@ def test_resolve_facet_unmapped_entity_fails_without_mapping(import_env): "source_entity_id": "source_entity", "explanation": "Entity 'source_entity' has no mapping in entities/state.json id_map", "source_path": "entities/source_entity/entity.json", - "source_data": json.dumps({"entity_id": "source_entity"}, ensure_ascii=False, indent=2) + "source_data": json.dumps( + {"entity_id": "source_entity"}, ensure_ascii=False, indent=2 + ) + "\n", "staged_at": "2026-04-14T00:00:00+00:00", }, @@ -592,13 +703,23 @@ def test_resolve_facet_unmapped_entity_fails_without_mapping(import_env): ) assert result.exit_code == 1 - assert "Entity source_entity has no mapping yet. Run entity review first." in result.stderr + assert ( + "Entity source_entity has no mapping yet. Run entity review first." + in result.stderr + ) assert staged_path.exists() def test_resolve_facet_skip(import_env): - staged_file = "personal/entity_relationship/entities__source_entity__entity.json.staged.json" - staged_path = get_state_directory(import_env["key_prefix"]) / "facets" / "staged" / staged_file + staged_file = ( + "personal/entity_relationship/entities__source_entity__entity.json.staged.json" + ) + staged_path = ( + get_state_directory(import_env["key_prefix"]) + / "facets" + / "staged" + / staged_file + ) _write_json( staged_path, { @@ -606,7 +727,9 @@ def test_resolve_facet_skip(import_env): "source_entity_id": "source_entity", "explanation": "Entity 'source_entity' has no mapping in entities/state.json id_map", "source_path": "entities/source_entity/entity.json", - "source_data": json.dumps({"entity_id": "source_entity"}, ensure_ascii=False, indent=2) + "source_data": json.dumps( + {"entity_id": "source_entity"}, ensure_ascii=False, indent=2 + ) + "\n", "staged_at": "2026-04-14T00:00:00+00:00", }, diff --git a/tests/test_journal_merge.py b/tests/test_journal_merge.py index 648919f2e..f3016f00e 100644 --- a/tests/test_journal_merge.py +++ b/tests/test_journal_merge.py @@ -240,9 +240,7 @@ def test_entity_id_collision(merge_journals_fixture, monkeypatch): paths["target"] / "entities" / "alice_johnson_2" / "entity.json" ).exists() artifact_root = _find_merge_artifact_root(paths["target"]) - staged = _read_json( - artifact_root / "staging" / "alice_johnson" / "entity.json" - ) + staged = _read_json(artifact_root / "staging" / "alice_johnson" / "entity.json") assert staged["id"] == "alice_johnson" assert staged["name"] == "Alice Cooper" assert "staged" in result.output @@ -664,7 +662,9 @@ def test_decision_log_entity_merge_snapshots(merge_journals_fixture, monkeypatch assert result.exit_code == 0 artifact_root = _find_merge_artifact_root(paths["target"]) entries = _read_jsonl(artifact_root / "decisions.jsonl") - entity_merged = next(entry for entry in entries if entry["action"] == "entity_merged") + entity_merged = next( + entry for entry in entries if entry["action"] == "entity_merged" + ) assert "source" in entity_merged assert "target" in entity_merged assert "fields_changed" in entity_merged diff --git a/tests/test_journal_stats.py b/tests/test_journal_stats.py index 3ccd193e3..f583dc85f 100644 --- a/tests/test_journal_stats.py +++ b/tests/test_journal_stats.py @@ -177,8 +177,7 @@ def test_token_usage(tmp_path, monkeypatch): assert "total_transcript_duration" in data["totals"] assert "total_percept_duration" in data["totals"] assert ( - data["tokens"]["by_day"]["20240101"]["gemini-2.5-flash"]["total_tokens"] - == 495 + data["tokens"]["by_day"]["20240101"]["gemini-2.5-flash"]["total_tokens"] == 495 ) @@ -235,8 +234,7 @@ def test_facet_event_mtime_invalidates_cache(tmp_path, monkeypatch): ts_dir = day / "default" / "123456_300" ts_dir.mkdir(parents=True) (ts_dir / "audio.jsonl").write_text( - '{"raw": "raw.flac"}\n' - '{"start": "10:00:00", "text": "hello"}\n' + '{"raw": "raw.flac"}\n{"start": "10:00:00", "text": "hello"}\n' ) # Create facet event file diff --git a/tests/test_ollama.py b/tests/test_ollama.py index 77d9af51b..196873989 100644 --- a/tests/test_ollama.py +++ b/tests/test_ollama.py @@ -680,8 +680,10 @@ class TestRunCogitate: self.run = AsyncMock(return_value="test result") MockCLIRunner.last_instance = self - with patch("shutil.which", return_value="/usr/bin/opencode"), \ - patch("think.providers.ollama.CLIRunner", MockCLIRunner): + with ( + patch("shutil.which", return_value="/usr/bin/opencode"), + patch("think.providers.ollama.CLIRunner", MockCLIRunner), + ): events = [] asyncio.run( provider.run_cogitate( @@ -717,8 +719,10 @@ class TestRunCogitate: self.run = AsyncMock(return_value="ok") MockCLIRunner.last_instance = self - with patch("shutil.which", return_value="/usr/bin/opencode"), \ - patch("think.providers.ollama.CLIRunner", MockCLIRunner): + with ( + patch("shutil.which", return_value="/usr/bin/opencode"), + patch("think.providers.ollama.CLIRunner", MockCLIRunner), + ): asyncio.run( provider.run_cogitate( {"prompt": "test", "model": "ollama-local/qwen3.5:35b-a3b-bf16"}, @@ -743,8 +747,10 @@ class TestRunCogitate: self.run = AsyncMock(return_value="ok") MockCLIRunner.last_instance = self - with patch("shutil.which", return_value="/usr/bin/opencode"), \ - patch("think.providers.ollama.CLIRunner", MockCLIRunner): + with ( + patch("shutil.which", return_value="/usr/bin/opencode"), + patch("think.providers.ollama.CLIRunner", MockCLIRunner), + ): asyncio.run( provider.run_cogitate( { @@ -774,8 +780,10 @@ class TestRunCogitate: self.run = AsyncMock(return_value="ok") MockCLIRunner.last_instance = self - with patch("shutil.which", return_value="/usr/bin/opencode"), \ - patch("think.providers.ollama.CLIRunner", MockCLIRunner): + with ( + patch("shutil.which", return_value="/usr/bin/opencode"), + patch("think.providers.ollama.CLIRunner", MockCLIRunner), + ): asyncio.run( provider.run_cogitate( { @@ -802,8 +810,10 @@ class TestRunCogitate: self.run = AsyncMock(side_effect=RuntimeError("CLI not found")) events = [] - with patch("shutil.which", return_value="/usr/bin/opencode"), \ - patch("think.providers.ollama.CLIRunner", MockCLIRunner): + with ( + patch("shutil.which", return_value="/usr/bin/opencode"), + patch("think.providers.ollama.CLIRunner", MockCLIRunner), + ): with pytest.raises(RuntimeError, match="CLI not found"): asyncio.run( provider.run_cogitate( diff --git a/tests/test_password_cli.py b/tests/test_password_cli.py index cf73106a7..d13c7667a 100644 --- a/tests/test_password_cli.py +++ b/tests/test_password_cli.py @@ -20,7 +20,9 @@ def _read_config(journal_dir): def _mock_getpass(monkeypatch, *responses): """Mock getpass.getpass to return successive responses.""" it = iter(responses) - monkeypatch.setattr("think.password_cli.getpass.getpass", lambda prompt="": next(it)) + monkeypatch.setattr( + "think.password_cli.getpass.getpass", lambda prompt="": next(it) + ) class TestSetPassword: diff --git a/tests/test_retention_config_cli.py b/tests/test_retention_config_cli.py index 1b9bd6698..a12e3fef4 100644 --- a/tests/test_retention_config_cli.py +++ b/tests/test_retention_config_cli.py @@ -133,7 +133,16 @@ def test_invalid_mode(journal_env): def test_clear_with_mode_rejected(journal_env): result = runner.invoke( call_app, - ["journal", "retention", "config", "--stream", "plaud", "--clear", "--mode", "keep"], + [ + "journal", + "retention", + "config", + "--stream", + "plaud", + "--clear", + "--mode", + "keep", + ], ) assert result.exit_code == 1 diff --git a/tests/test_segment.py b/tests/test_segment.py index 23ca5993f..173d829a7 100644 --- a/tests/test_segment.py +++ b/tests/test_segment.py @@ -88,14 +88,24 @@ def test_list_stream_filter(tmp_path, monkeypatch, capsys): "20240101", "default", "090000_300", - stream_json={"stream": "default", "prev_day": None, "prev_segment": None, "seq": 1}, + stream_json={ + "stream": "default", + "prev_day": None, + "prev_segment": None, + "seq": 1, + }, ) _make_segment( tmp_path, "20240101", "custom", "100000_300", - stream_json={"stream": "custom", "prev_day": None, "prev_segment": None, "seq": 1}, + stream_json={ + "stream": "custom", + "prev_day": None, + "prev_segment": None, + "seq": 1, + }, ) args = argparse.Namespace( @@ -115,7 +125,12 @@ def test_list_json(tmp_path, monkeypatch, capsys): "20240101", "default", "090000_300", - stream_json={"stream": "default", "prev_day": None, "prev_segment": None, "seq": 1}, + stream_json={ + "stream": "default", + "prev_day": None, + "prev_segment": None, + "seq": 1, + }, agents=["audio.md"], ) @@ -150,7 +165,12 @@ def test_inspect_basic(tmp_path, monkeypatch, capsys): "20240101", "default", "090000_300", - stream_json={"stream": "default", "prev_day": None, "prev_segment": None, "seq": 1}, + stream_json={ + "stream": "default", + "prev_day": None, + "prev_segment": None, + "seq": 1, + }, agents=["audio.md"], ) @@ -200,7 +220,12 @@ def test_inspect_json(tmp_path, monkeypatch, capsys): "20240101", "default", "090000_300", - stream_json={"stream": "default", "prev_day": None, "prev_segment": None, "seq": 1}, + stream_json={ + "stream": "default", + "prev_day": None, + "prev_segment": None, + "seq": 1, + }, agents=["audio.md"], ) @@ -223,7 +248,12 @@ def test_inspect_chain(tmp_path, monkeypatch, capsys): "20240101", "default", "090000_300", - stream_json={"stream": "default", "prev_day": None, "prev_segment": None, "seq": 1}, + stream_json={ + "stream": "default", + "prev_day": None, + "prev_segment": None, + "seq": 1, + }, ) _make_segment( tmp_path, @@ -267,7 +297,12 @@ def test_verify_all_pass(tmp_path, monkeypatch, capsys): "20240101", "default", "090000_300", - stream_json={"stream": "default", "prev_day": None, "prev_segment": None, "seq": 1}, + stream_json={ + "stream": "default", + "prev_day": None, + "prev_segment": None, + "seq": 1, + }, screen=False, ) streams_dir = tmp_path / "streams" @@ -322,7 +357,12 @@ def test_verify_missing_content(tmp_path, monkeypatch, capsys): "20240101", "default", "090000_300", - stream_json={"stream": "default", "prev_day": None, "prev_segment": None, "seq": 1}, + stream_json={ + "stream": "default", + "prev_day": None, + "prev_segment": None, + "seq": 1, + }, audio=False, screen=False, ) @@ -377,7 +417,10 @@ def test_verify_broken_backward_chain(tmp_path, monkeypatch, capsys): out = capsys.readouterr().out assert excinfo.value.code == 1 - assert "FAIL backward chain: missing previous segment 20240101/default/090000_300" in out + assert ( + "FAIL backward chain: missing previous segment 20240101/default/090000_300" + in out + ) def test_verify_day_mode(tmp_path, monkeypatch, capsys): @@ -387,7 +430,12 @@ def test_verify_day_mode(tmp_path, monkeypatch, capsys): "20240101", "default", "090000_300", - stream_json={"stream": "default", "prev_day": None, "prev_segment": None, "seq": 1}, + stream_json={ + "stream": "default", + "prev_day": None, + "prev_segment": None, + "seq": 1, + }, ) _make_segment( tmp_path, @@ -427,7 +475,12 @@ def test_verify_json(tmp_path, monkeypatch, capsys): "20240101", "default", "090000_300", - stream_json={"stream": "default", "prev_day": None, "prev_segment": None, "seq": 1}, + stream_json={ + "stream": "default", + "prev_day": None, + "prev_segment": None, + "seq": 1, + }, ) streams_dir = tmp_path / "streams" streams_dir.mkdir() @@ -485,7 +538,12 @@ def test_move_basic(tmp_path, monkeypatch, capsys): "20240101", "default", "090000_300", - stream_json={"stream": "default", "prev_day": None, "prev_segment": None, "seq": 1}, + stream_json={ + "stream": "default", + "prev_day": None, + "prev_segment": None, + "seq": 1, + }, ) streams_dir = tmp_path / "streams" streams_dir.mkdir() @@ -521,7 +579,12 @@ def test_move_with_to_time(tmp_path, monkeypatch, capsys): "20240101", "default", "090000_300", - stream_json={"stream": "default", "prev_day": None, "prev_segment": None, "seq": 1}, + stream_json={ + "stream": "default", + "prev_day": None, + "prev_segment": None, + "seq": 1, + }, ) streams_dir = tmp_path / "streams" streams_dir.mkdir() @@ -550,7 +613,12 @@ def test_move_dry_run(tmp_path, monkeypatch, capsys): "20240101", "default", "090000_300", - stream_json={"stream": "default", "prev_day": None, "prev_segment": None, "seq": 1}, + stream_json={ + "stream": "default", + "prev_day": None, + "prev_segment": None, + "seq": 1, + }, ) streams_dir = tmp_path / "streams" streams_dir.mkdir() @@ -581,14 +649,24 @@ def test_move_collision_no_to_time(tmp_path, monkeypatch, capsys): "20240101", "default", "090000_300", - stream_json={"stream": "default", "prev_day": None, "prev_segment": None, "seq": 1}, + stream_json={ + "stream": "default", + "prev_day": None, + "prev_segment": None, + "seq": 1, + }, ) _make_segment( tmp_path, "20240115", "default", "090000_300", - stream_json={"stream": "default", "prev_day": None, "prev_segment": None, "seq": 1}, + stream_json={ + "stream": "default", + "prev_day": None, + "prev_segment": None, + "seq": 1, + }, ) streams_dir = tmp_path / "streams" streams_dir.mkdir() @@ -619,7 +697,12 @@ def test_move_no_events_jsonl(tmp_path, monkeypatch, capsys): "20240101", "default", "090000_300", - stream_json={"stream": "default", "prev_day": None, "prev_segment": None, "seq": 1}, + stream_json={ + "stream": "default", + "prev_day": None, + "prev_segment": None, + "seq": 1, + }, audio=False, screen=False, ) @@ -651,7 +734,12 @@ def test_move_patches_successor(tmp_path, monkeypatch, capsys): "20240101", "default", "090000_300", - stream_json={"stream": "default", "prev_day": None, "prev_segment": None, "seq": 1}, + stream_json={ + "stream": "default", + "prev_day": None, + "prev_segment": None, + "seq": 1, + }, ) _make_segment( tmp_path, @@ -697,7 +785,12 @@ def test_move_stream_tail(tmp_path, monkeypatch, capsys): "20240101", "default", "090000_300", - stream_json={"stream": "default", "prev_day": None, "prev_segment": None, "seq": 1}, + stream_json={ + "stream": "default", + "prev_day": None, + "prev_segment": None, + "seq": 1, + }, ) _make_segment( tmp_path, @@ -739,15 +832,27 @@ def test_move_rewrites_events_jsonl(tmp_path, monkeypatch, capsys): "20240101", "default", "090000_300", - stream_json={"stream": "default", "prev_day": None, "prev_segment": None, "seq": 1}, + stream_json={ + "stream": "default", + "prev_day": None, + "prev_segment": None, + "seq": 1, + }, audio=False, screen=False, ) events = [ - {"tract": "observe", "event": "start", "day": "20240101", "segment": "090000_300"}, + { + "tract": "observe", + "event": "start", + "day": "20240101", + "segment": "090000_300", + }, {"tract": "dream", "event": "done", "day": "20240101", "segment": "090000_300"}, ] - (seg_dir / "events.jsonl").write_text("\n".join(json.dumps(e) for e in events) + "\n") + (seg_dir / "events.jsonl").write_text( + "\n".join(json.dumps(e) for e in events) + "\n" + ) streams_dir = tmp_path / "streams" streams_dir.mkdir() (streams_dir / "default.json").write_text( @@ -782,7 +887,12 @@ def test_move_touches_health_markers(tmp_path, monkeypatch, capsys): "20240101", "default", "090000_300", - stream_json={"stream": "default", "prev_day": None, "prev_segment": None, "seq": 1}, + stream_json={ + "stream": "default", + "prev_day": None, + "prev_segment": None, + "seq": 1, + }, ) streams_dir = tmp_path / "streams" streams_dir.mkdir() @@ -811,7 +921,12 @@ def test_move_same_location_refused(tmp_path, monkeypatch, capsys): "20240101", "default", "090000_300", - stream_json={"stream": "default", "prev_day": None, "prev_segment": None, "seq": 1}, + stream_json={ + "stream": "default", + "prev_day": None, + "prev_segment": None, + "seq": 1, + }, ) args = argparse.Namespace( @@ -835,7 +950,12 @@ def test_move_invalid_to_time(tmp_path, monkeypatch, capsys): "20240101", "default", "090000_300", - stream_json={"stream": "default", "prev_day": None, "prev_segment": None, "seq": 1}, + stream_json={ + "stream": "default", + "prev_day": None, + "prev_segment": None, + "seq": 1, + }, ) args = argparse.Namespace( @@ -859,7 +979,12 @@ def test_move_invalid_to_day(tmp_path, monkeypatch, capsys): "20240101", "default", "090000_300", - stream_json={"stream": "default", "prev_day": None, "prev_segment": None, "seq": 1}, + stream_json={ + "stream": "default", + "prev_day": None, + "prev_segment": None, + "seq": 1, + }, ) args = argparse.Namespace( diff --git a/tests/test_segment_ingest.py b/tests/test_segment_ingest.py index befa5b3c2..d0371d6dd 100644 --- a/tests/test_segment_ingest.py +++ b/tests/test_segment_ingest.py @@ -461,7 +461,9 @@ def test_ingest_skip_ignores_extra_existing_files(ingest_env): assert (segment_dir / "extra.txt").read_bytes() == b"keep me" state_data = _read_state(env["key_prefix"]) assert "laptop/143022_300" in state_data["20260413"] - assert state_data["20260413"]["laptop/143022_300"]["files"][0]["name"] == "audio.flac" + assert ( + state_data["20260413"]["laptop/143022_300"]["files"][0]["name"] == "audio.flac" + ) def test_ingest_stats_update(ingest_env): @@ -548,11 +550,7 @@ def test_ingest_default_stream_segment(ingest_env): state_data = _read_state(env["key_prefix"]) assert "_default/143022_300" in state_data["20260413"] assert ( - env["root"] - / "20260413" - / "_default" - / "143022_300" - / "transcript.jsonl" + env["root"] / "20260413" / "_default" / "143022_300" / "transcript.jsonl" ).read_bytes() == b'{"text":"default"}\n' diff --git a/tests/test_sol.py b/tests/test_sol.py index 6a2624e9f..4296bfe32 100644 --- a/tests/test_sol.py +++ b/tests/test_sol.py @@ -258,6 +258,7 @@ class TestMain: assert "--day" in captured_argv assert "20250101" in captured_argv + class TestCommandRegistry: """Tests for command registry completeness.""" diff --git a/tests/test_stats_contract.py b/tests/test_stats_contract.py index c62afb235..da27774ed 100644 --- a/tests/test_stats_contract.py +++ b/tests/test_stats_contract.py @@ -150,16 +150,18 @@ def test_contract_fields_exist_in_output(tmp_path, monkeypatch): output = _scan_output(journal, stats_mod) for python_path, _ in CONTRACT_FIELDS: - assert _resolve_path(output, python_path), f"{python_path} missing from stats output" + assert _resolve_path(output, python_path), ( + f"{python_path} missing from stats output" + ) def test_contract_fields_referenced_in_js(): js_source = JS_PATH.read_text() for _, js_ref in CONTRACT_FIELDS: - assert ( - js_ref in js_source - ), f"{js_ref} not found in dashboard.js — contract field may be stale" + assert js_ref in js_source, ( + f"{js_ref} not found in dashboard.js — contract field may be stale" + ) def test_all_day_fields_have_nonzero_values(tmp_path, monkeypatch): diff --git a/tests/test_stats_schema.py b/tests/test_stats_schema.py index ee91c7cb1..cbbbe9353 100644 --- a/tests/test_stats_schema.py +++ b/tests/test_stats_schema.py @@ -18,8 +18,7 @@ def test_validate_passes_on_valid_output(tmp_path, monkeypatch): ts_dir = day / "default" / "123456_300" ts_dir.mkdir(parents=True) (ts_dir / "audio.jsonl").write_text( - '{"raw": "raw.flac"}\n' - '{"start": "10:00:00", "text": "hello"}\n' + '{"raw": "raw.flac"}\n{"start": "10:00:00", "text": "hello"}\n' ) monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) @@ -87,12 +86,10 @@ def test_day_fields_present_in_scan_day(tmp_path, monkeypatch): ts_dir = day / "default" / "123456_300" ts_dir.mkdir(parents=True) (ts_dir / "audio.jsonl").write_text( - '{"raw": "raw.flac"}\n' - '{"start": "10:00:00", "text": "hello"}\n' + '{"raw": "raw.flac"}\n{"start": "10:00:00", "text": "hello"}\n' ) (ts_dir / "screen.jsonl").write_text( - '{"header": true}\n' - '{"frame_id": 1, "timestamp": "10:00:00"}\n' + '{"header": true}\n{"frame_id": 1, "timestamp": "10:00:00"}\n' ) monkeypatch.setenv("_SOLSTONE_JOURNAL_OVERRIDE", str(journal)) @@ -101,4 +98,6 @@ def test_day_fields_present_in_scan_day(tmp_path, monkeypatch): stats = day_data["stats"] for field in schema_mod.DAY_FIELDS: - assert field in stats, f"DAY_FIELDS field '{field}' missing from scan_day output" + assert field in stats, ( + f"DAY_FIELDS field '{field}' missing from scan_day output" + ) diff --git a/tests/test_system_status.py b/tests/test_system_status.py index 4e455f364..47a0b5baa 100644 --- a/tests/test_system_status.py +++ b/tests/test_system_status.py @@ -88,7 +88,12 @@ class TestSystemStatusEndpoint: def test_revoked_observers_excluded(self, client): now = int(time.time() * 1000) observers = [ - {"name": "phone", "last_seen": now - 5000, "enabled": True, "revoked": True}, + { + "name": "phone", + "last_seen": now - 5000, + "enabled": True, + "revoked": True, + }, ] with patch.object(system_mod, "list_observers", return_value=observers): data = client.get("/api/system/status").get_json() @@ -121,7 +126,9 @@ class TestSystemStatusEndpoint: def test_version_with_update_available(self, client): with ( patch.object(system_mod, "list_observers", return_value=[]), - patch.object(system_mod, "_check_latest_version", return_value={"latest": "99.0.0"}), + patch.object( + system_mod, "_check_latest_version", return_value={"latest": "99.0.0"} + ), patch.object(system_mod, "collect_version", return_value="0.1.0"), ): data = client.get("/api/system/status").get_json() @@ -134,7 +141,9 @@ class TestCaptureHealthDerivation: """Unit tests for _get_capture_health logic.""" def test_no_last_seen_is_offline(self): - with patch.object(system_mod, "list_observers", return_value=[{"name": "x", "enabled": True}]): + with patch.object( + system_mod, "list_observers", return_value=[{"name": "x", "enabled": True}] + ): result = system_mod._get_capture_health() assert result["observers"][0]["status"] == "offline" diff --git a/tests/test_transfer.py b/tests/test_transfer.py index 9475975e4..c257e53c3 100644 --- a/tests/test_transfer.py +++ b/tests/test_transfer.py @@ -572,7 +572,9 @@ class TestTransferSend: ) with patch("observe.transfer.requests.Session", return_value=mock_session): - send_segments("https://example.com", "test-key", ["20250103"], dry_run=False) + send_segments( + "https://example.com", "test-key", ["20250103"], dry_run=False + ) assert mock_session.post.call_count == 0 output = capsys.readouterr().out @@ -591,7 +593,9 @@ class TestTransferSend: ) with patch("observe.transfer.requests.Session", return_value=mock_session): - send_segments("https://example.com", "test-key", ["20250103"], dry_run=False) + send_segments( + "https://example.com", "test-key", ["20250103"], dry_run=False + ) assert mock_session.post.call_count == 1 post_kwargs = mock_session.post.call_args.kwargs @@ -600,8 +604,9 @@ class TestTransferSend: assert json.loads(post_kwargs["data"]["meta"]) == {"stream": "default"} # Auth is set on the session, not per-request assert mock_session.headers["Authorization"] == "Bearer test-key" - assert "Transfer complete: 1 sent, 0 skipped, 0 failed, 100 bytes transferred" in ( - capsys.readouterr().out + assert ( + "Transfer complete: 1 sent, 0 skipped, 0 failed, 100 bytes transferred" + in (capsys.readouterr().out) ) def test_send_retry_on_5xx(self, tmp_path, monkeypatch, capsys): @@ -621,11 +626,14 @@ class TestTransferSend: patch("observe.transfer.requests.Session", return_value=mock_session), patch("observe.transfer.time.sleep"), ): - send_segments("https://example.com", "test-key", ["20250103"], dry_run=False) + send_segments( + "https://example.com", "test-key", ["20250103"], dry_run=False + ) assert mock_session.post.call_count == 3 - assert "Transfer complete: 1 sent, 0 skipped, 0 failed, 100 bytes transferred" in ( - capsys.readouterr().out + assert ( + "Transfer complete: 1 sent, 0 skipped, 0 failed, 100 bytes transferred" + in (capsys.readouterr().out) ) def test_send_auth_error(self): @@ -671,15 +679,23 @@ class TestTransferSend: mock_session.get.side_effect = [first_get, second_get] with patch("observe.transfer.requests.Session", return_value=mock_session): - send_segments("https://example.com", "test-key", ["20250103"], dry_run=False) - send_segments("https://example.com", "test-key", ["20250103"], dry_run=False) + send_segments( + "https://example.com", "test-key", ["20250103"], dry_run=False + ) + send_segments( + "https://example.com", "test-key", ["20250103"], dry_run=False + ) assert mock_session.post.call_count == 1 output = capsys.readouterr().out - assert "Transfer complete: 1 sent, 0 skipped, 0 failed, 100 bytes transferred" in ( - output + assert ( + "Transfer complete: 1 sent, 0 skipped, 0 failed, 100 bytes transferred" + in (output) + ) + assert ( + "Transfer complete: 0 sent, 1 skipped, 0 failed, 0 bytes transferred" + in output ) - assert "Transfer complete: 0 sent, 1 skipped, 0 failed, 0 bytes transferred" in output def test_send_excludes_stream_json(self, tmp_path, monkeypatch): from observe.transfer import send_segments @@ -690,7 +706,9 @@ class TestTransferSend: mock_session = self._make_session(get_json=[]) with patch("observe.transfer.requests.Session", return_value=mock_session): - send_segments("https://example.com", "test-key", ["20250103"], dry_run=False) + send_segments( + "https://example.com", "test-key", ["20250103"], dry_run=False + ) files_arg = mock_session.post.call_args.kwargs["files"] uploaded_names = [entry[1][0] for entry in files_arg] diff --git a/tests/test_validate_key.py b/tests/test_validate_key.py index f8fdeb741..0f39bde83 100644 --- a/tests/test_validate_key.py +++ b/tests/test_validate_key.py @@ -140,12 +140,16 @@ def test_validate_vertex_credentials(tmp_path): import json sa_file = tmp_path / "sa.json" - sa_file.write_text(json.dumps({ - "type": "service_account", - "project_id": "test-project", - "client_email": "test@project.iam.gserviceaccount.com", - "private_key": "fake", - })) + sa_file.write_text( + json.dumps( + { + "type": "service_account", + "project_id": "test-project", + "client_email": "test@project.iam.gserviceaccount.com", + "private_key": "fake", + } + ) + ) client = Mock() client.models.list.return_value = [Mock()] @@ -154,9 +158,7 @@ def test_validate_vertex_credentials(tmp_path): mock_creds.service_account_email = "test@project.iam.gserviceaccount.com" with ( - patch( - "think.providers.google.genai.Client", return_value=client - ) as mock_cls, + patch("think.providers.google.genai.Client", return_value=client) as mock_cls, patch( "google.oauth2.service_account.Credentials.from_service_account_file", return_value=mock_creds, @@ -378,14 +380,16 @@ def test_providers_vertex_credentials_roundtrip(settings_client): """PUT/GET vertex_credentials saves file and returns email.""" client, journal = settings_client - sa_json = json.dumps({ - "type": "service_account", - "project_id": "test-project", - "client_email": "test@test-project.iam.gserviceaccount.com", - "private_key": "-----BEGIN RSA PRIVATE KEY-----\nfake\n-----END RSA PRIVATE KEY-----\n", - "client_id": "123", - "token_uri": "https://oauth2.googleapis.com/token", - }) + sa_json = json.dumps( + { + "type": "service_account", + "project_id": "test-project", + "client_email": "test@test-project.iam.gserviceaccount.com", + "private_key": "-----BEGIN RSA PRIVATE KEY-----\nfake\n-----END RSA PRIVATE KEY-----\n", + "client_id": "123", + "token_uri": "https://oauth2.googleapis.com/token", + } + ) # Mock validation (don't actually call Google API) with patch( diff --git a/tests/verify_api.py b/tests/verify_api.py index 943c45cb7..6d2a4f4ab 100644 --- a/tests/verify_api.py +++ b/tests/verify_api.py @@ -421,8 +421,7 @@ def normalize(data: Any, journal_path: str) -> Any: and isinstance(item_value, (int, float)) else ( "" - if item_key == "generated_at" - and isinstance(item_value, str) + if item_key == "generated_at" and isinstance(item_value, str) else ( round(item_value, 1) if item_key in {"score", "recency"} diff --git a/think/agents.py b/think/agents.py index 1dc8deace..a7e1cd9ed 100644 --- a/think/agents.py +++ b/think/agents.py @@ -55,6 +55,7 @@ LOG = logging.getLogger("think.agents") # Minimum content length for transcript-based generation MIN_INPUT_CHARS = 50 + def setup_logging(verbose: bool = False) -> logging.Logger: """Configure logging for agent CLI.""" level = logging.DEBUG if verbose else logging.INFO @@ -1183,7 +1184,10 @@ def _check_generate(provider_name: str, tier: int, timeout: int) -> tuple[str, s result = validate_key(provider_name, "") if not result.get("valid"): - return "skip", f"Ollama not reachable ({result.get('error', 'unreachable')})" + return ( + "skip", + f"Ollama not reachable ({result.get('error', 'unreachable')})", + ) try: module = get_provider_module(provider_name) @@ -1235,7 +1239,10 @@ async def _check_cogitate( result = validate_key(provider_name, "") if not result.get("valid"): - return "skip", f"Ollama not reachable ({result.get('error', 'unreachable')})" + return ( + "skip", + f"Ollama not reachable ({result.get('error', 'unreachable')})", + ) # Pre-flight: check cogitate CLI binary is installed binary = PROVIDER_METADATA[provider_name].get("cogitate_cli", "") diff --git a/think/conversation.py b/think/conversation.py index 337535444..86f2489be 100644 --- a/think/conversation.py +++ b/think/conversation.py @@ -390,4 +390,3 @@ def _normalize_exchange(ex: dict) -> dict: # Optionally, remove the old 'muse' key if it's no longer needed in the normalized dict # del ex["muse"] return ex - diff --git a/think/cortex.py b/think/cortex.py index c37921ab8..bd314bbc1 100644 --- a/think/cortex.py +++ b/think/cortex.py @@ -424,9 +424,9 @@ class CortexService: ) provider = event.get("provider") if provider: - self.agent_requests[agent.agent_id]["provider"] = ( - provider - ) + self.agent_requests[agent.agent_id][ + "provider" + ] = provider # Handle finish or error event if event.get("event") in ["finish", "error"]: diff --git a/think/merge.py b/think/merge.py index 67ff8f289..7bea90013 100644 --- a/think/merge.py +++ b/think/merge.py @@ -52,8 +52,12 @@ def merge_journals( _merge_segments(source, target, summary, dry_run, log_path=log_path) _merge_entities( - source, summary, dry_run, target_entities, - log_path=log_path, staging_path=staging_path, + source, + summary, + dry_run, + target_entities, + log_path=log_path, + staging_path=staging_path, ) _merge_facets(source, target, summary, dry_run, log_path=log_path) _merge_imports(source, target, summary, dry_run, log_path=log_path) @@ -457,20 +461,26 @@ def _merge_overlapping_facet( item_id = item.get("id", "") log_id = f"{facet_name}/entities/{source_det_file.name}/{item_id}" if item_id in seen_ids: - _log_decision(log_path, { - "action": "facet_detected_entity_merged", - "item_type": "facet_detected_entity", - "item_id": log_id, - "reason": "duplicate_skip", - }) + _log_decision( + log_path, + { + "action": "facet_detected_entity_merged", + "item_type": "facet_detected_entity", + "item_id": log_id, + "reason": "duplicate_skip", + }, + ) else: new_items.append(item) - _log_decision(log_path, { - "action": "facet_detected_entity_merged", - "item_type": "facet_detected_entity", - "item_id": log_id, - "reason": "appended", - }) + _log_decision( + log_path, + { + "action": "facet_detected_entity_merged", + "item_type": "facet_detected_entity", + "item_id": log_id, + "reason": "appended", + }, + ) if new_items and not dry_run: _append_jsonl(target_det_file, new_items) except Exception as exc: @@ -489,20 +499,26 @@ def _merge_overlapping_facet( for item in _read_jsonl(source_todo_file): log_id = f"{facet_name}/todos/{source_todo_file.name}/{item.get('text', '')}" if (item["text"], item.get("created_at")) in seen: - _log_decision(log_path, { - "action": "facet_todo_merged", - "item_type": "todo", - "item_id": log_id, - "reason": "duplicate_skip", - }) + _log_decision( + log_path, + { + "action": "facet_todo_merged", + "item_type": "todo", + "item_id": log_id, + "reason": "duplicate_skip", + }, + ) else: new_items.append(item) - _log_decision(log_path, { - "action": "facet_todo_merged", - "item_type": "todo", - "item_id": log_id, - "reason": "appended", - }) + _log_decision( + log_path, + { + "action": "facet_todo_merged", + "item_type": "todo", + "item_id": log_id, + "reason": "appended", + }, + ) if new_items and not dry_run: _append_jsonl(target_todo_file, new_items) except Exception as exc: @@ -523,20 +539,26 @@ def _merge_overlapping_facet( for item in _read_jsonl(source_calendar_file): log_id = f"{facet_name}/calendar/{source_calendar_file.name}/{item.get('title', '')}" if (item["title"], item.get("start")) in seen: - _log_decision(log_path, { - "action": "facet_calendar_merged", - "item_type": "calendar", - "item_id": log_id, - "reason": "duplicate_skip", - }) + _log_decision( + log_path, + { + "action": "facet_calendar_merged", + "item_type": "calendar", + "item_id": log_id, + "reason": "duplicate_skip", + }, + ) else: new_items.append(item) - _log_decision(log_path, { - "action": "facet_calendar_merged", - "item_type": "calendar", - "item_id": log_id, - "reason": "appended", - }) + _log_decision( + log_path, + { + "action": "facet_calendar_merged", + "item_type": "calendar", + "item_id": log_id, + "reason": "appended", + }, + ) if new_items and not dry_run: _append_jsonl(target_calendar_file, new_items) except Exception as exc: @@ -557,20 +579,26 @@ def _merge_overlapping_facet( for item in source_config: log_id = f"{facet_name}/activities/{item.get('id', '')}" if item.get("id") in existing_ids: - _log_decision(log_path, { - "action": "facet_activities_config_merged", - "item_type": "activity_config", - "item_id": log_id, - "reason": "duplicate_skip", - }) + _log_decision( + log_path, + { + "action": "facet_activities_config_merged", + "item_type": "activity_config", + "item_id": log_id, + "reason": "duplicate_skip", + }, + ) else: new_config.append(item) - _log_decision(log_path, { - "action": "facet_activities_config_merged", - "item_type": "activity_config", - "item_id": log_id, - "reason": "appended", - }) + _log_decision( + log_path, + { + "action": "facet_activities_config_merged", + "item_type": "activity_config", + "item_id": log_id, + "reason": "appended", + }, + ) if new_config and not dry_run: _append_jsonl(target_config_file, new_config) except Exception as exc: @@ -588,20 +616,26 @@ def _merge_overlapping_facet( for item in source_records: log_id = f"{facet_name}/activities/{source_day_file.name}/{item.get('id', '')}" if item.get("id") in existing_ids: - _log_decision(log_path, { - "action": "facet_activities_record_merged", - "item_type": "activity_record", - "item_id": log_id, - "reason": "duplicate_skip", - }) + _log_decision( + log_path, + { + "action": "facet_activities_record_merged", + "item_type": "activity_record", + "item_id": log_id, + "reason": "duplicate_skip", + }, + ) else: new_records.append(item) - _log_decision(log_path, { - "action": "facet_activities_record_merged", - "item_type": "activity_record", - "item_id": log_id, - "reason": "appended", - }) + _log_decision( + log_path, + { + "action": "facet_activities_record_merged", + "item_type": "activity_record", + "item_id": log_id, + "reason": "appended", + }, + ) if new_records and not dry_run: _append_jsonl(target_day_file, new_records) except Exception as exc: diff --git a/think/providers/google.py b/think/providers/google.py index 0375de2aa..442f3f744 100644 --- a/think/providers/google.py +++ b/think/providers/google.py @@ -128,9 +128,7 @@ def get_or_create_client(client: genai.Client | None = None) -> genai.Client: config = get_config() providers_config = config.get("providers", {}) - http_options = types.HttpOptions( - retry_options=types.HttpRetryOptions(attempts=8) - ) + http_options = types.HttpOptions(retry_options=types.HttpRetryOptions(attempts=8)) api_key = os.getenv("GOOGLE_API_KEY") diff --git a/think/segment.py b/think/segment.py index 173e7c33f..617a830cf 100644 --- a/think/segment.py +++ b/think/segment.py @@ -521,7 +521,9 @@ def cmd_move(args: argparse.Namespace) -> None: ) raise SystemExit(1) - succ_day, succ_seg, succ_path = _find_successor_segment(src_day, stream, src_segment) + succ_day, succ_seg, succ_path = _find_successor_segment( + src_day, stream, src_segment + ) events_path = src_dir / "events.jsonl" events_count = 0 @@ -558,7 +560,9 @@ def cmd_move(args: argparse.Namespace) -> None: rewritten = _rewrite_events_jsonl(dst_dir, to_day, new_segment) if rewritten: - print(f" rewrote {rewritten} events.jsonl lines (day: {src_day}->{to_day}, segment: {src_segment}->{new_segment})") + print( + f" rewrote {rewritten} events.jsonl lines (day: {src_day}->{to_day}, segment: {src_segment}->{new_segment})" + ) elif verbose: print(" no events.jsonl to rewrite") @@ -576,19 +580,25 @@ def cmd_move(args: argparse.Namespace) -> None: print(f" patched successor {succ_day}/{stream}/{succ_seg}") if verbose: print(f" prev_day: {succ_marker.get('prev_day')} -> {to_day}") - print(f" prev_segment: {succ_marker.get('prev_segment')} -> {new_segment}") + print( + f" prev_segment: {succ_marker.get('prev_segment')} -> {new_segment}" + ) elif verbose: print(" no successor to patch (stream tail)") summary = rebuild_stream_state(stream) print(f" rebuilt stream state: {stream}") if verbose: - print(f" scanned {summary['segments_scanned']} segments, rebuilt {len(summary['rebuilt'])} stream(s)") + print( + f" scanned {summary['segments_scanned']} segments, rebuilt {len(summary['rebuilt'])} stream(s)" + ) if index_info["available"]: deleted = _delete_index_rows(journal, old_rel) if any(deleted.values()) or verbose: - print(f" deleted index rows: chunks={deleted['chunks']}, files={deleted['files']}, entities={deleted['entities']}, signals={deleted['entity_signals']}") + print( + f" deleted index rows: chunks={deleted['chunks']}, files={deleted['files']}, entities={deleted['entities']}, signals={deleted['entity_signals']}" + ) new_rel = f"{to_day}/{stream}/{new_segment}" indexed = _reindex_segment(journal, dst_dir) print(f" re-indexed: {indexed} files at {new_rel}") diff --git a/think/stats_schema.py b/think/stats_schema.py index bf0127879..a85339088 100644 --- a/think/stats_schema.py +++ b/think/stats_schema.py @@ -52,7 +52,9 @@ def validate(data: dict) -> list[str]: if "schema_version" not in data: errors.append("missing 'schema_version'") elif data["schema_version"] != SCHEMA_VERSION: - errors.append(f"schema_version is {data['schema_version']}, expected {SCHEMA_VERSION}") + errors.append( + f"schema_version is {data['schema_version']}, expected {SCHEMA_VERSION}" + ) # Check generated_at if "generated_at" not in data: