Home's "recently modified" order was a proxy — drafts first, then the version counter, then the name. Both summaries now carry `updated_at`, read as the mtime of the working copy: every write in the store commits immediately, so a file's mtime is its commit time, and `git log -1 -- <path>` costs ~990ms across this instance's 15 documents (it walks the history back to the last commit touching each one, so it is slowest for the stalest) against ~1.8ms for the stats. No cache needed. `DashboardSummary` also carries `footprint`: each widget as its type plus the placement a panel resolves, which is the whole of what the mosaic draws. That drops the document fetch per tile and with it the cap of eight, past which tiles showed a name and nothing else. Verified against this instance's 8 dashboards: the blocks are identical to what the old client-side derivation produced. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CL9zvnnvcp1mvA8o7impxk
134 lines
3.8 KiB
Python
134 lines
3.8 KiB
Python
import os
|
|
import subprocess
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from fluksio.flow.messages import MessageSpec
|
|
from fluksio.flow.schemas import FlowDef, NodeDef
|
|
from fluksio.flow.store import FlowExists, FlowNotFound, FlowStore
|
|
|
|
|
|
@pytest.fixture
|
|
def store(tmp_path: Path) -> FlowStore:
|
|
return FlowStore(tmp_path / "flows")
|
|
|
|
|
|
def commit_count(store: FlowStore) -> int:
|
|
result = subprocess.run(
|
|
["git", "-C", str(store.root), "rev-list", "--count", "HEAD"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
return int(result.stdout.strip())
|
|
|
|
|
|
def a_flow() -> FlowDef:
|
|
return FlowDef(
|
|
name="heating",
|
|
nodes=[NodeDef(id="sensor", provides=[MessageSpec(name="temp")])],
|
|
)
|
|
|
|
|
|
def test_flow_round_trips(store: FlowStore):
|
|
store.write_flow(a_flow())
|
|
|
|
assert store.list_flows() == ["heating"]
|
|
assert store.read_flow("heating").nodes[0].provides[0].name == "temp"
|
|
|
|
|
|
def test_every_change_is_committed(store: FlowStore):
|
|
before = commit_count(store)
|
|
|
|
store.write_flow(a_flow())
|
|
assert commit_count(store) == before + 1
|
|
|
|
store.write_node_source("heating", "sensor", "def process():\n return {}\n")
|
|
assert commit_count(store) == before + 2
|
|
|
|
|
|
def test_saving_unchanged_content_does_nothing(store: FlowStore):
|
|
store.write_flow(a_flow())
|
|
commits = commit_count(store)
|
|
|
|
# Autosave repeats the same document; history should not grow.
|
|
assert store.write_flow(a_flow()) is False
|
|
assert commit_count(store) == commits
|
|
|
|
|
|
def test_a_flow_is_modified_when_only_its_node_code_is(store: FlowStore):
|
|
"""Node code is saved without touching the flow document."""
|
|
store.write_flow(a_flow())
|
|
# Backdated so the answer can only have come from the node file.
|
|
os.utime(store.root / "heating" / "flow.json", (0, 0))
|
|
|
|
store.write_node_source(
|
|
"heating", "sensor", "def process():\n return {'a': 1}\n"
|
|
)
|
|
|
|
updated = store.updated_at("heating")
|
|
assert updated is not None and updated > datetime.fromtimestamp(0, UTC)
|
|
|
|
|
|
def test_a_flow_that_is_not_there_has_no_modified_time(store: FlowStore):
|
|
assert store.updated_at("nope") is None
|
|
|
|
|
|
def test_missing_flow_is_reported(store: FlowStore):
|
|
with pytest.raises(FlowNotFound):
|
|
store.read_flow("nope")
|
|
|
|
|
|
def test_deleting_removes_flow_and_its_nodes(store: FlowStore):
|
|
store.write_flow(a_flow())
|
|
store.write_node_source("heating", "sensor", "def process():\n return {}\n")
|
|
|
|
store.delete_flow("heating")
|
|
|
|
assert store.list_flows() == []
|
|
assert not (store.root / "heating").exists()
|
|
|
|
|
|
def test_renaming_a_flow_carries_its_nodes(store: FlowStore):
|
|
store.write_flow(a_flow())
|
|
store.write_node_source("heating", "sensor", "def process():\n return {}\n")
|
|
|
|
renamed = store.rename_flow("heating", "warmth")
|
|
|
|
assert renamed.name == "warmth"
|
|
assert store.list_flows() == ["warmth"]
|
|
assert "def process" in store.read_node_source("warmth", "sensor")
|
|
|
|
|
|
def test_renaming_a_flow_repoints_the_flows_reading_it(store: FlowStore):
|
|
store.write_flow(a_flow())
|
|
store.write_flow(
|
|
FlowDef(
|
|
name="display",
|
|
nodes=[
|
|
NodeDef(
|
|
id="gauge",
|
|
# Reads across the flow boundary, so the name must follow.
|
|
requires=[MessageSpec(name="heating.temp")],
|
|
)
|
|
],
|
|
)
|
|
)
|
|
|
|
store.rename_flow("heating", "warmth")
|
|
|
|
display = store.read_flow("display")
|
|
assert display.nodes[0].requires[0].name == "warmth.temp"
|
|
|
|
|
|
def test_renaming_onto_an_existing_name_is_refused(store: FlowStore):
|
|
store.write_flow(a_flow())
|
|
store.write_flow(FlowDef(name="warmth"))
|
|
|
|
with pytest.raises(FlowExists):
|
|
store.rename_flow("heating", "warmth")
|
|
|
|
assert store.list_flows() == ["heating", "warmth"]
|