make(Kernel::class)->bootstrap(); requireCheck(! $app->configurationIsCached() && IsolatedQaDatabase::allowsSchemaChanges() && DB::connection()->getDatabaseName() === getenv('DB_DATABASE'), 'Effective QA target rejected.'); DB::statement("SET statement_timeout = '45s'"); $service = app(FundingProgrammes::class); requireCheck($service->installed(), 'Migrate the empty disposable database first.'); $directory = storage_path('app/private/funding-concurrency-20261102'); if (($argv[1] ?? null) === '--worker') { $path = realpath($argv[2] ?? ''); requireCheck($path !== false && dirname($path) === realpath($directory), 'Worker context path rejected.'); $context = json_decode(file_get_contents($path), true, 512, JSON_THROW_ON_ERROR); requireCheck(($context['database'] ?? null) === DB::connection()->getDatabaseName(), 'Worker database context differs.'); $task = $argv[3] ?? ''; $index = (int) ($argv[4] ?? -1); requireCheck(in_array($index, [0, 1], true) && in_array($task, ['prepare', 'save', 'publish', 'interest', 'withdraw', 'preferences', 'cancel', 'dispatch'], true), 'Worker operation rejected.'); $actor = User::findOrFail($context['actor']); requireCheck($actor->email === 'synthetic-funding-concurrency@example.invalid', 'Synthetic identity required.'); config(['portal.managers' => [$actor->email]]); $application = 'funding-qa-'.$context['run'].'-'.$task.'-'.$index; DB::select("SELECT set_config('application_name', ?, false)", [$application]); try { $result = match ($task) { 'preferences' => app(InvestmentOpportunityAlerts::class)->save($actor, $context['preferences']), 'cancel' => app(InvestmentOpportunityAlerts::class)->save($actor, ['version' => $context['preference_version'], 'enabled' => false]), 'dispatch' => ['delivered' => app(InvestmentOpportunityAlerts::class)->dispatchDue()], 'prepare' => app(FundingProgrammeReferences::class)->prepare($actor, 'fdel', $context['reference_hash']), 'save' => ['id' => $service->save($actor, $context['id'], ['version' => $context['version'], 'code' => 'fdel', 'reason' => 'Alteração sintética concorrente do processo '.$index.'.', 'draft' => [...$context['draft'], 'summary' => 'SINTÉTICO QA — Alteração concorrente '.$index]])], 'publish' => ['version_id' => $service->publish($actor, $context['id'], ['version' => $context['version'], 'hash' => $context['publication_hash'], 'reason' => 'Publicação sintética concorrente para ensaio isolado.', 'confirm' => true])], 'interest' => (function () use ($service, $actor, $context): array { $service->saveInterest($actor, $context['id'], $context['published'], true); return ['saved' => true]; })(), 'withdraw' => (function () use ($service, $actor, $context): array { $service->withdraw($actor, $context['id'], $context['version'], 'Retirada sintética concorrente para ensaio isolado.'); return ['withdrawn' => true]; })(), }; echo json_encode(['status' => 'ok', 'result' => $result], JSON_THROW_ON_ERROR).PHP_EOL; } catch (HttpExceptionInterface $error) { echo json_encode(['status' => $error->getStatusCode() === 409 ? 'conflict' : 'http_error', 'code' => $error->getStatusCode()], JSON_THROW_ON_ERROR).PHP_EOL; } catch (RecordNotFoundException $error) { echo json_encode(['status' => 'unavailable', 'code' => 404], JSON_THROW_ON_ERROR).PHP_EOL; } exit(0); } requireCheck(($argv[1] ?? null) === '--run', 'Use --run with an explicitly configured empty QA database.'); $lock = fopen(storage_path('framework/qa-database-'.hash('sha256', '127.0.0.1:55441:'.DB::connection()->getDatabaseName()).'.lock'), 'c'); requireCheck($lock !== false && flock($lock, LOCK_EX | LOCK_NB), 'Another QA runner owns this database.'); requireCheck(DB::table('users')->count() === 0 && DB::table('funding_programmes')->count() === 0, 'Disposable database must be empty; existing records will not be reset.'); if (! is_dir($directory)) { mkdir($directory, 0700, true); } $actor = User::factory()->create(['name' => 'SYNTHETIC QA funding concurrency', 'email' => 'synthetic-funding-concurrency@example.invalid']); config(['portal.managers' => [$actor->email]]); $run = bin2hex(random_bytes(5)); $context = ['actor' => $actor->id, 'run' => $run, 'database' => DB::connection()->getDatabaseName(), 'reference_hash' => app(FundingProgrammeReferences::class)->preview($actor, 'fdel')['hash']]; $checks = []; $phase = function (string $name, array $tasks, string $heldLock) use (&$context, $directory, &$checks): array { $path = $directory.'/'.$name.'-'.$context['run'].'.json'; file_put_contents($path, json_encode($context, JSON_THROW_ON_ERROR)); DB::beginTransaction(); if ($heldLock === 'reference') { DB::select('select pg_advisory_xact_lock(261102, hashtext(?))', ['fdel']); } elseif ($heldLock === 'actor') { DB::table('users')->where('id', $context['actor'])->lockForUpdate()->firstOrFail(); } elseif ($heldLock === 'preferences') { DB::table('investment_alert_preferences')->where('user_id', $context['actor'])->lockForUpdate()->firstOrFail(); } else { DB::table('funding_programmes')->where('id', $context['id'])->lockForUpdate()->firstOrFail(); } $processes = []; $observed = []; foreach ($tasks as $index => $task) { $process = new Process([PHP_BINARY, __FILE__, '--worker', $path, $task, (string) $index], dirname(__DIR__)); $process->setTimeout(60); $process->start(); $processes[] = $process; $application = 'funding-qa-'.$context['run'].'-'.$task.'-'.$index; $deadline = microtime(true) + 40; do { DB::select('SELECT pg_stat_clear_snapshot()'); $waiting = DB::selectOne("SELECT pid,wait_event,pg_blocking_pids(pid) AS blockers FROM pg_stat_activity WHERE datname=current_database() AND application_name=? AND wait_event_type='Lock'", [$application]); if ($waiting) { $observed[] = (array) $waiting; break; } requireCheck($process->isRunning(), 'Worker exited before overlapping lock wait: '.$name.' '.$process->getErrorOutput()); usleep(100000); } while (microtime(true) < $deadline); requireCheck($waiting !== null, 'No actual lock overlap observed: '.$name); } DB::commit(); $results = []; foreach ($processes as $process) { $process->wait(); requireCheck($process->isSuccessful(), 'Worker failed: '.$name.' '.$process->getErrorOutput()); $results[] = json_decode(trim($process->getOutput()), true, 512, JSON_THROW_ON_ERROR); } $checks[$name] = ['overlap_observed' => $observed, 'results' => $results]; echo json_encode(['phase' => $name, 'workers' => count($results), 'lock_overlap_verified' => true], JSON_THROW_ON_ERROR).PHP_EOL; return $results; }; $results = $phase('reference', ['prepare', 'prepare'], 'reference'); requireCheck(array_column($results, 'status') === ['ok', 'ok'] && count(array_filter(array_column(array_column($results, 'result'), 'created'))) === 1, 'Reference preparation duplicated or failed.'); requireCheck(DB::table('funding_programmes')->count() === 1 && DB::table('audit_events')->where('action', 'reference_prepared')->count() === 1, 'Reference history duplicated.'); $row = DB::table('funding_programmes')->where('code', 'fdel')->firstOrFail(); $context = [...$context, 'id' => $row->id, 'version' => (int) $row->version, 'draft' => json_decode($row->draft, true)]; $results = $phase('draft', ['save', 'save'], 'reference'); $statuses = array_column($results, 'status'); sort($statuses); requireCheck($statuses === ['conflict', 'ok'] && (int) DB::table('funding_programmes')->where('id', $row->id)->value('version') === 2, 'Concurrent stale draft was not rejected.'); $plan = $service->preview($actor, $row->id); $context = [...$context, 'version' => $plan['version'], 'publication_hash' => $plan['hash']]; $results = $phase('publication', ['publish', 'publish'], 'programme'); requireCheck(array_column($results, 'status') === ['ok', 'ok'] && $results[0]['result']['version_id'] === $results[1]['result']['version_id'] && DB::table('funding_programme_versions')->count() === 1, 'Concurrent publication duplicated.'); $context['published'] = $results[0]['result']['version_id']; $results = $phase('interest', ['interest', 'interest'], 'actor'); requireCheck(array_column($results, 'status') === ['ok', 'ok'] && DB::table('funding_programme_interests')->count() === 1 && DB::table('audit_events')->where('action', 'interest_saved')->count() === 1, 'Concurrent interest duplicated.'); $alerts = app(InvestmentOpportunityAlerts::class); requireCheck($alerts->fundingAvailable(), 'Funding alerts migration required for generator concurrency.'); $input = ['version' => 0, 'enabled' => true, 'frequency' => 'as_available', 'channel' => 'in_app', 'consent' => true, 'sources' => ['funding'], 'criteria' => []]; $alerts->save($actor, $input); $publishNext = function (string $label) use ($service, $actor, &$context): void { $row = DB::table('funding_programmes')->where('id', $context['id'])->firstOrFail(); $draft = json_decode($row->draft, true); $draft['summary'] = 'SYNTHETIC QA concurrent alert publication '.$label; $service->save($actor, $row->id, ['code' => 'fdel', 'version' => $row->version, 'reason' => 'Synthetic publication for generator race validation.', 'draft' => $draft]); $plan = $service->preview($actor, $row->id); $context['published'] = $service->publish($actor, $row->id, ['version' => $plan['version'], 'hash' => $plan['hash'], 'confirm' => true, 'reason' => 'Synthetic publication for generator race validation.']); $context['version'] = $plan['version']; }; $publishNext('duplicate-generators'); $results = $phase('duplicate_generators', ['dispatch', 'dispatch'], 'preferences'); requireCheck(array_column($results, 'status') === ['ok', 'ok'] && array_sum(array_column(array_column($results, 'result'), 'delivered')) === 1 && DB::table('investment_notifications')->count() === 1 && DB::table('investment_funding_alert_seen')->whereNotNull('notification_id')->count() === 1, 'Duplicate generator delivery.'); $context['preferences'] = [...$input, 'version' => 1, 'frequency' => 'daily']; $results = $phase('duplicate_preferences', ['preferences', 'preferences'], 'preferences'); requireCheck(array_column($results, 'status') === ['ok', 'ok'] && $results[0]['result']['version'] === 2 && $results[1]['result']['version'] === 2 && DB::table('audit_events')->where('source_system', 'investment-alerts')->where('action', 'preferences_saved')->count() === 2, 'Identical preference requests duplicated revisions.'); $publishNext('cancel-before-generator'); DB::table('investment_alert_preferences')->where('user_id', $actor->id)->update(['next_run_at' => now()]); $context['preference_version'] = 2; $results = $phase('cancel_before_generator', ['cancel', 'dispatch'], 'preferences'); requireCheck(array_column($results, 'status') === ['ok', 'ok'] && $results[1]['result']['delivered'] === 0 && ! $alerts->preferences($actor->id)['enabled'] && DB::table('investment_notifications')->count() === 1, 'Generator ignored effective cancellation.'); $alerts->save($actor, [...$input, 'version' => 3]); $publishNext('generator-before-cancel'); $context['preference_version'] = 4; $results = $phase('generator_before_cancel', ['dispatch', 'cancel'], 'preferences'); requireCheck(array_column($results, 'status') === ['ok', 'ok'] && $results[0]['result']['delivered'] === 1 && ! $alerts->preferences($actor->id)['enabled'] && DB::table('investment_notifications')->count() === 2 && $alerts->dispatchDue() === 0, 'Generator/cancellation race lost serialization or generated after cancellation.'); $results = $phase('withdrawal_vs_interest', ['withdraw', 'interest'], 'programme'); requireCheck($results[0]['status'] === 'ok' && in_array($results[1]['status'], ['ok', 'unavailable'], true) && DB::table('funding_programmes')->where('id', $row->id)->value('published_version_id') === null && DB::table('funding_programme_interests')->count() === 1, 'Withdrawal/interest race violated preservation.'); echo json_encode(['result' => 'passed', 'database' => DB::connection()->getDatabaseName(), 'synthetic' => true, 'phases' => $checks, 'active_writes' => 0, 'external_messages' => 0], JSON_THROW_ON_ERROR).PHP_EOL; } catch (Throwable $error) { if (isset($app) && DB::connection()->transactionLevel() > 0) { DB::rollBack(); } fwrite(STDERR, json_encode(['result' => 'failed', 'exception' => $error::class, 'code' => $error->getCode(), 'message' => $error instanceof RuntimeException ? $error->getMessage() : 'Inspect isolated QA process failure.'], JSON_THROW_ON_ERROR).PHP_EOL); exit(1); }