'test', 'confidence' => 'test:confidence', 'browser' => 'test:browser', 'heavy-governance' => 'test:heavy', 'profiling' => 'test:profile', 'junit' => 'test:junit', ]; /** * @return array */ public static function manifest(): array { return [ 'version' => 1, 'artifactDirectory' => self::artifactDirectory(), 'lanes' => [ [ 'id' => 'fast-feedback', 'governanceClass' => 'fast', 'description' => 'Quick representative feedback for normal local edits.', 'intendedAudience' => 'Contributors working in the default authoring loop.', 'includedFamilies' => ['unit', 'core-feature-safety'], 'excludedFamilies' => ['browser', 'heavy-governance'], 'ownershipExpectations' => 'Run on normal edits and escalate to confidence or heavy-governance when the touched surface expands.', 'defaultEntryPoint' => true, 'parallelMode' => 'required', 'selectors' => [ 'includeSuites' => ['Unit'], 'includePaths' => [ 'tests/Feature/Auth', 'tests/Feature/Authorization', 'tests/Feature/EntraAdminRoles', 'tests/Feature/Findings', 'tests/Feature/Guards', 'tests/Feature/Monitoring', 'tests/Feature/Navigation', 'tests/Feature/Onboarding', 'tests/Feature/RequiredPermissions', 'tests/Feature/Tenants', 'tests/Feature/Workspaces', ], 'includeGroups' => ['fast-feedback'], 'includeFiles' => [ 'tests/Feature/AdminConsentCallbackTest.php', 'tests/Feature/AdminNewRedirectTest.php', ], 'excludeSuites' => ['Browser'], 'excludePaths' => [ 'tests/Architecture', 'tests/Browser', 'tests/Deprecation', 'tests/Feature/078', 'tests/Feature/090', 'tests/Feature/144', 'tests/Feature/OpsUx', ], 'excludeGroups' => ['browser', 'heavy-governance'], 'excludeFiles' => [ 'tests/Feature/Filament/Alerts/AlertsKpiHeaderTest.php', 'tests/Feature/Guards/ActionSurfaceContractTest.php', 'tests/Feature/Guards/OperationLifecycleOpsUxGuardTest.php', 'tests/Feature/ProviderConnections/CredentialLeakGuardTest.php', ], ], 'artifacts' => ['summary', 'junit-xml', 'budget-report'], 'budget' => [ 'thresholdSeconds' => 200, 'baselineSource' => 'measured-current-suite', 'enforcement' => 'warn', 'lifecycleState' => 'documented', 'baselineDeltaTargetPercent' => 50, 'reviewCadence' => 'tighten after two stable runs', ], 'dbStrategy' => [ 'connectionMode' => 'sqlite-memory', 'resetStrategy' => 'refresh-database', 'seedsPolicy' => 'restricted', 'schemaBaselineCandidate' => false, ], ], [ 'id' => 'confidence', 'governanceClass' => 'confidence', 'description' => 'Broader pre-merge validation for non-browser feature and integration work.', 'intendedAudience' => 'Contributors and reviewers preparing a higher-confidence run before merge.', 'includedFamilies' => ['unit', 'non-browser-feature-integration'], 'excludedFamilies' => ['browser', 'heavy-governance'], 'ownershipExpectations' => 'Run before merge when a change touches multiple feature surfaces or shared infrastructure.', 'defaultEntryPoint' => false, 'parallelMode' => 'required', 'selectors' => [ 'includeSuites' => ['Unit'], 'includePaths' => ['tests/Feature'], 'includeGroups' => [], 'includeFiles' => [], 'excludeSuites' => ['Browser'], 'excludePaths' => [ 'tests/Architecture', 'tests/Browser', 'tests/Deprecation', 'tests/Feature/078', 'tests/Feature/090', 'tests/Feature/144', 'tests/Feature/OpsUx', ], 'excludeGroups' => ['browser', 'heavy-governance'], 'excludeFiles' => [ 'tests/Feature/Filament/Alerts/AlertsKpiHeaderTest.php', 'tests/Feature/Guards/ActionSurfaceContractTest.php', 'tests/Feature/Guards/OperationLifecycleOpsUxGuardTest.php', 'tests/Feature/ProviderConnections/CredentialLeakGuardTest.php', ], ], 'artifacts' => ['summary', 'junit-xml', 'budget-report'], 'budget' => [ 'thresholdSeconds' => 450, 'baselineSource' => 'measured-current-suite', 'enforcement' => 'warn', 'lifecycleState' => 'documented', 'reviewCadence' => 'tighten after two stable runs', ], 'dbStrategy' => [ 'connectionMode' => 'sqlite-memory', 'resetStrategy' => 'refresh-database', 'seedsPolicy' => 'restricted', 'schemaBaselineCandidate' => false, ], ], [ 'id' => 'browser', 'governanceClass' => 'heavy', 'description' => 'Dedicated browser runtime lane for smoke and workflow validation.', 'intendedAudience' => 'Contributors validating browser-specific behavior and smoke coverage.', 'includedFamilies' => ['browser'], 'excludedFamilies' => ['fast-feedback', 'confidence'], 'ownershipExpectations' => 'Run when a change touches browser behavior or before promoting browser-sensitive work.', 'defaultEntryPoint' => false, 'parallelMode' => 'forbidden', 'selectors' => [ 'includeSuites' => ['Browser'], 'includePaths' => ['tests/Browser'], 'includeGroups' => ['browser'], 'includeFiles' => [], 'excludeSuites' => [], 'excludePaths' => [], 'excludeGroups' => [], 'excludeFiles' => [], ], 'artifacts' => ['summary', 'junit-xml', 'budget-report'], 'budget' => [ 'thresholdSeconds' => 150, 'baselineSource' => 'measured-lane', 'enforcement' => 'warn', 'lifecycleState' => 'documented', 'reviewCadence' => 'tighten after two stable runs', ], 'dbStrategy' => [ 'connectionMode' => 'sqlite-memory', 'resetStrategy' => 'refresh-database', 'seedsPolicy' => 'restricted', 'schemaBaselineCandidate' => false, ], ], [ 'id' => 'heavy-governance', 'governanceClass' => 'heavy', 'description' => 'Intentionally expensive governance scans, architecture guards, and high-fan-out operational checks.', 'intendedAudience' => 'Maintainers and reviewers validating the heaviest governance families on purpose.', 'includedFamilies' => ['architecture-governance', 'ops-ux', 'surface-scan'], 'excludedFamilies' => ['browser'], 'ownershipExpectations' => 'Run intentionally when touching governance scans, high-fan-out guards, or operational UX surfaces.', 'defaultEntryPoint' => false, 'parallelMode' => 'optional', 'selectors' => [ 'includeSuites' => [], 'includePaths' => [ 'tests/Architecture', 'tests/Deprecation', 'tests/Feature/078', 'tests/Feature/090', 'tests/Feature/144', 'tests/Feature/OpsUx', ], 'includeGroups' => ['heavy-governance'], 'includeFiles' => [ 'tests/Feature/Filament/Alerts/AlertsKpiHeaderTest.php', 'tests/Feature/Guards/ActionSurfaceContractTest.php', 'tests/Feature/Guards/OperationLifecycleOpsUxGuardTest.php', 'tests/Feature/ProviderConnections/CredentialLeakGuardTest.php', ], 'excludeSuites' => ['Browser'], 'excludePaths' => [], 'excludeGroups' => ['browser'], 'excludeFiles' => [], ], 'artifacts' => ['summary', 'junit-xml', 'budget-report'], 'budget' => [ 'thresholdSeconds' => 120, 'baselineSource' => 'measured-lane', 'enforcement' => 'warn', 'lifecycleState' => 'documented', 'reviewCadence' => 'tighten after two stable runs', ], 'dbStrategy' => [ 'connectionMode' => 'mixed', 'resetStrategy' => 'refresh-database', 'seedsPolicy' => 'restricted', 'schemaBaselineCandidate' => false, ], ], [ 'id' => 'profiling', 'governanceClass' => 'support', 'description' => 'Serial non-browser profiling support run that exposes the slowest tests and families.', 'intendedAudience' => 'Maintainers investigating slow-test drift before it becomes baseline behavior.', 'includedFamilies' => ['unit', 'non-browser-feature-integration', 'heavy-governance'], 'excludedFamilies' => ['browser'], 'ownershipExpectations' => 'Run intentionally when refreshing baselines or refining the heavy-governance split from evidence.', 'defaultEntryPoint' => false, 'parallelMode' => 'forbidden', 'selectors' => [ 'includeSuites' => ['Unit'], 'includePaths' => ['tests/Feature'], 'includeGroups' => [], 'includeFiles' => [], 'excludeSuites' => ['Browser'], 'excludePaths' => ['tests/Browser'], 'excludeGroups' => ['browser'], 'excludeFiles' => [], ], 'artifacts' => ['summary', 'junit-xml', 'profile-top', 'budget-report'], 'budget' => [ 'thresholdSeconds' => 3000, 'baselineSource' => 'measured-current-suite', 'enforcement' => 'warn', 'lifecycleState' => 'documented', 'reviewCadence' => 'tighten after two stable runs', ], 'dbStrategy' => [ 'connectionMode' => 'sqlite-memory', 'resetStrategy' => 'refresh-database', 'seedsPolicy' => 'restricted', 'schemaBaselineCandidate' => false, ], ], [ 'id' => 'junit', 'governanceClass' => 'support', 'description' => 'Machine-readable report run for the broader confidence scope.', 'intendedAudience' => 'Contributors and reviewers who need durable artifacts instead of ad-hoc terminal output.', 'includedFamilies' => ['unit', 'non-browser-feature-integration'], 'excludedFamilies' => ['browser', 'heavy-governance'], 'ownershipExpectations' => 'Run when the latest broad report needs to be shared or attached to follow-up work.', 'defaultEntryPoint' => false, 'parallelMode' => 'required', 'selectors' => [ 'includeSuites' => ['Unit'], 'includePaths' => ['tests/Feature'], 'includeGroups' => [], 'includeFiles' => [], 'excludeSuites' => ['Browser'], 'excludePaths' => [ 'tests/Architecture', 'tests/Browser', 'tests/Deprecation', 'tests/Feature/078', 'tests/Feature/090', 'tests/Feature/144', 'tests/Feature/OpsUx', ], 'excludeGroups' => ['browser', 'heavy-governance'], 'excludeFiles' => [ 'tests/Feature/Filament/Alerts/AlertsKpiHeaderTest.php', 'tests/Feature/Guards/ActionSurfaceContractTest.php', 'tests/Feature/Guards/OperationLifecycleOpsUxGuardTest.php', 'tests/Feature/ProviderConnections/CredentialLeakGuardTest.php', ], ], 'artifacts' => ['summary', 'junit-xml', 'budget-report'], 'budget' => [ 'thresholdSeconds' => 450, 'baselineSource' => 'measured-lane', 'enforcement' => 'warn', 'lifecycleState' => 'documented', 'reviewCadence' => 'tighten after two stable runs', ], 'dbStrategy' => [ 'connectionMode' => 'sqlite-memory', 'resetStrategy' => 'refresh-database', 'seedsPolicy' => 'restricted', 'schemaBaselineCandidate' => false, ], ], ], 'familyBudgets' => [ [ 'familyId' => 'ops-ux-governance', 'selectorType' => 'path', 'selectors' => [ 'tests/Feature/OpsUx', 'tests/Feature/Filament/Alerts/AlertsKpiHeaderTest.php', 'tests/Feature/Guards/ActionSurfaceContractTest.php', 'tests/Feature/Guards/OperationLifecycleOpsUxGuardTest.php', 'tests/Feature/ProviderConnections/CredentialLeakGuardTest.php', ], 'thresholdSeconds' => 120, 'baselineSource' => 'measured-current-suite', 'enforcement' => 'warn', 'lifecycleState' => 'documented', 'reviewCadence' => 'tighten after two stable runs', ], [ 'familyId' => 'browser-smoke', 'selectorType' => 'path', 'selectors' => ['tests/Browser'], 'thresholdSeconds' => 150, 'baselineSource' => 'measured-lane', 'enforcement' => 'warn', 'lifecycleState' => 'documented', 'reviewCadence' => 'tighten after two stable runs', ], ], ]; } /** * @return list> */ public static function familyBudgets(): array { return self::manifest()['familyBudgets']; } /** * @return array */ public static function lane(string $id): array { foreach (self::manifest()['lanes'] as $lane) { if ($lane['id'] === $id) { return $lane; } } throw new InvalidArgumentException(sprintf('Unknown test lane [%s].', $id)); } public static function commandRef(string $laneId): string { if (! array_key_exists($laneId, self::COMMAND_REFS)) { throw new InvalidArgumentException(sprintf('Unknown lane command reference [%s].', $laneId)); } return self::COMMAND_REFS[$laneId]; } public static function artifactDirectory(): string { return self::ARTIFACT_DIRECTORY; } public static function fullSuiteBaselineSeconds(): int { return self::FULL_SUITE_BASELINE_SECONDS; } /** * @return list */ public static function buildCommand(string $laneId): array { $lane = self::lane($laneId); $command = ['php', 'artisan', 'test', '--without-tty', '--no-ansi']; $commandSuites = self::commandSuites($lane); if (! in_array('profile-top', $lane['artifacts'], true)) { $command[] = '--compact'; } if ($lane['parallelMode'] === 'required') { $command[] = '--parallel'; } if ($commandSuites !== []) { $command[] = '--testsuite='.implode(',', $commandSuites); } $command[] = '--log-junit='.TestLaneReport::artifactPaths($laneId)['junit']; if (in_array('profile-top', $lane['artifacts'], true)) { $command[] = '--profile'; } if ($lane['selectors']['includeGroups'] !== []) { $command[] = '--group='.implode(',', $lane['selectors']['includeGroups']); } if ($lane['selectors']['excludeGroups'] !== []) { $command[] = '--exclude-group='.implode(',', $lane['selectors']['excludeGroups']); } foreach (self::commandTargets($lane) as $target) { $command[] = $target; } return $command; } public static function runLane(string $laneId): int { self::ensureArtifactDirectory(); $command = self::buildCommand($laneId); $process = new Process($command, self::appRoot()); $process->setTimeout(null); $capturedOutput = ''; $startedAt = microtime(true); $process->run(function (string $type, string $buffer) use (&$capturedOutput): void { echo $buffer; $capturedOutput .= $buffer; }); TestLaneReport::finalizeLane($laneId, microtime(true) - $startedAt, $capturedOutput); return $process->getExitCode() ?? 1; } public static function renderLatestReport(string $laneId): int { $artifactPaths = TestLaneReport::artifactPaths($laneId); $reportPath = self::absolutePath($artifactPaths['report']); $wallClockSeconds = 0.0; if (is_file($reportPath)) { $existingReport = json_decode((string) file_get_contents($reportPath), true); $wallClockSeconds = (float) ($existingReport['wallClockSeconds'] ?? 0.0); } $parsed = TestLaneReport::parseJUnit(self::absolutePath($artifactPaths['junit']), $laneId); $profileOutputPath = self::absolutePath($artifactPaths['profile']); $report = TestLaneReport::buildReport( laneId: $laneId, wallClockSeconds: $wallClockSeconds, slowestEntries: $parsed['slowestEntries'], durationsByFile: $parsed['durationsByFile'], ); TestLaneReport::writeArtifacts( laneId: $laneId, report: $report, profileOutput: is_file($profileOutputPath) ? (string) file_get_contents($profileOutputPath) : null, ); echo json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR).PHP_EOL; return 0; } /** * @return list */ public static function discoverFiles(string $laneId): array { $lane = self::lane($laneId); $selectors = $lane['selectors']; $files = []; foreach (array_merge(self::suiteTargets($selectors['includeSuites']), $selectors['includePaths'], $selectors['includeFiles']) as $target) { foreach (self::discoverTarget($target) as $resolvedFile) { if (self::isExcluded($resolvedFile, $selectors)) { continue; } $files[] = $resolvedFile; } } $files = array_values(array_unique($files)); sort($files); return $files; } public static function absolutePath(string $relativePath): string { return self::appRoot().DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, ltrim($relativePath, '/')); } private static function appRoot(): string { return dirname(__DIR__, 2); } private static function ensureArtifactDirectory(): void { $directory = self::absolutePath(self::artifactDirectory()); if (is_dir($directory)) { return; } mkdir($directory, 0777, true); } /** * @param list $suites * @return list */ private static function suiteTargets(array $suites): array { $targets = []; foreach ($suites as $suite) { $targets = array_merge($targets, match ($suite) { 'Unit' => ['tests/Unit'], 'Feature' => ['tests/Feature'], 'Browser' => ['tests/Browser'], 'Pgsql' => ['tests/Feature/WorkspaceIsolation/WorkspaceIdForeignKeyConstraintTest.php'], default => [], }); } return array_values(array_unique($targets)); } /** * @param array $lane * @return list */ private static function commandTargets(array $lane): array { return []; } /** * @param array $lane * @return list */ private static function commandSuites(array $lane): array { $selectors = $lane['selectors']; $suites = []; foreach ($selectors['includeSuites'] as $suite) { $suites[] = $suite; } foreach (array_merge($selectors['includePaths'], $selectors['includeFiles']) as $target) { $suite = self::inferSuiteFromTarget($target); if ($suite !== null) { $suites[] = $suite; } } $suites = array_values(array_unique(array_filter($suites, static fn (mixed $suite): bool => is_string($suite) && $suite !== ''))); if ($selectors['excludeSuites'] !== []) { $suites = array_values(array_filter( $suites, static fn (string $suite): bool => ! in_array($suite, $selectors['excludeSuites'], true), )); } $suiteOrder = [ 'Unit' => 10, 'Feature' => 20, 'Browser' => 30, 'Architecture' => 40, 'Deprecation' => 50, 'Pgsql' => 60, ]; usort($suites, static function (string $left, string $right) use ($suiteOrder): int { return ($suiteOrder[$left] ?? 999) <=> ($suiteOrder[$right] ?? 999); }); return $suites; } private static function inferSuiteFromTarget(string $target): ?string { return match (true) { str_starts_with($target, 'tests/Unit') => 'Unit', str_starts_with($target, 'tests/Feature') => 'Feature', str_starts_with($target, 'tests/Browser') => 'Browser', str_starts_with($target, 'tests/Architecture') => 'Architecture', str_starts_with($target, 'tests/Deprecation') => 'Deprecation', default => null, }; } /** * @return list */ private static function discoverTarget(string $target): array { $absoluteTarget = self::absolutePath($target); if (is_file($absoluteTarget)) { return [str_replace(DIRECTORY_SEPARATOR, '/', $target)]; } if (! is_dir($absoluteTarget)) { return []; } $files = []; $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($absoluteTarget)); /** @var SplFileInfo $file */ foreach ($iterator as $file) { if (! $file->isFile() || ! str_ends_with($file->getFilename(), 'Test.php')) { continue; } $resolvedPath = str_replace(self::appRoot().DIRECTORY_SEPARATOR, '', $file->getPathname()); $files[] = str_replace(DIRECTORY_SEPARATOR, '/', $resolvedPath); } return $files; } /** * @param array $selectors */ private static function isExcluded(string $filePath, array $selectors): bool { if (in_array($filePath, $selectors['excludeFiles'], true)) { return true; } foreach ($selectors['excludePaths'] as $excludedPath) { if (str_starts_with($filePath, rtrim($excludedPath, '/'))) { return true; } } return false; } }