Spec 207: implement shared test fixture slimming

This commit is contained in:
Ahmed Darrazi 2026-04-16 19:27:47 +02:00
parent 3c38192405
commit 0fe98ca9ca
35 changed files with 2833 additions and 96 deletions

View File

@ -192,6 +192,8 @@ ## Active Technologies
- PostgreSQL plus existing Laravel session-backed table filter, search, and sort persistence; no schema change planned (198-monitoring-page-state)
- PHP 8.4.15 + Laravel 12, Pest v4, PHPUnit 12, Pest Browser plugin, Filament v5, Livewire v4, Laravel Sail (206-test-suite-governance)
- SQLite `:memory:` for the default test configuration, dedicated PostgreSQL config for the schema-level `Pgsql` suite, and local runner artifacts under `apps/platform/storage/logs/test-lanes` (206-test-suite-governance)
- PHP 8.4.15 + Laravel 12, Pest v4, PHPUnit 12, Filament v5, Livewire v4, Laravel Sail (207-shared-test-fixture-slimming)
- SQLite `:memory:` for the default test environment, isolated PostgreSQL coverage via the existing dedicated suite, and lane-measurement artifacts under the app-root contract path `storage/logs/test-lanes` (207-shared-test-fixture-slimming)
- PHP 8.4.15 (feat/005-bulk-operations)
@ -226,8 +228,8 @@ ## Code Style
PHP 8.4.15: Follow standard conventions
## Recent Changes
- 207-shared-test-fixture-slimming: Added PHP 8.4.15 + Laravel 12, Pest v4, PHPUnit 12, Filament v5, Livewire v4, Laravel Sail
- 206-test-suite-governance: Added PHP 8.4.15 + Laravel 12, Pest v4, PHPUnit 12, Pest Browser plugin, Filament v5, Livewire v4, Laravel Sail
- 198-monitoring-page-state: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `CanonicalAdminTenantFilterState`, `CanonicalNavigationContext`, `OperateHubShell`, Filament `InteractsWithTable`, and page-local Livewire state on the affected Filament pages
- 197-shared-detail-contract: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `VerificationReportViewer`, `VerificationReportChangeIndicator`, `PolicyNormalizer`, `VersionDiff`, `DriftFindingDiffBuilder`, and `SettingsCatalogSettingsTable`
<!-- MANUAL ADDITIONS START -->
<!-- MANUAL ADDITIONS END -->

View File

@ -89,10 +89,11 @@ ### Fixture Cost Guidance
- `createUserWithTenant()` now defaults to the explicit cheap `minimal` profile.
- Use `createMinimalUserWithTenant()` in high-usage callers that only need tenant membership and workspace/session wiring.
- Use `fixtureProfile: 'provider-enabled'` when a test needs a default Microsoft provider connection.
- Use `fixtureProfile: 'credential-enabled'` when provider identity resolution, admin-consent URLs, or Graph option construction needs dedicated credentials.
- Use `fixtureProfile: 'ui-context'` when the helper should also seed current tenant UI context and clear capability caches.
- Use `fixtureProfile: 'heavy'` only when the full side-effect bundle is intentionally required.
- Use `createStandardUserWithTenant()` or `fixtureProfile: 'standard'` when a test needs a default Microsoft provider connection without credentials, cache resets, or UI context.
- Use `createFullUserWithTenant()` or `fixtureProfile: 'full'` when a test intentionally needs provider, credential, cache-reset, and UI-context side effects together.
- Use `OperationRun::factory()->minimal()` for system-style runs and `OperationRun::factory()->withUser($user)` only when the initiator identity is materially part of the assertion.
- Use `BackupSet::factory()->full()` only when the test really needs backup items; the default backup-set factory path now stays item-free.
- `provider-enabled`, `credential-enabled`, `ui-context`, and `heavy` remain available only as temporary transition aliases while the first migration packs are landing.
### DB Reset and Seed Rules

View File

@ -23,12 +23,47 @@ public function definition(): array
'name' => fake()->words(3, true),
'created_by' => fake()->email(),
'status' => 'completed',
'item_count' => fake()->numberBetween(0, 100),
'item_count' => 0,
'completed_at' => now(),
'metadata' => [],
];
}
public function minimal(): static
{
return $this->state(fn (): array => [
'item_count' => 0,
'metadata' => [],
]);
}
/**
* @param array<string, mixed> $itemAttributes
*/
public function withItems(int $count = 1, array $itemAttributes = []): static
{
$count = max(1, $count);
return $this->state(fn (): array => [
'item_count' => $count,
])->afterCreating(function ($backupSet) use ($count, $itemAttributes): void {
BackupItem::factory()
->count($count)
->for($backupSet->tenant)
->for($backupSet)
->create(array_merge([
'payload' => ['id' => 'backup-item-'.fake()->uuid()],
'metadata' => [],
'assignments' => [],
], $itemAttributes));
});
}
public function full(): static
{
return $this->recentCompleted()->withItems();
}
public function recentCompleted(): static
{
return $this->state(fn (): array => [

View File

@ -44,8 +44,8 @@ public function definition(): array
return (int) $tenant->workspace_id;
},
'user_id' => User::factory(),
'initiator_name' => fake()->name(),
'user_id' => null,
'initiator_name' => 'System',
'type' => fake()->randomElement(OperationRunType::values()),
'status' => OperationRunStatus::Queued->value,
'outcome' => OperationRunOutcome::Pending->value,
@ -58,6 +58,22 @@ public function definition(): array
];
}
public function minimal(): static
{
return $this->state(fn (): array => [
'user_id' => null,
'initiator_name' => 'System',
]);
}
public function withUser(?User $user = null): static
{
return $this->state(fn (): array => [
'user_id' => $user?->getKey() ?? User::factory(),
'initiator_name' => $user?->name ?? fake()->name(),
]);
}
public function forTenant(Tenant $tenant): static
{
return $this->state(fn (): array => [

View File

@ -90,6 +90,15 @@ public function dedicated(): static
]);
}
public function standard(): static
{
return $this->dedicated()
->verifiedHealthy()
->state(fn (): array => [
'is_default' => true,
]);
}
public function consentGranted(): static
{
return $this->state(fn (): array => [
@ -147,4 +156,9 @@ public function withCredential(): static
$connection->refresh();
});
}
public function full(): static
{
return $this->withCredential();
}
}

View File

@ -31,6 +31,18 @@ public function definition(): array
];
}
public function standard(): static
{
return $this->state(fn (): array => [
'provider_connection_id' => ProviderConnection::factory()->standard(),
]);
}
public function verifiedConnection(): static
{
return $this->standard();
}
public function legacyMigrated(): static
{
return $this->state(fn (): array => [

View File

@ -16,7 +16,7 @@
use Carbon\CarbonImmutable;
it('Baseline finding fidelity is content when both baseline and current are content', function () {
[$user, $tenant] = createUserWithTenant();
[$user, $tenant] = createMinimalUserWithTenant();
$profile = BaselineProfile::factory()->active()->create([
'workspace_id' => $tenant->workspace_id,
@ -154,7 +154,7 @@
});
it('Baseline finding fidelity is meta when baseline evidence is meta (even if current content exists)', function () {
[$user, $tenant] = createUserWithTenant();
[$user, $tenant] = createMinimalUserWithTenant();
$profile = BaselineProfile::factory()->active()->create([
'workspace_id' => $tenant->workspace_id,

View File

@ -41,7 +41,7 @@ protected function makeBaselineCompareMatrixFixture(
string $viewerRole = 'owner',
?string $workspaceRole = null,
): array {
[$user, $visibleTenant] = createUserWithTenant(role: $viewerRole, workspaceRole: $workspaceRole ?? $viewerRole);
[$user, $visibleTenant] = createMinimalUserWithTenant(role: $viewerRole, workspaceRole: $workspaceRole ?? $viewerRole);
$workspace = Workspace::query()->findOrFail((int) $visibleTenant->workspace_id);

View File

@ -36,7 +36,7 @@ protected function makePortfolioTriageActor(
'name' => $tenantName,
]);
[$user, $tenant] = createUserWithTenant(
[$user, $tenant] = createMinimalUserWithTenant(
tenant: $tenant,
role: $role,
workspaceRole: $workspaceRole,
@ -55,7 +55,7 @@ protected function makePortfolioTriagePeer(User $user, Tenant $workspaceTenant,
'name' => $name,
]);
createUserWithTenant(
createMinimalUserWithTenant(
tenant: $tenant,
user: $user,
role: 'owner',

View File

@ -29,11 +29,10 @@
$startedAt = CarbonImmutable::parse('2026-01-01 00:00:00', 'UTC');
$operationRun = OperationRun::create([
'workspace_id' => (int) $tenant->workspace_id,
'tenant_id' => $tenant->id,
'user_id' => null,
'initiator_name' => 'System',
$operationRun = OperationRun::factory()
->minimal()
->forTenant($tenant)
->create([
'type' => 'backup_schedule_run',
'status' => 'running',
'outcome' => 'pending',

View File

@ -10,12 +10,9 @@
uses(RefreshDatabase::class);
it('reconciles stale covered runs from the console command', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
[$user, $tenant] = createMinimalUserWithTenant(role: 'owner');
$run = OperationRun::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'user_id' => (int) $user->getKey(),
$run = OperationRun::factory()->withUser($user)->forTenant($tenant)->create([
'type' => 'policy.sync',
'status' => OperationRunStatus::Queued->value,
'outcome' => OperationRunOutcome::Pending->value,
@ -34,12 +31,9 @@
});
it('supports dry-run mode without mutating runs', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
[$user, $tenant] = createMinimalUserWithTenant(role: 'owner');
$run = OperationRun::factory()->create([
'tenant_id' => (int) $tenant->getKey(),
'workspace_id' => (int) $tenant->workspace_id,
'user_id' => (int) $user->getKey(),
$run = OperationRun::factory()->withUser($user)->forTenant($tenant)->create([
'type' => 'policy.sync',
'status' => OperationRunStatus::Queued->value,
'outcome' => OperationRunOutcome::Pending->value,

View File

@ -2,15 +2,13 @@
declare(strict_types=1);
it('keeps the shared tenant helper profile matrix explicit and reviewable', function (): void {
$profiles = createUserWithTenantProfiles();
it('keeps the canonical shared tenant helper profile catalog explicit and reviewable', function (): void {
$profiles = createUserWithTenantProfileCatalog();
expect($profiles)->toHaveKeys([
'minimal',
'provider-enabled',
'credential-enabled',
'ui-context',
'heavy',
'standard',
'full',
])
->and($profiles['minimal'])->toMatchArray([
'workspace' => true,
@ -21,8 +19,63 @@
'cache' => false,
'uiContext' => false,
])
->and($profiles['provider-enabled']['provider'])->toBeTrue()
->and($profiles['credential-enabled']['credential'])->toBeTrue()
->and($profiles['ui-context']['uiContext'])->toBeTrue()
->and($profiles['heavy']['cache'])->toBeTrue();
->and($profiles['standard'])->toMatchArray([
'workspace' => true,
'membership' => true,
'session' => true,
'provider' => true,
'credential' => false,
'cache' => false,
'uiContext' => false,
])
->and($profiles['full'])->toMatchArray([
'workspace' => true,
'membership' => true,
'session' => true,
'provider' => true,
'credential' => true,
'cache' => true,
'uiContext' => true,
]);
});
it('keeps the temporary legacy fixture aliases explicit and reviewable', function (): void {
$aliases = createUserWithTenantLegacyProfileAliases();
expect($aliases)->toHaveKeys([
'provider-enabled',
'credential-enabled',
'ui-context',
'heavy',
])
->and($aliases['provider-enabled']['profile'])->toBe('standard')
->and($aliases['credential-enabled']['profile'])->toBe('full')
->and($aliases['ui-context']['profile'])->toBe('full')
->and($aliases['heavy']['profile'])->toBe('full')
->and($aliases['provider-enabled']['removalTrigger'])->not->toBe('')
->and($aliases['credential-enabled']['removalTrigger'])->not->toBe('')
->and($aliases['ui-context']['removalTrigger'])->not->toBe('')
->and($aliases['heavy']['removalTrigger'])->not->toBe('');
});
it('resolves legacy aliases to the promised side-effect bundles', function (): void {
$credentialEnabled = resolveCreateUserWithTenantProfile('credential-enabled');
$uiContext = resolveCreateUserWithTenantProfile('ui-context');
expect($credentialEnabled['canonicalProfile'])->toBe('full')
->and($credentialEnabled['legacyAlias'])->toBe('credential-enabled')
->and($credentialEnabled['sideEffects'])->toMatchArray([
'provider' => true,
'credential' => true,
'cache' => false,
'uiContext' => false,
])
->and($uiContext['canonicalProfile'])->toBe('full')
->and($uiContext['legacyAlias'])->toBe('ui-context')
->and($uiContext['sideEffects'])->toMatchArray([
'provider' => false,
'credential' => false,
'cache' => true,
'uiContext' => true,
]);
});

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
use Tests\Support\TestLaneManifest;
use Tests\Support\TestLaneReport;
it('keeps the shared fixture slimming pre-migration baselines recorded for the standard lanes', function (): void {
$fastFeedback = TestLaneManifest::comparisonBaseline('shared-test-fixture-slimming', 'fast-feedback');
$confidence = TestLaneManifest::comparisonBaseline('shared-test-fixture-slimming', 'confidence');
expect($fastFeedback)->toMatchArray([
'laneId' => 'fast-feedback',
'wallClockSeconds' => 176.73623,
'targetImprovementPercent' => 10,
'maxRegressionPercent' => 5,
])
->and($confidence)->toMatchArray([
'laneId' => 'confidence',
'wallClockSeconds' => 394.383441,
'targetImprovementPercent' => 10,
'maxRegressionPercent' => 5,
]);
});
it('classifies lane-impact comparison status against the recorded fixture slimming baseline', function (): void {
$improved = TestLaneReport::buildReport(
laneId: 'fast-feedback',
wallClockSeconds: 150.0,
slowestEntries: [],
durationsByFile: [],
comparisonProfile: 'shared-test-fixture-slimming',
);
$stable = TestLaneReport::buildReport(
laneId: 'fast-feedback',
wallClockSeconds: 180.0,
slowestEntries: [],
durationsByFile: [],
comparisonProfile: 'shared-test-fixture-slimming',
);
$regressed = TestLaneReport::buildReport(
laneId: 'fast-feedback',
wallClockSeconds: 190.0,
slowestEntries: [],
durationsByFile: [],
comparisonProfile: 'shared-test-fixture-slimming',
);
expect(data_get($improved, 'sharedFixtureSlimmingComparison.status'))->toBe('improved')
->and(data_get($stable, 'sharedFixtureSlimmingComparison.status'))->toBe('stable')
->and(data_get($regressed, 'sharedFixtureSlimmingComparison.status'))->toBe('regressed');
});

View File

@ -20,4 +20,25 @@
expect(file_exists($gitignore))->toBeTrue()
->and((string) file_get_contents($gitignore))->toContain('*')
->and((string) file_get_contents($gitignore))->toContain('!.gitignore');
});
it('publishes the shared fixture slimming comparison only for the governed standard lanes', function (): void {
$fastFeedback = TestLaneReport::buildReport(
laneId: 'fast-feedback',
wallClockSeconds: 176.73623,
slowestEntries: [],
durationsByFile: [],
comparisonProfile: 'shared-test-fixture-slimming',
);
$heavyGovernance = TestLaneReport::buildReport(
laneId: 'heavy-governance',
wallClockSeconds: 83.66,
slowestEntries: [],
durationsByFile: [],
comparisonProfile: 'shared-test-fixture-slimming',
);
expect($fastFeedback)->toHaveKey('sharedFixtureSlimmingComparison')
->and($heavyGovernance)->not->toHaveKey('sharedFixtureSlimmingComparison');
});

View File

@ -22,7 +22,7 @@
uses(RefreshDatabase::class);
it('reuses finding related navigation and caches deterministic negative results', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
[$user, $tenant] = createMinimalUserWithTenant(role: 'owner');
$this->actingAs($user);
Filament::setTenant($tenant, true);
@ -45,8 +45,7 @@
'policy_id' => (int) $policy->getKey(),
]);
$run = OperationRun::factory()->for($tenant)->create([
'workspace_id' => (int) $tenant->workspace_id,
$run = OperationRun::factory()->minimal()->forTenant($tenant)->create([
'type' => 'baseline_compare',
]);
@ -103,12 +102,11 @@
});
it('reuses operation-run related context across detail and header consumers', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
[$user, $tenant] = createMinimalUserWithTenant(role: 'owner');
$this->actingAs($user);
Filament::setTenant($tenant, true);
$run = OperationRun::factory()->for($tenant)->create([
'workspace_id' => (int) $tenant->workspace_id,
$run = OperationRun::factory()->minimal()->forTenant($tenant)->create([
'type' => 'backup_set.add_policies',
'context' => [
'backup_set_id' => 123,
@ -130,7 +128,7 @@
it('keeps related-navigation target routes tenant-safe for non-members and capability-limited members', function (): void {
$workspaceTenant = \App\Models\Tenant::factory()->create();
[$member, $workspaceTenant] = createUserWithTenant(tenant: $workspaceTenant, role: 'readonly');
[$member, $workspaceTenant] = createMinimalUserWithTenant(tenant: $workspaceTenant, role: 'readonly');
$nonMember = \App\Models\User::factory()->create();
$profile = BaselineProfile::factory()->active()->create([

View File

@ -24,7 +24,7 @@
});
it('allows workspace members to open the workspace-managed tenants index', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
[$user, $tenant] = createMinimalUserWithTenant(role: 'owner');
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
@ -43,7 +43,7 @@
});
it('allows workspace members to open the workspace-managed tenant view route', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
[$user, $tenant] = createMinimalUserWithTenant(role: 'owner');
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
@ -52,7 +52,7 @@
});
it('exposes a provider connections link from the workspace-managed tenant view page', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
[$user, $tenant] = createMinimalUserWithTenant(role: 'owner');
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
@ -72,7 +72,7 @@
});
it('exposes memberships management under workspace scope', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
[$user, $tenant] = createMinimalUserWithTenant(role: 'owner');
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
@ -89,7 +89,7 @@
'tenant_id' => '11111111-1111-1111-1111-111111111111',
]);
[$entitledUser] = createUserWithTenant($tenant, role: 'readonly');
[$entitledUser] = createMinimalUserWithTenant(tenant: $tenant, role: 'readonly');
$nonEntitledUser = User::factory()->create();
WorkspaceMembership::factory()->create([
@ -120,7 +120,7 @@
});
it('keeps tenant panel route shape canonical and rejects duplicated /t prefixes', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
[$user, $tenant] = createMinimalUserWithTenant(role: 'owner');
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
@ -134,7 +134,7 @@
});
it('removes tenant-scoped management routes', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
[$user, $tenant] = createMinimalUserWithTenant(role: 'owner');
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
@ -153,7 +153,7 @@
});
it('serves provider connection management under workspace-managed tenant routes only', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
[$user, $tenant] = createMinimalUserWithTenant(role: 'owner');
$connection = ProviderConnection::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
@ -174,7 +174,7 @@
});
it('returns 403 for workspace members missing mutation capability on provider connections', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'readonly', workspaceRole: 'readonly');
[$user, $tenant] = createMinimalUserWithTenant(role: 'readonly', workspaceRole: 'readonly');
$this->followingRedirects()
->actingAs($user)
@ -190,7 +190,7 @@
});
it('writes canonical membership audit entries for membership mutations', function (): void {
[$owner, $tenant] = createUserWithTenant(role: 'owner');
[$owner, $tenant] = createMinimalUserWithTenant(role: 'owner');
$member = User::factory()->create();
/** @var TenantMembershipManager $manager */
@ -233,7 +233,7 @@
});
it('keeps workspace navigation entries after panel split', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
[$user, $tenant] = createMinimalUserWithTenant(role: 'owner');
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
@ -251,7 +251,7 @@
expect($tenantPanelResources)->not->toContain(TenantResource::class);
expect($tenantPanelResources)->not->toContain(ProviderConnectionResource::class);
[$user, $tenant] = createUserWithTenant(role: 'owner');
[$user, $tenant] = createMinimalUserWithTenant(role: 'owner');
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
@ -262,7 +262,7 @@
});
it('keeps global search scoped to workspace-managed tenant resources only', function (): void {
[$workspaceUser, $tenant] = createUserWithTenant(role: 'owner');
[$workspaceUser, $tenant] = createMinimalUserWithTenant(role: 'owner');
Filament::setCurrentPanel('admin');
Filament::setTenant(null, true);

View File

@ -371,7 +371,7 @@ function createInventorySyncOperationRunWithCoverage(
/**
* @return array<string, array{workspace: bool, membership: bool, session: bool, provider: bool, credential: bool, cache: bool, uiContext: bool}>
*/
function createUserWithTenantProfiles(): array
function createUserWithTenantProfileCatalog(): array
{
return [
'minimal' => [
@ -383,7 +383,7 @@ function createUserWithTenantProfiles(): array
'cache' => false,
'uiContext' => false,
],
'provider-enabled' => [
'standard' => [
'workspace' => true,
'membership' => true,
'session' => true,
@ -392,25 +392,7 @@ function createUserWithTenantProfiles(): array
'cache' => false,
'uiContext' => false,
],
'credential-enabled' => [
'workspace' => true,
'membership' => true,
'session' => true,
'provider' => true,
'credential' => true,
'cache' => false,
'uiContext' => false,
],
'ui-context' => [
'workspace' => true,
'membership' => true,
'session' => true,
'provider' => false,
'credential' => false,
'cache' => true,
'uiContext' => true,
],
'heavy' => [
'full' => [
'workspace' => true,
'membership' => true,
'session' => true,
@ -422,6 +404,95 @@ function createUserWithTenantProfiles(): array
];
}
/**
* @return array<string, array{profile: string, overrides?: array{workspace?: bool, membership?: bool, session?: bool, provider?: bool, credential?: bool, cache?: bool, uiContext?: bool}, removalTrigger: string}>
*/
function createUserWithTenantLegacyProfileAliases(): array
{
return [
'provider-enabled' => [
'profile' => 'standard',
'removalTrigger' => 'Retire after the first fast-feedback and confidence migration packs use the canonical standard profile directly.',
],
'credential-enabled' => [
'profile' => 'full',
'overrides' => [
'cache' => false,
'uiContext' => false,
],
'removalTrigger' => 'Retire after the credential-dependent caller pack adopts createFullUserWithTenant() or fixtureProfile: full plus local overrides.',
],
'ui-context' => [
'profile' => 'full',
'overrides' => [
'provider' => false,
'credential' => false,
],
'removalTrigger' => 'Retire after UI-context callers switch to the canonical full profile or an explicit local UI-context helper.',
],
'heavy' => [
'profile' => 'full',
'removalTrigger' => 'Retire after legacy heavy callers migrate to the canonical full profile.',
],
];
}
/**
* @return array{requestedProfile: string, canonicalProfile: string, sideEffects: array{workspace: bool, membership: bool, session: bool, provider: bool, credential: bool, cache: bool, uiContext: bool}, legacyAlias: ?string, removalTrigger: ?string}
*/
function resolveCreateUserWithTenantProfile(string $fixtureProfile): array
{
$catalog = createUserWithTenantProfileCatalog();
if (array_key_exists($fixtureProfile, $catalog)) {
return [
'requestedProfile' => $fixtureProfile,
'canonicalProfile' => $fixtureProfile,
'sideEffects' => $catalog[$fixtureProfile],
'legacyAlias' => null,
'removalTrigger' => null,
];
}
$aliases = createUserWithTenantLegacyProfileAliases();
if (! array_key_exists($fixtureProfile, $aliases)) {
throw new \InvalidArgumentException(sprintf('Unknown fixture profile [%s].', $fixtureProfile));
}
$resolution = $aliases[$fixtureProfile];
$canonicalProfile = $resolution['profile'];
if (! array_key_exists($canonicalProfile, $catalog)) {
throw new \InvalidArgumentException(sprintf('Unknown canonical fixture profile [%s].', $canonicalProfile));
}
return [
'requestedProfile' => $fixtureProfile,
'canonicalProfile' => $canonicalProfile,
'sideEffects' => array_replace($catalog[$canonicalProfile], $resolution['overrides'] ?? []),
'legacyAlias' => $fixtureProfile,
'removalTrigger' => $resolution['removalTrigger'],
];
}
/**
* @return array<string, array{workspace: bool, membership: bool, session: bool, provider: bool, credential: bool, cache: bool, uiContext: bool}>
*/
function createUserWithTenantProfiles(): array
{
$profiles = createUserWithTenantProfileCatalog();
foreach (createUserWithTenantLegacyProfileAliases() as $alias => $resolution) {
$profiles[$alias] = array_replace(
$profiles[$resolution['profile']],
$resolution['overrides'] ?? [],
);
}
return $profiles;
}
/**
* @return array{0: User, 1: Tenant}
*/
@ -450,6 +521,174 @@ function createMinimalUserWithTenant(
);
}
/**
* @return array{0: User, 1: Tenant}
*/
function createStandardUserWithTenant(
?Tenant $tenant = null,
?User $user = null,
string $role = 'owner',
?string $workspaceRole = null,
bool $ensureDefaultMicrosoftProviderConnection = false,
string $defaultProviderConnectionType = ProviderConnectionType::Dedicated->value,
bool $ensureDefaultCredential = false,
?bool $clearCapabilityCaches = null,
?bool $setUiContext = null,
): array {
return createUserWithTenant(
tenant: $tenant,
user: $user,
role: $role,
workspaceRole: $workspaceRole,
ensureDefaultMicrosoftProviderConnection: $ensureDefaultMicrosoftProviderConnection,
defaultProviderConnectionType: $defaultProviderConnectionType,
fixtureProfile: 'standard',
ensureDefaultCredential: $ensureDefaultCredential,
clearCapabilityCaches: $clearCapabilityCaches,
setUiContext: $setUiContext,
);
}
/**
* @return array{0: User, 1: Tenant}
*/
function createFullUserWithTenant(
?Tenant $tenant = null,
?User $user = null,
string $role = 'owner',
?string $workspaceRole = null,
bool $ensureDefaultMicrosoftProviderConnection = false,
string $defaultProviderConnectionType = ProviderConnectionType::Dedicated->value,
bool $ensureDefaultCredential = false,
?bool $clearCapabilityCaches = null,
?bool $setUiContext = null,
): array {
return createUserWithTenant(
tenant: $tenant,
user: $user,
role: $role,
workspaceRole: $workspaceRole,
ensureDefaultMicrosoftProviderConnection: $ensureDefaultMicrosoftProviderConnection,
defaultProviderConnectionType: $defaultProviderConnectionType,
fixtureProfile: 'full',
ensureDefaultCredential: $ensureDefaultCredential,
clearCapabilityCaches: $clearCapabilityCaches,
setUiContext: $setUiContext,
);
}
/**
* @return array{0: User, 1: Tenant}
*/
function createProviderEnabledUserWithTenant(
?Tenant $tenant = null,
?User $user = null,
string $role = 'owner',
?string $workspaceRole = null,
bool $ensureDefaultMicrosoftProviderConnection = false,
string $defaultProviderConnectionType = ProviderConnectionType::Dedicated->value,
bool $ensureDefaultCredential = false,
?bool $clearCapabilityCaches = null,
?bool $setUiContext = null,
): array {
return createUserWithTenant(
tenant: $tenant,
user: $user,
role: $role,
workspaceRole: $workspaceRole,
ensureDefaultMicrosoftProviderConnection: $ensureDefaultMicrosoftProviderConnection,
defaultProviderConnectionType: $defaultProviderConnectionType,
fixtureProfile: 'provider-enabled',
ensureDefaultCredential: $ensureDefaultCredential,
clearCapabilityCaches: $clearCapabilityCaches,
setUiContext: $setUiContext,
);
}
/**
* @return array{0: User, 1: Tenant}
*/
function createCredentialEnabledUserWithTenant(
?Tenant $tenant = null,
?User $user = null,
string $role = 'owner',
?string $workspaceRole = null,
bool $ensureDefaultMicrosoftProviderConnection = false,
string $defaultProviderConnectionType = ProviderConnectionType::Dedicated->value,
bool $ensureDefaultCredential = false,
?bool $clearCapabilityCaches = null,
?bool $setUiContext = null,
): array {
return createUserWithTenant(
tenant: $tenant,
user: $user,
role: $role,
workspaceRole: $workspaceRole,
ensureDefaultMicrosoftProviderConnection: $ensureDefaultMicrosoftProviderConnection,
defaultProviderConnectionType: $defaultProviderConnectionType,
fixtureProfile: 'credential-enabled',
ensureDefaultCredential: $ensureDefaultCredential,
clearCapabilityCaches: $clearCapabilityCaches,
setUiContext: $setUiContext,
);
}
/**
* @return array{0: User, 1: Tenant}
*/
function createUiContextUserWithTenant(
?Tenant $tenant = null,
?User $user = null,
string $role = 'owner',
?string $workspaceRole = null,
bool $ensureDefaultMicrosoftProviderConnection = false,
string $defaultProviderConnectionType = ProviderConnectionType::Dedicated->value,
bool $ensureDefaultCredential = false,
?bool $clearCapabilityCaches = null,
?bool $setUiContext = null,
): array {
return createUserWithTenant(
tenant: $tenant,
user: $user,
role: $role,
workspaceRole: $workspaceRole,
ensureDefaultMicrosoftProviderConnection: $ensureDefaultMicrosoftProviderConnection,
defaultProviderConnectionType: $defaultProviderConnectionType,
fixtureProfile: 'ui-context',
ensureDefaultCredential: $ensureDefaultCredential,
clearCapabilityCaches: $clearCapabilityCaches,
setUiContext: $setUiContext,
);
}
/**
* @return array{0: User, 1: Tenant}
*/
function createHeavyUserWithTenant(
?Tenant $tenant = null,
?User $user = null,
string $role = 'owner',
?string $workspaceRole = null,
bool $ensureDefaultMicrosoftProviderConnection = false,
string $defaultProviderConnectionType = ProviderConnectionType::Dedicated->value,
bool $ensureDefaultCredential = false,
?bool $clearCapabilityCaches = null,
?bool $setUiContext = null,
): array {
return createUserWithTenant(
tenant: $tenant,
user: $user,
role: $role,
workspaceRole: $workspaceRole,
ensureDefaultMicrosoftProviderConnection: $ensureDefaultMicrosoftProviderConnection,
defaultProviderConnectionType: $defaultProviderConnectionType,
fixtureProfile: 'heavy',
ensureDefaultCredential: $ensureDefaultCredential,
clearCapabilityCaches: $clearCapabilityCaches,
setUiContext: $setUiContext,
);
}
/**
* @return array{0: User, 1: Tenant}
*/
@ -465,13 +704,8 @@ function createUserWithTenant(
?bool $clearCapabilityCaches = null,
?bool $setUiContext = null,
): array {
$profiles = createUserWithTenantProfiles();
if (! array_key_exists($fixtureProfile, $profiles)) {
throw new \InvalidArgumentException(sprintf('Unknown fixture profile [%s].', $fixtureProfile));
}
$profile = $profiles[$fixtureProfile];
$resolvedProfile = resolveCreateUserWithTenantProfile($fixtureProfile);
$profile = $resolvedProfile['sideEffects'];
$user ??= User::factory()->create();
$tenant ??= Tenant::factory()->create();

View File

@ -25,6 +25,27 @@ final class TestLaneManifest
'junit' => 'test:junit',
];
private const COMPARISON_BASELINES = [
'shared-test-fixture-slimming' => [
'fast-feedback' => [
'laneId' => 'fast-feedback',
'finishedAt' => '2026-04-16T13:11:57+00:00',
'wallClockSeconds' => 176.73623,
'budgetThresholdSeconds' => 200,
'targetImprovementPercent' => 10,
'maxRegressionPercent' => 5,
],
'confidence' => [
'laneId' => 'confidence',
'finishedAt' => '2026-04-16T13:11:57+00:00',
'wallClockSeconds' => 394.383441,
'budgetThresholdSeconds' => 450,
'targetImprovementPercent' => 10,
'maxRegressionPercent' => 5,
],
],
];
/**
* @return array<string, mixed>
*/
@ -384,6 +405,22 @@ public static function fullSuiteBaselineSeconds(): int
return self::FULL_SUITE_BASELINE_SECONDS;
}
/**
* @return array<string, float|int|string>|null
*/
public static function comparisonBaseline(string $comparisonProfile, string $laneId): ?array
{
$profileBaselines = self::COMPARISON_BASELINES[$comparisonProfile] ?? null;
if (! is_array($profileBaselines)) {
return null;
}
$baseline = $profileBaselines[$laneId] ?? null;
return is_array($baseline) ? $baseline : null;
}
/**
* @return list<string>
*/
@ -448,7 +485,7 @@ public static function runLane(string $laneId): int
return $process->getExitCode() ?? 1;
}
public static function renderLatestReport(string $laneId): int
public static function renderLatestReport(string $laneId, ?string $comparisonProfile = null): int
{
$artifactPaths = TestLaneReport::artifactPaths($laneId);
$reportPath = self::absolutePath($artifactPaths['report']);
@ -467,6 +504,7 @@ public static function renderLatestReport(string $laneId): int
wallClockSeconds: $wallClockSeconds,
slowestEntries: $parsed['slowestEntries'],
durationsByFile: $parsed['durationsByFile'],
comparisonProfile: $comparisonProfile,
);
TestLaneReport::writeArtifacts(

View File

@ -85,6 +85,7 @@ public static function buildReport(
array $slowestEntries,
array $durationsByFile,
?string $artifactDirectory = null,
?string $comparisonProfile = null,
): array {
$lane = TestLaneManifest::lane($laneId);
$budget = TestLaneBudget::fromArray($lane['budget']);
@ -130,6 +131,12 @@ public static function buildReport(
$report['baselineDeltaTargetPercent'] = $budget->baselineDeltaTargetPercent;
}
$comparison = self::buildSharedFixtureSlimmingComparison($laneId, $wallClockSeconds, $comparisonProfile);
if ($comparison !== null) {
$report['sharedFixtureSlimmingComparison'] = $comparison;
}
return $report;
}
@ -163,6 +170,7 @@ public static function writeArtifacts(
'budgetLifecycleState' => $report['budgetLifecycleState'],
'budgetStatus' => $report['budgetStatus'],
'familyBudgetEvaluations' => $report['familyBudgetEvaluations'],
'sharedFixtureSlimmingComparison' => $report['sharedFixtureSlimmingComparison'] ?? null,
], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
);
@ -181,7 +189,12 @@ public static function writeArtifacts(
/**
* @return array<string, mixed>
*/
public static function finalizeLane(string $laneId, float $wallClockSeconds, string $capturedOutput = ''): array
public static function finalizeLane(
string $laneId,
float $wallClockSeconds,
string $capturedOutput = '',
?string $comparisonProfile = null,
): array
{
$artifactPaths = self::artifactPaths($laneId);
$parsed = self::parseJUnit(TestLaneManifest::absolutePath($artifactPaths['junit']), $laneId);
@ -190,6 +203,7 @@ public static function finalizeLane(string $laneId, float $wallClockSeconds, str
wallClockSeconds: $wallClockSeconds,
slowestEntries: $parsed['slowestEntries'],
durationsByFile: $parsed['durationsByFile'],
comparisonProfile: $comparisonProfile,
);
self::writeArtifacts($laneId, $report, $capturedOutput);
@ -209,10 +223,22 @@ private static function buildSummaryMarkdown(array $report): string
sprintf('- Finished: %s', $report['finishedAt']),
sprintf('- Wall clock: %.2f seconds', (float) $report['wallClockSeconds']),
sprintf('- Budget: %d seconds (%s)', (int) $report['budgetThresholdSeconds'], $report['budgetStatus']),
'',
'## Slowest entries',
];
if (isset($report['sharedFixtureSlimmingComparison']) && is_array($report['sharedFixtureSlimmingComparison'])) {
$comparison = $report['sharedFixtureSlimmingComparison'];
$lines[] = sprintf(
'- Shared fixture slimming baseline: %.2f seconds (%s, %+0.2f%%)',
(float) $comparison['baselineSeconds'],
(string) $comparison['status'],
(float) $comparison['deltaPercent'],
);
}
$lines[] = '';
$lines[] = '## Slowest entries';
foreach ($report['slowestEntries'] as $entry) {
$lines[] = sprintf('- %s (%.2fs)', $entry['subject'], (float) $entry['durationSeconds']);
}
@ -220,6 +246,52 @@ private static function buildSummaryMarkdown(array $report): string
return implode(PHP_EOL, $lines).PHP_EOL;
}
/**
* @return array<string, float|int|string>|null
*/
private static function buildSharedFixtureSlimmingComparison(
string $laneId,
float $wallClockSeconds,
?string $comparisonProfile,
): ?array {
if ($comparisonProfile !== 'shared-test-fixture-slimming') {
return null;
}
$baseline = TestLaneManifest::comparisonBaseline($comparisonProfile, $laneId);
if (! is_array($baseline)) {
return null;
}
$baselineSeconds = (float) $baseline['wallClockSeconds'];
$deltaSeconds = round($wallClockSeconds - $baselineSeconds, 6);
$deltaPercent = $baselineSeconds > 0
? round(($deltaSeconds / $baselineSeconds) * 100, 6)
: 0.0;
$targetImprovementPercent = (int) ($baseline['targetImprovementPercent'] ?? 10);
$maxRegressionPercent = (int) ($baseline['maxRegressionPercent'] ?? 5);
$status = 'stable';
if ($deltaPercent <= -$targetImprovementPercent) {
$status = 'improved';
} elseif ($deltaPercent > $maxRegressionPercent) {
$status = 'regressed';
}
return [
'comparisonProfile' => $comparisonProfile,
'baselineFinishedAt' => (string) $baseline['finishedAt'],
'baselineSeconds' => $baselineSeconds,
'deltaSeconds' => $deltaSeconds,
'deltaPercent' => $deltaPercent,
'targetImprovementPercent' => $targetImprovementPercent,
'maxRegressionPercent' => $maxRegressionPercent,
'status' => $status,
];
}
private static function ensureDirectory(string $directory): void
{
if (is_dir($directory)) {

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
use App\Models\BackupSet;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('keeps the default backup set factory path free of backup item side effects', function (): void {
$backupSet = BackupSet::factory()->create();
expect($backupSet->item_count)->toBe(0)
->and($backupSet->items()->count())->toBe(0);
});
it('can opt into a fuller backup graph with explicit items', function (): void {
$backupSet = BackupSet::factory()->full()->create();
expect($backupSet->item_count)->toBe(1)
->and($backupSet->items()->count())->toBe(1);
});
it('keeps stale and degraded backup item graphs behind explicit named states', function (): void {
$stale = BackupSet::factory()->staleCompleted()->create();
$degraded = BackupSet::factory()->degradedCompleted()->create();
expect($stale->items()->count())->toBe(1)
->and($degraded->items()->count())->toBe(1);
});

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
use App\Models\OperationRun;
use App\Models\User;
use App\Models\Workspace;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('keeps the default operation run factory path lean by avoiding implicit user creation', function (): void {
$run = OperationRun::factory()->create();
expect($run->tenant_id)->not->toBeNull()
->and($run->workspace_id)->not->toBeNull()
->and($run->user_id)->toBeNull()
->and($run->initiator_name)->toBe('System');
});
it('can opt into an interactive operation context with an explicit user state', function (): void {
$user = User::factory()->create();
$run = OperationRun::factory()->withUser($user)->create();
expect($run->user_id)->toBe((int) $user->getKey())
->and($run->initiator_name)->toBe($user->name);
});
it('keeps tenantless workspace runs available through the explicit tenantless state', function (): void {
$workspace = Workspace::factory()->create();
$run = OperationRun::factory()
->minimal()
->tenantlessForWorkspace($workspace)
->create();
expect($run->tenant_id)->toBeNull()
->and($run->workspace_id)->toBe((int) $workspace->getKey());
});

View File

@ -13,9 +13,16 @@
expect($connection->credential()->exists())->toBeFalse();
});
it('can opt into a heavier provider graph with a checked-in factory state', function (): void {
$connection = ProviderConnection::factory()->withCredential()->create();
it('keeps the explicit standard provider connection state healthy without credential side effects', function (): void {
$connection = ProviderConnection::factory()->standard()->create();
expect($connection->is_default)->toBeTrue()
->and($connection->credential()->exists())->toBeFalse();
});
it('can opt into a heavier provider graph with a checked-in full factory state', function (): void {
$connection = ProviderConnection::factory()->full()->create();
expect($connection->credential()->exists())->toBeTrue()
->and($connection->refresh()->is_default)->toBeTrue();
});
});

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
use App\Models\ProviderCredential;
use App\Support\Providers\ProviderConnectionType;
use App\Support\Providers\ProviderCredentialSource;
use App\Support\Providers\ProviderVerificationStatus;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('keeps the default provider credential path tied to a dedicated connection without extra health side effects', function (): void {
$credential = ProviderCredential::factory()->create();
$connection = $credential->providerConnection()->first();
expect($connection)->not->toBeNull()
->and($connection?->connection_type->value)->toBe(ProviderConnectionType::Dedicated->value)
->and($connection?->is_default)->toBeFalse();
});
it('can opt into a verified dedicated connection graph explicitly', function (): void {
$credential = ProviderCredential::factory()->verifiedConnection()->create();
$connection = $credential->providerConnection()->first();
expect($connection)->not->toBeNull()
->and($connection?->is_default)->toBeTrue()
->and($connection?->verification_status->value)->toBe(ProviderVerificationStatus::Healthy->value)
->and($connection?->credential?->is($credential))->toBeTrue();
});
it('keeps legacy migrated credentials available through an explicit named state', function (): void {
$credential = ProviderCredential::factory()->legacyMigrated()->create();
expect($credential->source->value)->toBe(ProviderCredentialSource::LegacyMigrated->value)
->and($credential->expires_at)->toBeNull();
});

View File

@ -16,5 +16,13 @@
it('keeps the default tenant factory path workspace-ready for existing callers', function (): void {
$tenant = Tenant::factory()->create();
expect($tenant->workspace_id)->not->toBeNull();
});
it('restores default workspace provisioning after an explicit minimal tenant is created', function (): void {
Tenant::factory()->minimal()->create();
$tenant = Tenant::factory()->create();
expect($tenant->workspace_id)->not->toBeNull();
});

View File

@ -13,6 +13,10 @@
uses(RefreshDatabase::class);
beforeEach(function (): void {
Filament::setTenant(null, true);
});
it('keeps the default tenant helper profile cheap by skipping provider setup and cache clears', function (): void {
$capabilities = \Mockery::mock(CapabilityResolver::class);
$workspaceCapabilities = \Mockery::mock(WorkspaceCapabilityResolver::class);
@ -33,7 +37,52 @@
->and(ProviderConnection::query()->where('tenant_id', (int) $tenant->getKey())->exists())->toBeFalse();
});
it('opt-ins provider, credential, ui-context, and cache resets only when the fixture profile asks for them', function (): void {
it('opt-ins a standard provider context only when the canonical standard profile asks for it', function (): void {
$capabilities = \Mockery::mock(CapabilityResolver::class);
$workspaceCapabilities = \Mockery::mock(WorkspaceCapabilityResolver::class);
$capabilities->shouldNotReceive('clearCache');
$workspaceCapabilities->shouldNotReceive('clearCache');
app()->instance(CapabilityResolver::class, $capabilities);
app()->instance(WorkspaceCapabilityResolver::class, $workspaceCapabilities);
[, $tenant] = createStandardUserWithTenant();
$connection = ProviderConnection::query()
->where('tenant_id', (int) $tenant->getKey())
->where('is_default', true)
->first();
expect($connection)->not->toBeNull()
->and($connection?->credential()->exists())->toBeFalse()
->and(Filament::getTenant())->toBeNull();
});
it('keeps the credential-enabled alias explicit without forcing cache or ui side effects', function (): void {
$capabilities = \Mockery::mock(CapabilityResolver::class);
$workspaceCapabilities = \Mockery::mock(WorkspaceCapabilityResolver::class);
$capabilities->shouldNotReceive('clearCache');
$workspaceCapabilities->shouldNotReceive('clearCache');
app()->instance(CapabilityResolver::class, $capabilities);
app()->instance(WorkspaceCapabilityResolver::class, $workspaceCapabilities);
[, $tenant] = createCredentialEnabledUserWithTenant();
$connection = ProviderConnection::query()
->where('tenant_id', (int) $tenant->getKey())
->where('is_default', true)
->first();
expect($connection)->not->toBeNull()
->and($connection?->credential()->exists())->toBeTrue()
->and(ProviderCredential::query()->where('provider_connection_id', (int) $connection?->getKey())->exists())->toBeTrue()
->and(Filament::getTenant())->toBeNull();
});
it('keeps the ui-context alias explicit without provider or credential side effects', function (): void {
$capabilities = \Mockery::mock(CapabilityResolver::class);
$workspaceCapabilities = \Mockery::mock(WorkspaceCapabilityResolver::class);
@ -43,7 +92,23 @@
app()->instance(CapabilityResolver::class, $capabilities);
app()->instance(WorkspaceCapabilityResolver::class, $workspaceCapabilities);
[$user, $tenant] = createUserWithTenant(fixtureProfile: 'heavy');
[, $tenant] = createUiContextUserWithTenant();
expect(ProviderConnection::query()->where('tenant_id', (int) $tenant->getKey())->exists())->toBeFalse()
->and(Filament::getTenant()?->is($tenant))->toBeTrue();
});
it('opt-ins provider, credential, ui-context, and cache resets only when the canonical full profile asks for them', function (): void {
$capabilities = \Mockery::mock(CapabilityResolver::class);
$workspaceCapabilities = \Mockery::mock(WorkspaceCapabilityResolver::class);
$capabilities->shouldReceive('clearCache')->once();
$workspaceCapabilities->shouldReceive('clearCache')->once();
app()->instance(CapabilityResolver::class, $capabilities);
app()->instance(WorkspaceCapabilityResolver::class, $workspaceCapabilities);
[, $tenant] = createFullUserWithTenant();
$connection = ProviderConnection::query()
->where('tenant_id', (int) $tenant->getKey())

View File

@ -9,4 +9,4 @@ LANE="${1:-fast-feedback}"
cd "${APP_DIR}"
exec ./vendor/bin/sail php -r 'require "vendor/autoload.php"; exit(\Tests\Support\TestLaneManifest::renderLatestReport((string) ($argv[1] ?? "")));' "${LANE}"
exec ./vendor/bin/sail php -r 'require "vendor/autoload.php"; exit(\Tests\Support\TestLaneManifest::renderLatestReport((string) ($argv[1] ?? ""), (string) ($argv[2] ?? "shared-test-fixture-slimming")));' "${LANE}" "shared-test-fixture-slimming"

View File

@ -0,0 +1,36 @@
# Specification Quality Checklist: Shared Test Fixture Slimming
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-04-16
**Feature**: [spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- Validation pass completed after removing leftover template content from the initial scaffold.
- No clarification markers or placeholder sections remain in the spec.
- The spec is ready for `/speckit.plan`.

View File

@ -0,0 +1,511 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://tenantatlas.local/schemas/shared-fixture-profile.schema.json",
"title": "SharedFixtureProfileCatalog",
"type": "object",
"additionalProperties": false,
"required": [
"version",
"defaultProfile",
"profiles",
"helperBindings",
"auditedFactories",
"migrationPacks"
],
"properties": {
"version": {
"type": "integer",
"minimum": 1
},
"defaultProfile": {
"type": "string",
"const": "minimal"
},
"profiles": {
"type": "array",
"minItems": 3,
"items": {
"$ref": "#/$defs/profile"
},
"allOf": [
{
"contains": {
"type": "object",
"required": ["id"],
"properties": {
"id": {
"const": "minimal"
}
}
}
},
{
"contains": {
"type": "object",
"required": ["id"],
"properties": {
"id": {
"const": "standard"
}
}
}
},
{
"contains": {
"type": "object",
"required": ["id"],
"properties": {
"id": {
"const": "full"
}
}
}
}
]
},
"helperBindings": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/helperBinding"
}
},
"auditedFactories": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/auditedFactory"
}
},
"migrationPacks": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/migrationPack"
}
}
},
"$defs": {
"profile": {
"type": "object",
"additionalProperties": false,
"required": [
"id",
"costClass",
"notes",
"sideEffects"
],
"properties": {
"id": {
"type": "string",
"enum": [
"minimal",
"standard",
"full"
]
},
"costClass": {
"type": "string",
"enum": [
"minimal",
"standard",
"integration-heavy"
]
},
"legacyAliases": {
"type": "array",
"items": {
"type": "string"
},
"uniqueItems": true
},
"notes": {
"type": "string",
"minLength": 1
},
"sideEffects": {
"type": "object",
"additionalProperties": false,
"required": [
"workspace",
"workspaceMembership",
"tenantMembership",
"session",
"cache",
"providerConnection",
"providerCredential",
"uiContext"
],
"properties": {
"workspace": {
"type": "boolean"
},
"workspaceMembership": {
"type": "boolean"
},
"tenantMembership": {
"type": "boolean"
},
"session": {
"type": "boolean"
},
"cache": {
"type": "boolean"
},
"providerConnection": {
"type": "boolean"
},
"providerCredential": {
"type": "boolean"
},
"uiContext": {
"type": "boolean"
}
}
}
},
"allOf": [
{
"if": {
"properties": {
"id": {
"const": "minimal"
}
}
},
"then": {
"properties": {
"costClass": {
"const": "minimal"
},
"sideEffects": {
"properties": {
"providerConnection": {
"const": false
},
"providerCredential": {
"const": false
},
"cache": {
"const": false
},
"uiContext": {
"const": false
}
}
}
}
}
},
{
"if": {
"properties": {
"id": {
"const": "full"
}
}
},
"then": {
"properties": {
"costClass": {
"const": "integration-heavy"
}
}
}
}
]
},
"helperBinding": {
"type": "object",
"additionalProperties": false,
"required": [
"helperName",
"defaultProfile",
"supportedProfiles",
"transitionStatus"
],
"properties": {
"helperName": {
"type": "string",
"minLength": 1
},
"defaultProfile": {
"type": "string",
"enum": [
"minimal",
"standard",
"full"
]
},
"supportedProfiles": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"enum": [
"minimal",
"standard",
"full"
]
},
"uniqueItems": true
},
"legacyAliases": {
"type": "array",
"items": {
"type": "string"
},
"uniqueItems": true
},
"legacyAliasResolutions": {
"type": "array",
"items": {
"$ref": "#/$defs/legacyAliasResolution"
}
},
"transitionStatus": {
"type": "string",
"enum": [
"current",
"legacy-transition",
"planned-removal"
]
},
"removalTrigger": {
"type": "string",
"minLength": 1
},
"announcesHeavyContext": {
"type": "boolean"
}
},
"allOf": [
{
"if": {
"properties": {
"transitionStatus": {
"enum": [
"legacy-transition",
"planned-removal"
]
}
}
},
"then": {
"required": [
"removalTrigger"
]
}
}
]
},
"legacyAliasResolution": {
"type": "object",
"additionalProperties": false,
"required": [
"alias",
"resolvedProfile",
"removalTrigger"
],
"properties": {
"alias": {
"type": "string",
"minLength": 1
},
"resolvedProfile": {
"type": "string",
"enum": [
"minimal",
"standard",
"full"
]
},
"removalTrigger": {
"type": "string",
"minLength": 1
},
"sideEffectOverrides": {
"type": "object",
"additionalProperties": false,
"properties": {
"workspace": {
"type": "boolean"
},
"workspaceMembership": {
"type": "boolean"
},
"tenantMembership": {
"type": "boolean"
},
"session": {
"type": "boolean"
},
"cache": {
"type": "boolean"
},
"providerConnection": {
"type": "boolean"
},
"providerCredential": {
"type": "boolean"
},
"uiContext": {
"type": "boolean"
}
}
}
}
},
"auditedFactory": {
"type": "object",
"additionalProperties": false,
"required": [
"factoryName",
"defaultCostClass",
"cascadeFindings"
],
"properties": {
"factoryName": {
"type": "string",
"minLength": 1
},
"defaultCostClass": {
"type": "string",
"enum": [
"minimal",
"standard",
"integration-heavy"
]
},
"leanStates": {
"type": "array",
"items": {
"type": "string"
},
"uniqueItems": true
},
"heavyStates": {
"type": "array",
"items": {
"type": "string"
},
"uniqueItems": true
},
"cascadeFindings": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/cascadeFinding"
}
}
}
},
"cascadeFinding": {
"type": "object",
"additionalProperties": false,
"required": [
"source",
"triggerCondition",
"createdObjects",
"disposition"
],
"properties": {
"source": {
"type": "string",
"enum": [
"definition",
"attribute-callback",
"afterCreating",
"model-event",
"support-helper"
]
},
"triggerCondition": {
"type": "string",
"minLength": 1
},
"createdObjects": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
},
"uniqueItems": true
},
"disposition": {
"type": "string",
"enum": [
"remove",
"make-explicit",
"retain-documented",
"follow-up"
]
},
"notes": {
"type": "string"
}
}
},
"migrationPack": {
"type": "object",
"additionalProperties": false,
"required": [
"packId",
"targetLanes",
"selectionBasis",
"callerSelectors",
"targetProfile"
],
"properties": {
"packId": {
"type": "string",
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
},
"targetLanes": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"enum": [
"fast-feedback",
"confidence",
"browser",
"heavy-governance"
]
},
"uniqueItems": true
},
"selectionBasis": {
"type": "string",
"enum": [
"high-usage",
"high-cost",
"shared-surface",
"legacy-dependency"
]
},
"callerSelectors": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
},
"targetProfile": {
"type": "string",
"enum": [
"minimal",
"standard",
"full"
]
},
"legacyFallbackAllowed": {
"type": "boolean"
},
"expectedLaneImpact": {
"type": "string"
}
}
}
}
}

View File

@ -0,0 +1,327 @@
openapi: 3.1.0
info:
title: Shared Test Fixture Slimming Logical Contract
version: 1.0.0
summary: Logical contract for resolving fixture profiles, reading cascade audits, and comparing lane impact.
description: |
This is a logical contract for repository tooling, tests, and planning artifacts.
It does not imply a new runtime HTTP service. It documents the expected semantics
of fixture-profile resolution and lane-impact comparison so helper behavior,
guard tests, and reporting remain consistent during the migration.
x-logical-contract: true
servers:
- url: https://tenantatlas.local/logical
paths:
/shared-fixtures/helpers/{helperName}/contexts:
post:
summary: Resolve the promised side effects of a helper for a selected fixture profile.
operationId: resolveSharedFixtureContext
parameters:
- name: helperName
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/HelperResolutionRequest'
responses:
'200':
description: Logical resolution of the helper profile and its side effects.
content:
application/json:
schema:
$ref: '#/components/schemas/HelperResolution'
/shared-fixtures/audits/factories/{factoryName}:
get:
summary: Read the current cascade-audit result for a touched factory or model seam.
operationId: getFactoryCascadeAudit
parameters:
- name: factoryName
in: path
required: true
schema:
type: string
responses:
'200':
description: Current cascade-audit description for the requested seam.
content:
application/json:
schema:
$ref: '#/components/schemas/FactoryCascadeAudit'
/shared-fixtures/lane-impact/runs:
post:
summary: Compare pre- and post-migration lane measurements for a migration pack.
operationId: compareFixtureLaneImpact
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LaneImpactComparisonRequest'
responses:
'200':
description: Comparison result for the affected lanes against the current Spec 206 budgets.
content:
application/json:
schema:
$ref: '#/components/schemas/LaneImpactComparison'
components:
schemas:
ProfileId:
type: string
enum:
- minimal
- standard
- full
CostClass:
type: string
enum:
- minimal
- standard
- integration-heavy
TransitionStatus:
type: string
enum:
- current
- legacy-transition
- planned-removal
RemovalTrigger:
type: string
minLength: 1
Disposition:
type: string
enum:
- remove
- make-explicit
- retain-documented
- follow-up
LaneId:
type: string
enum:
- fast-feedback
- confidence
- browser
- heavy-governance
SideEffects:
type: object
additionalProperties: false
required:
- workspace
- workspaceMembership
- tenantMembership
- session
- cache
- providerConnection
- providerCredential
- uiContext
properties:
workspace:
type: boolean
workspaceMembership:
type: boolean
tenantMembership:
type: boolean
session:
type: boolean
cache:
type: boolean
providerConnection:
type: boolean
providerCredential:
type: boolean
uiContext:
type: boolean
HelperResolutionRequest:
type: object
additionalProperties: false
required:
- profileId
properties:
profileId:
$ref: '#/components/schemas/ProfileId'
legacyAlias:
type: string
expectedHeavyContext:
type: boolean
HelperResolution:
type: object
additionalProperties: false
required:
- helperName
- defaultProfile
- requestedProfile
- canonicalProfile
- costClass
- sideEffects
- transitionStatus
properties:
helperName:
type: string
defaultProfile:
$ref: '#/components/schemas/ProfileId'
requestedProfile:
$ref: '#/components/schemas/ProfileId'
canonicalProfile:
$ref: '#/components/schemas/ProfileId'
costClass:
$ref: '#/components/schemas/CostClass'
sideEffects:
$ref: '#/components/schemas/SideEffects'
transitionStatus:
$ref: '#/components/schemas/TransitionStatus'
legacyAliasUsed:
type: boolean
legacyAlias:
type: string
removalTrigger:
$ref: '#/components/schemas/RemovalTrigger'
notes:
type: string
allOf:
- if:
properties:
requestedProfile:
const: minimal
then:
properties:
costClass:
const: minimal
sideEffects:
properties:
providerConnection:
const: false
providerCredential:
const: false
cache:
const: false
uiContext:
const: false
FactoryCascadeFinding:
type: object
additionalProperties: false
required:
- source
- triggerCondition
- createdObjects
- disposition
properties:
source:
type: string
enum:
- definition
- attribute-callback
- afterCreating
- model-event
- support-helper
triggerCondition:
type: string
createdObjects:
type: array
minItems: 1
items:
type: string
disposition:
$ref: '#/components/schemas/Disposition'
notes:
type: string
FactoryCascadeAudit:
type: object
additionalProperties: false
required:
- factoryName
- defaultCostClass
- findings
properties:
factoryName:
type: string
defaultCostClass:
$ref: '#/components/schemas/CostClass'
leanStates:
type: array
items:
type: string
heavyStates:
type: array
items:
type: string
findings:
type: array
minItems: 1
items:
$ref: '#/components/schemas/FactoryCascadeFinding'
LaneImpactComparisonRequest:
type: object
additionalProperties: false
required:
- packId
- laneMeasurements
properties:
packId:
type: string
laneMeasurements:
type: array
minItems: 1
items:
$ref: '#/components/schemas/LaneMeasurement'
LaneMeasurement:
type: object
additionalProperties: false
required:
- laneId
- baselineSeconds
- postMigrationSeconds
- budgetThresholdSeconds
properties:
laneId:
$ref: '#/components/schemas/LaneId'
baselineSeconds:
type: number
minimum: 0
postMigrationSeconds:
type: number
minimum: 0
budgetThresholdSeconds:
type: number
minimum: 0
LaneImpactComparison:
type: object
additionalProperties: false
required:
- packId
- results
properties:
packId:
type: string
results:
type: array
minItems: 1
items:
type: object
additionalProperties: false
required:
- laneId
- deltaSeconds
- deltaPercent
- budgetThresholdSeconds
- status
properties:
laneId:
$ref: '#/components/schemas/LaneId'
deltaSeconds:
type: number
deltaPercent:
type: number
budgetThresholdSeconds:
type: number
status:
type: string
enum:
- improved
- stable
- regressed
notes:
type: string

View File

@ -0,0 +1,221 @@
# Data Model: Shared Test Fixture Slimming
This feature introduces no new runtime database tables. The data-model work formalizes repository-level fixture-governance objects that describe how shared helpers, factories, migration packs, and lane measurements should behave. Field names are conceptual and may later map to PHP arrays, guard tests, or documentation rather than to persisted runtime records.
## 1. Fixture Profile
### Purpose
Represents one named level of setup cost and side effects for shared test support.
### Fields
- `profile_id`: stable identifier such as `minimal`, `standard`, or `full`
- `display_name`: contributor-facing name
- `cost_class`: `minimal`, `standard`, or `integration-heavy`
- `creates_workspace`: boolean
- `creates_workspace_membership`: boolean
- `creates_tenant_membership`: boolean
- `sets_session_context`: boolean
- `clears_capability_caches`: boolean
- `creates_provider_connection`: boolean
- `creates_provider_credential`: boolean
- `sets_ui_context`: boolean
- `legacy_aliases`: optional list such as `provider-enabled`, `credential-enabled`, or `ui-context`
- `notes`: short guidance for when the profile is appropriate
### Validation Rules
- The catalog must contain at least `minimal`, `standard`, and `full`.
- Exactly one profile may be the default profile for a given helper binding.
- `minimal` must not create provider, credential, cache-clearing, or UI-context side effects.
- `full` may create integration-heavy side effects, but those side effects must be declared explicitly.
## 2. Helper Entry Point
### Purpose
Represents one shared helper or helper alias that resolves to a fixture profile contract.
### Fields
- `helper_name`: example `createUserWithTenant`
- `default_profile_id`: the profile used when no explicit profile is requested
- `supported_profile_ids`: list of allowed profiles
- `legacy_aliases`: transitional helper names or profile aliases
- `announces_heavy_context`: boolean indicating whether the name itself signals heavy behavior
- `side_effect_contract`: list of side effects promised by this helper when a given profile is selected
- `transition_status`: `current`, `legacy-transition`, or `planned-removal`
### Validation Rules
- Every shared helper must resolve to exactly one default profile.
- Heavy behavior must be reachable through an explicit profile or clearly named helper, not only hidden booleans.
- Legacy aliases must map to a declared profile, remain temporary, and point to a declared removal trigger in the transition-path model.
## 3. Factory Cascade Finding
### Purpose
Represents one audited place where a factory, model hook, or callback creates hidden extra context.
### Fields
- `finding_id`: stable identifier
- `subject_name`: factory or model name such as `TenantFactory` or `Tenant::booted`
- `cascade_source`: `definition`, `attribute-callback`, `afterCreating`, `model-event`, or `support-helper`
- `trigger_condition`: what causes the extra context to appear
- `created_objects`: list of related models or state written as a side effect
- `cost_class`: `standard` or `heavy`
- `disposition`: `remove`, `make-explicit`, `retain-documented`, or `follow-up`
- `notes`: rationale and migration concerns
### Validation Rules
- Every audited high-usage subject must have at least one explicit disposition.
- A finding marked `retain-documented` must explain why the behavior is materially required.
## 4. Factory State Contract
### Purpose
Represents the expected cost posture of a factory default or named state.
### Fields
- `factory_name`
- `state_name`
- `cost_class`: `minimal`, `standard`, or `integration-heavy`
- `default_state`: boolean
- `creates_relationships`: related models or graph expansions
- `forbidden_side_effects`: side effects that must not happen in this state
- `intended_usage`: short explanation of what the state is for
### Validation Rules
- A touched high-usage factory must have a declared default cost class.
- A `minimal` state must not create undeclared relationships or repair missing workspace/provider context behind the scenes.
- An `integration-heavy` state must document the graph it creates.
## 5. Legacy Transition Path
### Purpose
Represents how an old helper or heavy default remains temporarily available during migration.
### Fields
- `transition_id`
- `legacy_entry_point`: helper name, alias, or factory default being preserved temporarily
- `resolved_profile_id`: the explicit profile or state it maps to
- `visibility_mode`: `named-alias`, `warning-comment`, or `guard-tested`
- `removal_trigger`: condition for retiring the transition path
- `notes`
### Validation Rules
- A transition path must remain visibly heavier than the new default behavior.
- A transition path must have a declared removal trigger so it does not become permanent accidental API.
## 6. Caller Migration Pack
### Purpose
Represents one batch of high-usage callers migrated together.
### Fields
- `pack_id`
- `target_lanes`: one or more of `fast-feedback`, `confidence`, `browser`, or `heavy-governance`
- `selection_basis`: `high-usage`, `high-cost`, `shared-surface`, or `legacy-dependency`
- `caller_selectors`: directories, files, or patterns identifying the pack
- `target_profile_id`: preferred fixture profile after migration
- `legacy_fallback_allowed`: boolean
- `expected_lane_impact`: narrative or numeric expectation
### Validation Rules
- The first slice must include at least one migration pack affecting either `fast-feedback` or `confidence`.
- Packs migrated to `minimal` or `standard` must not silently fall back to full context without an explicit reason.
## 7. Fixture Guard Contract
### Purpose
Represents a guard test that protects fixture behavior from regression.
### Fields
- `guard_id`
- `subject_type`: `helper`, `factory`, `builder`, or `lane-measurement`
- `subject_name`
- `asserts_absent_context`: list of context that must not appear
- `asserts_present_context`: list of context that must appear
- `covers_transition_path`: boolean
- `failure_signal`: brief description of what regression should be obvious in CI
### Validation Rules
- Minimal-path guards must assert absence of hidden heavy context.
- Heavy-path guards must assert presence of promised heavy context.
- Transition-path guards must remain explicit about what legacy behavior is being preserved.
## 8. Lane Impact Measurement
### Purpose
Represents before and after measurement for a migration pack using the Spec 206 reporting seams.
### Fields
- `measurement_id`
- `lane_id`: example `fast-feedback` or `confidence`
- `baseline_seconds`: pre-migration wall-clock value
- `post_migration_seconds`: post-migration wall-clock value
- `budget_threshold_seconds`: current Spec 206 budget for the lane
- `status`: `improved`, `stable`, or `regressed`
- `notes`
### Validation Rules
- Standard-lane measurements must reference the existing Spec 206 budgets.
- A lane marked `regressed` must explain whether the regression is temporary, justified, or blocking.
## 9. First-Slice Governance Inventory
### Current Shared Fixture Surface
- `apps/platform/tests/Pest.php` now exposes a canonical `minimal`, `standard`, and `full` profile catalog plus explicit transition helpers for `provider-enabled`, `credential-enabled`, `ui-context`, and `heavy` callers.
- `apps/platform/app/Models/Tenant.php` still contains a test-only boot hook that auto-provisions a workspace when `workspace_id` is null.
- `apps/platform/database/factories/TenantFactory.php` uses `afterCreating()` workspace provisioning by default and only partially suppresses it in `minimal()`.
- `apps/platform/database/factories/OperationRunFactory.php` now keeps the default path userless and requires an explicit `withUser()` opt-in for interactive initiator context.
- `apps/platform/database/factories/ProviderConnectionFactory.php` now exposes explicit `minimal`, `standard`, and `full` provider graph states instead of leaving the cost posture implicit.
- `apps/platform/database/factories/ProviderCredentialFactory.php` now exposes an explicit verified-connection state and keeps legacy-migrated credentials visible through a named state.
- `apps/platform/database/factories/BackupSetFactory.php` now keeps the default path item-free and uses explicit `full`, `staleCompleted()`, and `degradedCompleted()` states for backup-item graphs.
- `apps/platform/tests/Feature/Concerns/BuildsBaselineCompareMatrixFixtures.php` and similar builders create intentionally rich multi-record graphs that should remain explicit heavy helpers.
### Recorded Cascade Dispositions
- `Tenant::booted` workspace auto-provisioning: `make-explicit` via `Tenant::skipTestWorkspaceProvisioning()` and `TenantFactory::minimal()`.
- `TenantFactory::configure()` default workspace provisioning: `retain-documented` for legacy callers, with `minimal()` as the lean escape hatch.
- `OperationRunFactory` implicit initiator user creation: `remove` from the default path, now restored only through `withUser()`.
- `OperationRunFactory` workspace repair when a tenant lacks workspace context: `follow-up`; guarded through lean caller migration rather than silently broadening helper defaults further in this slice.
- `ProviderConnectionFactory` provider credential graph inflation: `make-explicit` through `full()` and `withCredential()`.
- `ProviderCredentialFactory` verified dedicated connection graph: `make-explicit` through `verifiedConnection()`.
- `BackupSetFactory` backup-item graph creation: `retain-documented` on named states only (`full`, `staleCompleted`, `degradedCompleted`).
### First-Slice Migration Packs
- `console-navigation-pack`: `tests/Feature/Console/ReconcileOperationRunsCommandTest.php`, `tests/Feature/Console/ReconcileBackupScheduleOperationRunsCommandTest.php`, and `tests/Feature/Navigation/RelatedNavigationResolverMemoizationTest.php` now use explicit minimal helpers or lean operation-run factory states.
- `rbac-drift-pack`: `tests/Feature/Spec080WorkspaceManagedTenantAdminMigrationTest.php` and `tests/Feature/BaselineDriftEngine/FindingFidelityTest.php` now call `createMinimalUserWithTenant()` directly instead of depending on the generic default helper entry point.
- `heavy-builder-pack`: `BuildsBaselineCompareMatrixFixtures` and `BuildsPortfolioTriageFixtures` now make the chosen minimal actor-setup path explicit while still remaining intentionally graph-heavy builders.
### First-Slice Target Objects
- Canonical fixture-profile vocabulary and helper bindings
- Tenant and workspace provisioning cascade audit
- Lean state contracts for the touched high-usage factories
- Legacy transition paths for heavy historical callers
- High-usage migration packs for fast-feedback and confidence
- Guard contracts for lean, heavy, and legacy-path behavior
- Lane impact measurements against Spec 206 budgets

View File

@ -0,0 +1,178 @@
# Implementation Plan: Shared Test Fixture Slimming
**Branch**: `207-shared-test-fixture-slimming` | **Date**: 2026-04-16 | **Spec**: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/207-shared-test-fixture-slimming/spec.md`
**Input**: Feature specification from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/207-shared-test-fixture-slimming/spec.md`
## Summary
Complete the next stage of suite-cost reduction on top of Spec 206 by formalizing a repository-wide fixture-profile model, keeping minimal setup as the default in shared helpers and high-usage factories, auditing hidden cascades across the test support layer, migrating the highest-usage expensive callers in the fast-feedback and confidence lanes, and validating lane impact through the existing Spec 206 reporting and budget seams instead of introducing a new fixture framework.
## Technical Context
**Language/Version**: PHP 8.4.15
**Primary Dependencies**: Laravel 12, Pest v4, PHPUnit 12, Filament v5, Livewire v4, Laravel Sail
**Storage**: SQLite `:memory:` for the default test environment, isolated PostgreSQL coverage via the existing dedicated suite, and lane-measurement artifacts under the app-root contract path `storage/logs/test-lanes`
**Testing**: Pest feature, unit, browser, architecture, and guard coverage run through Sail-wrapped `artisan test`; shared-fixture regressions should be protected by focused Pest tests plus existing lane-report tooling
**Target Platform**: Laravel monorepo application with the implementation scoped to `apps/platform` test support and factories
**Project Type**: Monorepo with a Laravel platform app and separate Astro website; this feature affects only platform test infrastructure
**Performance Goals**: Lower per-test cost for the dominant shared fixture paths, keep Spec 206 fast-feedback and confidence lanes stable or improved, and achieve at least one measured standard-lane improvement of 10% or more after the migration pack with no affected standard lane regressing by more than 5%
**Constraints**: Sail-first commands only; no new product persistence or runtime routes; no new dependencies; migration must be incremental and backwards-compatible for legacy heavy callers; avoid a broad suite rewrite; keep helper cost visible by naming rather than by hidden booleans alone
**Scale/Scope**: `createUserWithTenant()` is referenced by roughly 607 callers, existing fixture profiles already exist in `apps/platform/tests/Pest.php`, hidden cascade hotspots include `TenantFactory`, `Tenant` model boot hooks, `OperationRunFactory`, `ProviderConnectionFactory`, `BackupSetFactory`, and multi-record builder traits under `apps/platform/tests/Feature/Concerns`
### Filament v5 Implementation Notes
- **Livewire v4.0+ compliance**: Preserved. This feature changes no runtime Filament or Livewire behavior.
- **Provider registration location**: Unchanged. Existing panel providers remain registered in `bootstrap/providers.php`.
- **Global search rule**: No globally searchable resources are added or changed.
- **Destructive actions**: No runtime destructive actions are introduced. Any new tests added by this feature continue to validate existing confirmation and authorization behavior rather than creating new action surfaces.
- **Asset strategy**: No panel or shared asset changes. Existing `filament:assets` deployment behavior remains unchanged.
- **Testing plan**: Add Pest coverage for fixture-profile semantics, hidden-cascade regression, legacy transition behavior, migrated high-usage caller packs, and lane impact against the Spec 206 budgets.
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
- Inventory-first: PASS. No inventory, snapshot, or backup truth is changed.
- Read/write separation: PASS. The feature governs repository test support behavior only and introduces no end-user write path.
- Graph contract path: PASS. No Graph calls or `config/graph_contracts.php` changes are introduced.
- Deterministic capabilities: PASS. No capability registry or runtime authorization behavior changes.
- RBAC-UX, workspace isolation, tenant isolation: PASS. No runtime routes, policies, or cross-plane semantics are changed.
- Run observability and Ops-UX: PASS. Lane measurements reuse Spec 206 filesystem artifacts and do not introduce `OperationRun` behavior.
- Data minimization: PASS. Fixture audits and lane measurements remain repository-local and contain no secrets.
- Proportionality and bloat control: PASS WITH LIMITS. The only new abstraction is a narrow fixture-profile and cascade-audit vocabulary for test support behavior; the plan explicitly rejects a generalized fixture framework.
- TEST-TRUTH-001: PASS. The feature improves test honesty by making hidden setup and heavy defaults more visible and verifiable.
- Filament/UI constitutions: PASS / NOT APPLICABLE. No operator-facing UI, asset, badge, or action-surface behavior is changed.
**Phase 0 Gate Result**: PASS
- The feature stays inside repository test infrastructure, factories, and documentation.
- No new runtime persistence, product routes, panels, or Graph seams are introduced.
- The chosen design narrows and documents existing behavior rather than layering a new meta-framework on top of the test support layer.
## Project Structure
### Documentation (this feature)
```text
specs/207-shared-test-fixture-slimming/
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
│ ├── shared-fixture-profile.schema.json
│ └── shared-test-fixture-slimming.logical.openapi.yaml
└── tasks.md
```
### Source Code (repository root)
```text
apps/
├── platform/
│ ├── app/
│ │ └── Models/
│ │ └── Tenant.php
│ ├── database/
│ │ └── factories/
│ │ ├── BackupSetFactory.php
│ │ ├── OperationRunFactory.php
│ │ ├── ProviderConnectionFactory.php
│ │ ├── RestoreRunFactory.php
│ │ └── TenantFactory.php
│ ├── tests/
│ │ ├── Pest.php
│ │ ├── Feature/
│ │ │ ├── Concerns/
│ │ │ ├── Console/
│ │ │ ├── Guards/
│ │ │ ├── Navigation/
│ │ │ └── Spec080WorkspaceManagedTenantAdminMigrationTest.php
│ │ ├── Support/
│ │ │ ├── TestLaneManifest.php
│ │ │ ├── TestLaneBudget.php
│ │ │ └── TestLaneReport.php
│ │ └── Unit/
│ └── storage/logs/test-lanes/
├── website/
└── ...
scripts/
├── platform-sail
├── platform-test-lane
└── platform-test-report
```
**Structure Decision**: Keep the feature concentrated in `apps/platform/tests/Pest.php`, the dominant cascading factories under `apps/platform/database/factories`, the hidden test-only model hook in `apps/platform/app/Models/Tenant.php`, selected high-usage caller files under `apps/platform/tests/Feature`, and guard or support coverage that validates fixture semantics and lane impact. Planning artifacts stay in the `specs/207-shared-test-fixture-slimming` directory.
## Complexity Tracking
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| None | Not applicable | Not applicable |
## Proportionality Review
- **Current operator problem**: Contributors and reviewers still pay hidden full-context setup cost in ordinary tests, which blunts the value of Spec 206 lane governance.
- **Existing structure is insufficient because**: Shared helpers, factory callbacks, and model boot hooks still create extra workspace, provider, credential, session, and related graph state without always making that cost visible.
- **Narrowest correct implementation**: Extend the existing helper-profile seam, audit the key cascading factories and builder traits, add lean and explicit heavy states, keep a transition path for legacy callers, and reuse the Spec 206 lane-report contract for measurement.
- **Ownership cost created**: The repo must maintain documented profile names, a small set of guard tests, and a cascade-audit vocabulary for touched factories and helpers.
- **Alternative intentionally rejected**: A brand-new generic fixture DSL or registry, because the repo already has concrete seams in `tests/Pest.php` and the current problem is hidden cost, not missing framework flexibility.
- **Release truth**: Current-release repository truth and a direct prerequisite for sharper heavy-suite segmentation in Spec 208.
## Phase 0 — Research (complete)
- Output: [research.md](./research.md)
- Resolved key decisions:
- Reuse the existing `createUserWithTenant()` profile seam in `apps/platform/tests/Pest.php` as the foundation instead of introducing a new fixture framework.
- Normalize the feature around the spec-facing model of `minimal`, `standard`, and `full` or `integration-heavy`, while preserving current explicit variants such as `provider-enabled`, `credential-enabled`, and `ui-context` as named heavy add-ons or temporary transition aliases with declared removal triggers.
- Audit tenant workspace provisioning at both the factory layer and the `Tenant` model boot hook because lean tenant creation is incomplete if only one layer is slimmed.
- Treat `OperationRunFactory`, `ProviderConnectionFactory`, `ProviderCredentialFactory`, and `BackupSetFactory` as first-class cascade-audit targets because they still create extra workspace or relationship context by default.
- Keep multi-record concern builders such as `BuildsBaselineCompareMatrixFixtures` explicit and heavy by intent; expose their cost rather than forcing them into the minimal path.
- Reuse Spec 206 lane artifacts and budgets in `storage/logs/test-lanes` for before and after measurement rather than creating a separate performance harness.
- Prefer a backwards-compatible transition path with explicit legacy aliases, removal triggers, and guard coverage over a mass rewrite of all callers.
- Protect the rollout with fixture guard tests that assert both absence of hidden heavy context on minimal paths and presence of promised context on explicit heavy paths.
## Phase 1 — Design & Contracts (complete)
- Output: [data-model.md](./data-model.md) formalizes fixture profiles, helper entry points, factory cascade findings, transition paths, migration packs, guard contracts, and lane-impact measurements.
- Output: [contracts/shared-fixture-profile.schema.json](./contracts/shared-fixture-profile.schema.json) defines the logical contract for a checked-in fixture profile catalog, helper bindings, audited factories, and migration packs.
- Output: [contracts/shared-test-fixture-slimming.logical.openapi.yaml](./contracts/shared-test-fixture-slimming.logical.openapi.yaml) captures the logical contract for resolving helper profiles, reading cascade audits, and comparing lane impact without implying a new runtime service.
- Output: [quickstart.md](./quickstart.md) provides the planned implementation order, focused validation commands, and rollout checkpoints.
### Post-design Constitution Re-check
- PASS: No runtime routes, panels, authorization planes, or Graph seams are introduced.
- PASS: The fixture-profile vocabulary is directly justified by current suite cost and does not become a generalized platform abstraction.
- PASS: The design favors extending concrete existing seams in `tests/Pest.php` and the touched factories over adding new registries or a new DSL.
- PASS WITH WORK: Legacy aliases must remain visible, temporary, and tied to explicit removal triggers so the transition layer does not become a permanent second system.
- PASS WITH WORK: Lane validation must use the existing Spec 206 budgets and reporting outputs so performance claims remain evidence-backed.
## Phase 2 — Implementation Planning
`tasks.md` should cover:
- Inventorying the current shared helper surface, high-usage caller patterns, and hidden cascade hotspots touched by this rollout.
- Capturing the pre-migration fast-feedback and confidence baseline with the existing Spec 206 wrappers before fixture edits begin.
- Inventorying and classifying the current shared helper surface, high-usage caller patterns, and hidden cascade hotspots touched by this rollout.
- Finalizing the canonical fixture-profile vocabulary of `minimal`, `standard`, and `full` or `integration-heavy`, including how existing explicit variants map into that model and how temporary aliases retire.
- Refining `createUserWithTenant()` and `createMinimalUserWithTenant()` so the default semantics remain lean and the heavier variants announce themselves clearly.
- Auditing and slimming `TenantFactory`, the `Tenant` model test-only workspace provisioning hook, `OperationRunFactory`, `ProviderConnectionFactory`, `ProviderCredentialFactory`, and `BackupSetFactory`, plus any direct shared-helper user-side effect that proves equally costly during implementation.
- Designing explicit lean and heavy states for the touched factories instead of leaving workspace or provider graph inflation hidden in callbacks or attribute resolvers.
- Adding a visible transition path for legacy heavy callers so existing tests can migrate incrementally without obscuring cost, while keeping every temporary alias on a declared retirement path.
- Migrating the first high-usage caller packs in the fast-feedback and confidence lanes, prioritizing console, authorization, navigation, and narrow finding or baseline tests that do not need provider-heavy setup.
- Reviewing heavy-by-intent concern builders and making their cost explicit through naming or parameterization rather than forcing them into default minimal semantics.
- Adding guard coverage for fixture-profile semantics, hidden cascade regression, and stable legacy-path behavior.
- Measuring post-migration lane impact with the existing Spec 206 wrappers and report artifacts, then documenting the comparison against the recorded pre-migration baseline and current budgets.
- Publishing concise contributor guidance for choosing the cheapest valid fixture path and recognizing when heavy integration context is being requested.
### Contract Implementation Note
- The fixture-profile schema is schema-first and intentionally repo-tooling-oriented. It defines what a checked-in fixture catalog must express even if the first implementation remains PHP arrays and guard tests rather than a new parser.
- The OpenAPI file is logical rather than transport-prescriptive. It documents the expected semantics of helper-profile resolution, cascade-audit inspection, and lane-impact comparison for commands, tests, or wrappers that remain in-process repository tooling.
- The plan intentionally avoids introducing a new runtime service, new database table, or new filesystem artifact family outside the existing Spec 206 lane-report directory.
### Deployment Sequencing Note
- No database migration is planned.
- No asset publish step changes.
- The rollout should start with audited helper and factory seams, then move to high-usage caller migration, then stabilize guard coverage, and finally validate lane impact against the existing Spec 206 budgets.

View File

@ -0,0 +1,104 @@
# Quickstart: Shared Test Fixture Slimming
## Goal
Reduce per-test cost in the shared support layer without reducing meaningful coverage by making minimal fixture behavior the default, moving heavy provider or membership or workspace behavior behind explicit opt-in paths, migrating the highest-usage expensive callers, and proving the result against the existing Spec 206 lane budgets.
## Implementation Order
1. Capture the pre-migration fast-feedback and confidence lane baseline with the existing Spec 206 wrappers before helper or factory edits begin.
2. Inventory and classify the existing helper and factory seams that still hide extra workspace, provider, credential, session, user-relationship, or graph state.
3. Finalize the fixture-profile vocabulary of `minimal`, `standard`, and `full` or `integration-heavy`, including mapping rules and retirement triggers for current explicit variants.
4. Audit and slim tenant workspace provisioning across both `TenantFactory` and the `Tenant` model boot hook.
5. Audit and slim the next high-cost factories, starting with `OperationRunFactory`, `ProviderConnectionFactory`, `ProviderCredentialFactory`, and `BackupSetFactory`.
6. Make heavy-by-intent builders and helpers announce their cost through naming or explicit parameters.
7. Add guard coverage that asserts lean paths stay lean and heavy paths create only their promised extra context.
8. Migrate the first high-usage caller packs in fast-feedback and confidence.
9. Measure post-migration lane impact against the recorded baseline with the existing Spec 206 lane-report tooling. `./scripts/platform-test-report` should emit the `sharedFixtureSlimmingComparison` block for `fast-feedback` and `confidence`.
10. Publish concise contributor guidance for choosing the cheapest valid fixture path.
## Suggested Code Touches
```text
apps/platform/app/Models/Tenant.php
apps/platform/database/factories/TenantFactory.php
apps/platform/database/factories/OperationRunFactory.php
apps/platform/database/factories/ProviderConnectionFactory.php
apps/platform/database/factories/BackupSetFactory.php
apps/platform/database/factories/RestoreRunFactory.php
apps/platform/tests/Pest.php
apps/platform/tests/Feature/Concerns/*
apps/platform/tests/Feature/Console/*
apps/platform/tests/Feature/Guards/*
apps/platform/tests/Feature/Navigation/*
apps/platform/tests/Feature/Spec080WorkspaceManagedTenantAdminMigrationTest.php
apps/platform/tests/Support/*
scripts/platform-test-lane
scripts/platform-test-report
```
## Validation Flow
Use focused suites while slimming one seam at a time, then validate the standard lanes against the Spec 206 baseline:
```bash
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Console
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Navigation
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Spec080WorkspaceManagedTenantAdminMigrationTest.php
./scripts/platform-test-lane fast-feedback
./scripts/platform-test-lane confidence
./scripts/platform-test-report fast-feedback
./scripts/platform-test-report confidence
cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent
```
If a touched factory or helper affects an intentionally heavy builder, validate that specific surface before measuring the lanes.
## Recorded Pre-Migration Baseline Capture
The current pre-migration snapshots for this spec were captured on `2026-04-16T13:11:57+00:00` and are the baseline that the lane-impact guard and report comparison use:
| Lane | Baseline | Budget | Target |
|------|----------|--------|--------|
| `fast-feedback` | `176.73623s` | `200s` | improve by `>=10%` or stay within `+5%` |
| `confidence` | `394.383441s` | `450s` | improve by `>=10%` or stay within `+5%` |
## Baseline References
Use the existing Spec 206 recorded measurements as the starting point for lane impact:
- `fast-feedback`: `176.74s` budget `200s`
- `confidence`: `394.38s` budget `450s`
- `heavy-governance`: `83.66s` budget `120s`
- `browser`: `128.87s` budget `150s`
The first success target for this feature is not to redesign the lane model. It is to improve per-test cost enough that at least one affected standard lane gets measurably faster while the others remain stable.
Record fresh pre-migration fast-feedback and confidence snapshots on the feature branch before fixture changes land so the eventual comparison is not inferred only from historical Spec 206 documentation.
## Legacy Alias Retirement
- `provider-enabled` retires after the first fast-feedback and confidence migration packs call `standard` directly.
- `credential-enabled` retires after credential-dependent callers move to `full` plus explicit local overrides where needed.
- `ui-context` retires after UI-context callers use the canonical `full` profile or an equally explicit local helper.
- `heavy` retires after the remaining legacy callers use `full` directly.
## Manual Review Checklist
1. Confirm the default helper path stays on `minimal` semantics and does not silently create provider or credential or UI context.
2. Confirm touched factories declare lean versus heavy behavior explicitly instead of hiding it in callbacks or attribute resolvers.
3. Confirm the tenant workspace cascade is controlled consistently across both factory and model layers.
4. Confirm legacy heavy paths remain visible, temporary during migration, and tied to explicit removal triggers.
5. Confirm the first migrated caller packs match their real business need and do not fall back to full context without explanation.
6. Confirm guard coverage asserts both absence and presence of context where appropriate.
7. Confirm both pre- and post-migration lane measurements are taken through the existing Spec 206 wrappers and artifacts under `storage/logs/test-lanes`.
## Exit Criteria
1. The shared support layer has a documented fixture-profile model with `minimal`, `standard`, and `full` or `integration-heavy`.
2. Touched high-usage helpers and factories no longer create hidden heavy context by default.
3. Legacy heavy behavior is available through an explicit and visibly temporary transition path with a declared removal trigger.
4. The first high-usage caller packs in fast-feedback and confidence have been migrated.
5. Guard coverage protects lean paths, heavy paths, and the transition path.
6. Post-migration lane measurements show at least one affected standard lane improved and no affected standard lane materially regressed against the Spec 206 budgets.

View File

@ -0,0 +1,73 @@
# Research: Shared Test Fixture Slimming
## Decision 1: Reuse the existing `createUserWithTenant()` seam instead of introducing a new fixture framework
- Decision: The rollout should build on the existing helper surface in `apps/platform/tests/Pest.php`, with `createUserWithTenant()` and `createMinimalUserWithTenant()` remaining the primary entry points for shared tenant-user setup.
- Rationale: The repo already has named fixture profiles and a dedicated helper used by roughly 607 callers. The problem is hidden cost and incomplete slimming, not the absence of a fixture abstraction.
- Alternatives considered:
- Create a new generic fixture registry or DSL: rejected because it would add abstraction cost without solving the immediate hidden-side-effect problem better than the existing seam.
- Push all setup inline into each test: rejected because it would duplicate setup logic and weaken shared guard coverage.
## Decision 2: Normalize the profile model around `minimal`, `standard`, and `full` or `integration-heavy`
- Decision: The plan should use the spec-facing profile vocabulary of `minimal`, `standard`, and `full` or `integration-heavy`, while mapping current explicit variants such as `provider-enabled`, `credential-enabled`, and `ui-context` as named heavy opt-ins or temporary transition aliases with declared removal triggers.
- Rationale: The specification requires a clear three-level model for authors and reviewers. The current profile set is useful, but it is helper-centric and does not yet present a consistent suite-wide vocabulary.
- Alternatives considered:
- Keep only the current helper-specific profile names: rejected because they do not express a unified fixture discipline across helpers and factories.
- Collapse everything into `minimal` and `heavy`: rejected because a bounded middle path is useful for common setup that is more than bare minimum but still not full integration context.
## Decision 3: Audit tenant workspace provisioning at both the factory and model-event layers
- Decision: The tenant cascade audit must treat `TenantFactory` and the `Tenant` model boot hook as one coupled hotspot, and lean tenant creation must disable both layers when the test requests minimal behavior.
- Rationale: `TenantFactory::minimal()` already tries to suppress workspace creation, but the `Tenant` model still has test-only workspace provisioning logic in `booted()`. Slimming only one layer leaves hidden graph inflation in place.
- Alternatives considered:
- Slim only the factory: rejected because the model event can still recreate the hidden workspace relationship.
- Remove all test-only workspace provisioning immediately without transition handling: rejected because some callers still rely on the historical behavior and need an explicit heavy path.
## Decision 4: Treat `OperationRunFactory`, `ProviderConnectionFactory`, and `BackupSetFactory` as first-class cascade-audit targets
- Decision: The first factory audit pass should cover `OperationRunFactory`, `ProviderConnectionFactory`, and `BackupSetFactory` in addition to `TenantFactory`.
- Rationale: These factories still create or repair extra workspace, tenant, credential, or backup-item context through attribute callbacks or `afterCreating()` hooks, and they appear in common test-support flows.
- Alternatives considered:
- Restrict the audit to `TenantFactory` only: rejected because several heavy test paths start from factories that hide cost even after tenant setup is slimmed.
- Attempt to audit every factory in one pass: rejected because the first slice should focus on high-usage or high-cost factories that materially affect the standard lanes.
## Decision 5: Keep concern builders explicit and heavy by intent
- Decision: Multi-record builders such as `BuildsBaselineCompareMatrixFixtures` should remain explicit heavy helpers, but their cost should be surfaced through naming, parameters, or guard documentation rather than being treated like ordinary default fixtures.
- Rationale: These builders exist to create deliberately rich graphs for matrix, portfolio, or governance scenarios. The correct fix is to announce cost, not to pretend they are minimal.
- Alternatives considered:
- Force concern builders into the minimal profile model: rejected because it would blur the difference between narrow test setup and intentionally broad scenario construction.
- Leave them undocumented: rejected because hidden heavy setup in support traits still harms review clarity.
## Decision 6: Reuse Spec 206 lane tooling for before and after measurement
- Decision: Lane impact should be measured through the existing `TestLaneManifest`, `TestLaneBudget`, `TestLaneReport`, `scripts/platform-test-lane`, and `scripts/platform-test-report` seams introduced by Spec 206.
- Rationale: The repository already has budgeted fast-feedback and confidence lanes plus artifact output under `storage/logs/test-lanes`. Creating a second performance harness would duplicate governance and weaken comparability.
- Alternatives considered:
- Measure ad hoc with one-off local scripts: rejected because it would not create stable, reviewable evidence.
- Introduce a dedicated new profiling subsystem: rejected because the repo already has lane reporting and budget evaluation contracts.
## Decision 7: Use a backwards-compatible transition layer rather than a hard cutover
- Decision: The rollout should keep a visible transition path for legacy heavy callers, mark aliases as temporary, and retire them once the first migration packs and guard coverage are complete rather than forcing a suite-wide cutover.
- Rationale: Existing tests may depend on historical side effects. A staged migration gives the suite time to expose hidden dependencies while keeping cost visible instead of silently restoring old defaults or preserving a permanent second vocabulary.
- Alternatives considered:
- Rewrite all callers in one pass: rejected because it creates too much review noise and raises breakage risk.
- Freeze legacy behavior permanently: rejected because it would blunt the cost reduction that the spec exists to achieve.
## Decision 8: Guard coverage must assert both absence and presence of context
- Decision: Fixture guard tests should verify that minimal paths do not create hidden provider, credential, workspace, session, or cache side effects, and that explicit heavy paths create only the promised additional context.
- Rationale: Slimming work regresses easily when helpers or factories gain new callbacks. Both negative and positive assertions are needed to keep the support layer honest.
- Alternatives considered:
- Test only the lean path: rejected because heavy profiles also need a stable contract.
- Rely only on wall-clock improvements: rejected because runtime improvements alone do not prove that the correct context is being created.
## Decision 9: Migrate high-usage caller packs by business need, not by file ownership
- Decision: The first migration packs should target high-usage callers in console, authorization, navigation, and narrow findings or baseline tests where provider-heavy or full integration context is usually unnecessary.
- Rationale: These packs sit closest to the fast-feedback and confidence lanes and provide early lane impact without forcing speculative rewrites of already heavy-by-intent suites.
- Alternatives considered:
- Start with the most complex multi-tenant builders: rejected because their business need often justifies heavy context and they are a weaker first proof of lane impact.
- Pick callers only by directory ownership: rejected because fixture cost and lane importance do not align neatly with team or folder boundaries.

View File

@ -0,0 +1,318 @@
# Feature Specification: Shared Test Fixture Slimming
**Feature Branch**: `207-shared-test-fixture-slimming`
**Created**: 2026-04-16
**Status**: Draft
**Input**: User description: "Spec 207 - Shared Test Fixture Slimming"
## Spec Candidate Check *(mandatory — SPEC-GATE-001)*
- **Problem**: Shared test fixtures and factory defaults regularly create more tenant, user, workspace, membership, provider, session, and capability context than many tests actually need.
- **Today's failure**: Short, ordinary tests silently pay full-context setup cost, hidden helper side effects make tests harder to read, and lane improvements from Spec 206 are limited because per-test cost stays inflated.
- **User-visible improvement**: Contributors get a cheaper standard test setup, clearer fixture intent at the call site, and a more stable fast-feedback or confidence experience without reducing legitimate coverage.
- **Smallest enterprise-capable version**: Define a small fixture-profile model, make minimal setup the default, move heavy provider or membership or workspace behavior behind explicit opt-in paths, slim the most-used factories and helpers, migrate the highest-usage expensive callers, and add guard coverage plus before and after runtime comparison.
- **Explicit non-goals**: No full-suite reorganization, no blanket rewrite of every existing test, no removal of valid integration-heavy coverage that still reflects real business risk, no browser-strategy redesign, and no CI rollout redesign.
- **Permanent complexity imported**: Fixture-profile vocabulary, lean versus heavy helper entry points, lighter factory states, a transition layer for legacy callers, guard coverage, and short author guidance.
- **Why now**: Spec 206 already made suite cost visible. Without reducing per-test cost now, standard lanes will keep inheriting expensive defaults and later lane segmentation work will remain more expensive than necessary.
- **Why not local**: Local cleanups in one or two tests cannot stop the support layer from reintroducing hidden full-context setup across the suite. The default behavior must change at the shared-fixture level.
- **Approval class**: Cleanup
- **Red flags triggered**: "Foundation" language and a new profile vocabulary. Defense: this feature narrows and clarifies existing fixture behavior instead of adding new product runtime truth, new operator surfaces, or speculative platform abstractions.
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexität: 1 | Produktnähe: 1 | Wiederverwendung: 2 | **Gesamt: 10/12**
- **Decision**: approve
## Spec Scope Fields *(mandatory)*
- **Scope**: workspace
- **Primary Routes**: No end-user HTTP routes change. The affected surfaces are repository-level test fixtures, shared setup paths, factory defaults, contributor guidance, and runtime comparison outputs.
- **Data Ownership**: Workspace-owned test-support conventions, fixture profiles, migration guidance, guard coverage, and runtime comparison evidence. No tenant-owned product records are added or changed.
- **RBAC**: No end-user authorization behavior changes. The affected actors are repository contributors and reviewers who need a cheaper, clearer default test path.
## Proportionality Review *(mandatory when structural complexity is introduced)*
- **New source of truth?**: no
- **New persisted entity/table/artifact?**: no new product persistence; only repository documentation, test support behavior, and runtime comparison evidence
- **New abstraction?**: yes, but limited to a small fixture-profile vocabulary for the shared test support layer
- **New enum/state/reason family?**: no
- **New cross-domain UI framework/taxonomy?**: no
- **Current operator problem**: Contributors and reviewers cannot rely on the shared test support layer to stay cheap by default, so ordinary tests inherit hidden setup cost and the standard lanes remain slower than they need to be.
- **Existing structure is insufficient because**: shared helpers and factory defaults currently behave like opt-out full-context builders, which hides cost and makes later lane governance less effective.
- **Narrowest correct implementation**: keep the change inside the test support layer, a small fixture-profile model, the most-used factories and helpers, a limited caller migration pack, and guard coverage.
- **Ownership cost**: The team must maintain profile naming, transition guidance, guard tests, and a small amount of runtime comparison evidence as fixture behavior evolves.
- **Alternative intentionally rejected**: isolated caller-by-caller cleanup without changing shared defaults, because it would leave the hidden cost model intact and allow regressions to re-enter through new tests.
- **Release truth**: current-release repository truth that reduces existing suite cost and prepares the next heavy-suite segmentation step.
## Problem Statement
TenantPilot's test suite is expensive not only because it is large, but because many shared fixtures act like full-context convenience builders by default.
The current pain is structural:
- Shared helpers often create broad tenant, user, workspace, membership, provider, session, or capability context even when the test only needs a narrow slice.
- Expensive context is commonly opt-out instead of opt-in.
- Factory defaults can trigger hidden cascades that create relationship graphs a test author did not clearly ask for.
- The real setup cost of a short test is hard to see during review.
- Standard lanes pay for integration-heavy defaults even when the business assertion is narrow.
If these defaults stay in place, the fast-feedback and confidence lanes introduced by Spec 206 will keep absorbing avoidable per-test cost as the suite grows.
## Dependencies
- Depends on Spec 206 - Test Suite Governance & Performance Foundation for the baseline, lane vocabulary, and runtime comparison discipline.
- Recommended before Spec 208 - Filament or Livewire Heavy Suite Segmentation so per-test cost is reduced before the heaviest families are moved into sharper lane boundaries.
- Does not block ongoing feature delivery as long as new tests already follow the minimal-fixture discipline introduced here.
## Goals
- Reduce per-test cost without reducing meaningful coverage.
- Make minimal fixtures the standard path for ordinary tests.
- Require explicit opt-in for provider, credential, membership, workspace, session, capability, and other integration-heavy setup.
- Reduce hidden helper and factory side effects.
- Make test intent easier to read because required context is visible at the call site.
- Prevent new tests from drifting back into expensive full-context defaults.
## Non-Goals
- Reorganizing the full suite or redefining all lane boundaries.
- Rewriting every existing test in one pass.
- Removing valid provider, membership, workspace, or panel-adjacent tests that still need full context.
- Redesigning CI behavior or browser strategy.
- Refactoring unrelated application architecture outside the test support layer.
## Assumptions
- Spec 206 baseline reporting is available or can be regenerated before lane impact is evaluated.
- Existing business-relevant coverage remains valuable; the goal is to lower setup cost, not to reduce confidence.
- Migration will be incremental, with a temporary transition layer where legacy callers still need heavier behavior.
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Use A Minimal Fixture By Default (Priority: P1)
As a contributor adding or editing an ordinary test, I want the default shared setup path to create only the smallest valid context for the behavior under test so the test stays cheap and easy to understand.
**Why this priority**: This is the highest-frequency authoring path in the repository. If it stays expensive by default, every new test can quietly push lane cost upward.
**Independent Test**: Use the default shared fixture path and default factory states for a representative narrow test, then verify that no provider, credential, membership, workspace, session, capability, or other heavy add-ons appear unless the test asked for them.
**Acceptance Scenarios**:
1. **Given** a contributor uses the default shared fixture path for a narrow test, **When** the test asks only for core context, **Then** only the smallest valid context is created and heavy add-ons are absent.
2. **Given** a contributor uses a default factory state, **When** no heavy state is requested, **Then** extra relationship graphs are not created implicitly.
---
### User Story 2 - Opt Into Heavy Context Deliberately (Priority: P1)
As a contributor working on behavior that truly depends on provider, membership, workspace, session, or workflow context, I want an explicit way to request that heavier setup so the test gets the right environment without hiding its cost.
**Why this priority**: Some tests genuinely need heavy context. The feature must preserve those cases while making the cost visible and intentional.
**Independent Test**: Select an explicit heavy profile or add-on state for a representative integration-heavy test and verify that the promised additional context is present while remaining clearly identifiable at the call site.
**Acceptance Scenarios**:
1. **Given** a test requires heavy integration context, **When** the author selects an explicit heavy profile or add-on state, **Then** the required extra context is created.
2. **Given** a reviewer inspects a heavy test setup, **When** the test asks for the heavier path, **Then** the setup clearly announces that it is requesting more than the minimal default.
---
### User Story 3 - Migrate High-Usage Callers Safely (Priority: P2)
As a maintainer reducing suite cost, I want the most frequently used expensive setup paths migrated to lighter defaults without causing unnecessary mass breakage.
**Why this priority**: Shared defaults only pay off when the most-used callers move to the cheaper profiles that match their real needs.
**Independent Test**: Migrate a representative pack of high-usage callers, run their tests, and verify that narrow tests move to lighter profiles while legacy full-context callers remain available through the transition path until they are intentionally migrated.
**Acceptance Scenarios**:
1. **Given** a high-usage caller only needs narrow context, **When** it is migrated, **Then** it uses a lighter profile and keeps its business assertions green.
2. **Given** a legacy caller still depends on full context, **When** the transition path is used, **Then** the behavior remains available during migration and its heavier cost stays visible.
---
### User Story 4 - Catch Fixture Cost Regressions Early (Priority: P2)
As a reviewer or maintainer, I want guard coverage and visible cost signals around shared fixtures so regressions in hidden setup cost are caught before they spread through the standard lanes.
**Why this priority**: Without early signals, expensive defaults quietly return and erase the benefit of the migration work.
**Independent Test**: Run guard coverage and runtime comparison after a shared-fixture change and confirm that unexpected extra context or lane regressions are detected.
**Acceptance Scenarios**:
1. **Given** a shared helper or factory begins creating unexpected extra context, **When** guard coverage runs, **Then** the regression fails clearly.
2. **Given** the main migration pack is complete, **When** runtime comparison is reviewed against the Spec 206 baseline, **Then** the affected standard lanes are stable or improved.
### Edge Cases
- A legacy test depended on hidden session or capability mutation from a generic helper; the migration must expose that dependency rather than silently recreating it in the minimal path.
- A test needs one expensive add-on, such as provider context, without needing the rest of the full integration graph.
- A factory callback or model default quietly recreates a removed relationship graph even after a lean state is selected.
- A high-usage caller pack spans more than one fixture profile during the transition period and must remain readable rather than falling back to generic magic.
## Requirements *(mandatory)*
**Constitution alignment (required):** This feature changes no end-user routes, no Microsoft Graph behavior, no queued operations, and no authorization planes. It does change repository-wide test support behavior, so the fixture-profile model, transition path, and guard coverage must remain explicit, reviewable, and measurable.
**Constitution alignment (PROP-001 / ABSTR-001 / PERSIST-001 / STATE-001 / BLOAT-001):** The only intentional new abstraction is a narrow fixture-profile model for the shared test support layer. It is justified because existing defaults hide cost and make lane governance ineffective. The implementation must prefer explicit, local behavior over a broad new framework and must not create new product persistence or speculative semantic layers.
### Functional Requirements
- **FR-001 Fixture Profile Model**: The shared test support layer MUST define at least three clearly named fixture profiles: minimal, standard, and full or integration-heavy. Each profile MUST have a documented purpose and clearly bounded cost expectations.
- **FR-002 Minimal Default**: Shared fixtures MUST use the minimal profile as the default path for ordinary tests.
- **FR-003 Explicit Heavy Opt-In**: Provider, credential, membership, workspace, session, capability, and other integration-heavy setup MUST be created only when a test explicitly requests it.
- **FR-004 Shared Helper Split**: Central shared helpers MUST be split into clearly distinguishable light and heavy paths, whether by separate entry points or explicit profile selection, so the cost difference is visible to authors and reviewers.
- **FR-005 Expensive Defaults Removed**: Automatic provider connections, provider credentials, membership enrichment, workspace enrichment, session or capability state, and extra user or tenant relationships MUST no longer be silent defaults in the shared fixture path.
- **FR-006 Factory State Discipline**: The highest-usage test factories MUST provide lean defaults and clearly named add-on states for provider, membership, workspace, and operation-context needs. Defaults MUST avoid unnecessary cascades.
- **FR-007 Hidden Cascade Audit**: The most important test aggregates, including tenant, workspace, user, provider connection, provider credential, operation context, and other high-usage shared setup inputs, MUST be audited for callbacks, defaults, or repair behavior that silently create extra objects or state.
- **FR-008 Transition Layer**: A backwards-compatible transition path MUST exist so existing tests do not break unnecessarily while migration is in progress, heavier legacy behavior MUST remain clearly identifiable, and every legacy alias or full-context fallback MUST carry a declared removal trigger tied to migration-pack completion.
- **FR-009 Caller Migration Pack**: The most frequently used expensive callers in the fast-feedback and confidence lanes MUST be migrated to the lightest profile that still matches their business needs.
- **FR-010 Cost Visibility**: The test support layer MUST make heavy setup visible through naming, guidance, or guard signals so contributors can tell when they are requesting more than the minimal path.
- **FR-011 Guard Coverage**: Guard tests MUST verify that minimal profiles do not create hidden heavy context, heavy profiles create only their promised extra context, and fixture regression is detected early.
- **FR-012 Coverage Preservation**: The rollout MUST preserve legitimate provider, membership, workspace, and other integration-heavy coverage where that context is materially required.
- **FR-013 Budget Validation**: After the main migration pack, the affected standard lanes MUST be compared against the Spec 206 baseline and show stable or improved runtime.
- **FR-014 Author Guidance**: Contributors MUST have concise guidance explaining when to use minimal, standard, and full fixture paths and how to avoid reintroducing hidden setup cost.
### Non-Functional Requirements
- **NFR-001 Cheap-Default Bias**: The cheapest valid setup path must be easier to reach than the heavy path.
- **NFR-002 Readable Intent**: Test setup must communicate required context more clearly than the current convenience-heavy defaults.
- **NFR-003 Incremental Adoption**: Migration must be possible in small slices without forcing a full-suite rewrite.
- **NFR-004 Lane Benefit Preservation**: Changes must reinforce, not undermine, the fast-feedback and confidence lanes defined in Spec 206.
## Work Packages
### Work Package A - Fixture Surface Inventory
- Catalogue the central shared helpers that are most frequently used in ordinary tests.
- Identify the highest-usage expensive caller paths.
- Record which hidden side effects currently inflate context or cost.
- Classify each hidden-cascade finding as removed, made explicit, retained-documented, or follow-up before caller migration begins.
- Distinguish where narrow context is enough versus where true full integration context is required.
### Work Package B - Profile Design
- Finalize the minimal, standard, and full or integration-heavy profile model.
- Make the light versus heavy API boundary obvious to authors and reviewers.
- Move hidden helper semantics into explicit profile choices or clearly separate entry points.
- Ensure heavy paths announce themselves rather than hiding behind generic convenience names.
### Work Package C - Factory Slimming
- Audit the most important factory defaults for hidden cascades.
- Introduce lean defaults and explicit heavy add-on states.
- Remove or isolate automatic graph inflation that is not required by most tests.
- Keep any unavoidable heavy behavior explicit and documented.
### Work Package D - Caller Migration
- Migrate the most-used expensive callers to lighter profiles first.
- Retain full-context setup only where the business assertion genuinely needs it.
- Prioritize callers exercised by the fast-feedback and confidence lanes.
- Use the transition layer to avoid unnecessary broad breakage during migration.
### Work Package E - Regression Pack
- Add guard coverage for minimal, standard, and full fixture semantics.
- Protect against reintroduction of hidden heavy defaults.
- Keep a small regression pack for legacy transition behavior until migration is complete.
- Validate lane impact against the Spec 206 budgets after the main migration slice.
## Deliverables
- Slimmed shared test fixtures with a documented profile model.
- Leaner default states for the most-used factories and explicit heavy add-on states.
- A hidden-cascade audit for the key shared setup inputs with a recorded disposition for each named finding.
- A transition path for legacy callers.
- Guard coverage for fixture behavior and regression detection.
- Migration of the highest-usage expensive callers.
- Concise author guidance for minimal versus full fixture setup.
- A before and after runtime comparison against the Spec 206 baseline.
## Risks
### Hidden Dependency Breakage
Existing tests may rely on side effects that are currently invisible. Slimming defaults can expose those dependencies quickly.
### Over-Slimming
If the minimal path removes context that is still meaningfully required, tests can become harder to write or understand.
### API Proliferation
Too many helper variants could make the support layer harder to navigate than the current defaults.
### Incomplete Migration
If shared defaults change but the highest-usage callers remain on legacy behavior, the lane-level impact will stay limited.
## Rollout Guidance
- Start with the most-used and most expensive shared setup paths rather than trying to slim everything at once.
- Capture a pre-migration fast-feedback and confidence baseline through the existing Spec 206 wrappers before helper or factory edits begin.
- Finalize the profile model before migrating callers so the new naming is stable.
- Remove silent heavy defaults before expanding the migration pack.
- Use the transition layer to keep the rollout incremental and reviewable, but attach a removal trigger to every temporary alias or legacy full-context path.
- Measure lane impact after the main migration slice rather than waiting for total suite completion.
## Design Rules
- **Minimal first**: The cheapest valid fixture is the default.
- **No surprise provider setup**: Provider or credential context is never created implicitly.
- **No surprise session or capability mutation**: Session or capability state changes require explicit intent.
- **No hidden graph inflation**: Factories must not silently inflate relationship graphs by default.
- **Readable intent over helper magic**: Slightly more explicit setup is acceptable when it makes the required context obvious.
- **Heavy context must announce itself**: Integration-heavy setup must be recognizable at the call site.
- **Legacy heavy aliases stay temporary**: Transitional helper aliases and legacy full-context fallbacks must declare how they will be retired.
## Suggested Task Breakdown
1. Inventory shared fixtures and high-usage callers.
2. Finalize the fixture-profile model.
3. Split shared helper APIs into light and heavy paths.
4. Audit the most-used factory defaults.
5. Introduce lean factory states and explicit heavy add-ons.
6. Remove provider, credential, membership, workspace, session, and capability defaults that should be opt-in.
7. Migrate the top caller pack.
8. Add guard and regression coverage.
9. Validate lane impact against the Spec 206 budgets.
10. Publish concise author guidance.
## Definition of Done
This feature is done when:
- the support layer has a clearly documented cheap default path,
- heavy defaults are explicit rather than silent,
- the central factories no longer create unnecessary hidden cascades by default,
- the highest-usage expensive callers have moved to the new lighter paths where appropriate,
- guard and regression coverage is green,
- the affected standard lanes are successfully validated against the Spec 206 baseline,
- and reviewers can tell from test setup whether a new test is asking for minimal, standard, or full integration context.
## Recommended Next Spec
After this feature, the next recommended step is Spec 208 - Filament or Livewire Heavy Suite Segmentation, so the suite first reduces per-test cost and then sharpens lane boundaries for the heaviest families.
## Key Entities *(include if feature involves data)*
- **Fixture Profile**: A named level of shared setup cost and scope, such as minimal, standard, or full or integration-heavy.
- **Shared Fixture Path**: A reusable setup path that creates only a defined amount of context for a test.
- **Lean Factory State**: A factory default or add-on state that keeps record creation limited to the context the test actually needs.
- **Hidden Cascade Finding**: An audited place where defaults, callbacks, or repair behavior silently create extra records or state.
- **Legacy Transition Path**: A temporary way to preserve heavier historical behavior while callers migrate to lighter profiles.
- **Fixture Cost Signal**: Naming, guidance, or guard coverage that makes a heavy setup request obvious.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: A documented fixture-profile model exists with minimal, standard, and full or integration-heavy levels, and all migrated call sites make the chosen level recognizable from the setup request.
- **SC-002**: The default shared fixture path and the default states of the highest-usage audited factories no longer create provider, credential, membership, workspace, session, or capability add-ons unless explicitly requested.
- **SC-003**: Guard coverage verifies both that minimal profiles stay lean and that heavy profiles create their promised extra context for the audited shared helpers and factories.
- **SC-004**: The top migration pack for the fast-feedback and confidence lanes is completed, and the migrated callers retain their intended business assertions without blanket fallback to legacy full-context setup.
- **SC-005**: Post-migration measurement shows that at least one affected standard lane improves wall-clock runtime by 10% or more versus the pre-migration baseline, and no affected standard lane regresses by more than 5% while coverage remains stable.
- **SC-006**: The hidden-cascade audit is completed for the named high-usage aggregates, and each finding is classified as removed, made explicit, or intentionally retained with a documented reason.
- **SC-007**: Contributors have concise author guidance for choosing the cheapest valid fixture path, and reviewers can identify when a new test is asking for heavy integration context.

View File

@ -0,0 +1,239 @@
# Tasks: Shared Test Fixture Slimming
**Input**: Design documents from `/specs/207-shared-test-fixture-slimming/`
**Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/, quickstart.md
**Tests**: Tests are REQUIRED for this feature because it changes shared test helpers, factory defaults, migration behavior, guard coverage, and lane-impact validation in a Laravel/Pest codebase.
**Operations / RBAC / UI surfaces**: Not applicable to product runtime. This feature is limited to repository test infrastructure, factories, contributor guidance, and Spec 206 lane-measurement reuse.
## Phase 1: Setup (Shared Infrastructure)
**Purpose**: Create the missing regression-test surfaces that the fixture-slimming work will fill.
- [X] T001 Create the missing fixture-slimming test files in `apps/platform/tests/Feature/Guards/FixtureLaneImpactBudgetTest.php`, `apps/platform/tests/Unit/Factories/OperationRunFactoryTest.php`, `apps/platform/tests/Unit/Factories/BackupSetFactoryTest.php`, and `apps/platform/tests/Unit/Factories/ProviderCredentialFactoryTest.php`
- [X] T002 [P] Expand the shared regression scaffolding in `apps/platform/tests/Feature/Guards/FixtureCostProfilesGuardTest.php`, `apps/platform/tests/Unit/Support/CreateUserWithTenantProfilesTest.php`, `apps/platform/tests/Unit/Factories/TenantFactoryTest.php`, `apps/platform/tests/Unit/Factories/ProviderConnectionFactoryTest.php`, and `apps/platform/tests/Unit/Factories/ProviderCredentialFactoryTest.php`
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Record the pre-migration measurement baseline, classify the hidden-cost surface, and establish the canonical fixture-profile vocabulary before story work begins.
**Critical**: No user story work should begin until this phase is complete.
- [X] T003 Capture the pre-migration fast-feedback and confidence lane baseline and reuse the Spec 206 comparison seams in `apps/platform/tests/Support/TestLaneManifest.php`, `apps/platform/tests/Support/TestLaneReport.php`, `apps/platform/tests/Feature/Guards/FixtureLaneImpactBudgetTest.php`, and `specs/207-shared-test-fixture-slimming/quickstart.md`
- [X] T004 [P] Inventory and classify the shared helper surface, high-usage caller packs, and hidden cascade findings in `specs/207-shared-test-fixture-slimming/data-model.md` and `specs/207-shared-test-fixture-slimming/quickstart.md`
- [X] T005 Implement the canonical `minimal`, `standard`, and `full` fixture profile catalog plus temporary legacy alias resolution in `apps/platform/tests/Pest.php`
**Checkpoint**: The shared support layer has a recorded pre-migration baseline, a classified audit surface, one canonical profile language, and one measurement path that all stories can build on.
---
## Phase 3: User Story 1 - Use A Minimal Fixture By Default (Priority: P1)
**Goal**: Make the default shared fixture path and default factory behavior create only the smallest valid context.
**Independent Test**: Run the helper and factory guards and verify that ordinary calls do not create provider, credential, cache, UI, or hidden workspace graph side effects.
### Tests for User Story 1
- [X] T006 [P] [US1] Add failing minimal-helper and user-side-effect coverage in `apps/platform/tests/Unit/Support/CreateUserWithTenantProfilesTest.php` and `apps/platform/tests/Feature/Guards/FixtureCostProfilesGuardTest.php`
- [X] T007 [P] [US1] Add failing lean tenant and workspace cascade coverage in `apps/platform/tests/Unit/Factories/TenantFactoryTest.php` and `apps/platform/tests/Unit/Factories/OperationRunFactoryTest.php`
### Implementation for User Story 1
- [X] T008 [US1] Make `minimal` the default shared fixture path and apply the already-defined `standard` side-effect contract in `apps/platform/tests/Pest.php`
- [X] T009 [US1] Remove implicit lean-workspace provisioning from `apps/platform/app/Models/Tenant.php` and `apps/platform/database/factories/TenantFactory.php`
- [X] T010 [US1] Add lean operation-context defaults in `apps/platform/database/factories/OperationRunFactory.php` and `apps/platform/database/factories/BackupSetFactory.php`
**Checkpoint**: The shared helper default and touched core factory defaults stay lean unless a test explicitly asks for more context.
---
## Phase 4: User Story 2 - Opt Into Heavy Context Deliberately (Priority: P1)
**Goal**: Preserve valid heavy integration setup, but require it to announce itself clearly through explicit profiles or states.
**Independent Test**: Run the helper and factory guards and verify that explicit provider, credential, backup, cache, and UI-context requests create the promised extra context without hiding behind the minimal default.
### Tests for User Story 2
- [X] T011 [P] [US2] Add failing explicit provider, credential, user-relationship, cache, and UI-context coverage in `apps/platform/tests/Unit/Support/CreateUserWithTenantProfilesTest.php` and `apps/platform/tests/Feature/Guards/FixtureCostProfilesGuardTest.php`
- [X] T012 [P] [US2] Add failing explicit heavy-state coverage for provider, provider-credential, and backup graphs in `apps/platform/tests/Unit/Factories/ProviderConnectionFactoryTest.php`, `apps/platform/tests/Unit/Factories/ProviderCredentialFactoryTest.php`, and `apps/platform/tests/Unit/Factories/BackupSetFactoryTest.php`
### Implementation for User Story 2
- [X] T013 [US2] Add explicit heavy, provider-enabled, credential-enabled, and `ui-context` helper entry points plus temporary transition aliases in `apps/platform/tests/Pest.php`
- [X] T014 [US2] Add explicit heavy factory states for provider, provider-credential, and backup graphs in `apps/platform/database/factories/ProviderConnectionFactory.php`, `apps/platform/database/factories/ProviderCredentialFactory.php`, and `apps/platform/database/factories/BackupSetFactory.php`
- [X] T015 [US2] Make heavy-by-intent builder setup announce itself in `apps/platform/tests/Feature/Concerns/BuildsBaselineCompareMatrixFixtures.php`, `apps/platform/tests/Feature/Concerns/BuildsGovernanceArtifactTruthFixtures.php`, `apps/platform/tests/Feature/Concerns/BuildsPortfolioTriageFixtures.php`, and `apps/platform/tests/Feature/Concerns/BuildsOperatorExplanationFixtures.php`
**Checkpoint**: Heavy setup remains available, but the call site now makes it obvious when the test is asking for integration-heavy context.
---
## Phase 5: User Story 3 - Migrate High-Usage Callers Safely (Priority: P2)
**Goal**: Move the first high-usage expensive callers onto the lightest valid profiles without unnecessary broad breakage.
**Independent Test**: Run the migrated caller packs and verify that narrow tests adopt `minimal` or `standard` profiles while the legacy full-context path remains available, visibly heavier, and marked for removal where still needed.
### Tests for User Story 3
- [X] T016 [P] [US3] Add failing migration-pack regression coverage for console and navigation callers in `apps/platform/tests/Feature/Console/ReconcileOperationRunsCommandTest.php`, `apps/platform/tests/Feature/Console/ReconcileBackupScheduleOperationRunsCommandTest.php`, and `apps/platform/tests/Feature/Navigation/RelatedNavigationResolverMemoizationTest.php`
- [X] T017 [P] [US3] Add failing migration-pack regression coverage for RBAC and drift callers in `apps/platform/tests/Feature/Spec080WorkspaceManagedTenantAdminMigrationTest.php` and `apps/platform/tests/Feature/BaselineDriftEngine/FindingFidelityTest.php`
### Implementation for User Story 3
- [X] T018 [US3] Migrate the first console and navigation caller pack to `minimal` or `standard` profiles in `apps/platform/tests/Feature/Console/ReconcileOperationRunsCommandTest.php`, `apps/platform/tests/Feature/Console/ReconcileBackupScheduleOperationRunsCommandTest.php`, and `apps/platform/tests/Feature/Navigation/RelatedNavigationResolverMemoizationTest.php`
- [X] T019 [US3] Migrate the first RBAC and drift caller pack to `minimal` or `standard` profiles in `apps/platform/tests/Feature/Spec080WorkspaceManagedTenantAdminMigrationTest.php` and `apps/platform/tests/Feature/BaselineDriftEngine/FindingFidelityTest.php`
- [X] T020 [US3] Preserve and label the visible legacy full-context path and declare its removal trigger in `apps/platform/tests/Pest.php`, `README.md`, and `specs/207-shared-test-fixture-slimming/quickstart.md`
**Checkpoint**: The first high-usage packs in the standard lanes use cheaper profiles, and any remaining full-context path is explicit rather than hidden.
---
## Phase 6: User Story 4 - Catch Fixture Cost Regressions Early (Priority: P2)
**Goal**: Add guard coverage and lane validation so hidden fixture-cost regressions are caught before they spread.
**Independent Test**: Run the fixture guards plus the Spec 206 fast-feedback and confidence reports and verify that hidden cascades fail loudly and lane outcomes stay stable or improved.
### Tests for User Story 4
- [X] T021 [P] [US4] Add hidden-cascade audit coverage for the tenant boot hook, user-side effects in shared helper defaults, provider connection defaults, provider-credential defaults, and backup-set callbacks in `apps/platform/tests/Unit/Support/CreateUserWithTenantProfilesTest.php`, `apps/platform/tests/Unit/Factories/TenantFactoryTest.php`, `apps/platform/tests/Unit/Factories/ProviderConnectionFactoryTest.php`, `apps/platform/tests/Unit/Factories/ProviderCredentialFactoryTest.php`, and `apps/platform/tests/Unit/Factories/BackupSetFactoryTest.php`
- [X] T022 [P] [US4] Add lane-impact regression coverage against the Spec 206 budgets in `apps/platform/tests/Feature/Guards/FixtureLaneImpactBudgetTest.php` and `apps/platform/tests/Feature/Guards/TestLaneArtifactsContractTest.php`
### Implementation for User Story 4
- [X] T023 [US4] Record the cascade-audit dispositions and fixture cost signals in `README.md` and `apps/platform/tests/Pest.php`
- [X] T024 [US4] Wire the recorded pre- and post-migration lane comparison into `scripts/platform-test-report`, `apps/platform/tests/Support/TestLaneReport.php`, and `apps/platform/tests/Support/TestLaneManifest.php`
- [X] T025 [US4] Publish concise author guidance for `minimal` vs `standard` vs `full` fixture setup in `README.md` and `specs/207-shared-test-fixture-slimming/quickstart.md`
**Checkpoint**: Hidden-cost regressions are guarded, and the fixture changes are measurable against the existing lane budgets.
---
## Phase 7: Polish & Cross-Cutting Concerns
**Purpose**: Reconcile the implementation with the design artifacts and validate the end-to-end workflow.
- [X] T026 [P] Reconcile the implemented fixture-profile catalog, temporary alias retirement data, and audit coverage with `specs/207-shared-test-fixture-slimming/contracts/shared-fixture-profile.schema.json` and `specs/207-shared-test-fixture-slimming/contracts/shared-test-fixture-slimming.logical.openapi.yaml`
- [X] T027 Run focused fixture and migration-pack validation with `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/CreateUserWithTenantProfilesTest.php`, `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Factories`, `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Console`, `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Navigation/RelatedNavigationResolverMemoizationTest.php`, `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Spec080WorkspaceManagedTenantAdminMigrationTest.php`, and `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/BaselineDriftEngine/FindingFidelityTest.php`
- [X] T028 Run post-migration Spec 206 lane validation and compare against the recorded pre-migration baseline with `./scripts/platform-test-lane fast-feedback`, `./scripts/platform-test-lane confidence`, `./scripts/platform-test-report fast-feedback`, and `./scripts/platform-test-report confidence`
- [X] T029 Run formatting with `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: No dependencies and can start immediately.
- **Foundational (Phase 2)**: Depends on Setup completion and blocks all story work.
- **User Story 1 (Phase 3)**: Starts after Foundational completion.
- **User Story 2 (Phase 4)**: Starts after Foundational completion and is independently testable, but it shares `apps/platform/tests/Pest.php` and the touched factory seams with US1.
- **User Story 3 (Phase 5)**: Depends on US1 and US2 because caller migration needs the new lean and heavy profile semantics to be stable first.
- **User Story 4 (Phase 6)**: Depends on US1, US2, and US3 because guard coverage and lane comparison need the implemented profile and migration behavior in place.
- **Polish (Phase 7)**: Runs after the desired user stories are complete.
### User Story Dependencies
- **User Story 1 (P1)**: No dependency on other stories. This is the recommended MVP slice.
- **User Story 2 (P1)**: Depends only on the foundational profile catalog and is independently testable once the helper and factory contracts exist.
- **User Story 3 (P2)**: Depends on US1 and US2 because migration packs need the new explicit profile behavior before callers can move safely.
- **User Story 4 (P2)**: Depends on US1, US2, and US3 because regression guards and lane comparisons must validate the final migrated behavior.
### Within Each User Story
- Tests MUST be written and fail before implementation.
- The pre-migration lane baseline must be recorded before helper or factory semantics change.
- Shared helper profile changes must land before touched caller files migrate to them.
- Lean tenant/workspace behavior must be fixed before operation or backup factories are treated as slimmed.
- Explicit heavy profiles and states must exist before legacy heavy callers are redirected to them.
- Lane-impact comparison must use the existing Spec 206 seams rather than a new one-off harness.
### Parallel Opportunities
- T002 can run in parallel with T001 after the new test files are created.
- T003 and T004 can run in parallel before T005 codifies the canonical profile vocabulary.
- US1 test tasks T006 and T007 can run in parallel.
- US2 test tasks T011 and T012 can run in parallel.
- US3 test tasks T016 and T017 can run in parallel.
- US4 test tasks T021 and T022 can run in parallel.
- After US1 and US2 are complete, one developer can migrate callers while another adds the lane-impact guard and reporting work.
---
## Parallel Example: User Story 1
```bash
# Run the lean-default tests in parallel:
Task: "Add failing minimal-helper side-effect coverage in apps/platform/tests/Unit/Support/CreateUserWithTenantProfilesTest.php and apps/platform/tests/Feature/Guards/FixtureCostProfilesGuardTest.php"
Task: "Add failing lean tenant and workspace cascade coverage in apps/platform/tests/Unit/Factories/TenantFactoryTest.php and apps/platform/tests/Unit/Factories/OperationRunFactoryTest.php"
```
---
## Parallel Example: User Story 2
```bash
# Run the explicit-heavy tests in parallel:
Task: "Add failing explicit provider, credential, cache, and UI-context coverage in apps/platform/tests/Unit/Support/CreateUserWithTenantProfilesTest.php and apps/platform/tests/Feature/Guards/FixtureCostProfilesGuardTest.php"
Task: "Add failing explicit heavy-state coverage for provider and backup graphs in apps/platform/tests/Unit/Factories/ProviderConnectionFactoryTest.php and apps/platform/tests/Unit/Factories/BackupSetFactoryTest.php"
```
---
## Parallel Example: User Story 3
```bash
# Run the migration-pack regressions in parallel:
Task: "Add failing migration-pack regression coverage for console and navigation callers in apps/platform/tests/Feature/Console/ReconcileOperationRunsCommandTest.php, apps/platform/tests/Feature/Console/ReconcileBackupScheduleOperationRunsCommandTest.php, and apps/platform/tests/Feature/Navigation/RelatedNavigationResolverMemoizationTest.php"
Task: "Add failing migration-pack regression coverage for RBAC and drift callers in apps/platform/tests/Feature/Spec080WorkspaceManagedTenantAdminMigrationTest.php and apps/platform/tests/Feature/BaselineDriftEngine/FindingFidelityTest.php"
```
---
## Parallel Example: User Story 4
```bash
# Run the regression guards in parallel:
Task: "Add hidden-cascade audit coverage for the tenant boot hook, provider connection defaults, and backup-set callbacks in apps/platform/tests/Unit/Factories/TenantFactoryTest.php, apps/platform/tests/Unit/Factories/ProviderConnectionFactoryTest.php, and apps/platform/tests/Unit/Factories/BackupSetFactoryTest.php"
Task: "Add lane-impact regression coverage against the Spec 206 budgets in apps/platform/tests/Feature/Guards/FixtureLaneImpactBudgetTest.php and apps/platform/tests/Feature/Guards/TestLaneArtifactsContractTest.php"
```
---
## Implementation Strategy
### MVP First (User Story 1 Only)
1. Complete Phase 1: Setup.
2. Complete Phase 2: Foundational.
3. Complete Phase 3: User Story 1.
4. Stop and validate that the default shared helper and touched default factories stay lean.
### Incremental Delivery
1. Record the pre-migration baseline, classify the audit surface, and land the canonical fixture profile vocabulary.
2. Ship the minimal-default behavior in the shared helper and core factories.
3. Add explicit heavy opt-in behavior so legitimate integration tests remain readable.
4. Migrate the first high-usage caller packs in the standard lanes.
5. Finish with guard coverage, lane validation, and contributor guidance.
### Parallel Team Strategy
1. One developer establishes the canonical profile catalog and shared comparison seams.
2. A second developer can own the minimal-default and explicit-heavy factory work once the profile catalog exists.
3. A third developer can migrate the first caller packs after the new profile behavior is stable.
4. A final pass adds regression guards, lane comparisons, and documentation updates.
---
## Notes
- `[P]` tasks touch different files and have no unresolved dependency on incomplete work.
- US1 is the recommended MVP because it restores the cheap default path first.
- US2 preserves legitimate heavy integration coverage without hiding its cost.
- US3 converts the highest-usage expensive callers into evidence-backed migration packs.
- US4 makes the new fixture discipline durable through guards and lane-budget validation.