fix(selfop): discover warehouses with bounded validation and optimize queries
This commit is contained in:
+10
-2
@@ -30,7 +30,7 @@ class AnalyticsQueryServiceTest {
|
||||
when(mapper.page(any())).thenReturn(List.of(aggregate(true,"300","VALID",time(D.minusDays(40)))));
|
||||
when(mapper.inventoryCount(any(),any())).thenReturn(1L);
|
||||
doAnswer(inv->List.of(inventory(true,"300",inv.getArgument(0,AnalyticsSqlRequest.class).stockFresh(),time(D.minusDays(40)),inv.getArgument(0,AnalyticsSqlRequest.class).complete30()?"150":null))).when(mapper).inventoryPage(any(),any());
|
||||
when(mapper.excluded(any())).thenReturn(List.of());
|
||||
when(mapper.rankingStats(any())).thenReturn(List.of(new AnalyticsReadMapper.RankingStat(1L,null,0L)));
|
||||
when(mapper.profile(11,"demo","W","C","SKU")).thenReturn(profile("PCS","VALID_OUTBOUND",time(D.minusDays(40))));
|
||||
when(mapper.profile(22,"demo","W","C","SKU")).thenReturn(profile("PCS","INVENTORY_OBSERVATION",time(D.minusDays(35))));
|
||||
when(mapper.daily(anyLong(),anyString(),anyString(),anyString(),anyString(),any(),any())).thenReturn(List.of());
|
||||
@@ -211,12 +211,20 @@ class AnalyticsQueryServiceTest {
|
||||
assertFalse(cap.getValue().completeWindow()); assertFalse(cap.getValue().complete30());
|
||||
}
|
||||
@Test void topOneHundredClipsFinalSqlPageAndSkipsBeyondIt(){
|
||||
when(mapper.count(any())).thenReturn(100L);
|
||||
when(mapper.rankingStats(any())).thenReturn(List.of(new AnalyticsReadMapper.RankingStat(100L,null,0L)));
|
||||
service.hotSkus(new AnalyticsQuery("demo","W",null,null,null,null,null,null,30,2,75,11L,22L,SH,IH,NOW));
|
||||
var cap=ArgumentCaptor.forClass(AnalyticsSqlRequest.class); verify(mapper).page(cap.capture());
|
||||
assertEquals(75,cap.getValue().offset()); assertEquals(25,cap.getValue().size());
|
||||
assertTrue(service.hotSkus(new AnalyticsQuery("demo","W",null,null,null,null,null,null,30,3,50,11L,22L,SH,IH,NOW)).records().isEmpty());
|
||||
verify(mapper,times(1)).page(any());
|
||||
verify(mapper,never()).count(any());
|
||||
}
|
||||
@Test void rankingTotalAndExclusionsUseOneStatisticsQuery(){
|
||||
when(mapper.rankingStats(any())).thenReturn(List.of(new AnalyticsReadMapper.RankingStat(1L,null,0L),
|
||||
new AnalyticsReadMapper.RankingStat(1L,"ZERO_OUTBOUND",7L)));
|
||||
var result=service.hotSkus(metrics(30));assertEquals(1L,result.total());
|
||||
assertEquals(Map.of("ZERO_OUTBOUND",7L),result.excludedReasons());
|
||||
verify(mapper).rankingStats(any());verify(mapper).page(any());verify(mapper,never()).count(any());
|
||||
}
|
||||
@Test void readRunsWithinConsistentSnapshot(){
|
||||
var t=AnalyticsQueryService.class.getAnnotation(Transactional.class); assertTrue(t.readOnly()); assertEquals(Isolation.REPEATABLE_READ,t.isolation());
|
||||
|
||||
+21
-3
@@ -25,7 +25,7 @@ class AnalyticsReadMapperTest {
|
||||
}
|
||||
@Test void allMapperStatementsAreSelectAndProvidersBindTheNamedRecord(){
|
||||
var c=config(); assertNotNull(AnalyticsReadMapper.class.getAnnotation(Mapper.class));
|
||||
for(String method:List.of("scopes","publishedRun","page","count","excluded","profile","daily","applyBarriers")){
|
||||
for(String method:List.of("scopes","publishedRun","page","count","rankingStats","profile","daily","applyBarriers")){
|
||||
var statement=c.getMappedStatement(NS+method); assertEquals(SqlCommandType.SELECT,statement.getSqlCommandType());
|
||||
Map<String,Object> params=new HashMap<>(); params.put("p",request("HOT")); params.put("id",11L); params.put("run",11L);
|
||||
params.put("account","demo"); params.put("warehouse","W"); params.put("customer","OWNER"); params.put("sku","Exact-SKU");
|
||||
@@ -34,7 +34,10 @@ class AnalyticsReadMapperTest {
|
||||
}
|
||||
var b=bound(c,"page",Map.of("p",request("HOT")));
|
||||
var names=b.getParameterMappings().stream().map(org.apache.ibatis.mapping.ParameterMapping::getProperty).toList();
|
||||
for(String property:List.of("p.account","p.warehouse","p.customer","p.sku","p.salesRun","p.inventoryRun","p.from","p.until","p.size","p.offset"))assertTrue(names.contains(property),property);
|
||||
for(String property:List.of("p.account","p.warehouse","p.customer","p.sku","p.salesRun","p.inventoryRun","p.anchor","p.windowFrom","p.size","p.offset"))assertTrue(names.contains(property),property);
|
||||
var summaryNames=bound(c,"page",Map.of("p",request("SUMMARY"))).getParameterMappings().stream()
|
||||
.map(org.apache.ibatis.mapping.ParameterMapping::getProperty).toList();
|
||||
for(String property:List.of("p.from","p.until"))assertTrue(summaryNames.contains(property),property);
|
||||
assertFalse(b.getSql().contains("#{"));
|
||||
assertEquals(AnalyticsReadRows.Aggregate.class,c.getMappedStatement(NS+"page").getResultMaps().get(0).getType());
|
||||
}
|
||||
@@ -56,6 +59,7 @@ class AnalyticsReadMapperTest {
|
||||
assertTrue(page.contains("d.sales_run_id=?")); assertTrue(page.contains("d.business_date>=CAST(? AS DATE) AND d.business_date<CAST(? AS DATE)"));
|
||||
assertTrue(page.contains("GROUP BY b.customer_code,b.sku_code")); assertFalse(page.contains("business_date>=DATE_SUB"));
|
||||
assertFalse(page.contains("ll_selfop_inventory_snapshot"));
|
||||
assertFalse(page.contains("metric_sales"));assertFalse(page.contains("FROM bounds WHERE 1=0"));
|
||||
assertTrue(page.endsWith("ORDER BY observed_quantity IS NULL,observed_quantity DESC,sku_code ASC,customer_code ASC LIMIT ? OFFSET ?"));
|
||||
assertTrue(page.indexOf("GROUP BY b.customer_code,b.sku_code")<page.lastIndexOf("LIMIT")); immutableOnly(page);
|
||||
}
|
||||
@@ -77,9 +81,23 @@ class AnalyticsReadMapperTest {
|
||||
assertTrue(slow.contains("evidence_at_utc<=CONVERT_TZ"));
|
||||
assertTrue(slow.contains("ORDER BY window_quantity ASC,available_quantity DESC,sku_code ASC,customer_code ASC LIMIT"));
|
||||
for(String kind:List.of("HOT","SLOW")){
|
||||
String count=bound(c,"count",Map.of("p",request(kind))).getSql(); assertTrue(count.contains("SELECT LEAST(COUNT(*),100) FROM result")); assertFalse(count.contains(" LIMIT "));
|
||||
String stats=bound(c,"rankingStats",Map.of("p",request(kind))).getSql();
|
||||
assertTrue(stats.contains("LEAST(SUM(CASE WHEN eligible=1 THEN amount ELSE 0 END) OVER (),100) total"));
|
||||
assertTrue(stats.contains("excluded_count"));assertFalse(stats.contains(" LIMIT "));
|
||||
assertFalse(stats.contains("ranged AS"));assertFalse(stats.contains("TIMESTAMPDIFF"));
|
||||
}
|
||||
}
|
||||
@Test void inventoryHistoryOnlyEnrichesQualifiedPageAndNeverCountsOrOwnerTotals(){
|
||||
var c=config();var p=request("INVENTORY");var o=new AnalyticsInventoryOptions(null,"COVERAGE_GT_THRESHOLD","PHYSICAL_DESC",90);
|
||||
String page=bound(c,"inventoryPage",Map.of("p",p,"o",o)).getSql();
|
||||
assertTrue(page.contains("paged AS (SELECT * FROM result WHERE above_threshold=1 ORDER BY"));
|
||||
assertTrue(page.indexOf("LIMIT ? OFFSET ?")<page.indexOf("last_sales AS"));
|
||||
assertTrue(page.contains("JOIN paged k ON k.customer_code=d.customer_code AND k.sku_code=d.sku_code"));
|
||||
assertTrue(page.contains("d.sales_run_id=? AND d.source_account=? AND d.warehouse_code=?"));
|
||||
assertFalse(bound(c,"inventoryCount",Map.of("p",p,"o",o)).getSql().contains("last_sales"));
|
||||
assertFalse(bound(c,"ownerTotals",Map.of("p",p,"o",o)).getSql().contains("last_sales"));
|
||||
immutableOnly(page);
|
||||
}
|
||||
@Test void observationAndUnitsRequireImmutableProofAndPcsNotFirstSeenOrStockPlaceholders(){
|
||||
String sql=bound(config(),"page",Map.of("p",request("INVENTORY"))).getSql();
|
||||
assertTrue(sql.contains("s.evidence_type='VALID_OUTBOUND' AND s.evidence_run_id IS NOT NULL"));
|
||||
|
||||
+36
@@ -163,6 +163,42 @@ public class AnalyticsSqlSemanticTest {
|
||||
var totals=totals(request("INVENTORY",D.minusDays(30),D,"C",null,utc(D,8),0,20),new AnalyticsInventoryOptions(null,null,null,30));
|
||||
assertDecimal("12",(BigDecimal)totals.get("owner_outbound_known"));assertEquals(0L,((Number)totals.get("owner_outbound_unknown")).longValue());
|
||||
}
|
||||
@Test void rankingStatisticsPreserveFullEligibilityTopCapExclusionsAndSharedSkuOwners() throws Exception {
|
||||
for(int i=0;i<110;i++){
|
||||
String sku=String.format("SKU-%03d",i);profile(11,"C",sku,"VALID_OUTBOUND",utc(D.minusDays(40),0),"PCS");
|
||||
profile(22,"C",sku,"INVENTORY_OBSERVATION",utc(D.minusDays(40),0),"PCS");
|
||||
daily(11,"W","C",sku,D.minusDays(1),"1");stock("C",sku,0,"10");
|
||||
}
|
||||
profile(11,"B","SKU-000","VALID_OUTBOUND",utc(D.minusDays(40),0),"PCS");
|
||||
profile(22,"B","SKU-000","INVENTORY_OBSERVATION",utc(D.minusDays(40),0),"PCS");
|
||||
daily(11,"W","B","SKU-000",D.minusDays(1),"2");stock("B","SKU-000",0,"10");
|
||||
profile(22,"C","ZERO","INVENTORY_OBSERVATION",utc(D.minusDays(40),0),"PCS");stock("C","ZERO",0,"0");
|
||||
profile(22,"C","NEW","INVENTORY_OBSERVATION",utc(D.minusDays(1),0),"PCS");stock("C","NEW",0,"10");
|
||||
profile(22,"C","UNIT","INVENTORY_OBSERVATION",utc(D.minusDays(40),0),null);stock("C","UNIT",0,"10");
|
||||
var hot=request("HOT",D.minusDays(30),D,null,null,utc(D,8),0,20);
|
||||
var hotStats=rankingStats(hot);assertTrue(hotStats.stream().allMatch(s->s.total()==100L));
|
||||
assertEquals(2L,hotStats.stream().filter(s->"ZERO_OUTBOUND".equals(s.reason())).findFirst().orElseThrow().excludedCount());
|
||||
assertEquals(1L,hotStats.stream().filter(s->"UNIT_UNKNOWN_OR_CONFLICT".equals(s.reason())).findFirst().orElseThrow().excludedCount());
|
||||
assertEquals("B",page(hot).get(0).customerCode());
|
||||
var slowStats=rankingStats(request("SLOW",D.minusDays(30),D,null,null,utc(D,8),0,20));
|
||||
assertTrue(slowStats.stream().allMatch(s->s.total()==100L));
|
||||
for(String reason:List.of("NO_AVAILABLE_INVENTORY","OBSERVATION_INSUFFICIENT","SALES_WINDOW_UNAVAILABLE"))
|
||||
assertEquals(1L,slowStats.stream().filter(s->reason.equals(s.reason())).findFirst().orElseThrow().excludedCount());
|
||||
}
|
||||
@Test void inventoryPageKeepsHistoricLastDateForOnlySelectedQualifiedKeys() throws Exception {
|
||||
for(String sku:List.of("OLD-1","OLD-2","RECENT")){
|
||||
profile(22,"C",sku,"INVENTORY_OBSERVATION",utc(D.minusDays(90),0),"PCS");stock("C",sku,0,"10");
|
||||
}
|
||||
daily(11,"W","C","OLD-1",D.minusDays(35),"4");daily(11,"W","C","OLD-2",D.minusDays(60),"8");
|
||||
daily(11,"W","C","RECENT",D.minusDays(1),"2");
|
||||
var first=inventory(null,30,"PHYSICAL_DESC",0,1);assertEquals("OLD-1",first.get(0).skuCode());
|
||||
assertEquals(D.minusDays(35),first.get(0).lastOutbound());
|
||||
var second=inventory(null,30,"PHYSICAL_DESC",1,1);assertEquals("OLD-2",second.get(0).skuCode());
|
||||
assertEquals(D.minusDays(60),second.get(0).lastOutbound());
|
||||
}
|
||||
@SuppressWarnings("unchecked") private List<AnalyticsReadMapper.RankingStat> rankingStats(AnalyticsSqlRequest p){
|
||||
return (List<AnalyticsReadMapper.RankingStat>)(List<?>)literalQuery("rankingStats",p,new AnalyticsInventoryOptions(null,null,null,null));
|
||||
}
|
||||
private static void assertDecimal(String expected,BigDecimal actual){assertNotNull(actual);assertEquals(0,new BigDecimal(expected).compareTo(actual));}
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<AnalyticsReadRows.Inventory> inventory(String attention,Integer threshold,String sort,long offset,int size){
|
||||
|
||||
+36
-4
@@ -19,6 +19,7 @@ class SelfopEgressServiceTest {
|
||||
cfg.setSourceAccount("synthetic");cfg.setCollectionMode("VALIDATION");cfg.setExclusiveGrantId("grant");cfg.setExclusiveGrantExpiresAtUtc(now().plusDays(2));cfg.setExclusiveHandoffVerified(true);cfg.setPriorEgressClosed(true);cfg.setInflightDrained(true);cfg.setTransferredBudgetVerified(true);cfg.setTransferredBudgetDate(now().plusHours(8).toLocalDate());cfg.setDailyRequestBudget(100);cfg.getWarehouses().put("A",new SelfopAnalyticsProperties.Warehouse());cfg.getWarehouses().put("B",new SelfopAnalyticsProperties.Warehouse());
|
||||
account.setId(10L);account.setSourceAccount("synthetic");account.setActiveTaskId(1L);account.setFenceToken(7L);account.setLeaseUntilUtc(now().plusDays(2));
|
||||
task.setId(1L);task.setDiscoveryScopeId(10L);task.setSourceAccount("synthetic");task.setStatus("RUNNING");task.setTaskRole("ACCOUNT_DISCOVERY");task.setExecutionPurpose("VALIDATION");task.setValidationRootTaskId(1L);task.setValidationBudgetJson(SelfopValidationBudget.freeze(now()));
|
||||
task.setTaskType("VALIDATION");task.setQueryScopeJson("{\"action\":\"VALIDATION\",\"warehouses\":[\"A\"]}");
|
||||
run.setId(2L);run.setTaskId(1L);run.setSourceAccount("synthetic");run.setStatus("FETCHING");run.setExecutionPurpose("VALIDATION");run.setFenceToken(7L);
|
||||
when(mapper.lockAccount("synthetic")).thenReturn(account);when(mapper.lockTask(1L)).thenReturn(task);when(mapper.lockRun(2L)).thenReturn(run);when(mapper.saveAccount(any())).thenReturn(1);when(mapper.saveRoot(any())).thenReturn(1);
|
||||
}
|
||||
@@ -59,9 +60,12 @@ class SelfopEgressServiceTest {
|
||||
assertThatThrownBy(()->new SelfopEgressService(mapper,cfg,clock).reserve(context(),Operation.OUTBOUND_QUERY,1,null,null,INTERNAL)).hasMessage("VALIDATION_PAGE_LIMIT").satisfies(e->assertThat(((EgressRejectedException)e).sampleDone()).isTrue());
|
||||
assertThat(task.getStatus()).isEqualTo("SAMPLE_DONE");assertThat(rootBudget().http).isEqualTo(10);verify(mapper).terminateValidationRuns(eq(1L),eq("SAMPLE_DONE"),eq("VALIDATION_PAGE_LIMIT"),any());
|
||||
}
|
||||
@Test void inventoryTwoPageLimitPerWarehouseSharesGlobalBudget(){
|
||||
success(reserve(Operation.PRODUCT_INVENTORY,1,"A",null),Operation.PRODUCT_INVENTORY);success(reserve(Operation.PRODUCT_INVENTORY,1,"B",null),Operation.PRODUCT_INVENTORY);success(reserve(Operation.PRODUCT_INVENTORY,1,"A",null),Operation.PRODUCT_INVENTORY);
|
||||
assertThatThrownBy(()->reserve(Operation.PRODUCT_INVENTORY,1,"A",null)).hasMessage("VALIDATION_INVENTORY_LIMIT");assertThat(rootBudget().inventoryPages).containsEntry("A",2).containsEntry("B",1);assertThat(rootBudget().http).isEqualTo(3);
|
||||
@Test void inventoryTwoPageLimitCannotSampleAnotherConfiguredWarehouse(){
|
||||
success(reserve(Operation.PRODUCT_INVENTORY,1,"A",null),Operation.PRODUCT_INVENTORY);
|
||||
assertThatThrownBy(()->reserve(Operation.PRODUCT_INVENTORY,1,"B",null)).hasMessage("EGRESS_WAREHOUSE_INVALID");
|
||||
assertThatThrownBy(()->reserve(Operation.OUTBOUND_DETAIL,0,"B","C/DO")).hasMessage("EGRESS_WAREHOUSE_INVALID");
|
||||
success(reserve(Operation.PRODUCT_INVENTORY,1,"A",null),Operation.PRODUCT_INVENTORY);
|
||||
assertThatThrownBy(()->reserve(Operation.PRODUCT_INVENTORY,1,"A",null)).hasMessage("VALIDATION_INVENTORY_LIMIT");assertThat(rootBudget().inventoryPages).containsEntry("A",2).doesNotContainKey("B");assertThat(rootBudget().http).isEqualTo(2);
|
||||
}
|
||||
@Test void repeatedDetailUsesHttpLimitEvenForSameIdentity(){for(int i=0;i<20;i++)success(reserve(Operation.OUTBOUND_DETAIL,0,"A","C/DO"),Operation.OUTBOUND_DETAIL);assertThatThrownBy(()->reserve(Operation.OUTBOUND_DETAIL,0,"A","C/DO")).hasMessage("VALIDATION_DETAIL_HTTP_LIMIT");assertThat(rootBudget().detailKeys).hasSize(1);assertThat(rootBudget().detailHttp).isEqualTo(20);}
|
||||
@Test void fiveMinuteDeadlineTerminalAndFrozenLimitsCannotIncrease(){clock.advance(300);assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("VALIDATION_DEADLINE");assertThat(task.getStatus()).isEqualTo("SAMPLE_DONE");var b=rootBudget();b.maxHttp=41;assertThatThrownBy(b::validate).hasMessage("VALIDATION_BUDGET_INVALID");}
|
||||
@@ -71,7 +75,35 @@ class SelfopEgressServiceTest {
|
||||
@Test void actualStartIntervalUsesAuthorizationTimeAndReturnsQueueDeadline(){var c=reserve(Operation.WAREHOUSES,1,null,null);clock.advance(4);service.authorize(new AuthorizationRequest(c,Operation.WAREHOUSES,PROVIDER));service.complete(new CompletionRequest(c,Operation.WAREHOUSES,PROVIDER,1,null,200,200,false,null));clock.advance(1);assertThatThrownBy(()->reserve(Operation.OUTBOUND_QUERY,1,null,null)).hasMessage("ACCOUNT_INTERVAL_WAIT").satisfies(e->assertThat(((EgressRejectedException)e).waitUntilUtc()).isEqualTo(now().plusSeconds(4)));}
|
||||
@Test void obsoleteExecutionFenceCannotReserveOrConsume(){run.setFenceToken(6L);assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("EGRESS_FENCE_LOST");run.setFenceToken(7L);var c=reserve(Operation.WAREHOUSES,1,null,null);account.setFenceToken(8L);assertThatThrownBy(()->service.authorize(new AuthorizationRequest(c,Operation.WAREHOUSES,PROVIDER))).hasMessage("EGRESS_FENCE_LOST");assertThat(budget().reservedHttp).isEqualTo(1);}
|
||||
@Test void totalHttpCapCountsEveryOperationAndPreparedReservation(){var b=rootBudget();b.http=40;task.setValidationBudgetJson(SelfopSupport.json(b));assertThatThrownBy(()->reserve(Operation.OUTBOUND_DETAIL,0,"A","C/DO")).hasMessage("VALIDATION_HTTP_LIMIT");assertThat(task.getStatus()).isEqualTo("SAMPLE_DONE");}
|
||||
@Test void warehouseRequestsShareAccountBudgetAndRejectScopeMismatch(){task.setTaskRole("WAREHOUSE_SYNC");task.setScopeId(100L);task.setWarehouseCode("A");run.setScopeId(100L);run.setWarehouseCode("A");when(mapper.warehouseFence(eq(100L),eq(7L),eq(1L),any())).thenReturn(1);var c=reserve(Operation.PRODUCT_INVENTORY,1,"A",null);success(c,Operation.PRODUCT_INVENTORY);assertThat(budget().reservedHttp).isEqualTo(1);run.setWarehouseCode("B");assertThatThrownBy(()->reserve(Operation.PRODUCT_INVENTORY,1,"A",null)).hasMessage("EGRESS_FENCE_LOST");}
|
||||
@Test void warehouseChildRequestsUseOriginalAccountRootBudgetAndRejectScopeMismatch(){
|
||||
var child=new SelfopSyncTask();child.setId(3L);child.setValidationRootTaskId(1L);child.setTaskRole("WAREHOUSE_SYNC");child.setExecutionPurpose("VALIDATION");child.setSourceAccount("synthetic");child.setStatus("RUNNING");child.setScopeId(100L);child.setWarehouseCode("A");
|
||||
var attempt=new SelfopSyncRun();attempt.setId(4L);attempt.setTaskId(3L);attempt.setSourceAccount("synthetic");attempt.setExecutionPurpose("VALIDATION");attempt.setStatus("FETCHING");attempt.setFenceToken(7L);attempt.setScopeId(100L);attempt.setWarehouseCode("A");
|
||||
when(mapper.lockTask(3L)).thenReturn(child);when(mapper.lockRun(4L)).thenReturn(attempt);when(mapper.warehouseFence(eq(100L),eq(7L),eq(3L),any())).thenReturn(1);
|
||||
var childContext=new SelfopRequestContext("synthetic","VALIDATION",1L,"selfop-1","grant",3L,4L);
|
||||
var c=service.reserve(childContext,Operation.PRODUCT_INVENTORY,1,"A",null,INTERNAL);success(c,Operation.PRODUCT_INVENTORY);assertThat(budget().reservedHttp).isEqualTo(1);
|
||||
attempt.setWarehouseCode("B");assertThatThrownBy(()->service.reserve(childContext,Operation.PRODUCT_INVENTORY,1,"A",null,INTERNAL)).hasMessage("EGRESS_FENCE_LOST");
|
||||
}
|
||||
@Test void directoryRootOnlyAllowsWarehouseOperationAndOneHttpWithinSixtySeconds(){
|
||||
task.setQueryScopeJson("{\"action\":\"WAREHOUSE_DISCOVERY\"}");task.setValidationBudgetJson(SelfopValidationBudget.freezeWarehouseDiscovery(now()));
|
||||
assertThatThrownBy(()->reserve(Operation.OUTBOUND_QUERY,10,null,null)).hasMessage("VALIDATION_DIRECTORY_OPERATION_ONLY");
|
||||
assertThatThrownBy(()->reserve(Operation.PRODUCT_INVENTORY,10,"A",null)).hasMessage("VALIDATION_DIRECTORY_OPERATION_ONLY");
|
||||
assertThat(budget().reservedHttp).isZero();
|
||||
assertThat(rootBudget().deadlineAtUtc).isEqualTo(rootBudget().startedAtUtc.plusSeconds(60));
|
||||
success(reserve(Operation.WAREHOUSES,10,null,null),Operation.WAREHOUSES);
|
||||
assertThatThrownBy(()->reserve(Operation.WAREHOUSES,10,null,null)).hasMessage("VALIDATION_HTTP_LIMIT");
|
||||
assertThat(budget().reservedHttp).isEqualTo(1);assertThat(budget().confirmedHttp).isEqualTo(1);assertThat(rootBudget().pages).isEqualTo(1);
|
||||
}
|
||||
@Test void separateWarehouseRootsKeepTheirOwnFiveMinuteBudgetsAndSameAccountConsumption(){
|
||||
success(reserve(Operation.PRODUCT_INVENTORY,10,"A",null),Operation.PRODUCT_INVENTORY);service.finishSample(1L,"VALIDATION_SAMPLE_SUFFICIENT");
|
||||
var bRoot=new SelfopSyncTask();bRoot.setId(3L);bRoot.setValidationRootTaskId(3L);bRoot.setTaskRole("ACCOUNT_DISCOVERY");bRoot.setTaskType("VALIDATION");bRoot.setExecutionPurpose("VALIDATION");bRoot.setSourceAccount("synthetic");bRoot.setDiscoveryScopeId(10L);bRoot.setStatus("RUNNING");bRoot.setQueryScopeJson("{\"action\":\"VALIDATION\",\"warehouses\":[\"B\"]}");bRoot.setValidationBudgetJson(SelfopValidationBudget.freeze(now()));
|
||||
var bRun=new SelfopSyncRun();bRun.setId(4L);bRun.setTaskId(3L);bRun.setSourceAccount("synthetic");bRun.setExecutionPurpose("VALIDATION");bRun.setStatus("FETCHING");bRun.setFenceToken(7L);
|
||||
when(mapper.lockTask(3L)).thenReturn(bRoot);when(mapper.lockRun(4L)).thenReturn(bRun);
|
||||
var c=service.reserve(new SelfopRequestContext("synthetic","VALIDATION",3L,"selfop-3","grant",3L,4L),Operation.PRODUCT_INVENTORY,10,"B",null,INTERNAL);success(c,Operation.PRODUCT_INVENTORY);
|
||||
assertThat(budget().reservedHttp).isEqualTo(2);assertThat(budget().confirmedHttp).isEqualTo(2);
|
||||
var second=SelfopSupport.read(bRoot.getValidationBudgetJson(),SelfopValidationBudget.class);
|
||||
assertThat(rootBudget().http).isEqualTo(1);assertThat(second.http).isEqualTo(1);assertThat(second.maxHttp).isEqualTo(40);
|
||||
assertThat(second.deadlineAtUtc).isEqualTo(second.startedAtUtc.plusSeconds(300));
|
||||
}
|
||||
@Test void unknownGrantTransferCooldownIsPreservedAndExpiredGrantCannotIssue(){cfg.setTransferredCooldownUntilUtc(now().plusSeconds(100));assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("ACCOUNT_COOLDOWN_WAIT");cfg.setExclusiveGrantExpiresAtUtc(now().minusSeconds(1));assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("EXCLUSIVE_HANDOFF_UNCONFIRMED");}
|
||||
@Test void persistedDailyCapCannotBeRaisedMiddayToBypassBudget(){cfg.setDailyRequestBudget(1);success(reserve(Operation.WAREHOUSES,1,null,null),Operation.WAREHOUSES);cfg.setDailyRequestBudget(100);assertThatThrownBy(()->reserve(Operation.OUTBOUND_QUERY,1,null,null)).hasMessage("ACCOUNT_DAILY_BUDGET");assertThat(budget().dailyLimit).isEqualTo(1);}
|
||||
@Test void oldAttemptCannotConsumeAfterNextPreparedRequest(){var c=reserve(Operation.WAREHOUSES,1,null,null);success(c,Operation.WAREHOUSES);reserve(Operation.OUTBOUND_QUERY,1,null,null);assertThatThrownBy(()->service.authorize(new AuthorizationRequest(c,Operation.WAREHOUSES,PROVIDER))).hasMessage("EGRESS_PERMIT_BINDING_INVALID");}
|
||||
|
||||
+34
@@ -5,6 +5,8 @@ import com.baiyue.lastleg.selfoperated.analytics.model.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import static com.baiyue.lastleg.selfoperated.analytics.model.SelfopSupport.read;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
@@ -90,6 +92,38 @@ class SelfopCommandServiceTest {
|
||||
assertThatThrownBy(()->service.request(c,"42")).hasMessage("当前采集模式与所选操作不一致,请使用对应入口");
|
||||
verify(taskService,never()).create(any(SelfopSyncTask.class));
|
||||
}
|
||||
@Test void warehouseDiscoveryCreatesAccountRootWithoutAnyWarehouseOrTimeWindow() {
|
||||
config.setCollectionMode("VALIDATION");config.setSourceAccount("synthetic-account");
|
||||
var account=new SelfopOutboundDiscoveryScope();account.setId(20L);
|
||||
when(taskService.initializeAccount()).thenReturn(account);
|
||||
when(taskService.create(any(SelfopSyncTask.class))).thenAnswer(call->call.getArgument(0));
|
||||
var task=service.request(new SelfopCommandService.Command("directory",null,"WAREHOUSE_DISCOVERY",null,null,null,null,null,null),"42");
|
||||
assertThat(task.getTaskRole()).isEqualTo("ACCOUNT_DISCOVERY");assertThat(task.getTaskType()).isEqualTo("VALIDATION");
|
||||
assertThat(task.getExecutionPurpose()).isEqualTo("VALIDATION");assertThat(task.getDiscoveryScopeId()).isEqualTo(20L);
|
||||
assertThat(task.getWarehouseCode()).isNull();assertThat(task.getScopeId()).isNull();assertThat(task.getRequestedBy()).isEqualTo("42");
|
||||
assertThat(read(task.getQueryScopeJson(),Map.class)).containsEntry("action","WAREHOUSE_DISCOVERY").doesNotContainKeys("warehouses","startTime","endTime");
|
||||
assertThat(task.getScopeHash()).matches("[a-f0-9]{64}");
|
||||
verify(taskService,never()).initialize();verifyNoInteractions(scopes,accounts,tasks);
|
||||
}
|
||||
@Test void directoryDiscoveryRejectsBusinessModeAndAnySuppliedWarehouseBeforeInitializing() {
|
||||
configure();
|
||||
var c=new SelfopCommandService.Command("directory",null,"WAREHOUSE_DISCOVERY",null,null,null,null,null,null);
|
||||
assertThatThrownBy(()->service.request(c,"42")).hasMessage("当前采集模式与所选操作不一致,请使用对应入口");
|
||||
config.setCollectionMode("VALIDATION");
|
||||
assertThatThrownBy(()->service.request(new SelfopCommandService.Command("directory","WH-A","WAREHOUSE_DISCOVERY",null,null,null,null,null,null),"42"))
|
||||
.hasMessage("仓库目录发现仅接受请求标识和操作类型,不接受仓库或采集范围");
|
||||
verifyNoInteractions(taskService,scopes,accounts,tasks);
|
||||
}
|
||||
@Test void ordinaryValidationFreezesOnlySelectedWarehouseDespiteOtherConfiguredWarehouse() {
|
||||
configure();config.setCollectionMode("VALIDATION");config.getWarehouses().put("WH-B",new SelfopAnalyticsProperties.Warehouse());
|
||||
var scope=new SelfopSyncScope();scope.setId(10L);scope.setScopeHash("scope");
|
||||
var account=new SelfopOutboundDiscoveryScope();account.setId(20L);
|
||||
when(scopes.selectOne(any())).thenReturn(scope);when(accounts.selectOne(any())).thenReturn(account);
|
||||
when(taskService.create(any(SelfopSyncTask.class))).thenAnswer(call->call.getArgument(0));
|
||||
var from=LocalDateTime.of(2026,10,12,8,0);
|
||||
var task=service.request(new SelfopCommandService.Command("sample-a","WH-A","VALIDATION",null,from,from.plusMinutes(5),null,null,null),"42");
|
||||
assertThat(read(task.getQueryScopeJson(),Map.class)).containsEntry("action","VALIDATION").containsEntry("warehouses",List.of("WH-A"));
|
||||
}
|
||||
private SelfopCommandService.Command retry() {
|
||||
return new SelfopCommandService.Command("retry-key", "WH-A", "RETRY", "LXCOL-20261012-00000001", null, null, null, null, "offline retry");
|
||||
}
|
||||
|
||||
+8
@@ -32,6 +32,14 @@ class SelfopSchedulePlannerTest {
|
||||
planner.tick();
|
||||
verifyNoInteractions(taskService, execution, scopes, accounts, tasks, orders, locks, tx);
|
||||
}
|
||||
@Test void validationWithoutConfiguredWarehouseOnlyInitializesAccountAndExecutesDurableCommand() {
|
||||
config.setCollectionMode("VALIDATION");
|
||||
var due=new SelfopSyncTask();due.setId(101L);due.setTaskType("VALIDATION");due.setExecutionPurpose("VALIDATION");
|
||||
when(taskService.due(10)).thenReturn(List.of(due));
|
||||
planner.tick();
|
||||
verify(taskService).initializeAccount();verify(taskService,never()).initialize();verify(taskService,never()).create(any(SelfopSyncTask.class));
|
||||
verify(execution).execute(101L);verifyNoInteractions(scopes,accounts,tasks,orders,locks,tx);
|
||||
}
|
||||
|
||||
@Test void unknownSourceTimezoneStillPlansBothInventoriesAndExecutesDurableDueWork() {
|
||||
config.setCollectionMode("BUSINESS"); config.setEnvironment("production");
|
||||
|
||||
+45
@@ -26,6 +26,51 @@ class SelfopTaskServiceTest {
|
||||
|
||||
@BeforeEach void configureBusiness() {config.setCollectionMode("BUSINESS");config.setEnvironment("production");}
|
||||
|
||||
@Test void accountInitializationNeedsNoWarehouseAndNeverTouchesWarehouseScopes() {
|
||||
config.setCollectionMode("VALIDATION");config.setSourceAccount("synthetic-account");
|
||||
var account=service.initializeAccount();
|
||||
assertThat(account.getSourceAccount()).isEqualTo("synthetic-account");assertThat(account.getNextSequence()).isEqualTo(1L);
|
||||
assertThat(account.getApiBudgetJson()).isNull();assertThat(account.getApiCooldownUntilUtc()).isNull();
|
||||
verify(accounts).insert(account);verifyNoInteractions(scopes,tasks,runs,locks,leases);
|
||||
}
|
||||
@Test void existingAccountInitializationPreservesConsumedBudgetPauseAndCooldown() {
|
||||
config.setCollectionMode("VALIDATION");config.setSourceAccount("synthetic-account");
|
||||
var account=new SelfopOutboundDiscoveryScope();account.setId(20L);account.setSourceAccount("synthetic-account");
|
||||
account.setApiBudgetJson("{\"reservedHttp\":7}");account.setApiPauseReason("EGRESS_REQUIRES_REVIEW");
|
||||
account.setApiCooldownUntilUtc(utcNow().plusMinutes(10));
|
||||
when(accounts.selectOne(any())).thenReturn(account);
|
||||
assertThat(service.initializeAccount()).isSameAs(account);
|
||||
assertThat(account.getApiBudgetJson()).isEqualTo("{\"reservedHttp\":7}");assertThat(account.getApiPauseReason()).isEqualTo("EGRESS_REQUIRES_REVIEW");
|
||||
verify(accounts,never()).insert(any(SelfopOutboundDiscoveryScope.class));verify(accounts,never()).updateById(any(SelfopOutboundDiscoveryScope.class));
|
||||
verifyNoInteractions(scopes,tasks,runs,locks,leases);
|
||||
}
|
||||
@Test void newDirectoryRootFreezesOnePageOneHttpSixtySecondsWithoutFakeWarehouse() {
|
||||
config.setCollectionMode("VALIDATION");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("VALIDATION");
|
||||
task.setTaskType("VALIDATION");task.setTaskRole("ACCOUNT_DISCOVERY");task.setDiscoveryScopeId(20L);
|
||||
task.setRequestKey("directory");task.setRequestHash("directory-hash");task.setScopeHash("scope-hash");
|
||||
task.setQueryScopeJson("{\"action\":\"WAREHOUSE_DISCOVERY\",\"sourceAccount\":\"synthetic-account\"}");
|
||||
service.create(task);
|
||||
assertThat(task.getValidationRootTaskId()).isEqualTo(task.getId());assertThat(task.getWarehouseCode()).isNull();assertThat(task.getScopeId()).isNull();
|
||||
var budget=read(task.getValidationBudgetJson(),com.baiyue.lastleg.selfoperated.analytics.request.SelfopValidationBudget.class);
|
||||
assertThat(budget.maxPages).isEqualTo(1);assertThat(budget.maxHttp).isEqualTo(1);assertThat(budget.maxPageSize).isEqualTo(10);
|
||||
assertThat(budget.deadlineAtUtc).isEqualTo(budget.startedAtUtc.plusSeconds(60));assertThat(task.getApplyState()).isEqualTo("NOT_APPLICABLE");
|
||||
verify(tasks).insert(task);verifyNoInteractions(scopes,runs,leases);
|
||||
}
|
||||
@Test void singleWarehouseRootKeepsFullFiveMinuteFortyHttpLimit() {
|
||||
config.setCollectionMode("VALIDATION");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("VALIDATION");task.setTaskType("VALIDATION");
|
||||
task.setTaskRole("ACCOUNT_DISCOVERY");task.setDiscoveryScopeId(20L);task.setRequestKey("sample");task.setRequestHash("sample-hash");
|
||||
task.setQueryScopeJson("{\"action\":\"VALIDATION\",\"warehouses\":[\"WH-A\"]}");
|
||||
service.create(task);
|
||||
var budget=read(task.getValidationBudgetJson(),com.baiyue.lastleg.selfoperated.analytics.request.SelfopValidationBudget.class);
|
||||
assertThat(budget.maxHttp).isEqualTo(40);assertThat(budget.deadlineAtUtc).isEqualTo(budget.startedAtUtc.plusSeconds(300));
|
||||
}
|
||||
|
||||
@Test void beginFixesTopLevelTimezoneStalenessAndInitialWindowInVersionSnapshot() {
|
||||
var task = task("READY");
|
||||
var scope = scope("OUTBOUND");
|
||||
|
||||
+50
-5
@@ -37,8 +37,8 @@ class SelfopValidationRunnerTest {
|
||||
config.setCollectionMode("VALIDATION");config.setSourceAccount("demo");config.setExclusiveGrantId("test-grant");
|
||||
config.getWarehouses().put("A",new SelfopAnalyticsProperties.Warehouse());config.getWarehouses().put("B",new SelfopAnalyticsProperties.Warehouse());
|
||||
task=new SelfopSyncTask();task.setId(1L);task.setValidationRootTaskId(1L);task.setTaskRole("ACCOUNT_DISCOVERY");task.setExecutionPurpose("VALIDATION");
|
||||
task.setSourceAccount("demo");task.setDiscoveryScopeId(3L);task.setAttemptCount(1);task.setStatus("RUNNING");
|
||||
task.setQueryScopeJson("{\"startTime\":\"2026-10-12T08:00:00\",\"endTime\":\"2026-10-12T08:10:00\"}");
|
||||
task.setSourceAccount("demo");task.setDiscoveryScopeId(3L);task.setAttemptCount(1);task.setStatus("RUNNING");task.setTaskType("VALIDATION");
|
||||
task.setQueryScopeJson("{\"action\":\"VALIDATION\",\"warehouses\":[\"A\"],\"startTime\":\"2026-10-12T08:00:00\",\"endTime\":\"2026-10-12T08:10:00\"}");
|
||||
task.setValidationBudgetJson(SelfopValidationBudget.freeze(utcNow()));
|
||||
run=new SelfopSyncRun();run.setId(2L);run.setTaskId(1L);run.setSourceAccount("demo");run.setExecutionPurpose("VALIDATION");run.setAttemptNo(1);run.setFenceToken(8L);run.setStatus("FETCHING");
|
||||
when(locks.lockTask(1)).thenReturn(task);when(locks.lockRun(2)).thenReturn(run);when(tasks.selectById(1L)).thenReturn(task);
|
||||
@@ -52,7 +52,7 @@ class SelfopValidationRunnerTest {
|
||||
private SelfopPayloads.Stock stock(String warehouse,String sku,BigDecimal qty){return new SelfopPayloads.Stock("demo","customer",warehouse,sku,0,"Name",qty,qty,BigDecimal.ZERO,BigDecimal.ZERO,null);}
|
||||
private LingxingAnalyticsClient.Summary summary(String number){return new LingxingAnalyticsClient.Summary("demo","customer","A",number,"source",100,null,"2026-10-12 08:02:00");}
|
||||
|
||||
@Test void truncatedTwoWarehouseSampleOnlyStagesAndDoesNotClaimCompleteCoverage() {
|
||||
@Test void selectedWarehouseSampleOnlyStagesItsOwnRowsAndDoesNotClaimCompleteCoverage() {
|
||||
when(client.inventory(any(SelfopRequestContext.class),eq("A"),eq(1),eq(10))).thenReturn(page(1,30,3,List.of(stock("A","one",BigDecimal.ZERO))));
|
||||
when(client.inventory(any(SelfopRequestContext.class),eq("A"),eq(2),eq(10))).thenReturn(page(2,30,3,List.of(stock("A","two",null))));
|
||||
when(client.inventory(any(SelfopRequestContext.class),eq("B"),eq(1),eq(10))).thenReturn(page(1,30,3,List.of(stock("B","one",BigDecimal.ONE))));
|
||||
@@ -60,8 +60,10 @@ class SelfopValidationRunnerTest {
|
||||
runner.run(task,run,lease);
|
||||
assertEquals("SAMPLE_DONE",run.getStatus());assertEquals("UNKNOWN",run.getCoverageStatus());assertNull(run.getPublishedAtUtc());assertNull(run.getCoveredUntilUtc());
|
||||
verify(client,never()).inventory(any(SelfopRequestContext.class),anyString(),eq(3),anyInt());
|
||||
var capture=ArgumentCaptor.forClass(SelfopSyncStage.class);verify(stages,times(4)).insert(capture.capture());
|
||||
var capture=ArgumentCaptor.forClass(SelfopSyncStage.class);verify(stages,times(2)).insert(capture.capture());
|
||||
assertTrue(capture.getAllValues().stream().allMatch(s->s.getObjectType().equals("PRODUCT_STOCK")&&s.getObservationSequence()==null));
|
||||
assertTrue(capture.getAllValues().stream().allMatch(s->"A".equals(s.getWarehouseCode())));
|
||||
verify(client,never()).inventory(any(SelfopRequestContext.class),eq("B"),anyInt(),anyInt());
|
||||
assertTrue(capture.getAllValues().stream().anyMatch(s->"PENDING".equals(s.getValidationStatus())));
|
||||
verify(egress).finishSample(1L,"VALIDATION_SAMPLE_SUFFICIENT");
|
||||
verify(tasks,never()).updateById(any(SelfopSyncTask.class)); // quota root JSON cannot be overwritten by an earlier task snapshot
|
||||
@@ -107,7 +109,7 @@ class SelfopValidationRunnerTest {
|
||||
verify(client,never()).warehouses(any(SelfopRequestContext.class),anyInt(),anyInt());
|
||||
verify(client,never()).detail(any(SelfopRequestContext.class),anyString(),anyString(),anyString());
|
||||
verify(client,never()).inventory(any(SelfopRequestContext.class),eq("A"),anyInt(),anyInt());
|
||||
verify(client).inventory(any(SelfopRequestContext.class),eq("B"),eq(1),eq(10));
|
||||
verify(client,never()).inventory(any(SelfopRequestContext.class),eq("B"),anyInt(),anyInt());
|
||||
assertEquals("SAMPLE_DONE",run.getStatus());
|
||||
}
|
||||
@Test void wrongPurposeCannotCallAnyClient() {
|
||||
@@ -153,4 +155,47 @@ class SelfopValidationRunnerTest {
|
||||
runner.run(task,run,lease);assertEquals("FAILED",run.getStatus());
|
||||
verify(egress).failValidation(1L,"VALIDATION_STRUCTURE_REJECTED");verify(stages,never()).insert(any(SelfopSyncStage.class));
|
||||
}
|
||||
private void directoryOnly() {
|
||||
config.getWarehouses().clear();task.setQueryScopeJson("{\"action\":\"WAREHOUSE_DISCOVERY\",\"sourceAccount\":\"demo\"}");
|
||||
task.setValidationBudgetJson(SelfopValidationBudget.freezeWarehouseDiscovery(utcNow()));
|
||||
}
|
||||
@Test void warehouseDiscoveryReadsOnePageWithoutTimeWindowOrConfiguredWarehouseAndImmediatelyStops() {
|
||||
directoryOnly();
|
||||
var a=new LingxingAnalyticsClient.Warehouse("REAL-A","A",0,0,"America/New_York");
|
||||
var b=new LingxingAnalyticsClient.Warehouse("REAL-B","B",0,0,"America/Los_Angeles");
|
||||
when(client.warehouses(any(SelfopRequestContext.class),eq(1),eq(10))).thenReturn(page(1,2,1,List.of(a,b)));
|
||||
runner.run(task,run,lease);
|
||||
assertEquals("SAMPLE_DONE",run.getStatus());assertEquals("UNKNOWN",run.getCoverageStatus());assertNull(run.getPublishedAtUtc());
|
||||
assertTrue(run.getObservedDimensionsJson().contains("REAL-A"));assertTrue(run.getObservedDimensionsJson().contains("REAL-B"));
|
||||
verify(client,times(1)).warehouses(any(SelfopRequestContext.class),eq(1),eq(10));
|
||||
verify(client,never()).discover(any(SelfopRequestContext.class),any(),any(),anyInt(),anyInt());
|
||||
verify(client,never()).detail(any(SelfopRequestContext.class),anyString(),anyString(),anyString());
|
||||
verify(client,never()).inventory(any(SelfopRequestContext.class),anyString(),anyInt(),anyInt());verifyNoInteractions(stages);
|
||||
verify(egress).finishSample(1L,"VALIDATION_WAREHOUSE_DIRECTORY_COMPLETE");
|
||||
}
|
||||
@Test void truncatedDirectoryIsExplicitAndCannotRequestSecondPage() {
|
||||
directoryOnly();when(client.warehouses(any(SelfopRequestContext.class),eq(1),eq(10)))
|
||||
.thenReturn(page(1,11,2,List.of(new LingxingAnalyticsClient.Warehouse("A",null,0,0,null))));
|
||||
runner.run(task,run,lease);verify(egress).finishSample(1L,"VALIDATION_WAREHOUSE_DIRECTORY_TRUNCATED");
|
||||
verify(client,never()).warehouses(any(SelfopRequestContext.class),eq(2),anyInt());
|
||||
}
|
||||
@Test void directoryRateLimitTerminatesRootWithoutAnyBusinessRequest() {
|
||||
directoryOnly();when(client.warehouses(any(SelfopRequestContext.class),eq(1),eq(10)))
|
||||
.thenThrow(new AnalyticsClientException("LINGXING_WMS_RATE_LIMITED",true));
|
||||
runner.run(task,run,lease);assertEquals("FAILED",run.getStatus());verify(egress).failValidation(1L,"LINGXING_WMS_RATE_LIMITED");
|
||||
verify(client,never()).discover(any(SelfopRequestContext.class),any(),any(),anyInt(),anyInt());verifyNoInteractions(stages);
|
||||
}
|
||||
@Test void mixedAccountDirectoryOnlyFetchesDetailsOfFrozenWarehouse() {
|
||||
var other=new LingxingAnalyticsClient.Summary("demo","customer","B","other","source",100,null,"2026-10-12 08:02:00");
|
||||
when(client.discover(any(SelfopRequestContext.class),any(),any(),eq(1),eq(10))).thenReturn(page(1,2,1,List.of(other,summary("selected"))));
|
||||
when(client.detail(any(SelfopRequestContext.class),eq("A"),eq("customer"),eq("selected")))
|
||||
.thenReturn(new SelfopPayloads.Order("demo","customer","A","selected","source",100,null,"2026-10-12 08:02:00",List.of()));
|
||||
runner.run(task,run,lease);assertEquals("SAMPLE_DONE",run.getStatus());
|
||||
verify(client,never()).detail(any(SelfopRequestContext.class),eq("B"),anyString(),anyString());
|
||||
verify(client,never()).inventory(any(SelfopRequestContext.class),eq("B"),anyInt(),anyInt());
|
||||
}
|
||||
@Test void missingOrMultipleFrozenWarehousesFailBeforeAnyProviderRequest() {
|
||||
task.setQueryScopeJson("{\"action\":\"VALIDATION\",\"warehouses\":[\"A\",\"B\"]}");
|
||||
runner.run(task,run,lease);assertEquals("FAILED",run.getStatus());verifyNoInteractions(client,stages);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user