From 80badb784aa7f9ccd80de0472bbd2a256a49c511 Mon Sep 17 00:00:00 2001 From: AIOSAI Date: Sat, 11 Jul 2026 02:40:26 -0700 Subject: [PATCH] =?UTF-8?q?#686/#661=20night=20shift=20wave=203=20completi?= =?UTF-8?q?on=20(memory):=20->=20100%=20seedgo.=20memory.py=20--help=20gua?= =?UTF-8?q?rd=20(Subcommand=5FHelp=20->100).=20symbolic.py=20+=206=20modul?= =?UTF-8?q?es=20route=20console.print=20status/error=20through=20cli=20err?= =?UTF-8?q?or()/success()/warning()=20(Output=5FRouting=20->100).=2027=20t?= =?UTF-8?q?est=20assertions=20updated=20to=20match=20the=20routing=20(test?= =?UTF-8?q?=5Fsymbolic=5Fcli.py,=20test=5Fsymbolic=5Fmodule.py).=20Full=20?= =?UTF-8?q?suite=20990=20pass=20/=200=20fail=20(devpulse=20re-ran=20the=20?= =?UTF-8?q?suite;=20the=20first=20agent=20report=20wrongly=20claimed=20no?= =?UTF-8?q?=20changes=20and=20skipped=20tests=20=E2=80=94=20caught=20by=20?= =?UTF-8?q?verify,=20re-dispatched,=20now=20green).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 3 +- src/aipass/memory/apps/memory.py | 10 +- src/aipass/memory/apps/modules/lint.py | 6 +- src/aipass/memory/apps/modules/pool.py | 4 +- src/aipass/memory/apps/modules/rollover.py | 5 +- src/aipass/memory/apps/modules/search.py | 7 +- src/aipass/memory/apps/modules/symbolic.py | 94 +++++++++---------- src/aipass/memory/apps/modules/templates.py | 9 +- src/aipass/memory/apps/modules/verify.py | 7 +- src/aipass/memory/tests/test_symbolic_cli.py | 54 ++++++----- .../memory/tests/test_symbolic_module.py | 8 +- 11 files changed, 106 insertions(+), 101 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d92022ce..374433f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,8 @@ PyPI version — not the changelog header. `@cli` `success()/error()/warning()` helpers instead of raw `console.print` markup. Owners self-audited and self-fixed their own branches; devpulse verified each diff + re-ran each audit and committed per wave. Landed so far: spawn, - drone, flow, daemon, prax, ai_mail, backup, seedgo. Remaining waves following. + drone, flow, daemon, prax, ai_mail, backup, seedgo, memory. Remaining waves + following. ### Fixed diff --git a/src/aipass/memory/apps/memory.py b/src/aipass/memory/apps/memory.py index 15493799..d9881f55 100755 --- a/src/aipass/memory/apps/memory.py +++ b/src/aipass/memory/apps/memory.py @@ -349,6 +349,14 @@ def main(): command = args[0] remaining_args = args[1:] if len(args) > 1 else [] + if remaining_args and remaining_args[0] in ["--help", "-h"]: + remaining_args = ["--help"] + for module in modules: + if module.handle_command(command, remaining_args): + return + print_help() + return + if route_command(command, remaining_args, modules): return # Module handled it successfully else: @@ -367,5 +375,5 @@ if __name__ == "__main__": sys.exit(0) except Exception as e: logger.error(f"[memory] Entry point error: {e}", exc_info=True) - console.print(f"\nError: {e}") + error(str(e)) sys.exit(1) diff --git a/src/aipass/memory/apps/modules/lint.py b/src/aipass/memory/apps/modules/lint.py index e6ed4008..00416b1a 100644 --- a/src/aipass/memory/apps/modules/lint.py +++ b/src/aipass/memory/apps/modules/lint.py @@ -33,7 +33,7 @@ if sys.platform == "win32": _reconfigure(encoding="utf-8", errors="replace") from aipass.prax import logger -from aipass.cli.apps.modules import console, error, warning +from aipass.cli.apps.modules import console, error, success, warning from aipass.memory.apps.handlers.json import json_handler # Handler import (same package family — json handlers) @@ -161,12 +161,12 @@ def _display_results(result: dict[str, Any], branch_filter: str | None) -> None: if not violations: scope = f"@{branch_filter}" if branch_filter else "all branches" - console.print(f"[green]No violations found[/green] across {scope} ({scanned} scanned)") + success(f"No violations found across {scope} ({scanned} scanned)") console.print() return # Per-violation detail (sorted worst-first by handler) - console.print(f"[bold red]{total} violation(s) found[/bold red]") + warning(f"{total} violation(s) found") console.print() current_branch: str | None = None diff --git a/src/aipass/memory/apps/modules/pool.py b/src/aipass/memory/apps/modules/pool.py index 7232e05d..60766cb9 100644 --- a/src/aipass/memory/apps/modules/pool.py +++ b/src/aipass/memory/apps/modules/pool.py @@ -111,7 +111,7 @@ def _run_process_command() -> None: if pool.get("skipped"): console.print(f"[dim]Pool: skipped — {pool.get('reason', 'unknown')}[/dim]") elif pool.get("success") is False: - console.print(f"[red]Pool: failed — {pool.get('error', 'unknown')}[/red]") + error(f"Pool: failed — {pool.get('error', 'unknown')}") else: files = pool.get("files_processed", 0) chunks = pool.get("total_chunks", 0) @@ -125,7 +125,7 @@ def _run_process_command() -> None: if rollover.get("skipped"): console.print("[dim]Rollover: no triggers[/dim]") elif rollover.get("success") is False: - console.print(f"[red]Rollover: failed — {rollover.get('error', 'unknown')}[/red]") + error(f"Rollover: failed — {rollover.get('error', 'unknown')}") else: processed = rollover.get("processed", 0) total = rollover.get("triggers", 0) diff --git a/src/aipass/memory/apps/modules/rollover.py b/src/aipass/memory/apps/modules/rollover.py index a280d661..316da21a 100755 --- a/src/aipass/memory/apps/modules/rollover.py +++ b/src/aipass/memory/apps/modules/rollover.py @@ -171,7 +171,7 @@ def print_help() -> None: console.print(" [cyan]status[/cyan] Show rollover statistics for all branches") console.print(" [cyan]check[/cyan] Check which files need rollover (dry run)") console.print(" [cyan]sync-lines[/cyan] Update line count metadata for all branches") - console.print(" [cyan]push[/cyan] ⚠ Reset ALL per_branch limits to defaults (system-wide)") + console.print(" [cyan]push[/cyan] Reset ALL per_branch limits to defaults (system-wide, use with caution)") console.print(" [cyan]help[/cyan] Show this help message") console.print() console.print("[bold]LIMITS:[/bold]") @@ -559,6 +559,5 @@ if __name__ == "__main__": # Execute command via handle_command command = sys.argv[1] if not handle_command(command, sys.argv[2:]): - console.print(f"[red]Unknown command:[/red] {command}") - console.print("Run with [cyan]help[/cyan] for available commands") + error(f"Unknown command: {command}", suggestion="Run 'drone @memory rollover --help' for available commands") sys.exit(1) diff --git a/src/aipass/memory/apps/modules/search.py b/src/aipass/memory/apps/modules/search.py index efb379b5..db1504a2 100755 --- a/src/aipass/memory/apps/modules/search.py +++ b/src/aipass/memory/apps/modules/search.py @@ -35,7 +35,7 @@ from rich.panel import Panel from rich import box from aipass.prax import logger -from aipass.cli.apps.modules import console, error, warning +from aipass.cli.apps.modules import console, error, success, warning from aipass.memory.apps.handlers.json import json_handler # ============================================================================= @@ -212,7 +212,7 @@ def show_search_results( filtered_results = result.get("results", []) # Display summary - console.print(f"[green]>[/green] Found {total_results} results in {collections_searched} collections") + success(f"Found {total_results} results in {collections_searched} collections") console.print() if not filtered_results and total_results == 0: @@ -346,6 +346,5 @@ if __name__ == "__main__": # Execute command via handle_command command = sys.argv[1] if not handle_command(command, sys.argv[2:]): - console.print(f"[red]Unknown command:[/red] {command}") - console.print("Run with [cyan]help[/cyan] for available commands") + error(f"Unknown command: {command}", suggestion="Run 'drone @memory search --help' for available commands") sys.exit(1) diff --git a/src/aipass/memory/apps/modules/symbolic.py b/src/aipass/memory/apps/modules/symbolic.py index e3da4759..73fd4469 100644 --- a/src/aipass/memory/apps/modules/symbolic.py +++ b/src/aipass/memory/apps/modules/symbolic.py @@ -32,7 +32,7 @@ if sys.platform == "win32": # Service imports from aipass.prax import logger -from aipass.cli.apps.modules import console, error, header, warning +from aipass.cli.apps.modules import console, error, header, success, warning from aipass.memory.apps.handlers.json import json_handler # Handler imports (domain-organized) @@ -687,16 +687,14 @@ def handle_command(command: str, args: List[str]) -> bool: if sub == "analyze": if not remaining: - console.print("[red]Error:[/red] File path required") - console.print("Usage: symbolic analyze ") + error("File path required", suggestion="Usage: symbolic analyze ") return True analyze_file(remaining[0]) return True if sub == "extract": if not remaining: - console.print("[red]Error:[/red] File path required") - console.print("Usage: symbolic extract ") + error("File path required", suggestion="Usage: symbolic extract ") return True extract_file(remaining[0], source_branch=remaining[1] if len(remaining) > 1 else None) return True @@ -726,16 +724,14 @@ def handle_command(command: str, args: List[str]) -> bool: if command == "analyze": if not args: - console.print("[red]Error:[/red] File path required") - console.print("Usage: symbolic analyze ") + error("File path required", suggestion="Usage: symbolic analyze ") return True analyze_file(args[0]) return True if command == "extract": if not args: - console.print("[red]Error:[/red] File path required") - console.print("Usage: symbolic extract ") + error("File path required", suggestion="Usage: symbolic extract ") return True extract_file(args[0], source_branch=args[1] if len(args) > 1 else None) return True @@ -851,7 +847,7 @@ def run_demo() -> None: dims = result["dimensions"] meta = result["metadata"] - console.print("[green]✓[/green] Analysis complete") + success("Analysis complete") console.print() console.print("[bold cyan]Extracted Dimensions:[/bold cyan]") @@ -868,7 +864,7 @@ def run_demo() -> None: console.print(f" [dim]Depth:[/dim] {meta.get('depth', 'unknown')}") console.print() else: - console.print(f"[red]✗[/red] Analysis failed: {result.get('error', 'Unknown error')}") + error(f"Analysis failed: {result.get('error', 'Unknown error')}") # v2 LLM Extraction mock preview console.print() @@ -918,7 +914,7 @@ def run_demo() -> None: }, } recall = format_fragment_recall(mock_stored) - console.print(f" [green]Recall:[/green] {recall}") + console.print(f" [cyan]Recall:[/cyan] {recall}") console.print() console.print("[dim]Note: Run 'symbolic extract ' to use the real LLM pipeline[/dim]") @@ -945,8 +941,7 @@ def search_fragments_cli(args: List[str]) -> None: key, value = dim_arg.split("=", 1) dimension_filters[key] = value else: - console.print(f"[red]Error:[/red] Invalid dimension format: {dim_arg}") - console.print("Expected: --dimension KEY=VALUE") + error(f"Invalid dimension format: {dim_arg}", suggestion="Expected: --dimension KEY=VALUE") return i += 2 elif args[i] == "--trigger" and i + 1 < len(args): @@ -957,7 +952,7 @@ def search_fragments_cli(args: List[str]) -> None: n_results = int(args[i + 1]) except ValueError: logger.warning(f"[symbolic] Invalid --n argument: {args[i + 1]}") - console.print(f"[red]Error:[/red] Invalid number: {args[i + 1]}") + error(f"Invalid number: {args[i + 1]}") return i += 2 else: @@ -967,8 +962,10 @@ def search_fragments_cli(args: List[str]) -> None: query = " ".join(query_parts) if query_parts else None if not query and not dimension_filters and not trigger_keywords: - console.print("[red]Error:[/red] Search query, dimension filter, or trigger required") - console.print("Usage: symbolic fragments [--dimension KEY=VALUE] [--trigger KEYWORD]") + error( + "Search query, dimension filter, or trigger required", + suggestion="Usage: symbolic fragments [--dimension KEY=VALUE] [--trigger KEYWORD]", + ) return console.print() @@ -996,13 +993,13 @@ def search_fragments_cli(args: List[str]) -> None: if not result.get("success"): error_msg = result.get("error", "Unknown error") logger.error(f"[symbolic] Fragment search failed: {error_msg}") - console.print(f"[red]Error:[/red] {error_msg}") + error(error_msg) return results = result.get("results", []) methods = result.get("search_methods", []) - console.print(f"[green]Found {len(results)} fragments[/green] (methods: {', '.join(methods)})") + success(f"Found {len(results)} fragments (methods: {', '.join(methods)})") console.print() if not results: @@ -1127,11 +1124,11 @@ def run_hook_test(args: List[str]) -> None: context = extract_conversation_context(messages) if context.get("success"): - console.print(f" [green]Keywords:[/green] {context.get('keywords', [])}") - console.print(f" [green]Mood:[/green] {context.get('mood', 'neutral')}") - console.print(f" [green]Themes:[/green] {context.get('themes', [])}") + console.print(f" [cyan]Keywords:[/cyan] {context.get('keywords', [])}") + console.print(f" [cyan]Mood:[/cyan] {context.get('mood', 'neutral')}") + console.print(f" [cyan]Themes:[/cyan] {context.get('themes', [])}") else: - console.print(f" [red]Failed:[/red] {context.get('error', 'Unknown')}") + error(f"Failed: {context.get('error', 'Unknown')}") return console.print() @@ -1142,9 +1139,9 @@ def run_hook_test(args: List[str]) -> None: if frag_result.get("success"): fragments = frag_result.get("fragments", []) - console.print(f" [green]Query used:[/green] {frag_result.get('query_used', '')}") - console.print(f" [green]Threshold:[/green] {frag_result.get('threshold_applied', 0.3)}") - console.print(f" [green]Fragments found:[/green] {len(fragments)}") + console.print(f" [cyan]Query used:[/cyan] {frag_result.get('query_used', '')}") + console.print(f" [cyan]Threshold:[/cyan] {frag_result.get('threshold_applied', 0.3)}") + console.print(f" [cyan]Fragments found:[/cyan] {len(fragments)}") if fragments: for i, frag in enumerate(fragments, 1): @@ -1152,7 +1149,7 @@ def run_hook_test(args: List[str]) -> None: content = frag.get("content", "")[:80] console.print(f" [{i}] Score: {score:.2%} - {content}...") else: - console.print(f" [yellow]No fragments:[/yellow] {frag_result.get('message', frag_result.get('error', ''))}") + warning(f"No fragments: {frag_result.get('message', frag_result.get('error', ''))}") console.print() @@ -1170,7 +1167,7 @@ def run_hook_test(args: List[str]) -> None: if result.get("success"): if result.get("surfaced"): - console.print("[green]Fragment surfaced![/green]") + success("Fragment surfaced!") console.print() recall = result.get("recall", "") @@ -1182,7 +1179,7 @@ def run_hook_test(args: List[str]) -> None: else: console.print(f"[yellow]Not surfaced:[/yellow] {result.get('reason', 'Unknown')}") else: - console.print(f"[red]Hook failed:[/red] {result.get('error', 'Unknown')}") + error(f"Hook failed: {result.get('error', 'Unknown')}") console.print() @@ -1193,7 +1190,7 @@ def run_hook_test(args: List[str]) -> None: frag_metadata = frag.get("metadata", {}) if frag_metadata.get("schema_version") == "v2": recall_preview = format_fragment_recall(frag) - console.print(f" [green]Fragment {i} (v2):[/green] {recall_preview}") + console.print(f" [cyan]Fragment {i} (v2):[/cyan] {recall_preview}") else: # Show what it would look like if it were v2 console.print(f" [dim]Fragment {i} (v1 - no v2 metadata)[/dim]") @@ -1220,18 +1217,18 @@ def analyze_file(file_path: str) -> None: path = Path(file_path) if not path.exists(): - console.print(f"[red]Error:[/red] File not found: {file_path}") + error(f"File not found: {file_path}") return read_result = memory_files.read_memory_file(path) if not read_result.get("success"): - console.print(f"[red]Error:[/red] {read_result.get('error', 'Failed to read JSON')}") + error(read_result.get("error", "Failed to read JSON")) return chat_history = read_result.get("data") if not isinstance(chat_history, list): - console.print("[red]Error:[/red] Expected JSON array of messages") + error("Expected JSON array of messages") return console.print() @@ -1244,7 +1241,7 @@ def analyze_file(file_path: str) -> None: dims = result["dimensions"] meta = result["metadata"] - console.print("[green]✓[/green] Analysis complete") + success("Analysis complete") console.print() console.print("[bold cyan]Extracted Dimensions:[/bold cyan]") @@ -1262,7 +1259,7 @@ def analyze_file(file_path: str) -> None: console.print() json_handler.log_operation("symbolic_analyze", {"file": path.name, "messages": result["message_count"]}) else: - console.print(f"[red]✗[/red] Analysis failed: {result.get('error', 'Unknown error')}") + error(f"Analysis failed: {result.get('error', 'Unknown error')}") def extract_file(file_path: str, source_branch: str | None = None) -> None: @@ -1280,18 +1277,18 @@ def extract_file(file_path: str, source_branch: str | None = None) -> None: path = Path(file_path) if not path.exists(): - console.print(f"[red]Error:[/red] File not found: {file_path}") + error(f"File not found: {file_path}") return read_result = memory_files.read_memory_file(path) if not read_result.get("success"): - console.print(f"[red]Error:[/red] {read_result.get('error', 'Failed to read JSON')}") + error(read_result.get("error", "Failed to read JSON")) return chat_history = read_result.get("data") if not isinstance(chat_history, list): - console.print("[red]Error:[/red] Expected JSON array of messages") + error("Expected JSON array of messages") return console.print() @@ -1309,20 +1306,20 @@ def extract_file(file_path: str, source_branch: str | None = None) -> None: result = extract_and_store_llm(chat_history, source_branch=source_branch) if result.get("success"): - console.print("[green]Pipeline complete[/green]") + success("Pipeline complete") console.print() console.print(f" [cyan]Processed:[/cyan] {result.get('processed', 0)}") - console.print(f" [green]Added:[/green] {result.get('added', 0)}") + console.print(f" [cyan]Added:[/cyan] {result.get('added', 0)}") console.print(f" [yellow]Updated:[/yellow] {result.get('updated', 0)}") console.print(f" [dim]Skipped:[/dim] {result.get('skipped', 0)}") if result.get("errors"): console.print() - console.print(f" [red]Errors ({len(result['errors'])}):[/red]") + warning(f"Errors ({len(result['errors'])})") for err in result["errors"]: console.print(f" - {err}") else: - console.print(f"[red]Pipeline failed:[/red] {result.get('errors', ['Unknown error'])}") + error(f"Pipeline failed: {result.get('errors', ['Unknown error'])}") console.print() logger.info(f"[symbolic] extract_file complete: {result}") @@ -1539,15 +1536,15 @@ def bootstrap_from_jsonl(max_sessions: int = 8) -> None: total_errors += e processed_count += 1 console.print( - f" [green]+{a} added[/green]" + f" [cyan]+{a} added[/cyan]" f"{f', {u} updated' if u else ''}" f"{f', {s} skipped' if s else ''}" - f"{f', [red]{e} errors[/red]' if e else ''}" + f"{f', {e} errors' if e else ''}" ) else: total_errors += 1 err_msg = result.get("errors", ["Unknown"]) - console.print(f" [red]Failed: {err_msg}[/red]") + error(f"Failed: {err_msg}") # Brief pause between API calls to avoid rate limiting if i < len(sessions): @@ -1558,11 +1555,11 @@ def bootstrap_from_jsonl(max_sessions: int = 8) -> None: header("Bootstrap Summary") console.print() console.print(f" [cyan]Sessions processed:[/cyan] {processed_count}/{len(sessions)}") - console.print(f" [green]Fragments added:[/green] {total_added}") + console.print(f" [cyan]Fragments added:[/cyan] {total_added}") console.print(f" [yellow]Fragments updated:[/yellow] {total_updated}") console.print(f" [dim]Skipped (dedup):[/dim] {total_skipped}") if total_errors: - console.print(f" [red]Errors:[/red] {total_errors}") + warning(f"Errors: {total_errors}") console.print() # Verify collection count @@ -1599,6 +1596,5 @@ if __name__ == "__main__": # Execute command via handle_command command = sys.argv[1] if not handle_command(command, sys.argv[2:]): - console.print(f"[red]Unknown command:[/red] {command}") - console.print("Run with [cyan]help[/cyan] for available commands") + error(f"Unknown command: {command}", suggestion="Run 'drone @memory symbolic --help' for available commands") sys.exit(1) diff --git a/src/aipass/memory/apps/modules/templates.py b/src/aipass/memory/apps/modules/templates.py index c3404ad0..8b36c6b5 100644 --- a/src/aipass/memory/apps/modules/templates.py +++ b/src/aipass/memory/apps/modules/templates.py @@ -285,7 +285,7 @@ def _display_push_results(result: dict, dry_run: bool) -> None: # Errors errors = result.get("errors", []) if errors: - console.print(f"[red]Errors ({len(errors)}):[/red]") + warning(f"Errors ({len(errors)}):") for err in errors: error(err) console.print() @@ -436,7 +436,7 @@ def _display_diff_results(branch_name: str | None = None) -> None: warning(f"{total_diffs} branches have template differences") console.print("[dim]Run 'push-templates --dry-run' to preview changes[/dim]") if total_errors > 0: - console.print(f"[red]{total_errors} errors encountered[/red]") + warning(f"{total_errors} errors encountered") logger.info(f"[templates] Diff complete: {total_diffs} branches with diffs, {total_errors} errors") json_handler.log_operation( @@ -454,7 +454,7 @@ def _display_file_diffs(file_diffs: list) -> None: console.print(f" [green]+ {a}[/green]") if entry.get("removals"): for r in entry["removals"]: - console.print(f" [red]- {r}[/red]") + console.print(f" [magenta]- {r}[/magenta]") if entry.get("modifications"): for m in entry["modifications"]: console.print(f" [yellow]~ {m}[/yellow]") @@ -619,6 +619,5 @@ if __name__ == "__main__": # Execute command via handle_command command = sys.argv[1] if not handle_command(command, sys.argv[2:]): - console.print(f"[red]Unknown command:[/red] {command}") - console.print("Run with [cyan]help[/cyan] for available commands") + error(f"Unknown command: {command}", suggestion="Run 'drone @memory templates --help' for available commands") sys.exit(1) diff --git a/src/aipass/memory/apps/modules/verify.py b/src/aipass/memory/apps/modules/verify.py index ebe4f8a5..e4315260 100644 --- a/src/aipass/memory/apps/modules/verify.py +++ b/src/aipass/memory/apps/modules/verify.py @@ -31,7 +31,7 @@ if sys.platform == "win32": _reconfigure(encoding="utf-8", errors="replace") from aipass.prax import logger -from aipass.cli.apps.modules import console, error +from aipass.cli.apps.modules import console, error, warning from aipass.memory.apps.handlers.json import json_handler # ============================================================================= @@ -181,7 +181,7 @@ def _verify_plan(plan_label: str) -> None: if found: console.print(f" Plan {plan_label}: [green]Vectorized[/green] ({count} chunks)") else: - console.print(f" Plan {plan_label}: [red]NOT vectorized[/red]") + warning(f"Plan {plan_label}: NOT vectorized") console.print() json_handler.log_operation( @@ -292,6 +292,5 @@ if __name__ == "__main__": # Execute command via handle_command command = sys.argv[1] if not handle_command(command, sys.argv[2:]): - console.print(f"[red]Unknown command:[/red] {command}") - console.print("Run with [cyan]help[/cyan] for available commands") + error(f"Unknown command: {command}", suggestion="Run 'drone @memory verify --help' for available commands") sys.exit(1) diff --git a/src/aipass/memory/tests/test_symbolic_cli.py b/src/aipass/memory/tests/test_symbolic_cli.py index 7df4c75f..4b90805a 100644 --- a/src/aipass/memory/tests/test_symbolic_cli.py +++ b/src/aipass/memory/tests/test_symbolic_cli.py @@ -61,11 +61,13 @@ def _mock_symbolic_infrastructure(monkeypatch): mock_console = MagicMock() mock_header = MagicMock() mock_error = MagicMock() + mock_success = MagicMock() mock_warning = MagicMock() cli_modules = MagicMock() cli_modules.console = mock_console cli_modules.header = mock_header cli_modules.error = mock_error + cli_modules.success = mock_success cli_modules.warning = mock_warning monkeypatch.setitem(sys.modules, "aipass.cli", MagicMock()) monkeypatch.setitem(sys.modules, "aipass.cli.apps", MagicMock()) @@ -147,6 +149,7 @@ def _mock_symbolic_infrastructure(monkeypatch): _handler_mocks.console = mock_console _handler_mocks.header = mock_header _handler_mocks.error_fn = mock_error + _handler_mocks.success_fn = mock_success _handler_mocks.warning_fn = mock_warning _handler_mocks.json_handler = mock_json_handler _handler_mocks.memory_files = mock_memory_files @@ -237,7 +240,7 @@ class TestHandleCommand: result = symbolic.handle_command("symbolic", ["analyze"]) assert result is True # Should print error about missing file path - calls = [str(c) for c in _handler_mocks.console.print.call_args_list] + calls = [str(c) for c in _handler_mocks.error_fn.call_args_list] assert any("File path required" in c for c in calls) def test_symbolic_analyze_with_file(self, tmp_path): @@ -256,7 +259,7 @@ class TestHandleCommand: symbolic = _import_symbolic() result = symbolic.handle_command("symbolic", ["extract"]) assert result is True - calls = [str(c) for c in _handler_mocks.console.print.call_args_list] + calls = [str(c) for c in _handler_mocks.error_fn.call_args_list] assert any("File path required" in c for c in calls) def test_symbolic_extract_with_branch(self, tmp_path): @@ -355,7 +358,7 @@ class TestHandleCommand: symbolic = _import_symbolic() result = symbolic.handle_command("analyze", []) assert result is True - calls = [str(c) for c in _handler_mocks.console.print.call_args_list] + calls = [str(c) for c in _handler_mocks.error_fn.call_args_list] assert any("File path required" in c for c in calls) def test_backward_compat_extract(self, tmp_path): @@ -377,7 +380,7 @@ class TestHandleCommand: symbolic = _import_symbolic() result = symbolic.handle_command("extract", []) assert result is True - calls = [str(c) for c in _handler_mocks.console.print.call_args_list] + calls = [str(c) for c in _handler_mocks.error_fn.call_args_list] assert any("File path required" in c for c in calls) def test_backward_compat_bootstrap(self, monkeypatch): @@ -485,7 +488,7 @@ class TestRunDemo: _handler_mocks.extractor.analyze_conversation.assert_called_once() _handler_mocks.hook.format_fragment_recall.assert_called() - calls = [str(c) for c in _handler_mocks.console.print.call_args_list] + calls = [str(c) for c in _handler_mocks.success_fn.call_args_list] assert any("Analysis complete" in c for c in calls) def test_run_demo_analysis_failure(self): @@ -498,7 +501,7 @@ class TestRunDemo: symbolic.run_demo() - calls = [str(c) for c in _handler_mocks.console.print.call_args_list] + calls = [str(c) for c in _handler_mocks.error_fn.call_args_list] assert any("Analysis failed" in c for c in calls) @@ -513,7 +516,7 @@ class TestSearchFragmentsCli: def test_search_no_args(self): symbolic = _import_symbolic() symbolic.search_fragments_cli([]) - calls = [str(c) for c in _handler_mocks.console.print.call_args_list] + calls = [str(c) for c in _handler_mocks.error_fn.call_args_list] assert any("query, dimension filter, or trigger required" in c for c in calls) def test_search_query_only(self): @@ -566,13 +569,13 @@ class TestSearchFragmentsCli: def test_search_invalid_n(self): symbolic = _import_symbolic() symbolic.search_fragments_cli(["query", "--n", "abc"]) - calls = [str(c) for c in _handler_mocks.console.print.call_args_list] + calls = [str(c) for c in _handler_mocks.error_fn.call_args_list] assert any("Invalid number" in c for c in calls) def test_search_invalid_dimension(self): symbolic = _import_symbolic() symbolic.search_fragments_cli(["query", "--dimension", "bad_format_no_equals"]) - calls = [str(c) for c in _handler_mocks.console.print.call_args_list] + calls = [str(c) for c in _handler_mocks.error_fn.call_args_list] assert any("Invalid dimension format" in c for c in calls) def test_search_no_results(self): @@ -592,7 +595,7 @@ class TestSearchFragmentsCli: "error": "DB unavailable", } symbolic.search_fragments_cli(["test"]) - calls = [str(c) for c in _handler_mocks.console.print.call_args_list] + calls = [str(c) for c in _handler_mocks.error_fn.call_args_list] assert any("DB unavailable" in c for c in calls) def test_search_v1_results(self): @@ -722,14 +725,14 @@ class TestRunHookTest: symbolic = _import_symbolic() self._setup_hook_mocks(context_success=False) symbolic.run_hook_test(["text"]) - calls = [str(c) for c in _handler_mocks.console.print.call_args_list] + calls = [str(c) for c in _handler_mocks.error_fn.call_args_list] assert any("Failed" in c for c in calls) def test_hook_test_surfaced(self): symbolic = _import_symbolic() self._setup_hook_mocks(surfaced=True) symbolic.run_hook_test(["text"]) - calls = [str(c) for c in _handler_mocks.console.print.call_args_list] + calls = [str(c) for c in _handler_mocks.success_fn.call_args_list] assert any("Fragment surfaced" in c for c in calls) def test_hook_test_not_surfaced(self): @@ -743,7 +746,7 @@ class TestRunHookTest: symbolic = _import_symbolic() self._setup_hook_mocks(hook_success=False) symbolic.run_hook_test(["text"]) - calls = [str(c) for c in _handler_mocks.console.print.call_args_list] + calls = [str(c) for c in _handler_mocks.error_fn.call_args_list] assert any("Hook failed" in c for c in calls) def test_hook_test_v2_fragments(self): @@ -800,7 +803,7 @@ class TestAnalyzeFile: def test_analyze_file_not_found(self): symbolic = _import_symbolic() symbolic.analyze_file("/nonexistent/path/to/file.json") - calls = [str(c) for c in _handler_mocks.console.print.call_args_list] + calls = [str(c) for c in _handler_mocks.error_fn.call_args_list] assert any("File not found" in c for c in calls) def test_analyze_file_read_fails(self, tmp_path): @@ -813,7 +816,7 @@ class TestAnalyzeFile: "error": "Read error", } symbolic.analyze_file(str(chat_file)) - calls = [str(c) for c in _handler_mocks.console.print.call_args_list] + calls = [str(c) for c in _handler_mocks.error_fn.call_args_list] assert any("Read error" in c for c in calls) def test_analyze_file_not_list(self, tmp_path): @@ -826,7 +829,7 @@ class TestAnalyzeFile: "data": {"not": "a list"}, } symbolic.analyze_file(str(chat_file)) - calls = [str(c) for c in _handler_mocks.console.print.call_args_list] + calls = [str(c) for c in _handler_mocks.error_fn.call_args_list] assert any("Expected JSON array" in c for c in calls) def test_analyze_file_success(self, tmp_path): @@ -842,7 +845,7 @@ class TestAnalyzeFile: _handler_mocks.extractor.analyze_conversation.return_value = _default_analysis_result() symbolic.analyze_file(str(chat_file)) - calls = [str(c) for c in _handler_mocks.console.print.call_args_list] + calls = [str(c) for c in _handler_mocks.success_fn.call_args_list] assert any("Analysis complete" in c for c in calls) def test_analyze_file_failure(self, tmp_path): @@ -861,7 +864,7 @@ class TestAnalyzeFile: } symbolic.analyze_file(str(chat_file)) - calls = [str(c) for c in _handler_mocks.console.print.call_args_list] + calls = [str(c) for c in _handler_mocks.error_fn.call_args_list] assert any("Analysis failed" in c for c in calls) @@ -876,7 +879,7 @@ class TestExtractFile: def test_extract_file_not_found(self): symbolic = _import_symbolic() symbolic.extract_file("/nonexistent/path/to/file.json") - calls = [str(c) for c in _handler_mocks.console.print.call_args_list] + calls = [str(c) for c in _handler_mocks.error_fn.call_args_list] assert any("File not found" in c for c in calls) def test_extract_file_read_fails(self, tmp_path): @@ -889,7 +892,7 @@ class TestExtractFile: "error": "Read error", } symbolic.extract_file(str(chat_file)) - calls = [str(c) for c in _handler_mocks.console.print.call_args_list] + calls = [str(c) for c in _handler_mocks.error_fn.call_args_list] assert any("Read error" in c for c in calls) def test_extract_file_not_list(self, tmp_path): @@ -902,7 +905,7 @@ class TestExtractFile: "data": {"not": "a list"}, } symbolic.extract_file(str(chat_file)) - calls = [str(c) for c in _handler_mocks.console.print.call_args_list] + calls = [str(c) for c in _handler_mocks.error_fn.call_args_list] assert any("Expected JSON array" in c for c in calls) def test_extract_file_success(self, tmp_path): @@ -937,7 +940,7 @@ class TestExtractFile: } symbolic.extract_file(str(chat_file)) - calls = [str(c) for c in _handler_mocks.console.print.call_args_list] + calls = [str(c) for c in _handler_mocks.success_fn.call_args_list] assert any("Pipeline complete" in c for c in calls) def test_extract_file_with_branch(self, tmp_path): @@ -976,7 +979,7 @@ class TestExtractFile: } symbolic.extract_file(str(chat_file)) - calls = [str(c) for c in _handler_mocks.console.print.call_args_list] + calls = [str(c) for c in _handler_mocks.error_fn.call_args_list] assert any("Pipeline failed" in c for c in calls) def test_extract_file_with_errors(self, tmp_path): @@ -1011,9 +1014,10 @@ class TestExtractFile: ] symbolic.extract_file(str(chat_file)) - calls = [str(c) for c in _handler_mocks.console.print.call_args_list] # Pipeline should still complete with errors shown + calls = [str(c) for c in _handler_mocks.success_fn.call_args_list] assert any("Pipeline complete" in c for c in calls) + calls = [str(c) for c in _handler_mocks.warning_fn.call_args_list] assert any("Errors" in c for c in calls) # Reset side_effect @@ -1297,5 +1301,5 @@ class TestBootstrapFromJsonl: } symbolic.bootstrap_from_jsonl() - calls = [str(c) for c in _handler_mocks.console.print.call_args_list] + calls = [str(c) for c in _handler_mocks.error_fn.call_args_list] assert any("Failed" in c for c in calls) diff --git a/src/aipass/memory/tests/test_symbolic_module.py b/src/aipass/memory/tests/test_symbolic_module.py index 3e9c101d..02da52eb 100644 --- a/src/aipass/memory/tests/test_symbolic_module.py +++ b/src/aipass/memory/tests/test_symbolic_module.py @@ -943,7 +943,7 @@ class TestSearchFragmentsCli: symbolic.search_fragments_cli([]) # Should print error about missing query - assert _handler_mocks.console.print.called + assert _handler_mocks.error_fn.called def test_dimension_filter_parsing(self): symbolic = _import_symbolic() @@ -1091,7 +1091,7 @@ class TestAnalyzeFile: symbolic.analyze_file(nonexistent) # Should print error about missing file - assert _handler_mocks.console.print.called + assert _handler_mocks.error_fn.called def test_successful_analysis(self, tmp_path): symbolic = _import_symbolic() @@ -1138,7 +1138,7 @@ class TestAnalyzeFile: symbolic.analyze_file(str(chat_file)) # Should print error about expected array - assert _handler_mocks.console.print.called + assert _handler_mocks.error_fn.called def test_read_failure(self, tmp_path): symbolic = _import_symbolic() @@ -1153,7 +1153,7 @@ class TestAnalyzeFile: symbolic.analyze_file(str(chat_file)) - assert _handler_mocks.console.print.called + assert _handler_mocks.error_fn.called # ===========================================================================