fix: complete shadow v3 operations safeguards

This commit is contained in:
zgw
2026-09-03 17:37:58 +08:00
parent 22cf1f9a03
commit 81888f5150
3 changed files with 122 additions and 67 deletions

View File

@@ -1,18 +1,25 @@
package com.ruoyi.algorithm.shadow;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.io.TempDir;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
class ShadowV3OperationsPackageTest {
private final Path root = projectRoot();
@TempDir
Path tempDir;
@Test
void packageContainsAllCmdOnlyOperationsFiles() {
assertThat(root.resolve("scripts/shadow-v3-preflight.cmd")).isRegularFile();
@@ -37,6 +44,11 @@ class ShadowV3OperationsPackageTest {
"jps -lv",
"sqlcmd",
"shadow_snapshot_state",
"snapshot_generation>0",
"snapshot_status='PUBLISHED'",
"DATEDIFF(SECOND,synchronized_at,SYSUTCDATETIME()) BETWEEN 0 AND 300",
"alg.shadow_instance_heartbeat",
"DATEDIFF(SECOND,last_seen_at,SYSUTCDATETIME()) BETWEEN 0 AND 120",
"FT546WMS_SHADOW_ONLY",
"2026-09-03-3",
"exit /b 1");
@@ -78,7 +90,10 @@ class ShadowV3OperationsPackageTest {
"shadow-v3-export.sql",
"sqlcmd", "-E", "-b", "-r1",
"FORTUCKY_SHADOW_",
"shadow_failure");
"shadow_failure",
"OUTPUT_DIR already exists and is not empty",
"environment-variable-names.txt",
"if errorlevel 2");
assertThat(exportSql).contains(
"$(warehouseid)",
"$(startutc)",
@@ -128,6 +143,108 @@ class ShadowV3OperationsPackageTest {
"EXPORT_OK");
}
@Test
void cmdScriptsHandleQuotedPathsAndPropagateToolFailures() throws Exception {
Assumptions.assumeTrue(isWindows(), "CMD behavior is verified on Windows");
Path fixture = Files.createDirectories(tempDir.resolve("fixture with spaces"));
Path repo = fixture.resolve("repo with spaces");
cloneRepository(repo);
Path logDir = Files.createDirectories(fixture.resolve("logs with spaces"));
Files.writeString(logDir.resolve("application log.txt"), "normal log line\n", StandardCharsets.UTF_8);
Path stubBin = writeSqlcmdFailureStub(fixture);
CmdResult preflight = runCmd(root.resolve("scripts/shadow-v3-preflight.cmd"), stubBin,
repo.toString(), gitHead(repo),
"SQL SERVER WITH SPACE", "FT546 WMS");
assertThat(preflight.exitCode()).isEqualTo(1);
assertThat(preflight.output()).contains("SQL connectivity or shadow package validation failed");
Path output = fixture.resolve("new export with spaces");
CmdResult export = runCmd(root.resolve("scripts/shadow-v3-export.cmd"), stubBin,
repo.toString(), "SQL SERVER WITH SPACE", "FT546 WMS",
"2026-09-03T00:00:00Z", output.toString(), logDir.toString());
assertThat(export.exitCode()).isEqualTo(1);
assertThat(export.output()).contains("read-only SQL export failed");
}
@Test
void exportRefusesToOverwriteExistingEvidence() throws Exception {
Assumptions.assumeTrue(isWindows(), "CMD behavior is verified on Windows");
Path fixture = Files.createDirectories(tempDir.resolve("repeat export fixture"));
Path repo = Files.createDirectories(fixture.resolve("repo"));
Files.createDirectory(repo.resolve(".git"));
Path logDir = Files.createDirectories(fixture.resolve("logs"));
Files.writeString(logDir.resolve("application.log"), "normal log line\n", StandardCharsets.UTF_8);
Path output = Files.createDirectories(fixture.resolve("existing evidence"));
Path marker = output.resolve("do-not-overwrite.txt");
Files.writeString(marker, "preserve me", StandardCharsets.UTF_8);
Path stubBin = writeSqlcmdFailureStub(fixture);
CmdResult result = runCmd(root.resolve("scripts/shadow-v3-export.cmd"), stubBin,
repo.toString(), "server", "database", "2026-09-03T00:00:00Z",
output.toString(), logDir.toString());
assertThat(result.exitCode()).isEqualTo(1);
assertThat(result.output()).contains("OUTPUT_DIR already exists and is not empty");
assertThat(Files.readString(marker, StandardCharsets.UTF_8)).isEqualTo("preserve me");
assertThat(output.resolve("deployment.txt")).doesNotExist();
}
private Path writeSqlcmdFailureStub(Path fixture) throws IOException {
Path bin = Files.createDirectories(fixture.resolve("stub tools"));
Path whereExecutable = Path.of(System.getenv("SystemRoot"), "System32", "where.exe");
Files.copy(whereExecutable, bin.resolve("sqlcmd.exe"));
return bin;
}
private void cloneRepository(Path destination) throws Exception {
CmdResult result = runProcess(new ProcessBuilder(
"git", "clone", "--quiet", root.toString(), destination.toString()), 60);
assertThat(result.exitCode()).as(result.output()).isZero();
}
private String gitHead(Path repository) throws Exception {
CmdResult result = runProcess(new ProcessBuilder(
"git", "-C", repository.toString(), "rev-parse", "HEAD"), 15);
assertThat(result.exitCode()).as(result.output()).isZero();
return result.output().trim();
}
private CmdResult runCmd(Path script, Path stubBin, String... arguments) throws Exception {
StringBuilder command = new StringBuilder("call ").append(quote(script.toString()));
for (String argument : arguments) {
command.append(' ').append(quote(argument));
}
ProcessBuilder builder = new ProcessBuilder("cmd.exe", "/d", "/c", command.toString());
String inheritedPath = builder.environment().getOrDefault("PATH", "");
builder.environment().put("PATH", stubBin + File.pathSeparator + inheritedPath);
builder.environment().put("FORTUCKY_ALGORITHM_WAREHOUSE_ID", "01");
builder.redirectErrorStream(true);
return runProcess(builder, 15);
}
private CmdResult runProcess(ProcessBuilder builder, int timeoutSeconds) throws Exception {
builder.redirectErrorStream(true);
Process process = builder.start();
boolean finished = process.waitFor(timeoutSeconds, TimeUnit.SECONDS);
if (!finished) {
process.destroyForcibly();
}
assertThat(finished).as("CMD script completed").isTrue();
String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
return new CmdResult(process.exitValue(), output);
}
private static String quote(String value) {
return "\"" + value.replace("\"", "\"\"") + "\"";
}
private static boolean isWindows() {
return System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("windows");
}
private record CmdResult(int exitCode, String output) { }
private String text(String relativePath) throws IOException {
return Files.readString(root.resolve(relativePath), StandardCharsets.UTF_8);
}