---
name: diagnose
description: Use when a test fails and you need to diagnose the root cause. Run the test, read errors, trace through generated and source code, fix, and verify.
allowed-tools: Bash(./mvnw:*), Bash(javap:*), Bash(unzip:*), Bash(grep:*)
---

# Diagnose Test Failure

## Overview

Systematic diagnosis of test failures in the Morphia project. Run the test, read the full error, trace the root cause through source and generated code, fix minimally, and verify.

## Workflow

1. **Run the failing test**
   ```
   ./mvnw test -pl :module-name -Dtest="ClassName#methodName" -Ddeploy.skip=true
   ```
   - Use `-Ddeploy.skip=true` to skip dokka
   - Do NOT use `-am` with `-Dtest=` (applies test filter to all modules)
   - If dependencies need rebuilding first:
     ```
     ./mvnw install -pl :dep1,:dep2 -DskipTests -Ddeploy.skip=true -Dinvoker.skip=true
     ```

2. **Read the full error** - stack trace, error message, line numbers. Don't skip anything.

3. **Find relevant code** - Read both source AND generated code:
   - Source: `src/main/`, `src/test/`
   - Generated bytecode: `target/test-classes/` - use `javap -c -p ClassName` to decompile
   - Generated sources: `target/generated-sources/`
   - Interfaces from dependencies: check `~/.m2/repository/` source jars with `unzip -p`

4. **Trace the root cause** - Follow the error backward:
   - What method is missing/wrong?
   - What generated it? All critter bytecode comes from the Class-File API generators in
     `core/src/main/java/dev/morphia/critter/parser/generator/` (see "Critter Code Generation" below).
   - Was it the AOT path (critter-maven, woven `__read*`/`__write*` methods) or the runtime path (hidden nestmate accessors)?
   - What does the interface/superclass require?
   - Compare with a working generator for the same pattern (e.g. how `PropertyAccessorGenerator` boxes primitives)

5. **Fix minimally** - One change at a time. Rebuild affected modules:
   ```
   ./mvnw install -pl :morphia-core,:critter-maven -DskipTests -Ddeploy.skip=true -Dinvoker.skip=true
   ```

6. **Verify** - Re-run the failing test AND related tests in the same class.

## Critter Code Generation

Critter generates bytecode with the Class-File API (`java.lang.classfile`), using the
`jdk-classfile-backport` library (package `io.smallrye.classfile`) so it runs on Java 17.
There is no Gizmo or ASM in the generators. Everything is in
`core/src/main/java/dev/morphia/critter/parser/generator/`, and `CritterGenerator` is the entry point.

| Generator | Produces |
|-----------|----------|
| `AddFieldAccessorMethods` / `AddMethodAccessorMethods` | AOT only: rewrites the entity class to add synthetic `__readXxx`/`__writeXxx` methods |
| `PropertyAccessorGenerator` | AOT only: a `PropertyAccessor` that calls the woven `__read*`/`__write*` methods |
| `NestmateAccessorGenerator` | Runtime only: a `PropertyAccessor` using direct `getfield`/`putfield`/`invokevirtual`, defined as a hidden nestmate (`Lookup.defineHiddenClass(..., NESTMATE)`) and registered in `NestmateAccessorRegistry` |
| `PropertyModelGenerator` | A `CritterPropertyModel` per property |
| `EntityModelGenerator` | The `CritterEntityModel` for the entity |
| `VarHandleAccessorGenerator` | A VarHandle/MethodHandle-based accessor; not used by the main pipeline right now (only `TestVarHandleAccessor`) |

The two paths:
- **AOT:** `critter-maven` (`generate-models` / `generate-test-models`) calls `CritterGenerator.generate(type, loader, false)`.
  It writes the generated models and the woven entity classes to disk under `__morphia/<entity>/`.
- **Runtime:** when no pre-generated model is on the classpath, `CritterMapper` calls `generate(type, loader, true)`.
  That defines classes in a `CritterClassLoader` plus hidden nestmates. If it fails, it logs once and falls back to a reflective `EntityModel`.
  Hidden nestmates need full privilege access, so an entity outside Morphia's module (on the classpath: loaded by a different
  class loader) fails with a `NestmateAccessException`; `CritterMapper` logs a specific "can't access ... isn't in Morphia's
  module" warning for it. That's expected for those layouts, and the fix is critter-maven AOT models, not a generator change.

`PropertyFinder` rejects some entities for AOT with `UnsupportedOperationException("AOT skip: ...")`:
inherited fields the entity can't reach and whose declaring class can't be rewritten (e.g. a private field in a library
superclass), fields shadowing a superclass field, array-typed fields, and entities whose `@Id` is on a getter.
Entities with no `@Id` at all (e.g., embedded types) are generated normally. A final field's `__writeXxx` sets it through
a cached `java.lang.reflect.Field`, since `putfield` may only write a final field from a constructor. An inherited field from a superclass in the
same output directory or jar gets its `__readXxx`/`__writeXxx` methods woven into that superclass, which the entity
inherits. The rejected entities use runtime generation instead, so an "AOT skip" warning in the critter-maven output is
expected for them and is not an error.

## Common Morphia/Critter Issues

| Error | Likely Cause |
|-------|-------------|
| `AbstractMethodError` | A generated class is missing an interface method. Generators emit methods with explicit erased descriptors (e.g. `get(Ljava/lang/Object;)Ljava/lang/Object;`), and nothing adds bridge methods for you |
| `VerifyError` / `Bad type on operand stack` | Usually a primitive/reference mismatch: a primitive was used where an `Object` was expected, or the reverse, without boxing or unboxing |
| `ClassCastException` in a generated accessor | `checkcast` to the wrong owner (for an inherited field, the declaring class vs the entity class) or to the wrong wrapper type |
| `IllegalAccessError` from a nestmate accessor | The hidden class was defined against the wrong lookup class. It must be `privateLookupIn` the field's *declaring* class |
| `NoSuchMethodError __read*/__write*` | The AOT accessor ran against an unwoven entity class, e.g. the woven class from `generated-classes/critter` is not ahead of the original on the classpath |
| `ClassNotFoundException __morphia.*` | Critter code generation didn't run or class not registered |

## Key Gotchas

- The Class-File API is low-level: no automatic boxing, casting, or bridge methods. Emit each step explicitly.
- Boxing: `invokestatic Wrapper.valueOf(prim)Wrapper`. Unboxing: `checkcast Wrapper`, then `invokevirtual Wrapper.xxxValue()`.
  Helpers live in `GenerationUtils` (`PRIMITIVE_TO_WRAPPER`, `primitiveUnboxMethod`, `primitiveClassDesc`).
- `checkcast` only accepts reference types. To get a primitive from an `Object`, cast to the wrapper and then unbox.
- Keep non-public types out of generated descriptors. AOT accessors use `Object` in `__read*`/`__write*` signatures for reference types,
  and the cast happens inside the entity.
- Inspect generated classes with `javap -c -p`. AOT output is under `target/generated-classes/critter` (main) or
  `target/test-classes/**/__morphia/` (tests). Runtime-generated classes are in memory only.
