Untested changes
On every pull request, evalship finds the LLM calls and prompt files the pull request adds or changes, and checks whether any eval exercises them. Those that no eval exercises appear in the comment:
- **Untested change:** `app/refunds.py#RefundPolicy.explain` (new) has no eval. [Suggested next step](https://evalship.com/docs/untested-changes)
The details block lists each one with its line and provider. This runs for pull requests on public repositories and on enabled private repositories.
What evalship looks at
evalship reads up to 25 added or modified files per pull request. Removed files are ignored.
Source files. A line is an LLM call when the file imports a supported SDK or framework and the line matches that library's call pattern. The call counts as changed when the pull request changes a line within 5 lines of it, such as its prompt or arguments. When GitHub leaves out a file's diff because it is too large, every call in the file counts as changed.
| Language | Extensions | Detected libraries |
|---|---|---|
| Python | .py |
OpenAI, Anthropic, Google GenAI and Generative AI, Mistral, Cohere, LiteLLM, LangChain, LlamaIndex, DSPy, Pydantic AI, Instructor |
| JavaScript and TypeScript | .js .jsx .ts .tsx .mjs .cjs .mts .cts |
OpenAI, Anthropic (@anthropic-ai/sdk), Google (@google/genai, @google/generative-ai), Vercel AI SDK (ai), LangChain (@langchain/*), Mastra (@mastra/*) |
| Ruby | .rb |
OpenAI (OpenAI::Client), Anthropic, RubyLLM, Langchain.rb |
| Go | .go |
sashabaranov/go-openai, openai/openai-go, anthropics/anthropic-sdk-go |
Examples of lines that count as calls: client.chat.completions.create(, client.messages.create(, model.generate_content(, litellm.completion(, chain.invoke(, generateText(, chat.ask "...", client.Messages.New(.
Detection is pattern-based. Comment lines are skipped, and a call split across lines before the method name, such as client.messages on one line and .create( on the next, is not detected.
Prompt files. Any change to a prompt file counts. Prompt files are:
.promptand.promptyfiles anywhere;.txt,.md,.mdx,.yaml,.yml,.j2,.jinja,.jinja2,.hbs,.mustache,.liquidand.tmplfiles inside a directory namedpromptorprompts, at any depth.
Each call is named by its file and up to two levels of enclosing function or class: app/refunds.py#RefundPolicy.explain.
What counts as tested
evalship checks these in order and stops at the first match. Only cases in the current run count for the first three.
- Targets. A case lists the file in its
targets.app/support_bot.pycovers every call in that file;app/support_bot.py#answercovers the calls insideanswer, includingSupportBot.answer. The path can be a glob:prompts/*.mdmatches files directly inprompts/, andapp/orapp/**matches everything belowapp/. See Result formats for how to set targets in each format. - Annotation. An
evalship: covered-by <pattern>comment near the call, anywhere from 3 lines above the enclosing function's definition down to the call itself. The pattern is a suite name or asuite/keyglob; separate several with commas. It must match at least one case. - Name. A suite or eval file is named after the source file, function or class. Names are compared in lowercase with everything but letters and digits removed, so
app/support_bot.pyis matched by the suitesupport-botor the eval filetests/evals/test_support_bot.py. Names shorter than 4 characters and generic names (app,client,utils,llm,ai,service,api,prompt,chat,agent,modeland similar) never match. - Audit. The latest audit has a suite whose
exerciseslists the file,file#symbol, or a prefix ending in*that the file starts with; or apromptsentry for the file with a non-emptytested_by, within 20 lines of the call.
Annotations look like this:
# evalship: covered-by support_bot
def answer(question):
return client.messages.create(model="claude-haiku-4-5", max_tokens=500, messages=[...])
// evalship: covered-by support-bot/refund-*, summarizer
export async function explainRefund(order: Order) {
return generateText({ model, prompt: refundPrompt(order) });
}
Fixing an untested change
- Add an eval that exercises the change, and give its cases
targetspointing at the file or function. This is the most reliable link, because it doesn't depend on names. - If an existing eval already covers it, add
targetsto that eval's cases, or put acovered-byannotation next to the call. - Re-run the setup prompt. The agent audits again, records which code each suite exercises, and can propose an eval for the new call as part of its plan. See Quickstart.
The check is recomputed on every run, so the bullet disappears from the comment once the next run finds a covering case.