test: record pre-migration workspace evidence

This commit is contained in:
张胜宇
2026-09-10 17:11:52 +08:00
parent d0793355fc
commit a789872c79
3 changed files with 266 additions and 0 deletions
@@ -0,0 +1,54 @@
[
{
"path": "D:\\桌面\\财富项目\\group_fqcd_jr",
"branch": "develop",
"head": "c6be99078e2be5d00397449c826fa049b60910ba",
"status": [
"?? \"docs/15-\\347\\237\\245\\350\\257\\206\\346\\243\\200\\347\\264\\242\\346\\216\\245\\345\\205\\245\\346\\226\\271\\346\\241\\210.md\"",
"?? docs/superpowers/plans/2026-09-10-customer-service-rag-plan-a.md"
]
},
{
"path": "D:\\桌面\\财富项目\\group_fqcd_jr\\.worktrees\\customer-service-rag",
"branch": "feature/customer-service-rag",
"head": "ad7172367a0d69903e8bb92916f5d927084d8970",
"status": [
" M app/core/config.py",
" M app/core/contracts.py",
" M app/main.py",
" M app/service/agent/bootstrap.py",
" M app/service/agent/factory.py",
" M app/service/agent/governance.py",
" M app/service/agent_run_application_service.py",
" M app/service/model_gateway.py",
" M app/worker/runtime.py",
" M tests/conftest.py",
" M tests/integration/test_agent_run_acceptance.py",
" M tests/integration/test_worker_runtime_mysql.py",
" M tests/unit/service/test_model_gateway.py",
"?? app/core/knowledge_contracts.py",
"?? app/infrastructure/milvus_knowledge_adapter.py",
"?? app/model/knowledge.py",
"?? app/service/agent/customer_service_agent.py",
"?? app/service/agent/customer_service_routing.py",
"?? app/service/knowledge_authority.py",
"?? app/service/knowledge_config.py",
"?? app/service/knowledge_retrieval_service.py",
"?? app/service/knowledge_tool_service.py",
"?? app/static/",
"?? tests/unit/api/test_customer_service_test_page.py",
"?? tests/unit/core/test_knowledge_contracts.py",
"?? tests/unit/infrastructure/test_milvus_knowledge_adapter.py",
"?? tests/unit/service/test_customer_service_agent.py",
"?? tests/unit/service/test_knowledge_authority.py",
"?? tests/unit/service/test_knowledge_config.py",
"?? tests/unit/service/test_knowledge_retrieval.py"
]
},
{
"path": "D:\\桌面\\财富项目\\111\\qyqy_develop",
"branch": "qyqy_develop",
"head": "6516ccb385024f7c9fe9e2207de69cd924e26262",
"status": []
}
]
@@ -0,0 +1,95 @@
"""验证迁移前状态证据采集器只读取 Git 元数据。"""
# 导入测试所需的标准库 JSON、路径和子进程结果类型。
import json
from pathlib import Path
from subprocess import CompletedProcess
# 导入 pytest 补丁类型以及待验证的预检工具接口。
from pytest import MonkeyPatch
from tools.foundation_migration_preflight import (
collect_workspace_state,
run_git,
write_preflight_report,
)
# 验证未跟踪客服文件会被记录,且采集结果不含环境变量名称或值。
def test_collect_workspace_state_records_untracked_paths_without_environment_values(
tmp_path: Path,
) -> None:
# 构造确定性的 Git 命令替身,不调用真实 Git 或环境变量。
def fake_git_runner(_worktree: Path, *args: str) -> str:
# 为分支查询返回客服功能分支名称。
if args == ("branch", "--show-current"):
return "feature/customer-service-rag\n"
# 为提交查询返回固定的非敏感提交标识。
if args == ("rev-parse", "HEAD"):
return "abc123\n"
# 为状态查询返回一个未跟踪客服文件。
if args == ("status", "--short"):
return "?? app/service/agent/customer_service_agent.py\n"
# 防止测试静默接受未定义的 Git 查询。
raise AssertionError(f"unexpected git arguments: {args}")
# 使用替身采集临时工作区的只读状态。
state = collect_workspace_state(tmp_path, runner=fake_git_runner)
# 断言采集到预期的分支名称。
assert state["branch"] == "feature/customer-service-rag"
# 断言未跟踪客服文件完整保留在状态清单中。
assert state["status"] == ["?? app/service/agent/customer_service_agent.py"]
# 断言序列化结果不包含任何环境变量敏感字段。
assert "MYSQL_PASSWORD" not in json.dumps(state)
# 验证报告写入器只输出传入的非敏感 Git 状态结构。
def test_write_preflight_report_persists_utf8_json(tmp_path: Path) -> None:
# 指定临时报告文件,避免写入任何真实工作目录。
report_path = tmp_path / "preflight.json"
# 构造只含安全 Git 元数据的状态条目。
states = [{"path": "D:/workspace", "branch": "develop", "head": "abc123", "status": []}]
# 写入迁移前报告。
write_preflight_report(report_path, states)
# 以 UTF-8 读取并解析报告正文。
report = json.loads(report_path.read_text(encoding="utf-8"))
# 断言报告保留原始状态条目。
assert report == states
# 验证每次 Git 查询只信任当前显式工作区,而不写入全局 Git 配置。
def test_run_git_scopes_safe_directory_to_the_requested_worktree(
tmp_path: Path,
monkeypatch: MonkeyPatch,
) -> None:
# 保存被测函数交给子进程层的命令参数。
captured_commands: list[list[str]] = []
# 构造返回固定分支名的无副作用子进程替身。
def fake_run(command: list[str], **_kwargs: object) -> CompletedProcess[str]:
# 记录命令以便后续断言安全目录范围。
captured_commands.append(command)
# 返回模拟的 Git 成功结果。
return CompletedProcess(command, 0, "develop\n", "")
# 让测试不执行真实 Git。
monkeypatch.setattr("tools.foundation_migration_preflight.subprocess.run", fake_run)
# 执行一个受限 Git 分支查询。
output = run_git(tmp_path, "branch", "--show-current")
# 断言调用仍返回 Git 输出。
assert output == "develop\n"
# 断言命令仅为该临时工作区附加安全目录。
assert captured_commands == [[
"git",
"-c",
f"safe.directory={tmp_path.resolve().as_posix()}",
"-C",
str(tmp_path.resolve()),
"branch",
"--show-current",
]]
+117
View File
@@ -0,0 +1,117 @@
"""为无损底座迁移保存并校验工作区的只读 Git 状态证据。"""
# 导入命令行参数解析器以支持生成和校验报告。
import argparse
# 导入 JSON 序列化工具以写入 UTF-8 状态证据。
import json
# 导入子进程工具以直接调用 Git 而不经过 shell。
import subprocess
# 导入可调用协议和类型别名支持。
from collections.abc import Callable
# 导入路径类型以约束工作区和报告位置。
from pathlib import Path
# 导入任意 JSON 对象的静态类型。
from typing import Any
# 定义只允许执行的 Git 只读子命令首参数集合。
READ_ONLY_GIT_COMMANDS = frozenset({"branch", "rev-parse", "status"})
# 定义便于测试注入的 Git 调用函数类型。
GitRunner = Callable[..., str]
# 执行受限的 Git 只读命令并返回标准输出文本。
def run_git(worktree: Path, *args: str) -> str:
# 解析目标工作区以确保 Git 和安全目录使用同一个绝对路径。
resolved = worktree.resolve()
# 拒绝空命令,避免形成未约束的 Git 调用。
if not args:
raise ValueError("git command is required")
# 拒绝任何不在白名单中的 Git 子命令。
if args[0] not in READ_ONLY_GIT_COMMANDS:
raise ValueError("git command is not read-only")
# 将安全目录限定为当前查询工作区,避免写入全局 Git 配置。
safe_directory = resolved.as_posix()
# 以参数数组执行 Git,禁止 shell 解释路径或输入内容。
result = subprocess.run(
["git", "-c", f"safe.directory={safe_directory}", "-C", str(resolved), *args],
check=True,
capture_output=True,
encoding="utf-8",
errors="replace",
)
# 返回 Git 的标准输出,供调用方以确定性方式解析。
return result.stdout
# 收集一个工作区的分支、提交与简短状态,不读取环境变量或业务数据。
def collect_workspace_state(worktree: Path, runner: GitRunner = run_git) -> dict[str, object]:
# 解析绝对路径,防止报告中出现随当前目录变化的相对路径。
resolved = worktree.resolve()
# 依次执行已白名单化的 Git 查询并构造安全状态字典。
return {
"path": str(resolved),
"branch": runner(resolved, "branch", "--show-current").strip(),
"head": runner(resolved, "rev-parse", "HEAD").strip(),
"status": runner(resolved, "status", "--short").splitlines(),
}
# 将已收集的状态以 UTF-8 JSON 写入调用者指定的报告文件。
def write_preflight_report(target: Path, states: list[dict[str, object]]) -> None:
# 确保报告父目录存在,但不创建或改动任何工作区内容。
target.parent.mkdir(parents=True, exist_ok=True)
# 使用稳定缩进和 UTF-8 编码写入仅由调用方提供的状态数据。
target.write_text(json.dumps(states, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
# 从磁盘读取先前报告并验证指定工作区状态完全一致。
def verify_preflight_report(target: Path, worktrees: list[Path]) -> list[str]:
# 以 UTF-8 读取 JSON 报告,避免系统默认编码影响比较。
expected_value: Any = json.loads(target.read_text(encoding="utf-8"))
# 拒绝非列表报告,防止错误文件被误当作迁移证据。
if not isinstance(expected_value, list):
raise ValueError("preflight report must be a list")
# 为每个当前工作区重新采集只读 Git 状态。
current = [collect_workspace_state(worktree) for worktree in worktrees]
# 返回 JSON 表示不同的工作区路径,空列表代表完全一致。
return [
str(item["path"])
for item, expected in zip(current, expected_value, strict=True)
if item != expected
]
# 解析 CLI 工作区参数并执行报告生成或校验。
def main() -> int:
# 创建命令行解析器并限定所有输入为显式路径。
parser = argparse.ArgumentParser(description=__doc__)
# 要求调用者指定报告文件路径。
parser.add_argument("--report", type=Path, required=True)
# 允许多次提供要采集或校验的工作区路径。
parser.add_argument("--worktree", type=Path, action="append", required=True)
# 启用校验模式时不覆盖报告。
parser.add_argument("--verify", action="store_true")
# 解析用户传入的参数。
arguments = parser.parse_args()
# 在校验模式下输出差异并返回非零状态。
if arguments.verify:
# 比较当前状态和报告状态。
differences = verify_preflight_report(arguments.report, arguments.worktree)
# 输出机器和人工都可识别的校验结果。
print("UNCHANGED" if not differences else f"CHANGED: {', '.join(differences)}")
# 有任何差异时返回失败状态。
return 0 if not differences else 1
# 收集调用方明确列出的工作区状态。
states = [collect_workspace_state(worktree) for worktree in arguments.worktree]
# 写入新的迁移前证据报告。
write_preflight_report(arguments.report, states)
# 输出报告保存位置,避免输出任何敏感运行配置。
print(f"WROTE: {arguments.report}")
# 报告创建成功时返回零状态。
return 0
# 仅在脚本直接执行时运行命令行入口。
if __name__ == "__main__":
# 用 main 的返回值作为进程退出码。
raise SystemExit(main())