Skip to content
Back to skills

Diagnose

ASecurity

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.

  • 1,676 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 7, 2026
developmentgojavabashapi

Works with

  • api

Security analysis

A100/100

Scanned November 7, 2026

npx -y skills add MorphiaOrg/morphia --skill diagnose --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Diagnose?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Diagnose
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/morphiaorg-diagnose/badge)](https://www.skillsdirectory.com/skills/morphiaorg-diagnose)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.