Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,31 @@ file carries most of the reasoning behind the tool.
holding it from its file. Logseq then keeps a placeholder `id:: <uuid>`
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.
- 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.
Expand Down
12 changes: 12 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Shared test helpers."""
import datetime
import re

import pytest
Expand Down Expand Up @@ -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.

Expand Down
4 changes: 2 additions & 2 deletions tests/test_analyze_journal_patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 2 additions & 0 deletions tests/test_bounded_page_reads.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions tests/test_create_page_exists.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", [
Expand Down
3 changes: 2 additions & 1 deletion tests/test_dead_block_refs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
1 change: 1 addition & 0 deletions tests/test_dry_run_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
1 change: 1 addition & 0 deletions tests/test_get_properties_lookup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions tests/test_get_todos.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
)
Expand Down
15 changes: 14 additions & 1 deletion tests/test_journal_bounded_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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"]
Expand All @@ -118,13 +119,25 @@ 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]]")
result = CliRunner().invoke(cli, [
"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
Expand Down
1 change: 1 addition & 0 deletions tests/test_journal_uuid_and_alias.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions tests/test_missing_page_created_first.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
3 changes: 3 additions & 0 deletions tests/test_one_block_round_trip.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---------------------------------------
Expand Down Expand Up @@ -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 -----------------
Expand Down Expand Up @@ -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"
3 changes: 3 additions & 0 deletions tests/test_read_only.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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

Expand Down
8 changes: 4 additions & 4 deletions tests/test_read_only_commands_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", []),
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions tests/test_rename_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading