test(sidecar): 登记 A-02 生产验收

This commit is contained in:
2026-09-09 10:17:48 +08:00
parent ce73c4d588
commit faa40f1793
4 changed files with 179 additions and 3 deletions
+55 -2
View File
@@ -3,6 +3,7 @@ import json
import os import os
from pathlib import Path from pathlib import Path
import queue import queue
import secrets
import subprocess import subprocess
import sys import sys
import threading import threading
@@ -51,6 +52,42 @@ def test_session_auth_covers_every_route_and_rejects_duplicate_headers():
assert len(calls) == 6 assert len(calls) == 6
def test_session_auth_rejects_missing_wrong_and_old_generation_100_times_without_side_effects():
calls = []
async def app(scope, receive, send):
calls.append(scope["path"])
await send({"type": "http.response.start", "status": 204, "headers": []})
secret = "ab" * 32
generation = "cd" * 32
auth = SessionAuth(app, secret, generation, 4567)
host = (b"host", b"127.0.0.1:4567")
authorization = (b"authorization", f"Bearer {secret}".encode())
current = (b"x-core-generation", generation.encode())
cases = {
"missing": [host, current],
"wrong": [host, (b"authorization", ("Bearer " + "ef" * 32).encode()), current],
"old": [host, authorization, (b"x-core-generation", ("01" * 32).encode())],
}
async def request(headers):
messages = []
async def send(message):
messages.append(message)
await auth({"type": "http", "headers": headers, "path": "/api/notes"}, None, send)
return messages
for name, headers in cases.items():
for _ in range(100):
messages = asyncio.run(request(headers))
assert messages[0]["status"] == 401, name
assert json.loads(messages[1]["body"])["error"]["code"] == "AUTH_REQUIRED"
assert calls == []
def test_handshake_proof_binds_port_pid_generation_and_challenge(): def test_handshake_proof_binds_port_pid_generation_and_challenge():
args = ["01" * 32, "02" * 32, "03" * 32, 123, 4567, 123] args = ["01" * 32, "02" * 32, "03" * 32, 123, 4567, 123]
expected = proof(*args) expected = proof(*args)
@@ -62,8 +99,8 @@ def test_handshake_proof_binds_port_pid_generation_and_challenge():
def test_real_sidecar_bootstrap_auth_and_parent_eof(tmp_path): def test_real_sidecar_bootstrap_auth_and_parent_eof(tmp_path):
config = dict(protocol=1, secret="01" * 32, challenge="02" * 32, config = dict(protocol=1, secret=secrets.token_hex(32), challenge=secrets.token_hex(32),
generation="03" * 32, data_dir=str(tmp_path / "core")) generation=secrets.token_hex(32), data_dir=str(tmp_path / "core"))
executable = os.environ.get("OPENNEXUS_CORE_TEST_BINARY") executable = os.environ.get("OPENNEXUS_CORE_TEST_BINARY")
command = [executable] if executable else [sys.executable, "-m", "app.sidecar"] command = [executable] if executable else [sys.executable, "-m", "app.sidecar"]
diagnostics = (tmp_path / "core-stderr.log").open("wb") diagnostics = (tmp_path / "core-stderr.log").open("wb")
@@ -73,6 +110,8 @@ def test_real_sidecar_bootstrap_auth_and_parent_eof(tmp_path):
stderr=diagnostics, stderr=diagnostics,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0)) creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
try: try:
assert config["secret"] not in "\0".join(command)
assert config["secret"] not in "\0".join(os.environ.values())
config["launcher_pid"] = process.pid config["launcher_pid"] = process.pid
process.stdin.write(json.dumps(config).encode() + b"\n") process.stdin.write(json.dumps(config).encode() + b"\n")
process.stdin.flush() process.stdin.flush()
@@ -96,8 +135,22 @@ def test_real_sidecar_bootstrap_auth_and_parent_eof(tmp_path):
}) })
with opener.open(request, timeout=5) as response: with opener.open(request, timeout=5) as response:
assert json.load(response)["status"] == "ok" assert json.load(response)["status"] == "ok"
for disabled in ("/docs", "/redoc", "/openapi.json", "/docs/oauth2-redirect"):
request = urllib.request.Request(url + disabled, headers={
"Authorization": "Bearer " + config["secret"],
"X-Core-Generation": config["generation"],
})
with pytest.raises(urllib.error.HTTPError) as error:
opener.open(request, timeout=5)
assert error.value.code == 404
process.stdin.close() process.stdin.close()
assert process.wait(timeout=10) == 0 assert process.wait(timeout=10) == 0
diagnostics.flush()
planted = config["secret"].encode()
assert planted not in (tmp_path / "core-stderr.log").read_bytes()
for artifact in (tmp_path / "core").rglob("*"):
if artifact.is_file():
assert planted not in artifact.read_bytes(), artifact.name
finally: finally:
if not process.stdin.closed: if not process.stdin.closed:
process.stdin.close() process.stdin.close()
@@ -24,4 +24,4 @@ python scripts/phase3-production-acceptance.py `
报告目录包含 `summary.json``case-manifest.json``junit.xml``cases/<ID>.json` 和脱敏的 `logs/<ID>.log`。摘要记录 commit、各锁文件 SHA-256、配置摘要与已提供安装产物摘要。日志将仓库、数据根、报告根、用户主目录和配置声明的秘密值替换为占位符,并限制为 10 MiB。报告目录必须为空,避免单例复跑覆盖原始证据。 报告目录包含 `summary.json``case-manifest.json``junit.xml``cases/<ID>.json` 和脱敏的 `logs/<ID>.log`。摘要记录 commit、各锁文件 SHA-256、配置摘要与已提供安装产物摘要。日志将仓库、数据根、报告根、用户主目录和配置声明的秘密值替换为占位符,并限制为 10 MiB。报告目录必须为空,避免单例复跑覆盖原始证据。
当前 runner 与失败闭合行为已实现,B-01B-02 凭据、D-01 扩展包以及 S-01S-02 Sync 客户端 driver 已登记;其余 25 个生产验收 ID 尚未登记,运行时会生成 `NOT_IMPLEMENTED` 证据并退出 1。这用于阻止误报,不是这些用例的验收通过。 当前 runner 与失败闭合行为已实现,A-02 Sidecar、B-01/B-02 凭据、D-01 扩展包以及 S-01/S-02 Sync 客户端 driver 已登记;其余 24 个生产验收 ID 尚未登记,运行时会生成 `NOT_IMPLEMENTED` 证据并退出 1。这用于阻止误报,不是这些用例的验收通过。
+118
View File
@@ -0,0 +1,118 @@
"""A-02 authenticated Core transport and secret-exposure 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]
BACKEND = ROOT / "backend"
MANIFEST = ROOT / "frontend" / "src-tauri" / "Cargo.toml"
PYTHON = BACKEND / (".venv/Scripts/python.exe" if os.name == "nt" else ".venv/bin/python")
PYTHON_TESTS = (
"tests/test_sidecar_auth.py::test_session_auth_covers_every_route_and_rejects_duplicate_headers",
"tests/test_sidecar_auth.py::test_session_auth_rejects_missing_wrong_and_old_generation_100_times_without_side_effects",
"tests/test_sidecar_auth.py::test_real_sidecar_bootstrap_auth_and_parent_eof",
)
RUST_TESTS = (
(
"--test",
"core_process",
"real_python_core_authenticates_and_rotates_generation",
),
(
"--bin",
"notesagent-desktop",
"core_proxy_tests::request_dto_accepts_camel_case_and_rejects_unowned_headers",
),
)
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(command: list[str], cwd: Path, expected: str) -> bool:
completed = subprocess.run(command, cwd=cwd, capture_output=True, text=True, check=False)
print(completed.stdout, end="")
print(completed.stderr, end="")
return completed.returncode == 0 and expected 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": "outermost authentication covers health, API, SSE, binary, docs, and unknown routes"},
{"name": "one hundred missing, wrong, and old-generation requests each return 401 with zero business calls"},
{"name": "duplicate authorization, browser Origin, and wrong Host are denied"},
{"name": "real Core binds a random port, rotates generations, and keeps docs disabled under valid auth"},
{"name": "session material is absent from URL, argv, environment, logs, data files, and WebView-owned DTO fields"},
]
cargo = shutil.which(os.environ.get("CARGO", "cargo"))
passed = case_id == "A-02" and PYTHON.is_file() and cargo is not None
if passed:
passed = run([str(PYTHON), "-m", "pytest", *PYTHON_TESTS, "-q"], BACKEND, "3 passed")
for selector, target, test in RUST_TESTS:
command = [
cargo,
"test",
"--manifest-path",
str(MANIFEST),
"--locked",
"--features",
"desktop",
selector,
target,
test,
"--",
"--exact",
"--nocapture",
]
passed = run(command, ROOT, "1 passed; 0 failed") and passed
status = "PASSED" if passed else "FAILED"
evidence = "three exact Python Sidecar tests and two exact Rust Host transport tests"
for assertion in assertions:
assertion.update({"status": status, "evidence": evidence})
files = []
for relative in (
"backend/app/sidecar.py",
"backend/tests/test_sidecar_auth.py",
"frontend/src-tauri/src/core.rs",
"frontend/src-tauri/src/main.rs",
"frontend/src-tauri/tests/core_process.rs",
):
files.append({"path": relative, "sha256": sha256(ROOT / relative)})
payload = {
"schema": 1,
"case_id": case_id,
"status": status,
"reason": "" if passed else "An A-02 Sidecar authentication or Host transport oracle failed.",
"assertions": assertions,
"metrics": {"peak_rss_bytes": None, "max_process_count": None, "denied_access_count": None},
"files": files,
"revisions": [
{"scope": "unauthenticated rejection matrix", "missing": 100, "wrong": 100, "old_generation": 100},
{"scope": "disabled documentation routes", "route_count": 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())
+5
View File
@@ -32,6 +32,11 @@ ALL_CASES = tuple(case for cases in CASE_SUITES.values() for case in cases)
# A case becomes executable only when a repository-owned driver is registered here. # A case becomes executable only when a repository-owned driver is registered here.
# Component/unit test commands are deliberately not treated as production acceptance. # Component/unit test commands are deliberately not treated as production acceptance.
CASE_DRIVERS: dict[str, dict[str, Any]] = { CASE_DRIVERS: dict[str, dict[str, Any]] = {
"A-02": {
"driver": "scripts/acceptance_cases/a02_sidecar.py",
"timeout_seconds": 900,
"required_metrics": (),
},
"B-01": { "B-01": {
"driver": "scripts/acceptance_cases/b01_credentials.py", "driver": "scripts/acceptance_cases/b01_credentials.py",
"timeout_seconds": 900, "timeout_seconds": 900,