Files
flow-center-service/src/test/java/com/yunhai/flowcenter/callback/FlStateEventRecoveryTest.java
T
chenxing 500a9a2fcb release: FL 2.0 候选(C01 记账边界+P2-2 认领协议+D10 变量契约+D11 资格终审+D09 激活代次+T01/T03/T04/T05)
按《FL最终代码审计与上线阻断清单》194 项已审计工作树形成候选,基座 = origin/main 11e0778。

- 源码/测试/资源:沿用已审计工作树内容(96 项相对远端为新增或更新),含未跟踪实现
  CallbackFactStore、CallbackSourcePolicy、VariableContract、FormalApplyCommandService、
  FlowActorContextGate 等;C01 续验夹具修正一并入内。
- CI:保留远端 11e0778 的门禁强化(CI_SOURCE_SHA 核对、容器回收验证、空 schema 反向门禁、
  main ref 才推镜像);ci/flow-test-schema.sql 按当前实体重新生成(14→20 表,补 6 新表与
  6 新列,回调日志复合唯一键 uk_flw_log_event_reg),workflow 表数断言同步 14→20。
- 依据:docs/tasks/evidence/FL-final-audit-20261027(194 项哈希/版本核对)。
2026-10-27 12:05:08 +08:00

193 lines
8.6 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.yunhai.flowcenter.callback;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yunhai.flowcenter.engine.FlowEvent;
import com.yunhai.flowcenter.entity.FlwDef;
import com.yunhai.flowcenter.entity.FlwInstance;
import com.yunhai.flowcenter.entity.FlwStateEvent;
import com.yunhai.flowcenter.repository.FlwDefRepository;
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
import com.yunhai.flowcenter.repository.FlwStateEventRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* 状态事件事实(T05;2026-10-25 R2 返修增补)规则单测:
*
* <ul>
* <li>事实与状态<b>同事务</b>落库(同步监听器语义)并为事件赋稳定身份;</li>
* <li>eventId 全链复用(事实行=投递行=重试);同事实重复发布不重复落行;</li>
* <li>同事务事实写失败=状态事务一并失败(不吞);</li>
* <li>URGE 站内信不入事件流;</li>
* <li>R2:事实载荷=完整冻结信封(含 eventId/eventSeq/flowGuid),并随身份冻结到事件对象——
* 正常分发与崩溃恢复共用同一冻结体。</li>
* </ul>
*/
@DisplayName("状态事件事实 FlwStateEventRecorder(T05/R2)")
class FlStateEventRecoveryTest {
private FlwStateEventRepository eventRepository;
private FlwInstanceRepository instanceRepository;
private FlwDefRepository defRepository;
private FlwStateEventRecorder recorder;
@BeforeEach
void setUp() {
eventRepository = mock(FlwStateEventRepository.class);
instanceRepository = mock(FlwInstanceRepository.class);
defRepository = mock(FlwDefRepository.class);
recorder = new FlwStateEventRecorder(eventRepository, instanceRepository, defRepository,
new ObjectMapper());
when(eventRepository.save(any())).thenAnswer(c -> {
FlwStateEvent e = c.getArgument(0);
e.setId(1L);
return e;
});
FlwInstance instance = new FlwInstance();
instance.setId(900L);
instance.setSchId(27L);
instance.setDefId(55L);
instance.setApplyRound(2);
instance.setBizType("svc-hall");
instance.setBizId("42");
when(instanceRepository.findById(900L)).thenReturn(Optional.of(instance));
when(instanceRepository.nextEventSeqForUpdate(900L)).thenReturn(5);
when(instanceRepository.setEventSeqCounter(Mockito.eq(900L), Mockito.anyInt())).thenReturn(1);
FlwDef def = new FlwDef();
def.setId(55L);
def.setFlowGuid("fg-r2-envelope");
when(defRepository.findById(55L)).thenReturn(Optional.of(def));
}
@Test
@DisplayName("同步监听:事实落库并赋稳定身份(eventId+实例内 seq=max+1)")
void recordAssignsStableIdentity() {
FlowEvent event = FlowEvent.of(FlowEvent.Type.SUBMITTED, 900L);
recorder.record(event);
Mockito.verify(eventRepository).save(any(FlwStateEvent.class));
assertThat(event.getStableEventId()).isNotBlank();
assertThat(event.getStableEventSeq()).isEqualTo(5);
}
@Test
@DisplayName("同事件对象重复发布:不重复落事实、身份不变")
void duplicatePublishKeepsIdentity() {
FlowEvent event = FlowEvent.of(FlowEvent.Type.COMPLETED, 900L);
recorder.record(event);
String first = event.getStableEventId();
recorder.record(event);
Mockito.verify(eventRepository, Mockito.times(1)).save(any(FlwStateEvent.class));
assertThat(event.getStableEventId()).isEqualTo(first);
}
@Test
@DisplayName("URGE 站内信:不入事件流(既有口径不变)")
void urgeSkipped() {
recorder.record(FlowEvent.of(FlowEvent.Type.URGE, 900L));
Mockito.verify(eventRepository, never()).save(any(FlwStateEvent.class));
}
@Test
@DisplayName("事实载荷快照含轮次身份(旧事件不被 AFTER_COMMIT 重读贴新轮标签)")
void payloadCarriesRoundIdentity() throws Exception {
FlowEvent event = FlowEvent.of(FlowEvent.Type.REJECTED, 900L);
event.captureRoundIdentity(2, "tok-1");
recorder.record(event);
Mockito.verify(eventRepository).save(Mockito.argThat(f -> {
try {
com.fasterxml.jackson.databind.JsonNode payload =
new ObjectMapper().readTree(f.getPayloadJson());
return payload.path("applyRound").asInt() == 2
&& "tok-1".equals(payload.path("data").path("requestToken").asText());
} catch (Exception e) {
return false;
}
}));
}
@Test
@DisplayName("R2:事实载荷=完整冻结信封——eventId/eventSeq/flowGuid/接收端必填四件套全部在体")
void payloadIsFullFrozenEnvelope() throws Exception {
FlowEvent event = FlowEvent.of(FlowEvent.Type.SUBMITTED, 900L);
recorder.record(event);
ArgumentCaptor<FlwStateEvent> captor = ArgumentCaptor.forClass(FlwStateEvent.class);
verify(eventRepository).save(captor.capture());
com.fasterxml.jackson.databind.JsonNode payload =
new ObjectMapper().readTree(captor.getValue().getPayloadJson());
// 接收端 FlowCallbackReceiver 必填:eventId/event/bizType/bizId(缺一按脏消息 200 丢弃)
assertThat(payload.path("eventId").asText()).isEqualTo(event.getStableEventId());
assertThat(payload.path("event").asText()).isEqualTo("flow.submitted");
assertThat(payload.path("bizType").asText()).isEqualTo("svc-hall");
assertThat(payload.path("bizId").asText()).isEqualTo("42");
// R2 新冻结字段:稳定序号 + flowGuid(原实现只在 HTTP 头/列里,消息体缺失)
assertThat(payload.path("eventSeq").asInt()).isEqualTo(5);
assertThat(payload.path("flowGuid").asText()).isEqualTo("fg-r2-envelope");
// 既有信封字段不回退
assertThat(payload.has("instanceId")).isTrue();
assertThat(payload.has("instanceVersion")).isTrue();
assertThat(payload.has("occurredTime")).isTrue();
}
@Test
@DisplayName("R2:冻结信封随身份一并赋给事件对象——分发器正常链直接复用冻结体(与恢复链同构)")
void frozenEnvelopeAssignedToEvent() throws Exception {
FlowEvent event = FlowEvent.of(FlowEvent.Type.COMPLETED, 900L);
recorder.record(event);
assertThat(event.getStablePayloadJson()).isNotBlank();
com.fasterxml.jackson.databind.JsonNode payload =
new ObjectMapper().readTree(event.getStablePayloadJson());
assertThat(payload.path("eventId").asText()).isEqualTo(event.getStableEventId());
assertThat(payload.path("flowGuid").asText()).isEqualTo("fg-r2-envelope");
// 重复发布不换冻结体(幂等)
String first = event.getStablePayloadJson();
recorder.record(event);
assertThat(event.getStablePayloadJson()).isEqualTo(first);
}
@Test
@DisplayName("R2:定义已删(def 缺席)——信封不带 flowGuid,不炸记录器(该场合分发器同样不投递)")
void missingDefStillRecordsWithoutFlowGuid() throws Exception {
FlwInstance instance = new FlwInstance();
instance.setId(901L);
instance.setSchId(27L);
instance.setDefId(404L);
instance.setBizType("svc-hall");
instance.setBizId("43");
when(instanceRepository.findById(901L)).thenReturn(Optional.of(instance));
when(defRepository.findById(404L)).thenReturn(Optional.empty());
when(instanceRepository.nextEventSeqForUpdate(901L)).thenReturn(1);
when(instanceRepository.setEventSeqCounter(Mockito.eq(901L), Mockito.anyInt())).thenReturn(1);
FlowEvent event = FlowEvent.of(FlowEvent.Type.SUBMITTED, 901L);
recorder.record(event);
ArgumentCaptor<FlwStateEvent> captor = ArgumentCaptor.forClass(FlwStateEvent.class);
verify(eventRepository).save(captor.capture());
com.fasterxml.jackson.databind.JsonNode payload =
new ObjectMapper().readTree(captor.getValue().getPayloadJson());
assertThat(payload.has("flowGuid")).isFalse();
assertThat(payload.path("eventId").asText()).isEqualTo(event.getStableEventId());
}
}