Files

261 lines
9.0 KiB
Python
Raw Permalink Normal View History

2026-04-22 22:14:26 +02:00
from __future__ import annotations
import unittest
from collections.abc import Sequence
2026-04-23 19:56:11 +02:00
from pathlib import Path
2026-04-22 22:14:26 +02:00
from unittest.mock import patch
from chromadb.errors import InternalError, NotFoundError
2026-04-22 22:14:26 +02:00
from click.testing import Result
from typer.testing import CliRunner
from chromy.cli import app
class CliTests(unittest.TestCase):
2026-04-23 19:56:11 +02:00
@staticmethod
def _fixture_path(path: str) -> str:
return str(Path(path).resolve())
2026-04-23 20:49:52 +02:00
def test_list_empty_collections(self) -> None:
2026-04-22 22:21:14 +02:00
with patch(
"chromy.handlers.list_collections.list_collections",
return_value=[],
):
result = _invoke(["list-collections"])
self.assertEqual(result.exit_code, 0)
self.assertEqual(result.stdout, "No collections found.\n")
2026-04-23 20:49:52 +02:00
def test_list_existing_collections(self) -> None:
with patch(
"chromy.handlers.list_collections.list_collections",
return_value=["books", "code"],
2026-04-29 12:44:28 +02:00
):
2026-04-23 20:49:52 +02:00
result = _invoke(["list-collections"])
self.assertEqual(result.exit_code, 0)
2026-04-29 12:44:28 +02:00
self.assertEqual(result.stdout, "· books\n· code\n")
2026-04-23 20:49:52 +02:00
2026-04-22 22:21:14 +02:00
def test_create_collection(self) -> None:
with patch(
"chromy.handlers.create_collection.create_collection",
return_value="notes",
) as create_collection:
result = _invoke(["create-collection", "notes"])
create_collection.assert_called_once_with("notes")
self.assertEqual(result.exit_code, 0)
2026-04-23 21:49:46 +02:00
self.assertEqual(result.stdout, "Created: collection 'notes'.\n")
2026-04-22 22:21:14 +02:00
def test_create_collection_with_same_name(self) -> None:
with patch(
"chromy.handlers.create_collection.create_collection",
2026-04-24 18:23:02 +02:00
side_effect=InternalError(),
) as create_collection:
result = _invoke(["create-collection", "notes"])
create_collection.assert_called_once_with("notes")
self.assertEqual(result.exit_code, 1)
2026-04-24 18:23:02 +02:00
self.assertEqual(result.stdout, "Error: Collection 'notes' already exists.\n")
2026-04-22 22:21:14 +02:00
def test_delete_collection(self) -> None:
with patch(
"chromy.handlers.delete_collection.delete_collection",
) as delete_collection:
result = _invoke(["delete-collection", "notes"])
delete_collection.assert_called_once_with("notes")
self.assertEqual(result.exit_code, 0)
self.assertEqual(result.stdout, "Deleted collection 'notes'.\n")
def test_delete_non_existent_collection(self) -> None:
with patch(
"chromy.handlers.delete_collection.delete_collection",
2026-04-24 18:23:02 +02:00
side_effect=NotFoundError(),
) as delete_collection:
result = _invoke(["delete-collection", "notes"])
delete_collection.assert_called_once_with("notes")
self.assertEqual(result.exit_code, 1)
2026-04-24 18:23:02 +02:00
self.assertEqual(result.stdout, "Error: Collection 'notes' does not exist.\n")
2026-04-22 22:21:14 +02:00
def test_count(self) -> None:
with patch(
"chromy.handlers.count_collection.count_collection",
return_value=7,
) as count_collection:
result = _invoke(["count", "notes"])
count_collection.assert_called_once_with("notes")
self.assertEqual(result.exit_code, 0)
2026-04-29 12:44:28 +02:00
self.assertEqual(
result.stdout,
"The 'notes' collection contains 7 records.\n",
)
2026-04-22 22:21:14 +02:00
2026-04-23 19:34:59 +02:00
def test_import_data(self) -> None:
2026-04-22 22:21:14 +02:00
with patch(
2026-04-23 19:34:59 +02:00
"chromy.handlers.import_data.ingest_file",
2026-04-22 22:21:14 +02:00
return_value=3,
) as ingest_file:
2026-04-23 19:34:59 +02:00
result = _invoke(["import", "notes", "romeo_and_juliet.txt"])
2026-04-22 22:21:14 +02:00
2026-04-23 19:56:11 +02:00
ingest_file.assert_called_once_with(
"notes",
self._fixture_path("romeo_and_juliet.txt"),
)
2026-04-22 22:21:14 +02:00
self.assertEqual(result.exit_code, 0)
2026-04-29 15:39:42 +02:00
self.assertEqual(
result.stdout,
"Added 3 records from 'romeo_and_juliet.txt' to collection 'notes'.\n"
"Imported 1 file(s) successfully; 0 failed.\n",
)
def test_import_data_accepts_multiple_files(self) -> None:
with patch(
"chromy.handlers.import_data.ingest_file",
side_effect=[3, 2],
) as ingest_file:
result = _invoke(
["import", "notes", "romeo_and_juliet.txt", "README.md"],
)
self.assertEqual(ingest_file.call_count, 2)
ingest_file.assert_any_call(
"notes",
self._fixture_path("romeo_and_juliet.txt"),
)
ingest_file.assert_any_call(
"notes",
self._fixture_path("README.md"),
)
self.assertEqual(result.exit_code, 0)
self.assertEqual(
result.stdout,
"Added 3 records from 'romeo_and_juliet.txt' to collection 'notes'.\n"
"Added 2 records from 'README.md' to collection 'notes'.\n"
"Imported 2 file(s) successfully; 0 failed.\n",
)
def test_import_data_continues_after_missing_file(self) -> None:
with patch(
"chromy.handlers.import_data.ingest_file",
return_value=3,
) as ingest_file:
result = _invoke(
["import", "notes", "missing.txt", "romeo_and_juliet.txt"],
)
ingest_file.assert_called_once_with(
"notes",
self._fixture_path("romeo_and_juliet.txt"),
)
self.assertEqual(result.exit_code, 1)
self.assertEqual(
result.stdout,
"Error: The file 'missing.txt' was not found.\n"
"Added 3 records from 'romeo_and_juliet.txt' to collection 'notes'.\n"
"Imported 1 file(s) successfully; 1 failed.\n",
)
2026-04-22 22:21:14 +02:00
2026-04-29 12:44:28 +02:00
def test_import_data_rejects_non_text_files(self) -> None:
with patch(
"chromy.handlers.import_data.is_probably_text_file",
return_value=False,
):
result = _invoke(["import", "notes", "romeo_and_juliet.txt"])
self.assertEqual(result.exit_code, 1)
self.assertEqual(
result.stdout,
2026-04-29 15:39:42 +02:00
"Error: The file 'romeo_and_juliet.txt' is not a text file.\n"
"Imported 0 file(s) successfully; 1 failed.\n",
)
def test_import_data_treats_literal_glob_as_missing_file(self) -> None:
result = _invoke(["import", "notes", "*.md"])
self.assertEqual(result.exit_code, 1)
self.assertEqual(
result.stdout,
"Error: The file '*.md' was not found.\n"
"Imported 0 file(s) successfully; 1 failed.\n",
)
def test_import_data_deduplicates_paths_within_single_invocation(self) -> None:
with patch(
"chromy.handlers.import_data.ingest_file",
return_value=3,
) as ingest_file:
result = _invoke(
["import", "notes", "README.md", "./README.md"],
)
ingest_file.assert_called_once_with(
"notes",
self._fixture_path("README.md"),
)
self.assertEqual(result.exit_code, 0)
self.assertEqual(
result.stdout,
"Added 3 records from 'README.md' to collection 'notes'.\n"
"Imported 1 file(s) successfully; 0 failed.\n",
2026-04-29 12:44:28 +02:00
)
2026-04-22 22:21:14 +02:00
def test_query(self) -> None:
2026-04-22 22:14:26 +02:00
query_result = {"ids": [["1"]], "documents": [["hello"]]}
2026-04-22 22:21:14 +02:00
with (
patch("chromy.handlers.query.run_query", return_value=query_result) as run,
patch(
"chromy.handlers.query.format_query_result",
return_value=["Query results:", "1"],
) as format_result,
):
result = _invoke(["query", "notes", "Where is Romeo?"])
run.assert_called_once_with("notes", "Where is Romeo?")
format_result.assert_called_once_with(query_result)
self.assertEqual(result.exit_code, 0)
self.assertEqual(result.stdout, "Query results:\n1\n")
def test_delete_records(self) -> None:
with patch(
"chromy.handlers.delete_collection.delete_data",
return_value=2,
) as delete_data:
result = _invoke(
["delete", "notes", "--where", " file_name = play.txt "],
2026-04-22 22:14:26 +02:00
)
2026-04-22 22:21:14 +02:00
delete_data.assert_called_once_with("notes", {"file_name": "play.txt"})
self.assertEqual(result.exit_code, 0)
self.assertEqual(
result.stdout,
2026-04-24 18:23:02 +02:00
"Deleted 2 record(s) from collection 'notes' where file_name=play.txt.\n",
2026-04-22 22:21:14 +02:00
)
2026-04-22 22:14:26 +02:00
def test_invalid_delete_filter_keeps_user_facing_error(self) -> None:
result = _invoke(["delete", "notes", "--where", "file_name"])
self.assertEqual(result.exit_code, 1)
self.assertEqual(
result.stdout,
2026-04-23 21:49:46 +02:00
"Error: Invalid --where value. Expected <condition>=<value>.\n",
2026-04-22 22:14:26 +02:00
)
def test_delete_requires_where_option(self) -> None:
result = _invoke(["delete", "notes"])
self.assertNotEqual(result.exit_code, 0)
self.assertIn("Missing option", result.output)
def _invoke(arguments: Sequence[str]) -> Result:
return CliRunner().invoke(app, list(arguments))
if __name__ == "__main__":
unittest.main()