mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 11:13:43 +08:00
242 lines
11 KiB
Java
242 lines
11 KiB
Java
package vip.mate.tool.builtin;
|
|
|
|
import com.fasterxml.jackson.databind.JsonNode;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import org.junit.jupiter.api.AfterEach;
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
import org.junit.jupiter.api.DisplayName;
|
|
import org.junit.jupiter.api.Test;
|
|
import org.junit.jupiter.api.io.TempDir;
|
|
import vip.mate.tool.document.GeneratedFileCache;
|
|
import vip.mate.tool.guard.WorkspacePathGuard;
|
|
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.util.regex.Matcher;
|
|
import java.util.regex.Pattern;
|
|
|
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
|
/** Regression coverage for the optional OfficeCLI adapter introduced by issue #583. */
|
|
class OfficeCliToolTest {
|
|
|
|
private static final Pattern GENERATED_ID = Pattern.compile(
|
|
"/api/v1/files/generated/([a-zA-Z0-9-]+)");
|
|
|
|
@TempDir
|
|
Path tempDir;
|
|
|
|
private final ObjectMapper mapper = new ObjectMapper();
|
|
private GeneratedFileCache cache;
|
|
private Path fakeCli;
|
|
|
|
@BeforeEach
|
|
void setUp() throws Exception {
|
|
WorkspacePathGuard.setDefaultRoot(tempDir.toString());
|
|
cache = new GeneratedFileCache(tempDir.resolve("cache"));
|
|
fakeCli = tempDir.resolve("officecli-fake");
|
|
Files.writeString(fakeCli, """
|
|
#!/bin/sh
|
|
command="$1"
|
|
shift
|
|
case "$command" in
|
|
batch)
|
|
file="$1"
|
|
printf '\nEDITED-BY-OFFICECLI' >> "$file"
|
|
printf '{"ok":true,"command":"batch"}\n'
|
|
;;
|
|
merge)
|
|
input="$1"
|
|
output="$2"
|
|
cp "$input" "$output"
|
|
printf '\nMERGED-BY-OFFICECLI' >> "$output"
|
|
printf '{"ok":true,"command":"merge"}\n'
|
|
;;
|
|
validate)
|
|
printf '{"valid":true}\n'
|
|
;;
|
|
view)
|
|
input="$1"
|
|
mode="$2"
|
|
shift 2
|
|
output=""
|
|
while [ "$#" -gt 0 ]; do
|
|
if [ "$1" = "-o" ]; then output="$2"; shift 2; else shift; fi
|
|
done
|
|
if [ -n "$output" ]; then
|
|
printf 'rendered:%s' "$mode" > "$output"
|
|
elif [ "$mode" = "html" ] || [ "$mode" = "svg" ]; then
|
|
printf 'rendered:%s' "$mode"
|
|
else
|
|
printf '{"mode":"%s","source":"%s"}\n' "$mode" "$input"
|
|
fi
|
|
;;
|
|
*)
|
|
printf 'unsupported fake command\n' >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
""", StandardCharsets.UTF_8);
|
|
assertTrue(fakeCli.toFile().setExecutable(true));
|
|
}
|
|
|
|
@AfterEach
|
|
void tearDown() {
|
|
WorkspacePathGuard.setDefaultRoot(null);
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("batch edits a scratch copy, preserves the source, and returns a cached download")
|
|
void batchIsCopyOnWriteAndDownloadable() throws Exception {
|
|
byte[] original = "PK-original-docx".getBytes(StandardCharsets.UTF_8);
|
|
Path source = Files.write(tempDir.resolve("source.docx"), original);
|
|
OfficeCliTool tool = new OfficeCliTool(cache, mapper, fakeCli.toString());
|
|
|
|
String result = tool.office_document(
|
|
"batch", source.toString(), null,
|
|
"[{\"command\":\"set\",\"path\":\"/body/p[1]\",\"props\":{\"text\":\"new\"}}]",
|
|
"客户/报告.docx", 10, null);
|
|
|
|
JsonNode json = mapper.readTree(result);
|
|
assertTrue(json.path("success").asBoolean(), result);
|
|
assertArrayEquals(original, Files.readAllBytes(source), "source document must never be mutated");
|
|
|
|
GeneratedFileCache.Entry entry = cachedEntry(json.path("generatedFile").asText());
|
|
assertEquals("客户_报告.docx", entry.filename());
|
|
assertTrue(new String(entry.bytes(), StandardCharsets.UTF_8).contains("EDITED-BY-OFFICECLI"));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("render returns the requested preview artifact through GeneratedFileCache")
|
|
void renderProducesDownload() throws Exception {
|
|
Path source = Files.writeString(tempDir.resolve("slides.pptx"), "PK-pptx");
|
|
OfficeCliTool tool = new OfficeCliTool(cache, mapper, fakeCli.toString());
|
|
|
|
String result = tool.office_document(
|
|
"render", source.toString(), "screenshot", null,
|
|
"预览.png", 10, null);
|
|
|
|
JsonNode json = mapper.readTree(result);
|
|
assertTrue(json.path("success").asBoolean(), result);
|
|
GeneratedFileCache.Entry entry = cachedEntry(json.path("generatedFile").asText());
|
|
assertEquals("预览.png", entry.filename());
|
|
assertEquals("image/png", entry.mimeType());
|
|
assertEquals("rendered:screenshot", new String(entry.bytes(), StandardCharsets.UTF_8));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("stdout render modes are captured as artifacts instead of diagnostics")
|
|
void stdoutRenderProducesDownload() throws Exception {
|
|
Path source = Files.writeString(tempDir.resolve("slides.pptx"), "PK-pptx");
|
|
OfficeCliTool tool = new OfficeCliTool(cache, mapper, fakeCli.toString());
|
|
|
|
JsonNode result = mapper.readTree(tool.office_document(
|
|
"render", source.toString(), "svg", null, null, 10, null));
|
|
|
|
assertTrue(result.path("success").asBoolean(), result.toString());
|
|
GeneratedFileCache.Entry entry = cachedEntry(result.path("generatedFile").asText());
|
|
assertEquals("image/svg+xml", entry.mimeType());
|
|
assertEquals("rendered:svg", new String(entry.bytes(), StandardCharsets.UTF_8));
|
|
assertEquals("", result.path("stdout").asText());
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("read-only validation returns structured stdout without generating a file")
|
|
void validateIsReadOnly() throws Exception {
|
|
Path source = Files.writeString(tempDir.resolve("book.xlsx"), "PK-xlsx");
|
|
OfficeCliTool tool = new OfficeCliTool(cache, mapper, fakeCli.toString());
|
|
|
|
JsonNode result = mapper.readTree(tool.office_document(
|
|
"validate", source.toString(), null, null, null, 10, null));
|
|
|
|
assertTrue(result.path("success").asBoolean());
|
|
assertTrue(result.path("stdout").asText().contains("\"valid\":true"));
|
|
assertFalse(result.has("generatedFile"));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("invalid payloads and unsupported formats fail before a subprocess mutates anything")
|
|
void rejectsInvalidRequests() throws Exception {
|
|
Path office = Files.writeString(tempDir.resolve("a.docx"), "PK-docx");
|
|
Path text = Files.writeString(tempDir.resolve("a.txt"), "text");
|
|
OfficeCliTool tool = new OfficeCliTool(cache, mapper, fakeCli.toString());
|
|
|
|
JsonNode badPayload = mapper.readTree(tool.office_document(
|
|
"batch", office.toString(), null, "{}", null, 10, null));
|
|
JsonNode badFormat = mapper.readTree(tool.office_document(
|
|
"validate", text.toString(), null, null, null, 10, null));
|
|
|
|
assertFalse(badPayload.path("success").asBoolean());
|
|
assertTrue(badPayload.path("error").asText().contains("JSON array"));
|
|
assertFalse(badFormat.path("success").asBoolean());
|
|
assertTrue(badFormat.path("error").asText().contains(".docx"));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("batch rejects raw XML, external media, and host filesystem references")
|
|
void rejectsUnsafeBatchSurface() throws Exception {
|
|
Path office = Files.writeString(tempDir.resolve("a.docx"), "PK-docx");
|
|
OfficeCliTool tool = new OfficeCliTool(cache, mapper, fakeCli.toString());
|
|
|
|
JsonNode raw = mapper.readTree(tool.office_document(
|
|
"batch", office.toString(), null,
|
|
"[{\"command\":\"raw-set\",\"path\":\"/body\",\"xml\":\"<x/>\"}]",
|
|
null, 10, null));
|
|
JsonNode media = mapper.readTree(tool.office_document(
|
|
"batch", office.toString(), null,
|
|
"[{\"command\":\"add\",\"path\":\"/body\",\"type\":\"image\",\"props\":{\"source\":\"/etc/passwd\"}}]",
|
|
null, 10, null));
|
|
JsonNode hostPath = mapper.readTree(tool.office_document(
|
|
"batch", office.toString(), null,
|
|
"[{\"command\":\"set\",\"path\":\"/body/p[1]\",\"props\":{\"source\":\"C:\\\\secret.txt\"}}]",
|
|
null, 10, null));
|
|
|
|
assertTrue(raw.path("error").asText().contains("Unsupported batch command"));
|
|
assertTrue(media.path("error").asText().contains("External media"));
|
|
assertTrue(hostPath.path("error").asText().contains("filesystem reference"));
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("timeout terminates the native process and reports a bounded failure")
|
|
void timeoutIsEnforced() throws Exception {
|
|
Path sleepy = tempDir.resolve("officecli-sleepy");
|
|
Files.writeString(sleepy, "#!/bin/sh\nsleep 10\n", StandardCharsets.UTF_8);
|
|
assertTrue(sleepy.toFile().setExecutable(true));
|
|
Path source = Files.writeString(tempDir.resolve("slow.docx"), "PK-docx");
|
|
OfficeCliTool tool = new OfficeCliTool(cache, mapper, sleepy.toString());
|
|
|
|
long started = System.nanoTime();
|
|
JsonNode result = mapper.readTree(tool.office_document(
|
|
"validate", source.toString(), null, null, null, 1, null));
|
|
long elapsedMillis = (System.nanoTime() - started) / 1_000_000;
|
|
|
|
assertFalse(result.path("success").asBoolean());
|
|
assertTrue(result.path("timedOut").asBoolean());
|
|
assertTrue(elapsedMillis < 5_000, "timeout took too long: " + elapsedMillis + "ms");
|
|
}
|
|
|
|
@Test
|
|
@DisplayName("a missing binary is reported as setup-required instead of an opaque exception")
|
|
void missingBinaryReportsSetupRequired() throws Exception {
|
|
Path source = Files.writeString(tempDir.resolve("a.docx"), "PK-docx");
|
|
OfficeCliTool tool = new OfficeCliTool(cache, mapper,
|
|
tempDir.resolve("missing-officecli").toString());
|
|
|
|
JsonNode result = mapper.readTree(tool.office_document(
|
|
"validate", source.toString(), null, null, null, 10, null));
|
|
|
|
assertFalse(result.path("success").asBoolean());
|
|
assertTrue(result.path("setupRequired").asBoolean(), result.toString());
|
|
}
|
|
|
|
private GeneratedFileCache.Entry cachedEntry(String generatedFileText) {
|
|
Matcher matcher = GENERATED_ID.matcher(generatedFileText);
|
|
assertTrue(matcher.find(), generatedFileText);
|
|
return cache.get(matcher.group(1)).orElseThrow();
|
|
}
|
|
}
|