package vip.mate.tool.builtin; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; /** * Unit tests for {@link SkillScriptTool#normalizeArgs(String)} — the decode * step that turns the JSON-encoded {@code args} tool parameter into the * positional argument list handed to a skill script. * *

The decisive cases are the ones a model gets wrong when a script needs a * JSON payload: the object passed directly, the object wrapped in an array, * and the object pre-escaped into an array of one string all have to converge * on the same single JSON argument. A bare scalar must survive untouched — * decoding {@code 2026-05-19} would otherwise truncate it to {@code 2026}. */ class SkillScriptToolArgsTest { private final ObjectMapper objectMapper = new ObjectMapper(); /** Unused collaborators are null — {@code normalizeArgs} only needs the mapper. */ private final SkillScriptTool tool = new SkillScriptTool(null, null, null, null, objectMapper); @Test @DisplayName("null / blank args yield no argument list") void emptyInputs() { assertThat(tool.normalizeArgs(null)).isNull(); assertThat(tool.normalizeArgs("")).isNull(); assertThat(tool.normalizeArgs(" ")).isNull(); assertThat(tool.normalizeArgs("[]")).isNull(); } @Test @DisplayName("a JSON object is forwarded as a single JSON argument") void objectBecomesOneArg() throws Exception { List args = tool.normalizeArgs("{\"date\":\"2026-05-19\",\"topic\":\"智能体\"}"); assertThat(args).hasSize(1); JsonNode parsed = objectMapper.readTree(args.get(0)); assertThat(parsed.get("date").asText()).isEqualTo("2026-05-19"); assertThat(parsed.get("topic").asText()).isEqualTo("智能体"); } @Test @DisplayName("an object wrapped in a single-element array still reaches the script as JSON") void objectWrappedInArray() throws Exception { List args = tool.normalizeArgs("[{\"date\":\"x\"}]"); assertThat(args).hasSize(1); assertThat(objectMapper.readTree(args.get(0)).get("date").asText()).isEqualTo("x"); } @Test @DisplayName("an object pre-escaped into an array of one string is unwrapped") void objectPreEscapedInArray() throws Exception { List args = tool.normalizeArgs("[\"{\\\"date\\\":\\\"x\\\"}\"]"); assertThat(args).hasSize(1); assertThat(objectMapper.readTree(args.get(0)).get("date").asText()).isEqualTo("x"); } @Test @DisplayName("a plain JSON array maps to one positional argument per element") void plainArrayKeepsElements() { assertThat(tool.normalizeArgs("[\"--verbose\",\"input.txt\"]")) .containsExactly("--verbose", "input.txt"); assertThat(tool.normalizeArgs("[1,2,3]")) .containsExactly("1", "2", "3"); } @Test @DisplayName("a bare scalar is forwarded verbatim, never JSON-decoded") void bareScalarUntouched() { // Decoding would truncate this to "2026" — it must survive intact. assertThat(tool.normalizeArgs("2026-05-19")).containsExactly("2026-05-19"); assertThat(tool.normalizeArgs("智能体")).containsExactly("智能体"); assertThat(tool.normalizeArgs(" hello world ")).containsExactly("hello world"); } @Test @DisplayName("text that looks like JSON but does not parse is forwarded verbatim") void malformedJsonForwardedVerbatim() { assertThat(tool.normalizeArgs("{bad json")).containsExactly("{bad json"); assertThat(tool.normalizeArgs("[1,2")).containsExactly("[1,2"); } }