diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c81f9500..ba6d9d43 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,40 +1,31 @@ -## Scope 与 Non-goals +## 本次改动 -- Scope: -- Non-goals: +- 目标与范围: +- 不包含: -## 兼容、Migration 与 Rollback +## 验证与状态 -- [ ] API / Contract / Fingerprint / Snapshot 兼容影响已说明 -- [ ] 数据库变更包含 Alembic upgrade/downgrade;无数据库变更时已注明 -- [ ] Standalone / Compact / Full 兼容影响已验证或标为不适用 -- Rollback: +- 已执行: +- 未执行及原因: +- 最新提交的远程合并门禁: +- [ ] 本地定向验证已记录;未将其表述为 CI 或发布验收完成 +- [ ] 合并前使用适用的 `ci:light` 或 `ci:milestone`;代码和开发规则改动不用 `ci:light` -## 数据、安全与可观测性 +## 风险与后续 -- [ ] 新字段已标记数据分类、加密、Rotation、Retention、Export、Support Bundle 与 Redaction -- [ ] 未记录或返回 Password、Authorization、Cookie、Token、Secret、PII 或未脱敏敏感正文 -- [ ] 外部错误使用标准错误信封并包含 Trace ID -- [ ] 外部 URL、文件、文档、Workflow/Template 和 Evidence 均按不可信输入处理 +- 风险、失败模式及回退: +- 阻断问题与非阻断跟踪: -## 四维 Review +## 按变更适用性补充 -- [ ] 正确性与边界条件 -- [ ] Security / Authorization / Tenant 隔离 -- [ ] Compatibility / Migration / Rollback -- [ ] Operability / Observability / Documentation +- API / Contract / Fingerprint / Snapshot 兼容影响: +- 数据库 migration upgrade/downgrade: +- Standalone / Compact / Full 兼容与 Playwright E2E: +- Golden / Roundtrip / Failure-path: +- Backend format / Ruff / mypy / pytest 与 Frontend format / lint / coverage / build: +- 安全、授权、租户隔离和不可信输入: +- 数据分类、加密、Rotation、Retention、Export、Support Bundle: +- 外部错误信封和 Trace ID: +- 人工审批与权限范围: -## 验证方式 - -- [ ] Backend:format、ruff、mypy、pytest -- [ ] Frontend:format、lint、coverage、build -- [ ] Compose Playwright E2E 或说明不适用原因 -- [ ] Golden / Roundtrip / Migration / Failure-path(按变更适用) -- [ ] 已选择 `ci:light` 或 `ci:milestone`,最新 Head 的对应远程门禁通过;轻量模式的本地定向验证已记录 -- 证据: - -## 风险、审批与 Review Thread - -- 风险: -- [ ] 高风险操作具备明确人工审批;本 PR 未扩大既有 Scope/权限 -- [ ] 所有 P1/P2 与未解决 Review Thread 已回复证据并关闭 +现有授权诊断采集遵循请求级安装/项目策略:默认 OFF 不扫描、替换或阻断已授权值;ON 使用既有脱敏。不扩大采集。Review Thread 以问题是否仍真实存在及证据判定;阻断缺陷须解决,非阻断建议可记录后续跟踪。 diff --git a/AGENTS.md b/AGENTS.md index d2582caa..8119a708 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,8 +27,25 @@ - Treat target URLs, uploaded files, imported documents, workflow definitions, and templates as untrusted input. - Secret values are write-only at API boundaries and encrypted at rest. -## Required checks +## Development workflow -- Backend: `uv run ruff format --check .`, `uv run ruff check .`, `uv run mypy app`, and `uv run pytest`. -- Frontend: `pnpm format:check`, `pnpm lint`, `pnpm test:coverage`, and `pnpm build`. -- End-to-end behavior: Playwright against the Compose stack. +- Identify the affected modules and their tests before editing. Make the smallest change that satisfies the task. +- Keep unrelated refactors, historical warnings, and toolchain upgrades outside the task. Report a separate issue if it does not block the current work. +- Run targeted checks while developing. Do not repeat an unchanged failure without a relevant fix or new diagnostic information. Preserve full logs, but read and report the relevant failure summary first. +- Do not treat a targeted pass as proof that the full CI gate or release acceptance passed. + +## Validation scope + +- Documentation-only changes: check the changed document's format, links, and required structure. CI governance files and executable documentation examples are not automatically documentation-only changes. +- Local backend changes: run relevant Ruff checks and named backend tests with `make test-backend-targeted TARGETS='["tests/test_imports_api.py"]'`. Run `mypy app` when cross-file typing may be affected. +- Local frontend changes: run named Vitest files with `make test-frontend-targeted TARGETS='["src/lib/api.test.ts"]'`, plus affected formatting, lint, type, or build checks as applicable. +- Core or cross-module changes: expand checks to affected integration paths and deployment shapes. Engine, scheduling, authentication, authorization, tenant isolation, execution snapshots, shared contracts, database models or migrations, dependencies, and deployment changes require broader validation before merge. +- During development, do not default to full coverage, Compose, Windows packaging, historical upgrades, or capacity checks. Run them when the change's risk or the merge/release gate requires them. + +`make check` remains the complete local backend and frontend check. It runs backend `uv run ruff format --check .`, `uv run ruff check .`, `uv run mypy app`, and `uv run pytest`; frontend `pnpm format:check`, `pnpm lint`, `pnpm test:coverage`, and `pnpm build`. Complete end-to-end acceptance uses Playwright against the Compose stack when applicable. Targeted backend tests explicitly disable coverage collection; the full backend 90% and frontend 80% coverage thresholds remain in force. + +## Completion and reporting + +- Distinguish local targeted validation, remote merge gates for the latest commit, and full release acceptance. +- Report changed files, checks actually run, checks not run, and remaining risks. Never infer a green CI gate or release readiness from a local targeted pass. +- Use `ci:light` only for its allowed documentation scope. Code and development-rule changes require the applicable `ci:milestone` or controlled governance path; do not bypass branch protection or required checks. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d9460321..a1916559 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,18 +7,24 @@ - 提交信息采用 Conventional Commits,例如 `feat(api): add project endpoint` - 一个 Pull Request 聚焦一个可验证目标 -## 提交前检查 +## 开发中的定向验证 ```bash -make check +make test-backend-targeted TARGETS='["tests/test_imports_api.py"]' +make test-frontend-targeted TARGETS='["src/lib/api.test.ts"]' ``` +`TARGETS` 是非空 JSON 路径数组,可传多个真实测试文件;文件名中的空格按一个目标处理。后端定向测试明确使用 `--no-cov`,前端使用普通 Vitest 测试,不运行全量覆盖率。按改动范围补充 Ruff、格式、lint、类型检查或构建。引擎、调度、认证授权、租户隔离、执行快照、共享契约、数据库、依赖与部署变更需要扩大相关集成和部署验证。 + +完整本地前后端检查仍使用 `make check`。Compose Playwright、跨部署形态、历史升级和发布验收按变更风险及合并/发布门禁执行;不要把每次编辑都当作发布验收。CI 治理调整由独立授权的变更处理,不使用 `ci:light` 规避代码门禁。 + 涉及数据库模型的变更必须包含 Alembic 迁移;涉及 API 契约的变更必须更新测试和 OpenAPI 示例。 ## 完成定义 -- 验收标准通过 -- 单元/集成测试通过 -- 静态检查通过 +- 本地定向验证:记录实际命令、结果和未运行的完整检查;仅说明当前改动范围内的结论。 +- 可合并:最新提交的远程必需门禁通过,真实阻断缺陷已处理;不能复用旧提交的成功状态。 +- 可发布:适用的完整发布验收完成,包括相关部署形态与端到端验证。 - 遵循当前项目脱敏策略:默认 OFF 不自动扫描或改写已授权内容;开启 ON 时不得写入未脱敏的敏感日志 - 文档与变更保持一致 +- 独立的非阻断改进可记录后续跟踪;同一失败只在获得新信息或相关修复后重试。保留完整日志,报告先摘取有关的失败信息。 diff --git a/Makefile b/Makefile index 5e87b071..6b642da5 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,11 @@ -.PHONY: help install dev-backend dev-frontend test test-integration lint format check up down logs +.PHONY: help install dev-backend dev-frontend test test-backend-targeted test-frontend-targeted test-integration lint format check up down logs help: @echo "make install Install backend and frontend dependencies" @echo "make dev-backend Run FastAPI development server" @echo "make dev-frontend Run Vite development server" + @echo "make test-backend-targeted TARGETS='[\"tests/test_imports_api.py\"]'" + @echo "make test-frontend-targeted TARGETS='[\"src/lib/api.test.ts\"]'" @echo "make check Run backend tests/lint and frontend checks" install: @@ -19,6 +21,12 @@ dev-frontend: test: cd backend && uv run pytest +test-backend-targeted: + @python3 scripts/run_targeted_tests.py backend + +test-frontend-targeted: + @python3 scripts/run_targeted_tests.py frontend + test-integration: docker compose up -d postgres redis minio cd backend && uv run pytest -m integration diff --git a/scripts/run_targeted_tests.py b/scripts/run_targeted_tests.py new file mode 100644 index 00000000..95fe261d --- /dev/null +++ b/scripts/run_targeted_tests.py @@ -0,0 +1,82 @@ +"""Run only explicitly named backend or frontend test files.""" + +from __future__ import annotations + +import json +import os +import shlex +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +PROJECT_NAMES = ("backend", "frontend") + + +def valid_test_name(project: str, name: str) -> bool: + if project == "backend": + return name.startswith("test_") and name.endswith(".py") + return name.endswith((".test.ts", ".test.tsx", ".spec.ts", ".spec.tsx")) + + +def test_paths(project: str) -> list[str]: + raw = os.environ.get("TARGETS", "") + try: + targets = json.loads(raw) + except json.JSONDecodeError as exc: + raise ValueError("TARGETS must be a nonempty JSON array of test paths") from exc + if not isinstance(targets, list) or not targets: + raise ValueError("TARGETS must be a nonempty JSON array of test paths") + + project_root = ROOT / project + test_root = project_root / ("tests" if project == "backend" else "src") + paths: list[str] = [] + for target in targets: + if not isinstance(target, str) or not target.strip(): + raise ValueError("Each target must be a nonempty test file path") + relative = Path(target) + if relative.is_absolute() or ".." in relative.parts: + message = f"Target must stay inside {test_root.relative_to(ROOT)}: {target}" + raise ValueError(message) + resolved = (project_root / relative).resolve() + if not resolved.is_relative_to(test_root.resolve()) or not resolved.is_file(): + raise ValueError(f"Target must be an existing test file: {target}") + if not valid_test_name(project, resolved.name): + raise ValueError(f"Target must be a test file: {target}") + paths.append(str(relative)) + return paths + + +def main() -> int: + valid_arguments = ( + len(sys.argv) in (2, 3) + and sys.argv[1] in PROJECT_NAMES + and (len(sys.argv) == 2 or sys.argv[2] == "--dry-run") + ) + if not valid_arguments: + print( + "Usage: run_targeted_tests.py {backend|frontend} [--dry-run]", + file=sys.stderr, + ) + return 2 + project = sys.argv[1] + try: + paths = test_paths(project) + except ValueError as exc: + print(f"Error: {exc}", file=sys.stderr) + return 2 + command = ( + ["uv", "run", "pytest", "--no-cov", *paths] + if project == "backend" + else ["pnpm", "test", *paths] + ) + project_root = ROOT / project + print(f"In {project_root}: {shlex.join(command)}", flush=True) + if len(sys.argv) == 3: + return 0 + # The command prefix is fixed; every file path was resolved inside the test root. + return subprocess.run(command, cwd=project_root, check=False).returncode # noqa: S603 + + +if __name__ == "__main__": + raise SystemExit(main())