Skip to content

Commit 078f7cd

Browse files
dfa1claude
andcommitted
Sonar: cover new code (cli client wiring, Laya size buckets, response equals)
Main.run takes the API key file as a parameter so MainTest can drive the real client against a local HttpServer; LayaEngine.sizeBucket is extracted and tested without model files; HttpTransportResponse.equals gets its remaining branches tested. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 3a4f3ac commit 078f7cd

5 files changed

Lines changed: 145 additions & 21 deletions

File tree

‎cli/src/main/java/io/github/dfa1/typesafe/cli/Main.java‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import java.io.IOException;
1717
import java.net.URI;
1818
import java.nio.charset.StandardCharsets;
19+
import java.nio.file.Files;
20+
import java.nio.file.Path;
1921
import java.io.PrintStream;
2022
import java.util.ArrayList;
2123
import java.util.Arrays;
@@ -57,8 +59,12 @@ public static void main(String[] args) throws Exception {
5759
System.exit(run(args, System.out, System.err));
5860
}
5961

60-
/** Parses {@code args} and, on success, builds the real client and evaluates. */
6162
static int run(String[] args, PrintStream out, PrintStream err) throws Exception {
63+
return run(args, Path.of(System.getProperty("user.home"), ".typesafe.apikey"), out, err);
64+
}
65+
66+
/** Parses {@code args} and, on success, builds the real client and evaluates. */
67+
static int run(String[] args, Path apiKeyFile, PrintStream out, PrintStream err) throws Exception {
6268
if (args.length == 1 && "--version".equals(args[0])) {
6369
out.println(Main.class.getPackage().getImplementationVersion());
6470
return 0;
@@ -79,7 +85,7 @@ static int run(String[] args, PrintStream out, PrintStream err) throws Exception
7985
DefaultTypeSafeClient.Builder builder = DefaultTypeSafeClient.builder()
8086
.codec(codec)
8187
.httpTransport(new JdkHttpTransport());
82-
ApiKey apiKey = loadApiKey(parsed.endpoint());
88+
ApiKey apiKey = loadApiKey(parsed.endpoint(), apiKeyFile);
8389
if (apiKey != null) {
8490
builder.apiKey(apiKey);
8591
}
@@ -94,15 +100,11 @@ static int run(String[] args, PrintStream out, PrintStream err) throws Exception
94100
/** The key file is required against the default endpoint (the real API needs auth), but
95101
* optional against any {@code --endpoint} override (a local/self-hosted server may need
96102
* none). */
97-
private static ApiKey loadApiKey(URI endpoint) throws IOException {
98-
if (endpoint == null) {
99-
return ApiKey.fromDefaultFile();
100-
}
101-
try {
102-
return ApiKey.fromDefaultFile();
103-
} catch (IOException e) {
104-
return null;
103+
private static ApiKey loadApiKey(URI endpoint, Path apiKeyFile) throws IOException {
104+
if (endpoint == null || Files.exists(apiKeyFile)) {
105+
return ApiKey.fromFile(apiKeyFile);
105106
}
107+
return null;
106108
}
107109

108110
/** The evaluate-and-print flow, taking an already-built client so it's testable without a

‎cli/src/test/java/io/github/dfa1/typesafe/cli/MainTest.java‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,29 @@
77
import io.github.dfa1.typesafe.client.TypeSafeClient;
88
import io.github.dfa1.typesafe.core.Usage;
99
import io.github.dfa1.typesafe.codec.jackson3.Jackson3Codec;
10+
import com.sun.net.httpserver.HttpServer;
11+
import org.junit.jupiter.api.AfterEach;
1012
import org.junit.jupiter.api.Test;
1113
import org.junit.jupiter.api.extension.ExtendWith;
14+
import org.junit.jupiter.api.io.TempDir;
1215
import org.mockito.Mock;
1316
import org.mockito.junit.jupiter.MockitoExtension;
1417

1518
import java.io.ByteArrayOutputStream;
1619
import java.io.PrintStream;
20+
import java.net.InetSocketAddress;
1721
import java.net.URI;
22+
import java.nio.charset.StandardCharsets;
23+
import java.nio.file.Files;
24+
import java.nio.file.NoSuchFileException;
25+
import java.nio.file.Path;
26+
import java.util.ArrayList;
1827
import java.util.List;
1928
import java.util.Map;
2029

2130
import static org.assertj.core.api.Assertions.assertThat;
2231
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
32+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2333
import static org.mockito.ArgumentMatchers.any;
2434
import static org.mockito.BDDMockito.given;
2535

@@ -35,6 +45,15 @@ class MainTest {
3545
private final PrintStream out = new PrintStream(outBuffer);
3646
private final PrintStream err = new PrintStream(errBuffer);
3747

48+
@TempDir
49+
private Path tempDir;
50+
51+
private Path apiKeyFile;
52+
53+
private HttpServer server;
54+
55+
private final List<String> authorizations = new ArrayList<>();
56+
3857
@Test
3958
void parseReadsStateModelAndAllFlagTypes() {
4059
// When
@@ -424,6 +443,76 @@ void runFromArgsReturns1AndPrintsUsageWhenArgsAreInvalid() throws Exception {
424443
assertThat(errBuffer.toString()).contains("Unknown flag: --bogus").contains("Usage: typesafe");
425444
}
426445

446+
@Test
447+
void runFromArgsRequiresTheKeyFileAgainstTheDefaultEndpoint() {
448+
// Given
449+
apiKeyFile = tempDir.resolve("missing.apikey");
450+
451+
// When / Then
452+
assertThatThrownBy(() -> Main.run(new String[] {"--state", "hi", "--noul", "Is this urgent?"},
453+
apiKeyFile, out, err))
454+
.isInstanceOf(NoSuchFileException.class);
455+
}
456+
457+
@Test
458+
void runFromArgsCallsTheEndpointWithoutAuthorizationWhenThereIsNoKeyFile() throws Exception {
459+
// Given
460+
URI endpoint = startServer();
461+
apiKeyFile = tempDir.resolve("missing.apikey");
462+
463+
// When
464+
int result = Main.run(new String[] {
465+
"--state", "hi", "--endpoint", endpoint.toString(), "--noul", "urgent=Is this urgent?",
466+
"--print", "urgent"
467+
}, apiKeyFile, out, err);
468+
469+
// Then
470+
assertThat(result).isZero();
471+
assertThat(outBuffer.toString()).isEqualToIgnoringNewLines("0.92");
472+
assertThat(authorizations).containsExactly((String) null);
473+
}
474+
475+
@Test
476+
void runFromArgsCallsTheEndpointWithTheKeyFromTheKeyFile() throws Exception {
477+
// Given
478+
URI endpoint = startServer();
479+
apiKeyFile = Files.writeString(tempDir.resolve("typesafe.apikey"), "secret\n");
480+
481+
// When
482+
int result = Main.run(new String[] {
483+
"--state", "hi", "--endpoint", endpoint.toString(), "--noul", "urgent=Is this urgent?"
484+
}, apiKeyFile, out, err);
485+
486+
// Then
487+
assertThat(result).isZero();
488+
assertThat(authorizations).containsExactly("Bearer secret");
489+
}
490+
491+
@AfterEach
492+
void stopServer() {
493+
if (server != null) {
494+
server.stop(0);
495+
}
496+
}
497+
498+
/** A local TypeSafe-compatible server answering every evaluate with one noul answer. */
499+
private URI startServer() throws Exception {
500+
byte[] body = """
501+
{"model": "jev-latest", "answers": {"urgent": {"type": "noul", "noul": 0.92}},
502+
"usage": {"input_tokens": 1, "output_tokens": 1}}
503+
""".getBytes(StandardCharsets.UTF_8);
504+
server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
505+
server.createContext("/v1/systemone", exchange -> {
506+
authorizations.add(exchange.getRequestHeaders().getFirst("Authorization"));
507+
exchange.getResponseHeaders().add("Content-Type", "application/json");
508+
exchange.sendResponseHeaders(200, body.length);
509+
exchange.getResponseBody().write(body);
510+
exchange.close();
511+
});
512+
server.start();
513+
return URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/v1/systemone");
514+
}
515+
427516
private static EvaluateResponse response(Map<String, Answer> answers) {
428517
return new EvaluateResponse(Model.LATEST, answers, new Usage(0, 0),
429518
new EvaluateResponse.Metadata(null, null));

‎client-http/src/test/java/io/github/dfa1/typesafe/client/http/HttpTransportResponseTest.java‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,17 @@ void equalsComparesTheBodyByContent() {
6060
assertThat(new HttpTransportResponse(200, Map.of("X-A", "1"), bytes("other"))).isNotEqualTo(sut);
6161
}
6262

63+
@Test
64+
void equalsComparesStatusCodeAndHeadersToo() {
65+
// Given
66+
HttpTransportResponse sut = new HttpTransportResponse(200, Map.of("X-A", "1"), bytes("body"));
67+
68+
// Then
69+
assertThat(new HttpTransportResponse(404, Map.of("X-A", "1"), bytes("body"))).isNotEqualTo(sut);
70+
assertThat(new HttpTransportResponse(200, Map.of("X-A", "2"), bytes("body"))).isNotEqualTo(sut);
71+
assertThat(sut).isNotEqualTo("body");
72+
}
73+
6374
private static byte[] bytes(String text) {
6475
return text.getBytes(StandardCharsets.UTF_8);
6576
}

‎client-local/src/main/java/io/github/dfa1/typesafe/client/local/LayaEngine.java‎

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -245,17 +245,7 @@ private static OnnxTensor bools(long[] mask, int n, int k) throws OrtException {
245245

246246
/** rl_agent_api: logits / temperature for this (type, option count) bucket, softmaxed over the real options. */
247247
private double[] probabilities(float[] logits, int k, int qtype) {
248-
String size;
249-
if (k <= 2) {
250-
size = "2";
251-
} else if (k <= 5) {
252-
size = "3-5";
253-
} else if (k <= 10) {
254-
size = "6-10";
255-
} else {
256-
size = "11+";
257-
}
258-
String bucket = List.of("choice", "score", "noul").get(qtype) + ":" + size;
248+
String bucket = List.of("choice", "score", "noul").get(qtype) + ":" + sizeBucket(k);
259249
double t = temperatureByOptions.getOrDefault(bucket, temperature[qtype]);
260250
float[] z = new float[k];
261251
for (int i = 0; i < k; i++) {
@@ -264,6 +254,18 @@ private double[] probabilities(float[] logits, int k, int qtype) {
264254
return Probabilities.softmax(z);
265255
}
266256

257+
/** The option-count part of a temperature bucket name, as rl_agent_api spells it. */
258+
static String sizeBucket(int k) {
259+
if (k <= 2) {
260+
return "2";
261+
} else if (k <= 5) {
262+
return "3-5";
263+
} else if (k <= 10) {
264+
return "6-10";
265+
}
266+
return "11+";
267+
}
268+
267269
/** Everything but the markers is caller text, so [CLS]/[SEP] spelled there stay text. */
268270
private long[] encode(String text) {
269271
return tokenizer.encodeText(text);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package io.github.dfa1.typesafe.client.local;
2+
3+
import org.junit.jupiter.params.ParameterizedTest;
4+
import org.junit.jupiter.params.provider.CsvSource;
5+
6+
import static org.assertj.core.api.Assertions.assertThat;
7+
8+
/** Untagged: needs no model files, unlike {@link LayaEngineTest}. */
9+
class LayaEngineSizeBucketTest {
10+
11+
@ParameterizedTest
12+
@CsvSource({"2, 2", "3, 3-5", "5, 3-5", "6, 6-10", "10, 6-10", "11, 11+", "40, 11+"})
13+
void sizeBucketGroupsOptionCountsLikeRlAgentApi(int k, String expected) {
14+
// When
15+
String result = LayaEngine.sizeBucket(k);
16+
17+
// Then
18+
assertThat(result).isEqualTo(expected);
19+
}
20+
}

0 commit comments

Comments
 (0)