From 668841417f561a007c2953e264ceedfbdf6683af Mon Sep 17 00:00:00 2001 From: AIOSAI Date: Thu, 4 Jun 2026 01:17:56 -0700 Subject: [PATCH] fix(portability): aipass init UTF-8 stdout on Windows + CI e2e lane (DPLAN-0194) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The real T1 Windows bug (diagnosed via the now-reverted error-surfacing probe): aipass init scaffolds fine, then crashes printing its success banner — Rich writes the success glyphs through a cp1252 stdout (UnicodeEncodeError 'charmap'). Same class as the drone fix. The aipass entry point now reconfigure()s stdout/stderr to UTF-8 in place on Windows. Also: ci.yml's broad 'pytest --rootdir=.' swept in tests/e2e (which build a wheel via the dedicated e2e-wheel.yml), failing the unit lane since the harness landed; now --ignore=tests/e2e in both pytest jobs. route_command error-surfacing probe reverted to honor 'no function change' (the masked-error mislabel is noted as a separate @aipass recommendation). Local: e2e 14/14 green, aipass units 24/24, ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 6 ++++-- CHANGELOG.md | 27 +++++++++++++++++---------- src/aipass/aipass/apps/aipass.py | 20 ++++++++++++++------ 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac490eb0..8f427a39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,9 @@ jobs: - run: | python -m pip install --upgrade pip pip install -e ".[dev]" - - run: coverage run -m pytest -v --tb=short --rootdir=. + # tests/e2e build a wheel + clean venv per the dedicated e2e-wheel.yml + # workflow — they are not part of the fast unit lane. + - run: coverage run -m pytest -v --tb=short --rootdir=. --ignore=tests/e2e standards: name: seedgo-audit @@ -66,7 +68,7 @@ jobs: - run: | python -m pip install --upgrade pip pip install -e ".[dev]" - - run: coverage run -m pytest --rootdir=. + - run: coverage run -m pytest --rootdir=. --ignore=tests/e2e - run: coverage xml - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index d806c6d9..df795222 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,18 +70,25 @@ and this project uses [Calendar Versioning](https://calver.org/) in the format Windows (the old Windows CI ran an editable install, `aipass`-less, and only routed to in-process modules, so both paths had zero Windows coverage). Pure portability fixes — Linux/macOS behaviour is unchanged. - - **`aipass init` failed on Windows with a misleading "Unknown command: init".** - `_preflight_check` walks every ancestor of CWD up to the filesystem root - calling `iterdir()`/`is_file()`; at the Windows drive root a locked system - entry (e.g. `pagefile.sys`) raises `OSError`, which the CLI's `route_command` - swallowed and mislabeled. The ancestor walk now skips un-enumerable / - un-stattable entries (logged), so init proceeds normally. - - **`drone @branch` crashed on Windows with `UnicodeEncodeError ('charmap')`.** - `drone` resolved + subprocessed the branch correctly, then crashed *printing* - the captured output: Rich wrote Unicode through a cp1252 stdout. The existing + - **`aipass init` crashed on Windows (surfaced as a misleading "Unknown + command: init").** Init scaffolded the project correctly, then crashed + *printing its `✓ Project initialized` banner* — Rich wrote the ✓/box glyphs + through a cp1252 stdout, raising `UnicodeEncodeError ('charmap')`; the error + handler's `✗` message hit the same wall, bubbling up to the command router + which mislabeled it. The `aipass` entry point now reconfigures stdout/stderr + to UTF-8 in place on Windows. (The init preflight ancestor-walk was also + hardened to skip un-enumerable Windows drive-root entries — defensive, not + the trigger.) + - **`drone @branch` crashed on Windows with the same `UnicodeEncodeError + ('charmap')`.** `drone` resolved + subprocessed the branch correctly, then + crashed *printing* the captured output through cp1252 stdout. The existing `PYTHONUTF8` guard only affected child interpreters, not the live process streams — `drone`'s entry point now also `reconfigure()`s stdout/stderr to - UTF-8 in place. (DPLAN-0194) + UTF-8 in place. + - **CI unit lane no longer runs the e2e wheel tests.** `ci.yml`'s + `pytest --rootdir=.` swept in `tests/e2e/` (which build a wheel per the + dedicated `e2e-wheel.yml`), failing the unit lane; it now `--ignore`s them. + (DPLAN-0194) - **A release merge can no longer destroy the `dev` branch** — `drone @git merge` passed `--delete-branch` to `gh pr merge` unconditionally, so merging a `dev`→`main` PR deleted the persistent `dev` branch on the remote and stranded diff --git a/src/aipass/aipass/apps/aipass.py b/src/aipass/aipass/apps/aipass.py index 555fb7f8..1c4a5daf 100644 --- a/src/aipass/aipass/apps/aipass.py +++ b/src/aipass/aipass/apps/aipass.py @@ -15,12 +15,25 @@ Auto-discovery architecture: - No manual imports or routing needed """ +import os import sys import importlib -import traceback from pathlib import Path from typing import List, Any +# Windows terminals/pipes default to cp1252, which can't encode the Unicode +# Rich emits (✓/✗, box-drawing, arrows). PYTHONUTF8 only affects child +# interpreters, not this process's already-open stdout/stderr — so we also +# reconfigure the live streams to UTF-8 in place (Python 3.7+). Without this, +# `aipass init` scaffolds correctly but crashes printing its success banner +# with UnicodeEncodeError ('charmap') on Windows. Mirrors drone/cli.py. +if sys.platform == "win32": + os.environ.setdefault("PYTHONUTF8", "1") # for child subprocesses + for _stream in (sys.stdout, sys.stderr): + _reconfigure = getattr(_stream, "reconfigure", None) + if _reconfigure is not None: + _reconfigure(encoding="utf-8", errors="replace") + from aipass.prax import logger # ============================================================================= @@ -60,12 +73,7 @@ def route_command(command: str, args: List[str], modules: List[Any]) -> bool: if module.handle_command(command, args): return True except Exception as e: - # A module that owns this command but fails must not be masked as - # "Unknown command" — surface the real error (and traceback) to - # stderr so failures are diagnosable instead of silently swallowed. logger.error(f"[AIPASS] Module {module.__name__} error: {e}") - print(f"[AIPASS] {module.__name__.split('.')[-1]} failed: {e}", file=sys.stderr) - traceback.print_exc(file=sys.stderr) return False