|
| 1 | +/*--------------------------------------------------------------------------------------------- |
| 2 | + * Copyright (c) Microsoft Corporation. All rights reserved. |
| 3 | + *--------------------------------------------------------------------------------------------*/ |
| 4 | + |
| 5 | +package com.github.copilot.tool; |
| 6 | + |
| 7 | +import static org.junit.jupiter.api.Assertions.*; |
| 8 | + |
| 9 | +import java.lang.annotation.ElementType; |
| 10 | +import java.lang.annotation.Retention; |
| 11 | +import java.lang.annotation.RetentionPolicy; |
| 12 | +import java.lang.annotation.Target; |
| 13 | +import java.lang.reflect.Method; |
| 14 | +import java.lang.reflect.Parameter; |
| 15 | +import java.util.concurrent.CompletableFuture; |
| 16 | + |
| 17 | +import org.junit.jupiter.api.Test; |
| 18 | + |
| 19 | +import com.github.copilot.CopilotExperimental; |
| 20 | +import com.github.copilot.rpc.ToolDefer; |
| 21 | + |
| 22 | +/** |
| 23 | + * Unit tests for {@link CopilotTool} and {@link Param} annotations. |
| 24 | + */ |
| 25 | +public class CopilotToolAnnotationTest { |
| 26 | + |
| 27 | + // --- @CopilotTool attribute verification --- |
| 28 | + |
| 29 | + @Test |
| 30 | + void copilotToolHasRuntimeRetention() { |
| 31 | + Retention retention = CopilotTool.class.getAnnotation(Retention.class); |
| 32 | + assertNotNull(retention); |
| 33 | + assertEquals(RetentionPolicy.RUNTIME, retention.value()); |
| 34 | + } |
| 35 | + |
| 36 | + @Test |
| 37 | + void copilotToolTargetsMethod() { |
| 38 | + Target target = CopilotTool.class.getAnnotation(Target.class); |
| 39 | + assertNotNull(target); |
| 40 | + assertArrayEquals(new ElementType[] {ElementType.METHOD}, target.value()); |
| 41 | + } |
| 42 | + |
| 43 | + @Test |
| 44 | + void copilotToolIsAnnotatedWithCopilotExperimental() { |
| 45 | + // @CopilotExperimental has CLASS retention so it is not visible via |
| 46 | + // reflection at runtime. However, we can confirm: |
| 47 | + // 1. The annotation type targets TYPE (which includes @interface declarations). |
| 48 | + // 2. Compilation succeeded with @CopilotExperimental on @CopilotTool |
| 49 | + // (the CopilotExperimentalProcessor would reject usage otherwise). |
| 50 | + Target expTarget = CopilotExperimental.class.getAnnotation(Target.class); |
| 51 | + assertNotNull(expTarget); |
| 52 | + boolean includesType = false; |
| 53 | + for (ElementType et : expTarget.value()) { |
| 54 | + if (et == ElementType.TYPE) { |
| 55 | + includesType = true; |
| 56 | + break; |
| 57 | + } |
| 58 | + } |
| 59 | + assertTrue(includesType, |
| 60 | + "@CopilotExperimental must target TYPE to be applicable to annotation declarations"); |
| 61 | + } |
| 62 | + |
| 63 | + @Test |
| 64 | + void copilotToolDefaultValues() throws Exception { |
| 65 | + Method nameMethod = CopilotTool.class.getDeclaredMethod("name"); |
| 66 | + assertEquals("", nameMethod.getDefaultValue()); |
| 67 | + |
| 68 | + Method overridesMethod = CopilotTool.class.getDeclaredMethod("overridesBuiltInTool"); |
| 69 | + assertEquals(false, overridesMethod.getDefaultValue()); |
| 70 | + |
| 71 | + Method skipMethod = CopilotTool.class.getDeclaredMethod("skipPermission"); |
| 72 | + assertEquals(false, skipMethod.getDefaultValue()); |
| 73 | + |
| 74 | + Method deferMethod = CopilotTool.class.getDeclaredMethod("defer"); |
| 75 | + assertEquals(ToolDefer.NONE, deferMethod.getDefaultValue()); |
| 76 | + } |
| 77 | + |
| 78 | + // --- @Param attribute verification --- |
| 79 | + |
| 80 | + @Test |
| 81 | + void paramHasRuntimeRetention() { |
| 82 | + Retention retention = Param.class.getAnnotation(Retention.class); |
| 83 | + assertNotNull(retention); |
| 84 | + assertEquals(RetentionPolicy.RUNTIME, retention.value()); |
| 85 | + } |
| 86 | + |
| 87 | + @Test |
| 88 | + void paramTargetsParameter() { |
| 89 | + Target target = Param.class.getAnnotation(Target.class); |
| 90 | + assertNotNull(target); |
| 91 | + assertArrayEquals(new ElementType[] {ElementType.PARAMETER}, target.value()); |
| 92 | + } |
| 93 | + |
| 94 | + @Test |
| 95 | + void paramDefaultValues() throws Exception { |
| 96 | + Method valueMethod = Param.class.getDeclaredMethod("value"); |
| 97 | + assertEquals("", valueMethod.getDefaultValue()); |
| 98 | + |
| 99 | + Method nameMethod = Param.class.getDeclaredMethod("name"); |
| 100 | + assertEquals("", nameMethod.getDefaultValue()); |
| 101 | + |
| 102 | + Method requiredMethod = Param.class.getDeclaredMethod("required"); |
| 103 | + assertEquals(true, requiredMethod.getDefaultValue()); |
| 104 | + |
| 105 | + Method defaultValueMethod = Param.class.getDeclaredMethod("defaultValue"); |
| 106 | + assertEquals("", defaultValueMethod.getDefaultValue()); |
| 107 | + } |
| 108 | + |
| 109 | + // --- Applicability test --- |
| 110 | + |
| 111 | + @SuppressWarnings("unused") |
| 112 | + static class SampleToolHolder { |
| 113 | + |
| 114 | + @CopilotTool(value = "Get weather for a location", name = "get_weather", defer = ToolDefer.AUTO) |
| 115 | + public CompletableFuture<String> getWeather( |
| 116 | + @Param(value = "City name", required = true) String location, |
| 117 | + @Param(value = "Temperature unit", required = false, defaultValue = "celsius") String unit) { |
| 118 | + return CompletableFuture.completedFuture("Sunny in " + location); |
| 119 | + } |
| 120 | + } |
| 121 | + |
| 122 | + @Test |
| 123 | + void annotationsAreAccessibleViaReflection() throws Exception { |
| 124 | + Method method = SampleToolHolder.class.getDeclaredMethod("getWeather", String.class, String.class); |
| 125 | + |
| 126 | + CopilotTool toolAnnotation = method.getAnnotation(CopilotTool.class); |
| 127 | + assertNotNull(toolAnnotation); |
| 128 | + assertEquals("Get weather for a location", toolAnnotation.value()); |
| 129 | + assertEquals("get_weather", toolAnnotation.name()); |
| 130 | + assertFalse(toolAnnotation.overridesBuiltInTool()); |
| 131 | + assertFalse(toolAnnotation.skipPermission()); |
| 132 | + assertEquals(ToolDefer.AUTO, toolAnnotation.defer()); |
| 133 | + |
| 134 | + Parameter[] params = method.getParameters(); |
| 135 | + assertEquals(2, params.length); |
| 136 | + |
| 137 | + Param locationParam = params[0].getAnnotation(Param.class); |
| 138 | + assertNotNull(locationParam); |
| 139 | + assertEquals("City name", locationParam.value()); |
| 140 | + assertTrue(locationParam.required()); |
| 141 | + assertEquals("", locationParam.defaultValue()); |
| 142 | + |
| 143 | + Param unitParam = params[1].getAnnotation(Param.class); |
| 144 | + assertNotNull(unitParam); |
| 145 | + assertEquals("Temperature unit", unitParam.value()); |
| 146 | + assertFalse(unitParam.required()); |
| 147 | + assertEquals("celsius", unitParam.defaultValue()); |
| 148 | + } |
| 149 | +} |
0 commit comments