diff --git a/README.md b/README.md index fd9b475..4eda415 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,11 @@ Sources: That `[mock]` answer plus **Sources** means retrieval works before you spend a token. If titles or scores drift, the note corpus or scorer changed — open an issue before "fixing" ranking by eye. +If no notes match (including an empty notes folder), the command prints a notice +to stderr and exits with status `1` before generating an answer. Try different +words or add a Markdown note to the folder selected by `--notes`. Stdout stays +empty, even with `--json`; no answer or Sources block is printed. + ## Real answers (optional) ```bash diff --git a/src/notes_rag/cli.py b/src/notes_rag/cli.py index 99610fb..989719d 100644 --- a/src/notes_rag/cli.py +++ b/src/notes_rag/cli.py @@ -51,6 +51,12 @@ def main(argv: list[str] | None = None) -> int: return 1 notes = load_notes(notes_dir) hits = retrieve(notes, args.question, k=args.k) + if not hits: + print( + f"no notes matched — try different words or add a note under {notes_dir}", + file=sys.stderr, + ) + return 1 answerer = Answerer.from_env(mock=args.mock) try: answer = answerer.answer(args.question, hits) diff --git a/tests/test_notes_rag.py b/tests/test_notes_rag.py index 37f9872..a36d054 100644 --- a/tests/test_notes_rag.py +++ b/tests/test_notes_rag.py @@ -1,6 +1,8 @@ import json from pathlib import Path +import pytest + from notes_rag.answer import Answerer from notes_rag.cli import main from notes_rag.retrieve import load_notes, retrieve @@ -61,9 +63,27 @@ def test_empty_notes_dir(tmp_path, capsys): empty = tmp_path / "empty" empty.mkdir() code = main(["--mock", "--notes", str(empty), "anything"]) - out = capsys.readouterr().out - assert code == 0 - assert "could not find" in out.lower() + captured = capsys.readouterr() + assert code == 1 + assert captured.out == "" + assert "no notes matched" in captured.err + assert str(empty) in captured.err + + +@pytest.mark.parametrize("flags", [[], ["--mock"], ["--json"], ["--mock", "--json"]]) +def test_no_matching_notes_reports_failure(flags, capsys, monkeypatch): + def unexpected_answerer(**kwargs): + pytest.fail("empty retrieval must not construct an answerer") + + monkeypatch.setattr(Answerer, "from_env", unexpected_answerer) + code = main([*flags, "--notes", str(NOTES), "qzxvplm"]) + captured = capsys.readouterr() + assert code == 1 + assert captured.out == "" + assert "no notes matched" in captured.err + assert "try different words or add a note" in captured.err + assert str(NOTES) in captured.err + assert "Sources:" not in captured.out + captured.err def test_missing_key_without_mock():