diff --git a/build.py b/build.py index 07b97c41c..66696000b 100644 --- a/build.py +++ b/build.py @@ -9,6 +9,7 @@ import shutil import subprocess import sys +import tempfile import time from dataclasses import dataclass from pathlib import Path @@ -18,6 +19,179 @@ DIAGNOSTIC_DIR = ROOT / "diagnostic" DIAGNOSTIC_CHUNK_SIZE = 40 * 1024 * 1024 ENCRYPTLY_BLOCKER_MESSAGE = "You need to fix your environment so encryptly runs before building." +DIAGNOSTIC_REDACTED_PATH = "" +DIAGNOSTIC_REDACTED_USER = "" +DIAGNOSTIC_REDACTED_HOST = "" + + +class DiagnosticArtifactError(Exception): + """Raised when diagnostic JSON and .logd artifacts are missing or mismatched.""" + + +def _diagnostic_path_prefixes(root: Path) -> list[str]: + prefixes: list[str] = [] + home = Path.home() + if home: + prefixes.append(str(home)) + prefixes.append(str(root)) + prefixes.append(tempfile.gettempdir()) + for key in ("TEMP", "TMP"): + value = os.environ.get(key) + if value: + prefixes.append(value) + return sorted({prefix for prefix in prefixes if prefix and len(prefix) > 1}, key=len, reverse=True) + + +def redact_diagnostic_text(text: str, root: Path = ROOT) -> str: + """Remove host-specific paths and identifiers from diagnostic text.""" + if not text: + return text + + redacted = text + for prefix in _diagnostic_path_prefixes(root): + redacted = redacted.replace(prefix, DIAGNOSTIC_REDACTED_PATH) + if os.sep != "/": + redacted = redacted.replace(prefix.replace(os.sep, "/"), DIAGNOSTIC_REDACTED_PATH) + if os.sep != "\\": + redacted = redacted.replace(prefix.replace(os.sep, "\\"), DIAGNOSTIC_REDACTED_PATH) + + username = getpass.getuser() + if username and len(username) > 1: + redacted = redacted.replace(username, DIAGNOSTIC_REDACTED_USER) + + hostname = platform.node() + if hostname and len(hostname) > 1: + redacted = redacted.replace(hostname, DIAGNOSTIC_REDACTED_HOST) + + return redacted + + +def diagnostic_repo_relpath(path: Path | str, root: Path = ROOT) -> str: + """Return a repository-relative path using forward slashes.""" + candidate = Path(path) + if candidate.is_absolute(): + try: + candidate = candidate.relative_to(root) + except ValueError: + return redact_diagnostic_text(str(path), root).replace("\\", "/") + return candidate.as_posix() + + +def sanitize_diagnostic_artifact_path(path: Optional[str], root: Path = ROOT) -> Optional[str]: + if path is None: + return None + return diagnostic_repo_relpath(path, root) + + +def normalize_logd_relpaths(logd_relpaths: Optional[list[str]], root: Path = ROOT) -> Optional[list[str]]: + if not logd_relpaths: + return None + return [diagnostic_repo_relpath(path, root) for path in logd_relpaths] + + +def encryptly_failure_message(stderr: str, stdout: str, default: str) -> str: + """Return a safe encryptly error message without leaking stdout passwords.""" + message = stderr.strip() + if message: + return message + + candidate = stdout.strip() + if not candidate: + return default + + # encryptly prints the decrypt password on stdout even when pack fails + if len(candidate) <= 64 and all(char in "0123456789abcdef" for char in candidate.lower()): + return default + + return candidate + + +def validate_diagnostic_artifact_pair( + root: Path, + metadata_path: Path, + *, + require_logd: bool = True, +) -> dict: + """Validate diagnostic JSON metadata and its referenced .logd artifact(s).""" + if not metadata_path.exists(): + raise DiagnosticArtifactError( + f"Diagnostic metadata missing: {diagnostic_repo_relpath(metadata_path, root)}" + ) + + try: + report = json.loads(metadata_path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise DiagnosticArtifactError( + f"Diagnostic metadata is not valid JSON: {diagnostic_repo_relpath(metadata_path, root)}" + ) from exc + + commit_id = report.get("commit") + if not commit_id: + raise DiagnosticArtifactError("Diagnostic metadata is missing commit id") + + expected_stem = f"build-{commit_id}" + if metadata_path.stem != expected_stem: + raise DiagnosticArtifactError( + f"Metadata filename {metadata_path.name} does not match commit id {commit_id}" + ) + + logd_ref = report.get("diagnostic_logd") + if logd_ref is None: + if require_logd and not report.get("diagnostic_logd_error"): + raise DiagnosticArtifactError( + "diagnostic_logd is null but no diagnostic_logd_error was recorded" + ) + return report + + refs = logd_ref if isinstance(logd_ref, list) else [logd_ref] + for ref in refs: + if not isinstance(ref, str): + raise DiagnosticArtifactError( + f"diagnostic_logd entry must be a string, got {type(ref).__name__}" + ) + if "\\" in ref: + raise DiagnosticArtifactError( + f"diagnostic_logd must use repository-relative forward-slash paths, got: {ref}" + ) + if Path(ref).is_absolute() or (len(ref) > 1 and ref[1] == ":"): + raise DiagnosticArtifactError( + f"diagnostic_logd must be repository-relative, got: {ref}" + ) + if not ref.startswith("diagnostic/"): + raise DiagnosticArtifactError( + f"diagnostic_logd must live under diagnostic/, got: {ref}" + ) + + logd_path = root / ref + if logd_path.stem != expected_stem and not logd_path.stem.startswith(f"{expected_stem}-part"): + raise DiagnosticArtifactError( + f"diagnostic_logd {ref} does not pair with metadata commit {commit_id}" + ) + if not logd_path.exists(): + raise DiagnosticArtifactError(f"Referenced diagnostic .logd artifact missing: {ref}") + if logd_path.stat().st_size == 0: + raise DiagnosticArtifactError(f"Referenced diagnostic .logd artifact is empty: {ref}") + + return report + + +def validate_diagnostic_metadata(metadata_path: Path, root: Path = ROOT) -> list[str]: + """Validate diagnostic metadata and return human-readable errors.""" + try: + validate_diagnostic_artifact_pair(root, metadata_path) + except DiagnosticArtifactError as exc: + return [str(exc)] + + report = json.loads(metadata_path.read_text(encoding="utf-8")) + errors: list[str] = [] + report_text = json.dumps(report) + for token in _diagnostic_path_prefixes(root): + if token and token in report_text: + errors.append(f"diagnostic metadata leaks local path `{token}`") + for token in (getpass.getuser(), platform.node()): + if token and len(token) > 1 and token in report_text: + errors.append(f"diagnostic metadata leaks local identifier `{token}`") + return errors def current_commit_id() -> str: @@ -251,7 +425,11 @@ def check_encryptly_runs(timeout: int = 60) -> tuple[bool, str]: timeout=timeout, ) if result.returncode != 0: - output = result.stderr.strip() or result.stdout.strip() or "encryptly pack preflight failed" + output = encryptly_failure_message( + result.stderr, + result.stdout, + "encryptly pack preflight failed", + ) return False, output if not logd_path.exists(): return False, "encryptly preflight completed without creating a .logd" @@ -445,8 +623,8 @@ def collect_system_info() -> str: "Tent of Trials - System Diagnostic Snapshot", "=" * 50, f"generated_at: {datetime.datetime.now(datetime.timezone.utc).isoformat()}", - f"hostname: {platform.node()}", - f"user: {getpass.getuser()}", + f"hostname: {DIAGNOSTIC_REDACTED_HOST}", + f"user: {DIAGNOSTIC_REDACTED_USER}", f"python: {sys.version}", f"platform: {platform.platform()}", f"processor: {platform.processor() or 'unknown'}", @@ -455,27 +633,31 @@ def collect_system_info() -> str: "--- uname ---", ] ok, out = run_cmd(["uname", "-a"]) - lines.append(out if ok else "unavailable") + lines.append(redact_diagnostic_text(out if ok else "unavailable")) lines.extend(["", "--- /etc/os-release ---"]) try: - lines.append((Path("/etc/os-release")).read_text(encoding="utf-8", errors="replace").strip()) + lines.append( + redact_diagnostic_text( + (Path("/etc/os-release")).read_text(encoding="utf-8", errors="replace").strip() + ) + ) except Exception as e: lines.append(f"unavailable: {e}") lines.extend(["", "--- memory ---"]) ok, out = run_cmd(["free", "-h"]) - lines.append(out if ok else "unavailable") + lines.append(redact_diagnostic_text(out if ok else "unavailable")) lines.extend(["", "--- disk ---"]) ok, out = run_cmd(["df", "-h"]) - lines.append(out if ok else "unavailable") + lines.append(redact_diagnostic_text(out if ok else "unavailable")) lines.extend(["", "--- build environment ---"]) for key in ["SHELL", "LANG", "TERM", "XDG_SESSION_TYPE", "DISPLAY", "EDITOR"]: value = os.environ.get(key) if value: - lines.append(f"{key}={value}") + lines.append(f"{key}={redact_diagnostic_text(value)}") lines.append("") return "\n".join(lines) @@ -490,6 +672,8 @@ def build_diagnostic_report( chunked: bool = False, message_blocker: Optional[str] = None, ) -> dict: + logd_relpaths = normalize_logd_relpaths(logd_relpaths) + diagnostic_logd: Optional[str | list[str]] if not logd_relpaths: diagnostic_logd = None @@ -500,13 +684,13 @@ def build_diagnostic_report( decrypt_target = logd_relpaths[0] if logd_relpaths and len(logd_relpaths) == 1 else None if logd_relpaths and len(logd_relpaths) > 1: - decrypt_target = str((DIAGNOSTIC_DIR / f"build-{commit_id}.logd").relative_to(ROOT)) + decrypt_target = diagnostic_repo_relpath(DIAGNOSTIC_DIR / f"build-{commit_id}.logd") report = { "generated_at": datetime.datetime.now(datetime.timezone.utc).isoformat(), "commit": commit_id, "diagnostic_logd": diagnostic_logd, - "diagnostic_logd_error": logd_error, + "diagnostic_logd_error": redact_diagnostic_text(logd_error) if logd_error else None, "message_blocker": message_blocker, "chunked": chunked, "chunk_size_bytes": DIAGNOSTIC_CHUNK_SIZE if chunked else None, @@ -523,8 +707,8 @@ def build_diagnostic_report( "name": name, "status": "PASS" if success else "FAIL", "elapsed_seconds": round(elapsed, 3), - "artifact": binary, - "output": output, + "artifact": sanitize_diagnostic_artifact_path(binary), + "output": redact_diagnostic_text(output), } for name, success, elapsed, output, binary in results ], @@ -539,7 +723,11 @@ def build_diagnostic_report( def write_diagnostic_report(metadata_path: Path, report: dict) -> None: metadata_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") - print(f" {color('✓', Colors.GREEN)} {metadata_path.relative_to(ROOT)} created") + try: + display_path = metadata_path.relative_to(ROOT) + except ValueError: + display_path = metadata_path + print(f" {color('✓', Colors.GREEN)} {display_path} created") def commit_diagnostic_artifacts(paths: list[Path], commit_id: str) -> bool: @@ -645,9 +833,11 @@ def generate_logd( "module results:", ] for name, success, elapsed, _, binary in results: + artifact_note = "" + if binary: + artifact_note = f" [{sanitize_diagnostic_artifact_path(binary)}]" summary_lines.append( - f" {name}: {'PASS' if success else 'FAIL'} ({elapsed:.2f}s)" - f"{f' [{binary}]' if binary else ''}" + f" {name}: {'PASS' if success else 'FAIL'} ({elapsed:.2f}s){artifact_note}" ) (safe_dir / "build-summary.txt").write_text( "\n".join(summary_lines), encoding="utf-8" @@ -660,9 +850,9 @@ def generate_logd( f"{'=' * 50}" ) if binary: - log_lines.append(f"artifact: {binary}") + log_lines.append(f"artifact: {sanitize_diagnostic_artifact_path(binary)}") if output: - log_lines.append(output) + log_lines.append(redact_diagnostic_text(output)) (safe_dir / "build.log").write_text("\n".join(log_lines), encoding="utf-8") sr = subprocess.run( @@ -681,7 +871,11 @@ def generate_logd( timeout=300, ) if sr.returncode != 0: - error = sr.stderr.strip() or sr.stdout.strip() or "encryptly pack failed" + error = encryptly_failure_message( + sr.stderr, + sr.stdout, + "encryptly pack failed", + ) print( f" {color('✗', Colors.RED)} {logd_path.relative_to(ROOT)} creation failed: " f"{error}" @@ -703,8 +897,12 @@ def generate_logd( safe_pw = sr.stdout.strip() logd_files = split_diagnostic_logd(logd_path) - logd_relpaths = [str(path.relative_to(ROOT)) for path in logd_files] - decrypt_target = logd_relpaths[0] if len(logd_relpaths) == 1 else str(logd_path.relative_to(ROOT)) + logd_relpaths = [diagnostic_repo_relpath(path) for path in logd_files] + decrypt_target = ( + logd_relpaths[0] + if len(logd_relpaths) == 1 + else diagnostic_repo_relpath(logd_path) + ) write_diagnostic_report( metadata_path, build_diagnostic_report( diff --git a/diagnostic/build-6b3111c3.json b/diagnostic/build-6b3111c3.json new file mode 100644 index 000000000..3d740a95c --- /dev/null +++ b/diagnostic/build-6b3111c3.json @@ -0,0 +1,87 @@ +{ + "generated_at": "2026-06-19T17:05:08.587063+00:00", + "commit": "6b3111c3", + "diagnostic_logd": "diagnostic/build-6b3111c3.logd", + "diagnostic_logd_error": null, + "message_blocker": null, + "chunked": false, + "chunk_size_bytes": null, + "password": "52f3f4426e3817fa432c", + "decrypt_command": "encryptly unpack diagnostic/build-6b3111c3.logd --password 52f3f4426e3817fa432c", + "total_modules": 10, + "passed": 6, + "failed": 4, + "modules": [ + { + "name": "backend", + "status": "PASS", + "elapsed_seconds": 0.203, + "artifact": "backend/target", + "output": "\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused import: `warn`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/ai/embeddings.rs:28:28\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m28\u001b[0m \u001b[1m\u001b[94m|\u001b[0m use tracing::{debug, info, warn};\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^\u001b[0m\n \u001b[1m\u001b[94m|\u001b[0m\n \u001b[1m\u001b[94m= \u001b[0m\u001b[1mnote\u001b[0m: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused import: `error`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/ai/inference.rs:25:22\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m25\u001b[0m \u001b[1m\u001b[94m|\u001b[0m use tracing::{debug, error, info, warn};\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused imports: `error` and `warn`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/ai/mod.rs:40:22\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m40\u001b[0m \u001b[1m\u001b[94m|\u001b[0m use tracing::{debug, error, info, warn};\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^\u001b[0m \u001b[1m\u001b[33m^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused imports: `c_int` and `c_uint`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/connector/bridge.rs:38:20\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m38\u001b[0m \u001b[1m\u001b[94m|\u001b[0m use std::os::raw::{c_int, c_uint, c_ulong};\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^\u001b[0m \u001b[1m\u001b[33m^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused import: `std::ffi::CString`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/connector/legacy.rs:35:5\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m35\u001b[0m \u001b[1m\u001b[94m|\u001b[0m use std::ffi::CString;\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused import: `c_char`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/connector/legacy.rs:36:20\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m36\u001b[0m \u001b[1m\u001b[94m|\u001b[0m use std::os::raw::{c_char, c_ulong};\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused import: `CStr`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/connector/types.rs:27:16\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m27\u001b[0m \u001b[1m\u001b[94m|\u001b[0m use std::ffi::{CStr, CString};\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused imports: `c_double` and `c_long`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/connector/types.rs:29:28\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m29\u001b[0m \u001b[1m\u001b[94m|\u001b[0m use std::os::raw::{c_char, c_double, c_int, c_uint, c_void, c_long, c_ulong};\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^\u001b[0m \u001b[1m\u001b[33m^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused import: `AtomicBool`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/legacy/deprecations.rs:14:25\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m14\u001b[0m \u001b[1m\u001b[94m|\u001b[0m use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused imports: `EntityKind` and `legacy_normalize_phone_number`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/legacy/v1_compat.rs:8:47\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m8\u001b[0m \u001b[1m\u001b[94m|\u001b[0m use crate::legacy::deprecations::{LegacyUuid, EntityKind, LegacyPagination, legacy_normalize_phone_number};\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused import: `super::ProtocolError`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/protocol/validate.rs:27:5\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m27\u001b[0m \u001b[1m\u001b[94m|\u001b[0m use super::ProtocolError;\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused import: `MAX_MESSAGE_SIZE`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/protocol/codec.rs:25:38\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m25\u001b[0m \u001b[1m\u001b[94m|\u001b[0m use crate::protocol::{ProtocolError, MAX_MESSAGE_SIZE, MIN_COMPATIBLE_VERSION, PROTOCOL_VERSION};\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused import: `Write`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/protocol/codec.rs:26:29\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m26\u001b[0m \u001b[1m\u001b[94m|\u001b[0m use std::io::{Cursor, Read, Write};\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused import: `Ordering`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/protocol/rpc.rs:25:36\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m25\u001b[0m \u001b[1m\u001b[94m|\u001b[0m use std::sync::atomic::{AtomicU64, Ordering};\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused imports: `Duration` and `Instant`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/protocol/rpc.rs:27:17\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m27\u001b[0m \u001b[1m\u001b[94m|\u001b[0m use std::time::{Duration, Instant};\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^\u001b[0m \u001b[1m\u001b[33m^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused imports: `Deserialize` and `Serialize`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/protocol/rpc.rs:28:13\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m28\u001b[0m \u001b[1m\u001b[94m|\u001b[0m use serde::{Deserialize, Serialize};\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused import: `MAX_MESSAGE_SIZE`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/protocol/rpc.rs:31:28\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m31\u001b[0m \u001b[1m\u001b[94m|\u001b[0m use super::{ProtocolError, MAX_MESSAGE_SIZE, DEFAULT_TIMEOUT_MS};\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused imports: `FrameDecoder` and `FrameEncoder`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/protocol/rpc.rs:32:27\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m32\u001b[0m \u001b[1m\u001b[94m|\u001b[0m use super::codec::{Frame, FrameEncoder, FrameDecoder, FLAG_REQUIRES_ACK};\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: use of deprecated unit variant `legacy::deprecations::EntityKind::Team`: Teams are now Organizations. Use Organization instead.\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/legacy/deprecations.rs:244:25\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m244\u001b[0m \u001b[1m\u001b[94m|\u001b[0m EntityKind::Team => \"org\", // Legacy mapping\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^\u001b[0m\n \u001b[1m\u001b[94m|\u001b[0m\n \u001b[1m\u001b[94m= \u001b[0m\u001b[1mnote\u001b[0m: `#[warn(deprecated)]` on by default\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: use of deprecated unit variant `legacy::deprecations::EntityKind::Project`: Projects were removed in the Platform v2 migration\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/legacy/deprecations.rs:245:25\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m245\u001b[0m \u001b[1m\u001b[94m|\u001b[0m EntityKind::Project => \"workspace\", // Legacy mapping\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: use of deprecated unit variant `legacy::deprecations::EntityKind::Team`: Teams are now Organizations. Use Organization instead.\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/legacy/deprecations.rs:266:25\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m266\u001b[0m \u001b[1m\u001b[94m|\u001b[0m EntityKind::Team\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: use of deprecated unit variant `legacy::deprecations::EntityKind::Project`: Projects were removed in the Platform v2 migration\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/legacy/deprecations.rs:267:31\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m267\u001b[0m \u001b[1m\u001b[94m|\u001b[0m | EntityKind::Project\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: variable does not need to be mutable\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/connector/bridge.rs:317:13\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m317\u001b[0m \u001b[1m\u001b[94m|\u001b[0m let mut buffer = unsafe { &mut *c_buffer };\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[94m----\u001b[0m\u001b[1m\u001b[33m^^^^^^\u001b[0m\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[94m|\u001b[0m\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[94mhelp: remove this `mut`\u001b[0m\n \u001b[1m\u001b[94m|\u001b[0m\n \u001b[1m\u001b[94m= \u001b[0m\u001b[1mnote\u001b[0m: `#[warn(unused_mut)]` (part of `#[warn(unused)]`) on by default\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused variable: `initialized`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/connector/bridge.rs:440:13\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m440\u001b[0m \u001b[1m\u001b[94m|\u001b[0m let initialized = Arc::new(AtomicBool::new(true));\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^\u001b[0m \u001b[1m\u001b[33mhelp: if this is intentional, prefix it with an underscore: `_initialized`\u001b[0m\n \u001b[1m\u001b[94m|\u001b[0m\n \u001b[1m\u001b[94m= \u001b[0m\u001b[1mnote\u001b[0m: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: variable does not need to be mutable\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/connector/legacy.rs:267:13\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m267\u001b[0m \u001b[1m\u001b[94m|\u001b[0m let mut buffer = unsafe { &mut *c_buffer };\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[94m----\u001b[0m\u001b[1m\u001b[33m^^^^^^\u001b[0m\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[94m|\u001b[0m\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[94mhelp: remove this `mut`\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused variable: `value`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/legacy/deprecations.rs:508:15\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m508\u001b[0m \u001b[1m\u001b[94m|\u001b[0m for (key, value) in configs {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^\u001b[0m \u001b[1m\u001b[33mhelp: if this is intentional, prefix it with an underscore: `_value`\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: unused variable: `obj`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/protocol/validate.rs:282:25\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m282\u001b[0m \u001b[1m\u001b[94m|\u001b[0m if let Some(obj) = value.as_object() {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^\u001b[0m \u001b[1m\u001b[33mhelp: if this is intentional, prefix it with an underscore: `_obj`\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: type `BridgeStats` is more private than the item `ConnectorBridge::stats`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/connector/bridge.rs:415:5\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m415\u001b[0m \u001b[1m\u001b[94m|\u001b[0m pub fn stats(&self) -> BridgeStats {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m \u001b[1m\u001b[33mmethod `ConnectorBridge::stats` is reachable at visibility `pub`\u001b[0m\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[92mnote\u001b[0m: but type `BridgeStats` is only usable at visibility `pub(self)`\n \u001b[1m\u001b[94m--> \u001b[0msrc/connector/bridge.rs:225:1\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m225\u001b[0m \u001b[1m\u001b[94m|\u001b[0m struct BridgeStats {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[92m^^^^^^^^^^^^^^^^^^\u001b[0m\n \u001b[1m\u001b[94m= \u001b[0m\u001b[1mnote\u001b[0m: `#[warn(private_interfaces)]` on by default\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: type `CircuitState` is more private than the item `ConnectorBridge::circuit_state`\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/connector/bridge.rs:423:5\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m423\u001b[0m \u001b[1m\u001b[94m|\u001b[0m pub fn circuit_state(&self) -> CircuitState {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m \u001b[1m\u001b[33mmethod `ConnectorBridge::circuit_state` is reachable at visibility `pub`\u001b[0m\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[92mnote\u001b[0m: but type `CircuitState` is only usable at visibility `pub(self)`\n \u001b[1m\u001b[94m--> \u001b[0msrc/connector/bridge.rs:79:1\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m 79\u001b[0m \u001b[1m\u001b[94m|\u001b[0m enum CircuitState {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[92m^^^^^^^^^^^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: constant `NCP_TEMPERATURE` is never used\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/ai/mod.rs:53:7\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m53\u001b[0m \u001b[1m\u001b[94m|\u001b[0m const NCP_TEMPERATURE: f64 = 0.42;\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^\u001b[0m\n \u001b[1m\u001b[94m|\u001b[0m\n \u001b[1m\u001b[94m= \u001b[0m\u001b[1mnote\u001b[0m: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: constant `MIN_CONFIDENCE_THRESHOLD` is never used\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/ai/mod.rs:61:7\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m61\u001b[0m \u001b[1m\u001b[94m|\u001b[0m const MIN_CONFIDENCE_THRESHOLD: f64 = 0.65;\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: constant `MAX_INFERENCE_RETRIES` is never used\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/ai/mod.rs:65:7\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m65\u001b[0m \u001b[1m\u001b[94m|\u001b[0m const MAX_INFERENCE_RETRIES: u32 = 5;\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: fields `discovery`, `broker`, and `registry` are never read\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/ai/mod.rs:173:5\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m171\u001b[0m \u001b[1m\u001b[94m|\u001b[0m pub struct AiOrchestrator {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[94m--------------\u001b[0m \u001b[1m\u001b[94mfields in this struct\u001b[0m\n\u001b[1m\u001b[94m172\u001b[0m \u001b[1m\u001b[94m|\u001b[0m /// Reference to the service discovery subsystem\n\u001b[1m\u001b[94m173\u001b[0m \u001b[1m\u001b[94m|\u001b[0m discovery: Arc>,\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^\u001b[0m\n\u001b[1m\u001b[94m174\u001b[0m \u001b[1m\u001b[94m|\u001b[0m /// Reference to the message broker subsystem\n\u001b[1m\u001b[94m175\u001b[0m \u001b[1m\u001b[94m|\u001b[0m broker: Arc>,\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^\u001b[0m\n\u001b[1m\u001b[94m176\u001b[0m \u001b[1m\u001b[94m|\u001b[0m /// Reference to the service registry subsystem\n\u001b[1m\u001b[94m177\u001b[0m \u001b[1m\u001b[94m|\u001b[0m registry: Arc>,\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: field `window_start` is never read\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/ai/embeddings.rs:661:5\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m658\u001b[0m \u001b[1m\u001b[94m|\u001b[0m pub struct ContextWindowManager {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[94m--------------------\u001b[0m \u001b[1m\u001b[94mfield in this struct\u001b[0m\n\u001b[1m\u001b[94m...\u001b[0m\n\u001b[1m\u001b[94m661\u001b[0m \u001b[1m\u001b[94m|\u001b[0m window_start: Instant,\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: constant `MAX_RETRIES` is never used\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/ai/inference.rs:41:7\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m41\u001b[0m \u001b[1m\u001b[94m|\u001b[0m const MAX_RETRIES: u32 = 3;\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: constant `RETRY_BASE_DELAY_MS` is never used\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/ai/inference.rs:44:7\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m44\u001b[0m \u001b[1m\u001b[94m|\u001b[0m const RETRY_BASE_DELAY_MS: u64 = 1000;\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: fields `api_key`, `base_url`, and `client` are never read\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/ai/inference.rs:453:5\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m452\u001b[0m \u001b[1m\u001b[94m|\u001b[0m pub struct AnthropicClient {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[94m---------------\u001b[0m \u001b[1m\u001b[94mfields in this struct\u001b[0m\n\u001b[1m\u001b[94m453\u001b[0m \u001b[1m\u001b[94m|\u001b[0m api_key: String,\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^\u001b[0m\n\u001b[1m\u001b[94m454\u001b[0m \u001b[1m\u001b[94m|\u001b[0m base_url: String,\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^\u001b[0m\n\u001b[1m\u001b[94m455\u001b[0m \u001b[1m\u001b[94m|\u001b[0m models: Vec,\n\u001b[1m\u001b[94m456\u001b[0m \u001b[1m\u001b[94m|\u001b[0m client: reqwest::Client,\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^\u001b[0m\n \u001b[1m\u001b[94m|\u001b[0m\n \u001b[1m\u001b[94m= \u001b[0m\u001b[1mnote\u001b[0m: `AnthropicClient` has a derived impl for the trait `Debug`, but this is intentionally ignored during dead code analysis\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: field `routing_table` is never read\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/ai/inference.rs:719:5\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m716\u001b[0m \u001b[1m\u001b[94m|\u001b[0m pub struct ModelRouter {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[94m-----------\u001b[0m \u001b[1m\u001b[94mfield in this struct\u001b[0m\n\u001b[1m\u001b[94m...\u001b[0m\n\u001b[1m\u001b[94m719\u001b[0m \u001b[1m\u001b[94m|\u001b[0m routing_table: RwLock>,\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^\u001b[0m\n \u001b[1m\u001b[94m|\u001b[0m\n \u001b[1m\u001b[94m= \u001b[0m\u001b[1mnote\u001b[0m: `ModelRouter` has a derived impl for the trait `Debug`, but this is intentionally ignored during dead code analysis\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: field `cost_history` is never read\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/ai/inference.rs:951:5\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m947\u001b[0m \u001b[1m\u001b[94m|\u001b[0m pub struct TokenCounter {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[94m------------\u001b[0m \u001b[1m\u001b[94mfield in this struct\u001b[0m\n\u001b[1m\u001b[94m...\u001b[0m\n\u001b[1m\u001b[94m951\u001b[0m \u001b[1m\u001b[94m|\u001b[0m cost_history: RwLock>,\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: constant `HEALTH_CHECK_TIMEOUT_MS` is never used\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/connector/bridge.rs:64:7\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m64\u001b[0m \u001b[1m\u001b[94m|\u001b[0m const HEALTH_CHECK_TIMEOUT_MS: u64 = 1000;\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: field `id` is never read\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/connector/bridge.rs:152:5\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m151\u001b[0m \u001b[1m\u001b[94m|\u001b[0m struct PoolEntry {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[94m---------\u001b[0m \u001b[1m\u001b[94mfield in this struct\u001b[0m\n\u001b[1m\u001b[94m152\u001b[0m \u001b[1m\u001b[94m|\u001b[0m id: usize,\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: method `stats` is never used\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/connector/bridge.rs:195:8\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m163\u001b[0m \u001b[1m\u001b[94m|\u001b[0m impl ConnectionPool {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[94m-------------------\u001b[0m \u001b[1m\u001b[94mmethod in this implementation\u001b[0m\n\u001b[1m\u001b[94m...\u001b[0m\n\u001b[1m\u001b[94m195\u001b[0m \u001b[1m\u001b[94m|\u001b[0m fn stats(&self) -> PoolStats {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: struct `PoolStats` is never constructed\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/connector/bridge.rs:203:8\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m203\u001b[0m \u001b[1m\u001b[94m|\u001b[0m struct PoolStats {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: fields `circuit_breaker_trips` and `health_check_failures` are never read\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/connector/bridge.rs:229:5\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m225\u001b[0m \u001b[1m\u001b[94m|\u001b[0m struct BridgeStats {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[94m-----------\u001b[0m \u001b[1m\u001b[94mfields in this struct\u001b[0m\n\u001b[1m\u001b[94m...\u001b[0m\n\u001b[1m\u001b[94m229\u001b[0m \u001b[1m\u001b[94m|\u001b[0m circuit_breaker_trips: u64,\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n\u001b[1m\u001b[94m230\u001b[0m \u001b[1m\u001b[94m|\u001b[0m health_check_failures: u64,\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^^^^^^^\u001b[0m\n \u001b[1m\u001b[94m|\u001b[0m\n \u001b[1m\u001b[94m= \u001b[0m\u001b[1mnote\u001b[0m: `BridgeStats` has derived impls for the traits `Clone` and `Debug`, but these are intentionally ignored during dead code analysis\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: field `consumers` is never read\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/messaging/mod.rs:38:5\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m35\u001b[0m \u001b[1m\u001b[94m|\u001b[0m pub struct MessageBroker {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[94m-------------\u001b[0m \u001b[1m\u001b[94mfield in this struct\u001b[0m\n\u001b[1m\u001b[94m...\u001b[0m\n\u001b[1m\u001b[94m38\u001b[0m \u001b[1m\u001b[94m|\u001b[0m consumers: DashMap>>,\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: field `version` is never read\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/protocol/messages.rs:293:5\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m291\u001b[0m \u001b[1m\u001b[94m|\u001b[0m pub struct MessageRegistry {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[94m---------------\u001b[0m \u001b[1m\u001b[94mfield in this struct\u001b[0m\n\u001b[1m\u001b[94m292\u001b[0m \u001b[1m\u001b[94m|\u001b[0m handlers: HashMap,\n\u001b[1m\u001b[94m293\u001b[0m \u001b[1m\u001b[94m|\u001b[0m version: u32,\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: field `version` is never read\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/protocol/serialize.rs:258:5\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m255\u001b[0m \u001b[1m\u001b[94m|\u001b[0m struct Schema {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[94m------\u001b[0m \u001b[1m\u001b[94mfield in this struct\u001b[0m\n\u001b[1m\u001b[94m...\u001b[0m\n\u001b[1m\u001b[94m258\u001b[0m \u001b[1m\u001b[94m|\u001b[0m version: u32,\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: fields `required` and `default_value` are never read\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/protocol/serialize.rs:264:5\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m261\u001b[0m \u001b[1m\u001b[94m|\u001b[0m struct SchemaField {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[94m-----------\u001b[0m \u001b[1m\u001b[94mfields in this struct\u001b[0m\n\u001b[1m\u001b[94m...\u001b[0m\n\u001b[1m\u001b[94m264\u001b[0m \u001b[1m\u001b[94m|\u001b[0m required: bool,\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^\u001b[0m\n\u001b[1m\u001b[94m265\u001b[0m \u001b[1m\u001b[94m|\u001b[0m default_value: Option,\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: variant `Custom` is never constructed\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/protocol/serialize.rs:276:5\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m269\u001b[0m \u001b[1m\u001b[94m|\u001b[0m enum FieldValidation {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[94m---------------\u001b[0m \u001b[1m\u001b[94mvariant in this enum\u001b[0m\n\u001b[1m\u001b[94m...\u001b[0m\n\u001b[1m\u001b[94m276\u001b[0m \u001b[1m\u001b[94m|\u001b[0m Custom(String),\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: fields `next_request_id`, `pending_requests`, `serializer`, and `timeout_ms` are never read\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/protocol/rpc.rs:218:5\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m217\u001b[0m \u001b[1m\u001b[94m|\u001b[0m pub struct RpcClient {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[94m---------\u001b[0m \u001b[1m\u001b[94mfields in this struct\u001b[0m\n\u001b[1m\u001b[94m218\u001b[0m \u001b[1m\u001b[94m|\u001b[0m next_request_id: AtomicU64,\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^\u001b[0m\n\u001b[1m\u001b[94m219\u001b[0m \u001b[1m\u001b[94m|\u001b[0m pending_requests: Arc, RpcError>>>>>,\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^^^^^^^\u001b[0m\n\u001b[1m\u001b[94m220\u001b[0m \u001b[1m\u001b[94m|\u001b[0m serializer: Serializer,\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^\u001b[0m\n\u001b[1m\u001b[94m221\u001b[0m \u001b[1m\u001b[94m|\u001b[0m timeout_ms: u64,\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: field `serializer` is never read\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/protocol/rpc.rs:271:5\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m269\u001b[0m \u001b[1m\u001b[94m|\u001b[0m pub struct RpcServer {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[94m---------\u001b[0m \u001b[1m\u001b[94mfield in this struct\u001b[0m\n\u001b[1m\u001b[94m270\u001b[0m \u001b[1m\u001b[94m|\u001b[0m handlers: HashMap,\n\u001b[1m\u001b[94m271\u001b[0m \u001b[1m\u001b[94m|\u001b[0m serializer: Serializer,\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m\u001b[1m: field `events` is never read\u001b[0m\n \u001b[1m\u001b[94m--> \u001b[0msrc/registry/mod.rs:31:5\n \u001b[1m\u001b[94m|\u001b[0m\n\u001b[1m\u001b[94m28\u001b[0m \u001b[1m\u001b[94m|\u001b[0m pub struct ServiceRegistry {\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[94m---------------\u001b[0m \u001b[1m\u001b[94mfield in this struct\u001b[0m\n\u001b[1m\u001b[94m...\u001b[0m\n\u001b[1m\u001b[94m31\u001b[0m \u001b[1m\u001b[94m|\u001b[0m events: Arc>>,\n \u001b[1m\u001b[94m|\u001b[0m \u001b[1m\u001b[33m^^^^^^\u001b[0m\n\n\u001b[1m\u001b[33mwarning\u001b[0m: `tent-backend` (lib) generated 52 warnings (run `cargo fix --lib -p tent-backend` to apply 23 suggestions)\n\u001b[1m\u001b[92m Finished\u001b[0m \u001b]8;;https://doc.rust-lang.org/cargo/reference/profiles.html#default-profiles\u001b\\`dev` profile [unoptimized + debuginfo]\u001b]8;;\u001b\\ target(s) in 0.17s" + }, + { + "name": "frontend", + "status": "PASS", + "elapsed_seconds": 4.704, + "artifact": "frontend/dist", + "output": "> tent-frontend@0.0.0 build\n> tsc -b && vite build\n\nvite v6.4.3 building for production...\ntransforming...\n\u2713 100 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/index.html 0.62 kB \u2502 gzip: 0.34 kB\ndist/assets/state-BkjSKDbY.js 8.91 kB \u2502 gzip: 3.54 kB \u2502 map: 57.15 kB\ndist/assets/vendor-CREcWLHI.js 48.93 kB \u2502 gzip: 17.25 kB \u2502 map: 481.27 kB\ndist/assets/index-CyxcoTyU.js 231.32 kB \u2502 gzip: 72.16 kB \u2502 map: 1,044.42 kB\n\u2713 built in 1.54s" + }, + { + "name": "market", + "status": "FAIL", + "elapsed_seconds": 1.62, + "artifact": null, + "output": "go: downloading go.uber.org/zap v1.27.0\ngo: downloading github.com/shopspring/decimal v1.4.0\ngo: downloading github.com/gorilla/websocket v1.5.3\ngo: downloading github.com/google/uuid v1.6.0\ngo: downloading go.uber.org/multierr v1.10.0\n# go.uber.org/multierr\n../../../go/pkg/mod/go.uber.org/multierr@v1.10.0/error.go:224:20: undefined: atomic.Bool\nnote: module requires Go 1.19\n# github.com/tent-of-trials/market/matching\nmatching/engine.go:26:20: undefined: atomic.Int64\nnote: module requires Go 1.26" + }, + { + "name": "frailbox", + "status": "FAIL", + "elapsed_seconds": 0.04, + "artifact": null, + "output": "gcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude -MMD -MP -c src/arena.c -o build/src/arena.o\n: warning: \"_FORTIFY_SOURCE\" redefined\n: note: this is the location of the previous definition\nsrc/arena.c: In function \u2018region_alloc\u2019:\nsrc/arena.c:13:36: error: \u2018MAP_ANONYMOUS\u2019 undeclared (first use in this function)\n 13 | int mmap_flags = MAP_PRIVATE | MAP_ANONYMOUS;\n | ^~~~~~~~~~~~~\nsrc/arena.c:13:36: note: each undeclared identifier is reported only once for each function it appears in\nsrc/arena.c:17:23: error: \u2018MAP_HUGETLB\u2019 undeclared (first use in this function)\n 17 | mmap_flags |= MAP_HUGETLB;\n | ^~~~~~~~~~~\nsrc/arena.c: In function \u2018arena_contains\u2019:\nsrc/arena.c:179:17: warning: comparison of distinct pointer types lacks a cast\n 179 | ptr < (char *)region->start + region->size) {\n | ^\nmake: *** [Makefile:27: build/src/arena.o] Error 1" + }, + { + "name": "engine", + "status": "FAIL", + "elapsed_seconds": 0.016, + "artifact": null, + "output": "CMake configure failed:\n-- Configuring incomplete, errors occurred!\nCMake Error at CMakeLists.txt:1 (cmake_minimum_required):\n CMake 3.28 or higher is required. You are running version 3.22.1" + }, + { + "name": "compliance", + "status": "PASS", + "elapsed_seconds": 0.881, + "artifact": "compliance/build", + "output": "Note: ComplianceAuditor.java uses or overrides a deprecated API.\nNote: Recompile with -Xlint:deprecation for details." + }, + { + "name": "v2-market-stream", + "status": "PASS", + "elapsed_seconds": 0.014, + "artifact": null, + "output": "Syntax OK" + }, + { + "name": "nfc-scanner", + "status": "PASS", + "elapsed_seconds": 0.002, + "artifact": null, + "output": "" + }, + { + "name": "openapi-haskell", + "status": "FAIL", + "elapsed_seconds": 0.294, + "artifact": null, + "output": "[1 of 8] Compiling Network.HTTP.Types ( Network/HTTP/Types.hs, nothing )\n[2 of 8] Compiling Network.Wai ( Network/Wai.hs, nothing )\n[3 of 8] Compiling Network.Wai.Handler.Warp ( Network/Wai/Handler/Warp.hs, nothing )\n[4 of 8] Compiling Network.Wai.Logger ( Network/Wai/Logger.hs, nothing )\n[5 of 8] Compiling Tent.OpenAPI.Types ( Types.hs, /ghc29039_0/ghc_6.o )\nTypes.hs:50:1: error:\n Could not find module \u2018Data.Aeson\u2019\n Perhaps you meant Data.Version (from base-4.13.0.0)\n Use -v (or `:set -v` in ghci) to see a list of the files searched for.\n |\n50 | import Data.Aeson (FromJSON(parseJSON), ToJSON(toJSON), Value(Object), (.!=), (.:?), (.=))\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nTypes.hs:51:1: error:\n Could not find module \u2018Data.Aeson.Types\u2019\n Use -v (or `:set -v` in ghci) to see a list of the files searched for.\n |\n51 | import Data.Aeson.Types (Parser, parseMaybe)\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nTypes.hs:71:1: error:\n Could not find module \u2018Data.Aeson\u2019\n Perhaps you meant Data.Version (from base-4.13.0.0)\n Use -v (or `:set -v` in ghci) to see a list of the files searched for.\n |\n71 | import qualified Data.Aeson as A\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nTypes.hs:72:1: error:\n Could not find module \u2018Data.Aeson.Key\u2019\n Use -v (or `:set -v` in ghci) to see a list of the files searched for.\n |\n72 | import qualified Data.Aeson.Key as K\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nTypes.hs:73:1: error:\n Could not find module \u2018Data.Aeson.KeyMap\u2019\n Use -v (or `:set -v` in ghci) to see a list of the files searched for.\n |\n73 | import qualified Data.Aeson.KeyMap as KM\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nTypes.hs:74:1: error:\n Could not find module \u2018Data.HashMap.Strict\u2019\n Perhaps you meant\n Data.IntMap.Strict (from containers-0.6.2.1)\n Data.Map.Strict (from containers-0.6.2.1)\n Use -v (or `:set -v` in ghci) to see a list of the files searched for.\n |\n74 | import qualified Data.HashMap.Strict as HM\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nTypes.hs:77:1: error:\n Could not find module \u2018Data.Yaml\u2019\n Use -v (or `:set -v` in ghci) to see a list of the files searched for.\n |\n77 | import qualified Data.Yaml as Y\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" + }, + { + "name": "openapi-tools", + "status": "PASS", + "elapsed_seconds": 0.003, + "artifact": null, + "output": "" + } + ], + "pr_note": "Include the encrypted diagnostic logd artifact(s): diagnostic/build-6b3111c3.logd. The encrypted .logd is the required diagnostic content for PR review; this JSON file is metadata. Maintainers may ask you to remove these diagnostic artifacts before merging." +} diff --git a/diagnostic/build-6b3111c3.logd b/diagnostic/build-6b3111c3.logd new file mode 100644 index 000000000..2d6e47766 Binary files /dev/null and b/diagnostic/build-6b3111c3.logd differ diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_diagnostic_redaction.py b/tests/test_diagnostic_redaction.py new file mode 100644 index 000000000..d4a2b3b9e --- /dev/null +++ b/tests/test_diagnostic_redaction.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +"""Regression tests for diagnostic redaction, artifact pairing, and path reporting.""" + +import getpass +import json +import platform +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +import build + + +class TestDiagnosticPathHelpers(unittest.TestCase): + def test_encryptly_failure_message_hides_password_stdout(self): + message = build.encryptly_failure_message("", "4d944e71565e39788edc", "encryptly pack failed") + self.assertEqual(message, "encryptly pack failed") + + def test_repo_relative_path_uses_forward_slashes(self): + artifact = build.diagnostic_repo_relpath(build.ROOT / "backend" / "target" / "backend") + self.assertEqual(artifact, "backend/target/backend") + self.assertNotIn("\\", artifact) + + def test_absolute_repo_path_becomes_relative(self): + artifact = build.sanitize_diagnostic_artifact_path(str(build.ROOT / "market" / "market")) + self.assertEqual(artifact, "market/market") + + def test_outside_repo_path_is_redacted(self): + outside = str(Path.home() / "outside" / "binary") + artifact = build.sanitize_diagnostic_artifact_path(outside) + self.assertEqual(artifact, build.DIAGNOSTIC_REDACTED_PATH + "/outside/binary") + + +class TestDiagnosticRedaction(unittest.TestCase): + def test_redacts_home_repo_temp_user_and_host(self): + home = str(Path.home()) + repo = str(build.ROOT) + tmp = tempfile.gettempdir() + username = getpass.getuser() + hostname = platform.node() + raw = f"home={home} repo={repo} tmp={tmp} user={username} host={hostname}" + redacted = build.redact_diagnostic_text(raw) + + self.assertNotIn(home, redacted) + self.assertNotIn(repo, redacted) + if tmp and tmp != "/tmp": + self.assertNotIn(tmp, redacted) + if username and len(username) > 1: + self.assertNotIn(username, redacted) + if hostname and len(hostname) > 1: + self.assertNotIn(hostname, redacted) + self.assertIn(build.DIAGNOSTIC_REDACTED_PATH, redacted) + self.assertIn(build.DIAGNOSTIC_REDACTED_USER, redacted) + self.assertIn(build.DIAGNOSTIC_REDACTED_HOST, redacted) + + def test_build_report_metadata_has_no_local_identifiers(self): + home = str(Path.home()) + username = getpass.getuser() + hostname = platform.node() + binary = str(build.ROOT / "backend" / "target" / "debug" / "backend") + output = ( + f"Compiling in {home}\n" + f"Built for {username}@{hostname}\n" + f"artifact at {binary}" + ) + results = [("backend", True, 1.0, output, binary)] + report = build.build_diagnostic_report( + results, + commit_id="abc12345", + logd_relpaths=["diagnostic/build-abc12345.logd"], + password="test-password", + ) + report_json = json.dumps(report) + + if home and home != "/": + self.assertNotIn(home, report_json) + if username and len(username) > 1: + self.assertNotIn(username, report_json) + if hostname and len(hostname) > 1: + self.assertNotIn(hostname, report_json) + self.assertEqual(report["modules"][0]["artifact"], "backend/target/debug/backend") + self.assertNotIn("\\", report["modules"][0]["artifact"] or "") + + +class TestDiagnosticArtifactPairing(unittest.TestCase): + def setUp(self): + self.temp_dir = tempfile.TemporaryDirectory() + self.root = Path(self.temp_dir.name) + self.diagnostic_dir = self.root / "diagnostic" + self.diagnostic_dir.mkdir() + + def tearDown(self): + self.temp_dir.cleanup() + + def _write_pair(self, commit_id: str, *, include_logd: bool = True) -> Path: + metadata_path = self.diagnostic_dir / f"build-{commit_id}.json" + logd_path = self.diagnostic_dir / f"build-{commit_id}.logd" + if include_logd: + logd_path.write_bytes(b"encrypted-diagnostic") + report = build.build_diagnostic_report( + [("backend", True, 1.0, "ok", None)], + commit_id=commit_id, + logd_relpaths=[f"diagnostic/build-{commit_id}.logd"] if include_logd else None, + password="pw" if include_logd else None, + logd_error=None if include_logd else "encryptly unavailable", + ) + metadata_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") + return metadata_path + + def test_validate_pair_succeeds(self): + metadata_path = self._write_pair("abc12345") + report = build.validate_diagnostic_artifact_pair(self.root, metadata_path) + self.assertEqual(report["commit"], "abc12345") + + def test_missing_json_fails_clearly(self): + metadata_path = self.diagnostic_dir / "build-missing.json" + with self.assertRaises(build.DiagnosticArtifactError) as ctx: + build.validate_diagnostic_artifact_pair(self.root, metadata_path) + self.assertIn("missing", str(ctx.exception).lower()) + + def test_missing_logd_fails_clearly(self): + metadata_path = self._write_pair("abc12345", include_logd=False) + report = build.build_diagnostic_report( + [("backend", True, 1.0, "ok", None)], + commit_id="abc12345", + logd_relpaths=["diagnostic/build-abc12345.logd"], + password="pw", + ) + metadata_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") + with self.assertRaises(build.DiagnosticArtifactError) as ctx: + build.validate_diagnostic_artifact_pair(self.root, metadata_path) + self.assertIn("missing", str(ctx.exception).lower()) + + def test_mismatched_commit_pair_fails_clearly(self): + metadata_path = self._write_pair("abc12345") + other_logd = self.diagnostic_dir / "build-other000.logd" + other_logd.write_bytes(b"encrypted-diagnostic") + report = json.loads(metadata_path.read_text(encoding="utf-8")) + report["diagnostic_logd"] = "diagnostic/build-other000.logd" + metadata_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") + with self.assertRaises(build.DiagnosticArtifactError) as ctx: + build.validate_diagnostic_artifact_pair(self.root, metadata_path) + self.assertIn("does not pair", str(ctx.exception).lower()) + + def test_backslash_logd_reference_fails_clearly(self): + metadata_path = self._write_pair("abc12345") + report = json.loads(metadata_path.read_text(encoding="utf-8")) + report["diagnostic_logd"] = "diagnostic\\build-abc12345.logd" + metadata_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") + with self.assertRaises(build.DiagnosticArtifactError) as ctx: + build.validate_diagnostic_artifact_pair(self.root, metadata_path) + self.assertIn("forward-slash", str(ctx.exception).lower()) + + +class TestDiagnosticReportShape(unittest.TestCase): + def test_chunked_logd_paths_are_repo_relative(self): + report = build.build_diagnostic_report( + [("backend", True, 1.0, "", None)], + commit_id="abc12345", + logd_relpaths=[ + "diagnostic/build-abc12345-part001.logd", + "diagnostic/build-abc12345-part002.logd", + ], + password="pw", + chunked=True, + ) + self.assertIsInstance(report["diagnostic_logd"], list) + for ref in report["diagnostic_logd"]: + self.assertTrue(ref.startswith("diagnostic/")) + self.assertNotIn("\\", ref) + + def test_error_only_report_allows_missing_logd(self): + metadata_dir = build.DIAGNOSTIC_DIR + metadata_dir.mkdir(parents=True, exist_ok=True) + commit_id = "erroronly" + metadata_path = metadata_dir / f"build-{commit_id}.json" + report = build.build_diagnostic_report( + [("encryptly-preflight", False, 0.1, "blocked", None)], + commit_id=commit_id, + logd_error="encryptly unavailable", + message_blocker=build.ENCRYPTLY_BLOCKER_MESSAGE, + ) + metadata_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") + validated = build.validate_diagnostic_artifact_pair( + build.ROOT, + metadata_path, + require_logd=False, + ) + self.assertIsNone(validated["diagnostic_logd"]) + metadata_path.unlink(missing_ok=True) + + +class TestStubDiagnosticArtifacts(unittest.TestCase): + def test_stub_pair_is_valid(self): + metadata_path = build.DIAGNOSTIC_DIR / "build-00000000.json" + if not metadata_path.exists(): + self.skipTest("stub diagnostic metadata is not present") + report = build.validate_diagnostic_artifact_pair(build.ROOT, metadata_path) + self.assertEqual(report["diagnostic_logd"], "diagnostic/build-00000000.logd") + + +if __name__ == "__main__": + unittest.main()