fix(update): .agents项目自维护文件改判可选更新,防止升级强制覆盖与强制删除

This commit is contained in:
augushong
2026-09-07 23:46:22 +08:00
parent e53fadbad8
commit 68823568ef
2 changed files with 89 additions and 0 deletions

View File

@@ -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',

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace tests\Unit;
use app\admin\service\AdminUpdateService;
use PHPUnit\Framework\TestCase;
/**
* AdminUpdateServiceBase::testIsOptionalFiles 分类矩阵单元测试(纯单元,不触 DB).
*
* 锚定 .agents 项目自维护文件改判"可选更新"的行为:
* - .agents/PROJECT.md(精确匹配)与 .agents/rules/project- 前缀文件属项目自维护,
* 升级时必须判为可选(optional=true),防止被强制覆盖/强制删除
* - .agents 其余内容(ulthon-* 规则、skills、非精确路径、无连字符文件名)保持强制(false)
*
* 22 用例 = 可选 12 + 强制 10。用 PHPUnit 10 属性式 DataProvider(不支持 @dataProvider 注解),
* 每个用例独立成测试,红/绿状态可逐例观察。
*/
class AdminUpdateServiceOptionalFilesTest extends TestCase
{
/**
* 通过匿名类暴露 protected testIsOptionalFiles(构造函数仅设常量属性,无副作用).
*/
private function makeExposer(): object
{
return new class extends AdminUpdateService {
public function expose(string $path): bool
{
return $this->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)'
)
);
}
}