diff --git a/docs/development/OpenNexus生产验收Runner.md b/docs/development/OpenNexus生产验收Runner.md index 498c914..ee9df21 100644 --- a/docs/development/OpenNexus生产验收Runner.md +++ b/docs/development/OpenNexus生产验收Runner.md @@ -24,4 +24,4 @@ python scripts/phase3-production-acceptance.py ` 报告目录包含 `summary.json`、`case-manifest.json`、`junit.xml`、`cases/.json` 和脱敏的 `logs/.log`。摘要记录 commit、各锁文件 SHA-256、配置摘要与已提供安装产物摘要。日志将仓库、数据根、报告根、用户主目录和配置声明的秘密值替换为占位符,并限制为 10 MiB。报告目录必须为空,避免单例复跑覆盖原始证据。 -当前 runner 与失败闭合行为已实现,B-01、B-02 凭据以及 S-01、S-02 Sync 客户端 driver 已登记;其余 26 个生产验收 ID 尚未登记,运行时会生成 `NOT_IMPLEMENTED` 证据并退出 1。这用于阻止误报,不是这些用例的验收通过。 +当前 runner 与失败闭合行为已实现,B-01、B-02 凭据、D-01 扩展包以及 S-01、S-02 Sync 客户端 driver 已登记;其余 25 个生产验收 ID 尚未登记,运行时会生成 `NOT_IMPLEMENTED` 证据并退出 1。这用于阻止误报,不是这些用例的验收通过。 diff --git a/scripts/acceptance_cases/d01_extensions.py b/scripts/acceptance_cases/d01_extensions.py new file mode 100644 index 0000000..17ac728 --- /dev/null +++ b/scripts/acceptance_cases/d01_extensions.py @@ -0,0 +1,107 @@ +"""D-01 signed package, ZIP limits, and safe extraction acceptance driver.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import shutil +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +MANIFEST = ROOT / "frontend" / "src-tauri" / "Cargo.toml" +TESTS = ( + "extension_package::tests::python_signature_binds_all_metadata_archive_and_signer", + "extension_package::tests::zip_rejects_traversal_aliases_links_duplicates_and_corrupt_local_headers", + "extension_package::tests::compressed_limits_are_inclusive_for_both_categories", + "extension_package::tests::category_entry_and_expanded_limits_accept_boundary_and_reject_next_byte", + "extension_trust::tests::live_responses_require_pinned_key_exact_release_and_no_revocation", + "extension_trust::tests::archive_download_verifies_real_fixture_and_enforces_stream_size", + "extension_unpack::tests::signed_package_extracts_and_rejects_modified_extra_missing_and_linked_files", + "extension_unpack::tests::junction_parent_cannot_redirect_output", +) + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as source: + for chunk in iter(lambda: source.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def run_exact(cargo: str, test: str) -> bool: + command = [ + cargo, + "test", + "--manifest-path", + str(MANIFEST), + "--locked", + "--features", + "desktop", + "--lib", + test, + "--", + "--exact", + "--nocapture", + ] + completed = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, check=False) + print(completed.stdout, end="") + print(completed.stderr, end="") + return completed.returncode == 0 and "1 passed; 0 failed" in completed.stdout + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--config", required=True) + parser.add_argument("--output", required=True) + args = parser.parse_args() + case_id = os.environ.get("OPENNEXUS_ACCEPTANCE_CASE_ID", "") + output = Path(args.output) + output.parent.mkdir(parents=True, exist_ok=True) + assertions = [ + {"name": "Python-produced Ed25519 vector verifies in Rust and metadata/archive/signer tampering fails"}, + {"name": "live key revocation, rotation, withdrawal, duplicate release, redirect, and stale trust fail closed"}, + {"name": "Theme and Plugin compressed, expanded, and entry limits accept the boundary only"}, + {"name": "traversal, aliases, links, duplicates, corrupt headers, encryption, and ZIP bombs are rejected"}, + {"name": "signed extraction detects extra, missing, modified, and hard-linked files"}, + {"name": "junction redirection creates zero files outside the installation root"}, + ] + cargo = shutil.which("cargo") + passed = case_id == "D-01" and cargo is not None and os.name == "nt" + if passed: + passed = all([run_exact(cargo, test) for test in TESTS]) + status = "PASSED" if passed else "FAILED" + evidence = "eight registered exact Rust package/trust/unpack oracles" + for assertion in assertions: + assertion.update({"status": status, "evidence": evidence}) + files = [] + for relative in ( + "frontend/src-tauri/src/extension_package.rs", + "frontend/src-tauri/src/extension_trust.rs", + "frontend/src-tauri/src/extension_unpack.rs", + "frontend/src/services/fixtures/community-python-vector.json", + ): + files.append({"path": relative, "sha256": sha256(ROOT / relative)}) + payload = { + "schema": 1, + "case_id": case_id, + "status": status, + "reason": "" if passed else "D-01 requires Windows P0, Cargo, and all eight exact oracles.", + "assertions": assertions, + "metrics": {"peak_rss_bytes": None, "max_process_count": None, "denied_access_count": None}, + "files": files, + "revisions": [ + {"scope": "archive classes", "classes": ["theme", "plugin"]}, + {"scope": "live trust responses", "cases": 8}, + {"scope": "archive download responses", "cases": 4}, + ], + } + output.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + return 0 if passed else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/phase3_acceptance.py b/scripts/phase3_acceptance.py index 436ff1f..233c6c6 100644 --- a/scripts/phase3_acceptance.py +++ b/scripts/phase3_acceptance.py @@ -42,6 +42,11 @@ CASE_DRIVERS: dict[str, dict[str, Any]] = { "timeout_seconds": 900, "required_metrics": (), }, + "D-01": { + "driver": "scripts/acceptance_cases/d01_extensions.py", + "timeout_seconds": 900, + "required_metrics": (), + }, "S-01": { "driver": "scripts/acceptance_cases/s01_sync_client.py", "timeout_seconds": 900,