From fd74a0b3b0a151a8ef06a3bb9394fa4fdf9eb4ea Mon Sep 17 00:00:00 2001 From: zizi Date: Fri, 18 Sep 2026 02:36:59 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B7=A5=E7=A8=8B(=E6=B5=8B=E8=AF=95):=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=B5=8B=E8=AF=95=E6=88=90=E6=9C=AC=E4=B8=8E?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E9=98=B2=E7=BA=BF=EF=BC=8C=E5=BC=95=E5=85=A5?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E5=85=B1=E4=BA=AB=E5=BA=93=E4=B8=8E=E4=BE=8B?= =?UTF-8?q?=E9=97=B4=E6=B8=85=E7=A9=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agent/rules/测试隔离.md | 3 +- .agent/rules/计划与提交.md | 4 +- .github/workflows/检查.yml | 2 +- AGENTS.md | 2 +- Makefile | 26 +++++++--- tests/单元/test_数据库底座生命周期.py | 18 +++++++ tests/支持/数据库夹具.py | 14 ++++- tests/支持/数据库底座.py | 74 +++++++++++++++++++++++++++ 8 files changed, 128 insertions(+), 15 deletions(-) diff --git a/.agent/rules/测试隔离.md b/.agent/rules/测试隔离.md index 6c9c68c..9c8ab21 100644 --- a/.agent/rules/测试隔离.md +++ b/.agent/rules/测试隔离.md @@ -4,7 +4,8 @@ > 权威合同:`tests/用例清单.json`、`docs/系统架构/新版设计/迁移与验收.md`;机械门禁:`tests/conftest.py`(OS 进程隔离 + 用例选择 + 全跳过守卫)、`工具/维护索引.py`。 1. **默认离线**:默认测试进程由 macOS sandbox 或 Linux seccomp 禁止外部连接,覆盖收集阶段、数据库驱动及其子进程;防护不可用时失败,不退回无防护执行。显式外部用例使用 `--外部环境` 与环境标记选择。 -2. **显式外部环境**:需要隔离 PostgreSQL 的用例标记 `数据库` 并使用 `隔离数据库URL` 夹具;缺 `MUSE_TEST_DATABASE_URL` 时直接失败,不跳过或回退旧库。 +2. **显式外部环境**:需要 PostgreSQL 的用例标记 `数据库` 并使用对应夹具;缺 `MUSE_TEST_DATABASE_URL` 时直接失败,不跳过或回退旧库。普通业务测试默认使用同一会话的共享已迁移测试库(例间在同一连接清空业务表并重设序列,保留模板与结构种子),不再为每条普通用例执行 `CREATE DATABASE ... TEMPLATE` 克隆与全量销毁;仅当用例自身验证目标为数据库创建、迁移升级、跨版本切换或模板隔离时,才使用独立克隆库。 + 超过三分钟的大 N 统计评测用例标记 `慢`,属于评测实验范畴,默认不包含在常规数据库验证中,只有用户显式请求实验时才运行。 实际Pi循环另标记`宿主`,通过`make 宿主测试`显式提供固定Node与Pi包路径;普通数据库测试剔除此标记。宿主验证使用合成提供方,不等于真实模型验证。真实外部模型另标记`真实模型`,通过`make 真实模型测试`显式提供地址与凭据文件;普通数据库测试剔除该标记。 3. **用例身份**:完整 `case_id` 与清单中的实际文件、符号及参数行绑定,不靠名称后缀匹配;`pytest --case TC-…` 选择用例,`--case 'TC-…[参数ID]'` 选择参数行。同符号承接多个 ID 时必须分别绑定无交叠参数。未知 ID、目标缺失与冒用均失败;Junit 留存完整 ID 和参数 ID。 4. **可判定性**:用例体内必须有 assert/raise/fail/skip/xfail;纯 print 用例被索引检查在执行前拦截。 diff --git a/.agent/rules/计划与提交.md b/.agent/rules/计划与提交.md index ed27f9d..74ad00b 100644 --- a/.agent/rules/计划与提交.md +++ b/.agent/rules/计划与提交.md @@ -27,7 +27,7 @@ - 发现影响目标或授权的设计缺口时返回对应决策,不以临时替身掩盖,也不扩大无关任务;已有目标、方案和证据仍有效时直接承接。 - **证据复用**:确认代码、合同、输入、环境、配置和工具版本仍适用后复用证据;出现相关变更、证据失效、新失败或未解决疑点时重验对应范围,影响不清时扩大到能覆盖风险的范围。 - **验证期冻结源码**:验证会话自持写闸门(`工具/验证锁.py`;`pytest` 会话开始持共享锁,结束比对源码哈希,不一致把退出码置 3 表示本轮作废)。持锁期间不改 `src/`、不跑 `make 生成` 与 `make 格式写入`;写入口用 `--执行` 在独占锁内落盘,检查与写入之间不留窗口。格式化同样算改源码:改过 `src/` 而没有重新生成的轮次一律作废,不采信其结论,也不把作废轮次的失败当成产品缺陷。 -- **全量只跑一次**:开发期只跑受影响用例(`make 快检 范围=<受影响路径>`);库层全量属于跨模块与收尾验收,走 `make 验收数据库`——该目标先在自己的写阶段完成生成与检查,再启动持共享锁的验证会话,因此与上一条不冲突;发现期需要并行时用 `make 数据库分片`。改过 `src/` 就必须重跑覆盖范围,无关改动不重跑已通过的全量。 +- **全量必须用户明确授权**:日常循环只跑受影响模块(`make 快检 范围=<受影响路径>`);严禁未经用户明确请求擅自执行 `make 验收数据库` 或全量 `make 数据库测试`(高成本违规操作)。模块纯逻辑/校验/提示词改动,以对应模块的快检与离线测试为完成证据;只有改动了 `src/muse/*/存储.py`、`数据库/迁移/` 或触发器/不可变约束时,才需要在共享测试库上验证对应模块的数据库用例。跨模块全量验证仅在用户明确指令时执行。发现期需要多模块时使用局部路径的 `make 数据库分片 范围=<路径>`。改过 `src/` 就必须在写独占锁内重新生成并重验覆盖范围。 ## 验证级别 @@ -35,7 +35,7 @@ |---|---|---| | L1 | 文档、注释、索引类改动,且未命中 L3 判据 | 对应机械检查通过,含本批链接、索引或声明核对 | | L2 | 未命中 L3 判据的局部实现 | 相关单元或契约测试通过,以及 `git diff --check` | -| L3 | 跨模块、超过 2 个文件、修改超过 100 行、核心逻辑变动、不可逆操作,任一命中 | 本批约定完整门禁、与变更有关的真实接缝验证,以及独立整体审查 | +| L3 | 跨模块、超过 2 个文件、修改超过 100 行、核心逻辑变动、不可逆操作,任一命中 | 本批约定模块门禁(非全量)、与变更直接相关的真实接缝验证,以及独立整体审查;全量数据库验收由用户按需授权,不作为自动前提 | 不可逆操作包括数据写入、删除和生产凭据变更。不可逆操作或核心逻辑变动须先评审方案,再对稳定批次审查;其余任务不叠加同一范围的评审。缺对应证据时标记 `needs_verification`,不以手工台账计数代替。纯声明批次按声明、索引和差异验证,不因级别运行无关数据库、模型或恢复演练;外部验证仍需相应授权。 diff --git a/.github/workflows/检查.yml b/.github/workflows/检查.yml index 3d0f925..0cf9ccf 100644 --- a/.github/workflows/检查.yml +++ b/.github/workflows/检查.yml @@ -57,7 +57,7 @@ jobs: - name: 初始化隔离用途角色 run: psql "$MUSE_TEST_DATABASE_URL" -v ON_ERROR_STOP=1 -f 数据库/初始化/用途角色.sql - name: 隔离数据库测试 - run: make 数据库测试 + run: MUSE_RUN_FULL_DB=1 make 数据库测试 - name: 固定Pi宿主整链 run: | npm install --prefix "$RUNNER_TEMP/muse-pi" --no-save --ignore-scripts @earendil-works/pi-coding-agent@0.85.1 diff --git a/AGENTS.md b/AGENTS.md index 494f461..047c19f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -70,7 +70,7 @@ SoT 按主题分域,不做跨主题的全局排序。可执行脚本与书面 ## 4. 工作协议(硬约束) 1. **读后动手与渐进发现**:复杂任务先读对应 SoT;涉及角色时读角色合同。技能发现遵守本文件开头的唯一入口,按当前任务展开,不预载无关资料。 -2. **机械验证优先与完成=验证**:工程统一使用仓内解释器 `.venv`(uv 管理);日常入口 `make 快检 范围=<受影响路径>`(开发期)、`make 检查`(格式+类型+模块边界+索引)、`make 测试`(默认离线进程隔离)、`make 数据库测试`(需 `MUSE_TEST_DATABASE_URL`);改完源码后的库层全量走 `make 验收数据库`(唯一入口,先生成再门禁后跑库),发现期要并行用 `make 数据库分片`,浏览器层用例走 `make 浏览器测试`(需隔离库与 `MUSE_BROWSER_EXECUTABLE`)。必须通过相关单元测试、门禁与 `git diff --check`,无自动化证据严禁声称“完成/修复/通过”。 +2. **机械验证优先与完成=验证**:工程统一使用仓内解释器 `.venv`(uv 管理);日常唯一入口为 `make 快检 范围=<受影响路径>`(秒级,开发期);离线全套 `make 测试`(~70秒);仅改动存储/迁移/触发器时跑对应模块的库测试。**全量数据库测试高耗时高耗盘,严禁未经用户明确请求擅自执行**:`make 验收数据库` 与全量 `make 数据库测试` 必须由用户显式触发(需传入 `MUSE_RUN_FULL_DB=1` 确认);慢评测实验(85次模型调用的终态验证)默认不跑,由 `make 评测实验` 显式申请;浏览器层走 `make 浏览器测试`。日常以模块快检或模块局部数据库测试为完成证据,无证据严禁声称“完成/修复/通过”。 3. **数据权威与先审后入**:数据库为唯一正式权威,严禁裸连操作;正文、规划与知识抽取默认生成 Shadow 候选,经用户明确确认后方可写入 Canonical 正典事实。 4. **模型治理与受控探索**:模型调用遵守[预算管理](src/muse/任务运行/预算管理.py)的固定日界窗口(默认 Asia/Shanghai,每日00/05/10/15/20开始,末窗20至24为4小时)与受控治理链;角色允许模型与策略版本以[角色策略](配置/角色策略.yaml)为准,并遵守[角色合同](.agent/角色/角色合同.md)(写手/规划固定顶级推理模型,裁判使用独立精确白名单);确定性逻辑、门禁与报告组装由脚本完成,严禁调用模型;智能体探索仅限圈定只读工具并留痕。 5. **会话交互与汇报纪律**:全程使用简体中文白话,坚决去除 AI 味(直陈事实、动作与后果,禁止清嗓子套话与空转缓冲词);需要用户决策时,必须交代清楚前因后果及各选项对下游的影响。 diff --git a/Makefile b/Makefile index e6a41f3..b4e57fa 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ export PYTHONPATH := $(CURDIR)/src:$(CURDIR)/工具 # Muse 安装、生成、检查、测试和构建入口。 # 每个声明命令都必须真实可执行(见 .agent/rules/文档与资源生成.md)。 -.PHONY: 迁移 前端旅程 快检 索引生成 包验收 安装 格式 格式写入 类型 模块边界 检查 测试 数据库测试 数据库分片 验收数据库 浏览器测试 宿主测试 真实模型测试 前端安装 前端检查 前端测试 生成 构建 资源核对 +.PHONY: 迁移 前端旅程 快检 索引生成 包验收 安装 格式 格式写入 类型 模块边界 检查 测试 数据库测试 数据库分片 验收数据库 评测实验 浏览器测试 宿主测试 真实模型测试 前端安装 前端检查 前端测试 生成 构建 资源核对 # 安装:建立并锁定 Python 依赖环境 安装: @@ -64,23 +64,33 @@ export PYTHONPATH := $(CURDIR)/src:$(CURDIR)/工具 测试: $(仓内解释器) -m pytest -m "not 数据库 and not 网络 and not 真实模型 and not 浏览器 and not 宿主 and not 安装包" $(范围) $(if $(用例),--case $(用例),) -# 验收数据库:改完源码后唯一该跑的全量入口。先生成、再门禁、最后跑库, -# 顺序由目标固定,避免“改了 src 没重新生成”这类整轮作废。 +# 验收数据库:全量高成本门禁,严禁自动执行,必须由用户显式提供 MUSE_RUN_FULL_DB=1。 +# 先生成、再门禁、最后跑库;排除慢评测实验(慢评测由 make 评测实验 显式执行)。 验收数据库: @test -n "$$MUSE_TEST_DATABASE_URL" || { echo "缺少 MUSE_TEST_DATABASE_URL(隔离库连接串);拒绝先付生成与门禁成本再失败" >&2; exit 1; } + @test "$$MUSE_RUN_FULL_DB" = "1" || { echo "全量数据库验收耗时高且写盘大,必须用户显式授权。请传入 MUSE_RUN_FULL_DB=1 确认执行" >&2; exit 1; } $(MAKE) 生成 $(MAKE) 检查 - $(仓内解释器) -m pytest --外部环境 -m "数据库 and not 宿主 and not 真实模型 and not 浏览器 and not 网络" $(范围) + $(仓内解释器) -m pytest --外部环境 -m "数据库 and not 慢 and not 宿主 and not 真实模型 and not 浏览器 and not 网络" $(范围) -# 数据库分片:发现与修复期用分片并行,全量只跑一次做最终判定 +# 数据库分片:发现与修复期用分片并行;默认排除慢用例 数据库分片: $(仓内解释器) 工具/并行数据库测试.py --片数 $(if $(片数),$(片数),3) $(范围) -# 数据库测试:需要 MUSE_TEST_DATABASE_URL;缺失即失败,不静默跳过。 -# 与 验收数据库 同口径排除 浏览器/网络:浏览器用例另有 浏览器测试 入口,不该在这里被静默 skip。 +# 数据库测试:需要 MUSE_TEST_DATABASE_URL。 +# 未指定范围时的全量测试必须显式传入 MUSE_RUN_FULL_DB=1;指定范围(如 范围=tests/集成/test_某.py)可直接运行。 +# 默认排除慢评测实验、浏览器、网络、宿主、真实模型。 数据库测试: @test -n "$$MUSE_TEST_DATABASE_URL" || { echo "缺少 MUSE_TEST_DATABASE_URL(隔离库连接串);拒绝静默跳过" >&2; exit 1; } - $(仓内解释器) -m pytest --外部环境 -m "数据库 and not 宿主 and not 真实模型 and not 浏览器 and not 网络" $(范围) $(if $(用例),--case $(用例),) + @if [ -z "$(范围)" ] && [ "$$MUSE_RUN_FULL_DB" != "1" ]; then \ + echo "未指定范围的全量数据库测试耗时高,必须用户显式授权。请指定范围(如 范围=tests/某文件.py)或传入 MUSE_RUN_FULL_DB=1 确认全量" >&2; exit 1; \ + fi + $(仓内解释器) -m pytest --外部环境 -m "数据库 and not 慢 and not 宿主 and not 真实模型 and not 浏览器 and not 网络" $(范围) $(if $(用例),--case $(用例),) + +# 评测实验:显式运行大 N 慢用例(85次模型调用的资格终态);默认从所有日常套件中排除。 +评测实验: + @test -n "$$MUSE_TEST_DATABASE_URL" || { echo "缺少 MUSE_TEST_DATABASE_URL(隔离库连接串)" >&2; exit 1; } + $(仓内解释器) -m pytest --外部环境 -m "数据库 and 慢" --timeout=900 $(范围) # 浏览器测试:需要隔离库与显式浏览器路径;11 个 数据库+浏览器 用例的唯一入口。 # MUSE_ISOLATED_TEST_ENVIRONMENT 由本目标声明:Playwright 配置加载期就要求它,缺了连 spec 都跑不到。 diff --git a/tests/单元/test_数据库底座生命周期.py b/tests/单元/test_数据库底座生命周期.py index 3d30526..61e7018 100644 --- a/tests/单元/test_数据库底座生命周期.py +++ b/tests/单元/test_数据库底座生命周期.py @@ -52,6 +52,9 @@ class 环境替身: def 工厂(self, 名称, 目录): return {"database": 名称} + def 清空业务数据(self, 身份): + self.清空次数 = getattr(self, "清空次数", 0) + 1 + @pytest.fixture def 环境(tmp_path): @@ -274,3 +277,18 @@ def test_已关闭底座拒绝复用旧模板(环境, tmp_path): with pytest.raises(RuntimeError, match="已关闭"), 底座.借库(tmp_path): pass assert not 后端.库 + + +@pytest.mark.case_id("NC-database-baseline-unit-15") +def test_共享库同指纹复用同一库且例间清空业务数据(环境, tmp_path): + 底座, 后端, _ = 环境 + 名称 = [] + for 行 in range(3): + with 底座.共享库(tmp_path / str(行), (1, 4, 16)) as 工厂: + 名称.append(工厂["database"]) + assert len(后端.库) == 2 # 1个模板 + 1个共享库 + assert len(后端.库) == 2 # 例后不销毁共享库 + assert len(set(名称)) == 1 # 全部复用同一个库名 + assert getattr(后端, "清空次数", 0) == 3 # 每次例后触发清空 + 底座.关闭() + assert 后端.库 == {} # 关闭时清理全部库 diff --git a/tests/支持/数据库夹具.py b/tests/支持/数据库夹具.py index 625c3a5..214aacd 100644 --- a/tests/支持/数据库夹具.py +++ b/tests/支持/数据库夹具.py @@ -61,7 +61,12 @@ def 测试数据库端点(数据库底座, tmp_path): @pytest.fixture def 应用测试库(数据库底座, tmp_path): - with 数据库底座.借库(tmp_path / "连接引用") as 工厂: + 借用 = ( + 数据库底座.借库 + if os.environ.get("MUSE_ISOLATED_CLONE") == "1" + else 数据库底座.共享库 + ) + with 借用(tmp_path / "连接引用") as 工厂: yield 工厂 @@ -97,7 +102,12 @@ def 内置种子方案(): @pytest.fixture def 内置结构测试库(数据库底座, 内置种子方案, tmp_path): 指纹, 导入 = 内置种子方案 - with 数据库底座.借库(tmp_path / "种子库引用", 种子指纹=指纹, 导入种子=导入) as 工厂: + 借用 = ( + 数据库底座.借库 + if os.environ.get("MUSE_ISOLATED_CLONE") == "1" + else 数据库底座.共享库 + ) + with 借用(tmp_path / "种子库引用", 种子指纹=指纹, 导入种子=导入) as 工厂: yield 工厂 diff --git a/tests/支持/数据库底座.py b/tests/支持/数据库底座.py index cfc36c1..c6429b6 100644 --- a/tests/支持/数据库底座.py +++ b/tests/支持/数据库底座.py @@ -169,6 +169,44 @@ class PostgreSQL后端: with 工厂[用途.维护].连接() as 连: return len(执行迁移(连, 目录)) + def 清空业务数据(self, 身份: 库身份) -> None: + """例间清空业务表并重置序列,保留迁移账本与结构模具种子;比整库销毁快百倍。""" + 参数 = conninfo_to_dict(self.管理URL) + 参数["user"] = 角色[用途.维护] + 参数["dbname"] = 身份.名称 + with psycopg.connect(make_conninfo(**参数), autocommit=True) as 连: + # 终止同一测试残留的空闲会话,避免 TRUNCATE 产生锁等待 + 连.execute( + """ + SELECT pg_terminate_backend(pid) + FROM pg_stat_activity + WHERE datname = %s + AND pid != pg_backend_pid() + AND backend_type = 'client backend' + AND state IN ('idle', 'idle in transaction', 'idle in transaction (aborted)'); + """, + (身份.名称,), + ) + 表行 = 连.execute( + """ + SELECT table_schema, table_name + FROM information_schema.tables + WHERE table_type = 'BASE TABLE' + AND ( + (table_schema IN ('public', 'evaluation', 'oracle') + AND table_name NOT IN ('muse_migration', 'muse_method_index_guard', 'muse_raw_namespace')) + OR + (table_schema = 'metadata' + AND table_name IN ('schema_binding', 'policy_version', 'activation_receipt')) + ) + ORDER BY table_schema, table_name; + """ + ).fetchall() + if 表行: + 目标 = ", ".join(f'"{s}"."{t}"' for s, t in 表行) + 连.execute(f"TRUNCATE TABLE {目标} RESTART IDENTITY CASCADE") + + class 数据库底座: """每个 session/worker 一个实例;模板构建失败不得进入可复用集合。""" @@ -180,6 +218,7 @@ class 数据库底座: self.会话 = uuid.uuid4().hex self.worker = worker self.模板: dict[str, 库身份] = {} + self.共享: dict[str, 库身份] = {} self.存活: dict[str, 库身份] = {} self.事件: list[dict] = [] self._已关闭 = False @@ -316,6 +355,41 @@ class 数据库底座: finally: self._删除(身份) + @contextmanager + def 共享库( + self, + 目录: Path, + 版本: tuple[int, ...] | None = None, + *, + 种子指纹: str = "", + 导入种子: Callable | None = None, + 清空: bool = True, + ) -> Iterator[dict]: + """会话内复用同一已迁移库,例后清空业务数据;仅会话结束时统一销毁。""" + self.初始化() + 指纹, _ = self._指纹(版本, 种子指纹) + if 指纹 not in self.共享: + 身份 = self._新库(self.基线(版本, 种子指纹=种子指纹, 导入种子=导入种子)) + self.共享[指纹] = 身份 + self.记录("shared_create", database=身份.名称, fingerprint=指纹) + else: + 身份 = self.共享[指纹] + self.记录("shared_reuse", database=身份.名称, fingerprint=指纹) + + 工厂 = self.后端.工厂(身份.名称, 目录) + try: + yield 工厂 + finally: + if 清空 and hasattr(self.后端, "清空业务数据"): + try: + self.后端.清空业务数据(身份) + self.记录("shared_clean", database=身份.名称) + except Exception as 错误: + self.记录( + "shared_clean_failed", database=身份.名称, reason=type(错误).__name__ + ) + raise + def 关闭(self) -> None: self._已关闭 = True 错误 = []