Drive Kotlin Test Clinic from your own code
Everything the web page does is available over HTTP: paste Kotlin source and its tests in, get the
same structured test-quality review back. The review knows Kotest, JUnit and kotlin.test, MockK,
coroutine testing, Flow collection, property-based testing and Kover. The natural use is a CI job
that re-reviews whenever a spec changes, or a script that runs the same ten-check table across
every module in a monorepo and fails the build when one of them drifts into
untested-risk.
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api and every response uses
the same envelope, so one helper covers the whole API:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }
Send your app slug as X-App-Slug: kotlin-test-clinic and your token as
Authorization: Bearer … on every call.
Error codes
| code | status | what to do |
|---|---|---|
unauthorized | 401 | The token is missing, malformed or expired. Get a new one from the token page. |
payment_required | 402 | The balance is below min_credits. Call /estimate first and top up. |
forbidden | 403 | The token is valid but not for this app, or a guest token tried a metered run. Check the X-App-Slug header. |
not_found | 404 | Unknown job id, unknown collection, or the app slug does not exist. |
conflict | 409 | The same Idempotency-Key was replayed with a different body. Change the key or send the original input. |
validation_error | 422 | The input object is missing a required field — files is the usual one — or a field is the wrong type. |
rate_limited | 429 | Too many requests. Back off and retry; do not tight-loop. |
internal | 5xx | A server-side failure. Retry with the SAME Idempotency-Key so you are not billed twice. |
1. A tiny client
One helper that adds the headers, unwraps data and raises on error.
# Every call is the same three things: the base URL, your bearer token,
# and a JSON body. Keep the token in a shell variable.
BASE="https://api.skillsafe.ai/v1/app-api"
SLUG="kotlin-test-clinic"
TOKEN="YOUR_TOKEN" # from https://kotlin-test-clinic.skillsafe.ai/tokens.html
call() { # call <path> [json-body]
if [ -n "$2" ]; then
curl -sS -X POST "$BASE/$1" \
-H "Authorization: Bearer $TOKEN" \
-H "X-App-Slug: $SLUG" \
-H "Content-Type: application/json" \
-d "$2"
else
curl -sS "$BASE/$1" -H "Authorization: Bearer $TOKEN" -H "X-App-Slug: $SLUG"
fi
}
import json, os, urllib.error, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "kotlin-test-clinic"
TOKEN = os.environ.get("SKILLSAFE_TOKEN", "YOUR_TOKEN") # from https://kotlin-test-clinic.skillsafe.ai/tokens.html
def call(path, body=None):
"""Returns the unwrapped `data`, or raises with the API error code."""
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(f"{BASE}/{path}", data=data, method="POST" if body is not None else "GET")
req.add_header("Authorization", f"Bearer {TOKEN}")
req.add_header("X-App-Slug", SLUG)
if body is not None:
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req) as r:
payload = json.load(r)
except urllib.error.HTTPError as e:
payload = json.load(e)
if not payload.get("ok"):
err = payload.get("error", {})
raise RuntimeError(f"{err.get('code')}: {err.get('message')}")
return payload["data"]
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "kotlin-test-clinic";
const TOKEN = "YOUR_TOKEN"; // from https://kotlin-test-clinic.skillsafe.ai/tokens.html
async function call(path, body) {
const res = await fetch(`${BASE}/${path}`, {
method: body ? "POST" : "GET",
headers: {
Authorization: `Bearer ${TOKEN}`,
"X-App-Slug": SLUG,
...(body ? { "Content-Type": "application/json" } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const payload = await res.json();
if (!payload.ok) throw new Error(`${payload.error.code}: ${payload.error.message}`);
return payload.data;
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
const (
base = "https://api.skillsafe.ai/v1/app-api"
slug = "kotlin-test-clinic"
)
var token = os.Getenv("SKILLSAFE_TOKEN") // from https://kotlin-test-clinic.skillsafe.ai/tokens.html
type envelope struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func call(path string, body any) (json.RawMessage, error) {
method := http.MethodGet
var rdr io.Reader
if body != nil {
method = http.MethodPost
b, _ := json.Marshal(body)
rdr = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, base+"/"+path, rdr)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("X-App-Slug", slug)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var env envelope
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return nil, err
}
if !env.OK {
return nil, fmt.Errorf("%s: %s", env.Error.Code, env.Error.Message)
}
return env.Data, nil
}
import java.net.URI;
import java.net.http.*;
public class Clinic {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String SLUG = "kotlin-test-clinic";
static final String TOKEN = System.getenv().getOrDefault("SKILLSAFE_TOKEN", "YOUR_TOKEN");
static final HttpClient HTTP = HttpClient.newHttpClient();
static String call(String path, String jsonBody) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + "/" + path))
.header("Authorization", "Bearer " + TOKEN)
.header("X-App-Slug", SLUG);
if (jsonBody != null) {
b.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody));
} else {
b.GET();
}
HttpResponse<String> res = HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString());
// The envelope is always {"ok":...,"data":...} or {"ok":false,"error":...}.
return res.body();
}
}
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
SLUG = "kotlin-test-clinic"
TOKEN = ENV.fetch("SKILLSAFE_TOKEN", "YOUR_TOKEN") # from https://kotlin-test-clinic.skillsafe.ai/tokens.html
def call(path, body = nil)
uri = URI("#{BASE}/#{path}")
req = body ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["X-App-Slug"] = SLUG
if body
req["Content-Type"] = "application/json"
req.body = JSON.generate(body)
end
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
payload = JSON.parse(res.body)
raise "#{payload['error']['code']}: #{payload['error']['message']}" unless payload["ok"]
payload["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
const SLUG = "kotlin-test-clinic";
define("TOKEN", getenv("SKILLSAFE_TOKEN") ?: "YOUR_TOKEN"); // from /tokens.html
function call(string $path, ?array $body = null) {
$ch = curl_init(BASE . "/" . $path);
$headers = ["Authorization: Bearer " . TOKEN, "X-App-Slug: " . SLUG];
if ($body !== null) {
$headers[] = "Content-Type: application/json";
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$payload = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($payload["ok"])) {
throw new RuntimeException($payload["error"]["code"] . ": " . $payload["error"]["message"]);
}
return $payload["data"];
}
using System.Net.Http.Json;
using System.Text.Json;
static class Clinic
{
const string Base = "https://api.skillsafe.ai/v1/app-api";
const string Slug = "kotlin-test-clinic";
static readonly string Token =
Environment.GetEnvironmentVariable("SKILLSAFE_TOKEN") ?? "YOUR_TOKEN";
static readonly HttpClient Http = new();
public static async Task<JsonElement> Call(string path, object? body = null)
{
var req = new HttpRequestMessage(body is null ? HttpMethod.Get : HttpMethod.Post, $"{Base}/{path}");
req.Headers.Add("Authorization", $"Bearer {Token}");
req.Headers.Add("X-App-Slug", Slug);
if (body is not null) req.Content = JsonContent.Create(body);
var res = await Http.SendAsync(req);
var payload = await res.Content.ReadFromJsonAsync<JsonElement>();
if (!payload.GetProperty("ok").GetBoolean())
{
var e = payload.GetProperty("error");
throw new Exception($"{e.GetProperty("code")}: {e.GetProperty("message")}");
}
return payload.GetProperty("data");
}
}
2. Get a token
The easiest route is the token page: it shows the token this browser already holds, with Copy token and Copy shell export buttons, and a sign-in button for a personal token. You never need to open the developer console.
A guest token can call /me and /estimate. Running a
review is metered, so it needs a personal token from signing in.
# A guest token is enough for /me and /estimate. Running a review is metered and
# needs a personal token: open the token page and press "Sign in".
#
# https://kotlin-test-clinic.skillsafe.ai/tokens.html
#
# That page also gives you a ready-made shell export:
# export SKILLSAFE_TOKEN="..."
#
# To mint a guest token from the command line instead:
curl -sS -X POST "https://api.skillsafe.ai/v1/app-api/guest" -H "X-App-Slug: kotlin-test-clinic"
# Open https://kotlin-test-clinic.skillsafe.ai/tokens.html and press "Copy token".
# The token page exists so you never have to dig a token out of the browser
# yourself; it also prints the `export SKILLSAFE_TOKEN=...` line.
#
# A guest token, which can call /me and /estimate but cannot run:
guest = call("guest")
TOKEN = guest["token"]
// Open https://kotlin-test-clinic.skillsafe.ai/tokens.html and press "Copy token".
// A guest token can call /me and /estimate but cannot run a metered review.
const guest = await call("guest");
// Use guest.token as the bearer for subsequent calls.
// Open https://kotlin-test-clinic.skillsafe.ai/tokens.html and press "Copy token".
// Or mint a guest token, which can call /me and /estimate but cannot run:
raw, err := call("guest", map[string]any{})
if err != nil {
panic(err)
}
var guest struct {
Token string `json:"token"`
}
_ = json.Unmarshal(raw, &guest)
// Open https://kotlin-test-clinic.skillsafe.ai/tokens.html and press "Copy token".
// A guest token can call /me and /estimate but cannot run a metered review.
String guest = call("guest", "{}");
System.out.println(guest);
# Open https://kotlin-test-clinic.skillsafe.ai/tokens.html and press "Copy token".
# A guest token can call /me and /estimate but cannot run a metered review.
guest = call("guest", {})
puts guest["token"]
<?php
// Open https://kotlin-test-clinic.skillsafe.ai/tokens.html and press "Copy token".
// A guest token can call /me and /estimate but cannot run a metered review.
$guest = call("guest", []);
echo $guest["token"];
// Open https://kotlin-test-clinic.skillsafe.ai/tokens.html and press "Copy token".
// A guest token can call /me and /estimate but cannot run a metered review.
var guest = await Clinic.Call("guest", new { });
Console.WriteLine(guest.GetProperty("token").GetString());
3. Check the session and the balance
GET /me tells you whether the token is a guest or a person, and what the balance is.
Compare it against min_credits from the next step before you run, so a shortfall
surfaces as your own clear message rather than a 402.
call me
# {"ok":true,"data":{"subject_type":"user","username":"you","credits":51234}}
me = call("me")
print(me["subject_type"], me.get("credits"))
const me = await call("me");
console.log(me.subject_type, me.credits);
raw, err := call("me", nil)
if err != nil {
panic(err)
}
var me struct {
SubjectType string `json:"subject_type"`
Credits int `json:"credits"`
}
_ = json.Unmarshal(raw, &me)
fmt.Println(me.SubjectType, me.Credits)
System.out.println(call("me", null));
me = call("me")
puts "#{me['subject_type']} #{me['credits']}"
<?php
$me = call("me");
echo $me["subject_type"], " ", $me["credits"], PHP_EOL;
var me = await Clinic.Call("me");
Console.WriteLine(me.GetProperty("subject_type").GetString());
4. Price the run — free
The input object is exactly what the app's own form submits:
| field | type | meaning |
|---|---|---|
files | string, required | The pasted files — production Kotlin source, its test files, and optionally build.gradle.kts, libs.versions.toml or a CI workflow. Put a // file: OrderService.kt marker line above each one so they can be told apart (a # file: marker is equivalent). This is the review's only evidence. A file whose middle has been removed should say so with a [... N characters cut ...] comment. |
focus | string | general, mocking, coroutines, flows, property-based, coverage or spec-style. Emphasis, not exclusivity: a high-severity finding from another area is never suppressed. |
context | string, optional | Free-form notes: what the module does, what keeps breaking, what the team argued about, CI constraints, the deadline. |
prescan_facts | object | {resources: [{id,label}], flags: [{id,label}]} — what a deterministic client-side scan established: files, spec classes and styles, test counts, mock declarations, suspend functions, Flow usages and test-stack dependencies in resources; checks that fired in flags. Every flags id must come back in coverage_check, which is how you hold the model to the facts. |
retry_note | string, optional | Send only on a retry, when a previous reply failed to parse or came back truncated. The instruction is obeyed exactly. |
/estimate creates no job and charges nothing. It returns the model
binding — model, model_alias, markup_bps — and the
reservation: hold_credits is what gets held, min_credits is the balance
you must clear to start, and sponsor_enabled says whether the app is covering the run.
The actual charge is normally far lower than the hold, because the hold prices the full output cap.
INPUT='{"files": "// file: OrderService.kt\nclass OrderService(private val payments: PaymentService) {\n suspend fun placeOrder(req: OrderRequest): Order =\n Order(req.userId, payments.charge(req.total).status)\n}\n\n// file: OrderServiceTest.kt\nclass OrderServiceTest : FunSpec({\n val payments: PaymentService = mockk(relaxed = true)\n test(\"placeOrder succeeds\") {\n Thread.sleep(500)\n OrderService(payments).placeOrder(OrderRequest(\"u1\", 40))\n }\n})", "focus": "coroutines", "context": "Payments module, 4 engineers. The suite is green but a checkout bug shipped last week.", "prescan_facts": {"resources": [{"id": "spec:OrderServiceTest", "label": "FunSpec, 1 test"}, {"id": "src:suspend", "label": "1 suspend function: placeOrder"}], "flags": [{"id": "coroutines:thread-sleep", "label": "1 Thread.sleep call in test code"}, {"id": "tests:no-assertions", "label": "1 test body asserts nothing"}]}}'
call estimate "$INPUT"
# {"ok":true,"data":{"model":"gpt-5.6-terra","model_alias":"gpt-terra",
# "markup_bps":1000,"hold_credits":1890,"min_credits":280,"sponsor_enabled":false}}
#
# estimate is FREE. It creates no job and charges nothing. hold_credits is what
# gets RESERVED; the charge afterwards is normally much lower.
INPUT = {
"files": "// file: OrderService.kt\nclass OrderService(private val payments: PaymentService) {\n suspend fun placeOrder(req: OrderRequest): Order =\n Order(req.userId, payments.charge(req.total).status)\n}\n\n// file: OrderServiceTest.kt\nclass OrderServiceTest : FunSpec({\n val payments: PaymentService = mockk(relaxed = true)\n test(\"placeOrder succeeds\") {\n Thread.sleep(500)\n OrderService(payments).placeOrder(OrderRequest(\"u1\", 40))\n }\n})",
"focus": "coroutines",
"context": "Payments module, 4 engineers. The suite is green but a checkout bug shipped last week.",
"prescan_facts": {
"resources": [
{"id": "spec:OrderServiceTest", "label": "FunSpec, 1 test"},
{"id": "src:suspend", "label": "1 suspend function: placeOrder"}
],
"flags": [
{"id": "coroutines:thread-sleep", "label": "1 Thread.sleep call in test code"},
{"id": "tests:no-assertions", "label": "1 test body asserts nothing"}
]
}
}
est = call("estimate", INPUT)
print(est["model"], est["model_alias"], est["markup_bps"])
print(est["hold_credits"], est["min_credits"], est["sponsor_enabled"])
# estimate is free: no job is created and nothing is charged.
const INPUT = {
files:
'// file: OrderService.kt\nclass OrderService(private val payments: PaymentService) {\n suspend fun placeOrder(req: OrderRequest): Order =\n Order(req.userId, payments.charge(req.total).status)\n}\n\n// file: OrderServiceTest.kt\nclass OrderServiceTest : FunSpec({\n val payments: PaymentService = mockk(relaxed = true)\n test("placeOrder succeeds") {\n Thread.sleep(500)\n OrderService(payments).placeOrder(OrderRequest("u1", 40))\n }\n})',
focus: "coroutines",
context: "Payments module, 4 engineers. The suite is green but a checkout bug shipped last week.",
prescan_facts: {
resources: [
{ id: "spec:OrderServiceTest", label: "FunSpec, 1 test" },
{ id: "src:suspend", label: "1 suspend function: placeOrder" },
],
flags: [
{ id: "coroutines:thread-sleep", label: "1 Thread.sleep call in test code" },
{ id: "tests:no-assertions", label: "1 test body asserts nothing" },
],
},
};
const est = await call("estimate", INPUT);
console.log(est.model, est.model_alias, est.markup_bps);
console.log(est.hold_credits, est.min_credits, est.sponsor_enabled);
// estimate is free: no job is created and nothing is charged.
input := map[string]any{
"files": "// file: OrderService.kt\nclass OrderService(private val payments: PaymentService) {\n suspend fun placeOrder(req: OrderRequest): Order =\n Order(req.userId, payments.charge(req.total).status)\n}\n\n// file: OrderServiceTest.kt\nclass OrderServiceTest : FunSpec({\n val payments: PaymentService = mockk(relaxed = true)\n test(\"placeOrder succeeds\") {\n Thread.sleep(500)\n OrderService(payments).placeOrder(OrderRequest(\"u1\", 40))\n }\n})",
"focus": "coroutines",
"context": "Payments module, 4 engineers. The suite is green but a checkout bug shipped last week.",
"prescan_facts": map[string]any{
"resources": []any{
map[string]string{"id": "spec:OrderServiceTest", "label": "FunSpec, 1 test"},
map[string]string{"id": "src:suspend", "label": "1 suspend function: placeOrder"},
},
"flags": []any{
map[string]string{"id": "coroutines:thread-sleep", "label": "1 Thread.sleep call in test code"},
map[string]string{"id": "tests:no-assertions", "label": "1 test body asserts nothing"},
},
},
}
raw, err := call("estimate", input)
if err != nil {
panic(err)
}
fmt.Println(string(raw)) // estimate is free - no job, no charge
String input = """
{
"files": "// file: OrderService.kt\\nclass OrderService(private val payments: PaymentService) {\\n suspend fun placeOrder(req: OrderRequest): Order =\\n Order(req.userId, payments.charge(req.total).status)\\n}\\n\\n// file: OrderServiceTest.kt\\nclass OrderServiceTest : FunSpec({\\n val payments: PaymentService = mockk(relaxed = true)\\n test(\\"placeOrder succeeds\\") {\\n Thread.sleep(500)\\n OrderService(payments).placeOrder(OrderRequest(\\"u1\\", 40))\\n }\\n})",
"focus": "coroutines",
"context": "Payments module, 4 engineers. The suite is green but a checkout bug shipped last week.",
"prescan_facts": {
"resources": [
{ "id": "spec:OrderServiceTest", "label": "FunSpec, 1 test" },
{ "id": "src:suspend", "label": "1 suspend function: placeOrder" }
],
"flags": [
{ "id": "coroutines:thread-sleep", "label": "1 Thread.sleep call in test code" },
{ "id": "tests:no-assertions", "label": "1 test body asserts nothing" }
]
}
}
""";
System.out.println(call("estimate", input));
// estimate is free: no job is created and nothing is charged.
// The data object carries model, model_alias, markup_bps, hold_credits,
// min_credits and sponsor_enabled.
input = {
"files" => "// file: OrderService.kt\nclass OrderService(private val payments: PaymentService) {\n suspend fun placeOrder(req: OrderRequest): Order =\n Order(req.userId, payments.charge(req.total).status)\n}\n\n// file: OrderServiceTest.kt\nclass OrderServiceTest : FunSpec({\n val payments: PaymentService = mockk(relaxed = true)\n test(\"placeOrder succeeds\") {\n Thread.sleep(500)\n OrderService(payments).placeOrder(OrderRequest(\"u1\", 40))\n }\n})",
"focus" => "coroutines",
"context" => "Payments module, 4 engineers. The suite is green but a checkout bug shipped last week.",
"prescan_facts" => {
"resources" => [
{ "id" => "spec:OrderServiceTest", "label" => "FunSpec, 1 test" },
{ "id" => "src:suspend", "label" => "1 suspend function: placeOrder" }
],
"flags" => [
{ "id" => "coroutines:thread-sleep", "label" => "1 Thread.sleep call in test code" },
{ "id" => "tests:no-assertions", "label" => "1 test body asserts nothing" }
]
}
}
est = call("estimate", input)
puts "#{est['model']} hold=#{est['hold_credits']} min=#{est['min_credits']}"
# estimate is free: no job is created and nothing is charged.
<?php
$input = [
"files" => "// file: OrderService.kt\nclass OrderService(private val payments: PaymentService) {\n suspend fun placeOrder(req: OrderRequest): Order =\n Order(req.userId, payments.charge(req.total).status)\n}\n\n// file: OrderServiceTest.kt\nclass OrderServiceTest : FunSpec({\n val payments: PaymentService = mockk(relaxed = true)\n test(\"placeOrder succeeds\") {\n Thread.sleep(500)\n OrderService(payments).placeOrder(OrderRequest(\"u1\", 40))\n }\n})",
"focus" => "coroutines",
"context" => "Payments module, 4 engineers. The suite is green but a checkout bug shipped last week.",
"prescan_facts" => [
"resources" => [
["id" => "spec:OrderServiceTest", "label" => "FunSpec, 1 test"],
["id" => "src:suspend", "label" => "1 suspend function: placeOrder"],
],
"flags" => [
["id" => "coroutines:thread-sleep", "label" => "1 Thread.sleep call in test code"],
["id" => "tests:no-assertions", "label" => "1 test body asserts nothing"],
],
],
];
$est = call("estimate", $input);
echo $est["model"], " ", $est["hold_credits"], " ", $est["min_credits"], PHP_EOL;
// estimate is free: no job is created and nothing is charged.
var input = new
{
files = "// file: OrderService.kt\nclass OrderService(private val payments: PaymentService) {\n suspend fun placeOrder(req: OrderRequest): Order =\n Order(req.userId, payments.charge(req.total).status)\n}\n\n// file: OrderServiceTest.kt\nclass OrderServiceTest : FunSpec({\n val payments: PaymentService = mockk(relaxed = true)\n test(\"placeOrder succeeds\") {\n Thread.sleep(500)\n OrderService(payments).placeOrder(OrderRequest(\"u1\", 40))\n }\n})",
focus = "coroutines",
context = "Payments module, 4 engineers. The suite is green but a checkout bug shipped last week.",
prescan_facts = new
{
resources = new[]
{
new { id = "spec:OrderServiceTest", label = "FunSpec, 1 test" },
new { id = "src:suspend", label = "1 suspend function: placeOrder" }
},
flags = new[]
{
new { id = "coroutines:thread-sleep", label = "1 Thread.sleep call in test code" },
new { id = "tests:no-assertions", label = "1 test body asserts nothing" }
}
}
};
var est = await Clinic.Call("estimate", input);
Console.WriteLine(est.GetProperty("hold_credits").GetInt32());
Console.WriteLine(est.GetProperty("sponsor_enabled").GetBoolean());
// estimate is free: no job is created and nothing is charged.
5. Run it, then poll
POST /run returns a job_id; poll GET jobs/{job_id} until
status is succeeded or failed. The review JSON is the string
at data.output.output.
Always send an Idempotency-Key. Derive it from the input, as the web
app does (kotlin-test-clinic:<hash>:a<attempt>). A retried request carrying
the same key returns the same job instead of billing a second run — which is what makes a CI retry
safe. Replaying a key with a different body is a 409 conflict, so bump the
attempt suffix whenever the input actually changed.
# Always send an Idempotency-Key derived from the input. A retried request with
# the same key returns the SAME job instead of billing a second run.
KEY="kotlin-test-clinic:$(printf '%s' "$INPUT" | shasum -a 256 | cut -c1-16):a1"
JOB=$(curl -sS -X POST "$BASE/run" \
-H "Authorization: Bearer $TOKEN" \
-H "X-App-Slug: $SLUG" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-d "$INPUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["job_id"])')
# Poll until the job reaches a terminal status.
while :; do
OUT=$(call "jobs/$JOB")
STATUS=$(printf '%s' "$OUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["status"])')
[ "$STATUS" = "succeeded" ] && break
[ "$STATUS" = "failed" ] && echo "$OUT" && exit 1
sleep 2
done
printf '%s' "$OUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["output"]["output"])'
import hashlib, time
# Always send an Idempotency-Key derived from the input: a retried request with
# the same key returns the SAME job instead of billing a second run.
digest = hashlib.sha256(json.dumps(INPUT, sort_keys=True).encode()).hexdigest()[:16]
key = f"kotlin-test-clinic:{digest}:a1"
req = urllib.request.Request(f"{BASE}/run", data=json.dumps(INPUT).encode(), method="POST")
req.add_header("Authorization", f"Bearer {TOKEN}")
req.add_header("X-App-Slug", SLUG)
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", key)
with urllib.request.urlopen(req) as r:
job_id = json.load(r)["data"]["job_id"]
while True:
job = call(f"jobs/{job_id}")
if job["status"] == "succeeded":
break
if job["status"] == "failed":
raise RuntimeError(job.get("error"))
time.sleep(2)
review = json.loads(job["output"]["output"])
print(review["verdict"], review["stack"], len(review["findings"]), "findings")
import { createHash } from "node:crypto";
// Always send an Idempotency-Key derived from the input: a retried request with
// the same key returns the SAME job instead of billing a second run.
const digest = createHash("sha256").update(JSON.stringify(INPUT)).digest("hex").slice(0, 16);
const key = `kotlin-test-clinic:${digest}:a1`;
const started = await fetch(`${BASE}/run`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"X-App-Slug": SLUG,
"Content-Type": "application/json",
"Idempotency-Key": key,
},
body: JSON.stringify(INPUT),
}).then((r) => r.json());
let job = started.data;
while (job.status !== "succeeded" && job.status !== "failed") {
await new Promise((r) => setTimeout(r, 2000));
job = await call(`jobs/${job.job_id}`);
}
if (job.status === "failed") throw new Error(JSON.stringify(job.error));
const review = JSON.parse(job.output.output);
console.log(review.verdict, review.stack, review.findings.length, "findings");
// Always send an Idempotency-Key derived from the input: a retried request with
// the same key returns the SAME job instead of billing a second run.
body, _ := json.Marshal(input)
sum := sha256.Sum256(body)
key := fmt.Sprintf("kotlin-test-clinic:%x:a1", sum[:8])
req, _ := http.NewRequest(http.MethodPost, base+"/run", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("X-App-Slug", slug)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
var started struct {
Data struct {
JobID string `json:"job_id"`
} `json:"data"`
}
_ = json.NewDecoder(res.Body).Decode(&started)
for {
raw, err := call("jobs/"+started.Data.JobID, nil)
if err != nil {
panic(err)
}
var job struct {
Status string `json:"status"`
Output struct {
Output string `json:"output"`
} `json:"output"`
}
_ = json.Unmarshal(raw, &job)
if job.Status == "succeeded" {
fmt.Println(job.Output.Output) // the review JSON, as a string
break
}
if job.Status == "failed" {
panic("run failed")
}
time.Sleep(2 * time.Second)
}
// Always send an Idempotency-Key derived from the input: a retried request with
// the same key returns the SAME job instead of billing a second run.
var digest = java.security.MessageDigest.getInstance("SHA-256")
.digest(input.getBytes(java.nio.charset.StandardCharsets.UTF_8));
var key = "kotlin-test-clinic:" + java.util.HexFormat.of().formatHex(digest).substring(0, 16) + ":a1";
var start = HttpRequest.newBuilder(URI.create(BASE + "/run"))
.header("Authorization", "Bearer " + TOKEN)
.header("X-App-Slug", SLUG)
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.POST(HttpRequest.BodyPublishers.ofString(input))
.build();
String started = HTTP.send(start, HttpResponse.BodyHandlers.ofString()).body();
// Parse job_id out of `started`, then poll GET jobs/{job_id} every two seconds
// until status is "succeeded" or "failed"; the review JSON is data.output.output.
System.out.println(started);
require "digest"
# Always send an Idempotency-Key derived from the input: a retried request with
# the same key returns the SAME job instead of billing a second run.
digest = Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]
key = "kotlin-test-clinic:#{digest}:a1"
uri = URI("#{BASE}/run")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["X-App-Slug"] = SLUG
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = key
req.body = JSON.generate(input)
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
job_id = JSON.parse(res.body)["data"]["job_id"]
loop do
job = call("jobs/#{job_id}")
break puts(job["output"]["output"]) if job["status"] == "succeeded"
raise "run failed" if job["status"] == "failed"
sleep 2
end
<?php
// Always send an Idempotency-Key derived from the input: a retried request with
// the same key returns the SAME job instead of billing a second run.
$digest = substr(hash("sha256", json_encode($input)), 0, 16);
$key = "kotlin-test-clinic:{$digest}:a1";
$ch = curl_init(BASE . "/run");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($input));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . TOKEN,
"X-App-Slug: " . SLUG,
"Content-Type: application/json",
"Idempotency-Key: " . $key,
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$jobId = json_decode(curl_exec($ch), true)["data"]["job_id"];
curl_close($ch);
while (true) {
$job = call("jobs/" . $jobId);
if ($job["status"] === "succeeded") { echo $job["output"]["output"]; break; }
if ($job["status"] === "failed") { throw new RuntimeException("run failed"); }
sleep(2);
}
using System.Security.Cryptography;
using System.Text;
// Always send an Idempotency-Key derived from the input: a retried request with
// the same key returns the SAME job instead of billing a second run.
var json = JsonSerializer.Serialize(input);
var digest = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(json)))[..16].ToLowerInvariant();
var key = $"kotlin-test-clinic:{digest}:a1";
var run = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/run");
run.Headers.Add("Authorization", "Bearer YOUR_TOKEN");
run.Headers.Add("X-App-Slug", "kotlin-test-clinic");
run.Headers.Add("Idempotency-Key", key);
run.Content = JsonContent.Create(input);
// POST it, read data.job_id, then poll GET jobs/{job_id} every two seconds until
// status is "succeeded" or "failed"; the review JSON is data.output.output.
6. Or stream it
POST /run-stream is the same call over server-sent events. The web app uses it to
advance a staged progress display as sections arrive, and to keep whatever parsed if the stream
dies mid-flight. The final done event carries charged_credits and the
truncated flag.
# Server-sent events. Each `delta` carries a chunk of the JSON review; the final
# `done` event carries the status, charged_credits and the truncated flag.
curl -N -X POST "$BASE/run-stream" \
-H "Authorization: Bearer $TOKEN" \
-H "X-App-Slug: $SLUG" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
-H "Accept: text/event-stream" \
-d "$INPUT"
# event: job {"job_id":"job_..."}
# event: delta {"text":"{\"review_name\":\"OrderService"}
# event: delta {"text":" — test review\",\"verdict\":\"needs-work\","}
# event: done {"status":"succeeded","charged_credits":438,"truncated":false}
# Server-sent events: the review arrives in chunks, so a UI can show progress.
req = urllib.request.Request(f"{BASE}/run-stream", data=json.dumps(INPUT).encode(), method="POST")
req.add_header("Authorization", f"Bearer {TOKEN}")
req.add_header("X-App-Slug", SLUG)
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", key)
req.add_header("Accept", "text/event-stream")
raw = ""
done = {}
event = None
with urllib.request.urlopen(req) as stream:
for line in stream:
line = line.decode().rstrip("\n")
if line.startswith("event: "):
event = line[7:]
elif line.startswith("data: ") and event == "delta":
raw += json.loads(line[6:]).get("text", "")
elif line.startswith("data: ") and event == "done":
done = json.loads(line[6:])
review = json.loads(raw[raw.index("{"):raw.rindex("}") + 1])
print(review["verdict"], len(review["findings"]), "findings", "truncated" if done.get("truncated") else "")
// Server-sent events: the review arrives in chunks, so a UI can show progress.
const res = await fetch(`${BASE}/run-stream`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"X-App-Slug": SLUG,
"Content-Type": "application/json",
"Idempotency-Key": key,
Accept: "text/event-stream",
},
body: JSON.stringify(INPUT),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let raw = "";
let done = {};
let event = null;
while (true) {
const chunk = await reader.read();
if (chunk.done) break;
buffer += decoder.decode(chunk.value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop();
for (const line of lines) {
if (line.startsWith("event: ")) event = line.slice(7);
else if (line.startsWith("data: ") && event === "delta") {
raw += JSON.parse(line.slice(6)).text ?? "";
} else if (line.startsWith("data: ") && event === "done") {
done = JSON.parse(line.slice(6));
}
}
}
const review = JSON.parse(raw.slice(raw.indexOf("{"), raw.lastIndexOf("}") + 1));
console.log(review.verdict, review.findings.length, "findings", done.charged_credits);
// Server-sent events: the review arrives in chunks, so a UI can show progress.
req, _ = http.NewRequest(http.MethodPost, base+"/run-stream", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("X-App-Slug", slug)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
req.Header.Set("Accept", "text/event-stream")
res, _ = http.DefaultClient.Do(req)
defer res.Body.Close()
var raw strings.Builder
var event string
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, "event: "):
event = strings.TrimPrefix(line, "event: ")
case strings.HasPrefix(line, "data: ") && event == "delta":
var d struct {
Text string `json:"text"`
}
_ = json.Unmarshal([]byte(strings.TrimPrefix(line, "data: ")), &d)
raw.WriteString(d.Text)
case strings.HasPrefix(line, "data: ") && event == "done":
fmt.Println(strings.TrimPrefix(line, "data: ")) // status, charged_credits, truncated
}
}
fmt.Println(raw.String())
// Server-sent events: the review arrives in chunks, so a UI can show progress.
var stream = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("X-App-Slug", SLUG)
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.header("Accept", "text/event-stream")
.POST(HttpRequest.BodyPublishers.ofString(input))
.build();
StringBuilder raw = new StringBuilder();
String[] event = { null };
HTTP.send(stream, HttpResponse.BodyHandlers.ofLines()).body().forEach(line -> {
if (line.startsWith("event: ")) event[0] = line.substring(7);
else if (line.startsWith("data: ") && "delta".equals(event[0])) {
raw.append(line.substring(6)); // each data line is {"text":"..."} - decode and append .text
}
});
System.out.println(raw);
# Server-sent events: the review arrives in chunks, so a UI can show progress.
uri = URI("#{BASE}/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["X-App-Slug"] = SLUG
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = key
req["Accept"] = "text/event-stream"
req.body = JSON.generate(input)
raw = +""
event = nil
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
line = line.chomp
if line.start_with?("event: ") then event = line[7..]
elsif line.start_with?("data: ") && event == "delta"
raw << (JSON.parse(line[6..])["text"] || "")
end
end
end
end
end
review = JSON.parse(raw[raw.index("{")..raw.rindex("}")])
puts "#{review['verdict']} #{review['findings'].length} findings"
<?php
// Server-sent events: the review arrives in chunks, so a UI can show progress.
$raw = "";
$event = null;
$ch = curl_init(BASE . "/run-stream");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($input));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . TOKEN,
"X-App-Slug: " . SLUG,
"Content-Type: application/json",
"Idempotency-Key: " . $key,
"Accept: text/event-stream",
]);
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $chunk) use (&$raw, &$event) {
foreach (explode("\n", $chunk) as $line) {
if (str_starts_with($line, "event: ")) {
$event = substr($line, 7);
} elseif (str_starts_with($line, "data: ") && $event === "delta") {
$raw .= json_decode(substr($line, 6), true)["text"] ?? "";
}
}
return strlen($chunk);
});
curl_exec($ch);
curl_close($ch);
$review = json_decode(substr($raw, strpos($raw, "{")), true);
echo $review["verdict"], PHP_EOL;
// Server-sent events: the review arrives in chunks, so a UI can show progress.
var stream = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/run-stream");
stream.Headers.Add("Authorization", "Bearer YOUR_TOKEN");
stream.Headers.Add("X-App-Slug", "kotlin-test-clinic");
stream.Headers.Add("Idempotency-Key", key);
stream.Headers.Add("Accept", "text/event-stream");
stream.Content = JsonContent.Create(input);
using var res = await Http.SendAsync(stream, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
var raw = new StringBuilder();
string? evt = null;
while (await reader.ReadLineAsync() is { } line)
{
if (line.StartsWith("event: ")) evt = line[7..];
else if (line.StartsWith("data: ") && evt == "delta")
{
var d = JsonSerializer.Deserialize<JsonElement>(line[6..]);
if (d.TryGetProperty("text", out var t)) raw.Append(t.GetString());
}
}
Console.WriteLine(raw.ToString());
7. Parse the review and check the reconciliation
Two invariants are worth enforcing on your side, because the app enforces them too: every
focus_areas[].finding_ids entry must name a real finding id, and every
prescan_facts.flags id must appear exactly once in
coverage_check. A flag missing from the reconciliation means the model quietly skipped
a fact you established — treat that as a failed run, not a passing one, and retry with a
retry_note naming the missing ids.
The truncated flag on the done event (and on the finished job) means the
reply hit the output cap. What you hold is a prefix, not a review: retry with a
retry_note asking for fewer, denser findings rather than trying to repair the JSON.
# The review JSON is a string inside the envelope, so unwrap it twice.
REVIEW=$(printf '%s' "$OUT" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["output"]["output"])')
printf '%s' "$REVIEW" | python3 -c '
import sys, json
r = json.load(sys.stdin)
print(r["verdict"], "|", r["verdict_line"])
print(r["stack"], r["test_file_count"], "test files,", r["test_case_count"], "test cases")
for c in r["checks"]:
print(f" {c[\"status\"]:8} {c[\"check\"]}")
for f in r["findings"]:
print(f" {f[\"id\"]} {f[\"priority\"]:8} {f[\"category\"]:14} {f[\"resource\"]}")
'
# Every prescan flag id must come back exactly once in coverage_check.
printf '%s' "$REVIEW" | python3 -c '
import sys, json
seen = [c["id"] for c in json.load(sys.stdin)["coverage_check"]]
want = ["coroutines:thread-sleep", "tests:no-assertions"]
missing = [i for i in want if seen.count(i) != 1]
if missing:
raise SystemExit("unreconciled prescan flags: " + ", ".join(missing))
print("coverage_check reconciles")
'
review = json.loads(job["output"]["output"])
# 1. Every prescan flag id appears exactly once in coverage_check.
sent = [f["id"] for f in INPUT["prescan_facts"]["flags"]]
seen = [c["id"] for c in review["coverage_check"]]
missing = [i for i in sent if seen.count(i) != 1]
extra = [i for i in seen if i not in sent]
if missing or extra:
raise RuntimeError(f"coverage_check drift: missing={missing} extra={extra}")
# 2. Every focus_areas finding id exists in findings.
ids = {f["id"] for f in review["findings"]}
for area in review["focus_areas"]:
for fid in area["finding_ids"]:
if fid not in ids:
raise RuntimeError(f"focus_areas references unknown finding {fid}")
# 3. A truncated reply is a prefix, not a review. Retry, do not repair.
if job.get("truncated"):
INPUT["retry_note"] = (
"The previous reply was truncated. Return the same ten checks but at most "
"eight findings, each with a shorter snippet."
)
# ... resubmit with an incremented attempt suffix in the Idempotency-Key.
for c in review["checks"]:
print(f"{c['status']:8} {c['check']:36} {c['evidence']}")
for f in review["findings"]:
print(f["id"], f["priority"], f["category"], f["resource"])
print(review["suggested_tests"])
print("\n".join(review["commands"])) # always ends with: ./gradlew test
const review = JSON.parse(job.output.output);
// 1. Every prescan flag id appears exactly once in coverage_check.
const sent = INPUT.prescan_facts.flags.map((f) => f.id);
const seen = review.coverage_check.map((c) => c.id);
const missing = sent.filter((id) => seen.filter((s) => s === id).length !== 1);
const extra = seen.filter((id) => !sent.includes(id));
if (missing.length || extra.length) {
throw new Error(`coverage_check drift: missing=${missing} extra=${extra}`);
}
// 2. Every focus_areas finding id exists in findings.
const ids = new Set(review.findings.map((f) => f.id));
for (const area of review.focus_areas) {
for (const fid of area.finding_ids) {
if (!ids.has(fid)) throw new Error(`focus_areas references unknown finding ${fid}`);
}
}
// 3. A truncated reply is a prefix, not a review. Retry, do not repair.
if (job.truncated) {
INPUT.retry_note =
"The previous reply was truncated. Return the same ten checks but at most eight findings.";
}
for (const c of review.checks) console.log(c.status.padEnd(8), c.check, "—", c.evidence);
for (const f of review.findings) console.log(f.id, f.priority, f.category, f.resource);
console.log(review.suggested_tests);
console.log(review.commands.join("\n")); // always ends with: ./gradlew test
type check struct {
Check string `json:"check"`
Status string `json:"status"`
Evidence string `json:"evidence"`
Requirement string `json:"requirement"`
}
type finding struct {
ID string `json:"id"`
Category string `json:"category"`
Severity string `json:"severity"`
Priority string `json:"priority"`
Resource string `json:"resource"`
Problem string `json:"problem"`
Fix string `json:"fix"`
Snippet string `json:"snippet"`
}
type review struct {
ReviewName string `json:"review_name"`
Verdict string `json:"verdict"`
VerdictLine string `json:"verdict_line"`
Stack string `json:"stack"`
TestFileCount int `json:"test_file_count"`
TestCaseCount int `json:"test_case_count"`
Checks []check `json:"checks"`
Findings []finding `json:"findings"`
SuggestedTests string `json:"suggested_tests"`
Commands []string `json:"commands"`
CoverageCheck []struct {
ID string `json:"id"`
Addressed bool `json:"addressed"`
Note string `json:"note"`
} `json:"coverage_check"`
}
var r review
if err := json.Unmarshal([]byte(job.Output.Output), &r); err != nil {
panic(err)
}
// Every prescan flag id must come back exactly once in coverage_check.
count := map[string]int{}
for _, c := range r.CoverageCheck {
count[c.ID]++
}
for _, id := range []string{"coroutines:thread-sleep", "tests:no-assertions"} {
if count[id] != 1 {
panic("unreconciled prescan flag: " + id)
}
}
fmt.Println(r.Verdict, r.Stack, r.TestCaseCount, "test cases,", len(r.Findings), "findings")
// The review JSON is a string inside data.output.output - parse it, then check
// the two invariants before you trust it:
//
// 1. every prescan_facts.flags id appears exactly once in coverage_check;
// 2. every focus_areas[].finding_ids entry names an id present in findings.
//
// A `truncated` job is a prefix, not a review: resubmit with a retry_note such as
// "The previous reply was truncated. Return the same ten checks but at most
// eight findings" and an incremented attempt suffix on the Idempotency-Key.
String reviewJson = /* data.output.output */ call("jobs/" + jobId, null);
System.out.println(reviewJson);
// checks[] is always the same ten entries in the same order, so a table can be
// rendered by index without searching for a check by name.
review = JSON.parse(job["output"]["output"])
# 1. Every prescan flag id appears exactly once in coverage_check.
sent = input["prescan_facts"]["flags"].map { |f| f["id"] }
seen = review["coverage_check"].map { |c| c["id"] }
missing = sent.reject { |id| seen.count(id) == 1 }
extra = seen - sent
raise "coverage_check drift: #{missing} / #{extra}" unless missing.empty? && extra.empty?
# 2. Every focus_areas finding id exists in findings.
ids = review["findings"].map { |f| f["id"] }
review["focus_areas"].each do |area|
area["finding_ids"].each { |fid| raise "unknown finding #{fid}" unless ids.include?(fid) }
end
review["checks"].each { |c| puts format("%-8s %s", c["status"], c["check"]) }
review["findings"].each { |f| puts "#{f['id']} #{f['priority']} #{f['category']} #{f['resource']}" }
puts review["suggested_tests"]
puts review["commands"].last # ./gradlew test
<?php
$review = json_decode($job["output"]["output"], true);
// 1. Every prescan flag id appears exactly once in coverage_check.
$sent = array_column($input["prescan_facts"]["flags"], "id");
$seen = array_column($review["coverage_check"], "id");
$counts = array_count_values($seen);
foreach ($sent as $id) {
if (($counts[$id] ?? 0) !== 1) {
throw new RuntimeException("unreconciled prescan flag: " . $id);
}
}
// 2. Every focus_areas finding id exists in findings.
$ids = array_column($review["findings"], "id");
foreach ($review["focus_areas"] as $area) {
foreach ($area["finding_ids"] as $fid) {
if (!in_array($fid, $ids, true)) {
throw new RuntimeException("focus_areas references unknown finding " . $fid);
}
}
}
foreach ($review["checks"] as $c) {
printf("%-8s %s\n", $c["status"], $c["check"]);
}
echo $review["suggested_tests"], PHP_EOL;
var review = JsonSerializer.Deserialize<JsonElement>(reviewJson);
// 1. Every prescan flag id appears exactly once in coverage_check.
var seen = review.GetProperty("coverage_check")
.EnumerateArray()
.Select(c => c.GetProperty("id").GetString())
.ToList();
foreach (var id in new[] { "coroutines:thread-sleep", "tests:no-assertions" })
{
if (seen.Count(s => s == id) != 1)
throw new Exception($"unreconciled prescan flag: {id}");
}
// 2. Every focus_areas finding id exists in findings.
var ids = review.GetProperty("findings")
.EnumerateArray()
.Select(f => f.GetProperty("id").GetString())
.ToHashSet();
foreach (var area in review.GetProperty("focus_areas").EnumerateArray())
foreach (var fid in area.GetProperty("finding_ids").EnumerateArray())
if (!ids.Contains(fid.GetString()))
throw new Exception($"focus_areas references unknown finding {fid}");
foreach (var c in review.GetProperty("checks").EnumerateArray())
Console.WriteLine($"{c.GetProperty("status")} {c.GetProperty("check")}");
The output contract
data.output.output is a JSON string holding one object. This is exactly what the web
app parses, so anything that renders here will render there:
{
"review_name": "OrderService — test review",
"verdict": "well-tested | needs-work | untested-risk",
"verdict_line": "one sentence justifying the verdict and naming the thing that decides it",
"stack": "Kotest 5 + MockK + kotlinx-coroutines-test",
"test_file_count": 1,
"test_case_count": 1,
"exec_summary": "2-3 paragraphs separated by blank lines",
"assumptions": ["..."],
"open_questions": ["..."],
"inventory": [
{ "kind": "TestClass", "name": "OrderServiceTest", "value": "FunSpec, 1 test",
"role": "sole coverage of OrderService" }
],
"checks": [
{ "check": "Virtual time in coroutine tests", "status": "fail",
"evidence": "Thread.sleep(500) in the test body",
"requirement": "runTest with advanceTimeBy" }
],
"findings": [
{
"id": "KT-001",
"category": "framework | spec-style | assertions | mocking | coroutines | flows | property-based | coverage | structure | flakiness",
"severity": "low | medium | high",
"likelihood": "low | medium | high",
"priority": "critical | high | medium | low",
"resource": "Test/'placeOrder succeeds'",
"problem": "...",
"impact": "...",
"fix": "...",
"snippet": "corrected Kotlin / Gradle fragment, or \"\""
}
],
"coverage_check": [
{ "id": "coroutines:thread-sleep", "addressed": true, "note": "KT-002." }
],
"suggested_tests": "a ready-to-paste Kotest spec, as a JSON string",
"commands": ["./gradlew test # the whole suite before pushing"],
"quick_wins": ["..."],
"focus_areas": [{ "area": "...", "why": "...", "finding_ids": ["KT-001"] }],
"summary": "closing paragraph"
}
Every key
| key | type | meaning |
|---|---|---|
review_name | string | Short title naming the module and the outcome, e.g. "OrderService — test review". |
verdict | enum | well-tested, needs-work or untested-risk. The single value a CI gate should branch on. |
verdict_line | string | One sentence justifying the verdict and naming the single thing that decides it. |
stack | string | The test stack the paste shows, e.g. "Kotest 5 + MockK + kotlinx-coroutines-test", or "none visible". Never guessed. |
test_file_count | number | How many test files the paste contains. |
test_case_count | number | How many test cases those files declare. |
exec_summary | string | Two to three paragraphs on the dominant themes, separated by blank lines. |
assumptions | string[] | Explicit assumptions filling gaps in the paste — which module the Gradle file belongs to, whether CI runs the suite. |
open_questions | string[] | Questions whose answers would change the review or its ordering. |
inventory | object[] | {kind, name, value, role}. kind is one of SourceClass, SourceFunction, TestClass, SpecStyle, Test, Mock, SuspendFunction, Flow, Dependency, GradlePlugin, Command. |
checks | object[] | {check, status, evidence, requirement}. Always the same ten checks in the same fixed order — render by index, do not search by name. |
findings | object[] | {id, category, severity, likelihood, priority, resource, problem, impact, fix, snippet}. Ids are sequential KT-001, KT-002, … Always at least one entry — for a well-tested paste they are the improvements worth making, honestly prioritized low. snippet is a pasteable Kotlin or Gradle fragment or "". |
coverage_check | object[] | {id, addressed, note}. One entry per prescan_facts.flags id, exactly once, and no ids the prescan did not send. addressed: false means deliberately set aside, with the reason in note. |
suggested_tests | string | A complete, compilable Kotest spec for the highest-value gap the review found — imports included, spec style matching the project's, mocks stubbed with the right every/coEvery variant, and only classes and functions visible in the paste. "" when the paste gives nothing to anchor one on. |
commands | string[] | Ordered shell commands, each with a trailing comment. Read-and-verify only — nothing that publishes, deploys or deletes. Always ends with ./gradlew test, or ./gradlew test koverVerify when the paste shows Kover. |
quick_wins | string[] | One-line changes worth doing immediately. |
focus_areas | object[] | {area, why, finding_ids}. Every id in finding_ids must exist in findings. |
summary | string | Closing paragraph: what to do first and what remains after that. |
The enums
| field | values | notes |
|---|---|---|
verdict | well-tested, needs-work, untested-risk | well-tested: the suite runs, asserts behaviour, handles coroutines correctly, and the remaining findings are improvements. needs-work: real tests exist but named findings undermine their protection — flaky time handling, mock hygiene, assertion-free bodies, framework mixing. untested-risk: the pasted production code has no meaningful coverage in the paste, and the review's centre of gravity is the TDD plan and the first failing spec. |
findings[].category | framework, spec-style, assertions, mocking, coroutines, flows, property-based, coverage, structure, flakiness | framework and spec-style are separate on purpose: mixing Kotest with JUnit in one module and mixing three Kotest spec styles are different problems with different migrations. |
findings[].severityfindings[].likelihood | low, medium, high | Severity is how much protection is lost; likelihood is how often it bites — how reachable the gap is from the code as pasted. |
findings[].priority | critical, high, medium, low | Severity by likelihood. critical is reserved for something meaning the tests are not protecting the code today: a suite that cannot run (Kotest on Gradle without useJUnitPlatform()), tests that pass without asserting anything on the core behaviour, or a suspend-stubbing misuse that keeps the suite green while the mocked path is never exercised. |
checks[].status | pass, fail, partial, unknown | unknown is a legitimate answer when the paste does not show enough to decide, and is preferred over a guess — no build.gradle.kts means the coverage gate is unknown, not fail. partial means the practice is present in some places and missing in others. |
The ten checks
checks always carries these ten, in this order, on every run — so a table can be
rendered by index and two reviews of the same module are diffable row by row:
1. Single test framework 6. Suspend stubs use coEvery/coVerify
2. Consistent spec style 7. Virtual time in coroutine tests
3. Behaviour over implementation 8. Failure paths and edge cases tested
4. Real values for data classes 9. Property-based tests where laws exist
5. Explicit mock stubbing and lifecycle 10. Coverage gate configured
The review only cites what the paste actually contains: classes, functions, specs, test names,
mocks, dependencies and Gradle blocks that appear in files. An absence — no test file
for a pasted class, no runTest despite suspend functions, no Kover block — is itself a
finding, named in resource or marked (missing from the paste). And
commands is read-and-verify only: nothing in it publishes, deploys or deletes.
Your saved reviews
Every run the app completes is written to the reviews collection, so a review follows
the user across devices. It is declared acl_read: owner and
acl_write: user: rows are scoped to the calling subject, which means a script must
reuse one token across the run and the query or it will see an empty collection. Each
POST /guest mints a new guest subject, so guest tokens are not a way to share
history.
| field | type | meaning |
|---|---|---|
uid | string | The app's own id for the run — stable across a re-render. |
title | string | review_name from the reply. |
verdict | string | well-tested, needs-work or untested-risk. |
stack | string | The test stack the paste showed, e.g. Kotest 5 + MockK. |
input_hash | string | Hash of the submitted input — the cheap way to tell whether a module actually changed between runs. |
findings_count | number | findings.length. |
critical_count | number | How many findings came back priority: "critical". |
test_count | number | test_case_count from the reply — how many test cases the paste declared. |
ran_at | timestamp | When the run completed. The natural sort key. |
Those nine fields are declared, and therefore filterable and orderable. The rest of the document —
the whole review — round-trips intact but is not indexed. embed is
["title", "verdict", "stack"], so POST /collections/reviews/similar with
a text or a record_id finds past reviews that read like this one: useful
for "have we seen this test smell before?" across a fleet of modules.
Every where entry must be an operator object — eq, ne,
lt, lte, gt, gte, in (up to 20
values) or contains. The bare-value shorthand {"verdict": "untested-risk"}
is rejected. Records come back wrapped: data.records[].doc holds the fields, alongside
a record_id.
# Your saved reviews, newest first.
call collections/reviews/query '{"sort":{"field":"ran_at","dir":"desc"},"limit":10}'
# Only the ones that came back untested-risk with something critical in them.
call collections/reviews/query '{"where":{"verdict":{"eq":"untested-risk"},"critical_count":{"gte":1}},
"sort":{"field":"ran_at","dir":"desc"},"limit":20}'
# Past reviews that read like this one, using the declared embed fields.
call collections/reviews/similar '{"text":"Thread.sleep in a Kotest spec that asserts nothing","limit":5}'
# Every `where` entry must be an operator object - the bare-value shorthand
# ({"verdict": "untested-risk"}) is rejected.
recent = call("collections/reviews/query", {
"where": {"verdict": {"eq": "untested-risk"}, "critical_count": {"gte": 1}},
"sort": {"field": "ran_at", "dir": "desc"},
"limit": 20,
})
for rec in recent["records"]:
d = rec["doc"] # the fields nest under .doc, not flat
print(d["ran_at"], d["title"], d["verdict"], d["stack"], d["critical_count"], "critical")
# Nearest neighbours over embed = ["title", "verdict", "stack"].
similar = call("collections/reviews/similar", {"text": recent["records"][0]["doc"]["title"], "limit": 5})
for rec in similar["records"]:
print(rec["doc"]["title"])
const recent = await call("collections/reviews/query", {
where: { verdict: { eq: "untested-risk" }, critical_count: { gte: 1 } },
sort: { field: "ran_at", dir: "desc" },
limit: 20,
});
for (const rec of recent.records) {
const d = rec.doc; // the fields nest under .doc, not flat
console.log(d.ran_at, d.title, d.verdict, d.stack, d.critical_count);
}
// Nearest neighbours over embed = ["title", "verdict", "stack"].
const similar = await call("collections/reviews/similar", {
text: "Thread.sleep in a Kotest spec that asserts nothing",
limit: 5,
});
console.log(similar.records.map((r) => r.doc.title));
query := map[string]any{
"where": map[string]any{
"verdict": map[string]any{"eq": "untested-risk"},
"critical_count": map[string]any{"gte": 1},
},
"sort": map[string]string{"field": "ran_at", "dir": "desc"},
"limit": 20,
}
raw, err := call("collections/reviews/query", query)
if err != nil {
panic(err)
}
var out struct {
Records []struct {
RecordID string `json:"record_id"`
Doc map[string]any `json:"doc"`
} `json:"records"`
}
_ = json.Unmarshal(raw, &out)
for _, r := range out.Records {
fmt.Println(r.Doc["ran_at"], r.Doc["title"], r.Doc["stack"], r.Doc["critical_count"])
}
// collections/reviews/similar takes {"text": "...", "limit": 5} and ranks over
// the declared embed fields: title, verdict and stack.
String query = """
{"where":{"verdict":{"eq":"untested-risk"},"critical_count":{"gte":1}},
"sort":{"field":"ran_at","dir":"desc"},"limit":20}
""";
String reviews = call("collections/reviews/query", query);
System.out.println(reviews);
// {"ok":true,"data":{"records":[{"record_id":"...","doc":{"title":"...","verdict":"untested-risk",...}}]}}
// The fields nest under .doc - never read them flat off the record.
String similar = call("collections/reviews/similar",
"{\"text\":\"Thread.sleep in a Kotest spec that asserts nothing\",\"limit\":5}");
System.out.println(similar);
recent = call("collections/reviews/query", {
"where" => { "verdict" => { "eq" => "untested-risk" }, "critical_count" => { "gte" => 1 } },
"sort" => { "field" => "ran_at", "dir" => "desc" },
"limit" => 20,
})
recent["records"].each do |r|
d = r["doc"] # the fields nest under .doc, not flat
puts "#{d['ran_at']} #{d['title']} #{d['verdict']} #{d['stack']}"
end
# Nearest neighbours over embed = ["title", "verdict", "stack"].
similar = call("collections/reviews/similar", { "text" => "assertion-free Kotest spec", "limit" => 5 })
similar["records"].each { |r| puts r["doc"]["title"] }
<?php
$recent = call("collections/reviews/query", [
"where" => ["verdict" => ["eq" => "untested-risk"], "critical_count" => ["gte" => 1]],
"sort" => ["field" => "ran_at", "dir" => "desc"],
"limit" => 20,
]);
foreach ($recent["records"] as $rec) {
$d = $rec["doc"]; // the fields nest under .doc, not flat
echo $d["ran_at"], " ", $d["title"], " ", $d["verdict"], " ", $d["stack"], PHP_EOL;
}
// Nearest neighbours over embed = ["title", "verdict", "stack"].
$similar = call("collections/reviews/similar", ["text" => "assertion-free Kotest spec", "limit" => 5]);
foreach ($similar["records"] as $rec) {
echo $rec["doc"]["title"], PHP_EOL;
}
var query = new
{
where = new { verdict = new { eq = "untested-risk" }, critical_count = new { gte = 1 } },
sort = new { field = "ran_at", dir = "desc" },
limit = 20,
};
var recent = await Clinic.Call("collections/reviews/query", query);
foreach (var rec in recent.GetProperty("records").EnumerateArray())
{
var d = rec.GetProperty("doc"); // the fields nest under .doc, not flat
Console.WriteLine($"{d.GetProperty("ran_at")} {d.GetProperty("title")} {d.GetProperty("stack")}");
}
// Nearest neighbours over embed = ["title", "verdict", "stack"].
var similar = await Clinic.Call("collections/reviews/similar",
new { text = "assertion-free Kotest spec", limit = 5 });
Console.WriteLine(similar.GetProperty("records").GetArrayLength());
A CI gate
The verdict is the natural exit code. Fail the job when a module drifts into
untested-risk, warn on needs-work, and pass on well-tested —
with the Idempotency-Key derived from the input so a re-run of the same commit replays instead of
re-billing. Comparing input_hash against the last row in the reviews
collection tells you whether it is even worth spending the credits.
VERDICT=$(printf '%s' "$REVIEW" | python3 -c 'import sys,json;print(json.load(sys.stdin)["verdict"])')
CRITICAL=$(printf '%s' "$REVIEW" | python3 -c 'import sys,json;print(sum(1 for f in json.load(sys.stdin)["findings"] if f["priority"]=="critical"))')
case "$VERDICT" in
untested-risk) echo "::error::Kotlin test review: untested risk ($CRITICAL critical)"; exit 1 ;;
needs-work) echo "::warning::Kotlin test review: needs work"; exit 0 ;;
well-tested) echo "Kotlin test review: well tested"; exit 0 ;;
esac