round-12: migrate P8 registries + P12 audit to AppStateStore

- app_state.py: host_state_db_path() (host-level DB, distinct from
  per-VM state_db_path); kv_get/kv_put/kv_delete on meta (namespaced
  keys; kv_put skips the no-secrets guard by design — registry docs
  hold user prompts, documented); audit_trail table (P12 record
  shape + hash chain, seq explicit); counts()/backup() gain
  audit_trail.
- swarm.py / presentation.py / browser_tasks.py: JSON-file backends
  replaced by KV rows (p8-swarm / p8-ledger / p8-browser-tasks);
  legacy JSON imported once on first load (renamed .migrated);
  corrupt row / unreadable store returns empty (fail-safe, same
  semantics as before; next successful save self-heals).
- audit_trail.py: StoreAuditTrail (same record shape, hash chain,
  filters, export as JSONL version, backed by audit_trail table);
  migrate_jsonl_to_store() moves a chain verbatim (broken chain
  refused, divergent seq refused, re-run idempotent). JSONL class
  kept for reading old files.
- P12 dict contracts now run against real P3/P5 code
  (ApprovalQueue.record -> build_approval_card ->
  card_matches_channel; CapabilityIssuer.issue -> describe ->
  PermissionRegistry.record_grant).
- P6->P3 local protocol test: real PrivsepClient + real worker
  server + real git status; worker git argv carries the real P3
  helper + useHttpPath; no token in argv/env/audit. runuser + HTTP
  pull stay real-machine-only (see gap docs).
- Gap docs updated: P8 migration done, guest-side browser follow-up
  explicitly left for the real-machine track (no task_id in the
  browser protocol yet; lifecycle needs a real VM to verify);
  P6 wiring + P12 migration marked done; production-readiness.md
  gains §20 (round-12 integration).
- Tests: new cloud/tests/test_chv_muse_round12.py, 24/24 OK;
  test_chv_muse_p8.py ledger corrupt test rewritten for the KV
  backend (+ legacy-import test). P7+P8: 95+1 tests OK.
This commit is contained in:
host committed 2026-11-07 00:36:25 +00:00
1 parent 466cf7eae7
commit d4cef29cdb
11 files changed
+1147 -63

No files matched your search

+25 -3
View File
@@ -547,15 +547,37 @@ class PresentationTestCase(unittest.TestCase):
with self.assertRaises(ValueError):
self.ledger.should_notify("t1", "", now=T0)
def test_ledger_corrupt_file_recovers(self):
def test_ledger_corrupt_row_recovers(self):
# round-12 迁移后:损坏的 KV 行回空账本(旧版是损坏的
# JSON 文件 quarantine 后回空;语义同款,fail-safe)。
self.assertTrue(self.ledger.should_notify("t1", "failed",
now=T0))
from cloud.chv.app_state import AppStateStore, \
host_state_db_path
with AppStateStore(host_state_db_path(self.state_dir)) as store:
store.kv_put("p8-ledger", "registry", "{not json")
self.assertTrue(self.ledger.should_notify("t2", "failed",
now=T0))
def test_ledger_legacy_json_imported_once(self):
# 旧版 JSON 文件首次加载时导入 KV 并改名 .migrated;
# 之后再写坏 JSON 也不影响 KV 里的账本。
with open(os.path.join(self.state_dir,
"notification-ledger.json"),
"w") as fh:
fh.write('{"notified": {"t0:failed": 1000}}')
self.assertFalse(self.ledger.should_notify(
"t0", "failed", now=1000 + 10))
self.assertTrue(os.path.exists(os.path.join(
self.state_dir, "notification-ledger.json.migrated")))
self.assertFalse(os.path.exists(os.path.join(
self.state_dir, "notification-ledger.json")))
with open(os.path.join(self.state_dir,
"notification-ledger.json"),
"w") as fh:
fh.write("{not json")
self.assertTrue(self.ledger.should_notify("t2", "failed",
now=T0))
self.assertFalse(self.ledger.should_notify(
"t0", "failed", now=1000 + 20))
def test_present_or_log_combined(self):
out = present_or_log({"status": "failed"}, self.ledger,