fix(firstleg): expose container documents and aggregate departure gates

This commit is contained in:
2026-08-26 13:07:30 +08:00
parent 1584c7a0a3
commit fc0a6ea7ca
22 changed files with 407 additions and 39 deletions
@@ -33,6 +33,7 @@ class ContainerControllerContractTest {
assertPermission("logisticsDocument", "platform:firstleg:container:list");
assertPermission("uploadLogisticsDocument", "platform:firstleg:container:edit");
assertPermission("downloadDocument", "platform:firstleg:container:list");
assertPermission("downloadClearance", "platform:firstleg:customs:manage");
}
@Test void overviewOpenApiFreezesContainerIdBasedPdaContract() throws Exception {
@@ -40,6 +41,8 @@ class ContainerControllerContractTest {
.getContentAsString(StandardCharsets.UTF_8);
assertThat(openapi).contains("/api/platform/v1/containers/{id}/overview:",
"/documents/{documentType}/{fileId}/download:", "ExceptionRequest",
"/clearance/document/{fileId}/download:",
"DESTINATION_CLEARANCE、CUSTOMS_DECLARATION、DELIVERY_ORDER、PICKUP_ORDER",
"enum: [DELIVERY_ORDER, PICKUP_ORDER]", "Idempotency-Key", "photoCounts:",
"/pda/photos/{fileId}/download:", "downloadPath:", "workflowStatus:",
"AWAITING_REVIEW", "version: {type: integer, minimum: 0}",
@@ -77,6 +80,8 @@ class ContainerControllerContractTest {
.doesNotContain("tenantId")
.doesNotContain("accountId");
assertThat(openapi)
.contains("/api/platform/v1/containers/{id}/clearance/document/{fileId}/download:")
.contains("Content-Disposition 与 Cache-Control no-store")
.contains("required: [oceanBookingId, requirementId, containerNo, expectedVersion]")
.contains("required: [id, bookingNo, cabinetNo, sourceType, recordStatus")
.contains("sourceType: {type: string, enum: [SIMULATION, OCEAN_BOOKING]")
@@ -16,6 +16,8 @@ import org.springframework.core.io.ClassPathResource;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.http.ResponseEntity;
import org.springframework.web.multipart.MultipartFile;
import java.lang.reflect.Method;
@@ -41,6 +43,8 @@ class LoadingListControllerContractTest {
ConfirmedQuery.class);
authority("confirmedDetail", "platform:firstleg:loading-list:list", AuthenticatedUser.class, long.class, long.class);
authority("declaration", "platform:firstleg:loading-list:list", AuthenticatedUser.class, long.class, long.class);
authority("downloadDeclaration", "platform:firstleg:loading-list:list", AuthenticatedUser.class,
long.class, long.class, long.class);
authority("attachDeclaration", "platform:firstleg:customs:manage", AuthenticatedUser.class,
long.class, long.class, String.class, DocumentAttachRequest.class);
authority("uploadDeclaration", "platform:firstleg:customs:manage", AuthenticatedUser.class,
@@ -88,6 +92,8 @@ class LoadingListControllerContractTest {
.contains("oceanBookingStatus")
.contains("pdaTaskId")
.contains("cabinetNo")
.contains("LOADING_CUSTOMS_DECLARATION")
.contains("Cache-Control 为 no-store")
.doesNotContain("internalContainerNo")
.doesNotContain("rollback")
.doesNotContain("delete:")
@@ -118,6 +124,18 @@ class LoadingListControllerContractTest {
assertThat(method.getParameters()[6].getAnnotation(RequestPart.class)).isNull();
}
@Test
void customsDownloadUsesStablePlanPathAndBinaryResponse() throws Exception {
Method method = PlatformLoadingListController.class.getDeclaredMethod("downloadDeclaration",
AuthenticatedUser.class, long.class, long.class, long.class);
assertThat(method.getAnnotation(GetMapping.class).value()).containsExactly(
"/loading-lists/{planId}/customs/document/{fileId}/download");
assertThat(method.getReturnType()).isEqualTo(ResponseEntity.class);
assertThat(method.getParameters()[1].getName()).isEqualTo("planId");
assertThat(method.getParameters()[2].getName()).isEqualTo("fileId");
assertThat(method.getParameters()[3].getName()).isEqualTo("warehouseId");
}
@Test
void yamlParses() throws Exception {
String yaml = new ClassPathResource("openapi/loading-lists-v1.yaml")
@@ -134,6 +152,7 @@ class LoadingListControllerContractTest {
"/api/platform/v1/first-leg/loading-lists/{planId}",
"/api/platform/v1/first-leg/loading-lists/{planId}/customs",
"/api/platform/v1/first-leg/loading-lists/{planId}/customs/document",
"/api/platform/v1/first-leg/loading-lists/{planId}/customs/document/{fileId}/download",
"/api/platform/v1/first-leg/loading-lists/{planId}/customs/document/upload",
"/api/platform/v1/first-leg/loading-lists/{planId}/customs/complete");
Map<String, Object> components = stringMap(document.get("components"));
@@ -24,15 +24,16 @@ class CustomsDispatchGateContributorTest {
@Test void passesOnlyCanonicalClearanceCompletedCase() {
CustomsCaseEntity value = new CustomsCaseEntity();
value.setStatus("RECEIPT_AVAILABLE");
when(repository.gateCase(10001L, 21L, 31L, 81L)).thenReturn(value);
assertThat(contributor.assess(context()).passed()).isFalse();
value.setStatus("CLEARANCE_COMPLETED");
when(repository.clearedContainerCase(81L)).thenReturn(value);
assertThat(contributor.assess(context()))
.satisfies(item -> {
assertThat(item.passed()).isTrue();
assertThat(item.requirement()).isEqualTo("CLEARANCE_COMPLETED");
});
verify(repository, times(2)).clearedContainerCase(81L);
verify(repository, never()).gateCase(anyLong(), anyLong(), anyLong(), anyLong());
}
private static Context context() { return new Context(10001L, 10002L, 21L, 31L, 81L, 91L, null); }
@@ -47,10 +47,23 @@ class OutboundDispatchReadMapperContractTest {
.getAnnotation(Select.class).value()[0];
assertThat(sql)
.contains("seal_no<>''")
.contains("evidence.seal_no<>''")
.doesNotContain("&lt;", "&gt;");
}
@Test
void evidenceGateAggregatesOnePhysicalEvidenceSetAcrossTheWholeLoadingPlan() throws Exception {
String sql = OutboundDispatchReadMapper.class.getMethod("evidence", long.class, long.class)
.getAnnotation(Select.class).value()[0];
assertThat(sql)
.contains("plan.id=#{loadingPlanId}", "plan.container_booking_id=#{containerBookingId}")
.contains("item.loading_plan_id=plan.id", "item.deleted=0")
.contains("evidence.inbound_id=item.inbound_id", "evidence.tenant_id=item.account_id")
.contains("COUNT(DISTINCT CASE", "evidence.evidence_type")
.doesNotContain("WHERE tenant_id=#{tenantId}", "inbound_id=#{inboundId}");
}
@Test
void oceanAndTruckQueriesAreResolvedOnceFromTheContainerRoot() throws Exception {
String oceanSql = OutboundDispatchReadMapper.class.getMethod("ocean", long.class)
@@ -14,6 +14,7 @@ import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.LongStream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -30,18 +31,38 @@ class OutboundDispatchServiceTest {
}
@Test
void gateAggregatesEveryRealInboundAndFailsClosedWhenOneBindingFails() {
void containerEvidenceFailureFailsEveryBindingConsistently() {
OutboundDispatchService service = serviceWithoutCarrier();
stubContainer();
when(repository.evidence(7L, 12L)).thenReturn(new OutboundDispatchReadMapper.Evidence(4, 1));
when(repository.evidence(22L, 44L)).thenReturn(new OutboundDispatchReadMapper.Evidence(4, 1));
ContainerGateResult result = service.checkGate(22L);
assertThat(result.passed()).isFalse();
assertThat(result.inbounds()).hasSize(2);
assertThat(result.inbounds().get(1).gate().items())
assertThat(result.inbounds()).allSatisfy(inbound -> assertThat(inbound.gate().items())
.filteredOn(item -> "LOADING_EVIDENCE_COMPLETE".equals(item.code()))
.extracting(GateItem::passed).containsExactly(false);
.extracting(GateItem::passed).containsExactly(false));
verify(repository, times(1)).evidence(22L, 44L);
}
@Test
void tenInboundBindingsShareOneCompleteEvidenceSetAndOneContainerCustomsResult() {
OutboundDispatchGateContributor customs = mock(OutboundDispatchGateContributor.class);
when(customs.code()).thenReturn("CUSTOMS_CLEARANCE");
when(customs.assess(any())).thenReturn(new GateItem(
"CUSTOMS_CLEARANCE", "报关完成", true, "CLEARANCE_COMPLETED", "CLEARANCE_COMPLETED"));
OutboundDispatchService service = new OutboundDispatchService(repository, List.of(customs), null);
List<OutboundDispatchReadMapper.Binding> tenBindings = LongStream.rangeClosed(11L, 20L)
.mapToObj(id -> binding(id, "I-" + id)).toList();
stubContainer(tenBindings);
ContainerGateResult result = service.checkGate(22L);
assertThat(result.passed()).isTrue();
assertThat(result.inbounds()).hasSize(10).allSatisfy(value -> assertThat(value.gate().passed()).isTrue());
verify(repository, times(1)).evidence(22L, 44L);
verify(customs, times(1)).assess(any());
}
@Test
@@ -72,6 +93,19 @@ class OutboundDispatchServiceTest {
verify(repository, times(2)).confirm(anyLong(), eq(0), eq(9L), eq("admin"), isNull());
}
@Test
void confirmAllowsMissingSailingScheduleContributor() {
OutboundDispatchService service = serviceWithoutCarrier();
stubContainer();
stubConfirmMutation("no-schedule");
Detail result = service.confirm(22L, 9L, "admin", "no-schedule", new ActionRequest(0, null));
assertThat(result.aggregate().gate().passed()).isTrue();
assertThat(result.aggregate().gate().inbounds()).allSatisfy(inbound -> assertThat(inbound.gate().items())
.extracting(GateItem::code).doesNotContain("SAILING_SCHEDULE"));
}
@Test
void detailReturnsOneVersionAndAllInboundBindings() {
OutboundDispatchService service = serviceWithoutCarrier();
@@ -117,7 +151,7 @@ class OutboundDispatchServiceTest {
OutboundDispatchService service = serviceWithoutCarrier();
stubContainer();
when(repository.lockBindings(22L)).thenReturn(bindings());
when(repository.evidence(7L, 12L)).thenReturn(new OutboundDispatchReadMapper.Evidence(4, 1));
when(repository.evidence(22L, 44L)).thenReturn(new OutboundDispatchReadMapper.Evidence(4, 1));
when(repository.claim(eq(9L), eq("CONFIRM_DISPATCH"), eq("blocked"), anyString())).thenReturn(true);
assertThatThrownBy(() -> service.confirm(22L, 9L, "admin", "blocked", new ActionRequest(0, null)))
@@ -201,16 +235,19 @@ class OutboundDispatchServiceTest {
private OutboundDispatchService service(CarrierScheduleQueryService carrierSchedules) {
return new OutboundDispatchService(repository,
List.of(passing("CUSTOMS_CLEARANCE"), passing("SAILING_SCHEDULE")), carrierSchedules);
List.of(passing("CUSTOMS_CLEARANCE")), carrierSchedules);
}
private void stubContainer() {
when(repository.bindings(22L)).thenReturn(bindings());
stubContainer(bindings());
}
private void stubContainer(List<OutboundDispatchReadMapper.Binding> values) {
when(repository.bindings(22L)).thenReturn(values);
when(repository.pick(44L)).thenReturn(new OutboundDispatchReadMapper.IdStatus(55L, "COMPLETED"));
when(repository.ocean(22L)).thenReturn(new OutboundDispatchReadMapper.OceanBinding(66L, "RELEASED", 7L));
when(repository.truck(22L, 44L)).thenReturn(new OutboundDispatchReadMapper.IdStatus(77L, "DEPARTED"));
when(repository.evidence(7L, 11L)).thenReturn(new OutboundDispatchReadMapper.Evidence(5, 1));
when(repository.evidence(7L, 12L)).thenReturn(new OutboundDispatchReadMapper.Evidence(5, 1));
when(repository.evidence(22L, 44L)).thenReturn(new OutboundDispatchReadMapper.Evidence(5, 1));
}
private void stubConfirmMutation(String key) {
@@ -50,6 +50,9 @@ class OutboundDispatchSqlContractTest {
.contains("Once an actual DEPARTED_WAREHOUSE node exists")
.contains("absence never blocks manual outbound confirmation")
.contains("PENDING or any non-terminal state never affects gate.passed")
.contains("Loading evidence is evaluated once for the physical container/loading plan")
.contains("Customs clearance is also resolved once at container scope")
.contains("missing standalone sailing schedule is reference-only")
.contains("expectedVersion")
.contains("Idempotency-Key")
.doesNotContain("confirm-dispatch", "reverse-dispatch", "check-gate", "inboundId, containerBookingId");
@@ -12,6 +12,14 @@ import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class ContainerOverviewSqlContractTest {
@Test void overviewIncludesExactlyTheFourCurrentBusinessDocumentTypes() throws Exception {
String sql = String.join(" ", ContainerOverviewMapper.class.getDeclaredMethod("documents", long.class)
.getAnnotation(Select.class).value());
assertThat(sql)
.contains("'DESTINATION_CLEARANCE'", "'CUSTOMS_DECLARATION'", "'DELIVERY_ORDER'", "'PICKUP_ORDER'")
.contains("ORDER BY document_type");
}
@Test void pdaSummarySelectsRecordFieldsExplicitlyIncludingTaskVersion() throws Exception {
String sql = String.join(" ", ContainerOverviewMapper.class.getDeclaredMethod("pickTask", long.class)
.getAnnotation(Select.class).value());
@@ -47,7 +47,13 @@ class ContainerOverviewServiceTest {
71L, 81L, 91L, "LOADING_DOOR_OPEN", 101L, "door.jpg", "ACTIVE", LocalDateTime.now()),
new ContainerOverviewMapper.PhotoRow(
72L, 81L, 91L, "LOADING_DOOR_OPEN", 102L, "void.jpg", "VOID", LocalDateTime.now())));
when(mapper.documents(41L)).thenReturn(java.util.List.of());
com.baiyue.firstleg.entity.ContainerDocumentEntity customs = new com.baiyue.firstleg.entity.ContainerDocumentEntity();
customs.setId(103L); customs.setDocumentType("CUSTOMS_DECLARATION"); customs.setFileId(104L);
customs.setFileName("customs.pdf"); customs.setVersion(3);
com.baiyue.firstleg.entity.ContainerDocumentEntity clearance = new com.baiyue.firstleg.entity.ContainerDocumentEntity();
clearance.setId(105L); clearance.setDocumentType("DESTINATION_CLEARANCE"); clearance.setFileId(106L);
clearance.setFileName("clearance.pdf"); clearance.setVersion(1);
when(mapper.documents(41L)).thenReturn(java.util.List.of(customs, clearance));
when(mapper.exceptions(41L)).thenReturn(java.util.List.of());
when(mapper.transport(41L)).thenReturn(java.util.List.of());
when(mapper.pickTimeline(61L)).thenReturn(java.util.List.of());
@@ -66,6 +72,15 @@ class ContainerOverviewServiceTest {
assertThat(count.evidenceType()).isEqualTo("LOADING_DOOR_OPEN");
assertThat(count.count()).isEqualTo(1);
});
assertThat(overview.documents()).extracting(document -> document.documentType())
.containsExactly("CUSTOMS_DECLARATION", "DESTINATION_CLEARANCE");
assertThat(overview.documents()).filteredOn(document -> "CUSTOMS_DECLARATION".equals(document.documentType()))
.singleElement().satisfies(document -> assertThat(document.version()).isEqualTo(3));
assertThat(overview.documents()).filteredOn(document -> "DESTINATION_CLEARANCE".equals(document.documentType()))
.singleElement().satisfies(document -> {
assertThat(document.fileId()).isEqualTo(106L);
assertThat(document.version()).isEqualTo(1);
});
}
@Test void downloadsOnlyActivePhotoOwnedByContainerLoadingPlan() {
@@ -450,16 +450,16 @@ class LoadingListServiceTest {
String contentHash = sha256Bytes(content);
var file = new MockMultipartFile("file", "customs.pdf", "application/pdf", content);
var metadata = new LoadingDocumentFileClient.Metadata(101L, 0L,
"LOADING_CUSTOMS_DOCUMENT", "10", "customs.pdf", contentHash, "application/pdf",
"LOADING_CUSTOMS_DECLARATION", "10", "customs.pdf", contentHash, "application/pdf",
(long) content.length);
ContainerDocumentEntity saved = document(41L, LoadingListService.CUSTOMS_DECLARATION, 0, false);
saved.setFileId(101L); saved.setFileName("customs.pdf"); saved.setFileHash(contentHash);
when(repository.aggregate(10L)).thenReturn(plan);
when(repository.history(anyString())).thenReturn(null);
when(repository.document(30L, LoadingListService.CUSTOMS_DECLARATION)).thenReturn(null, saved);
when(documentFiles.upload(anyString(), eq(9L), eq(0L), eq("LOADING_CUSTOMS_DOCUMENT"),
when(documentFiles.upload(anyString(), eq(9L), eq(0L), eq("LOADING_CUSTOMS_DECLARATION"),
eq("10"), eq(file))).thenReturn(ApiResponse.success(metadata));
when(documentFiles.metadata(anyString(), eq(0L), eq("LOADING_CUSTOMS_DOCUMENT"),
when(documentFiles.metadata(anyString(), eq(0L), eq("LOADING_CUSTOMS_DECLARATION"),
eq("10"), eq(101L))).thenReturn(ApiResponse.success(metadata));
when(repository.createDocument(org.mockito.ArgumentMatchers.any())).thenReturn(1);
@@ -467,10 +467,108 @@ class LoadingListServiceTest {
10L, 100L, 9L, "operator", "customs-upload", 0, null, file);
assertThat(result.documentType()).isEqualTo(LoadingListService.CUSTOMS_DECLARATION);
verify(documentFiles).upload(anyString(), eq(9L), eq(0L), eq("LOADING_CUSTOMS_DOCUMENT"),
verify(documentFiles).upload(anyString(), eq(9L), eq(0L), eq("LOADING_CUSTOMS_DECLARATION"),
eq("10"), eq(file));
}
@Test
void downloadsCompletedLoadingDeclarationWithExactLongFileOwnership() {
long planId = 9_007_199_254_740_993L;
long fileId = 9_007_199_254_740_995L;
AggregateRow plan = row("COMPLETED", 4);
plan.setPlanId(planId);
ContainerDocumentEntity document = document(41L, LoadingListService.CUSTOMS_DECLARATION, 4, true);
document.setFileId(fileId);
document.setFileName("出口报关单.pdf");
byte[] content = new byte[]{1, 2, 3};
var metadata = new LoadingDocumentFileClient.Metadata(fileId, 0L,
"LOADING_CUSTOMS_DECLARATION", Long.toString(planId), "出口报关单.pdf", hash(),
"application/pdf", (long) content.length);
when(repository.aggregate(planId)).thenReturn(plan);
when(repository.document(30L, LoadingListService.CUSTOMS_DECLARATION)).thenReturn(document);
when(documentFiles.metadata(anyString(), eq(0L), eq("LOADING_CUSTOMS_DECLARATION"),
eq(Long.toString(planId)), eq(fileId))).thenReturn(ApiResponse.success(metadata));
when(documentFiles.download(anyString(), eq(0L), eq("LOADING_CUSTOMS_DECLARATION"),
eq(Long.toString(planId)), eq(fileId))).thenReturn(org.springframework.http.ResponseEntity.ok(content));
var result = service.downloadLoadingDeclaration(planId, 100L, fileId);
assertThat(result.fileName()).isEqualTo("出口报关单.pdf");
assertThat(result.content()).containsExactly(1, 2, 3);
verify(documentFiles).download(anyString(), eq(0L), eq("LOADING_CUSTOMS_DECLARATION"),
eq("9007199254740993"), eq(fileId));
}
@Test
void rejectsNonCurrentLoadingDeclarationFileIdBeforeFileService() {
AggregateRow plan = row("COMPLETED", 4);
when(repository.aggregate(10L)).thenReturn(plan);
when(repository.document(30L, LoadingListService.CUSTOMS_DECLARATION))
.thenReturn(document(41L, LoadingListService.CUSTOMS_DECLARATION, 4, true));
assertThatThrownBy(() -> service.downloadLoadingDeclaration(10L, 100L, 100L))
.isInstanceOf(BizException.class)
.extracting(error -> ((BizException) error).getResultCode()).isEqualTo(ResultCode.NOT_FOUND);
verify(documentFiles, never()).metadata(anyString(), anyLong(), anyString(), anyString(), anyLong());
}
@Test
void rejectsLoadingDeclarationFileWithMismatchedBusinessOwnership() {
AggregateRow plan = row("COMPLETED", 4);
ContainerDocumentEntity document = document(41L, LoadingListService.CUSTOMS_DECLARATION, 4, true);
when(repository.aggregate(10L)).thenReturn(plan);
when(repository.document(30L, LoadingListService.CUSTOMS_DECLARATION)).thenReturn(document);
var metadata = new LoadingDocumentFileClient.Metadata(99L, 0L,
"LOADING_CUSTOMS_DECLARATION", "other-plan", "customs.pdf", hash(),
"application/pdf", 3L);
when(documentFiles.metadata(anyString(), eq(0L), eq("LOADING_CUSTOMS_DECLARATION"),
eq("10"), eq(99L))).thenReturn(ApiResponse.success(metadata));
assertThatThrownBy(() -> service.downloadLoadingDeclaration(10L, 100L, 99L))
.isInstanceOf(BizException.class)
.extracting(error -> ((BizException) error).getResultCode()).isEqualTo(ResultCode.NOT_FOUND);
verify(documentFiles, never()).download(anyString(), anyLong(), anyString(), anyString(), anyLong());
}
@Test
void downloadsCurrentDestinationClearanceWithExactContainerOwnership() {
long bookingId = 9_007_199_254_740_993L;
long fileId = 9_007_199_254_740_995L;
AggregateRow plan = row("COMPLETED", 4);
ContainerDocumentEntity document = document(41L, LoadingListService.DESTINATION_CLEARANCE, 1, true);
document.setContainerBookingId(bookingId);
document.setFileId(fileId);
document.setFileName("destination-clearance.pdf");
byte[] content = new byte[]{4, 5, 6};
var metadata = new LoadingDocumentFileClient.Metadata(fileId, 0L,
"CONTAINER_CLEARANCE_DOCUMENT", Long.toString(bookingId), "destination-clearance.pdf",
hash(), "application/pdf", (long) content.length);
when(repository.aggregateByBooking(bookingId)).thenReturn(plan);
when(repository.document(bookingId, LoadingListService.DESTINATION_CLEARANCE)).thenReturn(document);
when(documentFiles.metadata(anyString(), eq(0L), eq("CONTAINER_CLEARANCE_DOCUMENT"),
eq(Long.toString(bookingId)), eq(fileId))).thenReturn(ApiResponse.success(metadata));
when(documentFiles.download(anyString(), eq(0L), eq("CONTAINER_CLEARANCE_DOCUMENT"),
eq(Long.toString(bookingId)), eq(fileId))).thenReturn(org.springframework.http.ResponseEntity.ok(content));
var result = service.downloadContainerClearance(bookingId, fileId);
assertThat(result.fileName()).isEqualTo("destination-clearance.pdf");
assertThat(result.content()).containsExactly(4, 5, 6);
}
@Test
void rejectsNonCurrentDestinationClearanceFileIdAsNotFound() {
AggregateRow plan = row("COMPLETED", 4);
when(repository.aggregateByBooking(30L)).thenReturn(plan);
when(repository.document(30L, LoadingListService.DESTINATION_CLEARANCE))
.thenReturn(document(41L, LoadingListService.DESTINATION_CLEARANCE, 1, true));
assertThatThrownBy(() -> service.downloadContainerClearance(30L, 100L))
.isInstanceOf(BizException.class)
.extracting(error -> ((BizException) error).getResultCode()).isEqualTo(ResultCode.NOT_FOUND);
verify(documentFiles, never()).metadata(anyString(), anyLong(), anyString(), anyString(), anyLong());
}
@Test
void rejectsReusedIdempotencyKeyWithDifferentAttachRequest() {
AggregateRow plan = row("LOADING", 2);