feat(self-operated): persist continuous collection and frozen coverage

This commit is contained in:
2026-10-13 21:13:31 +08:00
parent c2bb769676
commit 242a76221e
45 changed files with 1021 additions and 118 deletions
@@ -7,12 +7,23 @@ import java.util.List;
import static org.assertj.core.api.Assertions.*;
class LingxingEgressPropertiesTest {
@Test void continuousBusinessFlagBindsOnlyWhenExplicitlyEnabled() {
String prefix = "baiyue.integration.lingxing-wms.egress";
var input = new java.util.HashMap<String, String>();
input.put(prefix + ".collection-mode", "BUSINESS");
var source = new org.springframework.boot.context.properties.source.MapConfigurationPropertySource(input);
var binder = new org.springframework.boot.context.properties.bind.Binder(source);
var target = org.springframework.boot.context.properties.bind.Bindable.of(LingxingEgressProperties.class);
assertThat(binder.bind(prefix, target).get().continuousBusinessCollectionEnabled()).isFalse();
source.put(prefix + ".continuous-business-collection-enabled", "true");
assertThat(binder.bind(prefix, target).get().continuousBusinessCollectionEnabled()).isTrue();
}
private LingxingEgressProperties properties(CollectionMode mode, Duration interval, Duration cooldown) {
return new LingxingEgressProperties(mode, "test", null, null, null, null, interval, cooldown);
return new LingxingEgressProperties(mode, "test", null, null, null, null, interval, cooldown, false);
}
@Test void businessAcceptsPositiveMillisecondIntervalsIncludingProbeSteps() {
for (long millis : new long[]{1, 1500, 2000, 3000, 5000, 300000}) {
for (long millis : new long[]{1, 1200, 1500, 2000, 3000, 5000, 300000}) {
assertThat(properties(CollectionMode.BUSINESS, Duration.ofMillis(millis), null).minStartInterval())
.isEqualTo(Duration.ofMillis(millis));
}
@@ -54,7 +54,88 @@ class LingxingEgressGateTest {
private LingxingEgressGate gate() { return new LingxingEgressGate(props(CollectionMode.VALIDATION, "test"), ledger, clock, clock::advanceMillis); }
static EgressContext context(String attempt) { return new EgressContext("demo-account", CollectionMode.VALIDATION, "root", "session", attempt, "demo-grant"); }
static LingxingEgressProperties props(CollectionMode mode, String environment) {
return new LingxingEgressProperties(mode, environment, "demo-account", "demo-grant", NOW.plusSeconds(3600), URI.create("http://127.0.0.1"), null, null);
return new LingxingEgressProperties(mode, environment, "demo-account", "demo-grant", NOW.plusSeconds(3600), URI.create("http://127.0.0.1"), null, null, false);
}
private LingxingEgressGate continuousGate(CollectionMode mode, String grant, Instant until) {
return new LingxingEgressGate(new LingxingEgressProperties(mode, "test", "demo-account", grant, until,
URI.create("http://127.0.0.1"), null, null, true), ledger, clock, clock::advanceMillis);
}
@Test void continuousBusinessCrossesOldGrantExpiryButKeepsFiniteSingleUsePermits() {
var gate = continuousGate(CollectionMode.BUSINESS, "demo-grant", NOW.minusSeconds(1));
assertThat(gate.execute(businessContext("continuous"), Operation.WAREHOUSES, HASH,
h -> { h.startHttp(); return h.metadata(); }).actualHttpAttempts()).isEqualTo(1);
clock.advanceMillis(86400000);
var restarted = continuousGate(CollectionMode.BUSINESS, "demo-grant", null);
assertThat(restarted.execute(businessContext("next-day"), Operation.WAREHOUSES, HASH,
h -> { h.startHttp(); return h.metadata(); }).actualHttpAttempts()).isEqualTo(1);
ledger.mutation = p -> new AttemptPermit(p.context(), p.operation(), p.requestHash(), 1,
clock.instant().plusSeconds(31), true, true);
assertThatThrownBy(() -> restarted.execute(businessContext("too-long"), Operation.WAREHOUSES, HASH, h -> true))
.hasMessageContaining("BUDGET_PERMIT_INVALID");
assertThat(ledger.completions).hasSize(2);
}
@Test void continuousFlagNeverReplacesMatchingGrantOrFiniteValidationWindow() {
for (String grant : Arrays.asList(null, "", "other-grant")) {
var gate = continuousGate(CollectionMode.BUSINESS, grant, null);
assertThatThrownBy(() -> gate.execute(businessContext("missing-grant"), Operation.WAREHOUSES, HASH, h -> true))
.hasMessageContaining("EXCLUSIVE_WINDOW_UNCONFIRMED");
}
for (Instant until : Arrays.asList(null, NOW)) {
var gate = continuousGate(CollectionMode.VALIDATION, "demo-grant", until);
assertThatThrownBy(() -> gate.execute(context("validation"), Operation.WAREHOUSES, HASH, h -> true))
.hasMessageContaining("EXCLUSIVE_WINDOW_UNCONFIRMED");
}
assertThat(ledger.authorizations).isZero();
}
@Test void continuousBusinessStillPausesOnUnconfirmedCompletion() {
var gate = continuousGate(CollectionMode.BUSINESS, "demo-grant", null);
ledger.failComplete = true;
assertThatThrownBy(() -> gate.execute(businessContext("lost"), Operation.WAREHOUSES, HASH,
h -> { h.startHttp(); return true; })).hasMessageContaining("BUDGET_COMPLETION_UNCONFIRMED");
ledger.failComplete = false; clock.advanceMillis(86400000);
assertThatThrownBy(() -> gate.execute(businessContext("after-lost"), Operation.WAREHOUSES, HASH, h -> true))
.hasMessageContaining("ACCOUNT_PAUSED");
assertThat(ledger.authorizations).isEqualTo(1);
}
@Test void disabledContinuousBusinessStillRequiresUnexpiredGrant() {
var gate = new LingxingEgressGate(new LingxingEgressProperties(CollectionMode.BUSINESS, "test",
"demo-account", "demo-grant", NOW, URI.create("http://127.0.0.1"), Duration.ofMillis(1200), null, false),
ledger, clock, clock::advanceMillis);
assertThatThrownBy(() -> gate.execute(businessContext("disabled"), Operation.WAREHOUSES, HASH, h -> true))
.hasMessageContaining("EXCLUSIVE_WINDOW_UNCONFIRMED");
assertThat(ledger.authorizations).isZero();
}
@Test void continuousBusinessRetains1200MillisSpacingAndOneHttpPerPermit() {
var gate = new LingxingEgressGate(new LingxingEgressProperties(CollectionMode.BUSINESS, "test",
"demo-account", "demo-grant", null, URI.create("http://127.0.0.1"), Duration.ofMillis(1200), null, true),
ledger, clock, clock::advanceMillis);
gate.execute(businessContext("first-spaced"), Operation.WAREHOUSES, HASH, h -> { h.startHttp(); return true; });
gate.execute(businessContext("second-spaced"), Operation.WAREHOUSES, HASH, h -> { h.startHttp(); return true; });
assertThat(Duration.between(NOW, clock.instant()).toMillis()).isGreaterThanOrEqualTo(1200);
assertThatThrownBy(() -> gate.execute(businessContext("one-send"), Operation.WAREHOUSES, HASH,
h -> { h.startHttp(); h.startHttp(); return true; })).hasMessageContaining("ATTEMPT_NOT_SENDABLE");
assertThat(ledger.completions).hasSize(3).allSatisfy(c -> assertThat(c.actualHttpAttempts()).isEqualTo(1));
}
@Test void continuousBusinessCannotSendWithExpiredPermitAfterSigning() {
var gate = continuousGate(CollectionMode.BUSINESS, "demo-grant", null);
assertThatThrownBy(() -> gate.execute(businessContext("expired-permit"), Operation.OUTBOUND_DETAIL, HASH,
h -> { clock.advanceMillis(30000); h.startHttp(); return true; })).hasMessageContaining("ATTEMPT_NOT_SENDABLE");
assertThat(ledger.completions).singleElement().satisfies(c -> assertThat(c.actualHttpAttempts()).isZero());
}
@Test void continuousBusinessRateCooldownStillRequiresFreshAuthorityApproval() {
var gate = continuousGate(CollectionMode.BUSINESS, "demo-grant", null);
assertThrows(LingxingWmsException.class, () -> gate.execute(businessContext("continuous-rate"), Operation.WAREHOUSES, HASH,
h -> { h.startHttp(); h.rateLimited(null); throw rateLimit(); }));
assertThatThrownBy(() -> gate.execute(businessContext("during-cooldown"), Operation.WAREHOUSES, HASH, h -> true))
.hasMessageContaining("ACCOUNT_PAUSED");
clock.advanceMillis(300000);ledger.denial = new AuthorizationDenial("ACCOUNT_REQUIRES_REVIEW", false, null);
assertThatThrownBy(() -> gate.execute(businessContext("authority-denied"), Operation.WAREHOUSES, HASH, h -> true))
.hasMessageContaining("BUDGET_AUTHORIZATION_DENIED");
assertThat(ledger.completions).hasSize(1);
ledger.denial = null;
assertThat(gate.execute(businessContext("fresh-approved"), Operation.WAREHOUSES, HASH,
h -> { h.startHttp(); return h.metadata(); }).accountPaused()).isFalse();
assertThat(ledger.completions).hasSize(2);
}
@Test void failureCompletionPreservesNullableTransportAndSourceCodes() {
for (Integer status : new Integer[] {null, 503}) {
@@ -72,8 +153,9 @@ class LingxingEgressGateTest {
}
}
@Test void defaultOffAndUnknownBusinessEnvironmentsNeverAuthorize() {
var defaults = new LingxingEgressProperties(null, null, null, null, null, null, null, null);
var defaults = new LingxingEgressProperties(null, null, null, null, null, null, null, null, false);
assertThat(defaults.collectionMode()).isEqualTo(CollectionMode.OFF);
assertThat(defaults.continuousBusinessCollectionEnabled()).isFalse();
assertThat(defaults.minStartInterval()).isEqualTo(Duration.ofSeconds(5));
var off = new LingxingEgressGate(defaults, ledger, clock, clock::advanceMillis);
assertThatThrownBy(() -> off.execute(context("a"), Operation.WAREHOUSES, HASH, h -> { h.startHttp(); return true; })).hasMessageContaining("EGRESS_OFF");
@@ -193,7 +275,7 @@ class LingxingEgressGateTest {
} finally { release.countDown(); executor.shutdownNow(); }
}
@Test void intervalCannotBeConfiguredBelowFiveSecondsAndPropertiesHideGrant() {
assertThatThrownBy(() -> new LingxingEgressProperties(CollectionMode.VALIDATION, null, null, null, null, null, Duration.ofMillis(4999), null)).isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> new LingxingEgressProperties(CollectionMode.VALIDATION, null, null, null, null, null, Duration.ofMillis(4999), null, false)).isInstanceOf(IllegalArgumentException.class);
assertThat(props(CollectionMode.VALIDATION, "test").toString()).doesNotContain("demo-grant", "demo-account", "127.0.0.1");
}
@Test void knownBudgetDenialDoesNotPermanentlyPauseOrConsumeHttp() {
@@ -43,7 +43,7 @@ class LingxingHttpBudgetAuthorityTest {
@AfterEach void stop() { server.stop(0); }
private LingxingHttpBudgetAuthority authority(String serviceToken) {
return new LingxingHttpBudgetAuthority(new LingxingEgressProperties(CollectionMode.VALIDATION, "test", "demo-account", "demo-grant",
LingxingEgressGateTest.NOW.plusSeconds(3600), URI.create("http://127.0.0.1:" + server.getAddress().getPort()), null, null), mapper, serviceToken);
LingxingEgressGateTest.NOW.plusSeconds(3600), URI.create("http://127.0.0.1:" + server.getAddress().getPort()), null, null, false), mapper, serviceToken);
}
private CompletionRequest completion() { return new CompletionRequest(request.context(), request.operation(), request.requestHash(), 1, null, 200, 200, false, null); }
private String permit() throws Exception { return mapper.writeValueAsString(new AttemptPermit(request.context(), request.operation(), request.requestHash(), 1,
@@ -86,7 +86,7 @@ class LingxingHttpBudgetAuthorityTest {
}
@Test void missingServerConfiguredAuthorityPreventsHttp() {
var authority = new LingxingHttpBudgetAuthority(new LingxingEgressProperties(CollectionMode.VALIDATION, "test", "demo-account", "demo-grant",
LingxingEgressGateTest.NOW.plusSeconds(3600), null, null, null), mapper, "synthetic-token");
LingxingEgressGateTest.NOW.plusSeconds(3600), null, null, null, false), mapper, "synthetic-token");
assertThrows(LingxingWmsException.class, () -> authority.authorize(request)); assertThat(calls.get()).isZero();
}
@Test void permitRejectsCoercedBooleansExtraFieldsAndInvalidAttemptCount() throws Exception {
@@ -254,7 +254,7 @@ class LingxingWmsClientTest {
var p = new LingxingWmsProperties(true, URI.create("http://127.0.0.1:" + server.getAddress().getPort()),
"synthetic-key", "synthetic-secret", Duration.ofSeconds(2), Duration.ofSeconds(2));
var props = new LingxingEgressProperties(mode, "production", "demo-account", grant ? "demo-grant" : null,
NOW.plusSeconds(3600), budget ? URI.create("http://127.0.0.1") : null, null, null);
NOW.plusSeconds(3600), budget ? URI.create("http://127.0.0.1") : null, null, null, false);
var ledger = new LingxingEgressGateTest.LedgerAuthority(clock) {
@Override public void complete(CompletionRequest request) { super.complete(request); completion.set(request); }
};