Files
Gnuboard7/tests/Unit/Services/CoreUpdateServiceQueueRestartTest.php
HeuJung ffab451b4a fix(core,installer): dev vendor 설치본의 코어 업데이트 중단 수정 — stale 패키지 매니페스트 3계층
Laravel PackageManifest 는 bootstrap/cache/packages.php 가 있으면 stale 여부를 검사하지
않고 그대로 읽어 등재된 provider 를 new 한다. 코어 업데이트는 Step 6/8 에서 vendor 를
--no-dev 로 교체하지만 그 파일은 Step 11 까지 이전 설치본의 것이 남으므로, 옵션 없는
`composer install` 로 깔린 사이트에서는 Step 10 spawn 자식이 새 vendor 에 없는 provider 를
찾다 부팅 단계에서 죽는다. 부팅 전이라 앱 로그에 흔적이 없고 부모에게는 자식의 비정상
종료로만 보여, 운영자에게는 「Class ... not found」 와 수동 재개 안내만 남는다.
(sir.kr 커뮤니티 제보, 7.0.9 → 7.0.10)

3계층으로 막는다.

 1. 부모 — spawn 직전 PackageManifestCacheHelper::clear
 2. 자식 — bootstrap/app.php 가 G7_UPDATE_IN_PROGRESS 를 보고 스스로 정리한다.
 이미 배포된 7.0.9·7.0.10 부모는 고칠 수 없으므로 그 아래에서 도는 신버전
 자식의 유일한 방어다. App\ 클래스를 참조하지 않고 실패는 무시한다.
 3. 범위 — CoreVersionChecker 의 env APP_VERSION 우선을 CoreUpdateContext 트리 안으로
 축소한다. 업데이트 전에 뜬 artisan serve·큐 워커가 옛 값을 물고 확장을
 incompatible_core 로 끄던 경로를 닫는다(관리자 템플릿이 대상이면 복구 UI
 자체에 도달할 수 없다).

업데이트 트리 판정은 App\Support\CoreUpdateContext 가 단독 소유하고
CoreServiceProvider::isCoreUpdateInProgress 는 위임으로 남는다. bootstrap/app.php 의
복제본은 부팅 전이라 불가피한 예외이며, 두 조건의 동형성을 테스트가 단언한다.

실측 중 드러난 결함 2건을 함께 고쳤다.

 - ConfigCacheHelper::withPreservedContainer 가 파사드 애플리케이션을 되돌리지 않아
 Step 11 이 `Target class [command.tinker] does not exist` 로 실패·롤백했다.
 - updateVersionInEnv 가 프로세스 환경을 갱신하지 않아 config 캐시에 이전 버전이 구워졌다
 (Laravel env 저장소가 불변이라 재부팅으로도 덮이지 않는다).

인스톨러는 재사용 vendor 의 개발용 패키지를 installed.json 으로 감지해 설치 환경 확인
카드·설치 로그로 알리되 설치를 차단하지 않고( 결정 D1), 재사용 경로에서도 컴파일 캐시를
정리한다. 실행되는 명령만이 아니라 실패 시 안내하는 수동 명령까지 --no-dev 로 맞췄다.
코어 업데이트 완료·핸드오프·단독 재개 사후 단계에서 queue:restart 신호를 보낸다(D3).

코어 7.0.10 → 7.0.11.
2026-09-08 09:46:57 +09:00

118 lines
5.1 KiB
PHP

<?php
namespace Tests\Unit\Services;
use App\Services\CoreUpdateService;
use Illuminate\Support\Facades\Artisan;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;
/**
* 코어 업데이트 후 큐 워커 재시작 신호 회귀 테스트 (dev-g7 #658).
*
* 코어 업데이트는 어느 지점에서도 `queue:restart` 를 보내지 않았다. 큐 워커는 부팅이 한 번뿐이라
* 코어 코드·config·확장 목록을 기동 시점 상태로 물고 있고, 업데이트가 파일을 전부 교체해도
* 옛 코드로 잡을 계속 처리한다 — 오류로 드러나지 않으므로 운영자가 손수 재시작할 때까지
* 조용히 어긋난 채 돈다(번들 확장 일괄 업데이트는 Manager 직접 호출이라 기존
* `ExtensionUpdateQueueListener` 도 걸리지 않는다).
*/
class CoreUpdateServiceQueueRestartTest extends TestCase
{
/**
* `queue:restart` 를 정확히 한 번 호출한다.
*
* @effects core_update_step11_signals_queue_restart
*/
#[Test]
public function signal_queue_restart_는_queue_restart_를_한_번_호출한다(): void
{
Artisan::shouldReceive('call')->once()->with('queue:restart')->andReturn(0);
app(CoreUpdateService::class)->signalQueueRestart();
$this->assertTrue(true, 'Artisan 기대치는 tearDown 에서 검증된다');
}
/**
* 신호 전송 실패는 업데이트를 되돌리지 않는다 — 예외를 삼키고 계속한다.
*/
#[Test]
public function signal_queue_restart_는_실패해도_예외를_전파하지_않는다(): void
{
Artisan::shouldReceive('call')
->once()
->with('queue:restart')
->andThrow(new \RuntimeException('큐 재시작 신호 전송 실패 시뮬레이션'));
app(CoreUpdateService::class)->signalQueueRestart();
$this->assertTrue(true, '예외가 전파되면 이 지점에 도달하지 못한다');
}
/**
* 업데이트를 마치는 세 지점이 캐시 정리 **직후** 신호를 보낸다 (호출 지점 계약).
*
* 행위 테스트로는 이 축을 덮을 수 없다 — `CoreUpdateCommandHandoffTest` 의 docblock 이
* 밝히듯 `handle()` 의 catch cleanup 시퀀스는 GitHub 연동·migration·vendor 설치까지
* 끌고 들어와야 하는 통합 면적이라 그 테스트 범위 밖이다. 그래서 세 지점의 배치를
* 소스로 잠근다: 새 마무리 지점이 신호 없이 추가되면 여기서 걸린다.
*
* 롤백 catch 는 의도적 제외다 — 백업으로 되돌린 **옛** 코드가 다시 도는 자리라 워커를
* 재기동시킬 이유가 없다.
*
* @effects core_update_step11_signals_queue_restart
*/
#[Test]
public function 캐시_정리_뒤_큐_신호를_보내는_지점이_셋이고_롤백은_제외된다(): void
{
$sites = [];
foreach ([
'app/Console/Commands/Core/CoreUpdateCommand.php',
'app/Console/Commands/Core/ExecuteUpgradeStepsCommand.php',
] as $relative) {
$source = (string) file_get_contents(base_path($relative));
$offset = 0;
while (($pos = strpos($source, '$service->clearAllCaches();', $offset)) !== false) {
// 정리 직후 구간(다음 400자)에 신호가 있는지 본다 — 사이에 낀 주석은 허용하되
// 다른 마무리 단계를 한참 지나 붙는 것은 "직후" 가 아니다.
$window = substr($source, $pos, 400);
$sites[] = [
'file' => $relative,
'line' => substr_count(substr($source, 0, $pos), "\n") + 1,
'signals' => str_contains($window, '$service->signalQueueRestart();'),
// 롤백 지점은 백업 복원 직후라 바로 뒤에 "롤백 후 캐시 정리 완료" 로그가 온다.
'is_rollback' => str_contains($window, '롤백 후 캐시 정리'),
];
$offset = $pos + 1;
}
}
$this->assertNotEmpty($sites, '전제: clearAllCaches 호출 지점을 찾을 수 있어야 한다');
$signalling = array_values(array_filter($sites, fn (array $s): bool => $s['signals']));
$silent = array_values(array_filter($sites, fn (array $s): bool => ! $s['signals']));
$this->assertCount(
3,
$signalling,
'Step 11 · 핸드오프 cleanup · 단독 실행 사후 단계 셋이 신호를 보내야 한다. 실제: '
.json_encode($sites, JSON_UNESCAPED_SLASHES)
);
$this->assertCount(
1,
$silent,
'신호를 보내지 않는 지점은 롤백 catch 하나뿐이어야 한다. 실제: '
.json_encode($silent, JSON_UNESCAPED_SLASHES)
);
$this->assertTrue(
$silent[0]['is_rollback'],
'제외된 지점은 롤백 catch 여야 한다 (옛 코드로 되돌린 자리라 워커 재기동 대상이 아니다). 실제: '
.json_encode($silent[0], JSON_UNESCAPED_SLASHES)
);
}
}