fix(shadow): align outbound predicates with site data for v3.3
Use the business project and captured site predicates, validate single-stock proposals, and skip unsupported scopes without changing Legacy. Add the guarded V3.3 SQL upgrade, CMD operations, regression tests and deployment runbook.
This commit is contained in:
@@ -13,6 +13,7 @@ import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
@@ -29,6 +30,7 @@ public class ShadowPlanningEngine {
|
||||
private static final int RESULT_REASON_MAX_LENGTH = 600;
|
||||
private static final int NUMBER_MAX_LENGTH = 64;
|
||||
private static final String PUBLISHED = "PUBLISHED";
|
||||
private static final int MAX_OUTBOUND_CANDIDATES = 200;
|
||||
|
||||
private final SchedulingFacade schedulingFacade;
|
||||
private final ShadowInputMapper inputMapper;
|
||||
@@ -87,19 +89,83 @@ public class ShadowPlanningEngine {
|
||||
String warehouseId = snapshot.warehouseId();
|
||||
ShadowStockReference reference = inputMapper.selectProjectOutboundReference(
|
||||
warehouseId, snapshot.generation(), stockoutDetailId);
|
||||
if (reference == null || isBlank(reference.getProjectNo())) {
|
||||
return OutboundShadowObservation.unavailable(snapshot, stockoutDetailId,
|
||||
"SKIPPED_MISSING_PROJECT",
|
||||
outboundDiagnostic(snapshot, stockoutDetailId, reference,
|
||||
"No project number was available for shadow planning"));
|
||||
if (reference == null || !Integer.valueOf(1).equals(reference.getPredicateVersion())) {
|
||||
return unavailableProject(snapshot, stockoutDetailId, reference,
|
||||
"SKIPPED_MISSING_PREDICATE_INPUT", "Request is missing or predates predicate version 1");
|
||||
}
|
||||
if (reference.getRequestedQuantity() == null || reference.getRequestedQuantity().signum() <= 0) {
|
||||
return unavailableProject(snapshot, stockoutDetailId, reference,
|
||||
"SKIPPED_INVALID_REMAINING_QUANTITY", "No positive unallocated quantity was captured");
|
||||
}
|
||||
List<ShadowStockReference> candidates = inputMapper.selectProjectOutboundCandidates(
|
||||
warehouseId, snapshot.generation(), stockoutDetailId);
|
||||
if (candidates.size() > MAX_OUTBOUND_CANDIDATES) {
|
||||
return unavailableProject(snapshot, stockoutDetailId, reference,
|
||||
"SKIPPED_CANDIDATE_LIMIT", "Eligible candidate set exceeds 200 rows");
|
||||
}
|
||||
if (candidates.isEmpty()) {
|
||||
return unavailableProject(snapshot, stockoutDetailId, reference,
|
||||
"ERROR_STOCK_NOT_FOUND", "No inventory satisfies the captured outbound predicates");
|
||||
}
|
||||
var projects = candidates.stream().map(ShadowStockReference::getProjectNo).distinct().toList();
|
||||
if (projects.size() != 1 || isBlank(projects.get(0))) {
|
||||
return unavailableProject(snapshot, stockoutDetailId, reference,
|
||||
"SKIPPED_UNSUPPORTED_PROJECT_SCOPE", "Planner requires one nonblank candidate project");
|
||||
}
|
||||
var products = candidates.stream().map(ShadowStockReference::getProductId).distinct().toList();
|
||||
if (products.size() != 1 || isBlank(products.get(0))) {
|
||||
return unavailableProject(snapshot, stockoutDetailId, reference,
|
||||
"SKIPPED_UNSUPPORTED_PRODUCT_SCOPE", "Planner requires one nonblank candidate product");
|
||||
}
|
||||
// The shared planner is single-source and does not apply the site's optional predicates.
|
||||
// Keep it unchanged; validate its proposal against the generation-qualified candidate set.
|
||||
OutboundRequest request = new OutboundRequest(stockoutDetailId, null,
|
||||
reference.getProjectNo(), reference.getProductId(), null, null, null, true);
|
||||
return toOutboundObservation(snapshot, reference.getRequestId(), reference,
|
||||
schedulingFacade.planOutboundFromPublishedSnapshot(warehouseId, request));
|
||||
projects.get(0), products.get(0), null, null, null, false);
|
||||
OutboundPlan plan = schedulingFacade.planOutboundFromPublishedSnapshot(warehouseId, request);
|
||||
if (plan.assignment().isEmpty()) {
|
||||
return toOutboundObservation(snapshot, stockoutDetailId, reference, plan);
|
||||
}
|
||||
var assignment = plan.assignment().orElseThrow();
|
||||
var selected = candidates.stream().filter(candidate ->
|
||||
Objects.equals(candidate.getSpaceId(), assignment.sourceSpaceId())
|
||||
&& Objects.equals(candidate.getTrayNo(), assignment.trayNo())
|
||||
&& Objects.equals(candidate.getProjectNo(), assignment.projectNo())
|
||||
&& Objects.equals(candidate.getProductId(), assignment.productId())).toList();
|
||||
if (selected.size() != 1) {
|
||||
return unavailableProject(snapshot, stockoutDetailId, reference,
|
||||
"SKIPPED_INELIGIBLE_PROPOSAL", "Proposed source is not one unambiguous eligible stock row");
|
||||
}
|
||||
ShadowStockReference stock = selected.get(0);
|
||||
if (!Integer.valueOf(0).equals(stock.getTrayJobCount())) {
|
||||
return unavailableProject(snapshot, stockoutDetailId, reference,
|
||||
"SKIPPED_EXISTING_TRAY_JOB", "Existing tray jobs require unsupported delivery compatibility checks");
|
||||
}
|
||||
if (!Integer.valueOf(1).equals(stock.getTrayItemCount())) {
|
||||
return unavailableProject(snapshot, stockoutDetailId, reference,
|
||||
"SKIPPED_MULTI_STOCK_TRAY", "Selected tray does not contain exactly one inventory row");
|
||||
}
|
||||
if (stock.getAvailableQuantity() == null
|
||||
|| stock.getAvailableQuantity().compareTo(reference.getRequestedQuantity()) < 0) {
|
||||
return unavailableProject(snapshot, stockoutDetailId, reference,
|
||||
"SKIPPED_MULTI_STOCK_REQUEST", "Selected single stock cannot cover the remaining request quantity");
|
||||
}
|
||||
StringBuilder reason = new StringBuilder(outboundDiagnostic(snapshot, stockoutDetailId, reference,
|
||||
assignment.reason()));
|
||||
appendField(reason, "selectedProject", stock.getProjectNo(), WIDE_IDENTIFIER_MAX_LENGTH);
|
||||
appendField(reason, "selectedTray", stock.getTrayNo(), WIDE_IDENTIFIER_MAX_LENGTH);
|
||||
appendField(reason, "availableQuantity", objectOrEmpty(stock.getAvailableQuantity()), NUMBER_MAX_LENGTH);
|
||||
return new OutboundShadowObservation(true, snapshot.generation(), warehouseId,
|
||||
snapshot.synchronizedAt(), snapshot.sourceSignature(), stockoutDetailId,
|
||||
"READY", stock.getStockId(), stock.getSpaceId(), reason.toString());
|
||||
});
|
||||
}
|
||||
|
||||
private OutboundShadowObservation unavailableProject(PublishedShadowSnapshot snapshot, String requestId,
|
||||
ShadowStockReference reference, String code, String reason) {
|
||||
return OutboundShadowObservation.unavailable(snapshot, requestId, code,
|
||||
outboundDiagnostic(snapshot, requestId, reference, reason));
|
||||
}
|
||||
|
||||
public OutboundShadowObservation planExactStockOutbound(PublishedShadowSnapshot snapshot,
|
||||
String stockoutDetailId, String stockId) {
|
||||
return planWithStableSnapshot(snapshot, () -> {
|
||||
@@ -313,6 +379,15 @@ public class ShadowPlanningEngine {
|
||||
reference == null ? null : reference.getSnapshotSynchronizedAt(), SNAPSHOT_SYNC_MAX_LENGTH);
|
||||
appendField(diagnostic, "shadowMatches", numberOrUnknown(
|
||||
reference == null ? null : reference.getShadowMatchCount()), NUMBER_MAX_LENGTH);
|
||||
appendField(diagnostic, "predicateVersion", numberOrUnknown(
|
||||
reference == null ? null : reference.getPredicateVersion()), NUMBER_MAX_LENGTH);
|
||||
if (reference != null && reference.getPredicateVersion() != null) {
|
||||
appendField(diagnostic, "spare1", reference.getSpare1(), 120);
|
||||
appendField(diagnostic, "spare2", reference.getSpare2(), 120);
|
||||
appendField(diagnostic, "spare3", reference.getSpare3(), 120);
|
||||
appendField(diagnostic, "voltage", reference.getVoltage(), 120);
|
||||
appendField(diagnostic, "phase", reference.getPhase(), 120);
|
||||
}
|
||||
appendField(diagnostic, "reason", resultReason, RESULT_REASON_MAX_LENGTH);
|
||||
return diagnostic.toString();
|
||||
}
|
||||
|
||||
@@ -13,6 +13,35 @@ public class ShadowStockReference {
|
||||
private String snapshotSynchronizedAt;
|
||||
private BigDecimal requestedQuantity;
|
||||
private Integer shadowMatchCount;
|
||||
private Integer predicateVersion;
|
||||
private String spare1;
|
||||
private String spare2;
|
||||
private String spare3;
|
||||
private String voltage;
|
||||
private String phase;
|
||||
private BigDecimal availableQuantity;
|
||||
private Integer trayItemCount;
|
||||
private Integer trayJobCount;
|
||||
|
||||
public Integer getTrayJobCount() { return trayJobCount; }
|
||||
public void setTrayJobCount(Integer value) { trayJobCount = value; }
|
||||
|
||||
public Integer getPredicateVersion() { return predicateVersion; }
|
||||
public void setPredicateVersion(Integer value) { predicateVersion = value; }
|
||||
public String getSpare1() { return spare1; }
|
||||
public void setSpare1(String value) { spare1 = value; }
|
||||
public String getSpare2() { return spare2; }
|
||||
public void setSpare2(String value) { spare2 = value; }
|
||||
public String getSpare3() { return spare3; }
|
||||
public void setSpare3(String value) { spare3 = value; }
|
||||
public String getVoltage() { return voltage; }
|
||||
public void setVoltage(String value) { voltage = value; }
|
||||
public String getPhase() { return phase; }
|
||||
public void setPhase(String value) { phase = value; }
|
||||
public BigDecimal getAvailableQuantity() { return availableQuantity; }
|
||||
public void setAvailableQuantity(BigDecimal value) { availableQuantity = value; }
|
||||
public Integer getTrayItemCount() { return trayItemCount; }
|
||||
public void setTrayItemCount(Integer value) { trayItemCount = value; }
|
||||
|
||||
public String getRequestId() {
|
||||
return requestId;
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ShadowInputMapper {
|
||||
@@ -21,6 +22,9 @@ public interface ShadowInputMapper {
|
||||
ShadowStockReference selectStockReference(@Param("warehouseId") String warehouseId,
|
||||
@Param("generation") long generation, @Param("stockId") String stockId);
|
||||
|
||||
List<ShadowStockReference> selectProjectOutboundCandidates(@Param("warehouseId") String warehouseId,
|
||||
@Param("generation") long generation, @Param("stockoutDetailId") String stockoutDetailId);
|
||||
|
||||
boolean claimInstance(@Param("warehouseId") String warehouseId,
|
||||
@Param("instanceId") String instanceId, @Param("staleSeconds") int staleSeconds);
|
||||
|
||||
|
||||
@@ -25,6 +25,29 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
and input.tray_no = #{trayNo}
|
||||
</select>
|
||||
|
||||
<sql id="outboundCandidatePredicate">
|
||||
item.warehouse_id = #{warehouseId}
|
||||
and item.snapshot_generation = #{generation}
|
||||
and cargo.warehouse_id = item.warehouse_id
|
||||
and cargo.snapshot_generation = item.snapshot_generation
|
||||
and cargo.space_id = item.space_id and cargo.tray_code = item.tray_code
|
||||
and cargo.locked = 'N'
|
||||
and ((cargo.space_status = '2' and item.out_quantity = 0)
|
||||
or (cargo.space_status = '4' and item.quantity > item.out_quantity))
|
||||
and (nullif(input.project_no, '') is null or item.project_no = input.project_no)
|
||||
and (nullif(input.product_id, '') is null or item.product_no = input.product_id)
|
||||
and (nullif(input.spare1, '') is null or item.spare1 = input.spare1)
|
||||
and (nullif(input.spare2, '') is null or item.spare2 = input.spare2)
|
||||
and (nullif(input.spare3, '') is null or item.spare3 = input.spare3)
|
||||
and (nullif(input.voltage, '') is null or item.voltage = input.voltage)
|
||||
and (nullif(input.phase, '') is null or item.phase = input.phase)
|
||||
and not exists (
|
||||
select 1 from alg.active_job_snapshot job
|
||||
where job.warehouse_id = item.warehouse_id
|
||||
and job.snapshot_generation = item.snapshot_generation
|
||||
and job.tray_code = item.tray_code and job.in_or_out = '26')
|
||||
</sql>
|
||||
|
||||
<select id="selectProjectOutboundReference"
|
||||
resultType="com.ruoyi.algorithm.shadow.ShadowStockReference">
|
||||
select top (1)
|
||||
@@ -32,21 +55,42 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
input.raw_warehouse_id as warehouseId,
|
||||
input.project_no as projectNo,
|
||||
input.product_id as productId,
|
||||
input.requested_quantity as requestedQuantity,
|
||||
input.requested_quantity - input.allocated_quantity as requestedQuantity,
|
||||
input.predicate_version as predicateVersion,
|
||||
input.spare1, input.spare2, input.spare3, input.voltage, input.phase,
|
||||
convert(varchar(40), input.synchronized_at, 126) as snapshotSynchronizedAt,
|
||||
(select count(*)
|
||||
from alg.inventory_item_snapshot item
|
||||
where item.warehouse_id = #{warehouseId}
|
||||
and item.snapshot_generation = #{generation}
|
||||
and item.project_no = nullif(ltrim(rtrim(input.project_no)), '')
|
||||
and (nullif(ltrim(rtrim(input.product_id)), '') is null
|
||||
or item.product_no = nullif(ltrim(rtrim(input.product_id)), ''))
|
||||
and isnull(item.quantity, 0) > isnull(item.out_quantity, 0))
|
||||
join alg.cargo_space_snapshot cargo on cargo.space_id = item.space_id
|
||||
where <include refid="outboundCandidatePredicate"/>)
|
||||
as shadowMatchCount
|
||||
from alg.shadow_outbound_request_snapshot input
|
||||
where input.warehouse_id = #{warehouseId}
|
||||
and input.snapshot_generation = #{generation}
|
||||
and input.stockout_detail_id = #{stockoutDetailId}
|
||||
and input.predicate_version = 1
|
||||
</select>
|
||||
|
||||
<select id="selectProjectOutboundCandidates" resultType="com.ruoyi.algorithm.shadow.ShadowStockReference">
|
||||
select top (201)
|
||||
item.stock_id as stockId, item.tray_code as trayNo, item.space_id as spaceId,
|
||||
item.project_no as projectNo, item.product_no as productId,
|
||||
item.tray_job_count as trayJobCount,
|
||||
item.quantity - item.out_quantity as availableQuantity,
|
||||
(select count(*) from alg.inventory_item_snapshot trayItem
|
||||
where trayItem.warehouse_id = item.warehouse_id
|
||||
and trayItem.snapshot_generation = item.snapshot_generation
|
||||
and trayItem.space_id = item.space_id and trayItem.tray_code = item.tray_code)
|
||||
as trayItemCount
|
||||
from alg.shadow_outbound_request_snapshot input
|
||||
join alg.inventory_item_snapshot item on item.warehouse_id = input.warehouse_id
|
||||
join alg.cargo_space_snapshot cargo on cargo.space_id = item.space_id
|
||||
where input.warehouse_id = #{warehouseId}
|
||||
and input.snapshot_generation = #{generation}
|
||||
and input.stockout_detail_id = #{stockoutDetailId}
|
||||
and input.predicate_version = 1
|
||||
and <include refid="outboundCandidatePredicate"/>
|
||||
order by item.stock_id
|
||||
</select>
|
||||
|
||||
<select id="selectStockReference" resultType="com.ruoyi.algorithm.shadow.ShadowStockReference">
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
package com.ruoyi.algorithm.shadow;
|
||||
|
||||
import com.ruoyi.algorithm.shadow.mapper.ShadowInputMapper;
|
||||
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
|
||||
import org.apache.ibatis.mapping.Environment;
|
||||
import org.apache.ibatis.session.Configuration;
|
||||
import org.apache.ibatis.session.SqlSession;
|
||||
import org.apache.ibatis.session.SqlSessionFactory;
|
||||
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
|
||||
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
|
||||
import org.h2.jdbcx.JdbcDataSource;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.StringReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ShadowOutboundPredicateTest {
|
||||
private JdbcTemplate jdbc;
|
||||
private SqlSessionFactory sessions;
|
||||
|
||||
@AfterEach
|
||||
void closeDatabase() {
|
||||
jdbc.execute("shutdown");
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void createIsolatedSnapshot() throws Exception {
|
||||
JdbcDataSource dataSource = new JdbcDataSource();
|
||||
dataSource.setURL("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MSSQLServer;DB_CLOSE_DELAY=-1");
|
||||
jdbc = new JdbcTemplate(dataSource);
|
||||
jdbc.execute("create schema alg");
|
||||
jdbc.execute("""
|
||||
create table alg.shadow_outbound_request_snapshot (
|
||||
warehouse_id varchar(80), snapshot_generation bigint, stockout_detail_id varchar(80),
|
||||
raw_warehouse_id varchar(80), project_no varchar(255), product_id varchar(255),
|
||||
requested_quantity decimal(38,6), allocated_quantity decimal(38,6),
|
||||
spare1 varchar(255), spare2 varchar(255), spare3 varchar(255),
|
||||
voltage varchar(255), phase varchar(255), predicate_version int,
|
||||
synchronized_at datetime2)
|
||||
""");
|
||||
jdbc.execute("""
|
||||
create table alg.inventory_item_snapshot (
|
||||
warehouse_id varchar(80), snapshot_generation bigint, stock_id varchar(80),
|
||||
space_id varchar(80), tray_code varchar(255), project_no varchar(255),
|
||||
product_no varchar(255), quantity decimal(38,6), out_quantity decimal(38,6),
|
||||
spare1 varchar(255), spare2 varchar(255), spare3 varchar(255),
|
||||
voltage varchar(255), phase varchar(255), synchronized_at datetime2, tray_job_count int)
|
||||
""");
|
||||
jdbc.execute("""
|
||||
create table alg.cargo_space_snapshot (
|
||||
warehouse_id varchar(80), snapshot_generation bigint, space_id varchar(80),
|
||||
tray_code varchar(255), space_status varchar(20), locked varchar(20))
|
||||
""");
|
||||
jdbc.execute("""
|
||||
create table alg.active_job_snapshot (
|
||||
warehouse_id varchar(80), snapshot_generation bigint, job_id varchar(80),
|
||||
tray_code varchar(255), in_or_out varchar(20))
|
||||
""");
|
||||
jdbc.update("""
|
||||
insert into alg.shadow_outbound_request_snapshot values
|
||||
('01',459,'CKD202609070015-1','','AD.L-00033.01.01','030210200000506',5,2,
|
||||
'LEVEL','PARENT','TOP','220V','PCS',1,'2026-09-07 02:37:10')
|
||||
""");
|
||||
stock("MATCH", "AD.L-00033.01.01");
|
||||
Configuration configuration = new Configuration(new Environment("test", new JdbcTransactionFactory(), dataSource));
|
||||
try (InputStream input = getClass().getClassLoader().getResourceAsStream("mapper/algorithm/ShadowInputMapper.xml")) {
|
||||
// H2 lacks SQL Server's timestamp style argument. Candidate SQL is unchanged.
|
||||
String xml = new String(input.readAllBytes(), StandardCharsets.UTF_8)
|
||||
.replace("convert(varchar(40), input.synchronized_at, 126)", "cast(input.synchronized_at as varchar(40))")
|
||||
.replace("convert(varchar(40), item.synchronized_at, 126)", "cast(item.synchronized_at as varchar(40))");
|
||||
new XMLMapperBuilder(new StringReader(xml), configuration, "mapper/algorithm/ShadowInputMapper.xml",
|
||||
configuration.getSqlFragments()).parse();
|
||||
}
|
||||
sessions = new SqlSessionFactoryBuilder().build(configuration);
|
||||
}
|
||||
|
||||
@Test
|
||||
void capturedBusinessProjectNeverBecomesWarehouseIdentifier() {
|
||||
stock("WAREHOUSE-PROJECT", "FT546");
|
||||
ShadowStockReference input = reference();
|
||||
assertThat(input.getProjectNo()).isEqualTo("AD.L-00033.01.01");
|
||||
assertThat(input.getShadowMatchCount()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void allOptionalSitePredicatesRestrictCandidates() {
|
||||
for (String column : new String[]{"spare1", "spare2", "spare3", "voltage", "phase"}) {
|
||||
stock(column, "AD.L-00033.01.01");
|
||||
jdbc.update("update alg.inventory_item_snapshot set " + column + "='WRONG' where stock_id=?", column);
|
||||
}
|
||||
assertThat(reference().getShadowMatchCount()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullProjectDoesNotInventWarehouseProjectOrRemoveOtherFilters() {
|
||||
jdbc.update("update alg.shadow_outbound_request_snapshot set project_no=null");
|
||||
stock("OTHER-PROJECT", "9204");
|
||||
stock("WRONG-PRODUCT", "9204");
|
||||
jdbc.update("update alg.inventory_item_snapshot set product_no='OTHER' where stock_id='WRONG-PRODUCT'");
|
||||
assertThat(reference().getProjectNo()).isNull();
|
||||
assertThat(reference().getShadowMatchCount()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void absentOptionalAttributesPermitInventoryWithoutThoseAttributes() {
|
||||
jdbc.update("update alg.shadow_outbound_request_snapshot set project_no='',spare1=null,spare2='',spare3=null,voltage='',phase=null");
|
||||
jdbc.update("update alg.inventory_item_snapshot set project_no=null,spare1=null,spare2=null,spare3=null,voltage=null,phase=null");
|
||||
assertThat(reference().getShadowMatchCount()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void lockTrayAlignmentAndConsolidationJobAreRequired() {
|
||||
for (String id : new String[]{"LOCKED", "WRONG-TRAY", "CONSOLIDATING", "INSPECTION", "ALLOCATED"}) {
|
||||
stock(id, "AD.L-00033.01.01");
|
||||
}
|
||||
jdbc.update("update alg.cargo_space_snapshot set locked='Y' where space_id='LOCKED'");
|
||||
jdbc.update("update alg.cargo_space_snapshot set tray_code='OTHER' where space_id='WRONG-TRAY'");
|
||||
jdbc.update("insert into alg.active_job_snapshot values ('01',459,'JOB','CONSOLIDATING','26')");
|
||||
jdbc.update("update alg.cargo_space_snapshot set space_status='3' where space_id='INSPECTION'");
|
||||
jdbc.update("update alg.inventory_item_snapshot set out_quantity=1 where stock_id='ALLOCATED'");
|
||||
assertThat(reference().getShadowMatchCount()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusFourWithRemainingStockIsCountedButWrongGenerationsAreNot() {
|
||||
stock("PARTIAL", "AD.L-00033.01.01");
|
||||
jdbc.update("update alg.cargo_space_snapshot set space_status='4' where space_id='PARTIAL'");
|
||||
jdbc.update("update alg.inventory_item_snapshot set out_quantity=1 where stock_id='PARTIAL'");
|
||||
stock("OLD", "AD.L-00033.01.01");
|
||||
jdbc.update("update alg.cargo_space_snapshot set snapshot_generation=458 where space_id='OLD'");
|
||||
assertThat(reference().getShadowMatchCount()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void planningQuantityIsUnallocatedRemainder() {
|
||||
assertThat(reference().getRequestedQuantity()).isEqualByComparingTo("3");
|
||||
}
|
||||
|
||||
@Test
|
||||
void candidateListUsesSamePredicatesAndReturnsSourceQuantity() {
|
||||
stock("WRONG", "FT546");
|
||||
try (SqlSession session = sessions.openSession()) {
|
||||
var candidates = session.getMapper(ShadowInputMapper.class)
|
||||
.selectProjectOutboundCandidates("01", 459L, "CKD202609070015-1");
|
||||
assertThat(candidates).hasSize(1);
|
||||
assertThat(candidates.get(0).getStockId()).isEqualTo("MATCH");
|
||||
assertThat(candidates.get(0).getAvailableQuantity()).isEqualByComparingTo("5");
|
||||
assertThat(candidates.get(0).getTrayItemCount()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void leadingCharactersAreNotTrimmedAway() {
|
||||
jdbc.update("update alg.shadow_outbound_request_snapshot set spare1=' LEVEL'");
|
||||
assertThat(reference().getShadowMatchCount()).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void preUpgradeSnapshotCannotBeUsedAsNewPredicateEvidence() {
|
||||
jdbc.update("update alg.shadow_outbound_request_snapshot set predicate_version=null");
|
||||
assertThat(reference()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void migrationProjectionCapturesRawRequestFieldsWithoutWarehouseProject() throws Exception {
|
||||
createLegacyRequestFixture();
|
||||
jdbc.execute(snapshotInsert("shadow_outbound_request_snapshot", "EXEC alg.usp_rebuild_space_topology"));
|
||||
var input = reference();
|
||||
assertThat(input.getProjectNo()).isEqualTo("AD.L-00033.01.01");
|
||||
assertThat(input.getSpare1()).isEqualTo("LEVEL");
|
||||
assertThat(input.getSpare2()).isEqualTo("PARENT");
|
||||
assertThat(input.getSpare3()).isEqualTo("TOP");
|
||||
assertThat(input.getVoltage()).isEqualTo("220V");
|
||||
assertThat(input.getPhase()).isEqualTo("PCS");
|
||||
assertThat(input.getRequestedQuantity()).isEqualByComparingTo("3");
|
||||
assertThat(input.getPredicateVersion()).isEqualTo(1);
|
||||
assertThat(input.getShadowMatchCount()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void oversizedRawSiteParameterIsPreservedButNotAdmitted() throws Exception {
|
||||
createLegacyRequestFixture();
|
||||
jdbc.update("update dbo.out_stockout_detail set spare1=?", "X".repeat(81));
|
||||
jdbc.execute(snapshotInsert("shadow_outbound_request_snapshot", "EXEC alg.usp_rebuild_space_topology"));
|
||||
assertThat(reference()).isNull();
|
||||
assertThat(jdbc.queryForObject("select spare1 from alg.shadow_outbound_request_snapshot", String.class))
|
||||
.hasSize(81);
|
||||
assertThat(jdbc.queryForObject("select predicate_version from alg.shadow_outbound_request_snapshot", Integer.class))
|
||||
.isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void migrationInventoryProjectionCapturesEvenOtherWarehouseTrayJobs() throws Exception {
|
||||
jdbc.execute("create schema dbo");
|
||||
jdbc.execute("alter table alg.inventory_item_snapshot add lot_no varchar(80)");
|
||||
jdbc.execute("alter table alg.inventory_item_snapshot add inbound_time_raw varchar(80)");
|
||||
jdbc.execute("alter table alg.inventory_item_snapshot add inbound_at datetime2");
|
||||
jdbc.execute("delete from alg.inventory_item_snapshot");
|
||||
jdbc.execute("""
|
||||
create table dbo.bas_stock (stock_id varchar(80), cargo_space_id varchar(80), tray_code varchar(255),
|
||||
spare4 varchar(255), product_id varchar(255), lot_no varchar(80), in_date varchar(80),
|
||||
quantity decimal(30,4), out_quantity decimal(30,4), spare1 varchar(255), spare2 varchar(255),
|
||||
spare3 varchar(255), voltage varchar(255), phase varchar(255))
|
||||
""");
|
||||
jdbc.execute("create table dbo.bas_cargo_space (warehouse_id varchar(80), cargo_space_id varchar(80))");
|
||||
jdbc.execute("create table dbo.mydo_job (warehouse_id varchar(80), tray_code varchar(255))");
|
||||
jdbc.update("""
|
||||
insert into dbo.bas_stock values ('MATCH','MATCH','MATCH','AD.L-00033.01.01','030210200000506',
|
||||
null,'2026-09-05 16:39:14',5,0,'LEVEL','PARENT','TOP','220V','PCS')
|
||||
""");
|
||||
jdbc.update("insert into dbo.bas_cargo_space values ('01','MATCH')");
|
||||
jdbc.update("insert into dbo.mydo_job values ('OTHER-WAREHOUSE','MATCH')");
|
||||
jdbc.execute(snapshotInsert("inventory_item_snapshot", "INSERT INTO alg.active_job_snapshot"));
|
||||
try (SqlSession session = sessions.openSession()) {
|
||||
var matches = session.getMapper(ShadowInputMapper.class)
|
||||
.selectProjectOutboundCandidates("01", 459L, "CKD202609070015-1");
|
||||
assertThat(matches).hasSize(1);
|
||||
assertThat(matches.get(0).getTrayJobCount()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
|
||||
private void createLegacyRequestFixture() {
|
||||
jdbc.execute("create schema dbo");
|
||||
jdbc.execute("delete from alg.shadow_outbound_request_snapshot");
|
||||
jdbc.execute("""
|
||||
create table dbo.out_stockout_detail (stockout_detail_id varchar(80), stockout_id varchar(80),
|
||||
warehouse_id varchar(80), spare4 varchar(255), product_id varchar(255),
|
||||
quantity decimal(30,4), allot_quantity decimal(30,4), spare1 varchar(255),
|
||||
spare2 varchar(255), spare3 varchar(255), fid varchar(100), unit varchar(255))
|
||||
""");
|
||||
jdbc.execute("create table dbo.out_stockout (stockout_id varchar(80), warehouse_id varchar(80))");
|
||||
jdbc.update("insert into dbo.out_stockout values ('ORDER','1')");
|
||||
jdbc.update("""
|
||||
insert into dbo.out_stockout_detail values
|
||||
('CKD202609070015-1','ORDER','','AD.L-00033.01.01','030210200000506',5,2,
|
||||
'LEVEL','PARENT','TOP','220V','PCS')
|
||||
""");
|
||||
}
|
||||
|
||||
private String snapshotInsert(String table, String nextStatement) throws Exception {
|
||||
try (var input = getClass().getResourceAsStream("/db/sqlserver/upgrade-shadow-v3-to-v33.sql")) {
|
||||
assertThat(input).isNotNull();
|
||||
String migration = new String(input.readAllBytes(), StandardCharsets.UTF_8).replace("''", "'");
|
||||
int start = migration.indexOf("INSERT INTO alg." + table + " (");
|
||||
assertThat(start).isGreaterThan(0);
|
||||
int end = migration.indexOf(nextStatement, start);
|
||||
assertThat(end).isGreaterThan(start);
|
||||
// Adapt only SQL Server syntax to H2; all source columns and predicates remain from the migration.
|
||||
return migration.substring(start, end)
|
||||
.replace("@warehouse_id", "'01'")
|
||||
.replace("@snapshot_generation", "459")
|
||||
.replace("@synchronized_at", "TIMESTAMP '2026-09-07 02:37:10'")
|
||||
.replace("@numeric_warehouse_id", "1")
|
||||
.replace("WITH (NOLOCK)", "")
|
||||
.replace("DATALENGTH(", "OCTET_LENGTH(")
|
||||
.replace("TRY_CONVERT(decimal(38,0), LTRIM(RTRIM(stockout.warehouse_id)))",
|
||||
"CAST(LTRIM(RTRIM(stockout.warehouse_id)) AS decimal(38,0))")
|
||||
.replace("TRY_CONVERT(datetime2, stock.in_date)", "CAST(stock.in_date AS datetime2)");
|
||||
}
|
||||
}
|
||||
|
||||
private ShadowStockReference reference() {
|
||||
try (SqlSession session = sessions.openSession()) {
|
||||
return session.getMapper(ShadowInputMapper.class).selectProjectOutboundReference("01", 459L, "CKD202609070015-1");
|
||||
}
|
||||
}
|
||||
|
||||
private void stock(String id, String project) {
|
||||
jdbc.update("""
|
||||
insert into alg.inventory_item_snapshot values
|
||||
('01',459,?,?,?,?,'030210200000506',5,0,
|
||||
'LEVEL','PARENT','TOP','220V','PCS','2026-09-07 02:37:10',0)
|
||||
""", id, id, id, project);
|
||||
jdbc.update("insert into alg.cargo_space_snapshot values ('01',459,?,?,'2','N')", id, id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package com.ruoyi.algorithm.shadow;
|
||||
|
||||
import com.ruoyi.algorithm.integration.SchedulingFacade;
|
||||
import com.ruoyi.algorithm.outbound.*;
|
||||
import com.ruoyi.algorithm.shadow.mapper.ShadowInputMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.support.SimpleTransactionStatus;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class ShadowOutboundValidationTest {
|
||||
private final PublishedShadowSnapshot snapshot = new PublishedShadowSnapshot(459, "01",
|
||||
Instant.parse("2026-09-07T02:37:10Z"), "ABC123");
|
||||
private final ShadowInputMapper mapper = mock(ShadowInputMapper.class);
|
||||
private final SchedulingFacade facade = mock(SchedulingFacade.class);
|
||||
private ShadowPlanningEngine engine;
|
||||
private ShadowStockReference input;
|
||||
private ShadowStockReference candidate;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
var properties = new ShadowObservationProperties();
|
||||
properties.setWarehouseId("01");
|
||||
var tx = mock(PlatformTransactionManager.class);
|
||||
when(tx.getTransaction(any())).thenReturn(new SimpleTransactionStatus());
|
||||
engine = new ShadowPlanningEngine(facade, mapper, properties, tx);
|
||||
var descriptor = new ShadowSnapshotDescriptor();
|
||||
descriptor.setWarehouseId("01");
|
||||
descriptor.setSnapshotGeneration(459);
|
||||
descriptor.setSnapshotStatus("PUBLISHED");
|
||||
descriptor.setSynchronizedAt(snapshot.synchronizedAt());
|
||||
descriptor.setSourceSignature(snapshot.sourceSignature());
|
||||
when(mapper.selectPublishedSnapshot("01")).thenReturn(descriptor);
|
||||
input = new ShadowStockReference();
|
||||
input.setRequestId("DETAIL");
|
||||
input.setPredicateVersion(1);
|
||||
input.setProjectNo("AD.L-00033.01.01");
|
||||
input.setProductId("PRODUCT");
|
||||
input.setRequestedQuantity(new BigDecimal("3"));
|
||||
input.setShadowMatchCount(1);
|
||||
candidate = new ShadowStockReference();
|
||||
candidate.setStockId("STOCK");
|
||||
candidate.setTrayNo("TRAY");
|
||||
candidate.setSpaceId("SPACE");
|
||||
candidate.setProjectNo(input.getProjectNo());
|
||||
candidate.setProductId("PRODUCT");
|
||||
candidate.setAvailableQuantity(new BigDecimal("5"));
|
||||
candidate.setTrayItemCount(1);
|
||||
candidate.setTrayJobCount(0);
|
||||
when(mapper.selectProjectOutboundReference("01", 459L, "DETAIL")).thenReturn(input);
|
||||
when(mapper.selectProjectOutboundCandidates("01", 459L, "DETAIL")).thenReturn(List.of(candidate));
|
||||
when(facade.planOutboundFromPublishedSnapshot(eq("01"), any())).thenReturn(
|
||||
OutboundPlan.ready(new OutboundAssignment("DETAIL", "TRAY", input.getProjectNo(), "PRODUCT",
|
||||
"SPACE", 1, false, List.of(), List.of(), false, 0, 0, 0,
|
||||
FifoDecision.off(), Optional.empty(), "proposal"), TaskCountEstimate.zero()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validProposalRecordsRealStockAndUsesPickingMode() {
|
||||
var result = plan();
|
||||
assertThat(result.resultCode()).isEqualTo("READY");
|
||||
assertThat(result.proposedStockId()).isEqualTo("STOCK");
|
||||
assertThat(result.reason()).contains("predicateVersion=1", "requestedQuantity=3", "selectedProject=AD.L-00033.01.01");
|
||||
verify(facade).planOutboundFromPublishedSnapshot(eq("01"), argThat(r -> !r.wholeTrayOrDefault()));
|
||||
verify(mapper, never()).refreshSnapshot(anyString(), anyLong(), anyString());
|
||||
verify(facade, never()).planOutbound(anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyProjectDerivesOnlyProvenCandidateScope() {
|
||||
input.setProjectNo(null);
|
||||
assertThat(plan().resultCode()).isEqualTo("READY");
|
||||
verify(facade).planOutboundFromPublishedSnapshot(eq("01"), argThat(r -> "AD.L-00033.01.01".equals(r.projectNo())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void ambiguousProjectScopeIsSkippedWithoutPlanning() {
|
||||
input.setProjectNo(null);
|
||||
var other = new ShadowStockReference();
|
||||
other.setProjectNo("9204");
|
||||
when(mapper.selectProjectOutboundCandidates("01", 459L, "DETAIL")).thenReturn(List.of(candidate, other));
|
||||
assertThat(plan().resultCode()).isEqualTo("SKIPPED_UNSUPPORTED_PROJECT_SCOPE");
|
||||
verifyNoInteractions(facade);
|
||||
}
|
||||
|
||||
@Test
|
||||
void oldSnapshotVersionIsNotAccepted() {
|
||||
input.setPredicateVersion(null);
|
||||
assertThat(plan().resultCode()).isEqualTo("SKIPPED_MISSING_PREDICATE_INPUT");
|
||||
verifyNoInteractions(facade);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fullyAllocatedRequestIsSkipped() {
|
||||
input.setRequestedQuantity(BigDecimal.ZERO);
|
||||
assertThat(plan().resultCode()).isEqualTo("SKIPPED_INVALID_REMAINING_QUANTITY");
|
||||
verifyNoInteractions(facade);
|
||||
}
|
||||
|
||||
@Test
|
||||
void overflowDoesNotProduceTruncatedSuccessEvidence() {
|
||||
when(mapper.selectProjectOutboundCandidates("01", 459L, "DETAIL"))
|
||||
.thenReturn(Collections.nCopies(201, candidate));
|
||||
assertThat(plan().resultCode()).isEqualTo("SKIPPED_CANDIDATE_LIMIT");
|
||||
verifyNoInteractions(facade);
|
||||
}
|
||||
|
||||
@Test
|
||||
void insufficientSingleSourceIsNotCalledReady() {
|
||||
candidate.setAvailableQuantity(BigDecimal.ONE);
|
||||
assertThat(plan().resultCode()).isEqualTo("SKIPPED_MULTI_STOCK_REQUEST");
|
||||
}
|
||||
|
||||
@Test
|
||||
void plannerSourceOutsideFilteredCandidateSetIsNotCalledReady() {
|
||||
candidate.setSpaceId("OTHER");
|
||||
assertThat(plan().resultCode()).isEqualTo("SKIPPED_INELIGIBLE_PROPOSAL");
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameSpaceWithDifferentTrayIsNotEligible() {
|
||||
candidate.setTrayNo("OTHER");
|
||||
assertThat(plan().resultCode()).isEqualTo("SKIPPED_INELIGIBLE_PROPOSAL");
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleRowsOnSelectedTrayAreExplicitlyUnsupported() {
|
||||
candidate.setTrayItemCount(2);
|
||||
assertThat(plan().resultCode()).isEqualTo("SKIPPED_MULTI_STOCK_TRAY");
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyEligibleSetIsAnErrorNotAReadyProposal() {
|
||||
when(mapper.selectProjectOutboundCandidates("01", 459L, "DETAIL")).thenReturn(List.of());
|
||||
assertThat(plan().resultCode()).isEqualTo("ERROR_STOCK_NOT_FOUND");
|
||||
verifyNoInteractions(facade);
|
||||
}
|
||||
|
||||
@Test
|
||||
void existingTrayJobCannotBeAssumedCompatibleWithRequest() {
|
||||
candidate.setTrayJobCount(1);
|
||||
assertThat(plan().resultCode()).isEqualTo("SKIPPED_EXISTING_TRAY_JOB");
|
||||
}
|
||||
|
||||
private OutboundShadowObservation plan() {
|
||||
return engine.planProjectOutbound(snapshot, "DETAIL");
|
||||
}
|
||||
}
|
||||
@@ -84,6 +84,8 @@ class ShadowPlanningEngineTest {
|
||||
ShadowInputMapper mapper = mock(ShadowInputMapper.class);
|
||||
SchedulingFacade facade = mock(SchedulingFacade.class);
|
||||
ShadowStockReference reference = projectReference("W02");
|
||||
when(mapper.selectProjectOutboundCandidates("01", 42L, "DETAIL-1"))
|
||||
.thenReturn(java.util.List.of(stockReference("01")));
|
||||
when(mapper.selectProjectOutboundReference("01", 42L, "DETAIL-1")).thenReturn(reference);
|
||||
when(mapper.selectPublishedSnapshot("01")).thenReturn(descriptor(SNAPSHOT));
|
||||
when(facade.planOutboundFromPublishedSnapshot(eq("01"), any()))
|
||||
@@ -384,6 +386,8 @@ class ShadowPlanningEngineTest {
|
||||
reference.setProductId("PRODUCT-1");
|
||||
reference.setSnapshotSynchronizedAt("2026-09-03T00:00:00Z");
|
||||
reference.setShadowMatchCount(1);
|
||||
reference.setPredicateVersion(1);
|
||||
reference.setRequestedQuantity(java.math.BigDecimal.ONE);
|
||||
return reference;
|
||||
}
|
||||
|
||||
@@ -392,6 +396,9 @@ class ShadowPlanningEngineTest {
|
||||
reference.setStockId("STOCK-1");
|
||||
reference.setTrayNo("TRAY-1");
|
||||
reference.setSpaceId("SPACE-9");
|
||||
reference.setAvailableQuantity(java.math.BigDecimal.TEN);
|
||||
reference.setTrayItemCount(1);
|
||||
reference.setTrayJobCount(0);
|
||||
return reference;
|
||||
}
|
||||
|
||||
|
||||
@@ -107,6 +107,24 @@ class ShadowV3OperationsPackageTest {
|
||||
"insert ", "update ", "delete ", "merge ", "truncate ", "drop ", "alter ", "create ", "exec ");
|
||||
}
|
||||
|
||||
@Test
|
||||
void v33InstallerChainsUpgradeAndPropagatesSecondStageFailure() throws Exception {
|
||||
Assumptions.assumeTrue(isWindows(), "CMD behavior is verified on Windows");
|
||||
Path bin = writeSqlcmd13Fixture(Files.createDirectories(tempDir.resolve("v33 fixture")));
|
||||
Path script = root.resolve("scripts/shadow-v3-install.cmd");
|
||||
CmdResult current = runCmd(script, bin, "SERVER", "FT546-wms", "v33");
|
||||
assertThat(current.exitCode()).as(current.output()).isZero();
|
||||
assertThat(current.output()).contains("FIXTURE_INPUT=upgrade-shadow-v3-to-v33.sql", "INSTALL_OK")
|
||||
.doesNotContain("FIXTURE_INPUT=install-shadow.sql", "FIXTURE_INPUT=upgrade-shadow-v2-to-v3.sql");
|
||||
CmdResult upgrade = runCmd(script, bin, "SERVER", "FT546-wms", "upgrade");
|
||||
assertThat(upgrade.exitCode()).as(upgrade.output()).isZero();
|
||||
assertThat(upgrade.output()).contains("FIXTURE_INPUT=upgrade-shadow-v2-to-v3.sql",
|
||||
"FIXTURE_INPUT=upgrade-shadow-v3-to-v33.sql");
|
||||
CmdResult failure = runCmd(script, bin, "SERVER", "FAIL_V33", "upgrade");
|
||||
assertThat(failure.exitCode()).isEqualTo(1);
|
||||
assertThat(failure.output()).doesNotContain("INSTALL_OK");
|
||||
}
|
||||
|
||||
@Test
|
||||
void runbookAndServerAiPromptPreserveTheSafeFieldOrder() throws IOException {
|
||||
String runbook = text("doc/algorithm-shadow/README.md");
|
||||
@@ -259,12 +277,12 @@ class ShadowV3OperationsPackageTest {
|
||||
boolean exportScript = script.getFileName().toString().equals("shadow-v3-export.cmd");
|
||||
Path callerDirectory = exportScript ? root : tempDir;
|
||||
Files.writeString(wrapper, "@echo off\r\n"
|
||||
+ (exportScript ? "pushd " + quote(root.toString()) + "\r\n" : "")
|
||||
+ "pushd " + quote(callerDirectory.toString()) + "\r\n"
|
||||
+ command + "\r\nset \"RESULT=%errorlevel%\"\r\n"
|
||||
+ "cd > " + quote(restored.toString()) + "\r\n"
|
||||
+ (exportScript ? "popd\r\n" : "") + "exit /b %RESULT%\r\n");
|
||||
+ "popd\r\nexit /b %RESULT%\r\n");
|
||||
ProcessBuilder builder = new ProcessBuilder("cmd.exe", "/d", "/c", "call " + quote(wrapper.toString()));
|
||||
builder.directory(tempDir.toFile());
|
||||
builder.directory(tempDir.getParent().toFile());
|
||||
String inheritedPath = builder.environment().getOrDefault("PATH", "");
|
||||
builder.environment().put("PATH", stubBin + File.pathSeparator + inheritedPath);
|
||||
builder.environment().put("FORTUCKY_ALGORITHM_WAREHOUSE_ID", "01");
|
||||
|
||||
@@ -13,6 +13,10 @@ class Sqlcmd13OutputFixture
|
||||
}
|
||||
int input = Array.IndexOf(args, "-i");
|
||||
if (input < 0) return 86;
|
||||
string inputName = Path.GetFileName(args[input + 1]);
|
||||
Console.WriteLine("FIXTURE_INPUT=" + inputName);
|
||||
if (inputName == "upgrade-shadow-v3-to-v33.sql" && Array.IndexOf(args, "FAIL_V33") >= 0)
|
||||
return 33;
|
||||
foreach (string line in File.ReadAllLines(args[input + 1]))
|
||||
{
|
||||
if (!line.StartsWith(":out ")) continue;
|
||||
|
||||
Reference in New Issue
Block a user