We pass `null` for the class name here:

https://github.com/elastic/elasticsearch/blob/f086dd18ddc705bf84a1ff99d35196ec2498fd52/server/src/main/java/org/elasticsearch/plugins/PluginDescriptor.java#L261

However the class name is mandatory for the transport protocol today:

https://github.com/elastic/elasticsearch/blob/f086dd18ddc705bf84a1ff99d35196ec2498fd52/server/src/main/java/org/elasticsearch/plugins/PluginDescriptor.java#L156

This prevents various APIs from working in a cluster with stable plugins installed.
server/src/test/java/org/elasticsearch/plugins/PluginDescriptorTests.java
Collections.singletonList(
                randomValueOtherThanMany(v -> descriptor1.getExtendedPlugins().contains(v), () -> randomAlphaOfLengthBetween(4, 12))
            ),
            descriptor1.hasNativeController() == false,
            descriptor1.isLicensed() == false,
            descriptor1.isModular() == false,
            descriptor1.isStable() == false
        );
        
        PluginDescriptor descriptor3 = new PluginDescriptor(
            randomValueOtherThan(descriptor1.getName(), () -> randomAlphaOfLengthBetween(4, 12)),
            descriptor1.getDescription(),
            descriptor1.getVersion(),
            descriptor1.getElasticsearchVersion(),
            descriptor1.getJavaVersion(),
            descriptor1.getClassname(),
            descriptor1.getModuleName().orElse(null),
            descriptor1.getExtendedPlugins(),
            descriptor1.hasNativeController(),
            descriptor1.isLicensed(),
            descriptor1.isModular(),
            descriptor1.isStable()
        );
        assertThat(descriptor1, equalTo(descriptor2));
        assertThat(descriptor1.hashCode(), equalTo(descriptor2.hashCode()));
        assertThat(descriptor1, not(equalTo(descriptor3)));
        assertThat(descriptor1.hashCode(), not(equalTo(descriptor3.hashCode())));
    }
}