test(form-builder): registry/model alignment consistency invariant (WS-6)

Sessie 1 left BindingTypeRegistryConsistencyTest as the cross-cutting
invariant for the binding registry. This commit extends it with a
new assertion: every registry entity must map to a real Eloquent
model class, and every registry attribute must exist as a column
on that model's table.

Future drift (someone adds a registry attribute without the column,
or renames a column without updating the registry) becomes a test
failure on the next test run, not a runtime surprise.

Implementation: queries information_schema.COLUMNS via the active
MySQL connection (opaque DBs are not in Crewli's deployment matrix
per CLAUDE.md). Skips the 'artist' entity entirely — it's
intentionally absent from v1 registry per BACKLOG
ARTIST-ADV-BINDING-MODEL.

Pre-existing tests not touched by this commit (already updated in
previous Task 2 commit a404865 for the renames):
  - BindingTypeRegistryTest (collection-target tests use Config::set
    synthetic injection)
  - AppendStrategyRequiresCollectionTargetTest (same pattern)
  - MaxOneIdentityKeyPerTargetEntityTest (company.email →
    company.contact_email)

Refs: WS-6 sessie 3a binding-target drift audit

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-28 20:29:26 +02:00
parent 0e986f42cb
commit d48d91bba7

View File

@@ -8,6 +8,7 @@ use App\Enums\FormBuilder\BindingTargetType;
use App\FormBuilder\Bindings\BindingTypeRegistry;
use App\FormBuilder\Publishing\RequiresIdentityKeyBinding;
use App\FormBuilder\Purposes\PurposeRegistry;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
/**
@@ -91,4 +92,69 @@ final class BindingTypeRegistryConsistencyTest extends TestCase
}
}
}
/**
* Sessie 3a.5 drift-prevention invariant: every registry entity
* must map to a real Eloquent model class, and every registry
* attribute must exist as a column on that model's table.
*
* Without this assertion, a renamed model column or a typo in
* binding_targets.php only surfaces at apply time (or worse, in
* production after publish). With it, drift becomes a test
* failure on the next test run.
*/
public function test_every_registry_entity_maps_to_an_eloquent_model_with_the_attribute(): void
{
$entityToModel = [
'person' => \App\Models\Person::class,
'company' => \App\Models\Company::class,
'user' => \App\Models\User::class,
// Note: 'artist' intentionally absent from registry in v1
// (BACKLOG ARTIST-ADV-BINDING-MODEL).
];
$registry = config('form_builder.binding_targets');
$this->assertIsArray($registry);
foreach ($registry as $entity => $attributes) {
$this->assertArrayHasKey(
$entity,
$entityToModel,
"Registry entity '{$entity}' has no Eloquent model class mapped. "
."Add it to entityToModel here, or remove the entity from the registry.",
);
$modelClass = $entityToModel[$entity];
$this->assertTrue(
class_exists($modelClass),
"Entity '{$entity}' maps to {$modelClass} but the class does not exist.",
);
$instance = new $modelClass;
$columns = $this->columnsOnTable($instance->getTable());
foreach (array_keys($attributes) as $attribute) {
$this->assertContains(
$attribute,
$columns,
"Registry says '{$entity}.{$attribute}' exists, but column "
."'{$attribute}' is not on table '{$instance->getTable()}'.",
);
}
}
}
/**
* @return list<string>
*/
private function columnsOnTable(string $table): array
{
$rows = DB::select(
'SELECT COLUMN_NAME FROM information_schema.COLUMNS '
.'WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?',
[$table],
);
return array_map(static fn (object $r): string => (string) $r->COLUMN_NAME, $rows);
}
}