Skip to content

Test/coverage improvements#19

Merged
Skobeltsyn merged 6 commits into
mainfrom
test/coverage-improvements
May 3, 2026
Merged

Test/coverage improvements#19
Skobeltsyn merged 6 commits into
mainfrom
test/coverage-improvements

Conversation

@Skobeltsyn
Copy link
Copy Markdown
Contributor

No description provided.

Skobeltsyn and others added 6 commits May 3, 2026 18:55
Adds 4 tests calling branch.invokeSuspend(input) directly inside
runBlocking { } so the suspend path is exercised through unambiguous
bytecode, not via the runBlocking-wrapped sync invoke().

Coverage outcome (PIT before → after):
- Mutations tracked on Branch.invokeSuspend: ~6 NO_COVERAGE → 11 KILLED
  + 3 TIMED_OUT + 3 SURVIVED + 12 NO_COVERAGE
- Net: 11 mutations now killed where previously nothing was tracked,
  and 3 actionable survivors surfaced (worth a follow-up to tighten).

Caveat called out in the test file's header comment: PIT's coverage
tracking through Kotlin's coroutine state-machine bytecode is partial.
12 NO_COVERAGE remain on the suspend function despite the code being
executed by these tests. That's a PIT-vs-coroutines limitation, not a
real coverage gap. The null-branch (lines 32-38) is also genuinely
defensive-dead-code — agent<IN, OUT : Any> requires non-null OUT, so
no public-API usage can produce null at the type level. Documented in
the test file rather than chased.

Tests cover:
- TypeRoute: result matches a registered type → that route fires.
- Registration order: more-specific-first wins.
- ElseRoute: result doesn't match any TypeRoute, falls through to else.
- Missing-branch error: result has no matching route AND no onElse,
  asserts the error message names the result type or onElse.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ed types

14 new tests targeting the NO_COVERAGE clusters from #885:

coerceToInt — Float input (was lines 340-345 untouched):
- in-range whole-number Float → coerce successfully
- fractional Float → reject (return null)
- out-of-range Float (1e10) → reject
- NaN, +Infinity, -Infinity → reject
- Short and Byte inputs → coerce (line 334 fallthrough)

coerceToLong — Float input (was lines 366-371 untouched):
- same six cases as above for Long with appropriate bounds
- Short/Byte fallthrough (line 360)

promptTypeName — List<T> recursion + nested-Generable simpleName:
- `List<String>` field renders type as "List<String>" (line 215-217)
- nested @generable field renders as its simpleName (line 219)

PIT delta on agents_engine.generation.GenerableSupportKt:
- NO_COVERAGE: 24 → 14 (10 mutants newly covered)
- promptTypeName: fully covered (6 → 0 NO_COVERAGE)
- coerceToInt: 5 → 2 NO_COVERAGE
- coerceToLong: 4 → 2 NO_COVERAGE

Remaining 14 NO_COVERAGE are in sealed-class path (sealedJsonSchema,
sealedPromptFragment, dataClassPromptFragment) and coerceValue wrapper —
out of scope for this ticket; tracked under #889 catch-all.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Spawns real child processes (cat, sh) to exercise the full forProcess
factory body and its three lambdas (stderr drain, onClose destroy,
default stderrSink). @EnabledOnOs(LINUX, MAC) so Windows runners skip
cleanly — CI on ubuntu-latest covers the path.

5 tests covering:
- Round-trip JSON-RPC over a `cat` child (proves stdin/stdout wiring).
- Env map applied to child (sh -c reads $TEST_VAR, prints as result).
- workingDir applied to child (sh -c prints $(pwd)).
- stderrSink receives lines from the child's stderr.
- close() destroys a long-running child within the 2-second waitFor
  window (asserts <3s wall-clock).

PIT delta on StdioMcpTransport$Companion:
- NO_COVERAGE: 12 → 1
- The lone remaining mutant is the default-lambda for
  `stderrSink: (String) -> Unit = {}` parameter (line 34) — equivalent
  mutant on a Kotlin-synthetic empty lambda.
- 5 mutations now KILLED + 10 SURVIVED (actionable signal where there
  was nothing before).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 new tests targeting the NO_COVERAGE clusters from #887:

recoverInvalidArguments — Retry path (lines 317-336):
- Retry where currentRaw is actually parseable → executor runs once.
- Retry where currentRaw stays unparseable → post-loop throw fires.

executeToolWithExecutionRecovery — Unrecoverable + null arms:
- Unrecoverable wraps the original exception with "unrecoverable"
  message and preserves cause. (line 388)
- handler returning null re-throws original exception. (line 391)

parseOutput — typed (non-String) OUT branch (line 417):
- Agent with @generable OUT type; model returns JSON; framework parses
  via fromLlmOutput.

PIT delta on agents_engine.model.AgenticLoopKt:
- NO_COVERAGE: 9 → 2 (7 mutants newly covered)
- KILLED: now 116
- SURVIVED: 14 (actionable)

Remaining 2 NO_COVERAGE are structural:
- L343 recoverInvalidArguments null-arm — defensive-dead-code; the
  OnErrorBuilder substitutes Unrecoverable for null so the user can't
  actually return null from the invalidArgs lambda.
- L196 selectSkillByLlm — Kotlin coroutine state-machine internal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7 tests exercising the previously-fully-untested castForumReturn helper
by emitting forum_return tool calls with each shape of value:

- L84  outType String + non-String value → toString cast
- L85  raw is instance of OUT (Int) → pass-through
- L87-89  raw is Map matching @generable → constructFromMap success
- L89  raw is Map but wrong shape → "could not be parsed" error
- L91-93  raw is JSON String matching @generable → fromLlmOutput success
- L93  raw is invalid String → "could not be parsed" error
- L95  raw is incompatible (Int into ForumVerdict OUT) → catch-all error

PIT delta on agents_engine.composition.forum.Forum:
- NO_COVERAGE: 7 → 0 (clean win)
- KILLED: 16 / SURVIVED: 4 — fully covered

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 tests exercising the 400/JSON-RPC-error branches that PIT marked
NO_COVERAGE in the catch-all #889 ticket:

- L86: malformed JSON body → HTTP 400
- L87: JSON missing "method" field → HTTP 400
- L133: tools/call without "name" param → JSON-RPC code -32602
- L135: tools/call with unknown tool → JSON-RPC code -32601
- L148: skill execute throws → response with isError:true
- bonus: unknown top-level method → -32601 "Method not found"

PIT delta on agents_engine.mcp.McpServer:
- NO_COVERAGE on handle/handleToolCall: 6 → 2 (4 newly covered)
- KILLED jumped to 56; SURVIVED 17 (actionable signal)

Remaining 2 NO_COVERAGE are out of reach via this test surface:
- L81 payload-too-large: already covered by McpServerBodySizeLimitTest
  from #851; PIT line-tracking through the early-return doesn't credit
  it consistently.
- L107 outer-catch 500: defensive wrapping around the entire dispatcher,
  fires only on unexpected runtime errors that the inner branches don't
  surface — engineered triggers would be contrived.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Skobeltsyn Skobeltsyn merged commit 0d55ed4 into main May 3, 2026
2 of 3 checks passed
@Skobeltsyn Skobeltsyn deleted the test/coverage-improvements branch May 3, 2026 16:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant