Fixes found by investigating the live lobste.rs run:
- Comments came back 0: the learner probed the external article, not the
discussion page. A comments_url field (RSS <comments>, JSON key, or an HTML
"comments" anchor) now drives comment learning; the article is still used
for the post body.
- HTML id selectors collapsed every story into one identity (constant /login
vote link). Id/url selectors must now be distinct; no distinct selector
rejects the feed.
- Over-long posts were rejected; title, post and comment are truncated to the
sync limits and reported as a warning.
- A failed article fetch is a warning, not a run error.
- A gate refusal (sync disabled, origin not allowed, aliasing off) ends the run
as skipped and no longer counts toward the failure floor.
- The same story from two feeds becomes one post, comments attached to it.
- HTML comments follow rel=next up to max_comment_pages.
- A dry run now predicts an unknown-record tombstone as unchanged, like a live
run.
- /nodes is exempt from the happy-404 decoy so a missing download is a real 404.
Sync log (admin): every run stores a verdict, exact per-outcome counts and one
row per record needing an explanation, with the reason in plain words, at
/admin/communities/{slug}/ledger (HTML, JSON, Devii tool, docs entry).
Bounded: 2000 routine + 2000 problem rows per run, 50 per page, detail kept for
the 10 newest runs, four indexes matching every ORDER BY (asserted by an
EXPLAIN QUERY PLAN test). Measured worst case 37 ms at the 40000-row ceiling.
Also: privacy disclosure for imported community content, status colour channel
tokens, and tests/unit/cli/nodes.py renamed to fix a duplicate-basename
collection error.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
45 lines
1.5 KiB
Python
45 lines
1.5 KiB
Python
# retoor <retoor@molodetz.nl>
|
|
|
|
import argparse
|
|
|
|
from devplacepy.cli import nodes as cli
|
|
from devplacepy.database import get_table
|
|
from devplacepy.database import nodes as store
|
|
from devplacepy.utils import generate_uid
|
|
|
|
|
|
def parser():
|
|
root = argparse.ArgumentParser()
|
|
cli.register_nodes(root.add_subparsers(dest="command"))
|
|
return root
|
|
|
|
|
|
def device(local_db):
|
|
row, _ = store.create_device(generate_uid(), "cli", "native")
|
|
return row["uid"]
|
|
|
|
|
|
def test_suspend_and_unsuspend(local_db, capsys):
|
|
uid = device(local_db)
|
|
args = parser().parse_args(["nodes", "suspend", uid, "--minutes", "5"])
|
|
args.func(args)
|
|
assert store.effective_status(get_table("node_devices").find_one(uid=uid)) == "suspended"
|
|
args = parser().parse_args(["nodes", "unsuspend", uid])
|
|
args.func(args)
|
|
assert store.effective_status(get_table("node_devices").find_one(uid=uid)) == "active"
|
|
|
|
|
|
def test_clear_is_a_dry_run_without_force(local_db, capsys):
|
|
get_table("node_events").insert({"uid": generate_uid(), "kind": "x", "detail": "", "created_at": "2026-01-01T00:00:00+00:00"})
|
|
before = store.clear_ephemeral(dry_run=True)["node_events"]
|
|
args = parser().parse_args(["nodes", "clear"])
|
|
args.func(args)
|
|
assert "pass --force" in capsys.readouterr().out
|
|
assert store.clear_ephemeral(dry_run=True)["node_events"] == before
|
|
|
|
|
|
def test_prune_dry_run_reports_counts(local_db, capsys):
|
|
args = parser().parse_args(["nodes", "prune", "--dry-run"])
|
|
args.func(args)
|
|
assert "would remove" in capsys.readouterr().out
|