Skip to content

Commit 1560353

Browse files
Skobeltsynclaude
andcommitted
test(#2107): Agent coverage — property getters + threshold boundary + describePrompt
14 tests covering the 11 substantive Agent mutants (16 - 5 PIT noise: 3 invokeSuspend*$lambda$0 inline-attribution + 2 outside-file artifacts at L 597/L 604 in a 535-line file). Same-package access exploits @PublishedApi internal visibility on the listener/state properties: - errorListener: observable invocation when agent throws - routerRationaleListener: direct property read + invocation - defaultToolErrorHandler: null by default, populated via tools{defaults{onError{executionError{retry(1)}}}} - skillSelectionConfidenceThreshold: getter returns configured Double (kills PrimitiveReturns 0.0 mutant); default 0.6 verified - frozen flag: true post-validate; structural setter throws post-freeze - setter boundary: 0.0 and 1.0 inclusive; -0.01 / 1.01 reject - describePrompt boundary: exactly 80 verbatim, 81 truncated to first 77 + "..."; blank → "(none)" — via describe() (NOT toString()) Deferred: L 410 (resolveSkill confidence comparison) + L 421 (rationale-listener fire) need a stubbed ModelClient; the infra would dwarf the two tests. Filed as part of the issue for a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ef92b4c commit 1560353

1 file changed

Lines changed: 213 additions & 0 deletions

File tree

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
package agents_engine.core
2+
3+
import kotlin.test.Test
4+
import kotlin.test.assertEquals
5+
import kotlin.test.assertFails
6+
import kotlin.test.assertFalse
7+
import kotlin.test.assertNotNull
8+
import kotlin.test.assertNull
9+
import kotlin.test.assertTrue
10+
11+
// Tests for Agent cluster (16 unkilled — 5 are PIT noise: 3 lambda$0
12+
// inline-attribution + 2 outside-file artifacts at L 597/L 604; 11 substantive).
13+
//
14+
// Focus: property-getter mutants (NullReturnVals on errorListener,
15+
// routerRationale, defaultToolErrorHandler; PrimitiveReturns on confidence
16+
// threshold; BooleanReturns on frozen) + skillSelectionConfidenceThreshold
17+
// setter boundary + describePrompt length boundary at 80.
18+
class AgentCoverageTest {
19+
20+
// ── property getters: listener round-trip via internal-property access ───
21+
22+
@Test
23+
fun `errorListener fires when an agentic invocation throws`() {
24+
// Kills NullReturnVals on getErrorListener — the listener must be
25+
// non-null after onError {} runs, AND the post-invocation propagation
26+
// path must INVOKE it. Both proven by observation.
27+
val captured = mutableListOf<Throwable>()
28+
val failing = agent<String, String>("failing") {
29+
skills {
30+
skill<String, String>("s") {
31+
implementedBy { _: String -> error("boom") }
32+
}
33+
}
34+
onError { e -> captured.add(e) }
35+
}
36+
val ex = assertFails { failing("anything") }
37+
assertEquals(1, captured.size, "errorListener must have fired before exception propagated")
38+
assertEquals("boom", captured[0].message)
39+
assertNotNull(ex)
40+
}
41+
42+
@Test
43+
fun `routerRationaleListener field reflects DSL configuration`() {
44+
// Kills NullReturnVals on getRouterRationaleListener via the
45+
// @PublishedApi internal property read (same-package access).
46+
val captured = mutableListOf<String>()
47+
val a = agent<String, String>("a") {
48+
skills { skill<String, String>("s") { implementedBy { it } } }
49+
routerRationale { r -> captured.add(r) }
50+
}
51+
// The getter must return the lambda we set.
52+
val listener = a.routerRationaleListener
53+
assertNotNull(listener,
54+
"routerRationale {} must populate the listener; null-return mutant fails here")
55+
listener.invoke("test-rationale")
56+
assertEquals(listOf("test-rationale"), captured,
57+
"the stored lambda is the one we registered (no swap)")
58+
}
59+
60+
@Test
61+
fun `routerRationaleListener is null when no routerRationale block was declared`() {
62+
val a = agent<String, String>("a") {
63+
skills { skill<String, String>("s") { implementedBy { it } } }
64+
}
65+
assertNull(a.routerRationaleListener,
66+
"no DSL block → listener stays null (default)")
67+
}
68+
69+
@Test
70+
fun `defaultToolErrorHandler null by default and populated via tools defaults block`() {
71+
// Kills NullReturnVals on getDefaultToolErrorHandler — verify default
72+
// null AND the populated-via-DSL case.
73+
val plain = agent<String, String>("plain") {
74+
skills { skill<String, String>("s") { implementedBy { it } } }
75+
}
76+
assertNull(plain.defaultToolErrorHandler,
77+
"no tools{defaults{onError}} block → default handler null")
78+
79+
val withHandler = agent<String, String>("with") {
80+
skills { skill<String, String>("s") { implementedBy { it } } }
81+
tools {
82+
defaults {
83+
onError {
84+
executionError { retry(1) }
85+
}
86+
}
87+
}
88+
}
89+
assertNotNull(withHandler.defaultToolErrorHandler,
90+
"tools{defaults{onError}} block must populate defaultToolErrorHandler; null-return mutant fails here")
91+
}
92+
93+
@Test
94+
fun `skillSelectionConfidenceThreshold getter returns configured Double`() {
95+
// Kills PrimitiveReturnsMutator on getSkillSelectionConfidenceThreshold
96+
// (replaces return with 0.0). Default is 0.6; configure 0.85.
97+
val a = agent<String, String>("a") {
98+
skills { skill<String, String>("s") { implementedBy { it } } }
99+
skillSelectionConfidenceThreshold(0.85)
100+
}
101+
assertEquals(0.85, a.skillSelectionConfidenceThreshold,
102+
"getter returns configured threshold, not 0.0 (PrimitiveReturns mutant)")
103+
}
104+
105+
@Test
106+
fun `skillSelectionConfidenceThreshold defaults to 0_6`() {
107+
val a = agent<String, String>("a") {
108+
skills { skill<String, String>("s") { implementedBy { it } } }
109+
}
110+
assertEquals(0.6, a.skillSelectionConfidenceThreshold,
111+
"default threshold 0.6 (kills PrimitiveReturns mutant that returns 0.0)")
112+
}
113+
114+
@Test
115+
fun `agent frozen flag becomes true after validate`() {
116+
// Kills BooleanFalseReturnVals on getFrozen — the flag IS set to true
117+
// by validate(). Direct property read via @PublishedApi internal.
118+
val a = agent<String, String>("a") {
119+
skills { skill<String, String>("s") { implementedBy { it } } }
120+
}
121+
assertTrue(a.frozen, "post-validate agent must be frozen (kills BooleanFalseReturnVals)")
122+
}
123+
124+
@Test
125+
fun `agent post-freeze structural mutation throws`() {
126+
// Kills BooleanTrueReturnVals on getFrozen — if the getter always
127+
// returned true, the pre-validate setters would throw too. Build an
128+
// agent, then verify a post-validate structural setter throws.
129+
val a = agent<String, String>("a") {
130+
skills { skill<String, String>("s") { implementedBy { it } } }
131+
}
132+
val ex = assertFails { a.skillSelectionConfidenceThreshold(0.5) }
133+
assertNotNull(ex, "post-freeze structural mutator must throw")
134+
}
135+
136+
// ── skillSelectionConfidenceThreshold setter boundary (L 231) ────────────
137+
138+
@Test
139+
fun `skillSelectionConfidenceThreshold accepts boundary 0_0`() {
140+
// Kills ConditionalsBoundaryMutator on `threshold in 0.0..1.0` left side.
141+
val a = agent<String, String>("a") {
142+
skills { skill<String, String>("s") { implementedBy { it } } }
143+
skillSelectionConfidenceThreshold(0.0)
144+
}
145+
assertEquals(0.0, a.skillSelectionConfidenceThreshold,
146+
"0.0 is inclusive; boundary mutant flipping `>=` to `>` would reject it")
147+
}
148+
149+
@Test
150+
fun `skillSelectionConfidenceThreshold accepts boundary 1_0`() {
151+
// Kills ConditionalsBoundaryMutator on `threshold in 0.0..1.0` right side.
152+
val a = agent<String, String>("a") {
153+
skills { skill<String, String>("s") { implementedBy { it } } }
154+
skillSelectionConfidenceThreshold(1.0)
155+
}
156+
assertEquals(1.0, a.skillSelectionConfidenceThreshold,
157+
"1.0 is inclusive; boundary mutant flipping `<=` to `<` would reject it")
158+
}
159+
160+
@Test
161+
fun `skillSelectionConfidenceThreshold rejects just-below-zero and just-above-one`() {
162+
val build: (Double) -> Unit = { t ->
163+
agent<String, String>("a") {
164+
skills { skill<String, String>("s") { implementedBy { it } } }
165+
skillSelectionConfidenceThreshold(t)
166+
}
167+
}
168+
assertNotNull(assertFails { build(-0.01) }, "-0.01 outside boundary must throw")
169+
assertNotNull(assertFails { build(1.01) }, "1.01 outside boundary must throw")
170+
}
171+
172+
// ── describePrompt length boundary (L 467: prompt.length <= 80) ──────────
173+
174+
@Test
175+
fun `agent toString shows prompt verbatim at exactly 80 chars`() {
176+
// L 467: `prompt.length <= 80 -> prompt`. The 80-char case must
177+
// render verbatim. Boundary mutant flipping `<=` to `<` would
178+
// truncate the 80-char case.
179+
val exactly80 = "x".repeat(80)
180+
val a = agent<String, String>("a") {
181+
prompt(exactly80)
182+
skills { skill<String, String>("s") { implementedBy { it } } }
183+
}
184+
val rendered = a.describe()
185+
assertTrue(rendered.contains(exactly80),
186+
"80-char prompt must render verbatim (inclusive boundary)")
187+
assertFalse(rendered.contains("$exactly80..."),
188+
"no ellipsis at exactly 80 chars")
189+
}
190+
191+
@Test
192+
fun `agent toString truncates prompt at 81 chars to first 77 plus ellipsis`() {
193+
val prompt81 = "x".repeat(81)
194+
val a = agent<String, String>("a") {
195+
prompt(prompt81)
196+
skills { skill<String, String>("s") { implementedBy { it } } }
197+
}
198+
val rendered = a.describe()
199+
assertTrue(rendered.contains("${"x".repeat(77)}..."),
200+
"81-char prompt must render as first 77 chars + '...'")
201+
}
202+
203+
@Test
204+
fun `agent toString shows (none) for blank prompt`() {
205+
// First branch of describePrompt's `when`: prompt.isBlank() -> "(none)".
206+
val a = agent<String, String>("a") {
207+
skills { skill<String, String>("s") { implementedBy { it } } }
208+
}
209+
val rendered = a.describe()
210+
assertTrue(rendered.contains("(none)"),
211+
"blank prompt → '(none)': $rendered")
212+
}
213+
}

0 commit comments

Comments
 (0)