parity: every public function of MessageGraph ported branch-for-branch —
_load (schema-v1 gate, corrupt/wrong-schema silent-empty, duplicate ids
last-wins with BOTH order entries kept, verbatim extra fields, non-dict
doc AttributeError, non-list nodes TypeError), save (arrival-order doc;
mkdir + json.dumps indent=2 FILE IO stays in the facade), add (the falsy
law: an explicit None parent still picks up the head but carries no edge,
a falsy "" parent stores "" and also carries no edge, ghost parents
allowed, kind checked before edge, Python-repr error strings), tool_call
(json.dumps content rendering stays in the facade — serialization
isomorphism), tool_result (existence check only — any node can parent a
result), revise (the root-revision quirk: old parent None substitutes
the current head, which at add-time is the old node itself), branch,
lineage (superseded skip, ghost superseded_by fallthrough, seen-set
cycle guard, missing-parent break), history (supersedes walk back to
root then forward via superseded_by — the FULL trail from any point on
it), migrate_flat (role/content defaults, existing-file seed), cue
(first supersedes/branches_from edge in iteration order wins, falsy
superseded_by is no cue).
Determinism seams: the uuid4 hex id and the _now() timestamp are kernel
ARGUMENTS (id/ts) — the frozen truth replays them verbatim. Graph wire:
{nodes: {id: node} insertion-ordered (serde_json preserve_order),
order: [ids]} — nodes ride as opaque Values so loaded extra fields
survive byte-verbatim. Errors ride typed (ValueError/KeyError/
AttributeError/TypeError) and the loader raises the matching Python
exception. The facade keeps the oracle's guard-clause ORDER (kind/edge/
existence checks BEFORE the uuid draw) so the id sequence is identical
under both cores. mg_loader mirrors broker_loader (doc-envelope,
cs_free_string, MG_RUST_LIB override); RUNE_MG_CORE=python pins the
fallback.
The freeze caught a real divergence first: Python 3.14's string-index
TypeError now reads "string indices must be integers, not 'str'" — the
kernel mirrors the runtime's wording, pinned by truth. Battery: 12
tests replay 68 frozen-oracle cases VERBATIM (include_str!); live
parity 68/68 byte-identical (truth regenerated through the kernel path
and diffed against the freeze); clippy 0; Python suite green under both
pins (9+9).
655 lines
29 KiB
Python
655 lines
29 KiB
Python
#!/usr/bin/env python3
|
|
"""Freeze message_graph oracle truth for the Rust port battery (dichotomy
|
|
wave #2479 tranche).
|
|
|
|
Exercises EVERY public function of src/rune/message_graph.py across every
|
|
branch: load (missing file, corrupt JSON, wrong/missing schema, valid doc,
|
|
duplicate ids, verbatim extra fields, non-dict doc -> AttributeError,
|
|
non-list nodes -> TypeError), save (doc shape + file bytes incl. mkdir),
|
|
add (default-parent chain, explicit parent with/without edge, explicit
|
|
None -> head, falsy parent "", edge to a ghost parent, unicode, every
|
|
kind, bad kind/edge -> ValueError), tool_call (json.dumps content shape,
|
|
nested args, parent / default head), tool_result (link, missing call ->
|
|
KeyError, non-tool-call parent allowed), revise (root revision picks up
|
|
the default-head parent quirk, child revision, chained revisions, missing
|
|
-> KeyError), branch (ok, missing -> KeyError), lineage (empty graph,
|
|
default head, explicit id, superseded skip, missing-parent break, ghost
|
|
superseded_by target, unknown id, parent cycle), history (single, full
|
|
chain, from the middle, missing id), migrate_flat (chain, empty
|
|
transcript, missing role/content keys, existing-file seed) and cue (no
|
|
cue, replaces, branched from, superseded by, falsy superseded_by, edge
|
|
iteration order).
|
|
|
|
Nondeterminism seams: the facade generates the uuid4 hex id and the
|
|
_now() timestamp and passes them to the kernel as plain arguments — here
|
|
both are pinned (sequential fake uuid4, frozen clock) so the frozen truth
|
|
replays VERBATIM on the Rust side and the kernel-side parity pass is a
|
|
byte-diff of this file.
|
|
|
|
Default run pins RUNE_MG_CORE=python (the ORACLE). The kernel-side parity
|
|
pass re-runs with RUNE_MG_CORE=auto and MG_TRUTH_OUT set; identical bytes
|
|
== 100% live parity.
|
|
|
|
Output: /tmp/mg_truth.json — replayed VERBATIM by
|
|
src/rust/message_graph/src/branch_battery.rs.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import json
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
os.environ.setdefault("RUNE_MG_CORE", "python") # freeze the ORACLE, never the kernel
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
|
|
from rune import message_graph as mg # noqa: E402
|
|
|
|
|
|
# ── determinism seams (see module docstring) ─────────────────────────────
|
|
class _FakeUUID:
|
|
def __init__(self, hex_: str) -> None:
|
|
self.hex = hex_
|
|
|
|
|
|
_seq = [0]
|
|
|
|
|
|
def _fake_uuid4():
|
|
_seq[0] += 1
|
|
return _FakeUUID(f"deadbeef{_seq[0]:04d}")
|
|
|
|
|
|
uuid.uuid4 = _fake_uuid4
|
|
CLOCK = "2031-01-01T00:00:00.000000+00:00"
|
|
mg._now = lambda: CLOCK # noqa: E731 (the oracle's clock, pinned)
|
|
|
|
TMP = Path(tempfile.mkdtemp(prefix="mg_truth_"))
|
|
|
|
GROUPS = ("load", "save", "add", "tool_call", "tool_result", "revise",
|
|
"branch", "lineage", "history", "migrate_flat", "cue")
|
|
groups: dict[str, list] = {g: [] for g in GROUPS}
|
|
|
|
|
|
def case(group: str, name: str, input_, expected) -> None:
|
|
groups[group].append({"name": name, "input": input_, "expected": expected})
|
|
|
|
|
|
def wire(g: "mg.MessageGraph") -> dict:
|
|
"""Graph state over the wire: nodes as an insertion-ordered object,
|
|
order as the arrival list."""
|
|
return {"nodes": copy.deepcopy(g.nodes), "order": list(g.order)}
|
|
|
|
|
|
def exc_of(fn, *a, **kw) -> dict:
|
|
try:
|
|
fn(*a, **kw)
|
|
except Exception as e: # noqa: BLE001 — the oracle's exception IS the truth
|
|
return {"exception": {"type": type(e).__name__, "message": str(e)}}
|
|
raise AssertionError("expected the oracle to raise")
|
|
|
|
|
|
def fresh(name: str, doc_text: str | None = None) -> "mg.MessageGraph":
|
|
p = TMP / f"{name}.json"
|
|
if doc_text is not None:
|
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
p.write_text(doc_text)
|
|
return mg.MessageGraph(path=p)
|
|
|
|
|
|
# ══ load ═════════════════════════════════════════════════════════════════
|
|
# missing file -> empty graph
|
|
g = fresh("load_missing")
|
|
case("load", "load-missing-file", {"text": None}, {"graph": wire(g)})
|
|
|
|
# empty object: no schema -> ignored
|
|
g = fresh("load_empty_obj", "{}")
|
|
case("load", "load-no-schema", {"text": "{}"}, {"graph": wire(g)})
|
|
|
|
# wrong schema
|
|
g = fresh("load_wrong_schema", json.dumps({"schema": 2, "nodes": []}))
|
|
case("load", "load-wrong-schema", {"text": json.dumps({"schema": 2, "nodes": []})},
|
|
{"graph": wire(g)})
|
|
|
|
# corrupt JSON
|
|
g = fresh("load_corrupt", "not json {")
|
|
case("load", "load-corrupt-json", {"text": "not json {"}, {"graph": wire(g)})
|
|
|
|
# valid doc: nodes ingested verbatim in file order
|
|
doc = {"schema": 1, "nodes": [
|
|
{"id": "aaa000000001", "kind": "message", "role": "user", "content": "q",
|
|
"ts": CLOCK, "parent": None, "edges": [], "superseded_by": None},
|
|
{"id": "bbb000000002", "kind": "reply", "role": "assistant", "content": "a",
|
|
"ts": CLOCK, "parent": "aaa000000001", "edges": [], "superseded_by": None},
|
|
]}
|
|
g = fresh("load_valid", json.dumps(doc))
|
|
case("load", "load-valid", {"text": json.dumps(doc)}, {"graph": wire(g)})
|
|
|
|
# duplicate ids: last wins in nodes, BOTH stay in order
|
|
doc = {"schema": 1, "nodes": [
|
|
{"id": "ccc000000003", "kind": "message", "role": "user", "content": "v1",
|
|
"ts": CLOCK, "parent": None, "edges": [], "superseded_by": None},
|
|
{"id": "ccc000000003", "kind": "message", "role": "user", "content": "v2",
|
|
"ts": CLOCK, "parent": None, "edges": [], "superseded_by": None},
|
|
]}
|
|
g = fresh("load_dup", json.dumps(doc))
|
|
case("load", "load-duplicate-ids-last-wins-order-keeps-both",
|
|
{"text": json.dumps(doc)}, {"graph": wire(g)})
|
|
|
|
# extra fields ride verbatim
|
|
doc = {"schema": 1, "nodes": [
|
|
{"id": "ddd000000004", "kind": "message", "role": "user", "content": "x",
|
|
"ts": CLOCK, "parent": None, "edges": [], "superseded_by": None,
|
|
"extra": {"deep": [1, 2.5, None, True]}, "π": "unicode key"},
|
|
]}
|
|
g = fresh("load_extra", json.dumps(doc))
|
|
case("load", "load-extra-fields-verbatim", {"text": json.dumps(doc)},
|
|
{"graph": wire(g)})
|
|
|
|
# non-dict doc -> the oracle's doc.get raises AttributeError
|
|
def _err_file(name: str, text: str) -> Path:
|
|
p = TMP / f"{name}.json"
|
|
p.write_text(text)
|
|
return p
|
|
|
|
|
|
_p = _err_file("load_list", "[1, 2]")
|
|
case("load", "load-list-doc-attributeerror", {"text": "[1, 2]"},
|
|
exc_of(mg.MessageGraph, path=_p))
|
|
_p = _err_file("load_str", '"hello"')
|
|
case("load", "load-string-doc-attributeerror", {"text": '"hello"'},
|
|
exc_of(mg.MessageGraph, path=_p))
|
|
_p = _err_file("load_int", "5")
|
|
case("load", "load-int-doc-attributeerror", {"text": "5"},
|
|
exc_of(mg.MessageGraph, path=_p))
|
|
|
|
# nodes present but not a list -> the for-loop raises TypeError
|
|
_p = _err_file("load_dictnodes", json.dumps({"schema": 1, "nodes": {"aaa": {}}}))
|
|
case("load", "load-dict-nodes-typeerror",
|
|
{"text": json.dumps({"schema": 1, "nodes": {"aaa": {}}})},
|
|
exc_of(mg.MessageGraph, path=_p))
|
|
|
|
# ══ add ══════════════════════════════════════════════════════════════════
|
|
# default parent on an empty graph: parent None, edges []
|
|
g = fresh("add_empty")
|
|
node = g.add("user", "hello")
|
|
case("add", "add-default-parent-empty-graph",
|
|
{"graph": {"nodes": {}, "order": []}, "id": node["id"],
|
|
"ts": node["ts"], "role": "user", "content": "hello", "kind": "message",
|
|
"parent": None, "edge": "parent"},
|
|
{"node": copy.deepcopy(node), "graph": wire(g)})
|
|
|
|
# default parent chains to the head; edges stay [] (parent arg was None)
|
|
g = fresh("add_chain")
|
|
g.add("user", "q1")
|
|
before = wire(g)
|
|
node = g.add("assistant", "a1")
|
|
case("add", "add-default-parent-head-no-edge",
|
|
{"graph": before, "id": node["id"], "ts": node["ts"], "role": "assistant",
|
|
"content": "a1", "kind": "message", "parent": None, "edge": "parent"},
|
|
{"node": copy.deepcopy(node), "graph": wire(g)})
|
|
|
|
# explicit parent + explicit edge
|
|
g = fresh("add_explicit")
|
|
a = g.add("user", "q")
|
|
before = wire(g)
|
|
node = g.add("assistant", "a", kind="reply", parent=a["id"], edge="reply_to")
|
|
case("add", "add-explicit-parent-edge",
|
|
{"graph": before, "id": node["id"], "ts": node["ts"], "role": "assistant",
|
|
"content": "a", "kind": "reply", "parent": a["id"], "edge": "reply_to"},
|
|
{"node": copy.deepcopy(node), "graph": wire(g)})
|
|
|
|
# explicit parent, default edge "parent"
|
|
g = fresh("add_explicit_default_edge")
|
|
a = g.add("user", "q")
|
|
before = wire(g)
|
|
node = g.add("assistant", "a", parent=a["id"])
|
|
case("add", "add-explicit-parent-default-edge",
|
|
{"graph": before, "id": node["id"], "ts": node["ts"], "role": "assistant",
|
|
"content": "a", "kind": "message", "parent": a["id"], "edge": "parent"},
|
|
{"node": copy.deepcopy(node), "graph": wire(g)})
|
|
|
|
# explicit None parent with a non-empty order: parent becomes the HEAD but
|
|
# edges stay [] (the arg was None — falsy)
|
|
g = fresh("add_none_parent")
|
|
a = g.add("user", "q")
|
|
before = wire(g)
|
|
node = g.add("assistant", "a", parent=None)
|
|
case("add", "add-explicit-none-parent-becomes-head",
|
|
{"graph": before, "id": node["id"], "ts": node["ts"], "role": "assistant",
|
|
"content": "a", "kind": "message", "parent": None, "edge": "parent"},
|
|
{"node": copy.deepcopy(node), "graph": wire(g)})
|
|
|
|
# falsy parent "": stored as parent, but NO edge (falsy)
|
|
g = fresh("add_falsy_parent")
|
|
a = g.add("user", "q")
|
|
before = wire(g)
|
|
node = g.add("assistant", "a", parent="")
|
|
case("add", "add-falsy-empty-parent-no-edge",
|
|
{"graph": before, "id": node["id"], "ts": node["ts"], "role": "assistant",
|
|
"content": "a", "kind": "message", "parent": "", "edge": "parent"},
|
|
{"node": copy.deepcopy(node), "graph": wire(g)})
|
|
|
|
# edge to a ghost parent is allowed (no referential check in add)
|
|
g = fresh("add_ghost_parent")
|
|
a = g.add("user", "q")
|
|
before = wire(g)
|
|
node = g.add("assistant", "a", parent="ghost0000001", edge="reply_to")
|
|
case("add", "add-ghost-parent-allowed",
|
|
{"graph": before, "id": node["id"], "ts": node["ts"], "role": "assistant",
|
|
"content": "a", "kind": "message", "parent": "ghost0000001",
|
|
"edge": "reply_to"},
|
|
{"node": copy.deepcopy(node), "graph": wire(g)})
|
|
|
|
# unicode content
|
|
g = fresh("add_unicode")
|
|
node = g.add("user", "こんにちは ☕ héllo")
|
|
case("add", "add-unicode-content",
|
|
{"graph": {"nodes": {}, "order": []}, "id": node["id"], "ts": node["ts"],
|
|
"role": "user", "content": "こんにちは ☕ héllo", "kind": "message",
|
|
"parent": None, "edge": "parent"},
|
|
{"node": copy.deepcopy(node), "graph": wire(g)})
|
|
|
|
# every kind
|
|
for k in ("message", "reply", "tool_call", "tool_result", "revision", "branch_head"):
|
|
g = fresh(f"add_kind_{k}")
|
|
node = g.add("user", f"content-{k}", kind=k)
|
|
case("add", f"add-kind-{k}",
|
|
{"graph": {"nodes": {}, "order": []}, "id": node["id"], "ts": node["ts"],
|
|
"role": "user", "content": f"content-{k}", "kind": k,
|
|
"parent": None, "edge": "parent"},
|
|
{"node": copy.deepcopy(node), "graph": wire(g)})
|
|
|
|
# bad kind / bad edge (ValueError with the oracle's repr formatting)
|
|
g = fresh("add_bad_kind")
|
|
case("add", "add-unknown-kind-valueerror",
|
|
{"graph": {"nodes": {}, "order": []}, "id": "deadbeef_err", "ts": CLOCK,
|
|
"role": "user", "content": "x", "kind": "nonsense", "parent": None,
|
|
"edge": "parent"},
|
|
exc_of(g.add, "user", "x", kind="nonsense"))
|
|
case("add", "add-unknown-edge-valueerror",
|
|
{"graph": {"nodes": {}, "order": []}, "id": "deadbeef_err", "ts": CLOCK,
|
|
"role": "user", "content": "x", "kind": "message", "parent": None,
|
|
"edge": "nope"},
|
|
exc_of(g.add, "user", "x", edge="nope"))
|
|
# kind checked BEFORE edge
|
|
case("add", "add-both-bad-kind-wins",
|
|
{"graph": {"nodes": {}, "order": []}, "id": "deadbeef_err", "ts": CLOCK,
|
|
"role": "user", "content": "x", "kind": "nonsense", "parent": None,
|
|
"edge": "nope"},
|
|
exc_of(g.add, "user", "x", kind="nonsense", edge="nope"))
|
|
|
|
# ══ tool_call ════════════════════════════════════════════════════════════
|
|
g = fresh("tc_basic")
|
|
node = g.tool_call("read", {"path": "a.py"})
|
|
case("tool_call", "tool-call-basic",
|
|
{"graph": {"nodes": {}, "order": []}, "id": node["id"], "ts": node["ts"],
|
|
"content": json.dumps({"call": "read", "args": {"path": "a.py"}}),
|
|
"parent": None},
|
|
{"node": copy.deepcopy(node), "graph": wire(g)})
|
|
|
|
g = fresh("tc_parent")
|
|
a = g.add("assistant", "running")
|
|
before = wire(g)
|
|
node = g.tool_call("write", {"path": "b.py", "bytes": 3}, parent=a["id"])
|
|
case("tool_call", "tool-call-explicit-parent",
|
|
{"graph": before, "id": node["id"], "ts": node["ts"],
|
|
"content": json.dumps({"call": "write", "args": {"path": "b.py", "bytes": 3}}),
|
|
"parent": a["id"]},
|
|
{"node": copy.deepcopy(node), "graph": wire(g)})
|
|
|
|
g = fresh("tc_nested")
|
|
node = g.tool_call("search", {"q": "π", "opts": {"deep": [1, 2, {"k": None}],
|
|
"flag": True}, "n": 0.5})
|
|
case("tool_call", "tool-call-nested-args",
|
|
{"graph": {"nodes": {}, "order": []}, "id": node["id"], "ts": node["ts"],
|
|
"content": json.dumps({"call": "search",
|
|
"args": {"q": "π", "opts": {"deep": [1, 2, {"k": None}],
|
|
"flag": True}, "n": 0.5}}),
|
|
"parent": None},
|
|
{"node": copy.deepcopy(node), "graph": wire(g)})
|
|
|
|
# ══ tool_result ══════════════════════════════════════════════════════════
|
|
g = fresh("tr_ok")
|
|
c = g.tool_call("read", {"path": "a.py"})
|
|
before = wire(g)
|
|
node = g.tool_result(c["id"], "file contents")
|
|
case("tool_result", "tool-result-links-call",
|
|
{"graph": before, "id": node["id"], "ts": node["ts"], "call_id": c["id"],
|
|
"result": "file contents"},
|
|
{"node": copy.deepcopy(node), "graph": wire(g)})
|
|
|
|
# no kind check on the parent: any node can be a "call"
|
|
g = fresh("tr_any_node")
|
|
a = g.add("user", "not a call")
|
|
before = wire(g)
|
|
node = g.tool_result(a["id"], "result anyway")
|
|
case("tool_result", "tool-result-non-call-parent-allowed",
|
|
{"graph": before, "id": node["id"], "ts": node["ts"], "call_id": a["id"],
|
|
"result": "result anyway"},
|
|
{"node": copy.deepcopy(node), "graph": wire(g)})
|
|
|
|
g = fresh("tr_missing")
|
|
case("tool_result", "tool-result-missing-keyerror",
|
|
{"graph": {"nodes": {}, "order": []}, "id": "deadbeef_err", "ts": CLOCK,
|
|
"call_id": "ghost0000001", "result": "x"},
|
|
exc_of(g.tool_result, "ghost0000001", "x"))
|
|
|
|
# ══ revise ═══════════════════════════════════════════════════════════════
|
|
# root revision: old.get("parent") is None -> add substitutes the HEAD,
|
|
# which at that moment is the old node itself (quirk, frozen verbatim)
|
|
g = fresh("rev_root")
|
|
a = g.add("assistant", "v1")
|
|
before = wire(g)
|
|
rev = g.revise(a["id"], "v2")
|
|
case("revise", "revise-root-head-parent-quirk",
|
|
{"graph": before, "node_id": a["id"], "new_content": "v2",
|
|
"id": rev["id"], "ts": rev["ts"]},
|
|
{"node": copy.deepcopy(rev), "graph": wire(g)})
|
|
|
|
# child revision: keeps the old parent, edges revises + supersedes
|
|
g = fresh("rev_child")
|
|
a = g.add("user", "q")
|
|
b = g.add("assistant", "v1", parent=a["id"])
|
|
before = wire(g)
|
|
rev = g.revise(b["id"], "v2")
|
|
case("revise", "revise-child-keeps-parent",
|
|
{"graph": before, "node_id": b["id"], "new_content": "v2",
|
|
"id": rev["id"], "ts": rev["ts"]},
|
|
{"node": copy.deepcopy(rev), "graph": wire(g)})
|
|
|
|
# chained revision: revise the revision
|
|
g = fresh("rev_chain")
|
|
a = g.add("assistant", "v1")
|
|
r1 = g.revise(a["id"], "v2")
|
|
before = wire(g)
|
|
r2 = g.revise(r1["id"], "v3")
|
|
case("revise", "revise-of-revision",
|
|
{"graph": before, "node_id": r1["id"], "new_content": "v3",
|
|
"id": r2["id"], "ts": r2["ts"]},
|
|
{"node": copy.deepcopy(r2), "graph": wire(g)})
|
|
|
|
g = fresh("rev_missing")
|
|
case("revise", "revise-missing-keyerror",
|
|
{"graph": {"nodes": {}, "order": []}, "node_id": "ghost0000001",
|
|
"new_content": "x", "id": "deadbeef_err", "ts": CLOCK},
|
|
exc_of(g.revise, "ghost0000001", "x"))
|
|
|
|
# ══ branch ═══════════════════════════════════════════════════════════════
|
|
g = fresh("branch_ok")
|
|
a = g.add("user", "root")
|
|
g.add("assistant", "answer", parent=a["id"])
|
|
before = wire(g)
|
|
head = g.branch(a["id"])
|
|
case("branch", "branch-from-message",
|
|
{"graph": before, "node_id": a["id"], "id": head["id"], "ts": head["ts"]},
|
|
{"node": copy.deepcopy(head), "graph": wire(g)})
|
|
|
|
g = fresh("branch_missing")
|
|
case("branch", "branch-missing-keyerror",
|
|
{"graph": {"nodes": {}, "order": []}, "node_id": "ghost0000001",
|
|
"id": "deadbeef_err", "ts": CLOCK},
|
|
exc_of(g.branch, "ghost0000001"))
|
|
|
|
# ══ lineage ══════════════════════════════════════════════════════════════
|
|
g = fresh("lin_empty")
|
|
case("lineage", "lineage-empty-graph", {"graph": wire(g), "node_id": None},
|
|
{"result": []})
|
|
|
|
# default head: full chain oldest-first
|
|
g = fresh("lin_chain")
|
|
a = g.add("user", "q")
|
|
b = g.add("assistant", "a1", parent=a["id"])
|
|
c = g.add("assistant", "a2", parent=b["id"])
|
|
case("lineage", "lineage-default-head-full-chain",
|
|
{"graph": wire(g), "node_id": None},
|
|
{"result": [copy.deepcopy(n) for n in g.lineage()]})
|
|
assert [n["id"] for n in g.lineage()] == [a["id"], b["id"], c["id"]]
|
|
|
|
# explicit id: subchain
|
|
case("lineage", "lineage-explicit-id-subchain",
|
|
{"graph": wire(g), "node_id": b["id"]},
|
|
{"result": [copy.deepcopy(n) for n in g.lineage(b["id"])]})
|
|
assert [n["id"] for n in g.lineage(b["id"])] == [a["id"], b["id"]]
|
|
|
|
# superseded nodes yield to their revision; unrelated branches excluded
|
|
g = fresh("lin_superseded")
|
|
a = g.add("user", "q")
|
|
b = g.add("assistant", "v1", parent=a["id"])
|
|
other = g.add("user", "unrelated")
|
|
rev = g.revise(b["id"], "v2")
|
|
case("lineage", "lineage-skips-superseded-default-head",
|
|
{"graph": wire(g), "node_id": None},
|
|
{"result": [copy.deepcopy(n) for n in g.lineage()]})
|
|
assert [n["id"] for n in g.lineage()] == [a["id"], rev["id"]]
|
|
case("lineage", "lineage-from-root-excludes-other-branch",
|
|
{"graph": wire(g), "node_id": a["id"]},
|
|
{"result": [copy.deepcopy(n) for n in g.lineage(a["id"])]})
|
|
assert [n["id"] for n in g.lineage(a["id"])] == [a["id"]]
|
|
|
|
# parent chain into a ghost: walk breaks quietly
|
|
g = fresh("lin_ghost_parent")
|
|
a = g.add("user", "q")
|
|
g.nodes[a["id"]]["parent"] = "ghost0000001"
|
|
case("lineage", "lineage-ghost-parent-breaks",
|
|
{"graph": wire(g), "node_id": a["id"]},
|
|
{"result": [copy.deepcopy(n) for n in g.lineage(a["id"])]})
|
|
assert [n["id"] for n in g.lineage(a["id"])] == [a["id"]]
|
|
|
|
# superseded_by pointing OUTSIDE the graph: node included (guard fails)
|
|
g = fresh("lin_ghost_superseded")
|
|
a = g.add("user", "q")
|
|
g.nodes[a["id"]]["superseded_by"] = "ghost0000001"
|
|
case("lineage", "lineage-ghost-superseded-by-included",
|
|
{"graph": wire(g), "node_id": a["id"]},
|
|
{"result": [copy.deepcopy(n) for n in g.lineage(a["id"])]})
|
|
assert [n["id"] for n in g.lineage(a["id"])] == [a["id"]]
|
|
|
|
# unknown id: empty
|
|
g = fresh("lin_unknown")
|
|
case("lineage", "lineage-unknown-id-empty",
|
|
{"graph": wire(g), "node_id": "ghost0000001"}, {"result": []})
|
|
|
|
# parent cycle: the seen-set guard stops the walk
|
|
doc = {"schema": 1, "nodes": [
|
|
{"id": "aaa000000001", "kind": "message", "role": "user", "content": "a",
|
|
"ts": CLOCK, "parent": "bbb000000002", "edges": [], "superseded_by": None},
|
|
{"id": "bbb000000002", "kind": "message", "role": "user", "content": "b",
|
|
"ts": CLOCK, "parent": "aaa000000001", "edges": [], "superseded_by": None},
|
|
]}
|
|
g = fresh("lin_cycle", json.dumps(doc))
|
|
case("lineage", "lineage-parent-cycle-guarded",
|
|
{"graph": wire(g), "node_id": "aaa000000001"},
|
|
{"result": [copy.deepcopy(n) for n in g.lineage("aaa000000001")]})
|
|
assert [n["id"] for n in g.lineage("aaa000000001")] == ["bbb000000002",
|
|
"aaa000000001"]
|
|
|
|
# ══ history ══════════════════════════════════════════════════════════════
|
|
# single node: no supersedes edge -> just itself
|
|
g = fresh("hist_single")
|
|
a = g.add("assistant", "v1")
|
|
case("history", "history-single-node",
|
|
{"graph": wire(g), "node_id": a["id"]},
|
|
{"result": [copy.deepcopy(n) for n in g.history(a["id"])]})
|
|
assert [n["content"] for n in g.history(a["id"])] == ["v1"]
|
|
|
|
# full chain oldest-first, asked from the tip AND from the middle
|
|
g = fresh("hist_chain")
|
|
a = g.add("assistant", "v1")
|
|
r1 = g.revise(a["id"], "v2")
|
|
r2 = g.revise(r1["id"], "v3")
|
|
case("history", "history-chain-from-tip",
|
|
{"graph": wire(g), "node_id": r2["id"]},
|
|
{"result": [copy.deepcopy(n) for n in g.history(r2["id"])]})
|
|
assert [n["content"] for n in g.history(r2["id"])] == ["v1", "v2", "v3"]
|
|
case("history", "history-chain-from-middle-full-trail",
|
|
{"graph": wire(g), "node_id": r1["id"]},
|
|
{"result": [copy.deepcopy(n) for n in g.history(r1["id"])]})
|
|
assert [n["content"] for n in g.history(r1["id"])] == ["v1", "v2", "v3"]
|
|
|
|
# unknown id: empty trail
|
|
g = fresh("hist_missing")
|
|
case("history", "history-unknown-id-empty",
|
|
{"graph": wire(g), "node_id": "ghost0000001"}, {"result": []})
|
|
|
|
# revision whose superseded_by target is a ghost: trail stops there
|
|
g = fresh("hist_ghost_target")
|
|
a = g.add("assistant", "v1")
|
|
r1 = g.revise(a["id"], "v2")
|
|
g.nodes[r1["id"]]["superseded_by"] = "ghost0000001"
|
|
case("history", "history-ghost-superseded-by-stops",
|
|
{"graph": wire(g), "node_id": a["id"]},
|
|
{"result": [copy.deepcopy(n) for n in g.history(a["id"])]})
|
|
assert [n["content"] for n in g.history(a["id"])] == ["v1", "v2"]
|
|
|
|
# ══ migrate_flat ═════════════════════════════════════════════════════════
|
|
def mig_ids(g, n):
|
|
"""The ids the oracle's migrate consumed (the last n arrivals)."""
|
|
return list(g.order[len(g.order) - n:]) if n else []
|
|
|
|
|
|
flat = [{"role": "user", "content": "q1"}, {"role": "assistant", "content": "a1"}]
|
|
g = mg.MessageGraph.migrate_flat(copy.deepcopy(flat), path=TMP / "mig_basic.json")
|
|
case("migrate_flat", "migrate-basic-chain",
|
|
{"text": None, "transcript": copy.deepcopy(flat),
|
|
"ids": mig_ids(g, 2), "tss": [CLOCK, CLOCK]},
|
|
{"graph": wire(g)})
|
|
assert [g.nodes[i]["parent"] for i in g.order] == [None, g.order[0]]
|
|
|
|
# empty transcript
|
|
g = mg.MessageGraph.migrate_flat([], path=TMP / "mig_empty.json")
|
|
case("migrate_flat", "migrate-empty-transcript",
|
|
{"text": None, "transcript": [], "ids": [], "tss": []}, {"graph": wire(g)})
|
|
|
|
# missing role/content keys -> system / "" defaults
|
|
flat = [{"role": "user"}, {}, {"content": "no role"}]
|
|
g = mg.MessageGraph.migrate_flat(copy.deepcopy(flat), path=TMP / "mig_defaults.json")
|
|
case("migrate_flat", "migrate-missing-keys-default-system-empty",
|
|
{"text": None, "transcript": copy.deepcopy(flat), "ids": mig_ids(g, 3),
|
|
"tss": [CLOCK] * 3},
|
|
{"graph": wire(g)})
|
|
assert [g.nodes[i]["role"] for i in g.order] == ["user", "system", "system"]
|
|
assert [g.nodes[i]["content"] for i in g.order] == ["", "", "no role"]
|
|
|
|
# existing file seeds the graph first, migration appends after
|
|
seed_doc = {"schema": 1, "nodes": [
|
|
{"id": "eee000000005", "kind": "message", "role": "user", "content": "old",
|
|
"ts": CLOCK, "parent": None, "edges": [], "superseded_by": None},
|
|
]}
|
|
_seed = _err_file("mig_seed", json.dumps(seed_doc))
|
|
g = mg.MessageGraph.migrate_flat([{"role": "user", "content": "new"}],
|
|
path=_seed)
|
|
case("migrate_flat", "migrate-existing-file-seed",
|
|
{"text": json.dumps(seed_doc), "transcript": [{"role": "user", "content": "new"}],
|
|
"ids": mig_ids(g, 1), "tss": [CLOCK]},
|
|
{"graph": wire(g)})
|
|
|
|
# ══ cue ══════════════════════════════════════════════════════════════════
|
|
g = fresh("cue_plain")
|
|
a = g.add("assistant", "v1")
|
|
plain = copy.deepcopy(g.nodes[a["id"]])
|
|
case("cue", "cue-plain-node", {"node": plain}, {"result": ""})
|
|
assert mg.MessageGraph.cue(plain) == ""
|
|
|
|
rev = g.revise(a["id"], "v2")
|
|
case("cue", "cue-revision-replaces",
|
|
{"node": copy.deepcopy(g.nodes[rev["id"]])},
|
|
{"result": f"replaces {a['id'][:8]}"})
|
|
assert mg.MessageGraph.cue(g.nodes[rev["id"]]) == f"replaces {a['id'][:8]}"
|
|
|
|
case("cue", "cue-superseded-by",
|
|
{"node": copy.deepcopy(g.nodes[a["id"]])},
|
|
{"result": f"superseded by {rev['id'][:8]}"})
|
|
assert mg.MessageGraph.cue(g.nodes[a["id"]]) == f"superseded by {rev['id'][:8]}"
|
|
|
|
g = fresh("cue_branch")
|
|
a = g.add("user", "root")
|
|
head = g.branch(a["id"])
|
|
case("cue", "cue-branch-head",
|
|
{"node": copy.deepcopy(g.nodes[head["id"]])},
|
|
{"result": f"branched from {a['id'][:8]}"})
|
|
assert mg.MessageGraph.cue(g.nodes[head["id"]]) == f"branched from {a['id'][:8]}"
|
|
|
|
# falsy superseded_by "" is NOT a cue
|
|
node = copy.deepcopy(plain)
|
|
node["superseded_by"] = ""
|
|
case("cue", "cue-falsy-superseded-by-empty", {"node": node}, {"result": ""})
|
|
assert mg.MessageGraph.cue(node) == ""
|
|
|
|
# first supersedes/branches_from edge in iteration order wins
|
|
node = copy.deepcopy(plain)
|
|
node["edges"] = [{"type": "parent", "to": "aaa000000001"},
|
|
{"type": "supersedes", "to": "bbb000000002"}]
|
|
case("cue", "cue-first-supersedes-edge-wins", {"node": node},
|
|
{"result": "replaces bbb00000"})
|
|
assert mg.MessageGraph.cue(node) == "replaces bbb00000"
|
|
node2 = copy.deepcopy(plain)
|
|
node2["edges"] = [{"type": "branches_from", "to": "ccc000000003"},
|
|
{"type": "supersedes", "to": "bbb000000002"}]
|
|
case("cue", "cue-branches-from-beats-later-supersedes", {"node": node2},
|
|
{"result": "branched from ccc00000"})
|
|
assert mg.MessageGraph.cue(node2) == "branched from ccc00000"
|
|
|
|
# ══ save ═════════════════════════════════════════════════════════════════
|
|
# basic save: doc shape + exact file bytes (json.dumps indent=2)
|
|
g = fresh("save_basic")
|
|
a = g.add("user", "persist me")
|
|
g.save()
|
|
doc = {"schema": mg.SCHEMA, "nodes": [g.nodes[i] for i in g.order]}
|
|
case("save", "save-basic-doc-and-bytes",
|
|
{"graph": {"nodes": {a["id"]: copy.deepcopy(g.nodes[a["id"]])},
|
|
"order": [a["id"]]}},
|
|
{"doc": copy.deepcopy(doc), "file": (TMP / "save_basic.json").read_text()})
|
|
assert (TMP / "save_basic.json").read_text() == json.dumps(doc, indent=2)
|
|
|
|
# save creates missing parent dirs
|
|
deep = TMP / "deep" / "nested" / "dir" / "g.json"
|
|
g2 = mg.MessageGraph(path=deep)
|
|
g2.add("user", "deep")
|
|
g2.save()
|
|
doc2 = {"schema": mg.SCHEMA, "nodes": [g2.nodes[i] for i in g2.order]}
|
|
case("save", "save-creates-parent-dirs",
|
|
{"graph": wire(g2)}, {"doc": copy.deepcopy(doc2), "file": deep.read_text()})
|
|
assert deep.exists()
|
|
|
|
# round trip: save -> fresh load reproduces nodes AND order
|
|
g3 = mg.MessageGraph(path=TMP / "save_basic.json")
|
|
case("load", "load-save-round-trip",
|
|
{"text": (TMP / "save_basic.json").read_text()}, {"graph": wire(g3)})
|
|
assert g3.order == g.order
|
|
assert g3.nodes[a["id"]]["content"] == "persist me"
|
|
|
|
# ══ oracle health (the repo's own battery must pass on the oracle) ══════
|
|
import unittest # noqa: E402
|
|
|
|
suite = unittest.defaultTestLoader.discover(
|
|
str(Path(__file__).resolve().parents[1] / "tests"),
|
|
pattern="test_message_graph.py")
|
|
runner = unittest.TextTestRunner(verbosity=0)
|
|
if not runner.run(suite).wasSuccessful():
|
|
raise AssertionError("oracle regression: test_message_graph.py failed")
|
|
|
|
# ══ write ════════════════════════════════════════════════════════════════
|
|
truth = {
|
|
"frozen": "message_graph load/save/add/tool_call/tool_result/revise/"
|
|
"branch/lineage/history/migrate_flat/cue",
|
|
"oracle": "src/rune/message_graph.py",
|
|
"note": "Graph wire: {nodes: {id: node} insertion-ordered, order: [ids]}. "
|
|
"Nondeterminism seams (uuid4 hex id, _now timestamp) are facade "
|
|
"arguments — frozen CLOCK and sequential deadbeefNNNN ids replay "
|
|
"verbatim. save() builds the doc in the kernel; the file write "
|
|
"(mkdir + json.dumps indent=2) and tool_call's json.dumps content "
|
|
"stay in the facade (serialization isomorphism).",
|
|
}
|
|
for gname in GROUPS:
|
|
truth[gname] = groups[gname]
|
|
|
|
out = Path(os.environ.get("MG_TRUTH_OUT", "/tmp/mg_truth.json"))
|
|
out.write_text(json.dumps(truth, indent=1, ensure_ascii=False) + "\n")
|
|
counts = ", ".join(f"{len(groups[g])} {g}" for g in GROUPS)
|
|
print(f"froze {sum(len(v) for v in groups.values())} cases -> {out}: {counts}")
|