mateclaw/mateclaw-server/src/main/java/vip/mate/approval/MetadataDecision.java
matevip 2f93d53737 refactor(approval): unify state machine across DB/metadata/memory
Foundation for the ghost-approval root-cause fix.

Adds ResolveOutcome / MetadataDecision; rewrites ApprovalWorkflowService so
every resolve / consume / timeout / supersede transitions through one
two-phase contract: snapshot → DB UPDATE conditional on status=PENDING →
metadata reconciliation → afterCommit memory mutation. ChatController,
ChannelMessageRouter, and ApprovalController all switch to the workflow;
ApprovalService.resolve / resolveAndConsume / consumeApproved /
cancelStalePending / denyAllByConversation are physically removed so
DB-bypass is no longer reachable at compile time.

Specific fixes:
- recoverFromDb preserves DB pendingId + createdAt (was generating fresh
  random ids, breaking every later DB sync)
- effectiveExpireAt = expireAt ?? createdAt + PENDING_TTL: legacy rows
  with NULL expireAt no longer resurrect as live PENDING after restart
- markPendingApprovalsResolved flips pendingApproval.status + currentPhase
  + MessageEntity.status atomically (was only flipping the first field;
  message.status uses existing completed/stopped, not approved/denied,
  to stay within the frontend Message.status union)
- GC scheduler moves to ApprovalWorkflowService; timeouts and overflow
  evictions now sync DB + metadata + memory through markTimeout
- DB UPDATE rows=0 returns alreadyResolved (concurrent-resolve safe);
  exception propagates so @Transactional rolls back; memory stays untouched
- expireRecoveredRow gates metadata write on DB success (was writing
  metadata even when DB update failed, producing the worst-case ghost)
- Mockito JDK 21 agent attach fixed via maven-dependency-plugin properties
  + surefire argLine (no more flaky self-attach across machines)

Tests: 34 new across 4 classes (recovery, resolve, GC, metadata sync).
Full suite: 788 / 788.
2026-04-27 19:30:52 +08:00

33 lines
1.5 KiB
Java

package vip.mate.approval;
/**
* Two-valued decision used when reconciling persisted approval state.
* <p>
* The frontend's {@code Message.status} union (mateclaw-ui/src/types/index.ts) only supports
* {@code generating | completed | stopped | failed | awaiting_approval | interrupted}, so
* approval decisions never appear at the message-status layer. {@code PendingApprovalMeta.status}
* holds the decision ({@code approved} / {@code denied}); when the message itself was
* persisted as {@code awaiting_approval} we collapse it back to one of the existing terminal
* message states ({@code completed} for approved, {@code stopped} for denied) so downstream
* consumers (history sanitizer, list ordering, stuck counters) keep working unchanged.
* <p>
* Timeout / superseded both map to {@link #DENIED} at the metadata layer (DB layer keeps
* the more specific {@code TIMEOUT} / {@code SUPERSEDED} status for audit purposes).
*/
public enum MetadataDecision {
APPROVED("approved", "completed"),
DENIED("denied", "stopped");
/** Target value for {@code metadata.pendingApproval.status}. */
public final String pendingApprovalStatus;
/** Target value for {@code MessageEntity.status} when source was {@code awaiting_approval}. */
public final String messageStatus;
MetadataDecision(String pendingApprovalStatus, String messageStatus) {
this.pendingApprovalStatus = pendingApprovalStatus;
this.messageStatus = messageStatus;
}
}