修正新人临时引导库与共享数据库接入状态的误判 #86

Merged
WuYongJie merged 1 commits from fix/WuYongJie-shared-database-readiness into main 2026-08-31 17:50:43 +08:00
7 changed files with 131 additions and 17 deletions

View File

@ -2,6 +2,7 @@
namespace App\Console\Commands;
use App\Utilities\SystemDatabaseConfiguration;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Throwable;
@ -18,7 +19,7 @@ class ApplicationReady extends Command
protected $description = '检查应用安装、运行目录和数据库是否已经可用';
public function handle(): int
public function handle(SystemDatabaseConfiguration $configuration): int
{
if (! config('app.installed')) {
return $this->failed('APP_INSTALLED 未被 Laravel 加载。', self::EXIT_NOT_INSTALLED);
@ -42,7 +43,11 @@ class ApplicationReady extends Command
return $this->failed('Laravel 数据库连接失败。', self::EXIT_DATABASE_CONNECTION_FAILED);
}
$this->components->info('应用已就绪。');
if ($configuration->current()['mode'] === 'bootstrap') {
$this->components->info('本地网页和临时引导库已就绪;团队共享数据尚未接入。');
} else {
$this->components->info('应用运行正常,当前数据库可查询。团队数据接入状态请运行 system-database:verify 检查。');
}
return self::SUCCESS;
}

View File

@ -15,6 +15,8 @@ class VerifySystemDatabase extends Command
public const EXIT_SYSTEM_TABLE_VALIDATION_FAILED = 12;
public const EXIT_SHARED_NOT_CONFIGURED = 13;
protected $signature = 'system-database:verify';
protected $description = '在新的 Laravel 进程中验证当前共享系统数据库';
@ -30,12 +32,16 @@ class VerifySystemDatabase extends Command
);
}
if (
$settings['mode'] !== 'shared'
|| ! config('upstreams.stored_configuration.enabled', false)
) {
if ($settings['mode'] === 'bootstrap') {
$this->components->warn('当前仍使用本机临时引导库,尚未接入团队共享系统数据库;本次没有测试远程连接。');
$this->line('下一步:在“系统设置 → 数据库管理 → 系统数据库”中输入一次负责人提供的系统数据库密码。');
return self::EXIT_SHARED_NOT_CONFIGURED;
}
if ($settings['mode'] !== 'shared' || ! config('upstreams.stored_configuration.enabled', false)) {
return $this->failed(
'新配置未被 Laravel 加载。',
'共享配置未完整启用,请通过系统数据库连接页面重新保存;不要仅手工修改运行模式。',
self::EXIT_CONFIG_NOT_LOADED
);
}

View File

@ -462,6 +462,7 @@ class SystemDatabaseConfiguration
VerifySystemDatabase::EXIT_CONFIG_NOT_LOADED => '新配置未被加载',
VerifySystemDatabase::EXIT_DATABASE_CONNECTION_FAILED => 'Laravel 数据库连接失败',
VerifySystemDatabase::EXIT_SYSTEM_TABLE_VALIDATION_FAILED => '系统表验证失败',
VerifySystemDatabase::EXIT_SHARED_NOT_CONFIGURED => '新配置仍指向本机临时引导库',
default => '应用级验证失败',
};
}

View File

@ -397,16 +397,30 @@ fi
echo ""
echo "====================================================="
echo "本地测试环境已就绪:http://127.0.0.1:18088"
echo "本地网页可访问:http://127.0.0.1:18088"
db_host="$(read_env_value DB_HOST)"
if [ "$db_host" = "mysql" ]; then
echo "临时登录凭据保存在本机私有文件:$credential_file"
echo "请自行打开该文件,不要把内容提交到 Git、聊天或日志。"
echo ""
echo "下一步:登录后打开“系统设置 → 数据库管理 → 系统数据库”,"
echo "点击“连接”,只填写负责人提供的系统数据库密码。"
# Docker healthy 只表示当前连接可用;共享接入必须由新 Laravel 进程实测,
# 不能根据 DB_HOST 不是 mysql 就宣称团队数据已连接。
if docker compose -f "$compose_file" exec -T -u www-data app \
php artisan system-database:verify --no-ansi; then
shared_status=0
else
echo "已连接团队共享系统数据库,请使用其中属于自己的正式账号登录。"
shared_status=$?
fi
case "$shared_status" in
0)
echo "团队共享系统数据库验证通过,请使用其中属于自己的正式账号登录。"
;;
13)
echo "团队数据接入尚未完成;后台 worker 启动不代表数据已经同步。"
echo "临时登录凭据保存在本机私有文件:$credential_file"
echo "请登录后输入一次系统数据库密码,连接成功后改用团队正式账号登录。"
;;
*)
echo "团队共享系统数据库未通过验证;不要将 Docker healthy 当作接入成功。" >&2
echo "请让 Codex 根据上方失败阶段检查本机配置,保留现有配置和未提交代码。" >&2
exit "$shared_status"
;;
esac
echo "====================================================="

View File

@ -18,4 +18,20 @@ class ApplicationReadyTest extends TestCase
$this->artisan('app:ready')
->assertExitCode(\App\Console\Commands\ApplicationReady::EXIT_NOT_INSTALLED);
}
public function test_bootstrap_stays_healthy_without_claiming_shared_data_is_ready(): void
{
config(['app.installed' => true, 'system_database.mode' => 'bootstrap']);
$this->artisan('app:ready')
->expectsOutputToContain('团队共享数据尚未接入')
->assertSuccessful();
}
public function test_shared_application_health_does_not_claim_upstream_sync_success(): void
{
config(['app.installed' => true, 'system_database.mode' => 'shared']);
$this->artisan('app:ready')
->expectsOutputToContain('团队数据接入状态请运行 system-database:verify')
->assertSuccessful();
}
}

View File

@ -10,7 +10,7 @@ use Tests\TestCase;
class VerifySystemDatabaseTest extends TestCase
{
public function test_verify_command_reports_configuration_not_loaded(): void
public function test_bootstrap_is_not_reported_as_a_failed_remote_connection(): void
{
$configuration = Mockery::mock(SystemDatabaseConfiguration::class);
$configuration->shouldReceive('current')->once()->andReturn([
@ -19,9 +19,42 @@ class VerifySystemDatabaseTest extends TestCase
$this->app->instance(SystemDatabaseConfiguration::class, $configuration);
$this->artisan('system-database:verify')
->expectsOutputToContain('尚未接入团队共享系统数据库;本次没有测试远程连接')
->assertExitCode(VerifySystemDatabase::EXIT_SHARED_NOT_CONFIGURED);
}
public function test_shared_mode_with_disabled_stored_configuration_is_not_ready(): void
{
config(['upstreams.stored_configuration.enabled' => false]);
$configuration = Mockery::mock(SystemDatabaseConfiguration::class);
$configuration->shouldReceive('current')->once()->andReturn(['mode' => 'shared']);
$this->app->instance(SystemDatabaseConfiguration::class, $configuration);
$this->artisan('system-database:verify')
->expectsOutputToContain('共享配置未完整启用')
->assertExitCode(VerifySystemDatabase::EXIT_CONFIG_NOT_LOADED);
}
public function test_current_configuration_failure_remains_a_configuration_error(): void
{
$configuration = Mockery::mock(SystemDatabaseConfiguration::class);
$configuration->shouldReceive('current')->once()->andThrow(new RuntimeException('private details'));
$this->app->instance(SystemDatabaseConfiguration::class, $configuration);
$this->artisan('system-database:verify')
->expectsOutputToContain('新配置未被 Laravel 加载')
->assertExitCode(VerifySystemDatabase::EXIT_CONFIG_NOT_LOADED);
}
public function test_shared_success_requires_connection_and_system_table_verification(): void
{
$this->enableSharedMode();
$settings = ['mode' => 'shared', 'connection' => config('database.default')];
$configuration = Mockery::mock(SystemDatabaseConfiguration::class);
$configuration->shouldReceive('current')->once()->andReturn($settings);
$configuration->shouldReceive('probe')->once()->with($settings)->andReturn([]);
$this->app->instance(SystemDatabaseConfiguration::class, $configuration);
$this->artisan('system-database:verify')->expectsOutputToContain('共享系统数据库已通过应用级验证')->assertSuccessful();
}
public function test_verify_command_reports_database_connection_failure(): void
{
$this->enableSharedMode();

View File

@ -0,0 +1,39 @@
<?php
namespace Tests\Unit\Deployment;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Process\Process;
class LocalSharedDatabaseStatusTest extends TestCase
{
/** @dataProvider verificationStates */
public function test_initializer_reports_the_verified_state(int $verificationExit, int $expectedExit, string $message): void
{
$script = file_get_contents(dirname(__DIR__, 3) . '/deploy/local-init.sh');
$start = strpos($script, 'echo "本地网页可访问:');
$this->assertNotFalse($start);
$process = new Process(['bash', '-c',
'set -eu; compose_file=fixture; credential_file=fixture; docker() { return ' . $verificationExit . '; }; '
. substr($script, $start),
]);
$process->setTimeout(5);
$process->run();
$this->assertSame($expectedExit, $process->getExitCode());
$this->assertStringContainsString($message, $process->getOutput() . $process->getErrorOutput());
if ($verificationExit !== 0) {
$this->assertStringNotContainsString('团队共享系统数据库验证通过', $process->getOutput());
}
}
public static function verificationStates(): array
{
return [
'bootstrap' => [13, 0, '团队数据接入尚未完成'],
'shared success' => [0, 0, '团队共享系统数据库验证通过'],
'config failure' => [10, 10, '未通过验证'],
'network failure' => [11, 11, '未通过验证'],
'schema failure' => [12, 12, '未通过验证'],
];
}
}