make(Kernel::class)->bootstrap(); $config = DB::connection()->getConfig(); if (! app()->environment('testing') || $config['database'] !== 'portal_services_test' || (string) $config['port'] !== '55441' || ! in_array($config['host'], ['127.0.0.1', 'localhost'], true)) { throw new RuntimeException('Requires isolated portal_services_test on localhost:55441.'); } $migration = require database_path('migrations/2026_09_30_110000_project_dependency_integrity.php'); $assertions = 0; $check = function (bool $condition) use (&$assertions): void { if (! $condition) { throw new RuntimeException('Dependency integrity assertion failed: '.($assertions + 1)); } $assertions++; }; $ids = array_map(fn () => (string) Str::uuid(), range(1, 3)); $link = fn (int $a, int $b, string $kind = 'dependency'): array => ['anchor_project_id' => $ids[$a], 'complementary_project_id' => $ids[$b], 'relation_type' => $kind]; $reject = function (callable $write) use ($check): void { try { $write(); } catch (QueryException $e) { $check($e->getCode() === '23514'); return; } throw new RuntimeException('Cycle was accepted'); }; try { foreach ($ids as $id) { DB::table('projects')->insert(['id' => $id, 'code' => 'test-dependency-'.$id, 'name' => 'Synthetic dependency test', 'status' => 'draft']); } DB::table('complementary_project_links')->insert($link(0, 1)); $reject(fn () => DB::table('complementary_project_links')->insert($link(1, 0))); DB::table('complementary_project_links')->insert($link(1, 2)); $reject(fn () => DB::table('complementary_project_links')->insert($link(2, 0))); $reject(fn () => DB::table('complementary_project_links')->insert($link(0, 0))); // Non-dependency relations may form a network; do not impose DAG semantics on them. DB::table('complementary_project_links')->insert($link(2, 0, 'supplier')); $reject(fn () => DB::table('complementary_project_links')->where('anchor_project_id', $ids[2])->update(['relation_type' => 'dependency'])); DB::table('complementary_project_links')->where('anchor_project_id', $ids[1])->update(['deleted_at' => now()]); DB::table('complementary_project_links')->insert($link(2, 0)); $reject(fn () => DB::table('complementary_project_links')->where('anchor_project_id', $ids[1])->update(['deleted_at' => null])); DB::table('complementary_project_links')->whereIn('anchor_project_id', $ids)->delete(); // Two real sessions: B→A waits for A→B, then must reject the resulting cycle. $parts = []; foreach (['host' => 'host', 'port' => 'port', 'dbname' => 'database', 'user' => 'username', 'password' => 'password'] as $key => $name) { $parts[] = $key."='".str_replace(['\\', "'"], ['\\\\', "\\'"], (string) ($config[$name] ?? ''))."'"; } $second = pg_connect(implode(' ', $parts), PGSQL_CONNECT_FORCE_NEW); if ($second === false) { throw new RuntimeException('Cannot connect second isolated session'); } pg_query($second, "SET statement_timeout='10s'"); DB::beginTransaction(); DB::table('complementary_project_links')->insert($link(0, 1)); pg_send_query_params($second, "INSERT INTO public.complementary_project_links(anchor_project_id,complementary_project_id,relation_type) VALUES($1,$2,'dependency')", [$ids[1], $ids[0]]); // Confirm the second statement is waiting on our advisory lock, not merely unscheduled. $waiting = false; for ($attempt = 0; $attempt < 100; $attempt++) { $state = DB::selectOne('SELECT wait_event FROM pg_stat_activity WHERE pid=?', [pg_get_pid($second)]); if (($state->wait_event ?? null) === 'advisory') { $waiting = true; break; } usleep(20000); DB::statement('SELECT pg_stat_clear_snapshot()'); } $check($waiting); DB::commit(); $result = pg_get_result($second); $check($result !== false && pg_result_error_field($result, PGSQL_DIAG_SQLSTATE) === '23514'); pg_close($second); $check(DB::table('complementary_project_links')->whereIn('anchor_project_id', $ids)->count() === 1); DB::table('complementary_project_links')->whereIn('anchor_project_id', $ids)->delete(); // Non-destructive DOWN/UP: schema reversibility without dropping any master or row. DB::transaction(function () use ($migration, $check): void { $migration->down(); $check(DB::selectOne("SELECT to_regprocedure('public.check_project_dependency_graph()') AS id")->id === null); $migration->up(); $check(DB::selectOne("SELECT to_regprocedure('public.check_project_dependency_graph()') AS id")->id !== null); }); echo json_encode(['passed' => true, 'assertions' => $assertions, 'two_session_cycle_rejected' => true, 'rollback_reapplication' => true], JSON_PRETTY_PRINT).PHP_EOL; } finally { while (DB::transactionLevel() > 0) { DB::rollBack(); } DB::table('complementary_project_links')->whereIn('anchor_project_id', $ids)->delete(); DB::table('projects')->whereIn('id', $ids)->delete(); }