fix(selfop): expose scoped validation tasks without published scopes

This commit is contained in:
2026-10-12 22:57:18 +08:00
parent b71380de31
commit cbc316b34d
4 changed files with 160 additions and 25 deletions
@@ -187,7 +187,7 @@ class CollectionQueryServiceTest {
parameters.put("ids",List.of(1L,7L));parameters.put("warehouses",List.of("A"));
parameters.put("from",LocalDate.of(2026,9,12));parameters.put("until",LocalDate.of(2026,10,12));
for(String method:List.of("taskBound","runBound","taskCount","tasks","runCount","runs","tasksByIds","latestAttempts",
"publishedRuns","currentTasks","lastSuccessfulTasks","sharedActiveAccount","barrierWarehouses",
"publishedRuns","currentTasks","latestValidationTasks","lastSuccessfulTasks","sharedActiveAccount","barrierWarehouses",
"inventoryDailySamples","inventoryAccumulationStarts")) {
String sql=configuration.getMappedStatement(CollectionReadMapper.class.getName()+"."+method).getBoundSql(parameters).getSql();
assertFalse(sql.contains("routing_manifest_json"));assertFalse(sql.contains("ll_selfop_sku_daily_sales"));assertFalse(sql.contains("ll_selfop_inventory_snapshot"));
@@ -230,4 +230,83 @@ class CollectionQueryServiceTest {
now.minusMonths(2),now,null,null,now.minusHours(1),"A",1L,"OLD-HASH")));
assertEquals("PARTIAL",service.overview(q(null,null,null)).warehouses().get(0).initialCoverageStatus());
}
private String sampleProgress() {
return "{\"phase\":\"SAMPLE_DONE\",\"unit\":\"NONE\",\"completed\":0,\"stages\":["
+"{\"phase\":\"DETAIL\",\"unit\":\"ORDER\",\"completed\":20,\"totalKnown\":true,\"total\":20},"
+"{\"phase\":\"INVENTORY\",\"unit\":\"INVENTORY_ROW\",\"completed\":20,\"totalKnown\":false}]}";
}
private Task validationRoot(String warehouse,Long rootId) {
return new Task(7L,"LXCOL-SAMPLE","primary",warehouse,"OUTBOUND","ACCOUNT_DISCOVERY","VALIDATION","VALIDATION",
"sample-hash","SAMPLE_DONE","NOT_APPLICABLE",1,null,null,null,null,rootId,null,sampleProgress(),now,
"{\"http\":23,\"detailHttp\":20,\"deadlineAtUtc\":\"2026-10-12T04:05:00\"}",now,now);
}
private Attempt sampleAttempt() {
return new Attempt(90L,7L,"LXCOL-SAMPLE",1,"SAMPLE_DONE","SAMPLE_DONE","VALIDATION",now.minusMinutes(2),now,null,
now,null,now,sampleProgress(),"UNKNOWN",null,null,null,null,now.minusMinutes(2),"A",null,"sample-hash");
}
@Test void completedAccountValidationRootIsReadableWithoutFormalScopeAndPreservesActualStageCounts() {
when(mapper.scopes("primary","A")).thenReturn(List.of());
when(mapper.task("primary","A","LXCOL-SAMPLE")).thenReturn(validationRoot("A",7L));
when(mapper.latestAttempts(eq("primary"),anyList())).thenReturn(List.of(sampleAttempt()));
var result=service.taskDetail("LXCOL-SAMPLE",q(null,null,null));
assertEquals("A",result.task().warehouseCode());assertEquals("SAMPLE_DONE",result.task().taskStatus());
assertEquals("NOT_PUBLISHED",result.task().resultPublicationState());assertTrue(result.task().allowedActions().isEmpty());
assertEquals("VALIDATION_NOT_RETRYABLE",result.task().actionBlockReason());assertNull(result.sharedUpstream());
assertNull(result.task().progress().get("completed")); // terminal NONE is not a sample count
var stages=(List<?>)result.task().progress().get("stages");
assertEquals(20L,((Map<?,?>)stages.get(0)).get("completed"));assertEquals("ORDER",((Map<?,?>)stages.get(0)).get("unit"));
assertEquals(20L,((Map<?,?>)stages.get(1)).get("completed"));assertEquals("INVENTORY_ROW",((Map<?,?>)stages.get(1)).get("unit"));
assertNull(result.task().latestAttempt().publishedAt());assertEquals("UNKNOWN",result.task().latestAttempt().coverageStatus());
}
@Test void sameSingleWarehouseValidationAppearsInInventoryFamilyWithoutInventingFormalScope() {
when(mapper.scopes("primary","A")).thenReturn(List.of());
when(mapper.task("primary","A","LXCOL-SAMPLE")).thenReturn(validationRoot("A",7L));
var query=new CollectionQuery("primary","A","INVENTORY",null,null,null,null,null,null,null,null,null);
assertEquals("INVENTORY",service.taskDetail("LXCOL-SAMPLE",query).task().dataFamily());
verify(mapper,never()).publishedRuns(anyString(),anyList());
}
@Test void validationListAndAttemptHistoryRemainVisibleWithoutScope() {
when(mapper.scopes("primary","A")).thenReturn(List.of());var upper=new Bound(now,100L);
when(mapper.taskBound(any())).thenReturn(upper);when(mapper.tasks(any(),eq(upper))).thenReturn(List.of(validationRoot("A",7L)));
when(mapper.taskCount(any(),eq(upper))).thenReturn(1L);
assertEquals(1,service.tasks(q(null,null,null)).records().size());
when(mapper.runBound(any())).thenReturn(upper);when(mapper.runs(any(),eq(upper))).thenReturn(List.of(sampleAttempt()));
when(mapper.tasksByIds(eq("primary"),anyList())).thenReturn(List.of(validationRoot("A",7L)));when(mapper.runCount(any(),eq(upper))).thenReturn(1L);
var attempts=service.runs(q(null,null,null));assertEquals(1,attempts.records().size());assertFalse(attempts.records().get(0).retryable());
assertEquals("SAMPLE_DONE",attempts.records().get(0).status());assertNull(attempts.records().get(0).publishedAt());
}
@Test void latestSingleWarehouseSampleIsVisibleInOverviewButNeverBecomesPublishedCoverage() {
config.getWarehouses().put("B",new SelfopAnalyticsProperties.Warehouse());
when(mapper.scopes("primary",null)).thenReturn(List.of());
when(mapper.latestValidationTasks("primary",List.of("A","B"))).thenReturn(List.of(validationRoot("A",7L)));
when(mapper.latestAttempts(eq("primary"),anyList())).thenReturn(List.of(sampleAttempt()));
var overview=service.overview(new CollectionQuery("primary",null,null,null,null,null,null,null,null,null,null,null));
for(var row:overview.warehouses()) {
if("A".equals(row.warehouseCode())){assertEquals("LXCOL-SAMPLE",row.currentTask().taskNo());assertEquals(row.dataFamily(),row.currentTask().dataFamily());}
else assertNull(row.currentTask());
assertNull(row.lastSuccessfulTaskNo());assertNull(row.lastPublishedAt());assertEquals("UNKNOWN",row.initialCoverageStatus());
assertEquals("NOT_COLLECTED",row.dataFreshness());assertFalse(row.publicationEnabled());
}
}
@Test void accountRootWithoutFrozenWarehouseProjectionOrWithWrongRootRelationCannotLeakIntoWarehouseDetail() {
when(mapper.task("primary","A","LXCOL-SAMPLE")).thenReturn(validationRoot(null,7L));
assertThrows(BizException.class,()->service.taskDetail("LXCOL-SAMPLE",q(null,null,null)));
when(mapper.task("primary","A","LXCOL-SAMPLE")).thenReturn(validationRoot("B",7L));
assertThrows(BizException.class,()->service.taskDetail("LXCOL-SAMPLE",q(null,null,null)));
when(mapper.task("primary","A","LXCOL-SAMPLE")).thenReturn(validationRoot("A",8L));
assertThrows(BizException.class,()->service.taskDetail("LXCOL-SAMPLE",q(null,null,null)));
}
@Test void validationMapperReadsFrozenSingleWarehouseAndKeepsAccountTaskRunRelations() {
var configuration=new org.apache.ibatis.session.Configuration();configuration.addMapper(CollectionReadMapper.class);
var f=new CollectionReadMapper.Filter("primary","A","INVENTORY",null,null,null,null,null,0,20);
var p=new HashMap<String,Object>();p.put("f",f);p.put("b",new Bound(now,100L));p.put("account","primary");p.put("warehouse","A");p.put("taskNo","LXCOL-SAMPLE");
for(String method:List.of("task","taskBound","taskCount","tasks","runBound","runCount","runs")) {
String sql=configuration.getMappedStatement(CollectionReadMapper.class.getName()+"."+method).getBoundSql(p).getSql();
assertTrue(sql.contains("JSON_LENGTH(JSON_EXTRACT(t.query_scope_json,'$.warehouses'))=1"));
assertTrue(sql.contains("t.validation_root_task_id=t.id"));assertTrue(sql.contains("t.source_account="));
assertTrue(sql.contains("'$.action'))='VALIDATION'"));assertFalse(sql.contains("WAREHOUSE_DISCOVERY"));
assertFalse(sql.contains("ll_selfop_sync_stage"));
if(method.startsWith("run")||method.equals("runs")){assertTrue(sql.contains("t.source_account=r.source_account"));assertTrue(sql.contains("r.execution_purpose=t.execution_purpose"));}
}
}
}