diff --git a/src/logsqueak/cli.py b/src/logsqueak/cli.py index 18abef5..c294330 100644 --- a/src/logsqueak/cli.py +++ b/src/logsqueak/cli.py @@ -414,12 +414,17 @@ def _display_search_results(results: list[dict], graph_path: Path): Display search results in terminal-friendly format. Uses OSC 8 escape codes for clickable links and color coding for readability. + Results are displayed in reverse relevance order (least to most relevant), + but numbered in reverse (most relevant = 1). """ for idx, result in enumerate(results, 1): page_name = result["page_name"] confidence = result["confidence"] snippet = result["snippet"] + # Reverse numbering so most relevant result (shown last) gets number "1" + display_idx = len(results) - idx + 1 + # Create clickable logseq:// link using OSC 8 escape codes clickable_link = _create_clickable_link(page_name, graph_path) @@ -444,8 +449,8 @@ def _display_search_results(results: list[dict], graph_path: Path): # Fallback if no bullet content (shouldn't happen) snippet_display = "(no content preview)" - # Display result - click.echo(f"{idx}. {clickable_link}") + # Display result with reversed numbering + click.echo(f"{display_idx}. {clickable_link}") click.echo(f" Relevance: {confidence}%") click.echo(f" {snippet_display}") click.echo() # Blank line between results diff --git a/tests/integration/test_cli_search.py b/tests/integration/test_cli_search.py index a35e781..d9b9bee 100644 --- a/tests/integration/test_cli_search.py +++ b/tests/integration/test_cli_search.py @@ -229,3 +229,64 @@ def test_search_respects_top_k_config(temp_config, temp_graph_with_pages, use_sh # Should have at most 2 results result_count = result.output.count("\n1.") + result.output.count("\n2.") + result.output.count("\n3.") assert result_count <= 2 + + +def test_search_displays_reversed_numbering(temp_config, temp_graph_with_pages, use_shared_encoder, monkeypatch): + """Test that search displays results in reverse order with reversed numbering. + + Most relevant result should appear last (closest to command prompt) and be numbered "1". + Less relevant results should appear earlier with higher numbers. + """ + # Monkeypatch config path + monkeypatch.setenv("HOME", str(temp_config.parent.parent)) + config_home = temp_config.parent.parent / ".config" / "logsqueak" + config_home.mkdir(parents=True, exist_ok=True) + (config_home / "config.yaml").write_text(temp_config.read_text()) + (config_home / "config.yaml").chmod(0o600) + + runner = CliRunner() + result = runner.invoke(cli, ["search", "programming"]) + + # Print output for debugging + print(f"\n=== CLI OUTPUT ===") + print(result.output) + print(f"=== EXIT CODE: {result.exit_code} ===\n") + + if result.exit_code != 0: + print(f"=== EXCEPTION ===") + if result.exception: + import traceback + traceback.print_exception(type(result.exception), result.exception, result.exception.__traceback__) + print("=================\n") + + assert result.exit_code == 0 + + # Parse the output to find result numbers and their relevance scores + lines = result.output.split('\n') + results = [] + + for i, line in enumerate(lines): + # Look for numbered results (e.g., "1. PageName" or "2. PageName") + if line and line[0].isdigit() and '. ' in line: + number = int(line.split('.')[0]) + # Next line should have "Relevance: XX%" + if i + 1 < len(lines) and "Relevance:" in lines[i + 1]: + relevance_str = lines[i + 1].split("Relevance:")[1].strip().rstrip('%') + relevance = int(relevance_str) + results.append((number, relevance)) + + # Should have at least 2 results for meaningful test + assert len(results) >= 2, f"Expected at least 2 results, got {len(results)}" + + # Most relevant result should be last in the list + last_result = results[-1] + first_result = results[0] + + # The last result (most relevant) should have number "1" + assert last_result[0] == 1, f"Expected last result to be numbered 1, got {last_result[0]}" + + # The first result (least relevant) should have the highest number + assert first_result[0] == len(results), f"Expected first result to be numbered {len(results)}, got {first_result[0]}" + + # Relevance should decrease as we go from last to first (reversed display) + assert last_result[1] >= first_result[1], f"Expected last result ({last_result[1]}%) to be more relevant than first ({first_result[1]}%)" diff --git a/tests/unit/test_cli_display_reversed_numbering.py b/tests/unit/test_cli_display_reversed_numbering.py new file mode 100644 index 0000000..d71f287 --- /dev/null +++ b/tests/unit/test_cli_display_reversed_numbering.py @@ -0,0 +1,109 @@ +"""Unit test for reversed numbering in search results display.""" + +from pathlib import Path +from unittest.mock import patch, MagicMock +from io import StringIO + + +def test_display_search_results_reversed_numbering(): + """Test that search results are numbered in reverse (most relevant = 1).""" + from logsqueak.cli import _display_search_results + + # Create mock results (already in reversed order - least to most relevant) + results = [ + { + "page_name": "LeastRelevant", + "block_id": "id1", + "confidence": 50, + "snippet": "- Some content" + }, + { + "page_name": "MiddleRelevant", + "block_id": "id2", + "confidence": 75, + "snippet": "- Some other content" + }, + { + "page_name": "MostRelevant", + "block_id": "id3", + "confidence": 100, + "snippet": "- The best content" + } + ] + + graph_path = Path("/fake/graph") + + # Capture output + output_lines = [] + + def mock_echo(msg=''): + output_lines.append(msg) + + # Mock click.echo and the clickable link function + with patch('logsqueak.cli.click.echo', side_effect=mock_echo): + with patch('logsqueak.cli._create_clickable_link', side_effect=lambda name, path: name): + _display_search_results(results, graph_path) + + # Join output for easier analysis + output = '\n'.join(output_lines) + + # Parse the output to find numbered results + result_numbers = [] + for i, line in enumerate(output_lines): + if '. LeastRelevant' in line or '. MiddleRelevant' in line or '. MostRelevant' in line: + # Extract the number before the dot + number = int(line.split('.')[0]) + page_name = line.split('. ')[1] + result_numbers.append((number, page_name)) + + # Verify we found all 3 results + assert len(result_numbers) == 3, f"Expected 3 results, got {len(result_numbers)}" + + # Verify numbering + # First result shown (least relevant) should have highest number (3) + assert result_numbers[0] == (3, "LeastRelevant"), \ + f"Expected (3, 'LeastRelevant'), got {result_numbers[0]}" + + # Middle result should have number 2 + assert result_numbers[1] == (2, "MiddleRelevant"), \ + f"Expected (2, 'MiddleRelevant'), got {result_numbers[1]}" + + # Last result shown (most relevant) should have number 1 + assert result_numbers[2] == (1, "MostRelevant"), \ + f"Expected (1, 'MostRelevant'), got {result_numbers[2]}" + + print("✓ Numbering is correctly reversed: most relevant result numbered as 1") + + +def test_display_search_results_reversed_numbering_with_two_results(): + """Test reversed numbering with only 2 results.""" + from logsqueak.cli import _display_search_results + + results = [ + {"page_name": "Less", "block_id": "id1", "confidence": 60, "snippet": "- Content"}, + {"page_name": "More", "block_id": "id2", "confidence": 90, "snippet": "- Better content"} + ] + + graph_path = Path("/fake/graph") + output_lines = [] + + def mock_echo(msg=''): + output_lines.append(msg) + + with patch('logsqueak.cli.click.echo', side_effect=mock_echo): + with patch('logsqueak.cli._create_clickable_link', side_effect=lambda name, path: name): + _display_search_results(results, graph_path) + + # Find numbered results + result_numbers = [] + for line in output_lines: + if '. Less' in line or '. More' in line: + number = int(line.split('.')[0]) + page_name = line.split('. ')[1] + result_numbers.append((number, page_name)) + + # Verify + assert result_numbers[0] == (2, "Less"), f"Expected (2, 'Less'), got {result_numbers[0]}" + assert result_numbers[1] == (1, "More"), f"Expected (1, 'More'), got {result_numbers[1]}" + + print("✓ Two results correctly numbered: 2 (less relevant), 1 (more relevant)")