Skip to content

Commit 2ca4c21

Browse files
Skobeltsynclaude
andcommitted
test(#2050): GeneratedMetaCache — 12 tests with hand-crafted __GeneratedSchema fixtures
GeneratedMetaCache is `private object` inside GenerableSupport.kt; existing tests touch it transitively but every @generable test class LACKS a __GeneratedSchema KSP-emitted sibling, so the lookup always misses and the "found a generated class" branches are all unkilled (14 mutants in tryLoad/load). Solution: 8 fixture pairs (target + sibling __GeneratedSchema object) mimicking what KSP would emit, each shaped to trip a specific guard: - Clean: both constants + constructor — happy-path read-back - Empty: companion has nothing — kills L103 (both-empty → null) - Bad type: `const val JSON_SCHEMA: Int = 42` — kills L88 (type filter) - Non-final: `@JvmField var ...` — kills L87 (final filter) - Null value: `@JvmField val ...: String? = null` — kills L91/92 - Non-static: Kotlin class (not object) so field is instance — kills L86 - Ctor-only: no string consts — kills L103 right side - No companion: ClassNotFoundException catch + cached MISS Cache-hit (L69) tested via `===` referential equality on returned String (const-pool entries come back as the same instance on cache hit). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d0a6e9a commit 2ca4c21

1 file changed

Lines changed: 271 additions & 0 deletions

File tree

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
package agents_engine.generation
2+
3+
import kotlin.test.Test
4+
import kotlin.test.assertEquals
5+
import kotlin.test.assertFalse
6+
import kotlin.test.assertNotEquals
7+
import kotlin.test.assertNotNull
8+
import kotlin.test.assertNull
9+
import kotlin.test.assertTrue
10+
11+
// Tests for the `private object GeneratedMetaCache` inside GenerableSupport.kt.
12+
// 14 mutants (13 SURVIVED + 1 NO_COVERAGE) cluster on `load` and `tryLoad` —
13+
// the cache-and-load helper that finds `${fqn}__GeneratedSchema` companions
14+
// emitted by the KSP processor. The cache is `private`; we exercise it via
15+
// the public lookup surface (jsonSchema / toLlmDescription / constructFromMap)
16+
// on hand-crafted fixtures with deliberately-shaped __GeneratedSchema classes.
17+
//
18+
// Each fixture is a pair (Target class + sibling __GeneratedSchema object)
19+
// where the object's static final String fields drive what GeneratedMetaCache
20+
// is supposed to find or filter out.
21+
class GeneratedMetaCacheTest {
22+
23+
// ── happy-path: clean __GeneratedSchema fills both lookups + ctor ─────────
24+
25+
@Test
26+
fun `lookupJsonSchema returns generated JSON_SCHEMA verbatim`() {
27+
// Kills the cache-hit / tryLoad-return-Entry chain: the assertion ties
28+
// the public return value to a string that ONLY the generated companion
29+
// carries (the reflection path would emit a different schema shape).
30+
val schema = MetaCacheCleanFoo::class.jsonSchema()
31+
assertEquals(GENERATED_CLEAN_FOO_SCHEMA, schema,
32+
"generated JSON_SCHEMA must be returned byte-identical, not reflection-built: '$schema'")
33+
}
34+
35+
@Test
36+
fun `toLlmDescription returns generated LLM_DESCRIPTION verbatim`() {
37+
val desc = MetaCacheCleanFoo::class.toLlmDescription()
38+
assertEquals(GENERATED_CLEAN_FOO_DESC, desc,
39+
"generated LLM_DESCRIPTION must be returned verbatim: '$desc'")
40+
}
41+
42+
@Test
43+
fun `constructFromMap dispatches to generated constructor when present`() {
44+
// The fixture's constructFromMap sets a sentinel flag we read back to
45+
// prove the GENERATED path executed (vs reflection-fallback).
46+
MetaCacheCleanFoo__GeneratedSchema.lastCtorInvoked = false
47+
val out = MetaCacheCleanFoo::class.constructFromMap(mapOf("dummy" to 1))
48+
assertNotNull(out)
49+
assertTrue(MetaCacheCleanFoo__GeneratedSchema.lastCtorInvoked,
50+
"generated constructFromMap must have run (sentinel flag)")
51+
}
52+
53+
// ── cache: second call returns same value (no re-parse) ───────────────────
54+
55+
@Test
56+
fun `lookupJsonSchema cache returns same value on repeat call`() {
57+
// Kills the L 69 cache-hit branch indirectly: if the cache check were
58+
// negated, the second call would re-walk the fields, but still return
59+
// the same content (so equality alone doesn't kill it). We assert
60+
// referential equality on the returned String — generated const-vals
61+
// come back as the SAME String instance via field.get(null), so the
62+
// intern() / cache-hit path yields `===` equality across calls.
63+
val a = MetaCacheCleanFoo::class.jsonSchema()
64+
val b = MetaCacheCleanFoo::class.jsonSchema()
65+
assertTrue(a === b, "repeat lookup must return the same String instance (cache hit + const pool)")
66+
}
67+
68+
// ── empty fixture: __GeneratedSchema exists but has nothing useful ────────
69+
70+
@Test
71+
fun `lookup falls through to reflection when __GeneratedSchema has no constants and no constructor`() {
72+
// Kills L 103 `if (constants.isEmpty() && constructor == null) null`:
73+
// both empty → tryLoad returns null → load caches MISS → public
74+
// lookup falls through to reflection.
75+
val schema = MetaCacheEmptyBar::class.jsonSchema()
76+
// Reflection-built schema includes "type":"object" with declared field name.
77+
assertTrue(schema.contains(""""type":"object""""),
78+
"empty generated schema must fall through to reflection: $schema")
79+
assertTrue(schema.contains("greeting"),
80+
"reflection schema must include the actual field 'greeting': $schema")
81+
}
82+
83+
// ── type filter: non-String JSON_SCHEMA is skipped ────────────────────────
84+
85+
@Test
86+
fun `non-String fields are filtered out of the constants map`() {
87+
// Kills L 88 `field.type == String::class.java` negation: the fixture
88+
// has `const val JSON_SCHEMA: Int = 42` — a static final NON-String.
89+
// The type guard must reject it; jsonSchema() should NOT return "42"
90+
// and must fall through to the reflection path.
91+
val schema = MetaCacheBadTypeBaz::class.jsonSchema()
92+
assertFalse(schema.contains("42"),
93+
"Int-typed JSON_SCHEMA must be filtered by the type guard: $schema")
94+
// LLM_DESCRIPTION (a real String const) IS still loaded.
95+
assertEquals(GENERATED_BAD_TYPE_DESC, MetaCacheBadTypeBaz::class.toLlmDescription(),
96+
"co-located String const must still load even when sibling Int is filtered")
97+
}
98+
99+
// ── final filter: @JvmField var (non-final) is skipped ────────────────────
100+
101+
@Test
102+
fun `non-final static String fields are filtered out`() {
103+
// Kills L 87 `Modifier.isFinal(mods)` negation: @JvmField var produces
104+
// a static-but-NOT-final field. The final-guard must reject it.
105+
val schema = MetaCacheMutableQux::class.jsonSchema()
106+
assertFalse(schema.contains("should-be-filtered"),
107+
"non-final @JvmField var must be filtered: $schema")
108+
// The const val LLM_DESCRIPTION (static final String) still loads.
109+
assertEquals(GENERATED_MUTABLE_QUX_DESC, MetaCacheMutableQux::class.toLlmDescription())
110+
}
111+
112+
// ── null-value filter: @JvmField val: String? = null is skipped ──────────
113+
114+
@Test
115+
fun `static final String fields with null value are skipped from constants map`() {
116+
// Kills L 91/92 `if (value != null) constants[field.name] = value`:
117+
// a String? field whose value is null must NOT pollute the constants
118+
// map. The lookup falls through to reflection.
119+
val schema = MetaCacheNullValueQuux::class.jsonSchema()
120+
// If the null-value guard were broken, constants["JSON_SCHEMA"] would
121+
// be set to "" or NPE on the cast; either way the assertion below
122+
// would fail vs the reflection schema.
123+
assertTrue(schema.contains(""""type":"object""""),
124+
"null-valued JSON_SCHEMA must be ignored; reflection schema returned: $schema")
125+
}
126+
127+
// ── static filter: instance (non-static) fields are skipped ───────────────
128+
129+
@Test
130+
fun `instance (non-static) fields are filtered out by the static guard`() {
131+
// Kills L 86 `Modifier.isStatic(mods)` negation. The fixture's
132+
// __GeneratedSchema is a Kotlin `class` (not object), so its primary
133+
// ctor parameter becomes a non-static instance field. Class.forName
134+
// succeeds but the field is filtered, leaving constants empty +
135+
// constructor null → MISS cached.
136+
val schema = MetaCacheInstanceFields::class.jsonSchema()
137+
assertFalse(schema.contains("should-be-filtered"),
138+
"non-static instance field must be filtered: $schema")
139+
assertTrue(schema.contains(""""type":"object""""),
140+
"expected reflection fallback after filter rejected everything: $schema")
141+
}
142+
143+
// ── constructor-only fixture: kills the L 103 constructor.isEmpty side ────
144+
145+
@Test
146+
fun `__GeneratedSchema with only constructFromMap (no constants) still loads via Entry`() {
147+
// Kills L 103: `constants.isEmpty() && constructor == null`. With
148+
// constants empty AND constructor present, the `&&` short-circuits
149+
// to false → Entry returned (not null) → MISS not cached.
150+
// Verify by constructing a value through the public API.
151+
MetaCacheCtorOnly__GeneratedSchema.lastCtorInvoked = false
152+
val out = MetaCacheCtorOnly::class.constructFromMap(emptyMap<String, Any?>())
153+
assertNotNull(out, "constructor-only fixture must produce an instance")
154+
assertTrue(MetaCacheCtorOnly__GeneratedSchema.lastCtorInvoked,
155+
"generated constructFromMap must have run")
156+
// ALSO: jsonSchema lookup falls through to reflection because constants empty.
157+
val schema = MetaCacheCtorOnly::class.jsonSchema()
158+
assertTrue(schema.contains(""""type":"object""""),
159+
"no JSON_SCHEMA const → fall through to reflection: $schema")
160+
}
161+
162+
// ── miss: class without any __GeneratedSchema at all ─────────────────────
163+
164+
@Test
165+
fun `class without __GeneratedSchema falls through to reflection (ClassNotFoundException path)`() {
166+
// Kills the `catch (_: ClassNotFoundException)` early-return + the
167+
// L 70 `?: MISS` fallback. MetaCacheNoCompanion has NO generated
168+
// sibling, so Class.forName throws and tryLoad returns null.
169+
val desc = MetaCacheNoCompanion::class.toLlmDescription()
170+
// Reflection-built description includes the class name.
171+
assertTrue(desc.contains("MetaCacheNoCompanion"),
172+
"reflection fallback must run when no __GeneratedSchema exists: $desc")
173+
// Repeat call exercises the cached-MISS path (L 69 hit).
174+
val again = MetaCacheNoCompanion::class.toLlmDescription()
175+
assertEquals(desc, again, "cached MISS must yield the same reflection result")
176+
}
177+
178+
// ── distinct return: generated and reflection paths differ ───────────────
179+
180+
@Test
181+
fun `generated JSON_SCHEMA differs from reflection-built schema (proves cache short-circuits)`() {
182+
// Defensive: if the cache return were skipped entirely, the public
183+
// method would still produce a valid (reflection) schema — but a
184+
// DIFFERENT one. Comparing them proves the cache fired.
185+
val generated = MetaCacheCleanFoo::class.jsonSchema()
186+
// The reflection path on the same class would compute something
187+
// structurally different — `{"type":"object","properties":{...}}`
188+
// — not our hand-crafted GENERATED_CLEAN_FOO_SCHEMA sentinel.
189+
assertNotEquals("""{"type":"object","properties":{},"additionalProperties":false}""", generated,
190+
"generated path must not collapse to the trivial empty-object schema")
191+
assertEquals(GENERATED_CLEAN_FOO_SCHEMA, generated)
192+
}
193+
}
194+
195+
// ─────────────────────────────────────────────────────────────────────────────
196+
// Fixtures — top-level so their FQNs are predictable for the
197+
// `${fqn}__GeneratedSchema` lookup convention.
198+
// ─────────────────────────────────────────────────────────────────────────────
199+
200+
// Sentinel constants used in assertions, defined once.
201+
internal const val GENERATED_CLEAN_FOO_SCHEMA = """{"type":"object","properties":{"clean":{"type":"boolean"}}}"""
202+
internal const val GENERATED_CLEAN_FOO_DESC = "## CleanFoo (generated)"
203+
internal const val GENERATED_BAD_TYPE_DESC = "## BadType (generated, mixed-type fields)"
204+
internal const val GENERATED_MUTABLE_QUX_DESC = "## MutableQux (generated, non-final sibling)"
205+
206+
// FIXTURE 1: clean — both constants + a constructor.
207+
class MetaCacheCleanFoo
208+
object MetaCacheCleanFoo__GeneratedSchema {
209+
const val JSON_SCHEMA: String = GENERATED_CLEAN_FOO_SCHEMA
210+
const val LLM_DESCRIPTION: String = GENERATED_CLEAN_FOO_DESC
211+
212+
// Sentinel so the test can verify the GENERATED ctor ran (vs reflection).
213+
@JvmField var lastCtorInvoked: Boolean = false
214+
215+
@JvmStatic
216+
fun constructFromMap(@Suppress("UNUSED_PARAMETER") fields: Map<*, Any?>): MetaCacheCleanFoo {
217+
lastCtorInvoked = true
218+
return MetaCacheCleanFoo()
219+
}
220+
}
221+
222+
// FIXTURE 2: empty — companion exists but has no string consts and no ctor.
223+
data class MetaCacheEmptyBar(val greeting: String = "hi")
224+
object MetaCacheEmptyBar__GeneratedSchema {
225+
// Intentionally empty. INSTANCE field is static final but type ≠ String.
226+
}
227+
228+
// FIXTURE 3: non-String const → must be filtered by the type guard.
229+
class MetaCacheBadTypeBaz
230+
object MetaCacheBadTypeBaz__GeneratedSchema {
231+
const val JSON_SCHEMA: Int = 42 // wrong type — filtered
232+
const val LLM_DESCRIPTION: String = GENERATED_BAD_TYPE_DESC
233+
}
234+
235+
// FIXTURE 4: non-final var → must be filtered by the final guard.
236+
class MetaCacheMutableQux
237+
object MetaCacheMutableQux__GeneratedSchema {
238+
@JvmField var JSON_SCHEMA: String = "should-be-filtered" // non-final
239+
const val LLM_DESCRIPTION: String = GENERATED_MUTABLE_QUX_DESC
240+
}
241+
242+
// FIXTURE 5: static final String? with null value → skipped by value-null guard.
243+
data class MetaCacheNullValueQuux(val payload: String = "p")
244+
object MetaCacheNullValueQuux__GeneratedSchema {
245+
@JvmField val JSON_SCHEMA: String? = null // static final String, value=null
246+
}
247+
248+
// FIXTURE 6: non-static instance fields → filtered by the static guard.
249+
// Using a Kotlin `class` (not object) for the __GeneratedSchema means the
250+
// primary-constructor backed field is an INSTANCE field, not static.
251+
data class MetaCacheInstanceFields(val text: String = "default")
252+
@Suppress("unused")
253+
class MetaCacheInstanceFields__GeneratedSchema {
254+
@JvmField val JSON_SCHEMA: String = "should-be-filtered" // instance field, not static
255+
}
256+
257+
// FIXTURE 7: constructor only, no string consts.
258+
class MetaCacheCtorOnly
259+
object MetaCacheCtorOnly__GeneratedSchema {
260+
@JvmField var lastCtorInvoked: Boolean = false
261+
262+
@JvmStatic
263+
fun constructFromMap(@Suppress("UNUSED_PARAMETER") fields: Map<*, Any?>): MetaCacheCtorOnly {
264+
lastCtorInvoked = true
265+
return MetaCacheCtorOnly()
266+
}
267+
}
268+
269+
// FIXTURE 8: no __GeneratedSchema sibling at all → ClassNotFoundException path.
270+
data class MetaCacheNoCompanion(val n: Int = 0)
271+
// (intentionally NO MetaCacheNoCompanion__GeneratedSchema)

0 commit comments

Comments
 (0)