1use std::path::Path;
14use std::sync::Arc;
15
16use crate::clock::{MockClock, SharedClock, SystemClock};
17use crate::delegation::Delegation;
18use crate::error::CoreError;
19use crate::gate::{Gate, GateDecision};
20use crate::intent::SpendIntent;
21use crate::pending::{
22 PendingError, PendingLedger, PendingOrder, PendingOutcome, PendingReceipt, PendingState,
23};
24use crate::wal::{fnv1a_64, Wal, WalDecision, WalRecord};
25
26#[derive(Debug)]
28pub struct WanningState {
29 gate: Gate,
30 wal: Option<Wal>,
31 pendings: PendingLedger,
33}
34
35impl WanningState {
36 pub fn new(clock: SharedClock) -> Self {
38 Self {
39 gate: Gate::new(clock),
40 wal: None,
41 pendings: PendingLedger::new(),
42 }
43 }
44
45 pub fn with_wal(clock: SharedClock, wal_path: impl AsRef<Path>) -> Result<Self, CoreError> {
47 Ok(Self {
48 gate: Gate::new(clock),
49 wal: Some(Wal::open(wal_path)?),
50 pendings: PendingLedger::new(),
51 })
52 }
53
54 pub fn live(wal_path: impl AsRef<Path>) -> Result<Self, CoreError> {
59 Self::with_wal(Arc::new(SystemClock), wal_path)
60 }
61
62 pub fn live_resuming(wal_path: impl AsRef<Path>) -> Result<Self, CoreError> {
77 let path = wal_path.as_ref();
78 let wal = Wal::open(path)?;
80 let resumed = Self::replay(path)?;
81 let WanningState {
82 gate,
83 wal: _,
84 pendings,
85 } = resumed;
86 Ok(Self {
87 gate: gate.with_clock(Arc::new(SystemClock)),
88 wal: Some(wal),
89 pendings,
91 })
92 }
93
94 pub fn gate(&self) -> &Gate {
95 &self.gate
96 }
97
98 pub fn pendings(&self) -> &PendingLedger {
101 &self.pendings
102 }
103
104 pub fn pending(&self, pending_id: &str) -> Option<&PendingOrder> {
106 self.pendings.get(pending_id)
107 }
108
109 pub fn wal_path(&self) -> Option<&Path> {
110 self.wal.as_ref().map(Wal::path)
111 }
112
113 pub fn wal_line_count(&self) -> Option<u64> {
115 self.wal.as_ref().map(Wal::line_count)
116 }
117
118 pub fn last_wal_line(&self) -> Option<u64> {
120 self.wal_line_count()
121 }
122
123 pub fn audit_chain_tail(&self) -> Option<u64> {
128 self.wal.as_ref().map(Wal::chain_tail)
129 }
130
131 pub fn register_delegation(&mut self, delegation: Delegation) -> Result<(), CoreError> {
133 delegation.validate()?;
136 if self.gate.delegation(&delegation.id).is_some() {
137 return Err(CoreError::DuplicateDelegation(delegation.id));
138 }
139 let record = WalRecord::RegisterDelegation {
140 ts: self.now(),
141 delegation: delegation.clone(),
142 };
143 if let Some(wal) = self.wal.as_mut() {
144 wal.append(&record)?;
145 }
146 self.gate.register_delegation(delegation)
147 }
148
149 pub fn revoke(&mut self, delegation_id: &str) -> Result<(), CoreError> {
151 if self.gate.delegation(delegation_id).is_none() {
152 return Err(CoreError::UnknownDelegation(delegation_id.to_string()));
153 }
154 let record = WalRecord::Revoke {
155 ts: self.now(),
156 delegation_id: delegation_id.to_string(),
157 };
158 if let Some(wal) = self.wal.as_mut() {
159 wal.append(&record)?;
160 }
161 self.gate.revoke(delegation_id)
162 }
163
164 pub fn decide(&mut self, intent: &SpendIntent) -> Result<GateDecision, CoreError> {
171 let ts = self.now();
175 self.evaluate_record_commit(intent, ts)
176 }
177
178 pub fn decide_opening_pending(
192 &mut self,
193 intent: &SpendIntent,
194 ttl_secs: u64,
195 ) -> Result<(GateDecision, Option<PendingReceipt>), CoreError> {
196 if ttl_secs == 0 {
197 return Err(CoreError::Pending(PendingError::InvalidTtl { ttl_secs }));
198 }
199 let ts = self.now();
200 let expires_ts = ts.checked_add(ttl_secs).ok_or_else(|| {
202 CoreError::LedgerOverflow(format!(
203 "待支付过期时刻溢出: 开单时刻 {ts} + TTL {ttl_secs} 秒"
204 ))
205 })?;
206 let verdict = self.evaluate_record_commit(intent, ts)?;
207 let GateDecision::Allow { budget_after_cents } = verdict else {
208 return Ok((verdict, None));
209 };
210 let pending_id = self.fresh_pending_id(&intent.delegation_id, intent.nonce, ts);
211 let pending_record = WalRecord::Pending {
212 ts,
213 pending_id: pending_id.clone(),
214 delegation_id: intent.delegation_id.clone(),
215 intent: intent.clone(),
216 approved_amount_cents: intent.amount_cents,
217 expires_ts,
218 };
219 let wal_line = match self.wal.as_mut() {
220 Some(wal) => Some(wal.append(&pending_record)?),
221 None => None,
222 };
223 self.pendings.apply_open(PendingOrder {
224 pending_id: pending_id.clone(),
225 delegation_id: intent.delegation_id.clone(),
226 intent: intent.clone(),
227 approved_amount_cents: intent.amount_cents,
228 created_ts: ts,
229 expires_ts,
230 state: PendingState::Open,
231 proof: None,
232 confirmed_ts: None,
233 })?;
234 Ok((
235 GateDecision::Allow { budget_after_cents },
236 Some(PendingReceipt {
237 pending_id,
238 approved_amount_cents: intent.amount_cents,
239 expires_ts,
240 wal_line,
241 }),
242 ))
243 }
244
245 pub fn confirm_pending(
252 &mut self,
253 pending_id: &str,
254 amount_cents: u64,
255 proof: &str,
256 ) -> Result<PendingOrder, CoreError> {
257 if proof.trim().is_empty() {
258 return Err(CoreError::Pending(PendingError::EmptyProof));
259 }
260 let ts = self.now();
261 let checked = self.pendings.check_confirm(pending_id, amount_cents, ts);
263 if let Err(err @ PendingError::Expired { .. }) = checked {
264 let record = WalRecord::Terminal {
266 ts,
267 pending_id: pending_id.to_string(),
268 outcome: PendingOutcome::ExpiredVoid,
269 };
270 if let Some(wal) = self.wal.as_mut() {
271 wal.append(&record)?;
272 }
273 self.pendings.apply_void(pending_id, ts)?;
274 return Err(CoreError::Pending(err));
275 }
276 checked.map_err(CoreError::Pending)?;
277 let confirm_record = WalRecord::Confirm {
278 ts,
279 pending_id: pending_id.to_string(),
280 amount_cents,
281 proof: proof.to_string(),
282 };
283 if let Some(wal) = self.wal.as_mut() {
284 wal.append(&confirm_record)?;
285 }
286 self.pendings
287 .apply_confirm(pending_id, amount_cents, proof, ts)?;
288 let terminal_record = WalRecord::Terminal {
289 ts,
290 pending_id: pending_id.to_string(),
291 outcome: PendingOutcome::Completed,
292 };
293 if let Some(wal) = self.wal.as_mut() {
294 wal.append(&terminal_record)?;
295 }
296 self.pendings.apply_complete(pending_id)?;
297 Ok(self
298 .pendings
299 .get(pending_id)
300 .cloned()
301 .expect("确认过的单必在台账"))
302 }
303
304 pub fn void_expired_pendings(&mut self) -> Result<Vec<String>, CoreError> {
310 let ts = self.now();
311 let expired: Vec<String> = self
312 .pendings
313 .iter()
314 .filter(|(_, order)| order.state == PendingState::Open && ts >= order.expires_ts)
315 .map(|(id, _)| id.clone())
316 .collect();
317 for pending_id in &expired {
318 let record = WalRecord::Terminal {
319 ts,
320 pending_id: pending_id.clone(),
321 outcome: PendingOutcome::ExpiredVoid,
322 };
323 if let Some(wal) = self.wal.as_mut() {
324 wal.append(&record)?;
325 }
326 self.pendings.apply_void(pending_id, ts)?;
327 }
328 Ok(expired)
329 }
330
331 fn fresh_pending_id(&self, delegation_id: &str, nonce: u64, ts: u64) -> String {
334 let mut salt = 0u8;
335 loop {
336 let mut bytes = Vec::with_capacity(delegation_id.len() + 17);
337 bytes.extend_from_slice(delegation_id.as_bytes());
338 bytes.extend_from_slice(&nonce.to_le_bytes());
339 bytes.extend_from_slice(&ts.to_le_bytes());
340 bytes.push(salt);
341 let candidate = format!("p-{:016x}", fnv1a_64(&bytes));
342 if !self.pendings.contains_key(&candidate) {
343 return candidate;
344 }
345 salt = salt.wrapping_add(1);
346 }
347 }
348
349 fn evaluate_record_commit(
353 &mut self,
354 intent: &SpendIntent,
355 ts: u64,
356 ) -> Result<GateDecision, CoreError> {
357 let verdict = self.gate.evaluate_at(intent, ts);
358 let spent_after = match verdict {
359 GateDecision::Allow { budget_after_cents } => budget_after_cents,
361 GateDecision::Deny { .. } => self.gate.spent_cents(&intent.delegation_id).unwrap_or(0),
362 };
363 let record = WalRecord::Decide {
364 ts,
365 decision: match verdict {
366 GateDecision::Allow { .. } => WalDecision::Allow,
367 GateDecision::Deny { .. } => WalDecision::Deny,
368 },
369 delegation_id: intent.delegation_id.clone(),
370 intent: intent.clone(),
371 reason: verdict.deny_reason(),
372 budget_after_cents: spent_after,
373 };
374 if let Some(wal) = self.wal.as_mut() {
375 wal.append(&record)?;
376 }
377 match verdict {
378 GateDecision::Allow { budget_after_cents } => {
379 let after = self.gate.commit_at(intent, ts)?;
380 debug_assert_eq!(after, budget_after_cents);
381 Ok(GateDecision::Allow {
382 budget_after_cents: after,
383 })
384 }
385 deny => Ok(deny),
386 }
387 }
388
389 pub fn state_hash(&self) -> u64 {
400 let mut snapshot = serde_json::json!({
401 "delegations": self.gate.delegations().collect::<Vec<_>>(),
402 "spent_cents": self.gate.ledger().entries().collect::<Vec<_>>(),
403 "revoked": self.gate.revocations().iter().collect::<Vec<_>>(),
404 "used_nonces": self.gate.replay_registry().iter().collect::<Vec<_>>(),
405 "policy_states": self.gate.policy_states().collect::<Vec<_>>(),
406 });
407 if !self.pendings.is_empty() {
408 snapshot["pendings"] = serde_json::to_value(self.pendings.iter().collect::<Vec<_>>())
409 .expect("待支付台账可序列化");
410 }
411 fnv1a_64(snapshot.to_string().as_bytes())
412 }
413
414 fn now(&self) -> u64 {
415 self.gate.clock().now()
416 }
417
418 pub fn replay(wal_path: impl AsRef<Path>) -> Result<Self, CoreError> {
425 let records = crate::wal::read_verified(wal_path)?.records;
427 let clock = MockClock::new(0);
428 let mut state = WanningState::new(Arc::new(clock.clone()));
429 let mut last_allow: Option<SpendIntent> = None;
432 for (line_no, record) in records {
433 let record_ts = record.ts();
434 clock.set_now(record_ts);
435 match record {
436 WalRecord::RegisterDelegation { delegation, .. } => state
437 .gate
438 .register_delegation(delegation)
439 .map_err(|e| CoreError::WalMismatch {
440 line: line_no,
441 message: format!("重放注册失败: {e}"),
442 })?,
443 WalRecord::Revoke { delegation_id, .. } => state
444 .gate
445 .revoke(&delegation_id)
446 .map_err(|e| CoreError::WalMismatch {
447 line: line_no,
448 message: format!("重放撤销失败: {e}"),
449 })?,
450 WalRecord::Decide {
451 decision,
452 intent,
453 reason,
454 budget_after_cents,
455 ..
456 } => {
457 let ts = record_ts;
461 let verdict = state.gate.evaluate_at(&intent, ts);
462 match (verdict, decision, reason) {
463 (
464 GateDecision::Allow {
465 budget_after_cents: recomputed,
466 },
467 WalDecision::Allow,
468 None,
469 ) => {
470 if recomputed != budget_after_cents {
471 return Err(CoreError::WalMismatch {
472 line: line_no,
473 message: format!(
474 "放行记录的累计消费与重算不一致:记录 {budget_after_cents} / 重算 {recomputed}"
475 ),
476 });
477 }
478 state.gate.commit_at(&intent, ts).map_err(|e| {
479 CoreError::WalMismatch {
480 line: line_no,
481 message: format!("重放扣减失败: {e}"),
482 }
483 })?;
484 last_allow = Some(intent);
485 }
486 (
487 GateDecision::Deny { reason: recomputed },
488 WalDecision::Deny,
489 Some(recorded_reason),
490 ) if recomputed == recorded_reason => {
491 }
493 (verdict, decision, reason) => {
494 return Err(CoreError::WalMismatch {
495 line: line_no,
496 message: format!(
497 "重算判定与记录不一致:重算 {verdict:?} / 记录 {decision:?} reason={reason:?}"
498 ),
499 });
500 }
501 }
502 }
503 WalRecord::Pending {
504 pending_id,
505 delegation_id,
506 intent: row_intent,
507 approved_amount_cents,
508 expires_ts,
509 ..
510 } => {
511 let anchored_on_allow = last_allow.as_ref() == Some(&row_intent);
515 let self_consistent = row_intent.amount_cents == approved_amount_cents
516 && row_intent.delegation_id == delegation_id
517 && expires_ts > record_ts;
518 let first_open = !state
519 .pendings
520 .contains_intent(&delegation_id, row_intent.nonce);
521 if !anchored_on_allow || !self_consistent || !first_open {
522 return Err(CoreError::WalMismatch {
523 line: line_no,
524 message: format!(
525 "待支付行与放行记录不一致:锚定放行 {anchored_on_allow} / \
526 行自洽 {self_consistent} / 单号与意图首次出现 {first_open}"
527 ),
528 });
529 }
530 state
531 .pendings
532 .apply_open(PendingOrder {
533 pending_id,
534 delegation_id,
535 intent: row_intent,
536 approved_amount_cents,
537 created_ts: record_ts,
538 expires_ts,
539 state: PendingState::Open,
540 proof: None,
541 confirmed_ts: None,
542 })
543 .map_err(|e| CoreError::WalMismatch {
544 line: line_no,
545 message: format!("重放开单失败: {e}"),
546 })?;
547 }
548 WalRecord::Confirm {
549 pending_id,
550 amount_cents,
551 proof,
552 ..
553 } => {
554 if proof.trim().is_empty() {
557 return Err(CoreError::WalMismatch {
558 line: line_no,
559 message: "确认行的支付凭证为空(实时侧写不出这种行)".to_string(),
560 });
561 }
562 state
563 .pendings
564 .apply_confirm(&pending_id, amount_cents, &proof, record_ts)
565 .map_err(|e| CoreError::WalMismatch {
566 line: line_no,
567 message: format!("重放确认失败: {e}"),
568 })?;
569 }
570 WalRecord::Terminal {
571 pending_id,
572 outcome,
573 ..
574 } => {
575 let applied = match outcome {
577 PendingOutcome::Completed => state.pendings.apply_complete(&pending_id),
578 PendingOutcome::ExpiredVoid => {
579 state.pendings.apply_void(&pending_id, record_ts)
580 }
581 };
582 applied.map_err(|e| CoreError::WalMismatch {
583 line: line_no,
584 message: format!("重放终态失败: {e}"),
585 })?;
586 }
587 }
588 }
589 Ok(state)
590 }
591}
592
593#[cfg(test)]
594mod tests {
595 use super::*;
596 use crate::clock::{Clock, MockClock};
597 use crate::gate::DenyReason;
598
599 fn tmp_wal(tag: &str) -> std::path::PathBuf {
600 use std::sync::atomic::{AtomicU64, Ordering};
601 static SEQ: AtomicU64 = AtomicU64::new(0);
602 let dir = std::env::temp_dir().join("wanning-state-tests");
603 std::fs::create_dir_all(&dir).expect("建临时目录");
604 let nanos = std::time::SystemTime::now()
606 .duration_since(std::time::UNIX_EPOCH)
607 .map(|d| d.as_nanos())
608 .unwrap_or(0);
609 dir.join(format!(
610 "{tag}-{}-{}-{nanos}.jsonl",
611 std::process::id(),
612 SEQ.fetch_add(1, Ordering::SeqCst)
613 ))
614 }
615
616 fn delegation() -> Delegation {
617 Delegation::new(
618 "d1",
619 "boss",
620 "claude-code",
621 1000,
622 1000,
623 2000,
624 "agent:claude-code",
625 )
626 }
627
628 fn long_lived_delegation() -> Delegation {
631 Delegation::new(
632 "d1",
633 "boss",
634 "claude-code",
635 1000,
636 1000,
637 SystemClock.now().checked_add(86_400).expect("有效期溢出"),
638 "agent:claude-code",
639 )
640 }
641
642 fn intent(nonce: u64, amount_cents: u64) -> SpendIntent {
643 SpendIntent::new("d1", nonce, amount_cents, "jd:shop-1", "grocery", "测试")
644 }
645
646 #[test]
647 fn allow_and_deny_are_both_recorded() {
648 let path = tmp_wal("both");
649 let clock = MockClock::new(1500);
650 let mut state = WanningState::with_wal(Arc::new(clock.clone()), &path).expect("开 WAL");
651 state.register_delegation(delegation()).expect("注册");
652
653 let a = state.decide(&intent(1, 500)).expect("判定");
655 assert!(a.is_allow());
656 let d = state.decide(&intent(2, 9000)).expect("判定");
658 assert_eq!(d.deny_reason(), Some(DenyReason::OverBudget));
659
660 let records = crate::wal::read_records(&path).expect("读回");
661 assert_eq!(records.len(), 3, "注册 + 放行 + 拒绝");
662 let (_, first_decide) = &records[1];
663 let (_, second_decide) = &records[2];
664 match (first_decide.kind(), first_decide.ts(), second_decide.kind()) {
665 ("decide", 1500, "decide") => {}
666 other => panic!("记录形状不符: {other:?}"),
667 }
668 let crate::wal::WalRecord::Decide {
670 decision,
671 reason,
672 budget_after_cents,
673 ..
674 } = second_decide
675 else {
676 panic!("第二条决策记录应是 Decide");
677 };
678 assert_eq!(*decision, WalDecision::Deny);
679 assert_eq!(*reason, Some(DenyReason::OverBudget));
680 assert_eq!(*budget_after_cents, 500, "拒绝不改账本,累计消费仍是 500");
681 }
682
683 #[test]
684 fn write_ahead_audit_failure_leaves_state_untouched() {
685 let dir = std::env::temp_dir().join("wanning-state-tests");
688 std::fs::create_dir_all(&dir).expect("建临时目录");
689 let nanos = std::time::SystemTime::now()
691 .duration_since(std::time::UNIX_EPOCH)
692 .map(|d| d.as_nanos())
693 .unwrap_or(0);
694 let path = dir.join(format!("dir-as-wal-{nanos}.jsonl"));
695 std::fs::create_dir_all(&path).expect("占位为目录");
696
697 let err = WanningState::with_wal(Arc::new(MockClock::new(1500)), &path).unwrap_err();
698 assert!(matches!(err, CoreError::WalIo(_)), "{err}");
699 }
700
701 #[test]
702 fn replay_rebuilds_state_and_is_deterministic() {
703 let path = tmp_wal("replay");
704 let clock = MockClock::new(1500);
705 let mut state = WanningState::with_wal(Arc::new(clock.clone()), &path).expect("开 WAL");
706 state.register_delegation(delegation()).expect("注册");
707 state.decide(&intent(1, 500)).expect("放行");
708 state.decide(&intent(2, 9000)).expect("超额拒");
709 state.decide(&intent(3, 100)).expect("再放行");
710 state.revoke("d1").expect("撤销");
711 state.decide(&intent(4, 100)).expect("撤销后拒");
712
713 let live_hash = state.state_hash();
714 assert_eq!(
715 state.gate().spent_cents("d1"),
716 Some(600),
717 "实时累计消费 = 500 + 100"
718 );
719
720 let replayed = WanningState::replay(&path).expect("回放");
722 let hash_once = replayed.state_hash();
723 let replayed_again = WanningState::replay(&path).expect("回放二遍");
724 let hash_twice = replayed_again.state_hash();
725
726 assert_eq!(hash_once, hash_twice, "回放两遍 hash 必相同(确定性)");
727 assert_eq!(hash_once, live_hash, "回放态必须与实时态完全一致");
728 assert_eq!(replayed.gate().spent_cents("d1"), Some(600));
729 assert!(replayed.gate().is_revoked("d1"));
730 assert!(
731 replayed
732 .gate()
733 .replay_registry()
734 .contains("agent:claude-code", 1),
735 "重放登记也必须被重建"
736 );
737 assert_eq!(replayed.wal_line_count(), None, "回放态不追加记录");
738 }
739
740 #[test]
741 fn replay_uses_recorded_ts_so_expiry_reproduces() {
742 let path = tmp_wal("expiry");
745 let clock = MockClock::new(1500);
746 let mut state = WanningState::with_wal(Arc::new(clock.clone()), &path).expect("开 WAL");
747 state.register_delegation(delegation()).expect("注册");
748 state.decide(&intent(1, 100)).expect("放行");
749 clock.set_now(2000); state.decide(&intent(2, 100)).expect("过期拒");
751
752 let replayed = WanningState::replay(&path).expect("回放");
753 assert_eq!(replayed.state_hash(), state.state_hash());
754 }
755
756 #[test]
757 fn replay_fails_closed_on_corrupted_line() {
758 let path = tmp_wal("replay-corrupt");
759 let mut state = WanningState::with_wal(Arc::new(MockClock::new(1500)), &path).expect("开");
760 state.register_delegation(delegation()).expect("注册");
761 drop(state);
762 use std::io::Write;
764 let mut f = std::fs::OpenOptions::new()
765 .append(true)
766 .open(&path)
767 .expect("开");
768 f.write_all(b"{\"kind\":\"decide\",\"ts\":1,\"dele\n")
769 .expect("追加坏行");
770 drop(f);
771
772 match WanningState::replay(&path) {
773 Err(CoreError::WalBadLine { line, .. }) => assert_eq!(line, 2),
774 other => panic!("应 fail-closed 报错,实际 {other:?}"),
775 }
776 }
777
778 #[test]
779 fn replay_fails_closed_when_record_disagrees_with_recomputation() {
780 let path = tmp_wal("replay-tampered");
782 let mut state = WanningState::with_wal(Arc::new(MockClock::new(1500)), &path).expect("开");
783 state.register_delegation(delegation()).expect("注册");
784 state.decide(&intent(1, 100)).expect("放行");
785 drop(state);
786 use std::io::Write;
790 let verified = crate::wal::read_verified(&path).expect("读已有历史");
791 let forged = crate::wal::WalLine {
792 seq: verified.records.len() as u64 + 1,
793 prev: verified.tail,
794 rec: WalRecord::Decide {
795 ts: 1500,
796 decision: WalDecision::Allow,
797 delegation_id: "d1".to_string(),
798 intent: intent(1, 100),
799 reason: None,
800 budget_after_cents: 200,
801 },
802 };
803 let mut f = std::fs::OpenOptions::new()
804 .append(true)
805 .open(&path)
806 .expect("开");
807 f.write_all(serde_json::to_string(&forged).unwrap().as_bytes())
808 .and_then(|()| f.write_all(b"\n"))
809 .expect("追加");
810 drop(f);
811
812 match WanningState::replay(&path) {
813 Err(CoreError::WalMismatch { line, message }) => {
814 assert_eq!(line, 3, "不一致要指到行");
815 assert!(message.contains("不一致"), "{message}");
816 }
817 other => panic!("篡改记录必须 fail-closed,实际 {other:?}"),
818 }
819 }
820
821 #[test]
822 fn audit_chain_tail_matches_independent_read_side_recompute() {
823 let path = tmp_wal("chain-tail");
825 let mut state = WanningState::with_wal(Arc::new(MockClock::new(1500)), &path).expect("开");
826 state.register_delegation(delegation()).expect("注册");
827 state.decide(&intent(1, 500)).expect("放行");
828 state.decide(&intent(2, 9000)).expect("超额拒");
829 state.revoke("d1").expect("撤销");
830
831 let live_tail = state.audit_chain_tail().expect("必有 WAL");
832 let verified = crate::wal::read_verified(&path).expect("读回验链");
833 assert_eq!(verified.tail, live_tail, "读侧独立重算链尾 == 实时链尾");
834 assert_eq!(
835 WanningState::replay(&path).expect("回放").state_hash(),
836 state.state_hash(),
837 "链验过后,回放对账照常成立"
838 );
839 }
840
841 #[test]
842 fn live_resuming_fails_closed_on_broken_chain() {
843 let path = tmp_wal("resume-chain");
846 {
847 let mut state =
848 WanningState::with_wal(Arc::new(MockClock::new(1500)), &path).expect("开");
849 state.register_delegation(delegation()).expect("注册");
850 state.decide(&intent(1, 100)).expect("放行");
851 state.decide(&intent(2, 9000)).expect("超额拒");
852 }
853 let mut lines = crate::wal::raw_lines(&path).expect("读 WAL");
854 let mut value: serde_json::Value = serde_json::from_str(&lines[1]).expect("行是 JSON");
855 value["rec"]["intent"]["memo"] = serde_json::json!("被改写的备注");
856 lines[1] = value.to_string();
857 std::fs::write(&path, lines.join("\n") + "\n").expect("重写 WAL");
858
859 match WanningState::live_resuming(&path) {
860 Err(CoreError::WalChainBroken { line, .. }) => {
861 assert_eq!(line, 3, "断链点 = 被改行的下一行(prev 对不上)")
862 }
863 other => panic!("链断裂必须拒启,实际 {other:?}"),
864 }
865 }
866
867 #[test]
868 fn state_hash_changes_when_state_changes() {
869 let path = tmp_wal("hash");
870 let clock = MockClock::new(1500);
871 let mut state = WanningState::with_wal(Arc::new(clock.clone()), &path).expect("开");
872 state.register_delegation(delegation()).expect("注册");
873 let h0 = state.state_hash();
874 state.decide(&intent(1, 100)).expect("放行");
875 let h1 = state.state_hash();
876 state.revoke("d1").expect("撤销");
877 let h2 = state.state_hash();
878 assert_ne!(h0, h1, "扣减后 hash 必变");
879 assert_ne!(h1, h2, "撤销后 hash 必变");
880 }
881
882 #[test]
883 fn empty_wal_replays_to_empty_state() {
884 let path = tmp_wal("empty");
885 std::fs::write(&path, "").expect("写空文件");
886 let replayed = WanningState::replay(&path).expect("空 WAL 是合法状态");
887 assert_eq!(
888 replayed.state_hash(),
889 WanningState::new(Arc::new(MockClock::new(0))).state_hash()
890 );
891 }
892
893 #[test]
898 fn live_resuming_carries_ledger_revocations_and_nonces() {
899 let path = tmp_wal("resume");
900 {
901 let clock = MockClock::new(1500);
902 let mut state = WanningState::with_wal(Arc::new(clock.clone()), &path).expect("开");
903 state
904 .register_delegation(long_lived_delegation())
905 .expect("注册");
906 state.decide(&intent(1, 500)).expect("放行");
907 state.decide(&intent(2, 100)).expect("再放行");
908 state.revoke("d1").expect("撤销");
909 } let resumed = WanningState::live_resuming(&path).expect("续跑");
912 assert_eq!(resumed.gate().spent_cents("d1"), Some(600));
914 assert!(resumed.gate().is_revoked("d1"), "撤销必须跨重启存活");
915 assert_eq!(
916 resumed.state_hash(),
917 WanningState::replay(&path).expect("回放").state_hash(),
918 "续跑态与回放态必须一致"
919 );
920 assert!(
922 resumed.gate().clock().now() > 1_700_000_000,
923 "续跑必须用系统时钟,得到 {}",
924 resumed.gate().clock().now()
925 );
926
927 let mut resumed = resumed;
930 let deny = resumed.decide(&intent(3, 100)).expect("判定");
931 assert_eq!(deny.deny_reason(), Some(DenyReason::Revoked));
932 let replay_deny = resumed.decide(&intent(1, 100)).expect("判定");
933 assert_eq!(replay_deny.deny_reason(), Some(DenyReason::Revoked));
934 assert_eq!(resumed.gate().spent_cents("d1"), Some(600), "账本不动");
935 let records = crate::wal::read_records(&path).expect("读回");
936 assert_eq!(records.len(), 6, "注册+2 放行+撤销+续跑后 2 条拒绝");
937 }
938
939 #[test]
940 fn live_resuming_on_fresh_wal_starts_empty() {
941 let path = tmp_wal("resume-fresh");
942 let mut state = WanningState::live_resuming(&path).expect("新 WAL 直接续跑=空账开张");
943 assert_eq!(
944 state.state_hash(),
945 WanningState::replay(&path).expect("回放").state_hash()
946 );
947 state
948 .register_delegation(long_lived_delegation())
949 .expect("注册");
950 assert!(state.decide(&intent(1, 100)).expect("判定").is_allow());
951 }
952
953 #[test]
954 fn live_resuming_fails_closed_on_corrupted_wal() {
955 let path = tmp_wal("resume-corrupt");
956 {
957 let mut state =
958 WanningState::with_wal(Arc::new(MockClock::new(1500)), &path).expect("开");
959 state.register_delegation(delegation()).expect("注册");
960 }
961 use std::io::Write;
962 let mut f = std::fs::OpenOptions::new()
963 .append(true)
964 .open(&path)
965 .expect("开");
966 f.write_all(b"{\"kind\":\"decide\",\"ts\":1,\"dele\n")
967 .expect("追加坏行");
968 drop(f);
969
970 match WanningState::live_resuming(&path) {
971 Err(CoreError::WalBadLine { line, .. }) => assert_eq!(line, 2),
972 other => panic!("审计损坏必须拒启,实际 {other:?}"),
973 }
974 }
975}