授权模型切换为个人副本签发;share 脱敏共建;自更新通道
- 公众版不再共享 1000 次授权池:移除 data/.license.json, 改置 data/.public_edition.json 标记,首次排盘本机签发个人副本 (初始 20 次、不限期);推送脱敏档案每盘 +10 次(同盘只计一次) - 新增 share/ 共建:结构化脱敏导出(share_export.py)+ 双远端推送(share_push.py)+ 隐私协议(share/README.md); 推送前必须展示脱敏包全文并获用户授权 - 新增自更新:VERSION + manifest.json(SHA256 清单)+ scripts/self_update.py(24h 缓存检查、先验后换、保留授权与档案); 排盘成功后 stderr 提示新版本 - SKILL.md 增「档案共建(share/)」「版本与自更新」两节; LICENSE.md 增第六条(档案权属与撤回) - 测试 100 项全绿
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"""软授权锁:母版放行、副本扣次、用尽/过期/篡改/总开关。"""
|
||||
"""软授权锁:母版放行、副本扣次、用尽/过期/篡改/总开关、公众版首启签发、计励幂等。"""
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -71,3 +72,92 @@ class TestMasterSwitch:
|
||||
gate.consume(root) # 不扣次
|
||||
st = json.loads((root / "data" / ".license.json").read_text("utf-8"))
|
||||
assert st["remaining_uses"] == 0
|
||||
|
||||
|
||||
def make_public_marker(root):
|
||||
(root / "data").mkdir(exist_ok=True)
|
||||
(root / "data" / ".public_edition.json").write_text(json.dumps(
|
||||
{"edition": "public", "pack_version": "0.1.0",
|
||||
"pool_copy_id": "LY-20261009-PUB0"}, ensure_ascii=False), "utf-8")
|
||||
|
||||
|
||||
class TestPublicEdition:
|
||||
def test_first_check_issues_personal_license(self, root):
|
||||
make_public_marker(root)
|
||||
assert gate.check(root) is None
|
||||
st = json.loads((root / "data" / ".license.json").read_text("utf-8"))
|
||||
assert re.fullmatch(r"LY-\d{8}-[0-9A-F]{4}", st["copy_id"])
|
||||
assert st["licensee"] == "public-personal"
|
||||
assert st["max_uses"] == 20
|
||||
assert st["remaining_uses"] == 20
|
||||
assert st["expires_at"] is None
|
||||
assert st["granted_archives"] == []
|
||||
|
||||
def test_second_check_keeps_same_copy_id(self, root):
|
||||
make_public_marker(root)
|
||||
gate.check(root)
|
||||
first = json.loads((root / "data" / ".license.json").read_text("utf-8"))
|
||||
gate.check(root)
|
||||
second = json.loads((root / "data" / ".license.json").read_text("utf-8"))
|
||||
assert first["copy_id"] == second["copy_id"]
|
||||
assert first["sig"] == second["sig"]
|
||||
|
||||
def test_issued_copy_consumes_then_denies(self, root):
|
||||
make_public_marker(root)
|
||||
assert gate.check(root) is None
|
||||
for _ in range(20):
|
||||
gate.consume(root)
|
||||
assert "次数已用尽" in gate.check(root)
|
||||
|
||||
def test_master_without_marker_stays_unlocked(self, root):
|
||||
assert gate.check(root) is None # 母版:无状态文件也无标记
|
||||
assert not (root / "data" / ".license.json").exists()
|
||||
|
||||
|
||||
class TestGrantUses:
|
||||
def test_grant_adds_uses_and_is_idempotent(self, root):
|
||||
make_state(root, max_uses=5)
|
||||
assert gate.grant_uses(root, 10, "hashA") is True
|
||||
st = json.loads((root / "data" / ".license.json").read_text("utf-8"))
|
||||
assert st["remaining_uses"] == 15
|
||||
# 同一盘只计一次:第二次返回 False 且不加次
|
||||
assert gate.grant_uses(root, 10, "hashA") is False
|
||||
st = json.loads((root / "data" / ".license.json").read_text("utf-8"))
|
||||
assert st["remaining_uses"] == 15
|
||||
assert st["granted_archives"] == ["hashA"]
|
||||
# 另一盘可再计
|
||||
assert gate.grant_uses(root, 10, "hashB") is True
|
||||
st = json.loads((root / "data" / ".license.json").read_text("utf-8"))
|
||||
assert st["remaining_uses"] == 25
|
||||
assert st["granted_archives"] == ["hashA", "hashB"]
|
||||
|
||||
def test_grant_on_master_returns_true_without_state(self, root):
|
||||
assert gate.grant_uses(root, 10, "hashA") is True
|
||||
assert not (root / "data" / ".license.json").exists()
|
||||
|
||||
def test_grant_on_tampered_state_refused(self, root):
|
||||
make_state(root, max_uses=5)
|
||||
p = root / "data" / ".license.json"
|
||||
st = json.loads(p.read_text("utf-8"))
|
||||
st["remaining_uses"] = 999 # 手改次数但不重算签名
|
||||
p.write_text(json.dumps(st, ensure_ascii=False), "utf-8")
|
||||
assert gate.grant_uses(root, 10, "hashA") is False
|
||||
st = json.loads(p.read_text("utf-8"))
|
||||
assert st["remaining_uses"] == 999 # 未被计励写回
|
||||
|
||||
def test_state_written_by_old_schema_still_verifiable(self, root):
|
||||
# 旧格式状态文件(无 granted_archives 字段)签名校验不受影响
|
||||
st = {
|
||||
"copy_id": "LY-20261007-A3F9", "licensee": "张三",
|
||||
"issued_at": "2026-10-07", "expires_at": None,
|
||||
"max_uses": 2, "remaining_uses": 2,
|
||||
}
|
||||
st["sig"] = gate._sig(st)
|
||||
(root / "data").mkdir()
|
||||
(root / "data" / ".license.json").write_text(
|
||||
json.dumps(st, ensure_ascii=False), "utf-8")
|
||||
assert gate.check(root) is None
|
||||
assert gate.grant_uses(root, 10, "hashA") is True
|
||||
st = json.loads((root / "data" / ".license.json").read_text("utf-8"))
|
||||
assert st["remaining_uses"] == 12
|
||||
assert gate.check(root) is None
|
||||
|
||||
Reference in New Issue
Block a user