feat(self-operated): persist continuous collection and frozen coverage

This commit is contained in:
2026-10-13 21:13:31 +08:00
parent c2bb769676
commit 242a76221e
45 changed files with 1021 additions and 118 deletions
@@ -24,7 +24,7 @@ class SelfopStageEstimateTest {
@Test void sufficientRealAdvancesEstimateCurrentStageAndIncludeIdleTime(){
var p=ready();var e=estimate(p,60);assertEquals("AVAILABLE",e.status());assertEquals(60L,e.completedDelta());
assertEquals(0,new BigDecimal("1").compareTo(e.ratePerSecond()));assertEquals(939L,e.remainingSeconds());
assertEquals(start,e.sampleFrom());assertEquals(start.plusSeconds(60),e.sampleUntil());assertEquals(start.plusSeconds(70),e.validUntil());
assertEquals(start,e.sampleFrom());assertEquals(start.plusSeconds(60),e.sampleUntil());assertEquals(start.plusSeconds(90),e.validUntil());
var idle=estimate(p,90);assertEquals("AVAILABLE",idle.status());assertEquals(new BigDecimal("0.666666"),idle.ratePerSecond());
assertEquals(1409L,idle.remainingSeconds());assertEquals("NO_RECENT_PROGRESS",estimate(p,120).unavailableReason());
}
@@ -33,6 +33,26 @@ class SelfopStageEstimateTest {
assertEquals("SAMPLING",estimate(p,59).unavailableReason());assertEquals("SAMPLING",estimate(p,60).unavailableReason());
commit(p,60,4,true);assertEquals("AVAILABLE",estimate(p,60).status());
}
@Test void unchangedBookkeepingDoesNotRestartSamplingOrInventAnAdvance(){
var p=ready();commit(p,65,61,false);
var e=estimate(p,65);
assertEquals("AVAILABLE",e.status());assertEquals(start,e.sampleFrom());
assertEquals(60L,e.completedDelta());assertEquals(new BigDecimal("0.923076"),e.ratePerSecond());
commit(p,119,61,false);
assertEquals("NO_RECENT_PROGRESS",estimate(p,120).unavailableReason());
}
@Test void rollingWindowKeepsAValidityMarginAcrossTenSecondPollingBoundaries(){
var p=progress();p.put("total",100000L);
for(int second=0;second<=600;second++){
commit(p,second,second,true);
if(second>=80&&second%10==0){
var e=estimate(p,second);
assertEquals("AVAILABLE",e.status(),"second="+second);
assertEquals(start.plusSeconds(second+30),e.validUntil());
assertFalse(e.sampleFrom().isBefore(e.validUntil().minusSeconds(SelfopStageEstimate.WINDOW_SECONDS)));
}
}
}
@Test void reuseBeforeInsideAndAfterWindowRestartsBaselineInsteadOfInflatingRate(){
var p=progress();commit(p,0,500,false);commit(p,20,501,true);commit(p,40,502,true);commit(p,60,503,true);
assertEquals(3L,estimate(p,60).completedDelta());
@@ -160,7 +160,7 @@ class OutboundCandidateBuildMySqlAcceptanceTest {
LocalDateTime now=utcNow().minusSeconds(1);String scopeJson=json(Map.of("quantityUnit","PCS","businessTimezone","Asia/Shanghai","sourceTimezone","Asia/Shanghai","fixtureOrigin","SYNTHETIC_BUILD_ACCEPTANCE"));String hash=hash(scopeJson);
transactions.executeWithoutResult(s->{
insert("ll_selfop_sync_scope",fields("id",scopeId,"created_at",now,"updated_at",now,"source_account",account,"warehouse_code",warehouse,"data_family","OUTBOUND","scope_json",scopeJson,"scope_hash",hash,"business_timezone","Asia/Shanghai","source_timezone","Asia/Shanghai","schedule_cron","0 0 0 1 1 *","stale_after_seconds",93600,"review_request_budget",1,"review_time_budget_seconds",1,"review_target_days",7,"fact_revision",1,"published_fact_revision",0,"fence_token",1,"lease_owner",owner,"lease_until_utc",utcNow().plusHours(1)));
for(long id:List.of(originTask,taskId))insert("ll_selfop_sync_task",fields("id",id,"created_at",now,"updated_at",now,"task_no","SYNTH-BUILD-"+id,"execution_purpose","BUSINESS","source_account",account,"warehouse_code",warehouse,"task_role","WAREHOUSE_SYNC","scope_id",scopeId,"request_key","build-"+id,"request_hash",hash,"task_type","REBUILD","query_scope_json",scopeJson,"scope_hash",hash,"status",id==taskId?"RUNNING":"SUCCEEDED","apply_state","APPLIED","attempt_count",1,"query_from_utc",utc(from),"query_until_utc",utc(until),"routing_manifest_json","{\"directoryComplete\":true}"));
for(long id:List.of(originTask,taskId))insert("ll_selfop_sync_task",fields("id",id,"created_at",now,"updated_at",now,"task_no","SYNTH-BUILD-"+id,"execution_purpose","BUSINESS","source_account",account,"warehouse_code",warehouse,"task_role","WAREHOUSE_SYNC","scope_id",scopeId,"request_key","build-"+id,"request_hash",hash,"task_type","REBUILD","query_scope_json",scopeJson,"scope_hash",hash,"status",id==taskId?"RUNNING":"SUCCEEDED","apply_state","APPLIED","apply_completed_at_utc",now,"attempt_count",1,"query_from_utc",utc(from),"query_until_utc",utc(until),"routing_manifest_json","{\"directoryComplete\":true}"));
for(long id:List.of(originRun,runId))insert("ll_selfop_sync_run",fields("id",id,"created_at",now,"updated_at",now,"task_id",id==runId?taskId:originTask,"execution_purpose","BUSINESS","scope_id",scopeId,"source_account",account,"warehouse_code",warehouse,"run_role","WAREHOUSE_SYNC","attempt_no",1,"scope_hash",hash,"scope_snapshot_json",scopeJson,"fence_token",1,"status","FACTS_APPLIED","last_phase","BUILD","coverage_status","UNKNOWN"));
});
for(int offset=0;offset<skus*days;offset+=500) {
@@ -10,6 +10,47 @@ import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
class OutboundCandidateBuilderTest {
@Test void disconnectedCompleteDayIsFrozenWithoutFillingGapOrChangingAnchor(){
var first=island(7);var latest=island(12);scope.setInitialFromUtc(first.getCoveredFromUtc());
task.setQueryFromUtc(latest.getCoveredFromUtc());task.setQueryUntilUtc(latest.getCoveredUntilUtc());
when(aggregates.contiguousCoverage(1L,"scope",scope.getInitialFromUtc(),null,null)).thenReturn(first);
when(aggregates.provenCoverageIslands(1L,"scope")).thenReturn(List.of(first,latest));
builder.build(1L,10L,"worker",2);
var proof=SelfopSalesCoverage.evidence(run);
assertTrue(SelfopSalesCoverage.contains(proof,latest.getCoveredFromUtc(),latest.getCoveredUntilUtc()));
assertFalse(SelfopSalesCoverage.contains(proof,first.getCoveredFromUtc(),latest.getCoveredUntilUtc()));
assertEquals(first.getCoveredUntilUtc(),run.getCoveredUntilUtc());assertEquals(first.getCoveredFromUtc(),scope.getInitialFromUtc());
var frozen=run.getScopeSnapshotJson();when(aggregates.provenCoverageIslands(1L,"scope")).thenReturn(List.of(first,latest,island(14)));
assertEquals(frozen,run.getScopeSnapshotJson());
assertFalse(SelfopSalesCoverage.contains(SelfopSalesCoverage.evidence(run),island(14).getCoveredFromUtc(),island(14).getCoveredUntilUtc()));
}
@Test void disconnectedZeroSalesDayForcesNewCandidateEvenWithUnchangedRevisionAndContinuousWatermark(){
var first=island(7);var latest=island(12);scope.setInitialFromUtc(first.getCoveredFromUtc());
scope.setPublishedRunId(9L);scope.setPublishedFactRevision(5L);
var published=new SelfopSyncRun();published.setId(9L);published.setScopeId(1L);published.setScopeHash("scope");
published.setStatus("PUBLISHED");published.setExecutionPurpose("BUSINESS");published.setCalculatedFactRevision(5L);
published.setWarehouseDailyReady(true);published.setBuildStartedAtUtc(SelfopSupport.utcNow().minusHours(1));
published.setCoveredFromUtc(first.getCoveredFromUtc());published.setCoveredUntilUtc(first.getCoveredUntilUtc());published.setScopeSnapshotJson(scope.getScopeJson());
SelfopSalesCoverage.freeze(published,List.of(new SelfopSalesCoverage.Interval(first.getCoveredFromUtc(),first.getCoveredUntilUtc())));
when(runs.selectById(9L)).thenReturn(published);
when(aggregates.contiguousCoverage(1L,"scope",scope.getInitialFromUtc(),null,null)).thenReturn(first);
when(aggregates.provenCoverageIslands(1L,"scope")).thenReturn(List.of(first,latest));
assertNull(builder.reusable(1L,10L,"worker",2));
assertEquals(1,SelfopSalesCoverage.evidence(published).intervals().size());
}
private SelfopSyncRun island(int day){
var r=new SelfopSyncRun();r.setCoveredFromUtc(LocalDateTime.of(2026,10,day,0,0).minusHours(8));r.setCoveredUntilUtc(r.getCoveredFromUtc().plusDays(1));return r;
}
@Test void fillingMiddleDaysMergesFrozenProofAndResumedBuildDoesNotReadLaterProof(){
var merged=island(7);merged.setCoveredUntilUtc(island(12).getCoveredUntilUtc());scope.setInitialFromUtc(merged.getCoveredFromUtc());
when(aggregates.contiguousCoverage(1L,"scope",scope.getInitialFromUtc(),null,null)).thenReturn(merged);
when(aggregates.provenCoverageIslands(1L,"scope")).thenReturn(List.of(merged));
var checkpoint=transaction.begin(1L,10L,"worker",2);var frozen=run.getScopeSnapshotJson();
when(aggregates.provenCoverageIslands(1L,"scope")).thenReturn(List.of(merged,island(14)));
assertEquals(checkpoint,transaction.begin(1L,10L,"worker",2));assertEquals(frozen,run.getScopeSnapshotJson());
assertTrue(SelfopSalesCoverage.contains(SelfopSalesCoverage.evidence(run),merged.getCoveredFromUtc(),merged.getCoveredUntilUtc()));
verify(aggregates,times(1)).provenCoverageIslands(1L,"scope");
}
final OutboundLockMapper locks=mock(OutboundLockMapper.class);
final OutboundFactService facts=mock(OutboundFactService.class);
final SelfopSyncRunMapper runs=mock(SelfopSyncRunMapper.class);
@@ -37,6 +78,7 @@ class OutboundCandidateBuilderTest {
when(locks.scope(1L)).thenReturn(scope); when(locks.run(10L)).thenReturn(run); when(locks.task(20L)).thenReturn(task);
SelfopSyncRun coverage=new SelfopSyncRun();coverage.setCoveredFromUtc(task.getQueryFromUtc());coverage.setCoveredUntilUtc(task.getQueryUntilUtc());
when(aggregates.contiguousCoverage(1L,"scope",scope.getInitialFromUtc(),null,null)).thenReturn(coverage);
when(aggregates.provenCoverageIslands(1L,"scope")).thenReturn(List.of(coverage));
}
@Test void manualFirstSliceUsesOnlyProvenIntervalWithoutInventingFullDays() {
scope.setInitialFromUtc(null);
@@ -90,6 +132,7 @@ class OutboundCandidateBuilderTest {
published.setWarehouseDailyReady(true);published.setStatus("PUBLISHED"); published.setExecutionPurpose("BUSINESS"); published.setCalculatedFactRevision(5L);
published.setBuildStartedAtUtc(SelfopSupport.utcNow().minusHours(1));
published.setScopeSnapshotJson(scope.getScopeJson()); published.setCoveredFromUtc(scope.getInitialFromUtc());
SelfopSalesCoverage.freeze(published,List.of(new SelfopSalesCoverage.Interval(task.getQueryFromUtc(),task.getQueryUntilUtc())));
published.setCoveredUntilUtc(task.getQueryUntilUtc().minusDays(1)); when(runs.selectById(9L)).thenReturn(published);
assertNull(builder.reusable(1L,10L,"worker",2));
published.setCoveredUntilUtc(task.getQueryUntilUtc()); assertEquals(9L,builder.reusable(1L,10L,"worker",2));
@@ -16,6 +16,71 @@ import static org.mockito.Mockito.*;
/** Offline contracts: SQL supplies bounded pages, never an in-memory warehouse history. */
class AnalyticsQueryServiceTest {
@Test void frozenHistoryStartExistsBeforeContinuousAnchorAndIncludesEarlierDisconnectedDays(){
when(mapper.publishedRun(11)).thenReturn(frozenRun(null,null,
new com.baiyue.lastleg.selfoperated.analytics.model.SelfopSalesCoverage.Interval(time(D.minusDays(1)),time(D))));
assertEquals(D.minusDays(1),service.inventoryCoverage(metrics(null),options()).records().get(0).observedHistoryStartDate());
assertEquals(D.minusDays(1),service.slowSkus(metrics(null)).records().get(0).observedHistoryStartDate());
when(mapper.publishedRun(11)).thenReturn(frozenRun(time(D.minusDays(5)),time(D.minusDays(4)),
new com.baiyue.lastleg.selfoperated.analytics.model.SelfopSalesCoverage.Interval(time(D.minusDays(10)),time(D.minusDays(9))),
new com.baiyue.lastleg.selfoperated.analytics.model.SelfopSalesCoverage.Interval(time(D.minusDays(5)),time(D.minusDays(4)))));
assertEquals(D.minusDays(10),service.inventoryCoverage(metrics(null),options()).records().get(0).observedHistoryStartDate());
assertEquals(D.minusDays(10),service.slowSkus(metrics(null)).records().get(0).observedHistoryStartDate());
}
@Test void slowTodayZeroUsesFrozenIslandAndMatchesTrendWithoutAuthorizingThirtyDays(){
var json=new ObjectMapper().findAndRegisterModules();
var row=json.convertValue(aggregate(true,"300","VALID",time(D.minusDays(40))),Map.class);row.put("todayQuantity",null);
when(mapper.page(any())).thenReturn(List.of(json.convertValue(row,Aggregate.class)));
when(mapper.publishedRun(11)).thenReturn(frozenRun(time(D.minusDays(5)),time(D.minusDays(4)),
new com.baiyue.lastleg.selfoperated.analytics.model.SelfopSalesCoverage.Interval(time(D.minusDays(5)),time(D.minusDays(4))),
new com.baiyue.lastleg.selfoperated.analytics.model.SelfopSalesCoverage.Interval(time(D),utc(NOW.minusSeconds(3600)))));
var trend=service.dailySales(chart("TODAY",null,null,"C","SKU")).points().get(0);
assertEquals("PARTIAL",trend.dataStatus());assertEquals(BigDecimal.ZERO,trend.observedQuantity());
assertEquals(trend.observedQuantity(),service.slowSkus(metrics(null)).records().get(0).todayObservedQuantity());
var cap=ArgumentCaptor.forClass(AnalyticsSqlRequest.class);verify(mapper).page(cap.capture());
assertFalse(cap.getValue().complete30());assertFalse(cap.getValue().completeWindow());
row.put("evidenceAtUtc",utc(NOW));when(mapper.page(any())).thenReturn(List.of(json.convertValue(row,Aggregate.class)));
assertNull(service.slowSkus(metrics(null)).records().get(0).todayObservedQuantity());
}
@Test void slowUncoveredTodayRemainsUnknownAndHistoricalGapIsNotFilled(){
var json=new ObjectMapper().findAndRegisterModules();
var row=json.convertValue(aggregate(true,"300","VALID",time(D.minusDays(40))),Map.class);row.put("todayQuantity",null);
when(mapper.page(any())).thenReturn(List.of(json.convertValue(row,Aggregate.class)));
when(mapper.publishedRun(11)).thenReturn(frozenRun(time(D.minusDays(5)),time(D.minusDays(4)),
new com.baiyue.lastleg.selfoperated.analytics.model.SelfopSalesCoverage.Interval(time(D.minusDays(5)),time(D.minusDays(4))),
new com.baiyue.lastleg.selfoperated.analytics.model.SelfopSalesCoverage.Interval(time(D.minusDays(1)),time(D))));
var trend=service.dailySales(chart("TODAY",null,null,"C","SKU")).points().get(0);
assertEquals("UNKNOWN",trend.dataStatus());assertNull(trend.observedQuantity());
assertNull(service.slowSkus(metrics(null)).records().get(0).todayObservedQuantity());
var gap=service.dailySales(chart("CUSTOM",D.minusDays(3),D.minusDays(2),"C","SKU"));
assertTrue(gap.points().stream().allMatch(p->"UNKNOWN".equals(p.dataStatus())&&p.observedQuantity()==null));
var cap=ArgumentCaptor.forClass(AnalyticsSqlRequest.class);verify(mapper).page(cap.capture());assertFalse(cap.getValue().complete30());
}
private static Run frozenRun(LocalDateTime from,LocalDateTime until,
com.baiyue.lastleg.selfoperated.analytics.model.SelfopSalesCoverage.Interval... intervals){
Run original=run(11,1,SH,NOW.minusSeconds(3600),null,"PUBLISHED","BUSINESS","Asia/Shanghai");
var frozen=new com.baiyue.lastleg.selfoperated.analytics.model.SelfopSyncRun();
frozen.setId(11L);frozen.setScopeId(1L);frozen.setScopeHash(SH);frozen.setScopeSnapshotJson(original.scopeSnapshotJson());
com.baiyue.lastleg.selfoperated.analytics.model.SelfopSalesCoverage.freeze(frozen,List.of(intervals));
var json=new ObjectMapper().findAndRegisterModules();var fields=json.convertValue(original,Map.class);
fields.put("scopeSnapshotJson",frozen.getScopeSnapshotJson());fields.put("coveredFromUtc",from);fields.put("coveredUntilUtc",until);
return json.convertValue(fields,Run.class);
}
@Test void selectedRunFrozenIslandsDriveDailyAndWindowEligibility(){
Run original=run(11,1,SH,NOW.minusSeconds(3600),null,"PUBLISHED","BUSINESS","Asia/Shanghai");
var frozen=new com.baiyue.lastleg.selfoperated.analytics.model.SelfopSyncRun();frozen.setId(11L);frozen.setScopeId(1L);frozen.setScopeHash(SH);frozen.setScopeSnapshotJson(original.scopeSnapshotJson());
com.baiyue.lastleg.selfoperated.analytics.model.SelfopSalesCoverage.freeze(frozen,List.of(
new com.baiyue.lastleg.selfoperated.analytics.model.SelfopSalesCoverage.Interval(time(D.minusDays(5)),time(D.minusDays(4))),
new com.baiyue.lastleg.selfoperated.analytics.model.SelfopSalesCoverage.Interval(time(D),time(D.plusDays(1)))));
var json=new ObjectMapper().findAndRegisterModules();var fields=json.convertValue(original,Map.class);fields.put("scopeSnapshotJson",frozen.getScopeSnapshotJson());
fields.put("coveredFromUtc",time(D.minusDays(5)));fields.put("coveredUntilUtc",time(D.minusDays(4)));
when(mapper.publishedRun(11)).thenReturn(json.convertValue(fields,Run.class));service=at(NOW.plusSeconds(86400));
var response=service.dailySales(chart("CUSTOM",D.minusDays(5),D,"C","SKU"));
assertEquals(List.of("COMPLETE","UNKNOWN","UNKNOWN","UNKNOWN","UNKNOWN","COMPLETE"),response.points().stream().map(AnalyticsResponses.Point::dataStatus).toList());
assertNull(response.summary().outboundQuantity());assertEquals(BigDecimal.ZERO,response.points().get(5).outboundQuantity());
var snapshot=service.context(chart("CUSTOM",D,D,null,null),true,false,false,null);
assertTrue(service.request(snapshot,"SUMMARY").completeWindow());assertFalse(service.request(snapshot,"SUMMARY").complete30());
}
private static final String SH="a".repeat(64), IH="b".repeat(64);
private static final LocalDate D=LocalDate.of(2026,10,12);
private static final Instant NOW=Instant.parse("2026-10-12T04:00:00Z");
@@ -16,7 +16,7 @@ class AnalyticsReadMapperTest {
private static AnalyticsSqlRequest request(String kind){
LocalDate d=LocalDate.of(2026,10,12);
return new AnalyticsSqlRequest("demo","W","OWNER","Exact-SKU",11,22,d.minusDays(2),d.plusDays(1),d,d.minusDays(7),
d.minusDays(30),d,LocalDateTime.of(2026,9,11,16,0),LocalDateTime.of(2026,10,12,3,0),true,true,true,true,true,20,20,kind);
d.minusDays(30),d,LocalDateTime.of(2026,9,11,16,0),LocalDateTime.of(2026,10,12,3,0),true,true,true,true,true,20,20,kind,java.util.List.of());
}
private static BoundSql bound(Configuration c,String method,Map<String,Object> parameters){return c.getMappedStatement(NS+method).getBoundSql(parameters);}
private static void immutableOnly(String sql){
@@ -17,6 +17,16 @@ import static org.junit.jupiter.api.Assertions.*;
* original BoundSql mappings. Candidate counts and separate binding contracts remain prepared.
* No external database, credentials or HTTP; MySQL plans/performance remain separate gates. */
public class AnalyticsSqlSemanticTest {
@Test void frozenDisconnectedDaysPreserveZeroDayAndUnknownGapInSummarySql() throws Exception {
profile(11,"C","SKU","VALID_OUTBOUND",utc(D.minusDays(40),0),"PCS");
daily(11,"W","C","SKU",D,"12");
var coverage=List.of(new com.baiyue.lastleg.selfoperated.analytics.model.SelfopSalesCoverage.Interval(utc(D.minusDays(5),0),utc(D.minusDays(4),0)),
new com.baiyue.lastleg.selfoperated.analytics.model.SelfopSalesCoverage.Interval(utc(D,0),utc(D.plusDays(1),0)));
var p=new AnalyticsSqlRequest("demo","W",null,null,11,22,D.minusDays(5),D.plusDays(1),D.plusDays(1),D.minusDays(30),D.minusDays(5),D.minusDays(4),
utc(D.minusDays(5),0),utc(D.minusDays(4),0),true,true,false,false,false,0,20,"SUMMARY",coverage);
var result=page(p).get(0);assertEquals(2,result.completeDayCount());assertEquals(0,result.partialDayCount());assertEquals(4,result.unknownDayCount());
assertEquals(0,new BigDecimal("12").compareTo(result.observedQuantity()));
}
private Connection schema;
private SqlSession session;
private AnalyticsReadMapper mapper;
@@ -68,7 +78,7 @@ public class AnalyticsSqlSemanticTest {
daily(11,"W","C","SOLD",from.minusDays(1),"999");daily(11,"W","C","SOLD",until,"999");
daily(33,"W","C","SOLD",from,"999");daily(11,"OTHER","C","SOLD",from,"999");
var p=new AnalyticsSqlRequest("demo","W",null,null,11,22,from,until,D,D.minusDays(30),from,D,
utc(from,0),utc(D,8),true,true,false,true,true,0,20,"SUMMARY");
utc(from,0),utc(D,8),true,true,false,true,true,0,20,"SUMMARY",java.util.List.of());
assertEquals(3,mapper.count(p));var rows=page(p);assertEquals(3,rows.size());
var sold=rows.get(0);assertEquals("SOLD",sold.skuCode());assertDecimal("5",sold.observedQuantity());
assertEquals(2L,sold.skuOrderCount());assertEquals(365,sold.completeDayCount());
@@ -147,15 +157,15 @@ public class AnalyticsSqlSemanticTest {
new LocalDateTime[]{utc(D.plusDays(1),0),utc(D.plusDays(2),0)},
new LocalDateTime[]{null,utc(D,8)})){
var p=new AnalyticsSqlRequest("demo","W",null,null,11,22,D,D.plusDays(1),D,D.minusDays(30),null,null,
span[0],span[1],true,false,false,false,true,0,1,"SUMMARY");
span[0],span[1],true,false,false,false,true,0,1,"SUMMARY",java.util.List.of());
var first=page(p).get(0);assertEquals("A-UNKNOWN",first.skuCode());assertNull(first.observedQuantity());
var last=new AnalyticsSqlRequest("demo","W",null,null,11,22,D,D.plusDays(1),D,D.minusDays(30),null,null,
span[0],span[1],true,false,false,false,true,1,1,"SUMMARY");
span[0],span[1],true,false,false,false,true,1,1,"SUMMARY",java.util.List.of());
assertNull(page(last).get(0).observedQuantity());
}
daily(11,"W","C","Z-EVIDENCE",D,"99");
var unavailable=new AnalyticsSqlRequest("demo","W",null,null,11,22,D,D.plusDays(1),D,D.minusDays(30),null,null,
utc(D,0),utc(D,8),false,false,false,false,true,0,1,"SUMMARY");
utc(D,0),utc(D,8),false,false,false,false,true,0,1,"SUMMARY",java.util.List.of());
var first=page(unavailable).get(0);assertEquals("A-UNKNOWN",first.skuCode());assertNull(first.observedQuantity());
}
@Test void evidenceAfterCoveredTimeOnSameDayDoesNotBecomeObservedZero() throws Exception {
@@ -307,7 +317,7 @@ public class AnalyticsSqlSemanticTest {
daily(11,"W","A","SAME",D.minusDays(1),"2");daily(11,"W","A","SAME",D,"5");
daily(11,"W","B","SAME",D.minusDays(2),"9");
var p=new AnalyticsSqlRequest("demo","W",null,null,11,22,D.minusDays(365),D.plusDays(1),D,D.minusDays(30),
D.minusDays(365),D,utc(D.minusDays(365),0),utc(D,8),true,true,false,true,true,0,20,"SUMMARY");
D.minusDays(365),D,utc(D.minusDays(365),0),utc(D,8),true,true,false,true,true,0,20,"SUMMARY",java.util.List.of());
assertEquals(4,mapper.count(p));var rows=page(p);assertEquals(4,rows.size());
for(var row:rows)if("SAME".equals(row.skuCode())){
assertDecimal("A".equals(row.customerCode())?"8":"9",row.observedQuantity());
@@ -432,11 +442,11 @@ public class AnalyticsSqlSemanticTest {
profile(22,"C","SKU","INVENTORY_OBSERVATION",utc(D.minusDays(60),0),"PCS");stock("C","SKU",0,"10");daily(11,"W","C","SKU",D.minusDays(1),"3");
var options=new AnalyticsInventoryOptions(null,null,null,90,null,null,null,null,null,null);
var stale=new AnalyticsSqlRequest("demo","W",null,null,11,22,D.minusDays(30),D,D,D.minusDays(30),D.minusDays(30),D,
utc(D.minusDays(30),0),utc(D,8),true,true,true,true,false,0,20,"INVENTORY");
utc(D.minusDays(30),0),utc(D,8),true,true,true,true,false,0,20,"INVENTORY",java.util.List.of());
var row=inventoryRows(stale,options).get(0);assertNull(row.physicalQuantity());assertNull(row.availableQuantity());assertDecimal("3",row.ownerOutbound());assertNull(row.coverageDays());
var noSales=new AnalyticsSqlRequest("demo","W",null,null,0,22,D.minusDays(30),D,D,D.minusDays(30),null,null,null,null,false,false,false,false,true,0,20,"INVENTORY");
var noSales=new AnalyticsSqlRequest("demo","W",null,null,0,22,D.minusDays(30),D,D,D.minusDays(30),null,null,null,null,false,false,false,false,true,0,20,"INVENTORY",java.util.List.of());
row=inventoryRows(noSales,options).get(0);assertDecimal("10",row.availableQuantity());assertNull(row.ownerOutbound());assertNull(row.observedOutbound());assertNull(row.outboundQuantity30d());assertNull(row.lastOutbound());
var empty=new AnalyticsSqlRequest("demo","OTHER",null,null,0,0,D.minusDays(30),D,D,D.minusDays(30),null,null,null,null,false,false,false,false,false,0,20,"INVENTORY");
var empty=new AnalyticsSqlRequest("demo","OTHER",null,null,0,0,D.minusDays(30),D,D,D.minusDays(30),null,null,null,null,false,false,false,false,false,0,20,"INVENTORY",java.util.List.of());
assertTrue(inventoryRows(empty,options).isEmpty());assertEquals(0L,((Number)literalQuery("inventoryCount",empty,options).get(0)).longValue());
}
@Test void tenDayInventoryUsesItsOwnCompleteCoverageAndUnroundedThresholdWithoutLeakingScope()throws Exception {
@@ -449,7 +459,7 @@ public class AnalyticsSqlSemanticTest {
daily(99,"W","C","TEN",D.minusDays(1),"700");daily(11,"OTHER","C","TEN",D.minusDays(1),"600");
daily(11,"W","OTHER","TEN",D.minusDays(1),"500");
var p=new AnalyticsSqlRequest("demo","W","C",null,11,22,D.minusDays(10),D,D,D.minusDays(30),D.minusDays(10),D,
utc(D.minusDays(10),0),utc(D,8),true,true,true,false,true,0,20,"INVENTORY");
utc(D.minusDays(10),0),utc(D,8),true,true,true,false,true,0,20,"INVENTORY",java.util.List.of());
var o=new AnalyticsInventoryOptions(null,null,"COVERAGE_DAYS_DESC",30,null,null,null,null,10,D.minusDays(1));
var rows=inventoryRows(p,o);var ten=rows.stream().filter(r->r.skuCode().equals("TEN")).findFirst().orElseThrow();
assertDecimal("2",ten.outboundQuantity30d());assertDecimal("30.04",ten.coverageDays());assertEquals(1,ten.aboveThreshold());
@@ -464,7 +474,7 @@ public class AnalyticsSqlSemanticTest {
assertEquals(1L,((Number)literalQuery("inventoryCount",p,filtered).get(0)).longValue());
assertEquals(List.of("TEN"),inventoryRows(p,filtered).stream().map(AnalyticsReadRows.Inventory::skuCode).toList());
var partial=new AnalyticsSqlRequest("demo","W","C",null,11,22,D.minusDays(10),D,D,D.minusDays(30),D.minusDays(9),D,
utc(D.minusDays(9),0),utc(D,8),true,true,false,false,true,0,20,"INVENTORY");
utc(D.minusDays(9),0),utc(D,8),true,true,false,false,true,0,20,"INVENTORY",java.util.List.of());
assertTrue(inventoryRows(partial,o).stream().allMatch(r->r.outboundQuantity30d()==null&&r.coverageDays()==null));
}
@SuppressWarnings("unchecked") private List<AnalyticsReadRows.Inventory> inventoryRows(AnalyticsSqlRequest p,AnalyticsInventoryOptions o){
@@ -536,7 +546,7 @@ public class AnalyticsSqlSemanticTest {
}
private AnalyticsSqlRequest request(String kind,LocalDate from,LocalDate until,String owner,String sku,LocalDateTime coveredUntil,long offset,int size) {
return new AnalyticsSqlRequest("demo","W",owner,sku,11,22,from,until,D,D.minusDays(30),D.minusDays(30),D,
utc(D.minusDays(30),0),coveredUntil,true,true,true,true,true,offset,size,kind);
utc(D.minusDays(30),0),coveredUntil,true,true,true,true,true,offset,size,kind,java.util.List.of());
}
private static LocalDateTime utc(LocalDate d,int hour){return d.atTime(hour,0).atZone(ZoneId.of("Asia/Shanghai")).withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime();}
private void profile(long run,String owner,String sku,String evidence,LocalDateTime at,String unit)throws Exception {
@@ -29,6 +29,15 @@ class CollectionQueryServiceTest {
return new Task(id,"LXCOL-"+id,"primary",account?null:"A","OUTBOUND",account?"ACCOUNT_DISCOVERY":"WAREHOUSE_SYNC","BACKFILL","BUSINESS",
"hash",status,"NOT_STARTED",1,now.minusDays(1),now,null,upstream,null,null,progress,now,null,null,false,created,created);
}
@Test void taskElapsedIncludesWaitingAndRetriesAndStopsAtTerminalTaskUpdate() {
for(String status:List.of("RUNNING","RETRY_WAIT","WAITING_SOURCE","SUCCEEDED")) {
var task=activity(7,status,null,false,now.minusMinutes(10),null);
when(mapper.task("primary","A",task.taskNo())).thenReturn(task);
var result=service.taskDetail(task.taskNo(),q(null,null,null)).task();
assertEquals(now.minusMinutes(10).toInstant(ZoneOffset.UTC),result.createdAt());
assertEquals("SUCCEEDED".equals(status)?0L:600L,result.elapsedSeconds());
}
}
@Test void currentActivityWinsOverNewerHistoryAndIsBoundedToEight() {
var candidates=new ArrayList<Task>();
candidates.add(activity(99,"SUCCEEDED",null,false,now,null));
@@ -132,6 +141,37 @@ class CollectionQueryServiceTest {
config.setEnvironment("development");
for(var row:service.overview(q(null,null,null)).warehouses()){assertFalse(row.executionEnabled());assertEquals("BUSINESS_ENVIRONMENT_REJECTED",row.disabledReason());}
}
@Test void continuousTestBusinessShowsDailyPlanDespiteExpiredGrantButKeepsSafetyGates() {
enableBusiness("test");config.setContinuousBusinessCollectionEnabled(true);
config.setExclusiveGrantExpiresAtUtc(now.minusDays(1));
var rows=service.overview(q(null,null,null)).warehouses();
var outbound=rows.stream().filter(r->"OUTBOUND".equals(r.dataFamily())).findFirst().orElseThrow();
assertTrue(outbound.executionEnabled());
assertEquals(Instant.parse("2026-10-12T16:05:00Z"),outbound.nextScheduledAt());
var atSlot=new CollectionQueryService(mapper,config,new ObjectMapper(),Clock.fixed(Instant.parse("2026-10-12T16:05:00Z"),ZoneOffset.UTC));
assertEquals(Instant.parse("2026-10-13T16:05:00Z"),atSlot.overview(q(null,null,null)).warehouses().stream()
.filter(r->"OUTBOUND".equals(r.dataFamily())).findFirst().orElseThrow().nextScheduledAt());
config.setPriorEgressClosed(false);
for(var row:service.overview(q(null,null,null)).warehouses()) {
assertFalse(row.executionEnabled());assertNull(row.nextScheduledAt());
assertEquals("EXCLUSIVE_GRANT_NOT_VALID",row.disabledReason());
}
config.setPriorEgressClosed(true);config.setContinuousBusinessCollectionEnabled(false);
assertFalse(service.overview(q(null,null,null)).warehouses().get(0).executionEnabled());
}
@Test void continuousBudgetRequiresEnabledModeAndNoDeadlineWhileBoundedBudgetsStillExpire() {
enableBusiness("test");config.setContinuousBusinessCollectionEnabled(true);
String continuous="{\"continuous\":true,\"startedAtUtc\":\""+now.minusDays(2)+"\",\"maxHttp\":0,\"http\":10,\"confirmedHttp\":10,\"deadlineAtUtc\":null,\"stopReason\":null}";
when(mapper.task("primary","A","LXCOL-TASK")).thenReturn(manualBusinessTask(continuous));
assertEquals(List.of("RETRY"),service.taskDetail("LXCOL-TASK",q(null,null,null)).task().allowedActions());
when(mapper.task("primary","A","LXCOL-TASK")).thenReturn(manualBusinessTask(continuous.replace("\"deadlineAtUtc\":null","\"deadlineAtUtc\":\""+now.plusDays(1)+"\"")));
assertEquals("BUSINESS_SESSION_BUDGET_MISSING",service.taskDetail("LXCOL-TASK",q(null,null,null)).task().actionBlockReason());
when(mapper.task("primary","A","LXCOL-TASK")).thenReturn(manualBusinessTask(businessBudget(now.minusMinutes(1).toString(),null,10)));
assertEquals("BUSINESS_SESSION_DEADLINE",service.taskDetail("LXCOL-TASK",q(null,null,null)).task().actionBlockReason());
config.setContinuousBusinessCollectionEnabled(false);
when(mapper.task("primary","A","LXCOL-TASK")).thenReturn(manualBusinessTask(continuous));
assertEquals("CONTINUOUS_COLLECTION_DISABLED",service.taskDetail("LXCOL-TASK",q(null,null,null)).task().actionBlockReason());
}
Scope scope() {
return new Scope(1L,"A","OUTBOUND","hash","Asia/Shanghai","0 0 8,20 * * *",93600,
now.minusMonths(2),now,null,null,0,null,null,null,7,null,null);
@@ -9,43 +9,52 @@ import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
class DashboardPolicyTest {
@Test void frozenIslandsProveLatestEmptyDayWithoutFillingInterveningDates(){
var first=DAY.minusDays(6).atStartOfDay().minusHours(8);var last=DAY.minusDays(1).atStartOfDay().minusHours(8);
var coverage=List.of(new com.baiyue.lastleg.selfoperated.analytics.model.SelfopSalesCoverage.Interval(first,first.plusDays(1)),
new com.baiyue.lastleg.selfoperated.analytics.model.SelfopSalesCoverage.Interval(last,last.plusDays(1)));
var c=context(first.toInstant(ZoneOffset.UTC),first.plusDays(1).toInstant(ZoneOffset.UTC),true);
var points=DashboardSalesPolicy.points(c,true,DAY.minusDays(6),DAY,List.of(),coverage);
assertEquals(List.of("COMPLETE","UNKNOWN","UNKNOWN","UNKNOWN","UNKNOWN","COMPLETE"),points.stream().map(p->p.dataStatus()).toList());
assertEquals(BigDecimal.ZERO,points.get(5).outboundQuantity());assertNull(SalesBucketPolicy.aggregate(points).outboundQuantity());
}
static final LocalDate DAY=LocalDate.of(2026,10,13);
static final Instant NOW=Instant.parse("2026-10-13T04:00:00Z");
static AnalyticsResponses.Context context(Instant coveredFrom,Instant coveredUntil,boolean inventory){return new AnalyticsResponses.Context(11L,inventory?22L:0L,"a".repeat(64),inventory?"b".repeat(64):null,NOW,NOW.plusSeconds(1800),"Asia/Shanghai",DAY,DAY.minusDays(14),DAY.plusDays(1),null,DAY.minusDays(30),DAY,coveredFrom,coveredUntil,NOW.minusSeconds(60),inventory?NOW.minusSeconds(60):null,inventory?NOW.minusSeconds(30):null,"VALID",inventory?"VALID":"UNKNOWN",false,"CURRENT",List.of());}
@Test void warehouseObservedTotalPreservesOutsideWatermarkAndUnknownGap(){
var c=context(null,null,true);var rows=List.of(new DashboardReadMapper.Day(DAY.minusDays(3),new BigDecimal("2"),0L),new DashboardReadMapper.Day(DAY.minusDays(1),new BigDecimal("5"),0L),new DashboardReadMapper.Day(DAY,BigDecimal.ONE,0L));
var points=DashboardSalesPolicy.points(c,true,DAY.minusDays(3),DAY.plusDays(1),rows);var summary=SalesBucketPolicy.aggregate(points);
var points=DashboardSalesPolicy.points(c,true,DAY.minusDays(3),DAY.plusDays(1),rows,java.util.List.of());var summary=SalesBucketPolicy.aggregate(points);
assertEquals(new BigDecimal("8"),summary.observedQuantity());assertNull(summary.outboundQuantity());assertNull(points.get(1).observedQuantity());assertEquals("PARTIAL",points.get(3).dataStatus());
}
@Test void completeWarehouseZeroAndPartialWarehouseZeroNeedNoSkuObservation(){
var c=context(DAY.minusDays(30).atStartOfDay().toInstant(ZoneOffset.ofHours(8)),NOW,true);
var points=DashboardSalesPolicy.points(c,true,DAY.minusDays(1),DAY.plusDays(1),List.of());
var points=DashboardSalesPolicy.points(c,true,DAY.minusDays(1),DAY.plusDays(1),List.of(),java.util.List.of());
assertEquals("COMPLETE",points.get(0).dataStatus());assertEquals(BigDecimal.ZERO,points.get(0).outboundQuantity());
assertEquals("PARTIAL",points.get(1).dataStatus());assertEquals(BigDecimal.ZERO,points.get(1).observedQuantity());assertNull(points.get(1).outboundQuantity());
var unknown=SalesBucketPolicy.aggregate(DashboardSalesPolicy.points(context(null,null,false),false,DAY.minusDays(1),DAY.plusDays(1),List.of()));assertNull(unknown.observedQuantity());
var unknown=SalesBucketPolicy.aggregate(DashboardSalesPolicy.points(context(null,null,false),false,DAY.minusDays(1),DAY.plusDays(1),List.of(),java.util.List.of()));assertNull(unknown.observedQuantity());
}
@Test void staleCollectionDoesNotDowngradePublishedHistoricalDayOrInventOtherFourteenDays(){
LocalDate published=DAY.minusDays(3);var c=publishedDayContext("STALE",false);
for(BigDecimal amount:List.of(BigDecimal.ZERO,new BigDecimal("2251"))) {
var rows=amount.signum()==0?List.<DashboardReadMapper.Day>of():List.of(new DashboardReadMapper.Day(published,amount,0L));
var points=DashboardSalesPolicy.points(c,true,DAY.minusDays(14),DAY.plusDays(1),rows);
var points=DashboardSalesPolicy.points(c,true,DAY.minusDays(14),DAY.plusDays(1),rows,java.util.List.of());
var point=points.stream().filter(p->p.date().equals(published)).findFirst().orElseThrow();
assertEquals("COMPLETE",point.dataStatus());assertEquals(amount,point.outboundQuantity());
assertFalse(point.reasonCodes().contains("SALES_DAY_INCOMPLETE"));assertFalse(point.reasonCodes().contains("SALES_STALE"));
var summary=SalesBucketPolicy.aggregate(points);
assertEquals(1,summary.completeDays());assertEquals(14,summary.unknownDays());assertEquals(0,summary.partialDays());
assertEquals("PARTIAL",summary.dataStatus());assertNull(summary.outboundQuantity());assertEquals(amount,summary.observedQuantity());
var single=SalesBucketPolicy.aggregate(DashboardSalesPolicy.points(c,true,published,published.plusDays(1),rows));
var single=SalesBucketPolicy.aggregate(DashboardSalesPolicy.points(c,true,published,published.plusDays(1),rows,java.util.List.of()));
assertEquals("COMPLETE",single.dataStatus());assertEquals(amount,single.outboundQuantity());
assertEquals("STALE",c.salesStatus());assertTrue(c.reasonCodes().contains("SALES_STALE"));
}
}
@Test void historicalRecencyFixDoesNotRelaxInvalidQuantitySourceOrRebuildGuards(){
LocalDate day=DAY.minusDays(3);var rows=List.of(new DashboardReadMapper.Day(day,new BigDecimal("2251"),0L));
assertEquals("UNKNOWN",DashboardSalesPolicy.points(publishedDayContext("STALE",false),false,day,day.plusDays(1),rows).get(0).dataStatus());
var invalid=DashboardSalesPolicy.points(publishedDayContext("STALE",false),true,day,day.plusDays(1),List.of(new DashboardReadMapper.Day(day,new BigDecimal("2"),1L))).get(0);
assertEquals("UNKNOWN",DashboardSalesPolicy.points(publishedDayContext("STALE",false),false,day,day.plusDays(1),rows,java.util.List.of()).get(0).dataStatus());
var invalid=DashboardSalesPolicy.points(publishedDayContext("STALE",false),true,day,day.plusDays(1),List.of(new DashboardReadMapper.Day(day,new BigDecimal("2"),1L)),java.util.List.of()).get(0);
assertEquals("PARTIAL",invalid.dataStatus());assertNull(invalid.outboundQuantity());assertTrue(invalid.reasonCodes().contains("SALES_QUANTITY_OR_UNIT_INVALID"));
var rebuilding=DashboardSalesPolicy.points(publishedDayContext("REBUILD_REQUIRED",true),true,day,day.plusDays(1),rows).get(0);
var rebuilding=DashboardSalesPolicy.points(publishedDayContext("REBUILD_REQUIRED",true),true,day,day.plusDays(1),rows,java.util.List.of()).get(0);
assertEquals("PARTIAL",rebuilding.dataStatus());assertNull(rebuilding.outboundQuantity());
}
private static AnalyticsResponses.Context publishedDayContext(String status,boolean rebuild){
@@ -20,7 +20,7 @@ class DashboardQueryServiceTest {
var range=SalesRangePolicy.resolve("LAST_15_DAYS",null,null,"DAY",now,ZoneId.of("Asia/Shanghai"),366);
var snapshot=new AnalyticsQueryService.Snapshot(q,salesRun(true),null,true,true,day.minusDays(30),day,range,context);
when(contexts.context(any(),eq(true),eq(false),eq(false),isNull())).thenReturn(snapshot);
var p=new AnalyticsSqlRequest("demo","W",null,null,11,0,day.minusDays(14),day.plusDays(1),day,day.minusDays(30),day.minusDays(30),day,day.minusDays(30).atStartOfDay().minusHours(8),day.atStartOfDay(),true,true,false,true,false,0,20,"DASHBOARD");
var p=new AnalyticsSqlRequest("demo","W",null,null,11,0,day.minusDays(14),day.plusDays(1),day,day.minusDays(30),day.minusDays(30),day,day.minusDays(30).atStartOfDay().minusHours(8),day.atStartOfDay(),true,true,false,true,false,0,20,"DASHBOARD",java.util.List.of());
when(contexts.request(snapshot,"DASHBOARD")).thenReturn(p);
when(mapper.daily(any(),any(),any())).thenReturn(List.of(new DashboardReadMapper.Day(day.minusDays(1),new BigDecimal("30"),0L)));
var reportRow=new HashMap<String,Object>(Map.of("total_skus",200L,"hot_eligible",200L,"hot_unknown",0L));
@@ -54,7 +54,7 @@ class DashboardQueryServiceTest {
var snapshot=new AnalyticsQueryService.Snapshot(q,salesPresent?salesRun(true):null,null,salesPresent,salesPresent,day.minusDays(30),day,range,context);
when(contexts.context(any(),eq(true),eq(false),eq(false),isNull())).thenReturn(snapshot);
when(contexts.request(snapshot,"DASHBOARD")).thenReturn(new AnalyticsSqlRequest("demo","W",null,null,salesPresent?11:0,0,day,day.plusDays(1),day,day.minusDays(30),day.minusDays(30),day,
day.minusDays(30).atStartOfDay().minusHours(8),day.atStartOfDay(),salesPresent,salesPresent,salesPresent,salesPresent,false,0,20,"DASHBOARD"));
day.minusDays(30).atStartOfDay().minusHours(8),day.atStartOfDay(),salesPresent,salesPresent,salesPresent,salesPresent,false,0,20,"DASHBOARD",java.util.List.of()));
var result=service.dashboard(q);
if(salesPresent){assertEquals(0,result.kpis().averageDailyOutboundQuantity30d().value().signum());assertEquals("COMPLETE",result.kpis().averageDailyOutboundQuantity30d().dataStatus());}
else {assertNull(result.kpis().averageDailyOutboundQuantity30d().value());assertEquals("UNKNOWN",result.kpis().averageDailyOutboundQuantity30d().dataStatus());}
@@ -44,12 +44,12 @@ class DashboardSqlSemanticTest {
daily("C","RECENT",D,"7","PCS");daily("C","OLD",D.minusDays(20),"100","PCS");
var b=request(false,true);
var p=new AnalyticsSqlRequest(b.account(),b.warehouse(),null,null,b.salesRun(),b.inventoryRun(),D.minusDays(2),D.plusDays(1),D,D.minusDays(2),
b.completeFrom(),b.completeUntil(),b.coveredFrom(),b.coveredUntil(),true,true,false,false,true,0,10,"HOT_DASHBOARD");
b.completeFrom(),b.completeUntil(),b.coveredFrom(),b.coveredUntil(),true,true,false,false,true,0,10,"HOT_DASHBOARD",java.util.List.of());
var rows=query(DashboardReadMapper.class,"report",Map.of("p",p,"o",options()));
assertEquals(1,rows.size());assertEquals("RECENT",((Map<?,?>)rows.get(0)).get("SKU_CODE"));
assertEquals(0,new BigDecimal("10").compareTo((BigDecimal)((Map<?,?>)rows.get(0)).get("MAIN_VALUE")));
var top=new AnalyticsSqlRequest(p.account(),p.warehouse(),null,null,p.salesRun(),p.inventoryRun(),p.from(),p.until(),D,p.from(),
p.completeFrom(),p.completeUntil(),p.coveredFrom(),p.coveredUntil(),true,true,false,false,true,0,100,"HOT");
p.completeFrom(),p.completeUntil(),p.coveredFrom(),p.coveredUntil(),true,true,false,false,true,0,100,"HOT",java.util.List.of());
var topRows=query(AnalyticsReadMapper.class,"page",Map.of("p",top));
assertEquals(1,topRows.size());var item=(AnalyticsReadRows.Aggregate)topRows.get(0);
assertEquals("RECENT",item.skuCode());assertEquals(0,new BigDecimal("10").compareTo(item.windowQuantity()));assertNull(item.sales30Quantity());
@@ -162,7 +162,7 @@ class DashboardSqlSemanticTest {
}
daily("C","SKU-199",D,"999","BOX");
assertEquals(200,count(stats(),"hot_eligible"));var top=ranks().stream().filter(r->r.rankCode().equals("HOT")).sorted(Comparator.comparing(DashboardReadMapper.Ranked::mainValue).reversed()).toList();assertEquals(10,top.size());
var p=request();var hot=new AnalyticsSqlRequest(p.account(),p.warehouse(),null,null,p.salesRun(),p.inventoryRun(),p.from(),p.until(),p.anchor(),p.windowFrom(),p.completeFrom(),p.completeUntil(),p.coveredFrom(),p.coveredUntil(),true,true,true,true,true,0,10,"HOT");
var p=request();var hot=new AnalyticsSqlRequest(p.account(),p.warehouse(),null,null,p.salesRun(),p.inventoryRun(),p.from(),p.until(),p.anchor(),p.windowFrom(),p.completeFrom(),p.completeUntil(),p.coveredFrom(),p.coveredUntil(),true,true,true,true,true,0,10,"HOT",java.util.List.of());
List<?> page=query(AnalyticsReadMapper.class,"page",Map.of("p",hot));assertEquals(10,page.size());
for(int i=0;i<10;i++){var item=(AnalyticsReadRows.Aggregate)page.get(i);assertEquals(top.get(i).skuCode(),item.skuCode());assertEquals(0,top.get(i).mainValue().compareTo(item.windowQuantity()));}
}
@@ -199,13 +199,13 @@ class DashboardSqlSemanticTest {
.stream().map(DashboardReadMapper.Day.class::cast).toList();
assertEquals(originalDailyOracle(D.minusDays(3),D.plusDays(1)),rows,"Published summaries must retain the former SKU aggregation's valid subtotal and invalid count");
var covered=DashboardPolicyTest.context(D.minusDays(30).atStartOfDay().toInstant(ZoneOffset.ofHours(8)),DashboardPolicyTest.NOW,true);
var points=DashboardSalesPolicy.points(covered,true,D.minusDays(3),D.plusDays(1),rows);
var points=DashboardSalesPolicy.points(covered,true,D.minusDays(3),D.plusDays(1),rows,java.util.List.of());
assertEquals("PARTIAL",points.get(0).dataStatus());assertEquals(0,new BigDecimal("7").compareTo(points.get(0).observedQuantity()));assertNull(points.get(0).outboundQuantity());
assertTrue(points.get(0).reasonCodes().contains("SALES_QUANTITY_OR_UNIT_INVALID"));
assertEquals("UNKNOWN",points.get(1).dataStatus());assertNull(points.get(1).observedQuantity());
assertEquals("COMPLETE",points.get(2).dataStatus());assertEquals(BigDecimal.ZERO,points.get(2).outboundQuantity());
assertEquals("PARTIAL",points.get(3).dataStatus());assertEquals(0,new BigDecimal("4").compareTo(points.get(3).observedQuantity()));assertNull(points.get(3).outboundQuantity());
var unproved=DashboardSalesPolicy.points(DashboardPolicyTest.context(null,null,true),true,D.minusDays(3),D.plusDays(1),rows);
var unproved=DashboardSalesPolicy.points(DashboardPolicyTest.context(null,null,true),true,D.minusDays(3),D.plusDays(1),rows,java.util.List.of());
assertTrue(unproved.stream().allMatch(point->point.outboundQuantity()==null));
assertNull(unproved.get(2).observedQuantity());assertEquals("UNKNOWN",unproved.get(2).dataStatus());
}
@@ -238,9 +238,9 @@ class DashboardSqlSemanticTest {
private void stock(String sku,String physical,String available)throws Exception{stock("C",sku,physical,available);}
private void stock(String owner,String sku,String physical,String available)throws Exception{try(var p=connection.prepareStatement("INSERT INTO ll_selfop_inventory_snapshot VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)")){
p.setLong(1,22);p.setString(2,"demo");p.setString(3,"W");p.setString(4,owner);p.setString(5,sku);p.setInt(6,0);p.setBigDecimal(7,physical==null?null:new BigDecimal(physical));p.setBigDecimal(8,available==null?null:new BigDecimal(available));p.setBigDecimal(9,BigDecimal.ZERO);p.setBigDecimal(10,BigDecimal.ZERO);p.setObject(11,D.atStartOfDay());p.setString(12,physical==null?"UNKNOWN":"VALID");p.setString(13,physical==null?"FIELD_MISSING":null);p.executeUpdate();}}
private AnalyticsSqlRequest request(){return new AnalyticsSqlRequest("demo","W",null,null,11,22,D.minusDays(30),D,D,D.minusDays(30),D.minusDays(30),D,D.minusDays(30).atStartOfDay().minusHours(8),D.atStartOfDay(),true,true,true,true,true,0,20,"DASHBOARD");}
private AnalyticsSqlRequest pageRequest(long offset,int size){var p=request();return new AnalyticsSqlRequest(p.account(),p.warehouse(),p.customer(),p.sku(),p.salesRun(),p.inventoryRun(),p.from(),p.until(),p.anchor(),p.windowFrom(),p.completeFrom(),p.completeUntil(),p.coveredFrom(),p.coveredUntil(),p.salesKnown(),p.formalSales(),p.completeWindow(),p.complete30(),p.stockFresh(),offset,size,"INVENTORY");}
private AnalyticsSqlRequest request(boolean complete,boolean fresh){var p=request();return new AnalyticsSqlRequest(p.account(),p.warehouse(),null,null,p.salesRun(),p.inventoryRun(),p.from(),p.until(),p.anchor(),p.windowFrom(),p.completeFrom(),p.completeUntil(),p.coveredFrom(),p.coveredUntil(),p.salesKnown(),p.formalSales(),complete,complete,fresh,p.offset(),p.size(),p.kind());}
private AnalyticsSqlRequest request(){return new AnalyticsSqlRequest("demo","W",null,null,11,22,D.minusDays(30),D,D,D.minusDays(30),D.minusDays(30),D,D.minusDays(30).atStartOfDay().minusHours(8),D.atStartOfDay(),true,true,true,true,true,0,20,"DASHBOARD",java.util.List.of());}
private AnalyticsSqlRequest pageRequest(long offset,int size){var p=request();return new AnalyticsSqlRequest(p.account(),p.warehouse(),p.customer(),p.sku(),p.salesRun(),p.inventoryRun(),p.from(),p.until(),p.anchor(),p.windowFrom(),p.completeFrom(),p.completeUntil(),p.coveredFrom(),p.coveredUntil(),p.salesKnown(),p.formalSales(),p.completeWindow(),p.complete30(),p.stockFresh(),offset,size,"INVENTORY",java.util.List.of());}
private AnalyticsSqlRequest request(boolean complete,boolean fresh){var p=request();return new AnalyticsSqlRequest(p.account(),p.warehouse(),null,null,p.salesRun(),p.inventoryRun(),p.from(),p.until(),p.anchor(),p.windowFrom(),p.completeFrom(),p.completeUntil(),p.coveredFrom(),p.coveredUntil(),p.salesKnown(),p.formalSales(),complete,complete,fresh,p.offset(),p.size(),p.kind(),java.util.List.of());}
private static AnalyticsInventoryOptions options(){return new AnalyticsInventoryOptions(null,null,null,null,null,null,null,null,null,null);}
@SuppressWarnings("unchecked") private List<Map<String,Object>> report(){
return ((List<Map<String,Object>>)(List<?>)query(DashboardReadMapper.class,"report",Map.of("p",request(),"o",options()))).stream().map(row->{
@@ -6,6 +6,15 @@ import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class SelfopBusinessBudgetTest {
@Test void continuousSessionHasNoDeadlineButKeepsStopAndAccountingGuards(){
var start=LocalDateTime.of(2026,10,13,0,0);
var budget=SelfopSupport.read(SelfopBusinessBudget.freezeContinuous(start),SelfopBusinessBudget.class);
assertTrue(budget.continuous);assertNull(budget.deadlineAtUtc);
budget.http=100000;budget.confirmedHttp=99999;
assertNull(budget.limit(start.plusYears(1)));
budget.stopReason="EGRESS_RESULT_UNKNOWN";assertEquals(budget.stopReason,budget.limit(start));
budget.deadlineAtUtc=start.plusDays(1);assertThrows(EgressRejectedException.class,budget::validate);
}
private final LocalDateTime start=LocalDateTime.of(2026,10,13,0,0);
@Test void serializedConsumptionAndDeadlineSurviveReload() {
@@ -44,6 +44,46 @@ class SelfopEgressServiceTest {
assertThat(budget().reservedHttp).isEqualTo(1);
}
@Test void frozenPageSizeCannotBeExpanded(){assertThatThrownBy(()->reserve(Operation.WAREHOUSES,11,null,null)).hasMessage("VALIDATION_PAGE_SIZE");assertThat(budget().reservedHttp).isZero();}
@Test void continuousBusinessGrantExpiryDoesNotClipShortPendingOrPermitAndSwitchKeepsCounters(){
testBusiness(5,300);success(reserve(Operation.WAREHOUSES,1,null,null),Operation.WAREHOUSES);
cfg.setContinuousBusinessCollectionEnabled(true);cfg.setExclusiveGrantExpiresAtUtc(now().minusSeconds(1));cfg.setEgressLeaseSeconds(120);
var c=reserve(Operation.WAREHOUSES,1,null,null);
assertThat(budget().pending.expiresAtUtc).isEqualTo(now().plusSeconds(120));
var restarted=new SelfopEgressService(mapper,cfg,clock);
assertThat(restarted.authorize(new AuthorizationRequest(c,Operation.WAREHOUSES,PROVIDER)).expiresAt()).isEqualTo(clock.instant().plusSeconds(30));
restarted.complete(new CompletionRequest(c,Operation.WAREHOUSES,PROVIDER,1,null,200,200,false,null));
assertThat(budget().reservedHttp).isEqualTo(2);assertThat(budget().confirmedHttp).isEqualTo(2);
cfg.setContinuousBusinessCollectionEnabled(false);
assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("EXCLUSIVE_HANDOFF_UNCONFIRMED");
}
@Test void continuousFlagNeverBypassesValidationWindowGrantIdentityOrHandoff(){
cfg.setContinuousBusinessCollectionEnabled(true);cfg.setExclusiveGrantExpiresAtUtc(null);
assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("EXCLUSIVE_HANDOFF_UNCONFIRMED");
testBusiness(5,300);
for(String grant:Arrays.asList(null,"","other")){cfg.setExclusiveGrantId(grant);assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("EXCLUSIVE_HANDOFF_UNCONFIRMED");}
cfg.setExclusiveGrantId("grant");cfg.setPriorEgressClosed(false);
assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("EXCLUSIVE_HANDOFF_UNCONFIRMED");verifyNoInteractions(mapper);
}
@Test void continuousGrantDoesNotRenewFiniteManualRootOrRelaxFence(){
testBusiness(5,10);cfg.setContinuousBusinessCollectionEnabled(true);cfg.setExclusiveGrantExpiresAtUtc(null);
account.setFenceToken(8L);
assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("EGRESS_FENCE_LOST");
account.setFenceToken(7L);var c=reserve(Operation.WAREHOUSES,1,null,null);
assertThat(budget().pending.expiresAtUtc).isEqualTo(businessBudget().deadlineAtUtc);
clock.advance(10);
assertThatThrownBy(()->service.authorize(new AuthorizationRequest(c,Operation.WAREHOUSES,PROVIDER))).hasMessage("BUSINESS_SESSION_DEADLINE");
assertThat(budget().pending.consumed).isFalse();
}
@Test void continuousBusinessAcrossMidnightRetainsUnknownPendingAndReviewAcrossRestart(){
business();cfg.setContinuousBusinessCollectionEnabled(true);cfg.setExclusiveGrantExpiresAtUtc(null);
var c=reserve(Operation.WAREHOUSES,1,null,null);service.authorize(new AuthorizationRequest(c,Operation.WAREHOUSES,PROVIDER));
service.pauseUncertain(c,Operation.WAREHOUSES,"INTEGRATION_TRANSPORT_FAILURE");clock.advance(86400);
var restarted=new SelfopEgressService(mapper,cfg,clock);
assertThatThrownBy(()->restarted.reserve(context(),Operation.WAREHOUSES,1,null,null,INTERNAL)).hasMessage("ACCOUNT_COOLDOWN_WAIT");
assertThat(budget().reservedHttp).isEqualTo(1);assertThat(budget().confirmedHttp).isZero();assertThat(budget().pending.abandoned).isTrue();
assertThat(account.getApiPauseReason()).isEqualTo("EGRESS_REQUIRES_REVIEW");clock.advance(cfg.getCooldownSeconds()+1);
assertThatThrownBy(()->restarted.reserve(context(),Operation.WAREHOUSES,1,null,null,INTERNAL)).hasMessage("ACCOUNT_REQUIRES_REVIEW");
}
@Test void allFamiliesAndFailuresConsumeOneRootAndAccountAcrossServiceRestart(){
var c=reserve(Operation.WAREHOUSES,2,null,null);success(c,Operation.WAREHOUSES);
c=reserve(Operation.PRODUCT_INVENTORY,2,"A",null);success(c,Operation.PRODUCT_INVENTORY);
@@ -150,6 +190,99 @@ class SelfopEgressServiceTest {
private void business(){cfg.setCollectionMode("BUSINESS");cfg.setEnvironment("production");task.setExecutionPurpose("BUSINESS");run.setExecutionPurpose("BUSINESS");}
private void testBusiness(int http,int seconds){business();cfg.setEnvironment("test");task.setBusinessRootTaskId(1L);task.setBusinessBudgetJson(SelfopBusinessBudget.freeze(now(),seconds));}
private SelfopBusinessBudget businessBudget(){return SelfopSupport.read(task.getBusinessBudgetJson(),SelfopBusinessBudget.class);}
private void continuousBusinessRoot(){
business();cfg.setEnvironment("test");cfg.setContinuousBusinessCollectionEnabled(true);
cfg.setExclusiveGrantExpiresAtUtc(now().minusSeconds(1));cfg.setBusinessMinRequestIntervalMs(1200);
task.setValidationRootTaskId(null);task.setValidationBudgetJson(null);task.setUpstreamTaskId(null);
task.setTaskType("BACKFILL");task.setRequestedBy(SelfopCollectionPolicy.CONTINUOUS_PLANNER);task.setScopeHash("a".repeat(64));
var from=LocalDate.of(2026,10,11).atStartOfDay();task.setQueryFromUtc(from.minusHours(8));task.setQueryUntilUtc(from.plusDays(1).minusHours(8));
task.setQueryScopeJson(SelfopSupport.json(Map.of("startTime",from.toString(),"endTime",from.plusDays(1).toString())));
task.setRequestKey("continuous:outbound:2026-10-11:"+task.getScopeHash().substring(0,12));
task.setBusinessRootTaskId(task.getId());task.setBusinessBudgetJson(SelfopBusinessBudget.freezeContinuous(now()));
assertThat(SelfopCollectionPolicy.continuousBusinessRoot(task)).isTrue();
}
@Test void continuousRootWithNoDeadlineReservesAuthorizesAcrossDayWithoutResettingAccumulated8820(){
continuousBusinessRoot();cfg.setEgressLeaseSeconds(999);cfg.setDailyRequestBudget(1);
var inherited=new SelfopApiBudget();inherited.budgetDate=now().plusHours(8).toLocalDate();inherited.importedGrantId="grant";
inherited.reservedHttp=8820;inherited.confirmedHttp=8820;inherited.minIntervalMs=1200;account.setApiBudgetJson(SelfopSupport.json(inherited));
var b=businessBudget();b.http=8820;b.confirmedHttp=8820;task.setBusinessBudgetJson(SelfopSupport.json(b));var started=b.startedAtUtc;
var c=reserve(Operation.OUTBOUND_QUERY,100,null,null);
assertThat(budget().pending.expiresAtUtc).isEqualTo(now().plusSeconds(120));
var restarted=new SelfopEgressService(mapper,cfg,clock);
var permit=restarted.authorize(new AuthorizationRequest(c,Operation.OUTBOUND_QUERY,PROVIDER));
assertThat(permit.expiresAt()).isEqualTo(clock.instant().plusSeconds(30));assertThat(permit.allowedAttempts()).isEqualTo(1);
var completion=new CompletionRequest(c,Operation.OUTBOUND_QUERY,PROVIDER,1,null,200,200,false,null);
restarted.complete(completion);restarted.complete(completion);
clock.advance(86400);account.setLeaseUntilUtc(now().plusMinutes(5));
var next=new SelfopEgressService(mapper,cfg,clock).reserve(context(),Operation.OUTBOUND_DETAIL,0,"A","C/DO",INTERNAL);success(next,Operation.OUTBOUND_DETAIL);
assertThat(budget().reservedHttp).isEqualTo(8822);assertThat(budget().confirmedHttp).isEqualTo(8822);assertThat(budget().dailyLimit).isZero();
assertThat(businessBudget().http).isEqualTo(8822);assertThat(businessBudget().confirmedHttp).isEqualTo(8822);
assertThat(businessBudget().continuous).isTrue();assertThat(businessBudget().deadlineAtUtc).isNull();assertThat(businessBudget().startedAtUtc).isEqualTo(started);
verify(mapper,never()).terminateBusinessTasks(any(),any());
}
@Test void continuousRootSwitchOffRejectsNewReserveAndPreparedAuthorizeButAccountsLateCompletion(){
continuousBusinessRoot();cfg.setExclusiveGrantExpiresAtUtc(now().plusDays(2));
cfg.setContinuousBusinessCollectionEnabled(false);
assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("BUSINESS_SESSION_ROOT_INVALID");
assertThat(budget().reservedHttp).isZero();
cfg.setContinuousBusinessCollectionEnabled(true);var c=reserve(Operation.WAREHOUSES,1,null,null);
cfg.setContinuousBusinessCollectionEnabled(false);
assertThatThrownBy(()->service.authorize(new AuthorizationRequest(c,Operation.WAREHOUSES,PROVIDER))).hasMessage("BUSINESS_SESSION_ROOT_INVALID");
assertThat(budget().pending.consumed).isFalse();
cfg.setContinuousBusinessCollectionEnabled(true);service.authorize(new AuthorizationRequest(c,Operation.WAREHOUSES,PROVIDER));
cfg.setContinuousBusinessCollectionEnabled(false);cfg.setCollectionMode("OFF");clock.advance(86400);
var completion=new CompletionRequest(c,Operation.WAREHOUSES,PROVIDER,1,null,200,200,false,null);
var restarted=new SelfopEgressService(mapper,cfg,clock);restarted.complete(completion);restarted.complete(completion);
assertThat(budget().reservedHttp).isEqualTo(1);assertThat(budget().confirmedHttp).isEqualTo(1);
assertThat(businessBudget().http).isEqualTo(1);assertThat(businessBudget().confirmedHttp).isEqualTo(1);
}
@Test void continuousRootInProductionAlsoRequiresSwitchAndValidPersistedPlannerIdentity(){
continuousBusinessRoot();cfg.setEnvironment("production");cfg.setExclusiveGrantExpiresAtUtc(now().plusDays(2));
cfg.setContinuousBusinessCollectionEnabled(false);
assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("BUSINESS_SESSION_ROOT_INVALID");
cfg.setContinuousBusinessCollectionEnabled(true);task.setRequestedBy("manual-operator");
assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("BUSINESS_SESSION_ROOT_INVALID");
task.setRequestedBy(SelfopCollectionPolicy.CONTINUOUS_PLANNER);success(reserve(Operation.WAREHOUSES,1,null,null),Operation.WAREHOUSES);
assertThat(businessBudget().confirmedHttp).isEqualTo(1);
task.setStatus("DEAD");assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("BUSINESS_SESSION_ROOT_STOPPED");
}
@Test void continuousRootAuthorizeStillChecksFenceStopAndPendingExpiry(){
continuousBusinessRoot();var c=reserve(Operation.WAREHOUSES,1,null,null);
account.setFenceToken(8L);
assertThatThrownBy(()->service.authorize(new AuthorizationRequest(c,Operation.WAREHOUSES,PROVIDER))).hasMessage("EGRESS_FENCE_LOST");
account.setFenceToken(7L);clock.advance(121);
assertThatThrownBy(()->service.authorize(new AuthorizationRequest(c,Operation.WAREHOUSES,PROVIDER))).hasMessage("EGRESS_PERMIT_REPLAY_OR_EXPIRED");
assertThat(budget().pending.consumed).isFalse();assertThat(businessBudget().confirmedHttp).isZero();
var b=businessBudget();b.stopReason="BUSINESS_MANUAL_STOP";task.setBusinessBudgetJson(SelfopSupport.json(b));
assertThatThrownBy(()->service.authorize(new AuthorizationRequest(c,Operation.WAREHOUSES,PROVIDER))).hasMessage("BUSINESS_MANUAL_STOP");
assertThat(task.getStatus()).isEqualTo("DEAD");
}
@Test void continuousRootStillPreservesIntervalSingleFlightAndUnknownOutcomeAfterRestart(){
continuousBusinessRoot();var c=reserve(Operation.WAREHOUSES,1,null,null);
assertThatThrownBy(()->reserve(Operation.OUTBOUND_QUERY,1,null,null)).hasMessage("ACCOUNT_REQUEST_INFLIGHT");
assertThat(budget().reservedHttp).isEqualTo(1);
service.authorize(new AuthorizationRequest(c,Operation.WAREHOUSES,PROVIDER));
service.complete(new CompletionRequest(c,Operation.WAREHOUSES,PROVIDER,1,null,200,200,false,null));
assertThatThrownBy(()->reserve(Operation.OUTBOUND_QUERY,1,null,null)).hasMessage("ACCOUNT_INTERVAL_WAIT");
clock.advance(1);assertThatThrownBy(()->reserve(Operation.OUTBOUND_QUERY,1,null,null)).hasMessage("ACCOUNT_INTERVAL_WAIT");
clock.advance(1);var pending=reserve(Operation.OUTBOUND_QUERY,1,null,null);
service.authorize(new AuthorizationRequest(pending,Operation.OUTBOUND_QUERY,PROVIDER));clock.advance(86400);
account.setLeaseUntilUtc(now().plusMinutes(5));var restarted=new SelfopEgressService(mapper,cfg,clock);
assertThatThrownBy(()->restarted.reserve(context(),Operation.WAREHOUSES,1,null,null,INTERNAL)).hasMessage("ACCOUNT_COOLDOWN_WAIT");
assertThat(budget().reservedHttp).isEqualTo(2);assertThat(budget().confirmedHttp).isEqualTo(1);assertThat(budget().pending.abandoned).isTrue();
assertThat(account.getApiPauseReason()).isEqualTo("REQUEST_OUTCOME_UNKNOWN");
}
@Test void continuousRootActual429AndAuthorizationFailureKeepAccountProtections(){
continuousBusinessRoot();var c=reserve(Operation.WAREHOUSES,1,null,null);service.authorize(new AuthorizationRequest(c,Operation.WAREHOUSES,PROVIDER));
service.complete(new CompletionRequest(c,Operation.WAREHOUSES,PROVIDER,1,"LINGXING_WMS_HTTP_RATE_LIMITED",429,null,true,null));
assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("ACCOUNT_COOLDOWN_WAIT");
clock.advance(cfg.getCooldownSeconds()+1);var next=reserve(Operation.OUTBOUND_DETAIL,0,"A","C/DO");service.authorize(new AuthorizationRequest(next,Operation.OUTBOUND_DETAIL,PROVIDER));
service.complete(new CompletionRequest(next,Operation.OUTBOUND_DETAIL,PROVIDER,1,"LINGXING_WMS_AUTHORIZATION_REJECTED",401,null,true,null));
clock.advance(86400);account.setLeaseUntilUtc(now().plusMinutes(5));
assertThatThrownBy(()->new SelfopEgressService(mapper,cfg,clock).reserve(context(),Operation.WAREHOUSES,1,null,null,INTERNAL)).hasMessage("ACCOUNT_REQUIRES_REVIEW");
assertThat(account.getApiPauseReason()).isEqualTo("AUTHORIZATION_REQUIRES_REVIEW");assertThat(budget().rateLimitFailures).isEqualTo(1);
assertThat(budget().reservedHttp).isEqualTo(2);assertThat(budget().confirmedHttp).isEqualTo(2);
}
@Test void businessSurpassesPriorDailyAndRootHttpCapsAcrossRestartWithoutRenewingDeadline(){
testBusiness(5000,300);cfg.setDailyRequestBudget(12000);cfg.setTransferredDailyHttpCount(12000);
var rootBudget=businessBudget();rootBudget.maxHttp=5000;rootBudget.http=5000;rootBudget.confirmedHttp=5000;task.setBusinessBudgetJson(SelfopSupport.json(rootBudget));
@@ -33,10 +33,40 @@ class SelfopSalesPublisherTest {
run.setFenceToken(3L); run.setRunRole("WAREHOUSE_SYNC"); run.setStatus("READY"); run.setCalculatedFactRevision(7L);run.setWarehouseDailyReady(true);
run.setBuildStartedAtUtc(SelfopSupport.utcNow().minusSeconds(1));run.setFetchStartedAtUtc(SelfopSupport.utcNow().minusSeconds(2));
run.setCoverageStatus("COMPLETE"); run.setCoveredFromUtc(LocalDateTime.of(2026,9,12,8,0)); run.setCoveredUntilUtc(LocalDateTime.of(2026,10,12,8,0));
SelfopSalesCoverage.freeze(run,java.util.List.of(new SelfopSalesCoverage.Interval(run.getCoveredFromUtc(),run.getCoveredUntilUtc())));
task=new SelfopSyncTask(); task.setExecutionPurpose("BUSINESS"); task.setTaskNo("LXCOL-20261012-00000020"); task.setId(20L); task.setApplyState("APPLIED"); task.setStatus("RUNNING"); task.setTaskType("SCHEDULED");
when(locks.lockScope(1L)).thenReturn(scope); when(locks.lockRun(10L)).thenReturn(run); when(locks.lockTask(20L)).thenReturn(task);
}
private void enableSyntheticPublication() { config.setCollectionMode("BUSINESS"); config.setEnvironment("production"); config.setSalesPublicationEnabled(true); config.setQuantityUnit("PCS"); }
@Test void disconnectedTargetPublishesOnlyWithFrozenProofAndKeepsGapUnknown(){
enableSyntheticPublication();var first=LocalDateTime.of(2026,10,6,16,0);var latest=LocalDateTime.of(2026,10,11,16,0);
run.setCoveredFromUtc(first);run.setCoveredUntilUtc(first.plusDays(1));
task.setQueryFromUtc(latest);task.setQueryUntilUtc(latest.plusDays(1));
SelfopSalesCoverage.freeze(run,java.util.List.of(new SelfopSalesCoverage.Interval(first,first.plusDays(1))));
assertEquals("SELFOP_TASK_COVERAGE_UNPROVEN",assertThrows(IllegalStateException.class,()->publisher.publish(1,10,"worker",3)).getMessage());
noWrites();
SelfopSalesCoverage.freeze(run,java.util.List.of(new SelfopSalesCoverage.Interval(first,first.plusDays(1)),new SelfopSalesCoverage.Interval(latest,latest.plusDays(1))));
publisher.publish(1,10,"worker",3);assertEquals("SUCCEEDED",task.getStatus());
assertEquals(first.plusDays(1),scope.getDiscoveryUntilUtc());
assertFalse(SelfopSalesCoverage.contains(SelfopSalesCoverage.evidence(run),first,latest.plusDays(1)));
}
@Test void latestDayCanPublishBeforeOldPlannedAnchorIsFilled(){
enableSyntheticPublication();var latest=LocalDateTime.of(2026,10,11,16,0);
scope.setInitialFromUtc(latest.minusMonths(2));run.setCoveredFromUtc(null);run.setCoveredUntilUtc(null);run.setCoverageStatus("UNKNOWN");
task.setQueryFromUtc(latest);task.setQueryUntilUtc(latest.plusDays(1));
SelfopSalesCoverage.freeze(run,java.util.List.of(new SelfopSalesCoverage.Interval(latest,latest.plusDays(1))));
publisher.publish(1,10,"worker",3);assertEquals("SUCCEEDED",task.getStatus());
assertEquals(latest.minusMonths(2),scope.getInitialFromUtc());assertNull(run.getCoveredUntilUtc());
}
@Test void reuseCannotCompleteLatestDayUsingOnlyOldIsland(){
enableSyntheticPublication();var first=LocalDateTime.of(2026,10,6,16,0);var latest=LocalDateTime.of(2026,10,11,16,0);
var published=new SelfopSyncRun();published.setId(9L);published.setScopeId(1L);published.setScopeHash(scope.getScopeHash());
published.setExecutionPurpose("BUSINESS");published.setStatus("PUBLISHED");published.setWarehouseDailyReady(true);published.setCalculatedFactRevision(7L);
published.setScopeSnapshotJson("{}");SelfopSalesCoverage.freeze(published,java.util.List.of(new SelfopSalesCoverage.Interval(first,first.plusDays(1))));
when(runs.selectById(9L)).thenReturn(published);task.setQueryFromUtc(latest);task.setQueryUntilUtc(latest.plusDays(1));
assertEquals("SELFOP_TASK_COVERAGE_UNPROVEN",assertThrows(IllegalStateException.class,()->publisher.reuse(1,10,9,"worker",3)).getMessage());
assertEquals("RUNNING",task.getStatus());verify(tasks,never()).update(any(),any());
}
private void noWrites() { verifyNoInteractions(scopes,runs,tasks); assertEquals(9L,scope.getPublishedRunId()); }
@Test void salesPublicationIsDisabledByDefaultBeforeAnyDatabaseRead() {
@@ -16,6 +16,95 @@ import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
class SelfopSchedulePlannerTest {
@Test void continuousInventoryWaitsForTodaysSlotAndNeverBackfillsPreviousDays(){
var state=continuousState();
var manual7=dayRoot("RUNNING",7,"manual:7");var manual6=dayRoot("READY",6,"manual:6");
state.add(manual7);state.add(manual6);
planner.planContinuous(LocalDateTime.of(2026,10,12,23,59));
assertThat(state).noneMatch(t->"WAREHOUSE_SYNC".equals(t.getTaskRole()));
planner.planContinuous(LocalDateTime.of(2026,10,13,0,0));
var inventory=state.stream().filter(t->"WAREHOUSE_SYNC".equals(t.getTaskRole())).toList();
assertThat(inventory).hasSize(1);assertThat(inventory.get(0).getPlannedAtUtc()).isEqualTo(LocalDateTime.of(2026,10,13,0,0));
assertThat(inventory.get(0).getNextRetryAtUtc()).isEqualTo(inventory.get(0).getPlannedAtUtc());
assertThat(SelfopCollectionPolicy.continuousBusinessRoot(inventory.get(0))).isTrue();
planner.planContinuous(LocalDateTime.of(2026,10,13,12,0));
planner.planContinuous(LocalDateTime.of(2026,10,13,16,0));
assertThat(state.stream().filter(t->"WAREHOUSE_SYNC".equals(t.getTaskRole()))).hasSize(1);
planner.planContinuous(LocalDateTime.of(2026,10,14,0,0));
assertThat(state.stream().filter(t->"WAREHOUSE_SYNC".equals(t.getTaskRole()))).hasSize(2);
assertThat(manual7.getStatus()).isEqualTo("RUNNING");assertThat(manual6.getStatus()).isEqualTo("READY");
verify(scopes,never()).updateById(any(SelfopSyncScope.class));verify(tasks,never()).updateById(any(SelfopSyncTask.class));
}
@Test void continuousInventoryRegistersOneOverdueSlotPerConfiguredWarehouse(){
var state=continuousState();config.getWarehouses().put("WH-B",new SelfopAnalyticsProperties.Warehouse());
var outboundA=scope(10L,"WH-A","OUTBOUND");outboundA.setBusinessTimezone("Asia/Shanghai");outboundA.setSourceTimezone("Asia/Shanghai");
var outboundB=scope(20L,"WH-B","OUTBOUND");outboundB.setBusinessTimezone("Asia/Shanghai");outboundB.setSourceTimezone("Asia/Shanghai");
doAnswer(call->{
com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<?> query=call.getArgument(0);query.getSqlSegment();
return query.getParamNameValuePairs().containsValue("INVENTORY")?List.of(inventoryScope(11L,"WH-A"),inventoryScope(21L,"WH-B")):List.of(outboundA,outboundB);}).when(scopes).selectList(any());
planner.planContinuous(LocalDateTime.of(2026,10,13,12,0));planner.planContinuous(LocalDateTime.of(2026,10,13,12,1));
var inventory=state.stream().filter(t->"WAREHOUSE_SYNC".equals(t.getTaskRole())).toList();
assertThat(inventory).hasSize(2);assertThat(inventory).extracting(SelfopSyncTask::getWarehouseCode).containsExactlyInAnyOrder("WH-A","WH-B");
assertThat(inventory).allMatch(t->t.getPlannedAtUtc().equals(LocalDateTime.of(2026,10,13,0,0))&&SelfopCollectionPolicy.continuousBusinessRoot(t));
}
@Test void continuousPoolPersistsCompleteDaysNewestFirstAndNeverChangesCoverageAnchor(){
var state=continuousState();
planner.planContinuous(LocalDateTime.of(2026,10,13,4,0));
assertThat(state).hasSize(62);
assertThat(state.get(0).getQueryFromUtc()).isEqualTo(LocalDateTime.of(2026,10,11,16,0));
assertThat(state.get(60).getQueryFromUtc()).isEqualTo(LocalDateTime.of(2026,8,12,16,0));
assertThat(state).allMatch(SelfopCollectionPolicy::continuousBusinessRoot);
verify(scopes,never()).updateById(any(SelfopSyncScope.class));verify(taskService,never()).initialize();
planner.planContinuous(LocalDateTime.of(2026,10,13,4,1));assertThat(state).hasSize(62);
planner.planContinuous(LocalDateTime.of(2026,10,13,16,4));assertThat(state).hasSize(62);
planner.planContinuous(LocalDateTime.of(2026,10,13,16,5));assertThat(state).hasSize(63);
assertThat(state.get(62).getQueryFromUtc()).isEqualTo(LocalDateTime.of(2026,10,12,16,0));
assertThat(state).anyMatch(t->LocalDateTime.of(2026,8,12,16,0).equals(t.getQueryFromUtc()));
}
@Test void deadOldManualRootDoesNotMaskSuccessfulOrActiveDayAndContinuousFailureIsNotRecreated(){
var state=continuousState();
state.add(dayRoot("DEAD",12,"manual:old"));state.add(dayRoot("SUCCEEDED",12,"manual:success"));
state.add(dayRoot("RUNNING",11,"manual:running"));
state.add(dayRoot("DEAD",10,"manual:failed-only"));
planner.planContinuous(LocalDateTime.of(2026,10,13,4,0));
assertThat(state.stream().filter(SelfopCollectionPolicy::continuousBusinessRoot)).hasSize(60);
assertThat(state.stream().filter(SelfopCollectionPolicy::continuousBusinessRoot)).noneMatch(t->LocalDateTime.of(2026,10,11,16,0).equals(t.getQueryFromUtc()));
var root=state.stream().filter(SelfopCollectionPolicy::continuousBusinessRoot).findFirst().orElseThrow();root.setStatus("DEAD");
int count=state.size();planner.planContinuous(LocalDateTime.of(2026,10,13,4,1));assertThat(state).hasSize(count);
}
@Test void continuousTickReReadsOneTaskAfterEverySafeDispatch(){
continuousState();var subject=spy(planner);doNothing().when(subject).planContinuous(any());
var old=dayRoot("RETRY_WAIT",10,"manual:old");old.setId(10L);old.setRequestedBy("operator");
var fresh=dayRoot("READY",12,"manual:new");fresh.setId(12L);fresh.setRequestedBy("operator");
when(taskService.due(1)).thenReturn(List.of(old),List.of(fresh));
subject.tick();subject.tick();
var order=inOrder(taskService,execution);order.verify(taskService).due(1);order.verify(execution).execute(10L);
order.verify(taskService).due(1);order.verify(execution).execute(12L);verify(taskService,never()).due(10);
}
private List<SelfopSyncTask> continuousState(){
directTransactions();config.setEnvironment("test");config.setContinuousBusinessCollectionEnabled(true);
config.setSourceAccount("synthetic");config.setSourceQueryTimezone("Asia/Shanghai");
config.getWarehouses().put("WH-A",new SelfopAnalyticsProperties.Warehouse());
var scope=scope(10L,"WH-A","OUTBOUND");scope.setBusinessTimezone("Asia/Shanghai");scope.setSourceTimezone("Asia/Shanghai");
scope.setScopeHash("a".repeat(64));scope.setInitialFromUtc(LocalDateTime.of(2026,10,7,16,0));
var stock=inventoryScope(11L,"WH-A");
when(scopes.selectList(any())).thenAnswer(call->{
com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<?> query=call.getArgument(0);query.getSqlSegment();
return query.getParamNameValuePairs().containsValue("INVENTORY")?List.of(stock):List.of(scope);});
var account=new SelfopOutboundDiscoveryScope();account.setId(30L);when(taskService.initializeAccount()).thenReturn(account);
var state=new ArrayList<SelfopSyncTask>();when(tasks.selectList(any())).thenAnswer(call->new ArrayList<>(state));
when(taskService.create(any())).thenAnswer(call->{SelfopSyncTask task=call.getArgument(0);state.add(task);return task;});return state;
}
private SelfopSyncTask dayRoot(String status,int day,String key){
var root=controlled(100L+day,"ACCOUNT_DISCOVERY","BACKFILL",key);root.setDiscoveryScopeId(30L);root.setScopeHash("a".repeat(64));root.setStatus(status);
var from=LocalDateTime.of(2026,10,day,0,0);root.setQueryFromUtc(from.minusHours(8));root.setQueryUntilUtc(from.plusDays(1).minusHours(8));
root.setQueryScopeJson(com.baiyue.lastleg.selfoperated.analytics.model.SelfopSupport.json(new SelfopDiscoveryService.Query(from.toString(),from.plusDays(1).toString(),null,null,null)));return root;
}
private SelfopSyncScope inventoryScope(long id,String warehouse){
var scope=scope(id,warehouse,"INVENTORY");scope.setSourceAccount("synthetic");scope.setScopeHash("b".repeat(64));
scope.setBusinessTimezone("Asia/Shanghai");scope.setSourceTimezone("Asia/Shanghai");
scope.setScopeJson(com.baiyue.lastleg.selfoperated.analytics.model.SelfopSupport.json(Map.of("sourceAccount","synthetic","warehouseCode",warehouse,"dataFamily","INVENTORY","businessTimezone","Asia/Shanghai")));return scope;
}
private final SelfopAnalyticsProperties config = new SelfopAnalyticsProperties();
private final SelfopTaskService taskService = mock(SelfopTaskService.class);
private final SelfopExecutionService execution = mock(SelfopExecutionService.class);
@@ -14,6 +14,37 @@ import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
class SelfopTaskServiceTest {
@Test void automaticInventoryRootRequiresFrozenInventoryScopeAndDoesNotAdmitOldScheduledTasks(){
config.setEnvironment("test");config.setContinuousBusinessCollectionEnabled(true);
var scope=scope("INVENTORY");scope.setSourceAccount("synthetic-account");scope.setWarehouseCode("WH-A");scope.setScopeHash("b".repeat(64));
scope.setScopeJson(json(Map.of("sourceAccount","synthetic-account","warehouseCode","WH-A","dataFamily","INVENTORY","businessTimezone","Asia/Shanghai")));
when(scopes.selectById(10L)).thenReturn(scope);
var task=task("READY");task.setTaskType("SCHEDULED");task.setRequestedBy(SelfopCollectionPolicy.CONTINUOUS_PLANNER);
task.setScopeHash(scope.getScopeHash());task.setQueryScopeJson(scope.getScopeJson());task.setPlannedAtUtc(LocalDateTime.of(2026,10,14,0,0));
task.setRequestKey("continuous:inventory:WH-A:2026-10-14T00:00:"+"b".repeat(12));
service.create(task);assertThat(task.getBusinessRootTaskId()).isEqualTo(task.getId());
assertThat(SelfopCollectionPolicy.controlledBusinessTask(task,tasks::selectById,true)).isTrue();
assertThat(SelfopCollectionPolicy.controlledBusinessTask(task,tasks::selectById,false)).isFalse();
task.setRequestKey("slot:old");assertThat(SelfopCollectionPolicy.controlledBusinessTask(task,tasks::selectById,true)).isFalse();
task.setRequestKey("continuous:inventory:WH-A:2026-10-14T00:00:"+"b".repeat(12));
task.setQueryScopeJson(json(Map.of("dataFamily","OUTBOUND")));assertThat(SelfopCollectionPolicy.continuousBusinessRoot(task)).isFalse();
}
@Test void continuousRootCreationFreezesUnboundedSessionAndUsesAccountQueue(){
config.setEnvironment("test");config.setContinuousBusinessCollectionEnabled(true);config.setSourceAccount("synthetic-account");
var account=new SelfopOutboundDiscoveryScope();account.setId(20L);account.setNextSequence(1L);account.setNextObservationSequence(1L);
when(locks.lockDiscovery(20L)).thenReturn(account);
var task=new SelfopSyncTask();task.setSourceAccount("synthetic-account");task.setExecutionPurpose("BUSINESS");
task.setTaskRole("ACCOUNT_DISCOVERY");task.setTaskType("BACKFILL");task.setDiscoveryScopeId(20L);
task.setRequestedBy(SelfopCollectionPolicy.CONTINUOUS_PLANNER);task.setScopeHash("a".repeat(64));
task.setRequestKey("continuous:outbound:2026-10-12:"+"a".repeat(12));task.setRequestHash("digest");
task.setQueryFromUtc(LocalDateTime.of(2026,10,11,16,0));task.setQueryUntilUtc(LocalDateTime.of(2026,10,12,16,0));
task.setQueryScopeJson(json(new SelfopDiscoveryService.Query("2026-10-12T00:00","2026-10-13T00:00",null,null,null)));
service.create(task);assertThat(task.getBusinessRootTaskId()).isEqualTo(task.getId());
var budget=read(task.getBusinessBudgetJson(),com.baiyue.lastleg.selfoperated.analytics.request.SelfopBusinessBudget.class);
assertThat(budget.continuous).isTrue();assertThat(budget.deadlineAtUtc).isNull();assertThat(budget.limit(utcNow().plusYears(1))).isNull();
service.due(1);verify(locks).dueContinuous(any(),eq(1),eq("synthetic-account"));verify(locks,never()).due(any(),anyInt(),anyString());
task.setScopeHash("invalid-hash");assertThat(SelfopCollectionPolicy.continuousBusinessRoot(task)).isFalse();
}
private final SelfopSyncScopeMapper scopes = mock(SelfopSyncScopeMapper.class);
private final SelfopOutboundDiscoveryScopeMapper accounts = mock(SelfopOutboundDiscoveryScopeMapper.class);
private final SelfopSyncTaskMapper tasks = mock(SelfopSyncTaskMapper.class);
@@ -414,6 +445,19 @@ class SelfopTaskServiceTest {
assertThatThrownBy(()->service.waitForBudget(1L,20L,utcNow().plusDays(1),lease)).isInstanceOf(SelfopTaskService.BusinessSessionStoppedException.class);
verify(tasks,never()).updateById(any(SelfopSyncTask.class));verify(runs,never()).updateById(any(SelfopSyncRun.class));
}
@Test void continuousFailureStopsInsteadOfBecomingAnotherScheduledRetry(){
var root=boundedRoot(0,utcNow().plusMinutes(3));config.setContinuousBusinessCollectionEnabled(true);
root.setTaskRole("ACCOUNT_DISCOVERY");root.setTaskType("BACKFILL");root.setScopeId(null);root.setWarehouseCode(null);root.setDiscoveryScopeId(99L);
root.setRequestedBy(SelfopCollectionPolicy.CONTINUOUS_PLANNER);root.setScopeHash("a".repeat(64));
root.setRequestKey("continuous:outbound:2026-10-12:"+"a".repeat(12));root.setStatus("RUNNING");
root.setQueryFromUtc(LocalDateTime.of(2026,10,11,16,0));root.setQueryUntilUtc(LocalDateTime.of(2026,10,12,16,0));
root.setQueryScopeJson(json(new SelfopDiscoveryService.Query("2026-10-12T00:00","2026-10-13T00:00",null,null,null)));
root.setBusinessBudgetJson(com.baiyue.lastleg.selfoperated.analytics.request.SelfopBusinessBudget.freezeContinuous(utcNow()));
var run=run("FETCHING");when(locks.lockRun(20L)).thenReturn(run);
service.fail(1L,20L,"LINGXING_WMS_RATE_LIMITED",true,new SelfopLeaseService.Lease(99L,"owner",7L,true));
assertThat(root.getStatus()).isEqualTo("DEAD");assertThat(root.getNextRetryAtUtc()).isNull();
assertThat(run.getStatus()).isEqualTo("FAILED");assertThat(run.getErrorCode()).isEqualTo("LINGXING_WMS_RATE_LIMITED");
}
@Test void egressStopReasonSurvivesExecutorFailureHandling() {
var root=boundedRoot(7,utcNow().plusMinutes(3));
var budget=read(root.getBusinessBudgetJson(),com.baiyue.lastleg.selfoperated.analytics.request.SelfopBusinessBudget.class);