Push A (single-machine): - HLC + event-sourced SQLite (events table is source of truth, projections rebuildable) - Pydantic v2 domain models (Project, Task, Assignment, Session, Group, Update) - rclaude subprocess wrapper (local_sessions via _claude-projects --sessions) - Typer CLI: init, project, task, assign, pull, status, broadcast, serve, sync - FastAPI + Jinja2 + HTMX dashboard - 26 unit tests passing Push B (HTTP API + sync substrate): - /api/v1/* JSON routes (projects, tasks, assignments, sessions, status, broadcast, sync) - CLI refactored as thin httpx client over the API — single business-logic codepath - web/service.py: every business op defined once; HTML routes + API routes both call into it - sync.py: peer-to-peer sync via /api/v1/sync/events with HLC + uuid-based dedup - 32 tests passing including two-Clare convergence test Push C (cross-host deployment): - apricot install via uv (Python 3.12.12) - systemd --user unit for clare-serve on apricot - Cross-host sync demoed plum (10.9.0.3) ↔ apricot (10.9.0.2) over wg - .local → .lan rename for forge URLs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
49 lines
1.7 KiB
Python
49 lines
1.7 KiB
Python
from __future__ import annotations
|
|
|
|
from clare.hlc import HLC, HLCGenerator
|
|
|
|
|
|
def test_hlc_monotonic_within_same_wall_ms() -> None:
|
|
"""Two ticks in the same millisecond must bump the counter."""
|
|
clock = [1_000_000_000_000]
|
|
gen = HLCGenerator("m1", now_fn=lambda: clock[0])
|
|
a = gen.tick()
|
|
b = gen.tick()
|
|
assert a < b
|
|
assert a.wall_ms == b.wall_ms
|
|
assert b.counter == a.counter + 1
|
|
|
|
|
|
def test_hlc_resets_counter_on_wall_advance() -> None:
|
|
clock = [1_000_000_000_000]
|
|
gen = HLCGenerator("m1", now_fn=lambda: clock[0])
|
|
gen.tick()
|
|
gen.tick()
|
|
clock[0] += 50
|
|
later = gen.tick()
|
|
assert later.counter == 0
|
|
assert later.wall_ms == 1_000_000_000_050
|
|
|
|
|
|
def test_hlc_update_pulls_forward_from_remote() -> None:
|
|
"""Receiving an HLC from a peer with a higher wall_ms pulls us forward."""
|
|
clock = [1_000_000_000_000]
|
|
gen = HLCGenerator("m1", now_fn=lambda: clock[0])
|
|
remote = HLC(wall_ms=2_000_000_000_000, counter=5, machine_id="m2")
|
|
new = gen.update(remote)
|
|
assert new.wall_ms == remote.wall_ms
|
|
assert new.counter == remote.counter + 1
|
|
|
|
|
|
def test_hlc_encode_decode_roundtrip() -> None:
|
|
hlc = HLC(wall_ms=1716253199000, counter=42, machine_id="abc-123")
|
|
assert HLC.decode(hlc.encode()) == hlc
|
|
|
|
|
|
def test_hlc_string_ordering_matches_tuple_ordering() -> None:
|
|
"""Critical: HLCs sort the same way as strings as they do as tuples."""
|
|
a = HLC(wall_ms=1000, counter=1, machine_id="m1")
|
|
b = HLC(wall_ms=1000, counter=2, machine_id="m1")
|
|
c = HLC(wall_ms=1001, counter=0, machine_id="m1")
|
|
encoded = sorted([c.encode(), b.encode(), a.encode()])
|
|
assert encoded == [a.encode(), b.encode(), c.encode()]
|