fix(selfop): publish complete single-pass inventory observations

This commit is contained in:
2026-10-13 00:10:12 +08:00
parent 51c0d77fd4
commit ae1a809357
3 changed files with 26 additions and 26 deletions
@@ -54,18 +54,18 @@ class SelfopInventoryCollectorTest {
when(client.inventory(any(SelfopRequestContext.class), eq("A"), eq(2), eq(2))).thenReturn(page(2, 3, 2, stock("c", null)));
}
@Test void twoIdenticalScansValidateOneAttemptAndOnlyFirstScanWritesEvidence() {
@Test void oneCompleteObservationValidatesAttemptAndPersistsEachPageOnce() {
stableThreeRows();
assertTrue(collector.collect(task, run, lease));
assertEquals("VALIDATED", run.getStatus()); assertEquals("COMPLETE", run.getCoverageStatus());
assertEquals(3L, run.getSourceTotalCount()); assertEquals(3L, run.getUniqueObjectCount()); assertEquals(2, run.getPageCount());
verify(client, times(2)).inventory(any(SelfopRequestContext.class), eq("A"), eq(1), eq(2)); verify(client, times(2)).inventory(any(SelfopRequestContext.class), eq("A"), eq(2), eq(2));
verify(client).inventory(any(SelfopRequestContext.class), eq("A"), eq(1), eq(2)); verify(client).inventory(any(SelfopRequestContext.class), eq("A"), eq(2), eq(2));
var capture = ArgumentCaptor.forClass(SelfopSyncStage.class);
verify(stages, times(3)).insert(capture.capture());
assertTrue(capture.getAllValues().stream().allMatch(s -> s.getRunId() == 10L && s.getStageBatchId() == 10L && s.getLastFetchRunId() == 10L));
assertNotNull(run.getFetchFinishedAtUtc());
var contexts=ArgumentCaptor.forClass(SelfopRequestContext.class);
verify(client,times(4)).inventory(contexts.capture(),eq("A"),anyInt(),eq(2));
verify(client,times(2)).inventory(contexts.capture(),eq("A"),anyInt(),eq(2));
assertTrue(contexts.getAllValues().stream().allMatch(c->"demo".equals(c.sourceAccount())&&"BUSINESS".equals(c.executionPurpose())
&&Objects.equals(c.taskId(),20L)&&Objects.equals(c.runId(),10L)&&Objects.equals(c.rootTaskId(),20L)));
}
@@ -112,28 +112,31 @@ class SelfopInventoryCollectorTest {
assertTrue(restarted.collect(task, run, lease));
var capture = ArgumentCaptor.forClass(SelfopSyncStage.class); verify(stages, times(3)).insert(capture.capture());
assertTrue(capture.getAllValues().stream().allMatch(s -> Objects.equals(s.getStageBatchId(), 10L)));
verify(client, times(2)).inventory(any(SelfopRequestContext.class), eq("A"), eq(1), eq(2)); verify(client, times(2)).inventory(any(SelfopRequestContext.class), eq("A"), eq(2), eq(2));
verify(client).inventory(any(SelfopRequestContext.class), eq("A"), eq(1), eq(2)); verify(client).inventory(any(SelfopRequestContext.class), eq("A"), eq(2), eq(2));
assertEquals("COMPLETE", run.getCoverageStatus());
}
@Test void secondScanCanYieldAndResumeWithoutDuplicatingFirstScanStages() {
stableThreeRows(); config.setRequestBudget(3);
assertFalse(collector.collect(task, run, lease));
var cursor = read(task.getRoutingManifestJson(), SelfopRoutingManifest.class);
assertEquals(2, cursor.getScan()); assertEquals(2, cursor.getNextPage()); assertEquals(2, cursor.getObjects().size());
config.setRequestBudget(10); assertTrue(collector.collect(task, run, lease));
verify(stages, times(3)).insert(any(SelfopSyncStage.class));
verify(client, times(2)).inventory(any(SelfopRequestContext.class), eq("A"), eq(1), eq(2)); verify(client, times(2)).inventory(any(SelfopRequestContext.class), eq("A"), eq(2), eq(2));
}
@Test void secondScanChangedQuantityCannotPassByKeepingSameIdentitySet() {
when(client.inventory(any(SelfopRequestContext.class), eq("A"), eq(1), eq(2))).thenReturn(page(1, 1, 1, stock("a", BigDecimal.ONE)),
page(1, 1, 1, stock("a", BigDecimal.TEN)));
assertEquals("SELFOP_STOCK_SCAN_UNSTABLE", assertThrows(IllegalStateException.class,
@Test void missingRowCannotCompleteObservation() {
when(client.inventory(any(SelfopRequestContext.class), eq("A"), eq(1), eq(2))).thenReturn(page(1, 3, 2, stock("a", BigDecimal.ONE), stock("b", BigDecimal.ZERO)));
when(client.inventory(any(SelfopRequestContext.class), eq("A"), eq(2), eq(2))).thenReturn(page(2, 3, 2));
assertEquals("SELFOP_STOCK_COUNT_MISMATCH", assertThrows(IllegalStateException.class,
() -> collector.collect(task, run, lease)).getMessage());
assertIncompleteAttemptWithPersistedProgress();
}
@Test void equivalentScientificAndDecimalQuantitiesPassBothScans() {
@Test void completeObservationDoesNotReReadChangingSource() {
var started = run.getFetchStartedAtUtc();
when(client.inventory(any(SelfopRequestContext.class), eq("A"), eq(1), eq(2))).thenReturn(page(1, 1, 1, stock("a", BigDecimal.ONE)),
page(1, 1, 1, stock("a", BigDecimal.TEN)));
assertTrue(collector.collect(task, run, lease));
verify(client).inventory(any(SelfopRequestContext.class), eq("A"), eq(1), eq(2));
var captured = ArgumentCaptor.forClass(SelfopSyncStage.class); verify(stages).insert(captured.capture());
assertEquals(BigDecimal.ONE, read(captured.getValue().getPayloadJson(), SelfopPayloads.Stock.class).availableQuantity());
assertEquals(started, run.getFetchStartedAtUtc());
assertFalse(run.getFetchFinishedAtUtc().isBefore(started));
assertTrue(read(task.getRoutingManifestJson(), SelfopRoutingManifest.class).isDirectoryComplete());
}
@Test void scientificQuantityPersistsCanonicalFingerprint() {
when(client.inventory(any(SelfopRequestContext.class),eq("A"),eq(1),eq(2))).thenReturn(
page(1,1,1,stock("a",new BigDecimal("1E+2"))),page(1,1,1,stock("a",new BigDecimal("100.0"))));
page(1,1,1,stock("a",new BigDecimal("1E+2"))));
assertTrue(collector.collect(task,run,lease));
assertEquals("COMPLETE",run.getCoverageStatus());
var captured=ArgumentCaptor.forClass(SelfopSyncStage.class);verify(stages).insert(captured.capture());
@@ -94,7 +94,7 @@ class SelfopInventoryRecoveryTest {
assertTrue(collector.collect(task, fresh, lease));
var saved = ArgumentCaptor.forClass(SelfopSyncStage.class); verify(stages).insert(saved.capture());
assertEquals(fresh.getId(), saved.getValue().getStageBatchId()); assertEquals(fresh.getId(), saved.getValue().getRunId());
assertEquals(1, saved.getValue().getFirstPageNo()); verify(client, times(2)).inventory(any(SelfopRequestContext.class), eq("A"), eq(1), eq(2));
assertEquals(1, saved.getValue().getFirstPageNo()); verify(client).inventory(any(SelfopRequestContext.class), eq("A"), eq(1), eq(2));
}
@Test void exhaustedFailuresRetainOldPublishedPointerAndCannotCreateAnotherAttempt() {