fix: enforce stable shadow snapshot boundaries

This commit is contained in:
zgw
2026-09-03 15:07:37 +08:00
parent 935d7af43d
commit d77fac1d56
12 changed files with 256 additions and 88 deletions

View File

@@ -141,7 +141,8 @@ class ShadowPlanningEngineTest {
assertThatThrownBy(() -> engine.planInbound(SNAPSHOT, "TASK-1", "TRAY-1"))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("published snapshot changed during planning");
verify(mapper, never()).insertComparison(any(), any(), any(), any(Long.class), any(), any());
verify(mapper, never()).insertComparison(any(), any(), any(), any(Long.class),
any(Instant.class), any(), any(), any());
}
@Test
@@ -160,30 +161,66 @@ class ShadowPlanningEngineTest {
@Test
void comparisonPersistsLeasedGenerationAsInputVersion() {
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
when(mapper.selectPublishedSnapshot("01")).thenReturn(descriptor(SNAPSHOT));
when(mapper.insertComparison(any(), any(), any(), any(Long.class),
any(Instant.class), any(), any(), any())).thenReturn(1);
ShadowPlanningEngine engine = engine(mock(SchedulingFacade.class), mapper);
InboundShadowObservation observation = new InboundShadowObservation(
true, 42L, "01", "JOB-1", "READY", "SPACE-9", "shadow diagnostic");
true, 42L, "01", SNAPSHOT.synchronizedAt(), SNAPSHOT.sourceSignature(),
"JOB-1", "READY", "SPACE-9", "shadow diagnostic");
engine.compareInbound(observation, "READY", "SPACE-9");
verify(mapper).insertComparison(any(), eq("01"), eq("INBOUND_COMPARE"), eq(42L),
eq("MATCH"), argThat(reason -> reason.contains("request=JOB-1")));
eq(SNAPSHOT.synchronizedAt()), eq("ABC123"), eq("MATCH"),
argThat(reason -> reason.contains("request=JOB-1")));
}
@Test
void comparisonDoesNotPersistAfterPublishedGenerationChanges() {
void comparisonFailsWhenAtomicDescriptorGuardRejectsTheInsert() {
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
when(mapper.selectPublishedSnapshot("01"))
.thenReturn(descriptor(snapshot(43L, "01")));
ShadowPlanningEngine engine = engine(mock(SchedulingFacade.class), mapper);
InboundShadowObservation observation = new InboundShadowObservation(
true, 42L, "01", "JOB-1", "READY", "SPACE-9", "shadow diagnostic");
true, 42L, "01", SNAPSHOT.synchronizedAt(), SNAPSHOT.sourceSignature(),
"JOB-1", "READY", "SPACE-9", "shadow diagnostic");
assertThatThrownBy(() -> engine.compareInbound(observation, "READY", "SPACE-9"))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("published snapshot changed before comparison");
verify(mapper, never()).insertComparison(any(), any(), any(), any(Long.class), any(), any());
verify(mapper).insertComparison(any(), eq("01"), eq("INBOUND_COMPARE"), eq(42L),
eq(SNAPSHOT.synchronizedAt()), eq("ABC123"), any(), any());
}
@Test
void outboundComparisonCarriesTheCompleteSnapshotDescriptor() {
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
when(mapper.insertComparison(any(), any(), any(), any(Long.class),
any(Instant.class), any(), any(), any())).thenReturn(1);
ShadowPlanningEngine engine = engine(mock(SchedulingFacade.class), mapper);
OutboundShadowObservation observation = new OutboundShadowObservation(
true, 42L, "01", SNAPSHOT.synchronizedAt(), SNAPSHOT.sourceSignature(),
"DETAIL-1", "READY", "STOCK-1", "SPACE-9", "shadow diagnostic");
engine.compareOutbound(observation, "10");
verify(mapper).insertComparison(any(), eq("01"), eq("OUTBOUND_COMPARE"), eq(42L),
eq(SNAPSHOT.synchronizedAt()), eq("ABC123"), eq("LEGACY_10"), any());
}
@Test
void comparisonReasonIsBoundedToDecisionLogCapacity() {
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
when(mapper.insertComparison(any(), any(), any(), any(Long.class),
any(Instant.class), any(), any(), any())).thenReturn(1);
ShadowPlanningEngine engine = engine(mock(SchedulingFacade.class), mapper);
InboundShadowObservation observation = new InboundShadowObservation(
true, 42L, "01", SNAPSHOT.synchronizedAt(), SNAPSHOT.sourceSignature(),
"JOB-1", "READY", "SPACE-9", "X".repeat(2500));
engine.compareInbound(observation, "READY", "SPACE-9");
verify(mapper).insertComparison(any(), any(), any(), any(Long.class),
any(Instant.class), any(), any(),
argThat(reason -> reason.length() <= 2000));
}
@Test
@@ -247,7 +284,12 @@ class ShadowPlanningEngineTest {
assertThat(statements.get("selectStockReference"))
.contains("alg.inventory_item_snapshot", "stock_id = #{stockid}");
assertThat(statements.get("insertComparison"))
.contains("convert(binary(8), #{inputversion})");
.contains("convert(binary(8), #{inputversion})",
"from alg.shadow_snapshot_state state with (updlock, holdlock)",
"state.snapshot_status = 'published'",
"state.snapshot_generation = #{inputversion}",
"state.synchronized_at = #{snapshotsynchronizedat}",
"convert(varchar(64), state.source_signature, 2) = #{sourcesignature}");
assertThat(statements.get("selectPublishedSnapshot"))
.contains("alg.shadow_snapshot_state", "convert(varchar(64),",
"source_signature", ", 2)");