fix(core): 공개 템플릿 config 캐시 무효화 누락과 확장 캐시 삭제 경로 동종 결함 정비

공개 config.json 의 고정 캐시 키(template.config.{identifier})가 어떤 갱신
경로에서도 forget 되지 않아 template:update/cache-clear/비활성화/삭제 후에도
최대 1시간(TTL) 이전 manifest 로 응답되던 결함(공개 )을 수정한다.

- TemplateManager::clearTemplateCache 공개화 — config/components_manifest(두
 변종)/현재 버전 routes·language 키 능동 forget, updateTemplate cleanup 호출
- forgetLayoutCacheKeys 가 편집기용 .with_source_meta 변종까지 삭제
- ext.cache_version 쓰기를 incrementExtensionCacheVersion 단일 지점으로 통일
- module/plugin:cache-clear 의 유령 키 forget 을 실재 캐시(레이아웃 서빙/해석
 캐시, 상태 키, 미들웨어 인덱스, 버전 bump) 무효화로 교체
- 공개 routes/language/layout API 의 ?v 생략 폴백을 0 → 현재 버전으로 교정
This commit is contained in:
HeuJung committed 2026-08-17 13:22:48 +09:00
1 parent 09a700e4c7
commit e819db9d42
27 files changed
+891 -247

No files matched your search

@@ -4,10 +4,17 @@ namespace Tests\Feature\Console\Commands\Module;
use App\Enums\ExtensionOwnerType;
use App\Enums\ExtensionStatus;
use App\Enums\LayoutSourceType;
use App\Extension\Cache\CoreCacheDriver;
use App\Extension\ExtensionMiddlewareRegistry;
use App\Extension\ModuleManager;
use App\Extension\Traits\ClearsTemplateCaches;
use App\Models\Module;
use App\Models\Permission;
use App\Models\Template;
use App\Models\TemplateLayout;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Tests\Helpers\ProtectsExtensionDirectories;
use Tests\TestCase;
@@ -163,24 +170,97 @@ class ModuleArtisanCommandsTest extends TestCase
/**
* module:cache-clear 커맨드가 모든 캐시를 삭제하는지 테스트
*
* 실재하는 상태 키(ext.modules.*_identifiers)가 소멸하고 캐시 버전이 bump 되는지
* 검증한다 (#588 — 기존 forget 대상은 writer 없는 유령 키였다).
*/
public function test_cache_clear_command_clears_all_caches(): void
{
$coreCache = new CoreCacheDriver(config('cache.default', 'array'));
$coreCache->put('ext.modules.active_identifiers', ['stale-module'], 3600);
$coreCache->put('ext.modules.installed_identifiers', ['stale-module'], 3600);
// 실제 writer(ExtensionMiddlewareRegistry::buildIndex)와 동형으로 태그 포함
// remember — flush() 는 taggable 스토어에서 태그 인덱스 기반으로 삭제한다
$coreCache->remember('extension.middleware_index', fn () => [['stale' => true]], 3600, [ExtensionMiddlewareRegistry::CACHE_TAG]);
Cache::put('g7:core:ext.cache_version', 1000);
$this->artisan('module:cache-clear')
->expectsOutput(__('modules.commands.cache_clear.clearing_all'))
->assertExitCode(0);
$this->assertNull(
$coreCache->get('ext.modules.active_identifiers'),
'module:cache-clear 후 활성 모듈 상태 캐시가 삭제되어야 합니다.'
);
$this->assertNull(
$coreCache->get('ext.modules.installed_identifiers'),
'module:cache-clear 후 설치 모듈 상태 캐시가 삭제되어야 합니다.'
);
$this->assertNull(
$coreCache->get('extension.middleware_index'),
'module:cache-clear 후 확장 미들웨어 인덱스 캐시(extension.middleware_index)가 삭제되어야 합니다.'
);
$this->assertGreaterThan(
1000,
ClearsTemplateCaches::getExtensionCacheVersion(),
'module:cache-clear 후 확장 캐시 버전이 bump 되어야 합니다.'
);
}
/**
* module:cache-clear 커맨드가 특정 모듈의 캐시만 삭제하는지 테스트
*
* 해석 캐시(layout_resolver.{templateId}.{name})는 버전 접미사 없는 고정 키로
* 레이아웃 row ID 를 저장하므로, 커맨드가 능동 삭제하지 않으면 TTL 동안
* 이전 해석 결과가 유지된다 (#588 동종 보강).
*/
public function test_cache_clear_command_clears_single_module_cache(): void
{
$identifier = 'sirsoft-board';
// 모듈 소유 레이아웃 row — 해석 캐시 키 도출(getLayoutsByModule)의 원천
$template = Template::factory()->create([
'identifier' => 'cachetest-admin_basic',
'type' => 'admin',
'status' => ExtensionStatus::Active->value,
]);
TemplateLayout::create([
'template_id' => $template->id,
'name' => 'sirsoft-board.cache_probe',
'source_type' => LayoutSourceType::Module->value,
'source_identifier' => $identifier,
'content' => ['meta' => ['title' => 'probe'], 'components' => []],
]);
$coreCache = new CoreCacheDriver(config('cache.default', 'array'));
$coreCache->put('ext.modules.active_identifiers', ['stale-module'], 3600);
$coreCache->put("layout_resolver.{$template->id}.sirsoft-board.cache_probe", 12345, 3600);
// 실제 writer(ExtensionMiddlewareRegistry::buildIndex)와 동형으로 태그 포함
// remember — flush() 는 taggable 스토어에서 태그 인덱스 기반으로 삭제한다
$coreCache->remember('extension.middleware_index', fn () => [['stale' => true]], 3600, [ExtensionMiddlewareRegistry::CACHE_TAG]);
Cache::put('g7:core:ext.cache_version', 1000);
$this->artisan('module:cache-clear', ['identifier' => $identifier])
->expectsOutput(__('modules.commands.cache_clear.clearing_single', ['module' => $identifier]))
->assertExitCode(0);
$this->assertNull(
$coreCache->get('ext.modules.active_identifiers'),
'module:cache-clear(단일) 후 모듈 상태 캐시가 삭제되어야 합니다.'
);
$this->assertNull(
$coreCache->get("layout_resolver.{$template->id}.sirsoft-board.cache_probe"),
'module:cache-clear(단일) 후 해당 모듈 레이아웃의 해석 캐시(layout_resolver.*)가 삭제되어야 합니다.'
);
$this->assertNull(
$coreCache->get('extension.middleware_index'),
'module:cache-clear(단일) 후 확장 미들웨어 인덱스 캐시(extension.middleware_index)가 삭제되어야 합니다.'
);
$this->assertGreaterThan(
1000,
ClearsTemplateCaches::getExtensionCacheVersion(),
'module:cache-clear(단일) 후 확장 캐시 버전이 bump 되어야 합니다.'
);
}
/**