|
| 1 | +# Covariance Validation Test Specification |
| 2 | + |
| 3 | +## Purpose |
| 4 | +Define exhaustive tests for GraphQL interface implementation covariance and nullability compatibility in FSharp.Data.GraphQL type validation. |
| 5 | + |
| 6 | +## Problem Statement (Current Bug) |
| 7 | +During schema initialization, executor calls `Validation.Types.validateTypeMap schema.TypeMap` and throws `GQLMessageException` when validation returns errors. |
| 8 | + |
| 9 | +Observed runtime error pattern: |
| 10 | +- `'<Object>.<field>' field signature does not match it's definition in interface <Interface>` |
| 11 | + |
| 12 | +Exact failure location in library: |
| 13 | +- `src/FSharp.Data.GraphQL.Server/Executor.fs` (schema startup validation) |
| 14 | +- `src/FSharp.Data.GraphQL.Shared/Validation.fs`, function `validateImplements` |
| 15 | + |
| 16 | +Current implementation in `validateImplements` uses strict equality: |
| 17 | +- `Some objf when objf = f -> acc` |
| 18 | +- otherwise reports signature mismatch |
| 19 | + |
| 20 | +This equality-based check is stricter than GraphQL spec subtyping rules for interface field return types and nullability covariance. |
| 21 | + |
| 22 | +## GraphQL Compatibility Rules to Validate |
| 23 | +For object type `O implements I`, each interface field `f` must satisfy: |
| 24 | +1. Field exists on object with same name. |
| 25 | +2. Arguments are compatible (same required args; extra args on object must be optional). |
| 26 | +3. Return type on object is equal to or a valid subtype of interface return type. |
| 27 | +4. Non-null covariance: `T!` is subtype of `T` (allowed). |
| 28 | +5. List/null wrappers must be compared structurally by spec subtyping rules. |
| 29 | +6. If interface return type is interface/union, object return type may be a concrete implementing/member type (covariance). |
| 30 | + |
| 31 | +## Nullable / StructNullable Coverage |
| 32 | +In this codebase: |
| 33 | +- `Nullable X` and `StructNullable X` both produce nullable GraphQL wrappers. |
| 34 | +- Non-wrapper `X` is non-null GraphQL type. |
| 35 | + |
| 36 | +Tests must cover both wrappers equivalently for compatibility decisions: |
| 37 | +- `Nullable InterfaceType` vs concrete non-null implementor type. |
| 38 | +- `StructNullable InterfaceType` vs concrete non-null implementor type. |
| 39 | +- `Nullable T` vs `Nullable T` exact match. |
| 40 | +- `StructNullable T` vs `StructNullable T` exact match. |
| 41 | +- Negative cases where nested wrappers are incompatible (e.g., list item nullability mismatch). |
| 42 | + |
| 43 | +## Generic Test Model (No domain-specific names) |
| 44 | +Use neutral names only: |
| 45 | + |
| 46 | +Interfaces: |
| 47 | +- `IParentView` |
| 48 | +- `IChildView` |
| 49 | + |
| 50 | +GraphQL interfaces: |
| 51 | +- `IChildInfo` |
| 52 | +- `IParentInfo` with field `child: IChildInfo` |
| 53 | + |
| 54 | +Concrete object types: |
| 55 | +- `ChildAInfo implements IChildInfo` |
| 56 | +- `ChildBInfo implements IChildInfo` |
| 57 | +- `ParentAInfo implements IParentInfo` with `child: ChildAInfo` |
| 58 | +- `ParentBInfo implements IParentInfo` with `child: ChildBInfo` |
| 59 | + |
| 60 | +This model must be reused for all covariance and nullability test cases. |
| 61 | + |
| 62 | +## Test Matrix (Must Cover All Cases) |
| 63 | +### A. Positive covariance cases (must pass) |
| 64 | +1. Interface field type `IChildInfo`, object field type `ChildAInfo` (implements `IChildInfo`). |
| 65 | +2. Same as A1 for second implementation (`ChildBInfo`). |
| 66 | +3. Interface field `Nullable IChildInfo`, object field non-null `ChildAInfo`. |
| 67 | +4. Interface field `StructNullable IChildInfo`, object field non-null `ChildAInfo`. |
| 68 | +5. Interface field non-null `IChildInfo`, object field same non-null `IChildInfo` (exact). |
| 69 | +6. Interface field list `List<IChildInfo>`, object field list `List<ChildAInfo>` where library supports list covariance by member subtype. |
| 70 | +7. Deep wrappers: interface `Nullable(List(Nullable(IChildInfo)))`, object `List(ChildAInfo)` where valid by non-null covariance. |
| 71 | + |
| 72 | +### B. Negative covariance cases (must fail) |
| 73 | +1. Interface field `IChildInfo`, object field unrelated object type `OtherInfo` (not implementing). |
| 74 | +2. Interface field non-null `IChildInfo`, object field nullable `Nullable IChildInfo` (wider, invalid). |
| 75 | +3. Interface field list `List<IChildInfo>`, object field scalar `ChildAInfo`. |
| 76 | +4. Interface field `List<NonNull IChildInfo>`, object field `List<Nullable ChildAInfo>` (invalid nullability widening). |
| 77 | +5. Interface field arguments mismatch (missing required arg, type mismatch, extra required arg). |
| 78 | + |
| 79 | +### C. Nullable vs StructNullable parity (must pass/fail identically) |
| 80 | +For each scenario A3, A4, B2, B4 create paired tests: |
| 81 | +- one with `Nullable` |
| 82 | +- one with `StructNullable` |
| 83 | +Expected result must be identical for semantic-equivalent wrappers. |
| 84 | + |
| 85 | +### D. Existing strict-equality regression (must reproduce old bug) |
| 86 | +Create a test where only difference is: |
| 87 | +- interface field type = interface def |
| 88 | +- object field type = implementing concrete object def |
| 89 | + |
| 90 | +Expected by spec: Success. |
| 91 | +Current behavior before fix: ValidationError with signature mismatch message. |
| 92 | +This test documents the bug and prevents reintroduction. |
| 93 | + |
| 94 | +## Test File Placement |
| 95 | +- Extend `tests/FSharp.Data.GraphQL.Tests/TypeValidationTests.fs` for focused unit cases, or |
| 96 | +- create `tests/FSharp.Data.GraphQL.Tests/TypeValidationCovarianceTests.fs` if separation is preferred. |
| 97 | + |
| 98 | +## Assertion Style |
| 99 | +- Use `validateImplements` for unit-level behavior. |
| 100 | +- Use `validateTypeMap` for end-to-end schema-level validation with multiple types registered. |
| 101 | +- Verify exact error strings for negative tests where stable, otherwise verify error contains object+field+interface identifiers. |
| 102 | + |
| 103 | +## Proposed Fix in Validation Engine |
| 104 | +Replace strict `objf = f` signature equality with structural GraphQL compatibility check: |
| 105 | +1. Compare field names and argument compatibility by spec rules. |
| 106 | +2. Compare return types via `isOutputSubtype(objectType, interfaceType)`. |
| 107 | +3. Implement recursive wrapper-aware subtype check: |
| 108 | + - `NonNull(A)` subtype of `A` |
| 109 | + - `List(A)` subtype of `List(B)` iff `A` subtype of `B` |
| 110 | + - object subtype of interface if object implements interface |
| 111 | + - object subtype of union if object is a union member |
| 112 | + - named scalars/enums require exact type identity |
| 113 | + |
| 114 | +Pseudo-contract: |
| 115 | +- `isFieldImplementationCompatible(objectField, interfaceField) -> bool` |
| 116 | +- used by `validateImplements` instead of direct equality. |
| 117 | + |
| 118 | +## Acceptance Criteria |
| 119 | +1. All positive covariance tests pass. |
| 120 | +2. All negative compatibility tests fail with deterministic errors. |
| 121 | +3. Nullable/StructNullable parity tests pass. |
| 122 | +4. No regressions in existing `TypeValidationTests.fs`. |
| 123 | +5. Schema initialization no longer throws for valid covariance implementations. |
| 124 | + |
| 125 | +## Notes for Reviewers |
| 126 | +- This is a spec-driven validation correction, not a domain-model workaround. |
| 127 | +- Goal is GraphQL spec compliance at type-system validation layer. |
0 commit comments