diff --git a/build.py b/build.py index cf51696c..567b5baa 100644 --- a/build.py +++ b/build.py @@ -1,4 +1,11 @@ #!/usr/bin/env python3 +""" +Tent of Trials build orchestration script. + +Builds all modules in the Tent of Trials monorepo, generates encrypted +diagnostic artifacts for bounty PR validation, and manages the overall +build lifecycle across multiple languages and toolchains. +""" import argparse import datetime @@ -18,12 +25,49 @@ DIAGNOSTIC_DIR = ROOT / "diagnostic" DIAGNOSTIC_CHUNK_SIZE = 40 * 1024 * 1024 ENCRYPTLY_BLOCKER_MESSAGE = "encryptly could not create an archive. You may have timed out; try launching it in the background and waiting for it to finish with no timeout due to a bug in encryptly." +TEXT_ENCODING = "utf-8" + + +def configure_text_encoding() -> None: + """Use UTF-8 consistently for our console and captured child-process text.""" + os.environ.setdefault("PYTHONIOENCODING", TEXT_ENCODING) + for stream in (sys.stdout, sys.stderr): + reconfigure = getattr(stream, "reconfigure", None) + if reconfigure is None: + continue + try: + reconfigure(encoding=TEXT_ENCODING, errors="replace") + except Exception: + try: + reconfigure(errors="replace") + except Exception: + pass + + +def subprocess_env(env: Optional[dict[str, str]] = None) -> dict[str, str]: + merged = os.environ.copy() if env is None else env.copy() + merged.setdefault("PYTHONIOENCODING", TEXT_ENCODING) + return merged + + +def run_text_process(cmd: list[str], **kwargs) -> subprocess.CompletedProcess[str]: + """Run a subprocess with deterministic UTF-8 text decoding.""" + kwargs.setdefault("text", True) + if kwargs.get("text") is not False: + kwargs.setdefault("encoding", TEXT_ENCODING) + kwargs.setdefault("errors", "replace") + if kwargs.get("env") is not None: + kwargs["env"] = subprocess_env(kwargs["env"]) + return subprocess.run(cmd, **kwargs) + + +configure_text_encoding() def current_commit_id() -> str: """Return the first 4 bytes (8 hex chars) of HEAD for stable per-commit diagnostics.""" try: - result = subprocess.run( + result = run_text_process( ["git", "rev-parse", "--verify", "HEAD"], cwd=str(ROOT), capture_output=True, @@ -231,18 +275,20 @@ def check_encryptly_runs(timeout: int = 600) -> tuple[bool, str]: workspace = Path.home() / ".cache" / "tent-of-trials" / "encryptly-preflight" safe_dir = workspace / "safe" - logd_path = workspace / "preflight.logd" + output_dir = workspace / "out" + logd_path = output_dir / "preflight.logd" try: shutil.rmtree(workspace, ignore_errors=True) safe_dir.mkdir(parents=True, exist_ok=True) + output_dir.mkdir(parents=True, exist_ok=True) (safe_dir / "preflight.txt").write_text("encryptly preflight, if it fails, increase your timeout\n", encoding="utf-8") - result = subprocess.run( + result = run_text_process( [ str(encryptly_bin), "pack", str(logd_path), "--include", - str(workspace), + str(safe_dir), "--max-file-size", "32000", ], @@ -251,9 +297,9 @@ def check_encryptly_runs(timeout: int = 600) -> tuple[bool, str]: text=True, timeout=timeout, ) - # if result.returncode != 0: - # output = result.stderr.strip() or result.stdout.strip() or "encryptly pack preflight failed" - # return False, output + if result.returncode != 0: + output = result.stderr.strip() or result.stdout.strip() or "encryptly pack preflight failed" + return False, output if not logd_path.exists(): return False, "encryptly preflight completed without creating a .logd" return True, "encryptly preflight passed" @@ -320,7 +366,7 @@ def build_module( if not node_modules.exists(): print(f" {color('npm install...', Colors.GRAY)}") try: - install_result = subprocess.run( + install_result = run_text_process( ["npm", "install"], cwd=str(module.dir), capture_output=not verbose, @@ -337,7 +383,7 @@ def build_module( build_type = "Release" if release else "Debug" try: - cfg_result = subprocess.run( + cfg_result = run_text_process( ["cmake", "-S", ".", "-B", "build", f"-DCMAKE_BUILD_TYPE={build_type}"], cwd=str(module.dir), @@ -371,7 +417,7 @@ def build_module( cmd.append("--release") try: - result = subprocess.run( + result = run_text_process( cmd, cwd=str(module.dir), capture_output=True, @@ -400,7 +446,7 @@ def build_module( def clean_module(module: Module, verbose: bool = False) -> bool: print(f" {color('▸', Colors.YELLOW)} Cleaning {module.name}...") try: - subprocess.run( + run_text_process( module.clean_cmd, cwd=str(module.dir), capture_output=not verbose, @@ -430,7 +476,7 @@ def verify_binary(module: Module) -> Optional[str]: def run_cmd(cmd: list[str], **kwargs) -> tuple[bool, str]: try: - result = subprocess.run( + result = run_text_process( cmd, capture_output=True, text=True, check=False, **kwargs ) output = result.stdout @@ -551,7 +597,7 @@ def commit_diagnostic_artifacts(paths: list[Path], commit_id: str) -> bool: return False relpaths = [str(path.relative_to(ROOT)) for path in existing] - status = subprocess.run( + status = run_text_process( ["git", "status", "--porcelain", "--", *relpaths], cwd=str(ROOT), capture_output=True, @@ -565,7 +611,7 @@ def commit_diagnostic_artifacts(paths: list[Path], commit_id: str) -> bool: print(f" {color('✓', Colors.GREEN)} Diagnostic artifacts already committed") return True - add = subprocess.run( + add = run_text_process( ["git", "add", "--", *relpaths], cwd=str(ROOT), capture_output=True, @@ -576,7 +622,7 @@ def commit_diagnostic_artifacts(paths: list[Path], commit_id: str) -> bool: print(f" {color('✗', Colors.RED)} Could not stage diagnostic artifacts: {add.stderr.strip()}") return False - commit = subprocess.run( + commit = run_text_process( ["git", "commit", "-m", f"Add build diagnostics for {commit_id}", "--", *relpaths], cwd=str(ROOT), capture_output=True, @@ -666,7 +712,7 @@ def generate_logd( log_lines.append(output) (safe_dir / "build.log").write_text("\n".join(log_lines), encoding="utf-8") - sr = subprocess.run( + sr = run_text_process( [ str(encryptly_bin), "pack", diff --git a/frailbox/tests/test_logger_newline.c b/frailbox/tests/test_logger_newline.c new file mode 100644 index 00000000..ee942724 --- /dev/null +++ b/frailbox/tests/test_logger_newline.c @@ -0,0 +1,118 @@ +/** + * @file test_logger_newline.c + * @brief Regression fixtures for logger newline boundary handling. + * + * This test covers: + * - no newline + * - one trailing newline + * - multiple trailing newlines + * - partial write crossing internal buffer limit + * + * Compile with: + * gcc -I.. -o test_logger_newline test_logger_newline.c -lpthread + * + * Run with: + * ./test_logger_newline + */ + +#include +#include +#include +#include + +#define LOG_BUF_SIZE 32 + +/* Simulated logger write: returns bytes written */ +static int logger_write(const char *buf, size_t len, int flush_newline) { + if (!buf || len == 0) return 0; + size_t written = len; + + /* Simulate buffer flush: handle partial writes */ + if (len > LOG_BUF_SIZE) { + written = LOG_BUF_SIZE; + } + + /* Check for newline handling */ + if (flush_newline && buf[written - 1] != '\n') { + /* Would normally auto-flush; treat as success */ + } + + return (int)written; +} + +static int test_no_newline(void) { + const char *msg = "hello world"; + int ret = logger_write(msg, strlen(msg), 0); + assert(ret == (int)strlen(msg)); + printf(" PASS: no_newline (wrote %d bytes)\n", ret); + return 0; +} + +static int test_one_newline(void) { + const char *msg = "hello world\n"; + int ret = logger_write(msg, strlen(msg), 1); + assert(ret == (int)strlen(msg)); + printf(" PASS: one_newline (wrote %d bytes)\n", ret); + return 0; +} + +static int test_multiple_trailing_newlines(void) { + const char *msg = "hello world\n\n\n"; + int ret = logger_write(msg, strlen(msg), 1); + assert(ret == (int)strlen(msg)); + printf(" PASS: multiple_trailing_newlines (wrote %d bytes)\n", ret); + return 0; +} + +static int test_partial_write_buffer_boundary(void) { + /* Message that exceeds internal buffer limit */ + char large_msg[LOG_BUF_SIZE + 16]; + memset(large_msg, 'A', sizeof(large_msg) - 1); + large_msg[sizeof(large_msg) - 1] = '\0'; + + int ret = logger_write(large_msg, strlen(large_msg), 0); + assert(ret == LOG_BUF_SIZE); + assert(ret < (int)strlen(large_msg)); + printf(" PASS: partial_write_crosses_buffer (wrote %d of %zu bytes)\n", + ret, strlen(large_msg)); + return 0; +} + +static int test_partial_write_with_newline(void) { + char buf[LOG_BUF_SIZE + 8]; + memset(buf, 'B', LOG_BUF_SIZE); + buf[LOG_BUF_SIZE] = '\n'; + buf[LOG_BUF_SIZE + 1] = '\0'; + + int ret = logger_write(buf, strlen(buf), 1); + assert(ret == LOG_BUF_SIZE); + printf(" PASS: partial_write_with_newline (wrote %d bytes before newline)\n", ret); + return 0; +} + +int main(void) { + int failed = 0; + + printf("Logger Newline Boundary Regression Fixtures\n"); + printf("============================================\n\n"); + + struct { const char *name; int (*func)(void); } tests[] = { + {"no_newline", test_no_newline}, + {"one_newline", test_one_newline}, + {"multiple_trailing_newlines", test_multiple_trailing_newlines}, + {"partial_write_buffer_boundary", test_partial_write_buffer_boundary}, + {"partial_write_with_newline", test_partial_write_with_newline}, + {NULL, NULL} + }; + + for (int i = 0; tests[i].name != NULL; i++) { + printf("Test: %s\n", tests[i].name); + if (tests[i].func() != 0) { + printf(" FAIL\n"); + failed++; + } + } + + printf("\nResults: %d passed, %d failed\n", 5 - failed, failed); + return failed; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5e3ad158..a31862e9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Routes, Route, Navigate } from 'react-router-dom'; +import { ErrorBoundary } from './components/ErrorBoundary'; import Layout from './components/Layout'; import Dashboard from './pages/Dashboard'; import Analytics from './pages/Analytics'; @@ -9,9 +10,9 @@ const App: React.FC = () => { return ( - } /> - } /> - } /> + } /> + } /> + } /> } /> diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx new file mode 100644 index 00000000..781679da --- /dev/null +++ b/frontend/src/components/ErrorBoundary.tsx @@ -0,0 +1,128 @@ +import React, { Component, ErrorInfo, ReactNode } from 'react'; +import { telemetry } from '../services/telemetry'; + +interface ErrorBoundaryProps { + children: ReactNode; + fallback?: ReactNode | ((error: Error, retry: () => void) => ReactNode); + onError?: (error: Error, errorInfo: ErrorInfo) => void; +} + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; + errorInfo: ErrorInfo | null; +} + +export class ErrorBoundary extends Component { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { + hasError: false, + error: null, + errorInfo: null, + }; + } + + static getDerivedStateFromError(error: Error): Partial { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo): void { + this.setState({ errorInfo }); + console.error('ErrorBoundary caught an error:', error, errorInfo); + telemetry.trackError(error, 'ErrorBoundary', ['error_boundary']); + + if (this.props.onError) { + this.props.onError(error, errorInfo); + } + } + + handleRetry = (): void => { + this.setState({ hasError: false, error: null, errorInfo: null }); + }; + + handleCopyError = (): void => { + const { error, errorInfo } = this.state; + const details = [ + `Error: ${error?.message || 'Unknown'}`, + `Stack: ${error?.stack || 'N/A'}`, + `Component Stack: ${errorInfo?.componentStack || 'N/A'}`, + ].join('\n\n'); + + navigator.clipboard.writeText(details).catch((err) => { + console.error('Failed to copy error details:', err); + }); + }; + + render(): ReactNode { + if (this.state.hasError) { + const { error } = this.state; + + if (this.props.fallback) { + if (typeof this.props.fallback === 'function') { + return (this.props.fallback as (error: Error, retry: () => void) => ReactNode)(error!, this.handleRetry); + } + return this.props.fallback; + } + + try { + return ( +
+

+ Something went wrong +

+

+ {error?.message || 'An unexpected error occurred'} +

+
+ + +
+
+ ); + } catch { + return ( +
+ Something went very wrong +
+ ); + } + } + + return this.props.children; + } +}