present( baseline: [ 'description' => 'Before', 'display_name' => 'TenantPilot', 'retired_setting' => 'Legacy', ], current: [ 'description' => 'After', 'display_name' => 'TenantPilot', 'new_setting' => 'Enabled', ], labels: [ 'description' => 'Description', 'display_name' => 'Display name', 'new_setting' => 'New setting', 'retired_setting' => 'Retired setting', ], ); $rows = collect($presentation->rows)->keyBy('key'); expect($presentation->summary->changedCount)->toBe(1) ->and($presentation->summary->addedCount)->toBe(1) ->and($presentation->summary->removedCount)->toBe(1) ->and($presentation->summary->unchangedCount)->toBe(1) ->and($presentation->summary->hasRows)->toBeTrue() ->and($rows->get('description')?->status)->toBe(DiffRowStatus::Changed) ->and($rows->get('display_name')?->status)->toBe(DiffRowStatus::Unchanged) ->and($rows->get('new_setting')?->status)->toBe(DiffRowStatus::Added) ->and($rows->get('retired_setting')?->status)->toBe(DiffRowStatus::Removed); }); it('returns rows in deterministic label order', function (): void { $presentation = app(DiffPresenter::class)->present( baseline: [ 'zeta_value' => 'same', 'alpha_value' => 'before', ], current: [ 'alpha_value' => 'after', 'middle_value' => 'new', 'zeta_value' => 'same', ], labels: [ 'alpha_value' => 'Alpha value', 'middle_value' => 'Middle value', 'zeta_value' => 'Zeta value', ], ); expect(array_map( static fn ($row): string => $row->label, $presentation->rows, ))->toBe([ 'Alpha value', 'Middle value', 'Zeta value', ]); }); it('prepares inline list fragments for simple list comparisons', function (): void { $presentation = app(DiffPresenter::class)->present( baseline: [ 'scope_tags' => ['Default', 'Legacy', 'Shared'], ], current: [ 'scope_tags' => ['Default', 'Shared', 'Workspace'], ], labels: [ 'scope_tags' => 'Scope tags', ], ); $row = $presentation->rows[0] ?? null; expect($row)->not->toBeNull() ->and($row?->isListLike)->toBeTrue() ->and($row?->addedItems)->toBe(['Workspace']) ->and($row?->removedItems)->toBe(['Legacy']) ->and($row?->unchangedItems)->toBe(['Default', 'Shared']); }); it('falls back to generated labels and safe empty meta when optional metadata is missing or invalid', function (): void { $presentation = app(DiffPresenter::class)->present( baseline: [ 'customSettingFoo' => 'Before', ], current: [ 'customSettingFoo' => 'After', ], meta: [ 'customSettingFoo' => 'invalid-meta-shape', ], ); $row = $presentation->rows[0] ?? null; expect($row)->not->toBeNull() ->and($row?->label)->toBe('Custom Setting Foo') ->and($row?->meta)->toBe([]); }); it('returns a no-data presentation for empty or sparse compare payloads', function (): void { $presentation = app(DiffPresenter::class)->present( baseline: [], current: [], changedKeys: ['ghost_key'], labels: ['ghost_key' => 'Ghost key'], meta: ['ghost_key' => ['note' => 'unused']], ); expect($presentation->rows)->toBe([]) ->and($presentation->summary->hasRows)->toBeFalse() ->and($presentation->summary->changedCount)->toBe(0) ->and($presentation->summary->addedCount)->toBe(0) ->and($presentation->summary->removedCount)->toBe(0) ->and($presentation->summary->unchangedCount)->toBe(0) ->and($presentation->summary->message)->toBe('No diff data available.'); });