diff --git a/src/main/java/com/volcengine/ark/runtime/models/skill/CreateSkillVersionRequest.java b/src/main/java/com/volcengine/ark/runtime/models/skill/CreateSkillVersionRequest.java new file mode 100644 index 0000000..ca8193d --- /dev/null +++ b/src/main/java/com/volcengine/ark/runtime/models/skill/CreateSkillVersionRequest.java @@ -0,0 +1,245 @@ +// Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +// SPDX-License-Identifier: Apache-2.0 + +/* + * Ark Managed Agents Skill API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +package com.volcengine.ark.runtime.models.skill; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * CreateSkillVersionRequest + */ +@JsonPropertyOrder({ + CreateSkillVersionRequest.JSON_PROPERTY_DISPLAY_TITLE, + CreateSkillVersionRequest.JSON_PROPERTY_FILES, + CreateSkillVersionRequest.JSON_PROPERTY_FILES_BRACKET +}) +@javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.21.0") +public class CreateSkillVersionRequest { + public static final String JSON_PROPERTY_DISPLAY_TITLE = "display_title"; + @javax.annotation.Nullable + private String displayTitle; + + public static final String JSON_PROPERTY_FILES = "files"; + @javax.annotation.Nullable + private List files; + + public static final String JSON_PROPERTY_FILES_BRACKET = "files[]"; + @javax.annotation.Nullable + private List filesBracket; + + public CreateSkillVersionRequest() { + } + + public CreateSkillVersionRequest displayTitle(@javax.annotation.Nullable String displayTitle) { + + this.displayTitle = displayTitle; + return this; + } + + /** + * Skill 版本展示名。 + * @return displayTitle + */ + @javax.annotation.Nullable + @JsonProperty(value = JSON_PROPERTY_DISPLAY_TITLE, required = false) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + + public String getDisplayTitle() { + return displayTitle; + } + + + @JsonProperty(value = JSON_PROPERTY_DISPLAY_TITLE, required = false) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public void setDisplayTitle(@javax.annotation.Nullable String displayTitle) { + this.displayTitle = displayTitle; + } + + public CreateSkillVersionRequest files(@javax.annotation.Nullable List files) { + + this.files = files; + return this; + } + + public CreateSkillVersionRequest addFilesItem(Object filesItem) { + if (this.files == null) { + this.files = new ArrayList<>(); + } + this.files.add(filesItem); + return this; + } + + /** + * 新版本技能包内容。可以是单个 `.zip`,也可以是一组带相对路径的松散文件; 该字段名允许在同一次请求中重复出现以传多个文件。 与 `files[]` 语义等价,两者至少提供其一;同时提供时服务端会合并处理。 + * @return files + */ + @javax.annotation.Nullable + @JsonProperty(value = JSON_PROPERTY_FILES, required = false) + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + + public List getFiles() { + return files; + } + + + @JsonProperty(value = JSON_PROPERTY_FILES, required = false) + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + public void setFiles(@javax.annotation.Nullable List files) { + this.files = files; + } + + public CreateSkillVersionRequest filesBracket(@javax.annotation.Nullable List filesBracket) { + + this.filesBracket = filesBracket; + return this; + } + + public CreateSkillVersionRequest addFilesBracketItem(Object filesBracketItem) { + if (this.filesBracket == null) { + this.filesBracket = new ArrayList<>(); + } + this.filesBracket.add(filesBracketItem); + return this; + } + + /** + * `files` 的可重复字段名别名,语义完全等价。 + * @return filesBracket + */ + @javax.annotation.Nullable + @JsonProperty(value = JSON_PROPERTY_FILES_BRACKET, required = false) + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + + public List getFilesBracket() { + return filesBracket; + } + + + @JsonProperty(value = JSON_PROPERTY_FILES_BRACKET, required = false) + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) + public void setFilesBracket(@javax.annotation.Nullable List filesBracket) { + this.filesBracket = filesBracket; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CreateSkillVersionRequest createSkillVersionRequest = (CreateSkillVersionRequest) o; + return Objects.equals(this.displayTitle, createSkillVersionRequest.displayTitle) && + Objects.equals(this.files, createSkillVersionRequest.files) && + Objects.equals(this.filesBracket, createSkillVersionRequest.filesBracket); + } + + @Override + public int hashCode() { + return Objects.hash(displayTitle, files, filesBracket); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class CreateSkillVersionRequest {\n"); + sb.append(" displayTitle: ").append(toIndentedString(displayTitle)).append("\n"); + sb.append(" files: ").append(toIndentedString(files)).append("\n"); + sb.append(" filesBracket: ").append(toIndentedString(filesBracket)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + return o == null ? "null" : o.toString().replace("\n", "\n "); + } + + public static class Builder { + + private CreateSkillVersionRequest instance; + + public Builder() { + this(new CreateSkillVersionRequest()); + } + + protected Builder(CreateSkillVersionRequest instance) { + this.instance = instance; + } + + public CreateSkillVersionRequest.Builder displayTitle(String displayTitle) { + this.instance.displayTitle = displayTitle; + return this; + } + public CreateSkillVersionRequest.Builder files(List files) { + this.instance.files = files; + return this; + } + public CreateSkillVersionRequest.Builder filesBracket(List filesBracket) { + this.instance.filesBracket = filesBracket; + return this; + } + + + /** + * returns a built CreateSkillVersionRequest instance. + * + * The builder is not reusable. + */ + public CreateSkillVersionRequest build() { + try { + return this.instance; + } finally { + // ensure that this.instance is not reused + this.instance = null; + } + } + + @Override + public String toString() { + return getClass() + "=(" + instance + ")"; + } + } + + /** + * Create a builder with no initialized field. + */ + public static CreateSkillVersionRequest.Builder builder() { + return new CreateSkillVersionRequest.Builder(); + } + + /** + * Create a builder with a shallow copy of this instance. + */ + public CreateSkillVersionRequest.Builder toBuilder() { + return new CreateSkillVersionRequest.Builder() + .displayTitle(getDisplayTitle()) + .files(getFiles()) + .filesBracket(getFilesBracket()); + } + + +} diff --git a/src/main/java/com/volcengine/ark/runtime/models/skill/SkillVersion.java b/src/main/java/com/volcengine/ark/runtime/models/skill/SkillVersion.java new file mode 100644 index 0000000..acb7316 --- /dev/null +++ b/src/main/java/com/volcengine/ark/runtime/models/skill/SkillVersion.java @@ -0,0 +1,410 @@ +// Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +// SPDX-License-Identifier: Apache-2.0 + +/* + * Ark Managed Agents Skill API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +package com.volcengine.ark.runtime.models.skill; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.Objects; + +/** + * Skill 版本摘要。 CreateSkillVersion / ListSkillVersions / GetSkillVersion 返回该对象;主 Skill 的 latest_version 会由服务端在新版本创建成功后推进。 + */ +@JsonPropertyOrder({ + SkillVersion.JSON_PROPERTY_ID, + SkillVersion.JSON_PROPERTY_TYPE, + SkillVersion.JSON_PROPERTY_SKILL_ID, + SkillVersion.JSON_PROPERTY_VERSION, + SkillVersion.JSON_PROPERTY_DISPLAY_TITLE, + SkillVersion.JSON_PROPERTY_DESCRIPTION, + SkillVersion.JSON_PROPERTY_CREATED_AT +}) +@javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.21.0") +public class SkillVersion { + public static final String JSON_PROPERTY_ID = "id"; + @javax.annotation.Nonnull + private String id; + + /** + * 固定 `\"skill_version\"`。 + */ + public enum TypeEnum { + SKILL_VERSION(String.valueOf("skill_version")); + + private String value; + + TypeEnum(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + @JsonCreator + public static TypeEnum fromValue(String value) { + for (TypeEnum b : TypeEnum.values()) { + if (b.value.equalsIgnoreCase(value)) { + return b; + } + } + throw new IllegalArgumentException("Unexpected value '" + value + "'"); + } + } + + public static final String JSON_PROPERTY_TYPE = "type"; + @javax.annotation.Nonnull + private TypeEnum type; + + public static final String JSON_PROPERTY_SKILL_ID = "skill_id"; + @javax.annotation.Nonnull + private String skillId; + + public static final String JSON_PROPERTY_VERSION = "version"; + @javax.annotation.Nonnull + private String version; + + public static final String JSON_PROPERTY_DISPLAY_TITLE = "display_title"; + @javax.annotation.Nonnull + private String displayTitle; + + public static final String JSON_PROPERTY_DESCRIPTION = "description"; + @javax.annotation.Nullable + private String description; + + public static final String JSON_PROPERTY_CREATED_AT = "created_at"; + @javax.annotation.Nonnull + private String createdAt; + + public SkillVersion() { + } + + public SkillVersion id(@javax.annotation.Nonnull String id) { + + this.id = id; + return this; + } + + /** + * SkillVersion ID,形如 `skillver-...`。 + * @return id + */ + @javax.annotation.Nonnull + @JsonProperty(value = JSON_PROPERTY_ID, required = true) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + + public String getId() { + return id; + } + + + @JsonProperty(value = JSON_PROPERTY_ID, required = true) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public void setId(@javax.annotation.Nonnull String id) { + this.id = id; + } + + public SkillVersion type(@javax.annotation.Nonnull TypeEnum type) { + + this.type = type; + return this; + } + + /** + * 固定 `\"skill_version\"`。 + * @return type + */ + @javax.annotation.Nonnull + @JsonProperty(value = JSON_PROPERTY_TYPE, required = true) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + + public TypeEnum getType() { + return type; + } + + + @JsonProperty(value = JSON_PROPERTY_TYPE, required = true) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public void setType(@javax.annotation.Nonnull TypeEnum type) { + this.type = type; + } + + public SkillVersion skillId(@javax.annotation.Nonnull String skillId) { + + this.skillId = skillId; + return this; + } + + /** + * 归属 Skill ID。 + * @return skillId + */ + @javax.annotation.Nonnull + @JsonProperty(value = JSON_PROPERTY_SKILL_ID, required = true) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + + public String getSkillId() { + return skillId; + } + + + @JsonProperty(value = JSON_PROPERTY_SKILL_ID, required = true) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public void setSkillId(@javax.annotation.Nonnull String skillId) { + this.skillId = skillId; + } + + public SkillVersion version(@javax.annotation.Nonnull String version) { + + this.version = version; + return this; + } + + /** + * 版本号,服务端按字符串形式自增,例如 `\"1\"`、`\"2\"`。 + * @return version + */ + @javax.annotation.Nonnull + @JsonProperty(value = JSON_PROPERTY_VERSION, required = true) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + + public String getVersion() { + return version; + } + + + @JsonProperty(value = JSON_PROPERTY_VERSION, required = true) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public void setVersion(@javax.annotation.Nonnull String version) { + this.version = version; + } + + public SkillVersion displayTitle(@javax.annotation.Nonnull String displayTitle) { + + this.displayTitle = displayTitle; + return this; + } + + /** + * 该版本的展示名。 + * @return displayTitle + */ + @javax.annotation.Nonnull + @JsonProperty(value = JSON_PROPERTY_DISPLAY_TITLE, required = true) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + + public String getDisplayTitle() { + return displayTitle; + } + + + @JsonProperty(value = JSON_PROPERTY_DISPLAY_TITLE, required = true) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public void setDisplayTitle(@javax.annotation.Nonnull String displayTitle) { + this.displayTitle = displayTitle; + } + + public SkillVersion description(@javax.annotation.Nullable String description) { + + this.description = description; + return this; + } + + /** + * 该版本的描述信息。 + * @return description + */ + @javax.annotation.Nullable + @JsonProperty(value = JSON_PROPERTY_DESCRIPTION, required = false) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + + public String getDescription() { + return description; + } + + + @JsonProperty(value = JSON_PROPERTY_DESCRIPTION, required = false) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public void setDescription(@javax.annotation.Nullable String description) { + this.description = description; + } + + public SkillVersion createdAt(@javax.annotation.Nonnull String createdAt) { + + this.createdAt = createdAt; + return this; + } + + /** + * 创建时间,RFC 3339 UTC 秒级精度。 + * @return createdAt + */ + @javax.annotation.Nonnull + @JsonProperty(value = JSON_PROPERTY_CREATED_AT, required = true) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + + public String getCreatedAt() { + return createdAt; + } + + + @JsonProperty(value = JSON_PROPERTY_CREATED_AT, required = true) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public void setCreatedAt(@javax.annotation.Nonnull String createdAt) { + this.createdAt = createdAt; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SkillVersion skillVersion = (SkillVersion) o; + return Objects.equals(this.id, skillVersion.id) && + Objects.equals(this.type, skillVersion.type) && + Objects.equals(this.skillId, skillVersion.skillId) && + Objects.equals(this.version, skillVersion.version) && + Objects.equals(this.displayTitle, skillVersion.displayTitle) && + Objects.equals(this.description, skillVersion.description) && + Objects.equals(this.createdAt, skillVersion.createdAt); + } + + @Override + public int hashCode() { + return Objects.hash(id, type, skillId, version, displayTitle, description, createdAt); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class SkillVersion {\n"); + sb.append(" id: ").append(toIndentedString(id)).append("\n"); + sb.append(" type: ").append(toIndentedString(type)).append("\n"); + sb.append(" skillId: ").append(toIndentedString(skillId)).append("\n"); + sb.append(" version: ").append(toIndentedString(version)).append("\n"); + sb.append(" displayTitle: ").append(toIndentedString(displayTitle)).append("\n"); + sb.append(" description: ").append(toIndentedString(description)).append("\n"); + sb.append(" createdAt: ").append(toIndentedString(createdAt)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + return o == null ? "null" : o.toString().replace("\n", "\n "); + } + + public static class Builder { + + private SkillVersion instance; + + public Builder() { + this(new SkillVersion()); + } + + protected Builder(SkillVersion instance) { + this.instance = instance; + } + + public SkillVersion.Builder id(String id) { + this.instance.id = id; + return this; + } + public SkillVersion.Builder type(TypeEnum type) { + this.instance.type = type; + return this; + } + public SkillVersion.Builder skillId(String skillId) { + this.instance.skillId = skillId; + return this; + } + public SkillVersion.Builder version(String version) { + this.instance.version = version; + return this; + } + public SkillVersion.Builder displayTitle(String displayTitle) { + this.instance.displayTitle = displayTitle; + return this; + } + public SkillVersion.Builder description(String description) { + this.instance.description = description; + return this; + } + public SkillVersion.Builder createdAt(String createdAt) { + this.instance.createdAt = createdAt; + return this; + } + + + /** + * returns a built SkillVersion instance. + * + * The builder is not reusable. + */ + public SkillVersion build() { + try { + return this.instance; + } finally { + // ensure that this.instance is not reused + this.instance = null; + } + } + + @Override + public String toString() { + return getClass() + "=(" + instance + ")"; + } + } + + /** + * Create a builder with no initialized field. + */ + public static SkillVersion.Builder builder() { + return new SkillVersion.Builder(); + } + + /** + * Create a builder with a shallow copy of this instance. + */ + public SkillVersion.Builder toBuilder() { + return new SkillVersion.Builder() + .id(getId()) + .type(getType()) + .skillId(getSkillId()) + .version(getVersion()) + .displayTitle(getDisplayTitle()) + .description(getDescription()) + .createdAt(getCreatedAt()); + } + + +} diff --git a/src/main/java/com/volcengine/ark/runtime/service/ArkApi.java b/src/main/java/com/volcengine/ark/runtime/service/ArkApi.java index e5af3ef..a2984f8 100644 --- a/src/main/java/com/volcengine/ark/runtime/service/ArkApi.java +++ b/src/main/java/com/volcengine/ark/runtime/service/ArkApi.java @@ -61,6 +61,7 @@ import com.volcengine.ark.runtime.models.session.SessionThread; import com.volcengine.ark.runtime.models.session.UpdateSessionRequest; import com.volcengine.ark.runtime.models.skill.Skill; +import com.volcengine.ark.runtime.models.skill.SkillVersion; import com.volcengine.ark.runtime.models.tokenization.TokenizationRequest; import com.volcengine.ark.runtime.models.tokenization.TokenizationResponse; import com.volcengine.ark.runtime.models.vault.CreateCredentialRequest; @@ -340,6 +341,13 @@ Single createSkill(@Part MultipartBody.Part files, @Part("display_title") RequestBody displayTitle, @HeaderMap Map customHeaders); + @Multipart + @POST("/api/v3/skills/{skillId}/versions") + Single createSkillVersion(@Path("skillId") String skillId, + @Part MultipartBody.Part files, + @Part("display_title") RequestBody displayTitle, + @HeaderMap Map customHeaders); + @GET("/api/v3/skills/{skillId}") Single getSkill(@Path("skillId") String skillId, @HeaderMap Map customHeaders); diff --git a/src/main/java/com/volcengine/ark/runtime/service/ArkService.java b/src/main/java/com/volcengine/ark/runtime/service/ArkService.java index 779b936..95c084f 100644 --- a/src/main/java/com/volcengine/ark/runtime/service/ArkService.java +++ b/src/main/java/com/volcengine/ark/runtime/service/ArkService.java @@ -80,6 +80,7 @@ import com.volcengine.ark.runtime.models.session.SessionThread; import com.volcengine.ark.runtime.models.session.UpdateSessionRequest; import com.volcengine.ark.runtime.models.skill.Skill; +import com.volcengine.ark.runtime.models.skill.SkillVersion; import com.volcengine.ark.runtime.models.tokenization.TokenizationRequest; import com.volcengine.ark.runtime.models.tokenization.TokenizationResponse; import com.volcengine.ark.runtime.models.vault.CreateCredentialRequest; @@ -790,6 +791,16 @@ public Skill createSkill(MultipartBody.Part files, String displayTitle) { return execute(api.createSkill(files, titlePart, new HashMap<>())); } + public SkillVersion createSkillVersion(String skillId, MultipartBody.Part files, String displayTitle) { + if (skillId == null || skillId.isEmpty()) { + throw new IllegalArgumentException("skillId is required"); + } + RequestBody titlePart = displayTitle == null + ? null + : RequestBody.create(MediaType.parse("text/plain"), displayTitle); + return execute(api.createSkillVersion(skillId, files, titlePart, new HashMap<>())); + } + public Skill getSkill(String skillId) { return execute(api.getSkill(skillId, new HashMap<>())); } diff --git a/src/test/java/com/volcengine/ark/runtime/service/ArkServiceSkillTest.java b/src/test/java/com/volcengine/ark/runtime/service/ArkServiceSkillTest.java new file mode 100644 index 0000000..621fbf4 --- /dev/null +++ b/src/test/java/com/volcengine/ark/runtime/service/ArkServiceSkillTest.java @@ -0,0 +1,170 @@ +// Copyright (c) 2026 ByteDance Ltd. and/or its affiliates. +// SPDX-License-Identifier: Apache-2.0 + +package com.volcengine.ark.runtime.service; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.volcengine.ark.runtime.models.skill.SkillVersion; +import io.reactivex.Single; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; +import okio.Buffer; +import org.junit.Test; +import retrofit2.http.POST; +import retrofit2.http.Part; +import retrofit2.http.Path; + +public class ArkServiceSkillTest { + @Test + public void createSkillVersionPassesMultipartPartsAndReturnsSkillVersion() throws Exception { + CapturingArkApi handler = new CapturingArkApi(); + ArkService service = new ArkService(api(handler)); + MultipartBody.Part files = filePart(); + + SkillVersion out = service.createSkillVersion("skill-1", files, "Readiness Skill v2"); + + assertEquals("skillver-2", out.getId()); + assertEquals(SkillVersion.TypeEnum.SKILL_VERSION, out.getType()); + assertEquals("skill-1", out.getSkillId()); + assertEquals("2", out.getVersion()); + assertEquals("Readiness Skill v2", out.getDisplayTitle()); + assertEquals("ok", out.getDescription()); + assertEquals("2026-09-14T10:11:12Z", out.getCreatedAt()); + + assertEquals("skill-1", handler.args[0]); + assertSame(files, handler.args[1]); + assertEquals("Readiness Skill v2", requestBodyToString((RequestBody) handler.args[2])); + assertTrue(((Map) handler.args[3]).isEmpty()); + } + + @Test + public void createSkillVersionOmitsDisplayTitleWhenNull() { + CapturingArkApi handler = new CapturingArkApi(); + ArkService service = new ArkService(api(handler)); + + service.createSkillVersion("skill-1", filePart(), null); + + assertNull(handler.args[2]); + } + + @Test + public void createSkillVersionRejectsMissingSkillId() { + ArkService service = new ArkService(api(new CapturingArkApi())); + + try { + service.createSkillVersion(null, filePart(), "title"); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException err) { + assertEquals("skillId is required", err.getMessage()); + } + + try { + service.createSkillVersion("", filePart(), "title"); + fail("expected IllegalArgumentException"); + } catch (IllegalArgumentException err) { + assertEquals("skillId is required", err.getMessage()); + } + } + + @Test + public void createSkillVersionApiKeepsMultipartContract() throws Exception { + Method method = ArkApi.class.getMethod( + "createSkillVersion", + String.class, + MultipartBody.Part.class, + RequestBody.class, + Map.class + ); + + assertEquals("/api/v3/skills/{skillId}/versions", method.getAnnotation(POST.class).value()); + + Annotation[][] annotations = method.getParameterAnnotations(); + assertEquals("skillId", findAnnotation(annotations[0], Path.class).value()); + assertEquals("display_title", findAnnotation(annotations[2], Part.class).value()); + } + + private static ArkApi api(CapturingArkApi handler) { + return (ArkApi) Proxy.newProxyInstance( + ArkApi.class.getClassLoader(), + new Class[] {ArkApi.class}, + handler + ); + } + + private static MultipartBody.Part filePart() { + RequestBody body = RequestBody.create( + MediaType.parse("application/zip"), + "zip-bytes".getBytes(StandardCharsets.UTF_8) + ); + return MultipartBody.Part.createFormData("files", "skill.zip", body); + } + + private static String requestBodyToString(RequestBody body) throws IOException { + Buffer buffer = new Buffer(); + body.writeTo(buffer); + return buffer.readUtf8(); + } + + private static T findAnnotation(Annotation[] annotations, Class type) { + for (Annotation annotation : annotations) { + if (type.isInstance(annotation)) { + return type.cast(annotation); + } + } + throw new AssertionError("missing annotation " + type.getSimpleName()); + } + + private static SkillVersion skillVersion() { + return SkillVersion.builder() + .id("skillver-2") + .type(SkillVersion.TypeEnum.SKILL_VERSION) + .skillId("skill-1") + .version("2") + .displayTitle("Readiness Skill v2") + .description("ok") + .createdAt("2026-09-14T10:11:12Z") + .build(); + } + + private static final class CapturingArkApi implements InvocationHandler { + private Object[] args; + + @Override + public Object invoke(Object proxy, Method method, Object[] args) { + if (method.getDeclaringClass() == Object.class) { + return objectMethod(proxy, method, args); + } + if (!"createSkillVersion".equals(method.getName())) { + throw new AssertionError("unexpected API call: " + method.getName()); + } + this.args = args; + return Single.just(skillVersion()); + } + + private Object objectMethod(Object proxy, Method method, Object[] args) { + if ("toString".equals(method.getName())) { + return "capturing ArkApi proxy"; + } + if ("hashCode".equals(method.getName())) { + return System.identityHashCode(proxy); + } + if ("equals".equals(method.getName())) { + return proxy == args[0]; + } + throw new AssertionError("unexpected Object method: " + method.getName()); + } + } +}