liquid-loop 0.9.2__tar.gz → 0.9.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/PKG-INFO +1 -1
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/liquid_loop/workspace.py +174 -0
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/liquid_loop.egg-info/PKG-INFO +1 -1
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/liquid_loop.egg-info/SOURCES.txt +1 -0
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/pyproject.toml +1 -1
- liquid_loop-0.9.3/tests/test_auth_guard.py +162 -0
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/LICENSE +0 -0
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/README.md +0 -0
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/liquid_loop/__init__.py +0 -0
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/liquid_loop/__main__.py +0 -0
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/liquid_loop/cli.py +0 -0
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/liquid_loop/cognitive_budget.py +0 -0
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/liquid_loop/entropy.py +0 -0
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/liquid_loop/mesh/__init__.py +0 -0
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/liquid_loop/mesh/v2.py +0 -0
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/liquid_loop/storage.py +0 -0
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/liquid_loop.egg-info/dependency_links.txt +0 -0
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/liquid_loop.egg-info/entry_points.txt +0 -0
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/liquid_loop.egg-info/requires.txt +0 -0
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/liquid_loop.egg-info/top_level.txt +0 -0
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/setup.cfg +0 -0
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/tests/test_consensus_expansion.py +0 -0
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/tests/test_entropy.py +0 -0
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/tests/test_layer1_persistence.py +0 -0
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/tests/test_nucleate_dual_track.py +0 -0
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/tests/test_peek_seal.py +0 -0
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/tests/test_seal_persistence.py +0 -0
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/tests/test_self_evolve.py +0 -0
- {liquid_loop-0.9.2 → liquid_loop-0.9.3}/tests/test_workspace.py +0 -0
|
@@ -577,6 +577,180 @@ class WorkspaceState:
|
|
|
577
577
|
self._iteration += dt # 推进有效状态更新计数(τ)
|
|
578
578
|
self.updated_at = now()
|
|
579
579
|
|
|
580
|
+
# ─────────────────────────────────────────────────────────────
|
|
581
|
+
# v0.9.3 多智能体授权原语
|
|
582
|
+
# 修复共识误删事故:把归属校验从各接入层 wrapper 固化进核心,
|
|
583
|
+
# 使 REST / MCP / trae 桥 / 任意未来 agent 共用同一套授权,杜绝重复实现漂移。
|
|
584
|
+
# 注:本段不调用 save(),落盘由调用方在变更生效时负责(与 add_evidence 等一致)。
|
|
585
|
+
# ─────────────────────────────────────────────────────────────
|
|
586
|
+
|
|
587
|
+
def list_for(self, agent_id: str = "", category: Optional[str] = None) -> list[dict]:
|
|
588
|
+
"""归属隔离列表:共识仅对贡献者可见,私有仅对贡献者可见,他人证据隔离。
|
|
589
|
+
|
|
590
|
+
- evidence:仅返回 agent_id 本人的(agent_id 为空 → 不返回任何证据,避免泄漏)。
|
|
591
|
+
- consensus 结晶:仅当 agent_id 是其 contributors 之一才返回(修复误删根因)。
|
|
592
|
+
- private 结晶:仅当 agent_id 在 contributors 才返回。
|
|
593
|
+
- 未知 scope 一律不暴露。
|
|
594
|
+
"""
|
|
595
|
+
out: list[dict] = []
|
|
596
|
+
if agent_id:
|
|
597
|
+
for e in self.evidences:
|
|
598
|
+
if e.agent_id != agent_id:
|
|
599
|
+
continue
|
|
600
|
+
cat = ""
|
|
601
|
+
anchor = next((a for a in self.anchors if a.id == e.anchor_id), None)
|
|
602
|
+
if anchor is not None:
|
|
603
|
+
cat = anchor.name
|
|
604
|
+
out.append({
|
|
605
|
+
"memory_id": e.id,
|
|
606
|
+
"type": "evidence",
|
|
607
|
+
"category": cat,
|
|
608
|
+
"content": e.content,
|
|
609
|
+
"agent_id": e.agent_id,
|
|
610
|
+
})
|
|
611
|
+
for m in self.memories:
|
|
612
|
+
if category and category != "(结晶)":
|
|
613
|
+
continue
|
|
614
|
+
if m.scope == "consensus":
|
|
615
|
+
if not agent_id or agent_id not in m.contributors:
|
|
616
|
+
continue
|
|
617
|
+
elif m.scope == "private":
|
|
618
|
+
if not agent_id or agent_id not in m.contributors:
|
|
619
|
+
continue
|
|
620
|
+
else:
|
|
621
|
+
continue
|
|
622
|
+
out.append({
|
|
623
|
+
"memory_id": m.id,
|
|
624
|
+
"type": "memory",
|
|
625
|
+
"category": "(结晶)",
|
|
626
|
+
"content": m.content,
|
|
627
|
+
"scope": m.scope,
|
|
628
|
+
"contributors": m.contributors,
|
|
629
|
+
})
|
|
630
|
+
return out
|
|
631
|
+
|
|
632
|
+
def delete_as(self, agent_id: str, memory_id: str) -> dict:
|
|
633
|
+
"""带归属校验的删除(不自行落盘,由调用方 save)。
|
|
634
|
+
|
|
635
|
+
约束:
|
|
636
|
+
- 缺 agent_id → 拒绝。
|
|
637
|
+
- evidence:仅所有者可删。
|
|
638
|
+
- private 结晶:仅贡献者可删。
|
|
639
|
+
- consensus 结晶:禁止单端删除(需全体一致 dissolve),返回明确错误。
|
|
640
|
+
- content 兜底:仅删调用者贡献的 private + 本人 evidence;命中 consensus 整体拒绝。
|
|
641
|
+
"""
|
|
642
|
+
if not agent_id:
|
|
643
|
+
return {"ok": False, "error": "forbidden: agent_id required to delete",
|
|
644
|
+
"memory_id": memory_id}
|
|
645
|
+
ev = next((e for e in self.evidences if e.id == memory_id), None)
|
|
646
|
+
if ev is not None:
|
|
647
|
+
if ev.agent_id != agent_id:
|
|
648
|
+
return {"ok": False, "error": "forbidden: not owner of evidence",
|
|
649
|
+
"memory_id": memory_id}
|
|
650
|
+
for a in self.anchors:
|
|
651
|
+
if ev.anchor_id == a.id and memory_id in a.evidence_ids:
|
|
652
|
+
a.evidence_ids.remove(memory_id)
|
|
653
|
+
self.evidences.remove(ev)
|
|
654
|
+
return {"ok": True, "deleted": "evidence", "memory_id": memory_id}
|
|
655
|
+
mem = next((m for m in self.memories if m.id == memory_id), None)
|
|
656
|
+
if mem is not None:
|
|
657
|
+
if mem.scope == "consensus":
|
|
658
|
+
return {"ok": False,
|
|
659
|
+
"error": "forbidden: consensus memory requires unanimous contributor dissolution, not single-agent delete",
|
|
660
|
+
"memory_id": memory_id}
|
|
661
|
+
if agent_id not in mem.contributors:
|
|
662
|
+
return {"ok": False, "error": "forbidden: not a contributor of this private memory",
|
|
663
|
+
"memory_id": memory_id}
|
|
664
|
+
self.memories.remove(mem)
|
|
665
|
+
return {"ok": True, "deleted": "memory", "memory_id": memory_id}
|
|
666
|
+
mems = [m for m in self.memories if m.content == memory_id]
|
|
667
|
+
if mems:
|
|
668
|
+
if any(m.scope == "consensus" for m in mems):
|
|
669
|
+
return {"ok": False,
|
|
670
|
+
"error": "forbidden: content matches consensus memory; use unanimous dissolution",
|
|
671
|
+
"memory_id": memory_id}
|
|
672
|
+
owned = [m for m in mems if agent_id in m.contributors]
|
|
673
|
+
if not owned:
|
|
674
|
+
return {"ok": False, "error": "forbidden: no owned private memory matches content",
|
|
675
|
+
"memory_id": memory_id}
|
|
676
|
+
ev_ids = {eid for m in owned for eid in m.evidence_ids}
|
|
677
|
+
ev_ids |= {e.id for e in self.evidences if e.content == memory_id and e.agent_id == agent_id}
|
|
678
|
+
for m in owned:
|
|
679
|
+
self.memories.remove(m)
|
|
680
|
+
to_del = [e for e in self.evidences if e.id in ev_ids]
|
|
681
|
+
for e in to_del:
|
|
682
|
+
for a in self.anchors:
|
|
683
|
+
if e.id in a.evidence_ids:
|
|
684
|
+
a.evidence_ids.remove(e.id)
|
|
685
|
+
self.evidences.remove(e)
|
|
686
|
+
return {"ok": True, "deleted": "memory+evidence(owned only)", "memory_id": memory_id,
|
|
687
|
+
"memories": len(owned), "evidences": len(to_del)}
|
|
688
|
+
return {"ok": False, "error": "not_found", "memory_id": memory_id}
|
|
689
|
+
|
|
690
|
+
def dissolve_as(self, agent_id: str, memory_id: str, votes_root: Path) -> dict:
|
|
691
|
+
"""consensus 结晶的合法移除:全体贡献者各调用一次,集齐才真删。
|
|
692
|
+
|
|
693
|
+
- 缺 agent_id / 非 consensus / 非贡献者 → 拒绝。
|
|
694
|
+
- 每贡献者一票;投票持久化于 votes_root/.dissolve_votes.json。
|
|
695
|
+
- 集齐 → 真删(移除 memory + 其底层 evidence),清票,返回 dissolved=True。
|
|
696
|
+
- 未集齐 → 记票返回进度,不删。
|
|
697
|
+
不自行落盘 state(由调用方在 dissolved=True 时 save)。
|
|
698
|
+
"""
|
|
699
|
+
if not agent_id:
|
|
700
|
+
return {"ok": False, "error": "forbidden: agent_id required to dissolve",
|
|
701
|
+
"memory_id": memory_id}
|
|
702
|
+
mem = next((m for m in self.memories if m.id == memory_id), None)
|
|
703
|
+
if mem is None:
|
|
704
|
+
return {"ok": False, "error": "not_found", "memory_id": memory_id}
|
|
705
|
+
if mem.scope != "consensus":
|
|
706
|
+
return {"ok": False, "error": "not_consensus: use delete_as for private memory",
|
|
707
|
+
"memory_id": memory_id}
|
|
708
|
+
if agent_id not in mem.contributors:
|
|
709
|
+
return {"ok": False, "error": "forbidden: not a contributor of this consensus",
|
|
710
|
+
"memory_id": memory_id}
|
|
711
|
+
votes = _load_dissolve_votes(votes_root)
|
|
712
|
+
cast = set(votes.get(memory_id, []))
|
|
713
|
+
cast.add(agent_id)
|
|
714
|
+
need = set(mem.contributors)
|
|
715
|
+
if cast >= need:
|
|
716
|
+
ev_ids = set(mem.evidence_ids)
|
|
717
|
+
self.memories.remove(mem)
|
|
718
|
+
to_del = [e for e in self.evidences if e.id in ev_ids]
|
|
719
|
+
for e in to_del:
|
|
720
|
+
for a in self.anchors:
|
|
721
|
+
if e.id in a.evidence_ids:
|
|
722
|
+
a.evidence_ids.remove(e.id)
|
|
723
|
+
self.evidences.remove(e)
|
|
724
|
+
votes.pop(memory_id, None)
|
|
725
|
+
_save_dissolve_votes(votes_root, votes)
|
|
726
|
+
return {"ok": True, "dissolved": True, "memory_id": memory_id,
|
|
727
|
+
"evidences": len(to_del), "voters": sorted(cast)}
|
|
728
|
+
votes[memory_id] = sorted(cast)
|
|
729
|
+
_save_dissolve_votes(votes_root, votes)
|
|
730
|
+
return {"ok": True, "dissolved": False, "memory_id": memory_id,
|
|
731
|
+
"voted": sorted(cast), "remaining": sorted(need - cast)}
|
|
732
|
+
|
|
733
|
+
# ── dissolve 投票持久化(独立于核心 dataclass,守 North-Star 零改动)──
|
|
734
|
+
def _dissolve_votes_path(root: Path) -> Path:
|
|
735
|
+
return Path(root) / ".dissolve_votes.json"
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def _load_dissolve_votes(root: Path) -> dict:
|
|
739
|
+
p = _dissolve_votes_path(root)
|
|
740
|
+
try:
|
|
741
|
+
return json.loads(p.read_text(encoding="utf-8"))
|
|
742
|
+
except Exception:
|
|
743
|
+
return {}
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def _save_dissolve_votes(root: Path, votes: dict) -> None:
|
|
747
|
+
p = _dissolve_votes_path(root)
|
|
748
|
+
try:
|
|
749
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
750
|
+
p.write_text(json.dumps(votes, ensure_ascii=False), encoding="utf-8")
|
|
751
|
+
except Exception:
|
|
752
|
+
pass
|
|
753
|
+
|
|
580
754
|
|
|
581
755
|
# ==============================================================================
|
|
582
756
|
# CPERegularizer — 能力保留正则化引擎(借鉴 UIUC CPE 论文 arXiv:2605.09315)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "liquid-loop"
|
|
7
|
-
version = "0.9.
|
|
7
|
+
version = "0.9.3"
|
|
8
8
|
description = "Self-Organizing Cognitive Memory for AI Agents — Liquid Loop theory implementation"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""v0.9.3 多智能体授权原语无死角测试。
|
|
2
|
+
|
|
3
|
+
覆盖:读隔离(list_for) / 删除授权(delete_as) / 一致解散(dissolve_as)。
|
|
4
|
+
复现并固化「共识误删事故」修复,确保授权逻辑是单一事实源(库核心),
|
|
5
|
+
而非各接入层 wrapper 重复实现。
|
|
6
|
+
"""
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from liquid_loop import WorkspaceState
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def ws():
|
|
17
|
+
s = WorkspaceState()
|
|
18
|
+
a = s.add_anchor("fact")
|
|
19
|
+
ev_vera = s.add_evidence(a, "vera 知道 X", agent_id="vera")
|
|
20
|
+
ev_trae = s.add_evidence(a, "trae 知道 Y", agent_id="trae")
|
|
21
|
+
# vera 私有结晶(由 vera 自己的 2 条证据成核)
|
|
22
|
+
ev_vera2 = s.add_evidence(a, "vera 知道 X(佐证)", agent_id="vera")
|
|
23
|
+
priv = s.add_memory("vera 的私有结论", [ev_vera.id, ev_vera2.id])
|
|
24
|
+
priv.scope = "private"
|
|
25
|
+
priv.contributors = ["vera"]
|
|
26
|
+
# 跨主体共识结晶(vera × trae)
|
|
27
|
+
cons = s.add_memory("vera 与 trae 共同结论", [ev_vera.id, ev_trae.id])
|
|
28
|
+
cons.scope = "consensus"
|
|
29
|
+
cons.contributors = ["vera", "trae"]
|
|
30
|
+
return s, dict(a=a, ev_vera=ev_vera, ev_trae=ev_trae,
|
|
31
|
+
priv=priv, cons=cons)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ── 1. 读隔离 list_for ──────────────────────────────────────────────
|
|
35
|
+
def test_list_empty_agent_sees_nothing(ws):
|
|
36
|
+
s, _ = ws
|
|
37
|
+
out = s.list_for("")
|
|
38
|
+
assert out == [], "agent_id 为空时不得泄漏任何证据/结晶"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_list_vera_sees_own_and_consensus(ws):
|
|
42
|
+
s, d = ws
|
|
43
|
+
ids = {x["memory_id"] for x in s.list_for("vera")}
|
|
44
|
+
assert d["ev_vera"].id in ids # 自己的证据可见
|
|
45
|
+
assert d["cons"].id in ids # 作为贡献者,共识可见
|
|
46
|
+
assert d["priv"].id in ids # 自己的私有可见
|
|
47
|
+
assert d["ev_trae"].id not in ids # 他人证据隔离
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_list_trae_sees_own_and_consensus_not_veras_private(ws):
|
|
51
|
+
s, d = ws
|
|
52
|
+
ids = {x["memory_id"] for x in s.list_for("trae")}
|
|
53
|
+
assert d["ev_trae"].id in ids
|
|
54
|
+
assert d["cons"].id in ids
|
|
55
|
+
assert d["ev_vera"].id not in ids
|
|
56
|
+
assert d["priv"].id not in ids # vera 私有对 trae 隔离
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_list_consensus_hidden_from_non_contributor(ws):
|
|
60
|
+
s, d = ws
|
|
61
|
+
# 伪造一个「非贡献者」agent
|
|
62
|
+
ids = {x["memory_id"] for x in s.list_for("mallory")}
|
|
63
|
+
assert d["cons"].id not in ids, "共识仅对贡献者可见"
|
|
64
|
+
assert d["priv"].id not in ids
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ── 2. 删除授权 delete_as ──────────────────────────────────────────
|
|
68
|
+
def test_delete_requires_agent_id(ws):
|
|
69
|
+
s, d = ws
|
|
70
|
+
r = s.delete_as("", d["ev_vera"].id)
|
|
71
|
+
assert r["ok"] is False and "agent_id required" in r["error"]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_delete_evidence_only_by_owner(ws):
|
|
75
|
+
s, d = ws
|
|
76
|
+
bad = s.delete_as("trae", d["ev_vera"].id)
|
|
77
|
+
assert bad["ok"] is False and "not owner" in bad["error"]
|
|
78
|
+
good = s.delete_as("vera", d["ev_vera"].id)
|
|
79
|
+
assert good["ok"] is True and good["deleted"] == "evidence"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_delete_consensus_forbidden(ws):
|
|
83
|
+
s, d = ws
|
|
84
|
+
r = s.delete_as("vera", d["cons"].id)
|
|
85
|
+
assert r["ok"] is False and "consensus" in r["error"]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_delete_private_only_by_contributor(ws):
|
|
89
|
+
s, d = ws
|
|
90
|
+
bad = s.delete_as("trae", d["priv"].id)
|
|
91
|
+
assert bad["ok"] is False and "not a contributor" in bad["error"]
|
|
92
|
+
good = s.delete_as("vera", d["priv"].id)
|
|
93
|
+
assert good["ok"] is True and good["deleted"] == "memory"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_delete_by_content_owned_private(ws):
|
|
97
|
+
s, d = ws
|
|
98
|
+
# vera 的私有结晶按 content 兜底删除(仅删本人相关)
|
|
99
|
+
r = s.delete_as("vera", "vera 的私有结论")
|
|
100
|
+
assert r["ok"] is True and r["memories"] >= 1
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_delete_by_content_hits_consensus_forbidden(ws):
|
|
104
|
+
s, d = ws
|
|
105
|
+
r = s.delete_as("vera", "vera 与 trae 共同结论")
|
|
106
|
+
assert r["ok"] is False and "consensus" in r["error"]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_delete_not_found(ws):
|
|
110
|
+
s, d = ws
|
|
111
|
+
r = s.delete_as("vera", "nonexistent-id")
|
|
112
|
+
assert r["ok"] is False and r["error"] == "not_found"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ── 3. 一致解散 dissolve_as ────────────────────────────────────────
|
|
116
|
+
def test_dissolve_requires_agent_id(ws):
|
|
117
|
+
s, d = ws
|
|
118
|
+
root = Path(tempfile.mkdtemp())
|
|
119
|
+
r = s.dissolve_as("", d["cons"].id, root)
|
|
120
|
+
assert r["ok"] is False and "agent_id required" in r["error"]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_dissolve_rejects_private(ws):
|
|
124
|
+
s, d = ws
|
|
125
|
+
root = Path(tempfile.mkdtemp())
|
|
126
|
+
r = s.dissolve_as("vera", d["priv"].id, root)
|
|
127
|
+
assert r["ok"] is False and "not_consensus" in r["error"]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_dissolve_rejects_non_contributor(ws):
|
|
131
|
+
s, d = ws
|
|
132
|
+
root = Path(tempfile.mkdtemp())
|
|
133
|
+
r = s.dissolve_as("mallory", d["cons"].id, root)
|
|
134
|
+
assert r["ok"] is False and "not a contributor" in r["error"]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_dissolve_partial_then_unanimous(ws):
|
|
138
|
+
s, d = ws
|
|
139
|
+
root = Path(tempfile.mkdtemp())
|
|
140
|
+
# 第一票:vera,未集齐
|
|
141
|
+
r1 = s.dissolve_as("vera", d["cons"].id, root)
|
|
142
|
+
assert r1["dissolved"] is False
|
|
143
|
+
assert set(r1["remaining"]) == {"trae"}
|
|
144
|
+
assert d["cons"].id in {m.id for m in s.memories}, "未集齐不应真删"
|
|
145
|
+
# 第二票:trae,集齐 → 真删
|
|
146
|
+
r2 = s.dissolve_as("trae", d["cons"].id, root)
|
|
147
|
+
assert r2["dissolved"] is True
|
|
148
|
+
assert d["cons"].id not in {m.id for m in s.memories}
|
|
149
|
+
# 票已清
|
|
150
|
+
from liquid_loop.workspace import _load_dissolve_votes
|
|
151
|
+
assert d["cons"].id not in _load_dissolve_votes(root)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_dissolve_vote_persists_across_instances(ws):
|
|
155
|
+
"""投票状态独立持久化,不依赖 WorkspaceState 内存(重启不丢票)"""
|
|
156
|
+
from liquid_loop.workspace import _load_dissolve_votes
|
|
157
|
+
s, d = ws
|
|
158
|
+
root = Path(tempfile.mkdtemp())
|
|
159
|
+
s.dissolve_as("vera", d["cons"].id, root)
|
|
160
|
+
# 重新从磁盘读票(模拟重启)
|
|
161
|
+
votes = _load_dissolve_votes(root)
|
|
162
|
+
assert d["cons"].id in votes and "vera" in votes[d["cons"].id]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|