diff --git a/extend/base/admin/service/AdminUpdateServiceBase.php b/extend/base/admin/service/AdminUpdateServiceBase.php index b32c62a..47203eb 100644 --- a/extend/base/admin/service/AdminUpdateServiceBase.php +++ b/extend/base/admin/service/AdminUpdateServiceBase.php @@ -664,6 +664,14 @@ class AdminUpdateServiceBase protected function testIsOptionalFiles($file_path) { + // .agents 下项目自维护的文件:可选更新,防止被强制覆盖/强制删除 + if ($file_path === '.agents/PROJECT.md') { + return true; + } + if (str_starts_with($file_path, '.agents/rules/project-')) { + return true; + } + // 如果file_path以app或config开头,则是可选更新的文件 $optional_files_prefix = [ 'app', diff --git a/tests/Unit/AdminUpdateServiceOptionalFilesTest.php b/tests/Unit/AdminUpdateServiceOptionalFilesTest.php new file mode 100644 index 0000000..aa340f5 --- /dev/null +++ b/tests/Unit/AdminUpdateServiceOptionalFilesTest.php @@ -0,0 +1,81 @@ +testIsOptionalFiles($path); + } + }; + } + + public static function classificationProvider(): iterable + { + // 可选(true) 12 例:既有规则覆盖 8 例 + .agents 项目自维护新规则 4 例 + yield 'optional: root AGENTS.md' => ['AGENTS.md', true]; + yield 'optional: root composer.json' => ['composer.json', true]; + yield 'optional: root .php-cs-fixer.php' => ['.php-cs-fixer.php', true]; + yield 'optional: app prefix' => ['app/common/service/X.php', true]; + yield 'optional: config prefix' => ['config/app.php', true]; + yield 'optional: route prefix' => ['route/app.php', true]; + yield 'optional: source/docker prefix' => ['source/docker/Dockerfile', true]; + yield 'optional: extend/think prefix' => ['extend/think/store/X.php', true]; + yield 'optional: .agents/PROJECT.md exact' => ['.agents/PROJECT.md', true]; + yield 'optional: project rule flat' => ['.agents/rules/project-dev-runtime-deploy.md', true]; + yield 'optional: project rule flat 2' => ['.agents/rules/project-account-system.md', true]; + yield 'optional: project rule nested' => ['.agents/rules/project-deploy-detail/sub.md', true]; + + // 强制(false) 10 例:核心入口、框架内核、静态资源、迁移、.agents 其余内容 + yield 'force: think entry' => ['think', false]; + yield 'force: extend/base kernel' => ['extend/base/admin/service/AdminUpdateServiceBase.php', false]; + yield 'force: public static' => ['public/static/common/css/a.css', false]; + yield 'force: database migrations' => ['database/migrations/2024.php', false]; + yield 'force: ulthon rule' => ['.agents/rules/ulthon-naming-convention.md', false]; + yield 'force: ulthon skill' => ['.agents/skills/ulthon-timer/SKILL.md', false]; + yield 'force: project skill (skills 始终强制)' => ['.agents/skills/project-x/SKILL.md', false]; + yield 'force: project.md 无连字符不匹配前缀' => ['.agents/rules/project.md', false]; + yield 'force: .agents/PROJECT 非精确不匹配' => ['.agents/PROJECT/about.md', false]; + yield 'force: .agents/rules/.README' => ['.agents/rules/.README', false]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('classificationProvider')] + public function test_classification(string $path, bool $expectedOptional): void + { + $exposer = $this->makeExposer(); + + $this->assertSame( + $expectedOptional, + $exposer->expose($path), + sprintf( + '路径 %s 分类不符:预期%s,实际%s', + $path, + $expectedOptional ? '可选(true)' : '强制(false)', + $expectedOptional ? '强制(false)' : '可选(true)' + ) + ); + } +}