From 032e7b633295046d77a5645b5857c451bcdc80fd Mon Sep 17 00:00:00 2001 From: Patrick <320190286+muellerei@users.noreply.github.com> Date: Sat, 3 Oct 2026 03:25:59 +0200 Subject: [PATCH 1/2] Derive journal fixture dates from the clock, not from 2026 Ten tests broke on 2026-10-11 and one more on 2027-01-01. Their fixtures held fixed journal days (2026-09-10, 2026-08-xx) while the commands under test filter against "last 30 days" / "this year". Once the fixture left the window, the command saw an empty journal and the assertions failed. journal_day() in conftest gives a journalDay relative to today. The analyze-journal-patterns and mood tests use it, and the get-journal-summary tests get pages inside the current year. Two tests in test_journal_bounded_output looped over entries and passed over an empty list: the no-content test (silently, from 2027) and the heading test (with an empty journal at any date). Both now assert the list is not empty; an empty journal makes each of them fail. Checked with a faked clock on 15 dates through 2028 (month ends, year change, leap day) at 00:00 and 23:59: all 3265 tests pass. With the fixtures reverted, the same sweep fails on 2026-10-11 (10 tests) and 2027-01-01. --- CHANGELOG.md | 12 ++++++++++++ tests/conftest.py | 12 ++++++++++++ tests/test_analyze_journal_patterns.py | 4 ++-- tests/test_journal_bounded_output.py | 15 ++++++++++++++- tests/test_read_only_commands_smoke.py | 4 ++-- 5 files changed, 42 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7a5295..410c551 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -210,6 +210,18 @@ file carries most of the reasoning behind the tool. holding it from its file. Logseq then keeps a placeholder `id:: ` without a page (#70), which was inlined as the ref's text. +- The suite would have gone red with no change to the code: ten tests failed + from 2026-10-11 and one more from 2027-01-01. Their fixtures held a fixed + journal day (2026-09-10, August 2026) while the commands under test filter + against "last 30 days" or "this year", so once the day left the window the + command saw an empty journal and the assertions failed. `journal_day()` in + `tests/conftest.py` now derives the day from the clock. Reproduced with a + faked clock on 15 dates through 2028 (month ends, year change, leap day) at + 00:00 and 23:59; all 3265 tests pass on each. No command changed. + - Two tests looped over the entries of a result and would pass over an empty + one: `get-journal-summary --no-content`, silently from 2027, and + `get-journal-range --heading`, on an empty journal at any date. Both now + assert the list is not empty. ## [0.16.0] - 2026-09-26 Every write is now proven, and none overwrites a block you are editing. diff --git a/tests/conftest.py b/tests/conftest.py index 100bd9b..c068f09 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ """Shared test helpers.""" +import datetime import re import pytest @@ -120,6 +121,17 @@ def mock_api(**overrides): return api +def journal_day(days_ago=1): + """Logseq's ``journalDay`` integer for a day relative to today. + + A fixed date works until it leaves a relative window such as + ``--timeframe "last 30 days"``; then every test built on it silently + sees an empty journal. Derive the day from the clock instead. + """ + day = datetime.date.today() - datetime.timedelta(days=days_ago) + return int(day.strftime("%Y%m%d")) + + def split_runner(): """CliRunner that captures stderr separately from stdout. diff --git a/tests/test_analyze_journal_patterns.py b/tests/test_analyze_journal_patterns.py index 3bc7ae7..ff1c37f 100644 --- a/tests/test_analyze_journal_patterns.py +++ b/tests/test_analyze_journal_patterns.py @@ -12,9 +12,9 @@ from logseq_cli.cli import cli -from tests.conftest import split_runner +from tests.conftest import journal_day, split_runner -JOURNAL = {"originalName": "2026-09-10", "journalDay": 20260910, "journal?": True} +JOURNAL = {"originalName": "J", "journalDay": journal_day(), "journal?": True} def run(text, *args, config=None, tmp_path=None): diff --git a/tests/test_journal_bounded_output.py b/tests/test_journal_bounded_output.py index e5b457d..123d2ae 100644 --- a/tests/test_journal_bounded_output.py +++ b/tests/test_journal_bounded_output.py @@ -15,7 +15,7 @@ from click.testing import CliRunner from logseq_cli.cli import cli -from tests.conftest import split_runner +from tests.conftest import journal_day, split_runner def _journal_pages(days): @@ -105,6 +105,7 @@ def test_heading_narrows_to_one_section(self, api): "get-journal-range", "--from", "2026-08-01", "--to", "2026-08-02", "--heading", "## Log", "--json"]) entries = json.loads(result.stdout) + assert entries, "the loop below would pass over nothing" for entry in entries: contents = [b["content"] for b in entry["blocks"]] assert contents == ["## Log"] @@ -118,6 +119,17 @@ def test_without_heading_all_sections_present(self, api): class TestJournalSummaryNoContent: + @pytest.fixture(autouse=True) + def pages_in_this_year(self, api): + """``--range "this year"`` needs pages inside the current year. + + The module fixture's August 2026 pages fall out of it from 1 Jan 2027; the + no-content test then loops over zero entries and passes for nothing. + """ + day = journal_day(0) + api.get_all_pages.return_value = [ + {"originalName": "J", "name": "j", "journalDay": day}] + def test_no_content_drops_bodies_but_keeps_length(self, api, monkeypatch): monkeypatch.setattr("logseq_cli.commands.journal.get_page_content", lambda api_, name: "x" * 500 + " [[Alice]]") @@ -125,6 +137,7 @@ def test_no_content_drops_bodies_but_keeps_length(self, api, monkeypatch): "get-journal-summary", "--range", "this year", "--no-content", "--json"]) payload = json.loads(result.stdout) assert payload["content_omitted"] is True + assert payload["entries"], "the loop below would pass over nothing" for entry in payload["entries"]: assert "content" not in entry assert entry["content_length"] == 510 diff --git a/tests/test_read_only_commands_smoke.py b/tests/test_read_only_commands_smoke.py index 05a11c4..ef3d572 100644 --- a/tests/test_read_only_commands_smoke.py +++ b/tests/test_read_only_commands_smoke.py @@ -14,7 +14,7 @@ import pytest from logseq_cli.cli import cli -from tests.conftest import split_runner +from tests.conftest import journal_day, split_runner COMMANDS = [ ("analyze-graph", []), @@ -289,7 +289,7 @@ def _run(self, text, tmp_path): cfg.write_text(self.CONFIG, encoding="utf-8") api = MagicMock() api.get_all_pages.return_value = [ - {"originalName": "J", "journalDay": 20260910, "journal?": True}] + {"originalName": "J", "journalDay": journal_day(), "journal?": True}] with patch.dict(os.environ, {"LOGSEQ_CLI_CONFIG": str(cfg)}, clear=False), \ patch("logseq_cli.group.LogseqAPI", return_value=api), \ patch("logseq_cli.commands.analysis.get_page_content", return_value=text): From 6f5e4e72f69dcc569d0486ffe38f9e9577bf7a2d Mon Sep 17 00:00:00 2001 From: Patrick <320190286+muellerei@users.noreply.github.com> Date: Sat, 3 Oct 2026 04:17:36 +0200 Subject: [PATCH 2/2] Assert the effect of a command in tests that only checked its exit code Sixteen tests passed whether or not the command did anything. Most asserted exit code 0 and, at most, that something was not written; one searched stderr for "dead" where the message reads "no longer exists", so it could not fail at all. A command that silently returned early passed them. Each now asserts something that exists only if the command acted: the written block, the rendered page, the dry-run notice, the query sent, the JSON payload. test_survives_an_empty_graph now requires exit 0 and JSON on an empty graph instead of allowing SystemExit. Found by turning CliRunner.invoke into a no-op (exit 0, no output) and listing the tests that still passed, then reading each against a twin that proves the effect. 135 passed under the no-op; 16 had no such twin. Checked: with invoke a no-op, fourteen of them now fail. The other two cannot show it that way and were mutated separately: forcing resolve_refs on in get-page makes the new dead-ref assertion fail where the old one passed, and --from 2099, which drops every task, makes the get-todos assertion fail. --- CHANGELOG.md | 13 +++++++++++++ tests/test_bounded_page_reads.py | 2 ++ tests/test_create_page_exists.py | 2 ++ tests/test_dead_block_refs.py | 3 ++- tests/test_dry_run_coverage.py | 1 + tests/test_get_properties_lookup.py | 1 + tests/test_get_todos.py | 1 + tests/test_journal_uuid_and_alias.py | 1 + tests/test_missing_page_created_first.py | 1 + tests/test_one_block_round_trip.py | 3 +++ tests/test_read_only.py | 3 +++ tests/test_read_only_commands_smoke.py | 4 ++-- tests/test_rename_target.py | 1 + 13 files changed, 33 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 410c551..ad77178 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -222,6 +222,19 @@ file carries most of the reasoning behind the tool. one: `get-journal-summary --no-content`, silently from 2027, and `get-journal-range --heading`, on an empty journal at any date. Both now assert the list is not empty. +- Sixteen tests passed whether or not the command did anything. They asserted + exit code 0 and, at most, that something was not written; one searched + stderr for "dead" where the message reads "no longer exists", so it could + not fail. Found by making `CliRunner.invoke` a no-op (exit 0, no output): + 135 tests still passed, and for 16 of them no twin test proved that the + command acted. Each now asserts what exists only if it did: the written + block, the rendered page, the dry-run notice, the query sent, the JSON + payload. With the no-op, fourteen of them fail; the other two were mutated + on their own (forcing `--resolve-refs` on makes the new dead-ref assertion + fail where the old one passed; `get-todos --from 2099`, which drops every + task, fails the new one). The empty-graph smoke test now requires exit 0 and + JSON where it allowed `SystemExit`. No command changed. + ## [0.16.0] - 2026-09-26 Every write is now proven, and none overwrites a block you are editing. diff --git a/tests/test_bounded_page_reads.py b/tests/test_bounded_page_reads.py index 05dd8a1..5b489f8 100644 --- a/tests/test_bounded_page_reads.py +++ b/tests/test_bounded_page_reads.py @@ -551,11 +551,13 @@ def test_names_that_only_casefold_alike_are_two_pages(self): result, _ = _run(["get-page", "--name", "final", "--name", "final", "--max-chars", "100000", "--no-backlinks"], _big_tree()) assert result.exit_code == 0, result.output + assert "=== final ===" in result.stdout and "=== final ===" in result.stdout def test_a_page_named_twice_still_reads_without_a_cap(self): result, _ = _run(["get-page", "--name", "A", "--name", "A", "--no-backlinks"], _big_tree()) assert result.exit_code == 0 + assert result.stdout.count("=== A ===") == 2, result.stdout def test_markdown_continuation_keeps_the_bullet_of_a_properties_block(self): """Only the page's own first block is written without a bullet; a diff --git a/tests/test_create_page_exists.py b/tests/test_create_page_exists.py index 23c3387..08b0523 100644 --- a/tests/test_create_page_exists.py +++ b/tests/test_create_page_exists.py @@ -167,6 +167,8 @@ def test_create_page_proof_survives_unicode_normalisation(monkeypatch): assert name != unicodedata.normalize("NFC", name) r = _invoke(double, ["create-page", "--page", name]) assert r.exit_code == 0, r.stderr + assert [a[0] for a in double.sent("createPage")] == [name] + assert json.loads(r.stdout)["created"] == name @pytest.mark.parametrize("name,day", [ diff --git a/tests/test_dead_block_refs.py b/tests/test_dead_block_refs.py index a4f8ddf..d80cf29 100644 --- a/tests/test_dead_block_refs.py +++ b/tests/test_dead_block_refs.py @@ -88,4 +88,5 @@ def test_page_without_dead_refs_says_nothing(self, api): def test_without_resolve_refs_nothing_is_claimed(self, api): """No lookups happen, so no claim about liveness can be made.""" result = split_runner().invoke(cli, ["get-page", "--page", "Page"]) - assert "dead" not in result.stderr.lower(), result.stderr + assert "no longer exists" not in result.stderr, result.stderr + api.get_block.assert_not_called() diff --git a/tests/test_dry_run_coverage.py b/tests/test_dry_run_coverage.py index 74995c7..fca0e5d 100644 --- a/tests/test_dry_run_coverage.py +++ b/tests/test_dry_run_coverage.py @@ -761,5 +761,6 @@ def test_add_journal_entry_dry_run_creates_no_page(self): result = CliRunner().invoke( cli, ["add-journal-entry", "--content", "Entry", "--dry-run"]) assert result.exit_code == 0, result.output + assert "Would add 1 block" in result.output, result.output api.create_page.assert_not_called() api.append_block_in_page.assert_not_called() diff --git a/tests/test_get_properties_lookup.py b/tests/test_get_properties_lookup.py index 59c9a78..4d6751b 100644 --- a/tests/test_get_properties_lookup.py +++ b/tests/test_get_properties_lookup.py @@ -58,6 +58,7 @@ def test_single_word_key_unchanged(self): def test_uppercase_typed_single_word_still_matches(self): r = _invoke(_api_with_page({"type": "Person"}), "Type") assert r.exit_code == 0, r.output + assert json.loads(r.stdout)["value"] == "Person" def test_missing_key_still_fails(self): r = _invoke(_api_with_page({"type": "Person"}), "team") diff --git a/tests/test_get_todos.py b/tests/test_get_todos.py index 14539ea..47f6026 100644 --- a/tests/test_get_todos.py +++ b/tests/test_get_todos.py @@ -164,6 +164,7 @@ def test_only_from_still_drops_undated(self): result = runner.invoke(cli, ["get-todos", "--from", "2026-05-01", "--json"]) assert result.exit_code == 0, result.output contents = [t["content"] for t in _json.loads(result.output)["todos"]] + assert any("in range" in c for c in contents), contents assert not any("plain page" in c for c in contents), ( f"task without a journal date passed a one-sided range: {contents!r}" ) diff --git a/tests/test_journal_uuid_and_alias.py b/tests/test_journal_uuid_and_alias.py index 2256ed9..3ab4dbc 100644 --- a/tests/test_journal_uuid_and_alias.py +++ b/tests/test_journal_uuid_and_alias.py @@ -67,6 +67,7 @@ def test_find_block_accepts_name(self): "find-block", "--content", "x", "--name", "SomePage", "--json", ]) assert r.exit_code == 0, r.output # would be 2 if --name were unknown + assert '[?p :block/name "somepage"]' in api.datascript_query.call_args[0][0] def test_insert_block_accepts_name(self): api = MagicMock() diff --git a/tests/test_missing_page_created_first.py b/tests/test_missing_page_created_first.py index 7148893..3b60314 100644 --- a/tests/test_missing_page_created_first.py +++ b/tests/test_missing_page_created_first.py @@ -102,6 +102,7 @@ def test_preview_creates_nothing(monkeypatch, args): _source(api) result = _run(*args, "--dry-run") assert result.exit_code == 0, result.stderr + assert "DRY RUN" in result.stdout, result.stdout api.create_page.assert_not_called() api.append_block_in_page.assert_not_called() diff --git a/tests/test_one_block_round_trip.py b/tests/test_one_block_round_trip.py index ef4b492..4a5509f 100644 --- a/tests/test_one_block_round_trip.py +++ b/tests/test_one_block_round_trip.py @@ -201,6 +201,7 @@ def test_accepted(args): api = _graph() r = _run(args, api) assert r.exit_code == 0, r.stderr + assert _writes(api) >= 1 # --- writes of text that already exists --------------------------------------- @@ -245,6 +246,7 @@ def test_replace_text_and_set_todo_status_leave_an_existing_line_alone(): assert r.exit_code == 0, r.stderr r = _run(["set-todo-status", "--id", ANCHOR, "--status", "DONE"], api) assert r.exit_code == 0, r.stderr + assert api.update_block.call_args.args[1] == "DONE a term\n- b" # --- the heading written first, and the words of each refusal ----------------- @@ -315,3 +317,4 @@ def test_replace_text_may_change_the_text_of_a_line_the_block_had(): api.graph.locate(u)[1][api.graph.locate(u)[2]].update(content=c) r = _run(["replace-text", "--page", "Page A", "--find", "old", "--replace", "new"], api) assert r.exit_code == 0, r.stderr + assert api.update_block.call_args.args[1] == "TODO a\n- new" diff --git a/tests/test_read_only.py b/tests/test_read_only.py index 5432cc9..0b80592 100644 --- a/tests/test_read_only.py +++ b/tests/test_read_only.py @@ -274,6 +274,7 @@ def test_a_reader_runs(self, tmp_path, monkeypatch, double): monkeypatch.setenv("LOGSEQ_CLI_CONFIG", str(tmp_path / "gone.toml")) result = invoke("get-page", "--page", "Probe Page") assert result.exit_code == 0 + assert "alpha block" in result.stdout def test_no_variable_and_no_file_is_a_normal_run(self, sent): result = invoke("add-journal-block", "--content", "x") @@ -330,6 +331,7 @@ def test_a_reader_is_not_stopped_by_a_slip_in_safety(self, config, double): config("[safety]\nreadonly = true\n") result = invoke("get-page", "--page", "Probe Page") assert result.exit_code == 0 + assert "alpha block" in result.stdout def test_a_reader_that_loads_the_config_warns_and_goes_on(self, config, capsys): config("[safety]\nreadonly = true\n") @@ -552,6 +554,7 @@ def test_a_config_without_safety_gets_none(self, config, double): path = config('[journal]\ndefault_heading = "## Old"\n') result = invoke("init", "--force") assert result.exit_code == 0 + assert "## Old" not in path.read_text(encoding="utf-8") assert "[safety]" not in path.read_text(encoding="utf-8") assert "Kept" not in result.stderr diff --git a/tests/test_read_only_commands_smoke.py b/tests/test_read_only_commands_smoke.py index ef3d572..30fd02e 100644 --- a/tests/test_read_only_commands_smoke.py +++ b/tests/test_read_only_commands_smoke.py @@ -72,8 +72,8 @@ def test_survives_an_empty_graph(command, args, tmp_path): with patch.dict(os.environ, {"LOGSEQ_CLI_CONFIG": str(cfg)}, clear=False), \ patch("logseq_cli.group.LogseqAPI", return_value=empty_api()): result = split_runner().invoke(cli, ["--token", "X", command, *args, "--json"]) - assert result.exception is None or isinstance(result.exception, SystemExit), \ - f"{command} raised {result.exception!r}" + assert result.exit_code == 0, f"{command}: {result.exception!r} {result.stderr}" + json.loads(result.stdout) def graph_with(pages_topics): diff --git a/tests/test_rename_target.py b/tests/test_rename_target.py index ce065f4..5e09f5b 100644 --- a/tests/test_rename_target.py +++ b/tests/test_rename_target.py @@ -117,6 +117,7 @@ def test_a_change_of_case_is_allowed(double, extra): assert r.exit_code == 0, r.stderr if extra: assert double.sent("renamePage") == [] + assert json.loads(r.stdout)["new_name"] == "OLD page" return assert double.sent("renamePage") == [["Old Page", "OLD page"]] page = LogseqAPI(token="t").get_page("old page")