fix(selfop): persist bounded next-root quantity diagnostics

This commit is contained in:
2026-10-14 04:59:05 +08:00
parent 765b6e6208
commit 913b9e9262
17 changed files with 1142 additions and 914 deletions
@@ -1,146 +1,50 @@
package com.baiyue.integration.provider.lingxing;
import static com.baiyue.integration.provider.lingxing.LingxingQuantityEvidence.*;
import static org.assertj.core.api.Assertions.*;
import com.baiyue.integration.config.LingxingQuantityCaptureProperties;
import com.baiyue.integration.dto.lingxing.LingxingEgressDtos.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.time.*;
import java.util.*;
import java.util.concurrent.*;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;
class LingxingQuantityCaptureTest {
private static final Instant NOW = Instant.parse("2026-10-13T01:00:00Z");
private final ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule());
private final BlockingQueue<LingxingQuantityEvidence> saved = new LinkedBlockingQueue<>();
private EgressContext context(String session) { return new EgressContext("account", CollectionMode.BUSINESS,
"root", session, "attempt", "grant"); }
private LingxingQuantityCapture capture(Instant expiry, LingxingQuantityEvidence.Sink sink) {
return new LingxingQuantityCapture(new LingxingQuantityCaptureProperties(true, "capture", "account",
"root", "session", "*", "*", NOW.minusSeconds(1), expiry), mapper, sink, Clock.fixed(NOW, ZoneOffset.UTC));
}
private void observe(LingxingQuantityCapture c, boolean detail, String items, String session, int status) throws Exception {
String order = "{\"whCode\":\"W\",\"customerCode\":\"C\",\"deliveryNo\":\"D\",\"status\":" + status
+ ",\"" + (detail ? "skuVOList" : "productList") + "\":[" + items + "]}";
c.observe(mapper.readTree(detail ? order : "{\"records\":[" + order + "]}"), detail, context(session), NOW);
}
@ParameterizedTest @ValueSource(ints = {10, 8})
void preservesBothSemanticOutcomesAndStopsAtDifference(int listQty) throws Exception {
try (var c = new CloseableCapture(capture(NOW.plusSeconds(60), saved::add))) {
observe(c.value, false, "{\"productSku\":\"S\",\"qty\":" + listQty + "}", "session", 100);
observe(c.value, true, "{\"productSku\":\"S\",\"deliveryQty\":10,\"actualDeliveryQty\":8}", "session", 100);
var e = saved.poll(3, TimeUnit.SECONDS);
assertThat(e).isNotNull(); assertThat(e.forecastActualDifference()).isTrue();
assertThat(e.skus().get(0).listQty()).isEqualByComparingTo(Integer.toString(listQty));
assertThat(e.skuSetEqual()).isTrue();
awaitStop(c.value, "FORECAST_ACTUAL_DIFFERENCE");
}
}
@Test void duplicateDecimalsUnknownsAndSkuMismatchRemainVisibleForCancelledOrder() throws Exception {
try (var c = new CloseableCapture(capture(NOW.plusSeconds(60), saved::add))) {
observe(c.value, false, "{\"productSku\":\"S\",\"qty\":0.1},{\"productSku\":\"S\",\"qty\":0.2},"
+ "{\"productSku\":\"LIST_ONLY\",\"qty\":1}", "session", 99);
observe(c.value, true, "{\"productSku\":\"S\",\"deliveryQty\":1,\"actualDeliveryQty\":null},"
+ "{\"productSku\":\"S\",\"deliveryQty\":2,\"actualDeliveryQty\":1}", "session", 99);
var e = saved.poll(3, TimeUnit.SECONDS);
assertThat(e).isNotNull(); assertThat(e.skuSetEqual()).isFalse();
var line = e.skus().stream().filter(v -> v.sku().equals("S")).findFirst().orElseThrow();
assertThat(line.listQty()).isEqualByComparingTo("0.3");
assertThat(line.forecastQty()).isEqualByComparingTo("3"); assertThat(line.actualQty()).isNull();
assertThat(e.detailObservation().status()).isEqualTo("99"); assertThat(e.detailObservation().outboundTime()).isNull();
assertThat(e.forecastActualDifference()).isFalse();
}
}
@Test void crossSessionCannotPairAndExpiredCaptureDoesNotWrite() throws Exception {
try (var c = new CloseableCapture(capture(NOW.plusSeconds(60), saved::add))) {
observe(c.value, false, "{\"productSku\":\"S\",\"qty\":1}", "other", 100);
observe(c.value, true, "{\"productSku\":\"S\",\"deliveryQty\":1,\"actualDeliveryQty\":1}", "session", 100);
assertThat(saved.poll(100, TimeUnit.MILLISECONDS)).isNull();
}
try (var c = new CloseableCapture(capture(NOW, saved::add))) {
observe(c.value, false, "{\"productSku\":\"S\",\"qty\":1}", "session", 100);
assertThat(c.value.stopReason()).isEqualTo("EXPIRED"); assertThat(saved).isEmpty();
}
}
@Test void sinkFailureStopsOnlyDiagnosticAndNeverEscapesCaller() throws Exception {
try (var c = new CloseableCapture(capture(NOW.plusSeconds(60), evidence -> { throw new IllegalStateException("private"); }))) {
observe(c.value, false, "{\"productSku\":\"S\",\"qty\":1}", "session", 100);
observe(c.value, true, "{\"productSku\":\"S\",\"deliveryQty\":1,\"actualDeliveryQty\":1}", "session", 100);
awaitStop(c.value, "DIAGNOSTIC_FAILED");
}
}
@Test void hundredCompleteOrdersStopsFurtherCapture() throws Exception {
try (var c = new CloseableCapture(capture(NOW.plusSeconds(60), saved::add))) {
for (int i = 0; i < 100; i++) {
String head = "\"whCode\":\"W\",\"customerCode\":\"C\",\"deliveryNo\":\"D" + i + "\",\"status\":100,";
c.value.observe(mapper.readTree("{\"records\":[{" + head + "\"productList\":[{\"productSku\":\"S\",\"qty\":1}]}]}"), false, context("session"), NOW);
c.value.observe(mapper.readTree("{" + head + "\"skuVOList\":[{\"productSku\":\"S\",\"deliveryQty\":1,\"actualDeliveryQty\":1}]}"), true, context("session"), NOW);
assertThat(saved.poll(3, TimeUnit.SECONDS)).isNotNull();
}
awaitStop(c.value, "ORDER_LIMIT");
observe(c.value, false, "{\"productSku\":\"S\",\"qty\":1}", "session", 100);
assertThat(saved).isEmpty();
}
}
@Test void thousandRowPageSelectsHundredAndSlowSinkDoesNotBlockDetails() throws Exception {
CountDownLatch entered = new CountDownLatch(1), release = new CountDownLatch(1);
try (var c = new CloseableCapture(capture(NOW.plusSeconds(60), evidence -> {
entered.countDown();
try { release.await(3, TimeUnit.SECONDS); } catch (InterruptedException error) { Thread.currentThread().interrupt(); }
saved.add(evidence);
}))) {
var page = mapper.createObjectNode(); var records = page.putArray("records");
for (int i = 0; i < 1000; i++) {
var order = records.addObject().put("whCode", "W").put("customerCode", "C").put("deliveryNo", "D" + i).put("status", 100);
order.putArray("productList").addObject().put("productSku", "S").put("qty", 1);
}
c.value.observe(page, false, context("session"), NOW);
assertThat(c.value.stopReason()).isNull();
// Unselected details arrive first: they must not consume queue or evidence capacity.
for (int i = 999; i >= 0; i--) {
var detail = mapper.createObjectNode().put("whCode", "W").put("customerCode", "C").put("deliveryNo", "D" + i).put("status", 100);
detail.putArray("skuVOList").addObject().put("productSku", "S").put("deliveryQty", 1).put("actualDeliveryQty", 1);
c.value.observe(detail, true, context("session"), NOW);
}
assertThat(entered.await(3, TimeUnit.SECONDS)).isTrue();
assertThat(c.value.stopReason()).isEqualTo("ORDER_LIMIT");
release.countDown();
for (int i = 0; i < 100; i++) assertThat(saved.poll(3, TimeUnit.SECONDS)).isNotNull();
assertThat(saved).isEmpty();
} finally { release.countDown(); }
}
private static void awaitStop(LingxingQuantityCapture capture, String reason) throws Exception {
long end = System.nanoTime() + TimeUnit.SECONDS.toNanos(3);
while (capture.stopReason() == null && System.nanoTime() < end) Thread.sleep(5);
assertThat(capture.stopReason()).isEqualTo(reason);
}
@ParameterizedTest @ValueSource(strings = {"FORECAST_ACTUAL_DIFFERENCE", "LIMIT_REACHED"})
void savedFinalOrderRetainsNormalReceiverReasonAndDiscardsQueuedOrders(String reason) throws Exception {
CountDownLatch entered = new CountDownLatch(1), release = new CountDownLatch(1);
try (var c = new CloseableCapture(capture(NOW.plusSeconds(60), evidence -> {
entered.countDown();
try { release.await(3, TimeUnit.SECONDS); } catch (InterruptedException error) { Thread.currentThread().interrupt(); }
saved.add(evidence); // Receiver persisted the last accepted order before returning its stop decision.
throw new LingxingQuantityEvidence.ReceiverStopped(reason, true);
}))) {
for (int i = 0; i < 3; i++) {
var order = mapper.createObjectNode().put("whCode", "W").put("customerCode", "C").put("deliveryNo", "D" + i).put("status", 100);
order.putArray("productList").addObject().put("productSku", "S").put("qty", 1);
var page = mapper.createObjectNode(); page.putArray("records").add(order);
c.value.observe(page, false, context("session"), NOW);
order.putArray("skuVOList").addObject().put("productSku", "S").put("deliveryQty", 1).put("actualDeliveryQty", 1);
c.value.observe(order, true, context("session"), NOW);
}
assertThat(entered.await(3, TimeUnit.SECONDS)).isTrue();
release.countDown(); awaitStop(c.value, reason);
assertThat(saved).hasSize(1);
assertThat(saved.remove().orderNo()).isEqualTo("D0");
assertThat(saved.poll(100, TimeUnit.MILLISECONDS)).isNull();
} finally { release.countDown(); }
}
private record CloseableCapture(LingxingQuantityCapture value) implements AutoCloseable {
@Override public void close() { value.close(); }
}
final Instant now=Instant.parse("2026-10-14T00:00:00Z");final ObjectMapper json=new ObjectMapper();
final EgressContext context=new EgressContext("account",CollectionMode.BUSINESS,"2","selfop-2","attempt","grant");
View view(){return new View("diag","account",null,null,"ARMED",null,now,now.plusSeconds(36000),now.plusSeconds(21600),List.of("W"),"*","1",0,0,0,0,false,List.of());}
com.fasterxml.jackson.databind.JsonNode list(String id)throws Exception{return json.readTree("{\"records\":[{\"whCode\":\"W\",\"customerCode\":\"C\",\"deliveryNo\":\""+id+"\",\"productList\":[{\"productSku\":\"S\",\"qty\":10}]}]}");}
class Fake implements Sink {View state=view();List<Event> received=new ArrayList<>();String stopped;public View status(Status s){return state;}public Receipt observe(Event e){received.add(e);return new Receipt("ACCEPTED",null,state);}public View stop(Stop s){stopped=s.reason();return state;}}
LingxingQuantityCapture capture(Sink sink){return new LingxingQuantityCapture(new LingxingQuantityCaptureProperties(true,"account"),sink,Clock.fixed(now,ZoneOffset.UTC),false);}
@Test void observationDoesNotCallReceiverAndLocateNeverBinds()throws Exception{
Fake sink=new Fake();try(var c=new TestClose(capture(sink))){c.value.pump();c.value.observe(list("a"),Operation.OUTBOUND_LOCATE,context,now);c.value.pump();assertThat(sink.received).isEmpty();
c.value.observe(list("a"),Operation.OUTBOUND_QUERY,context,now);assertThat(sink.received).isEmpty();c.value.pump();assertThat(sink.received).hasSize(1);assertThat(sink.received.get(0).operation()).isEqualTo("OUTBOUND_QUERY");}
}
@Test void slowReceiverNeverBlocksNormalObservationAndFullQueueStopsOnlyDiagnosis()throws Exception{
CountDownLatch entered=new CountDownLatch(1),release=new CountDownLatch(1);Fake sink=new Fake(){public Receipt observe(Event e){entered.countDown();try{release.await(3,TimeUnit.SECONDS);}catch(InterruptedException x){Thread.currentThread().interrupt();}return super.observe(e);}};
var c=capture(sink);c.pump();c.observe(list("first"),Operation.OUTBOUND_QUERY,context,now);var worker=Executors.newSingleThreadExecutor();try{var pending=worker.submit(c::pump);assertThat(entered.await(1,TimeUnit.SECONDS)).isTrue();
assertThatCode(()->{for(int i=0;i<100;i++)c.observe(list("o"+i),Operation.OUTBOUND_QUERY,context,now);}).doesNotThrowAnyException();
release.countDown();pending.get(5,TimeUnit.SECONDS);
}finally{release.countDown();worker.shutdownNow();c.close();}
}
@Test void failureIsolatedAndNoRearmOfSameGrant()throws Exception{
Fake sink=new Fake(){public Receipt observe(Event e){throw new IllegalStateException("receiver down");}};var c=capture(sink);try{c.pump();c.observe(list("a"),Operation.OUTBOUND_QUERY,context,now);assertThatCode(c::pump).doesNotThrowAnyException();assertThat(c.stopReason()).isEqualTo("INCOMPLETE");c.pump();assertThat(sink.stopped).isEqualTo("INCOMPLETE");}finally{c.close();}
}
@Test void selectedCandidatesRecoveredWithoutReobservingList()throws Exception{
Fake sink=new Fake();sink.state=new View("diag","account","2","selfop-2","BOUND",null,now,now.plusSeconds(14400),now.plusSeconds(21600),List.of("W"),"*","1",1,0,1,100,true,List.of(new Key("W","C","a",false)));
var c=capture(sink);try{c.pump();c.observe(list("a"),Operation.OUTBOUND_QUERY,context,now);c.observe(json.readTree("{\"whCode\":\"W\",\"customerCode\":\"C\",\"deliveryNo\":\"a\",\"skuVOList\":[{\"productSku\":\"S\",\"deliveryQty\":10,\"actualDeliveryQty\":10}]}"),Operation.OUTBOUND_DETAIL,context,now);c.pump();assertThat(sink.received).hasSize(1);assertThat(sink.received.get(0).side()).isEqualTo("DETAIL");}finally{c.close();}
}
@Test void malformedOrOversizedSnapshotsAreIsolated()throws Exception{
Fake sink=new Fake();var c=capture(sink);try{c.pump();var data=json.createObjectNode();var records=data.putArray("records");for(int i=0;i<101;i++)records.addObject();assertThatCode(()->c.observe(data,Operation.OUTBOUND_QUERY,context,now)).doesNotThrowAnyException();c.pump();assertThat(sink.stopped).isEqualTo("OBSERVER_FAILED");assertThat(sink.received).isEmpty();}finally{c.close();}
}
@Test void queueIsBoundedAndNoProducerWaitsForIt()throws Exception{
Fake sink=new Fake();var c=capture(sink);try{c.pump();for(int i=0;i<100;i++)c.observe(list("o"+i),Operation.OUTBOUND_QUERY,context,now);
var detail=json.readTree("{\"whCode\":\"W\",\"customerCode\":\"C\",\"deliveryNo\":\"o0\",\"skuVOList\":[{\"productSku\":\"S\",\"deliveryQty\":10,\"actualDeliveryQty\":10}]}");
c.observe(detail,Operation.OUTBOUND_DETAIL,context,now);assertThat(c.stopReason()).isEqualTo("QUEUE_FULL");c.pump();assertThat(sink.stopped).isEqualTo("QUEUE_FULL");assertThat(sink.received).isEmpty();
}finally{c.close();}
}
@Test void diagnosticLockContentionNeverWaitsOnCollector()throws Exception{
Fake sink=new Fake();var c=capture(sink);c.pump();var field=LingxingQuantityCapture.class.getDeclaredField("intake");field.setAccessible(true);var lock=(java.util.concurrent.locks.ReentrantLock)field.get(c);
var pool=Executors.newSingleThreadExecutor();lock.lock();try{var observation=pool.submit(()->{try{c.observe(list("a"),Operation.OUTBOUND_QUERY,context,now);}catch(Exception e){throw new RuntimeException(e);}});observation.get(500,TimeUnit.MILLISECONDS);assertThat(c.stopReason()).isEqualTo("OBSERVER_FAILED");}finally{lock.unlock();pool.shutdownNow();c.close();}
}
record TestClose(LingxingQuantityCapture value)implements AutoCloseable{public void close(){value.close();}}
}
@@ -1,65 +1,32 @@
package com.baiyue.integration.provider.lingxing;
import com.baiyue.integration.config.LingxingEgressProperties;
import com.baiyue.integration.dto.lingxing.LingxingEgressDtos.CollectionMode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.sun.net.httpserver.HttpServer;
import org.junit.jupiter.api.Test;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.Flow;
import static org.assertj.core.api.Assertions.*;
class LingxingQuantityHttpSinkTest {
@Test void exactContractAuthenticationAndNoRetryWhenReceiverStops() throws Exception {
var calls = new AtomicInteger(); var body = new AtomicReference<String>(); var header = new AtomicReference<String>();
var outcome = new AtomicReference<>("ACCEPTED");
var reason = new AtomicReference<>("null");
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/internal/lastleg/self-operated-analytics/quantity-diagnostics/orders", exchange -> {
calls.incrementAndGet(); body.set(new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8));
header.set(exchange.getRequestHeaders().getFirst("X-Internal-Service-Token"));
byte[] response = ("{\"code\":\"200\",\"data\":{\"outcome\":\"" + outcome.get() + "\",\"stopReason\":" + reason.get() + "}}")
.getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, response.length); exchange.getResponseBody().write(response); exchange.close();
}); server.start();
try {
var props = new LingxingEgressProperties(CollectionMode.OFF, "test", null, null, null,
URI.create("http://127.0.0.1:" + server.getAddress().getPort()), null, null, false);
var mapper = new ObjectMapper().registerModule(new JavaTimeModule())
.disable(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
String token = "synthetic-internal-token-000000000000";
var sink = new LingxingQuantityHttpSink(props, mapper, token);
var obs = new LingxingQuantityEvidence.Observation("99", null, Instant.parse("2026-10-13T00:00:00Z"), "attempt", true);
var evidence = new LingxingQuantityEvidence("diagnostic", "account", "root", "collection", "W", "C", "D", obs, obs,
List.of(new LingxingQuantityEvidence.Line("S", true, true, null, null, null)));
sink.accept(evidence);
var json = mapper.readTree(body.get());
assertThat(header.get()).isEqualTo(token); assertThat(json.path("diagnosticSessionId").asText()).isEqualTo("diagnostic");
assertThat(json.path("listObservation").path("observedAt").asText()).isEqualTo("2026-10-13T00:00:00Z");
assertThat(json.path("skus").get(0).path("forecastQty").isNull()).isTrue();
assertThat(json.has("skuSetEqual")).isFalse(); assertThat(json.has("forecastActualDifference")).isFalse();
outcome.set("STOPPED"); assertThatThrownBy(() -> sink.accept(evidence)).isInstanceOf(IllegalStateException.class);
assertThat(calls.get()).isEqualTo(2);
for (String stopReason : List.of("FORECAST_ACTUAL_DIFFERENCE", "LIMIT_REACHED")) {
outcome.set("ACCEPTED"); reason.set("\"" + stopReason + "\"");
assertThatThrownBy(() -> sink.accept(evidence)).isInstanceOfSatisfying(
LingxingQuantityEvidence.ReceiverStopped.class, stopped -> {
assertThat(stopped.reason()).isEqualTo(stopReason);
assertThat(stopped.orderStored()).isTrue();
});
}
outcome.set("STOPPED"); reason.set("\"DISABLED\"");
assertThatThrownBy(() -> sink.accept(evidence)).isInstanceOfSatisfying(
LingxingQuantityEvidence.ReceiverStopped.class, stopped -> {
assertThat(stopped.reason()).isEqualTo("DISABLED");
assertThat(stopped.orderStored()).isFalse();
});
assertThat(calls.get()).isEqualTo(5);
} finally { server.stop(0); }
}
@Test void internalWireContractAuthenticatesAndDoesNotRetry()throws Exception{
var server=com.sun.net.httpserver.HttpServer.create(new java.net.InetSocketAddress("127.0.0.1",0),0);var calls=new java.util.concurrent.atomic.AtomicInteger();var header=new java.util.concurrent.atomic.AtomicReference<String>();var path=new java.util.concurrent.atomic.AtomicReference<String>();
server.createContext("/internal/lastleg/self-operated-analytics/quantity-diagnostics/",exchange->{calls.incrementAndGet();header.set(exchange.getRequestHeaders().getFirst("X-Internal-Service-Token"));path.set(exchange.getRequestURI().getPath());exchange.getRequestBody().readAllBytes();byte[] payload="{\"code\":\"200\",\"data\":null}".getBytes(java.nio.charset.StandardCharsets.UTF_8);exchange.sendResponseHeaders(200,payload.length);exchange.getResponseBody().write(payload);exchange.close();});server.start();
try{
var props=new com.baiyue.integration.config.LingxingEgressProperties(com.baiyue.integration.dto.lingxing.LingxingEgressDtos.CollectionMode.OFF,"test",null,null,null,java.net.URI.create("http://127.0.0.1:"+server.getAddress().getPort()),null,null,false);
String token="synthetic-internal-token-000000000000";var sink=new LingxingQuantityHttpSink(props,new com.fasterxml.jackson.databind.ObjectMapper().findAndRegisterModules(),token);
assertThat(sink.status(new LingxingQuantityEvidence.Status("account","producer",true,false))).isNull();assertThat(header.get()).isEqualTo(token);assertThat(path.get()).endsWith("/status");assertThat(calls.get()).isEqualTo(1);
sink.stop(new LingxingQuantityEvidence.Stop("diag","account","producer","INCOMPLETE"));assertThat(path.get()).endsWith("/stop");assertThat(calls.get()).isEqualTo(2);
}finally{server.stop(0);}
}
@Test void bodyLimitCancelsBeforeRetainingOversizedResponse(){
var body=new LingxingQuantityHttpSink.LimitedBody(16);boolean[] cancelled={false};body.onSubscribe(new Flow.Subscription(){public void request(long n){}public void cancel(){cancelled[0]=true;}});
body.onNext(List.of(ByteBuffer.wrap(new byte[8])));body.onNext(List.of(ByteBuffer.wrap(new byte[9])));
assertThat(cancelled[0]).isTrue();assertThat(body.getBody().toCompletableFuture()).isCompletedExceptionally();
}
@Test void hundredFullUnicodeKeysFitTheBoundedResponse()throws Exception{
var keys=new java.util.ArrayList<LingxingQuantityEvidence.Key>();for(int i=0;i<100;i++)keys.add(new LingxingQuantityEvidence.Key("倉".repeat(64),"客".repeat(128),"単".repeat(125)+i,false));
byte[] payload=new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsBytes(java.util.Map.of("code","200","data",java.util.Map.of("selected",keys)));
assertThat(payload.length).isGreaterThan(65536).isLessThan(262144);
var body=new LingxingQuantityHttpSink.LimitedBody(262144);body.onSubscribe(new Flow.Subscription(){public void request(long n){}public void cancel(){}});body.onNext(List.of(ByteBuffer.wrap(payload)));body.onComplete();assertThat(body.getBody().toCompletableFuture().join()).isEqualTo(payload);
}
@Test void boundedSuccessfulBodyIsExact(){
var body=new LingxingQuantityHttpSink.LimitedBody(16);body.onSubscribe(new Flow.Subscription(){public void request(long n){}public void cancel(){}});body.onNext(List.of(ByteBuffer.wrap(new byte[]{1,2})));body.onComplete();assertThat(body.getBody().toCompletableFuture().join()).containsExactly(1,2);
}
}
@@ -55,7 +55,7 @@ class LingxingWmsClientTest {
@Test void observerFailureCannotChangeBusinessResponseOrIssueExtraRequests() {
var observer = org.mockito.Mockito.mock(LingxingQuantityCapture.class);
org.mockito.Mockito.doThrow(new IllegalStateException("diagnostic failure")).when(observer)
.observe(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.anyBoolean(),
.observe(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any(Operation.class),
org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
var c = client(); c.setQuantityCapture(observer);
assertThat(c.queryPage(query(1, 1), context()).records()).hasSize(1);
@@ -83,7 +83,10 @@ class LingxingWmsClientTest {
}
@Test void cancelledLocateOmitsTimeAndStatusAndRejectsFuzzyMatch() throws Exception {
response.set(page("current", "size", 1, 1, 1, 1, ORDER.replace("100", "99").replace("2026-10-01 01:00:00", "")));
var result = client().locate(new LocateRequest("DO-DEMO", 1, 1), context());
var observer = org.mockito.Mockito.mock(LingxingQuantityCapture.class);
var c = client(); c.setQuantityCapture(observer);
var result = c.locate(new LocateRequest("DO-DEMO", 1, 1), context());
org.mockito.Mockito.verifyNoInteractions(observer);
var data = mapper.readTree(body.get()).path("data");
assertThat(data.size()).isEqualTo(3);
assertThat(data.has("status")).isFalse(); assertThat(data.has("startTime")).isFalse();