fix(selfop): allow controlled manual collection in test

This commit is contained in:
2026-10-12 23:21:57 +08:00
parent 6e9f4b7932
commit 86e05a77e6
15 changed files with 160 additions and 20 deletions
@@ -25,6 +25,18 @@ class CollectionQueryServiceTest {
CollectionQuery q(Integer page,LocalDateTime upper,Long id) {
return new CollectionQuery("primary","A",null,null,null,null,null,null,page,null,upper==null?null:upper.toInstant(ZoneOffset.UTC),id);
}
@Test void controlledTestBusinessProjectsEnabledInventoryPublicationWithoutAutomaticSchedule() {
config.setCollectionMode("BUSINESS");config.setEnvironment("test");config.setQuantityUnit("PCS");
config.setExclusiveHandoffVerified(true);config.setPriorEgressClosed(true);config.setInflightDrained(true);config.setTransferredBudgetVerified(true);
config.setExclusiveGrantExpiresAtUtc(now.plusHours(1));config.setDailyRequestBudget(100);
var rows=service.overview(q(null,null,null)).warehouses();
assertFalse(rows.isEmpty());for(var row:rows){assertTrue(row.executionEnabled());assertNull(row.disabledReason());assertNull(row.nextScheduledAt());
assertEquals("INVENTORY".equals(row.dataFamily()),row.publicationEnabled());}
config.setExclusiveGrantExpiresAtUtc(now.minusSeconds(1));
for(var row:service.overview(q(null,null,null)).warehouses()){assertFalse(row.executionEnabled());assertEquals("EXCLUSIVE_GRANT_NOT_VALID",row.disabledReason());}
config.setEnvironment("development");
for(var row:service.overview(q(null,null,null)).warehouses()){assertFalse(row.executionEnabled());assertEquals("BUSINESS_ENVIRONMENT_REJECTED",row.disabledReason());}
}
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);
@@ -29,11 +29,19 @@ class SelfopEgressServiceTest {
private void success(EgressContext c,Operation op){service.authorize(new AuthorizationRequest(c,op,PROVIDER));service.complete(new CompletionRequest(c,op,PROVIDER,1,null,200,200,false,null));clock.advance(5);}
private SelfopApiBudget budget(){return SelfopSupport.read(account.getApiBudgetJson(),SelfopApiBudget.class);}
private SelfopValidationBudget rootBudget(){return SelfopSupport.read(task.getValidationBudgetJson(),SelfopValidationBudget.class);}
@Test void offUnknownModeNonProductionBusinessAndUnverifiedHandoffFailBeforeAnyLock(){
@Test void offUnknownModeUnknownEnvironmentBusinessAndUnverifiedHandoffFailBeforeAnyLock(){
cfg.setCollectionMode("OFF");assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("EGRESS_MODE_REJECTED");
cfg.setCollectionMode("BUSINESS");assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("BUSINESS_ENVIRONMENT_REJECTED");
cfg.setCollectionMode("BUSINESS");cfg.setEnvironment("unknown");assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("BUSINESS_ENVIRONMENT_REJECTED");
cfg.setCollectionMode("VALIDATION");cfg.setPriorEgressClosed(false);assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("EXCLUSIVE_HANDOFF_UNCONFIRMED");verifyNoInteractions(mapper);
}
@Test void testBusinessStillChargesPersistentAccountAndRequiresLiveExclusiveGrant(){
business();cfg.setEnvironment("test");
var c=reserve(Operation.WAREHOUSES,1,null,null);success(c,Operation.WAREHOUSES);
assertThat(budget().reservedHttp).isEqualTo(1);assertThat(budget().confirmedHttp).isEqualTo(1);
cfg.setExclusiveGrantExpiresAtUtc(now());
assertThatThrownBy(()->reserve(Operation.WAREHOUSES,1,null,null)).hasMessage("EXCLUSIVE_HANDOFF_UNCONFIRMED");
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 allFamiliesAndFailuresConsumeOneRootAndAccountAcrossServiceRestart(){
var c=reserve(Operation.WAREHOUSES,2,null,null);success(c,Operation.WAREHOUSES);
@@ -75,17 +75,43 @@ class SelfopCommandServiceTest {
task.setWarehouseCode(warehouse); task.setTaskRole("WAREHOUSE_SYNC"); task.setStatus("DEAD");
return task;
}
@Test void offModeAndNonProductionBusinessDoNotAcceptNewCollectionTask() {
@Test void offModeAndUnknownEnvironmentDoNotAcceptNewCollectionTask() {
configure();config.setCollectionMode("OFF");
assertThat(config.isExecutionEnabled()).isFalse();
var c=new SelfopCommandService.Command("disabled","WH-A","INVENTORY",null,null,null,null,null,"offline test");
assertThatThrownBy(()->service.request(c,"42")).hasMessage("采集功能未启用,当前环境不允许执行采集或重试");
assertThatThrownBy(()->service.request(retry(),"42")).hasMessage("采集功能未启用,当前环境不允许执行采集或重试");
config.setCollectionMode("BUSINESS");config.setEnvironment("test");
config.setCollectionMode("BUSINESS");config.setEnvironment("development");
assertThat(config.isExecutionEnabled()).isFalse();
assertThatThrownBy(()->service.request(c,"42")).hasMessage("采集功能未启用,当前环境不允许执行采集或重试");
verifyNoInteractions(taskService,scopes,accounts,tasks);
}
@Test void testBusinessManualInventoryUsesWholeScopeAndBackfillKeepsOneDayBound() {
configure();config.setEnvironment("test");config.setSourceQueryTimezone("Asia/Shanghai");
var scope=new SelfopSyncScope();scope.setId(10L);scope.setScopeHash("scope");scope.setScopeJson("{\"qualities\":\"ALL\"}");
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 inventory=service.request(new SelfopCommandService.Command("full-inventory","WH-A","INVENTORY",null,null,null,null,null,null),"42");
assertThat(config.isExecutionEnabled()).isTrue();assertThat(config.getEnvironment()).isEqualTo("test");
assertThat(inventory.getExecutionPurpose()).isEqualTo("BUSINESS");assertThat(inventory.getValidationRootTaskId()).isNull();
assertThat(inventory.getTaskRole()).isEqualTo("WAREHOUSE_SYNC");assertThat(inventory.getScopeId()).isEqualTo(10L);
assertThat(inventory.getQueryScopeJson()).isEqualTo(scope.getScopeJson());assertThat(inventory.getRequestKey()).isEqualTo("manual:full-inventory");
var from=LocalDateTime.of(2026,10,12,0,0);
var outbound=service.request(new SelfopCommandService.Command("one-day","WH-A","BACKFILL",null,from,from.plusDays(1),null,null,null),"42");
assertThat(outbound.getExecutionPurpose()).isEqualTo("BUSINESS");assertThat(outbound.getTaskRole()).isEqualTo("ACCOUNT_DISCOVERY");
assertThat(outbound.getQueryUntilUtc()).isEqualTo(outbound.getQueryFromUtc().plusDays(1));
clearInvocations(taskService);
assertThatThrownBy(()->service.request(new SelfopCommandService.Command("too-long","WH-A","BACKFILL",null,from,from.plusDays(1).plusSeconds(1),null,null,null),"42"))
.hasMessage("单次采集时间范围不能超过一天");
verify(taskService,never()).create(any(SelfopSyncTask.class));
}
@Test void testCannotUseRetryToReviveAutomaticScheduledWork() {
configure();config.setEnvironment("test");var original=original("synthetic-account","WH-A");
original.setTaskType("SCHEDULED");original.setRequestKey("slot:old");when(tasks.selectOne(any())).thenReturn(original);
assertThatThrownBy(()->service.request(retry(),"42")).hasMessage("测试环境仅允许重试人工采集任务及其派生任务");
verify(taskService,never()).retry(anyLong(),anyString(),anyString(),anyString());
}
@Test void validationModeRejectsFormalInventoryEntry() {
configure();config.setCollectionMode("VALIDATION");
var c=new SelfopCommandService.Command("wrong-purpose","WH-A","INVENTORY",null,null,null,null,null,"offline test");
@@ -32,6 +32,30 @@ class SelfopSchedulePlannerTest {
planner.tick();
verifyNoInteractions(taskService, execution, scopes, accounts, tasks, orders, locks, tx);
}
@Test void testBusinessPollsManualRootAndRelatedChildrenWithoutAutomaticPlanningOrInitialization() {
config.setCollectionMode("BUSINESS");config.setEnvironment("test");
var root=controlled(101L,"ACCOUNT_DISCOVERY","BACKFILL","manual:one-day");root.setRequestedBy("42");
var child=controlled(102L,"WAREHOUSE_SYNC","BACKFILL","route:101:WH-A");child.setWarehouseCode("WH-A");child.setUpstreamTaskId(101L);
var refresh=controlled(103L,"ACCOUNT_DISCOVERY","RECHECK","refresh:101:object:4");refresh.setQueryScopeJson("{\"parentTaskId\":101}");
var scheduled=controlled(104L,"WAREHOUSE_SYNC","SCHEDULED","slot:old");
var orphan=controlled(105L,"WAREHOUSE_SYNC","BACKFILL","route:999:WH-A");orphan.setWarehouseCode("WH-A");orphan.setUpstreamTaskId(999L);
var other=controlled(106L,"WAREHOUSE_SYNC","BACKFILL","route:101:WH-A");other.setSourceAccount("other");other.setWarehouseCode("WH-A");other.setUpstreamTaskId(101L);
when(tasks.selectById(101L)).thenReturn(root);when(taskService.due(10)).thenReturn(List.of(root,child,refresh,scheduled,orphan,other));
planner.tick();verify(execution).execute(101L);verify(execution).execute(102L);verify(execution).execute(103L);
verify(execution,never()).execute(104L);verify(execution,never()).execute(105L);verify(execution,never()).execute(106L);
verify(taskService,never()).initialize();verify(taskService,never()).initializeAccount();verify(taskService,never()).create(any(SelfopSyncTask.class));
verifyNoInteractions(scopes,accounts,orders,locks,tx);
}
@Test void testRejectsDirectPlanAndReviewBeforeAnyStateOrTwoMonthWindowWrite() {
config.setCollectionMode("BUSINESS");config.setEnvironment("test");var preview=scope(10L,"WH-A","OUTBOUND");
assertThatThrownBy(()->planner.plan(preview,LocalDateTime.of(2026,10,12,4,0))).hasMessage("SELFOP_AUTOMATIC_PLANNING_ENVIRONMENT_REJECTED");
assertThatThrownBy(()->planner.review(preview,LocalDateTime.of(2026,10,12,4,0))).hasMessage("SELFOP_AUTOMATIC_PLANNING_ENVIRONMENT_REJECTED");
assertThat(preview.getInitialUntilUtc()).isNull();verifyNoInteractions(taskService,execution,scopes,accounts,tasks,orders,locks,tx);
}
private SelfopSyncTask controlled(long id,String role,String type,String key) {
var task=new SelfopSyncTask();task.setId(id);task.setSourceAccount("synthetic");task.setExecutionPurpose("BUSINESS");
task.setTaskRole(role);task.setTaskType(type);task.setRequestKey(key);return task;
}
@Test void validationWithoutConfiguredWarehouseOnlyInitializesAccountAndExecutesDurableCommand() {
config.setCollectionMode("VALIDATION");
var due=new SelfopSyncTask();due.setId(101L);due.setTaskType("VALIDATION");due.setExecutionPurpose("VALIDATION");