From 5c018ec1963175df259dda9f994823bc5533252a Mon Sep 17 00:00:00 2001 From: Sami Ahmed Date: Wed, 12 Aug 2026 02:45:12 -0700 Subject: [PATCH] Revert "[grade=D] Sprint 1: .gitignore, test_installer.sh, test_voice_bridge.sh, README, roadmap, desktop packages, CI improvements" This reverts commit 9c63557e627b256d9eb45a2dc6f9a55035210cbd. --- .gitea/workflows/ci.yml | 63 +--- .gitignore | 22 +- README.md | 310 +++++++++++----- docs/roadmap.md | 45 ++- packaging/archiso/packages.x86_64.desktop | 68 ---- tests/test_helper.sh | 7 +- tests/test_installer.sh | 244 +++++++----- tests/test_version.sh | 6 +- tests/test_voice_bridge.sh | 433 ++++++++++++++++++---- 9 files changed, 791 insertions(+), 407 deletions(-) delete mode 100644 packaging/archiso/packages.x86_64.desktop diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 3a78639..82e119c 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -24,69 +24,10 @@ jobs: sudo apt-get install -y -qq make fi - - name: Install Python 3 - run: | - if ! command -v python3 >/dev/null 2>&1; then - sudo apt-get install -y -qq python3 - fi - # ── Shellcheck ─────────────────────────────────────────────────── - name: Run shellcheck on all shell scripts run: make lint - # ── Python AST validation ──────────────────────────────────────── - - name: Validate Python syntax (AST parse) - run: | - set -e - for f in $(find packaging/scripts -name '*.py'); do - echo "Checking $f..." - python3 -c "import ast; ast.parse(open('$f').read())" - echo " ✓ $f" - done - - # ── VERSION SSOT consistency ───────────────────────────────────── - - name: Verify VERSION single-source-of-truth - run: | - set -e - VERSION="$(cat VERSION)" - echo "Canonical VERSION: $VERSION" - - # Mirror must match - MIRROR="$(head -n1 packaging/archiso/airootfs/etc/samios-version)" - if [ "$VERSION" != "$MIRROR" ]; then - echo "ERROR: VERSION ($VERSION) != airootfs mirror ($MIRROR)" - echo "Fix: make version-sync" - exit 1 - fi - echo " ✓ airootfs mirror matches VERSION" - - # PKGBUILD pkgver must resolve to VERSION (hyphen → underscore) - PKGVER="$(cd packaging/packages/samios-branding && source PKGBUILD >/dev/null 2>&1 && echo "$pkgver")" - EXPECTED_PKGVER="$(echo "$VERSION" | tr '-' '_')" - if [ "$PKGVER" != "$EXPECTED_PKGVER" ]; then - echo "ERROR: PKGBUILD pkgver ($PKGVER) != expected ($EXPECTED_PKGVER)" - exit 1 - fi - echo " ✓ PKGBUILD pkgver resolves to VERSION" - - # No hardcoded version literal in production files - # (spot-check the main components) - for f in \ - packaging/archiso/airootfs/usr/local/bin/samios \ - packaging/scripts/samios-installer.sh \ - packaging/scripts/voice-bridge.py \ - packaging/scripts/samios-desktop-setup.sh; do - if grep -qE '(^|[^a-zA-Z0-9])0\.[1-9][0-9]*\.[0-9]+' "$f" 2>/dev/null; then - echo "ERROR: $f contains a hardcoded version literal" - grep -nE '(^|[^a-zA-Z0-9])0\.[1-9][0-9]*\.[0-9]+' "$f" - exit 1 - fi - done - echo " ✓ No hardcoded version literals in production components" - - echo "" - echo "VERSION SSOT consistency check PASSED ✓" - # ── Profile structure validation ───────────────────────────────── - name: Validate archiso profile structure run: | @@ -158,5 +99,5 @@ jobs: echo "Profile structure validation PASSED ✓" # ── Test suite ─────────────────────────────────────────────────── - - name: Run test suite (lint + tests) - run: make check + - name: Run test suite + run: make test diff --git a/.gitignore b/.gitignore index 4f4daa6..b64968c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,27 +1,19 @@ # Python __pycache__/ *.pyc -*.pyo # Temporary / scratch .tmp *.tmp -# Build artifacts +# Build output directories /work/ /out/ -/build/ -/output/ +build/ + +# Python virtualenv +.venv/ + +# Build artifacts *.iso *.tar.gz -*.tar.xz - -# Virtual environments -.venv/ -venv/ - -# Editor -*.swp -*.swo -*~ -.DS_Store diff --git a/README.md b/README.md index 10ecd88..b269e1a 100644 --- a/README.md +++ b/README.md @@ -1,116 +1,254 @@ -# SamiOS +# SamiOS — A Custom Arch Linux Distribution -A custom Arch Linux distribution designed as a Windows replacement, built around voice-first interaction. +## Overview -## Quick Start +SamiOS is a custom Linux distribution based on Arch Linux, designed as a +personal daily-driver desktop OS. It ships a custom CLI (`samios`), an +automated disk installer, a single-source-of-truth version system, and +a voice-bridge prototype for hands-free OS control via Hermes. + +## Features + +- **Rolling release** Arch-based +- **Custom CLI** (`samios`) for system management — version, status, update, install, remove, services, fonts +- **Automated installer** (`samios-installer.sh`) — UEFI/BIOS, partitioning, pacstrap, GRUB, user creation +- **VERSION single-source-of-truth** — one file drives every component +- **Custom branding** — GRUB theme, MOTD, bashrc, zshrc, pacman welcome message +- **Font policy** — excludes fonts with "7777" in the filename (personal branding) +- **Voice bridge prototype** — `voice-bridge.py` listens to SamiType transcripts and routes to Hermes (command mode) or `xdotool` (dictation mode) +- **CI** — Gitea Actions runs `make check` on pushes to `master` / `main` and on pull requests targeting those branches + +## Building from Source + +### Prerequisites + +```bash +# On Arch Linux +sudo pacman -S archiso git shellcheck +``` + +### Clone and Build ```bash -# Clone git clone https://git.sami/sami7777/samios.git -cd samios - -# Run the full test suite (lint + tests) -make check - -# Build the ISO (requires Arch Linux + archiso) -cd packaging/archiso && sudo bash build.sh +cd samios/packaging/archiso +sudo ./build.sh ``` -## Version Management +## Installation -SamiOS uses a **single-source-of-truth** VERSION system. One file at the repo root controls the version everywhere: +### From ISO -```bash -# Change the version across all 7 components (CLI, installer, PKGBUILD, etc.) -make version-set NEW=X.Y.Z +1. Boot the generated ISO. The ISO ships the live environment, the + `samios` package-management CLI, and `/etc/samios-version` (the + mirrored VERSION file). It does **not** ship + `samios-installer.sh` automatically — that script lives in + `packaging/scripts/` in the source repo and is invoked manually + after boot (or copied into the live environment first). +2. To install SamiOS to disk from the live ISO, run the installer: + ```bash + sudo /root/samios-installer.sh # if you copied it into the live env + # — or — + sudo bash /path/to/samios-installer.sh + ``` + The installer is interactive by default; pass `--disk`, `--user`, + and `--password` to drive it non-interactively from automation: + ```bash + sudo /path/to/samios-installer.sh --disk /dev/sda --user sami --password ... + ``` -# Sync the canonical VERSION to the airootfs mirror -make version-sync +> **Note:** `samios install` (the CLI subcommand on the live system +> and on the installed OS) is the **package** installer — e.g. +> `samios install firefox`. It does **not** invoke the OS installer. +> The OS installer is the `samios-installer.sh` script. -# Print the current version -make version -``` +### Manual Installation -No component hardcodes a version literal — they all derive from `./VERSION` via a walk-up lookup pattern. - -## Makefile Targets - -| Target | Description | -|--------|-------------| -| `make check` | Run shellcheck + full test suite | -| `make test` | Run test suite only | -| `make lint` | Run shellcheck on all shell scripts | -| `make version` | Print the current SamiOS version | -| `make version-set NEW=X` | Set VERSION to X (e.g. `X.Y.Z` or `X.Y.Z-prerelease`) | -| `make version-sync` | Mirror VERSION to `packaging/archiso/airootfs/etc/samios-version` | - -## Test Suite - -8 test suites with 85+ assertions covering: - -| Suite | Coverage | -|-------|----------| -| `test_version.sh` | SSOT invariant: one VERSION file, no hardcoded literals, all components agree | -| `test_installer.sh` | Installer syntax, VERSION resolution, partition/format/pacstrap/bootloader logic | -| `test_voice_bridge.sh` | Python AST, version resolution, class/method structure, imports | -| `test_samios_cli.sh` | CLI commands, version output, help text | -| `test_profiledef.sh` | Arch ISO profile structure and metadata | -| `test_packages.sh` | Package list completeness | -| `test_pacman_conf.sh` | Pacman configuration sections | -| `test_font_policy.sh` | Font exclusion policy (no "7777" fonts) | +See [`docs/installation.md`](docs/installation.md) for detailed +instructions. ## Repository Layout ``` -. -├── VERSION # Canonical version (single source of truth) -├── Makefile # Build, test, version management targets -├── .gitea/workflows/ci.yml # CI pipeline (shellcheck + tests) -├── branding/ # Logos, icons, GRUB theme, SDDM theme, wallpapers -├── docs/ # Installation, roadmap, voice architecture docs +samios/ +├── VERSION # Single source of truth for the release version +├── Makefile # Targets: test, lint, check, version, version-set, version-sync, build, clean, help (see "Makefile Targets" below) +├── README.md # This file +├── branding/ # GRUB theme, pacman welcome, fonts +├── docs/ # roadmap, installation, voice-to-Hermes design ├── packaging/ -│ ├── archiso/ # Arch ISO profile (profiledef, packages, airootfs) -│ │ ├── airootfs/ # Root filesystem overlay -│ │ │ ├── etc/ # System config (hostname, hosts, locale, fonts) -│ │ │ └── usr/local/bin/ # samios CLI -│ │ ├── grub/ # GRUB boot config -│ │ └── profiledef.sh # Arch ISO metadata +│ ├── archiso/ # The archiso profile (profiledef.sh, packages, airootfs overlay) +│ │ └── airootfs/ +│ │ ├── etc/samios-version # Mirrored from /VERSION by `make version-sync` +│ │ └── usr/local/bin/samios # The CLI tool (installed) │ ├── packages/ -│ │ └── samios-branding/ # PKGBUILD for branding package +│ │ └── samios-branding/ # PKGBUILD (also reads ../../../VERSION) │ └── scripts/ -│ ├── samios-installer.sh # Automated disk installer -│ ├── samios-desktop-setup.sh # WSL desktop environment setup -│ ├── voice-bridge.py # Voice command bridge (prototype) -│ └── samios-voice.service # systemd unit for voice bridge -├── tests/ # Bash test suite (test_helper.sh + test_*.sh) -└── .gitignore +│ ├── samios-installer.sh # The disk installer +│ ├── samios-desktop-setup.sh # Post-install KDE/desktop setup +│ └── voice-bridge.py # Voice command/dictation bridge prototype +└── tests/ # Test suite (see "Test Suite" below) ``` -## Voice Integration (Phase 4) +## VERSION Single Source of Truth -SamiOS is being developed with voice-first interaction as the north star: +There is exactly **one** authoritative source for the release version: +the `VERSION` file at the repo root. Each consumer resolves it via +its own mechanism: -- **`voice-bridge.py`** — Prototype bridge connecting SamiType STT to system commands -- Supports command mode ("Computer, open Firefox") and dictation mode -- Routes to Hermes for complex queries -- See `docs/voice-integration-architecture.md` for the full design +- **`samios` CLI** (`packaging/archiso/airootfs/usr/local/bin/samios`) — + walks up from `$0` to find `./VERSION`, falls back to + `/etc/samios-version`, then to a documented placeholder. +- **`samios-installer.sh`** (`packaging/scripts/samios-installer.sh`) — + walks up from `$0` to find `./VERSION`, falls back to + `/etc/samios-version`, then to a documented placeholder. Same + algorithm as the CLI it emits. +- **`voice-bridge.py`** (`packaging/scripts/voice-bridge.py`) — + walks up from its own location to find `./VERSION`, falls back to + `/etc/samios-version`, then to a documented placeholder. +- **`profiledef.sh`** (`packaging/archiso/profiledef.sh`) — + walks up from `$0` to find `./VERSION`, then to the documented + placeholder. (No `/etc/samios-version` fallback — this runs at + ISO *build* time, not at runtime.) +- **`PKGBUILD`** (`packaging/packages/samios-branding/PKGBUILD`) — + reads `$startdir/../../../VERSION` first, falls back to + `../../../VERSION`, then **fails loudly** with `exit 1`. There is + no walk-up and no placeholder — the SSOT invariant is absolute: a + release build without VERSION is a build error. +- **`.bashrc` / `.zshrc`** (in the airootfs overlay — run in the live + ISO when root opens a shell) — read `/etc/samios-version` directly; + fall back to the documented placeholder. (No walk-up: the live ISO + ships `/etc/samios-version` via `make version-sync`. Note: the + installer does **not** copy these root dotfiles to installed + systems — it writes its own `/etc/bash.bashrc` instead.) -## CI/CD +The `test_version.sh` suite enforces this invariant: any X.Y.Z literal +introduced into a production file fails CI. To bump the version: -Gitea Actions pipeline runs on every push to master: -1. Shellcheck on all shell scripts -2. Python AST validation (voice-bridge.py) -3. VERSION SSOT consistency check (canonical = mirror = PKGBUILD pkgver) -4. Arch ISO profile structure validation -5. Full test suite (`make check`) +```bash +make version-set NEW= +``` -## Branding +That edits `./VERSION` and mirrors it into +`packaging/archiso/airootfs/etc/samios-version` (the on-system anchor +that installed systems read at runtime). Then commit and push. -- **Logo:** Cobalt blue pyramid (#1424CE) with face -- **Fonts:** Sami Grotesk (primary), Sami Sans (secondary) — excludes "7777" branding fonts -- **Boot:** Custom GRUB theme with SamiOS colors -- **Login:** SDDM theme with cobalt blue pyramid wallpaper +If you prefer editing manually: + +```bash +# Write your target version to the SSOT file: +echo "" > VERSION +# Mirror into the airootfs overlay: +make version-sync +# Verify the SSOT invariant still holds: +make check +``` + +(The exact X.Y.Z string is intentionally omitted from this README — +the version lives in the `VERSION` file, not in documentation.) + +## Makefile Targets + +```bash +make help # Show all targets +make version # Print current SamiOS VERSION +make version-set NEW= # Bump VERSION (semver-ish, X.Y.Z[-suffix]) +make version-sync # Mirror VERSION into the airootfs overlay +make test # Run the test suite +make lint # Run shellcheck on all shell scripts +make check # Run lint + test (CI entry point) +make build # Build the SamiOS ISO (requires archiso + root) +make clean # Remove build artifacts +``` + +## Test Suite + +The test suite lives under `tests/` and is wired into +`tests/run_tests.sh`. It contains **142+ assertions across 8 test files**: + +| Test file | What it covers | +|-----------|----------------| +| `test_profiledef.sh` | archiso profiledef.sh — script presence, package lists, hooks | +| `test_packages.sh` | package list shape and required packages | +| `test_pacman_conf.sh` | pacman.conf configuration | +| `test_samios_cli.sh` | the installed `samios` CLI — syntax, --help, version, status, fonts | +| `test_font_policy.sh` | 7777-exclusion font policy | +| `test_version.sh` | VERSION single-source-of-truth — 55+ assertions across all 7 components | +| `test_installer.sh` | `samios-installer.sh` — syntax, SSOT, heredoc-emitted CLI | +| `test_voice_bridge.sh` | `voice-bridge.py` — 20 hermetic behavioral assertions | + +Run them with: + +```bash +make test # just the tests +make lint # just shellcheck +make check # both (the CI entry point) +``` + +Individual tests can be run directly: + +```bash +bash tests/test_voice_bridge.sh --verbose +``` + +## CLI Tool + +The `samios` command provides system management: + +```bash +samios version # Show SamiOS version +samios status # Show system status (disk, memory, network) +samios update # Update system packages +samios install # Install a package +samios remove # Remove a package +samios services # List running services +samios fonts # Check installed fonts and the 7777-exclusion policy +samios help # Show all commands +``` + +The CLI walks up at runtime to find `./VERSION` (or `/etc/samios-version` +on a freshly installed system) so the version is never duplicated. + +## Voice Bridge (Prototype) + +`packaging/scripts/voice-bridge.py` is an early prototype of the +voice-to-Hermes pipeline. It listens for transcripts on stdin +(eventually from SamiType's STT socket), parses them, and: + +- If the transcript starts with the wake word **"computer"**, strips the + wake word and POSTs the remainder to the Hermes API at + `http://localhost:8765/command`. +- If the transcript is **"start dictation"**, enters dictation mode and + routes subsequent phrases to `xdotool type` (with `xdotool key` for + dictation commands like "new paragraph", "tab", "scratch that"). +- If the transcript is **"stop dictation"**, exits dictation mode. + +The script also exposes `--version` / `-V` which prints +`SamiOS voice-bridge v` — the version comes from the same +`./VERSION` walk-up resolver used by every other component. + +The bridge is a **prototype**: it does not yet connect to a real +SamiType socket, does not implement TTS feedback, and uses +`localhost:8765` as a placeholder Hermes endpoint. See +[`docs/voice-hermes-pipeline.md`](docs/voice-hermes-pipeline.md) and +[`docs/voice-integration-architecture.md`](docs/voice-integration-architecture.md) +for the design direction. The 20 behavioral assertions in +`tests/test_voice_bridge.sh` guard the version SSOT, the wake-word +routing, the dictation mode state machine, and the documented fallbacks. + +## Font Policy + +SamiOS excludes any fonts with "7777" in the filename as these are +personal branding fonts. `samios fonts` enforces this at runtime; the +CI test `test_font_policy.sh` enforces it at build time. + +## Development + +This project is under active development. See +[`docs/roadmap.md`](docs/roadmap.md) for planned features and the +current phase, and [`docs/developer-guide.md`](docs/developer-guide.md) +for build/test/release workflows. ## License -GPL3 +TBD diff --git a/docs/roadmap.md b/docs/roadmap.md index 26e6cec..4b37f54 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -10,9 +10,8 @@ - [x] SamiOS branding package (logos, icons, wallpapers) - [x] GRUB theme (#1424CE cobalt blue) - [x] SDDM theme -- [x] Build automation CI pipeline (Gitea Actions: shellcheck + structure + tests) -- [x] Test suite (85+ assertions across 8 test suites) -- [x] VERSION single-source-of-truth (SSOT) system +- [x] Build automation CI pipeline (Gitea Actions runs `make lint` and `make test` on pushes to master/main + pull requests targeting those branches — see `.gitea/workflows/ci.yml`) +- [x] Test suite (142+ assertions across 8 test files in `tests/` — see `tests/run_tests.sh`) ## Phase 2: Desktop Environment ✅ (WSL) - [x] KDE Plasma integration (WSL on SAMI-PC) @@ -28,19 +27,20 @@ - [ ] KDE panel configuration (bottom taskbar, Win11 start menu style) - [ ] Full system settings GUI -## Phase 3: Applications -- [x] Web browser (Firefox) — installed -- [x] File manager (Dolphin) — installed -- [x] Terminal emulator (Konsole) — installed -- [x] Text editor (Kate) — installed -- [ ] Office suite (LibreOffice) — package list ready -- [ ] Media player (VLC) — package list ready -- [ ] Image viewer (Gwenview) — package list ready +## Phase 3: Applications (Next) +- [x] Web browser (Firefox) — installed by `samios-desktop-setup.sh` +- [x] File manager (Dolphin) — installed by `samios-desktop-setup.sh` +- [x] Terminal emulator (Konsole) — installed by `samios-desktop-setup.sh` +- [x] Text editor (Kate) — installed by `samios-desktop-setup.sh` +- [ ] Office suite (LibreOffice) — *not yet installed; planned* +- [ ] Media player (VLC) +- [ ] Image viewer (Gwenview) — *not yet installed; planned* - [ ] Package manager GUI (Pamac) -- [ ] Email client (Thunderbird) — package list ready +- [ ] Email client (Thunderbird/KMail) +- [x] Audio (PipeWire + WirePlumber) — installed by `samios-desktop-setup.sh` +- [x] Display manager (SDDM) — installed by `samios-desktop-setup.sh` ## Phase 4: SamiType Voice Integration -- [x] Voice bridge prototype (voice-bridge.py — command + dictation modes) - [ ] SamiType STT service (systemd, always-on) - [ ] Wake word detection ("Computer") - [ ] Command router (intent classification) @@ -62,8 +62,17 @@ ## Phase 6: Polish - [x] Custom themes (Win11 dark/light color schemes) - [x] SamiOS branding (logos, wallpapers, boot screens) -- [x] Documentation (installation, voice architecture, developer guide) -- [x] Testing suite (85+ assertions, CI pipeline, VERSION SSOT) +- [x] Documentation (installation, voice architecture, VERSION SSOT) +- [x] Testing suite — 142+ assertions across 8 test files: + - `tests/test_profiledef.sh` — archiso profiledef.sh shape + - `tests/test_packages.sh` — package list shape + - `tests/test_pacman_conf.sh` — pacman.conf configuration + - `tests/test_samios_cli.sh` — samios CLI behavior + - `tests/test_font_policy.sh` — 7777-exclusion font policy + - `tests/test_version.sh` — VERSION SSOT (55+ assertions, 7 components) + - `tests/test_installer.sh` — samios-installer.sh coverage + - `tests/test_voice_bridge.sh` — voice-bridge.py hermetic behavioral tests + `make check` is a developer-side target that runs lint + test together; CI invokes the two separately. - [ ] Release automation (ISO build pipeline) ## Phase 7: Advanced Features @@ -82,10 +91,10 @@ - [ ] Enterprise features ## Timeline -- Phase 1: ✅ Complete (including CI + test suite + SSOT) +- Phase 1: ✅ Complete - Phase 2: ✅ Complete (WSL), bare-metal pending -- Phase 3: Core apps installed, remaining in package list -- Phase 4: Prototype ready, integration begins Q4 2026 +- Phase 3: Q3 2026 +- Phase 4: Q4 2026 (SamiType integration begins) - Phase 5-7: 2027+ ## Notes diff --git a/packaging/archiso/packages.x86_64.desktop b/packaging/archiso/packages.x86_64.desktop deleted file mode 100644 index 76ae4bf..0000000 --- a/packaging/archiso/packages.x86_64.desktop +++ /dev/null @@ -1,68 +0,0 @@ -# SamiOS desktop overlay packages -# This file lists packages for the desktop variant of SamiOS. -# It is used IN ADDITION TO packages.x86_64 (which stays minimal/CLI-only). -# -# To build a desktop ISO: -# 1. Create a temporary profile directory -# 2. Copy the base archiso profile into it -# 3. Merge both package lists into packages.x86_64: -# cat packaging/archiso/packages.x86_64 \ -# packaging/archiso/packages.x86_64.desktop \ -# > /tmp/desktop-profile/packages.x86_64 -# 4. Build: -# sudo mkarchiso -w work/ -o out/ /tmp/desktop-profile/ -# -# All packages are from the standard Arch repos (core/extra). -# NOTE: Verify availability before building — Arch repos change. -# Run: pacman -Ss to check. - -# ── Desktop environment ────────────────────────────────────────────────── -plasma-meta -sddm -sddm-kcm -kdeplasma-addons -plasma-nm -plasma-pa -kmenuedit -kwrite - -# ── Applications ───────────────────────────────────────────────────────── -firefox -dolphin -konsole -kate -gwenview -okular -ark -vlc -libreoffice-fresh -thunderbird - -# ── Audio ──────────────────────────────────────────────────────────────── -pipewire -pipewire-pulse -pipewire-alsa -pipewire-jack -wireplumber - -# ── Network ────────────────────────────────────────────────────────────── -network-manager-applet - -# ── Graphics ───────────────────────────────────────────────────────────── -mesa -vulkan-radeon -vulkan-intel -vulkan-icd-loader - -# ── Fonts (desktop) ────────────────────────────────────────────────────── -ttf-dejavu -ttf-liberation -noto-fonts -noto-fonts-emoji - -# ── System utilities ──────────────────────────────────────────────────── -xdg-desktop-portal -xdg-desktop-portal-kde -xdg-user-dirs -htop -fastfetch diff --git a/tests/test_helper.sh b/tests/test_helper.sh index 9cfd1da..cfec8be 100755 --- a/tests/test_helper.sh +++ b/tests/test_helper.sh @@ -18,9 +18,10 @@ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" PROFILE_DIR="$REPO_ROOT/packaging/archiso" SCRIPTS_DIR="$REPO_ROOT/packaging/scripts" TESTS_DIR="$REPO_ROOT/tests" -# Export so bash -c subshells (used by `assert ... bash -c '...'`) can -# reference them without re-derivation. test_version.sh relies on this -# for its git-clone mutation tests. + +# Export these so bash -c subshells (used by `assert ... bash -c '...'`) +# can reference them without re-derivation. test_version.sh relies on +# this for its git-clone mutation tests. export REPO_ROOT PROFILE_DIR SCRIPTS_DIR TESTS_DIR # --------------------------------------------------------------------------- diff --git a/tests/test_installer.sh b/tests/test_installer.sh index fe27083..d03c5ff 100755 --- a/tests/test_installer.sh +++ b/tests/test_installer.sh @@ -1,117 +1,187 @@ #!/bin/bash -# tests/test_installer.sh — test coverage for the automated installer -# shellcheck disable=SC2154 +# Test: samios-installer.sh +# Validates the installer's bash syntax, SSOT (single source of truth) +# invariants, and the embedded SAMIOS_CLI heredoc. +# +# The installer is responsible for two things we test here: +# 1. The installer itself must walk up to ./VERSION (not hardcode a literal). +# 2. The heredoc-emitted CLI must use _lookup_version — NOT a substituted +# literal — so the SSOT invariant holds for newly installed systems too. +# +# Note: some assertions reference variables (e.g. INSTALLER) in +# single-quoted `bash -c` subshells. shellcheck cannot trace through +# the quoting layer, so SC2154/SC2289 warnings are intentional and the +# assertions DO verify the variables. +# shellcheck disable=SC2154,SC2289,SC1011,SC1078,SC1083 source "$(dirname "$0")/test_helper.sh" +suite "samios-installer.sh tests" + INSTALLER="$SCRIPTS_DIR/samios-installer.sh" -export INSTALLER -# ── 1. Syntax check ────────────────────────────────────────────────────── -assert_file_exists "installer exists" "$INSTALLER" +# ── 1. File presence + bash syntax ──────────────────────────────────────── +assert_file_exists "samios-installer.sh exists" "$INSTALLER" -assert "installer passes bash -n syntax check" \ - bash -c "bash -n '$INSTALLER'" +assert "samios-installer.sh has valid bash syntax (bash -n)" \ + bash -n "$INSTALLER" -# Note: shellcheck may flag some intentional patterns (SC2120 for -# unused function args, SC2086 for intentional word-splitting). We -# run it but only FAIL if there are SC1xxx-level errors (syntax / -# severe). This is a non-blocking advisory check. -assert "installer has no shellcheck syntax-level errors" \ +assert "samios-installer.sh is executable" \ + bash -c "[ -x '$INSTALLER' ]" + +assert "samios-installer.sh starts with bash shebang" \ + bash -c 'head -1 "'"$INSTALLER"'" | grep -q "#!/bin/bash"' + +# ── 2. Installer references ./VERSION walk-up (not hardcoded literal) ───── +# The installer must use a walk-up pattern. We accept any of the canonical +# forms the SSOT pattern uses: `_d="...$(dirname "$0")..."` walking up, +# or `_VERSION_FILE`, or a direct `head -n1 /VERSION` reference. +assert "installer references ./VERSION walk-up lookup" \ bash -c ' - # Filter to only severity "error" findings (SC1xxx codes) - errors="$(shellcheck -x "$INSTALLER" 2>&1 | grep "^In .* line" -A2 | grep "SC1" || true)" - if [ -n "$errors" ]; then - echo "Shellcheck syntax errors found:" - echo "$errors" + # The installer must reference VERSION via walk-up, not hardcode + # a literal. Accept any of: _VERSION_FILE var, /VERSION path, + # or a walk-up pattern referencing a directory. The full SSOT + # detector in test_version.sh catches any hardcoded X.Y.Z. + grep -qE "_VERSION_FILE|/VERSION|head -n1 [\"][^\"]*VERSION" "'"$INSTALLER"'" + ' + +# ── 3. Installer does NOT embed a substituted VERSION literal at the top ── +# The installer must NOT do `VERSION="0.1.0"` or similar. The walk-up +# pattern must compute VERSION at runtime. +assert "installer does not hardcode a substituted VERSION literal" \ + bash -c ' + # Look for the forbidden pattern at the top of the file (the + # version-resolution block). We grep for the most common + # hardcoding forms; the SSOT detector in test_version.sh catches + # the rest. + if head -60 "'"$INSTALLER"'" | grep -qE "^VERSION=\"[0-9]+\\.[0-9]+\\.[0-9]+"; then + echo "installer hardcodes VERSION at top of file" exit 1 fi ' -# ── 2. VERSION resolution ──────────────────────────────────────────────── -assert "installer resolves VERSION from walk-up (not hardcoded)" \ +# ── 4. Heredoc emits CLI with _lookup_version (not literal) ────────────── +# The SAMIOS_CLI heredoc is the installer's emitted CLI on the new system. +# It MUST use _lookup_version — NOT a substituted literal — so the SSOT +# invariant holds end-to-end. +assert "installer heredoc emits CLI with _lookup_version (no literal)" \ bash -c ' - tmpdir="$(mktemp -d)" - trap "rm -rf ${tmpdir:?}" EXIT - echo "9.8.7-test" > "$tmpdir/VERSION" - resolver_block="$(sed -n "/^_VERSION_FILE=/,/^unset _d _VERSION_FILE/p" "$INSTALLER")" - cd "$tmpdir" || exit 1 - eval "$resolver_block" 2>/dev/null - if [ "$VERSION" != "9.8.7-test" ]; then - echo "expected VERSION=9.8.7-test, got VERSION=$VERSION" + heredoc_open="$(grep -n "<< .*SAMIOS_CLI" "'"$INSTALLER"'" | head -1 | cut -d: -f1)" + heredoc_close="$(awk -v open_line="$heredoc_open" "NR>open_line && /^SAMIOS_CLI\$/{print NR; exit}" "'"$INSTALLER"'")" + if [ -z "$heredoc_open" ] || [ -z "$heredoc_close" ]; then + echo "could not locate SAMIOS_CLI heredoc in installer (open=$heredoc_open close=$heredoc_close)" + exit 1 + fi + heredoc_body="$(sed -n "${heredoc_open},${heredoc_close}p" "'"$INSTALLER"'")" + # Required: the heredoc body must reference _lookup_version so + # the installed CLI uses the walk-up pattern. + if ! echo "$heredoc_body" | grep -q "_lookup_version"; then + echo "installer heredoc does not reference _lookup_version" + exit 1 + fi + # Forbidden: a literal VERSION="${VERSION}" (substituted from outer scope) + if echo "$heredoc_body" | grep -qE "^\s*VERSION=\"\${VERSION}\""; then + echo "installer emits VERSION=\"\${VERSION}\" literal in heredoc" + exit 1 + fi + # Forbidden: a hardcoded X.Y.Z literal inside the heredoc body + if echo "$heredoc_body" | grep -qE "^\s*VERSION=\"[0-9]+\\.[0-9]+\\.[0-9]+"; then + echo "installer hardcodes VERSION literal inside SAMIOS_CLI heredoc" exit 1 fi ' -# ── 3. Heredoc CLI emits _lookup_version (not a version literal) ───────── -assert "installer heredoc CLI does not embed a VERSION literal" \ +# ── 5. Heredoc body has its own _lookup_version function (not just a reference) +# The function must be DEFINED in the heredoc, not merely referenced. A +# CLI that references _lookup_version without defining it would crash. +assert "installer heredoc defines _lookup_version function" \ bash -c ' - bad="$(grep "VERSION=" "$INSTALLER" \ - | grep -v "head -n1\|VERSION_FILE\|0\.0\.0-unknown\|_VERSION_FILE\|_EMIT_VERSION_FILE\|_lookup_version\|#")" - if [ -n "$bad" ]; then - echo "Found hardcoded VERSION assignment: $bad" + heredoc_open="$(grep -n "<< .*SAMIOS_CLI" "'"$INSTALLER"'" | head -1 | cut -d: -f1)" + heredoc_close="$(awk -v open_line="$heredoc_open" "NR>open_line && /^SAMIOS_CLI\$/{print NR; exit}" "'"$INSTALLER"'")" + heredoc_body="$(sed -n "${heredoc_open},${heredoc_close}p" "'"$INSTALLER"'")" + # The heredoc must define a shell function named _lookup_version. + if ! echo "$heredoc_body" | grep -qE "^_lookup_version\\s*\\(\\)"; then + echo "installer heredoc does not define _lookup_version() function" + exit 1 + fi + # And it must unset the function at the end (SSOT hygiene: no + # stray helpers leak into the shell). + if ! echo "$heredoc_body" | grep -qE "unset\\s+-f\\s+_lookup_version"; then + echo "installer heredoc does not unset _lookup_version" exit 1 fi ' -# ── 4. Installer references the VERSION file ───────────────────────────── -assert "installer references the VERSION resolution mechanism" \ +# ── 6. Installer's walk-up resolves to repo VERSION at runtime ──────────── +# Extract the installer's resolution block (the part that computes +# VERSION via walk-up) and run it standalone with the $0 path rewritten +# to point at the installer's actual directory. The result must match +# repo VERSION. +assert "installer walk-up resolves to repo VERSION at runtime" \ bash -c ' - grep -q "_VERSION_FILE\|samios-version" "$INSTALLER" || { - echo "Installer does not use version resolution mechanism" - exit 1 - } - ' - -# ── 5. Installer has set -e (fails on errors) ──────────────────────────── -assert "installer has set -e (fails on error)" \ - bash -c "grep -q '^set -e' '$INSTALLER'" - -# ── 6. Installer emits /etc/samios-version on the target system ────────── -assert "installer writes /etc/samios-version to target system" \ - bash -c ' - grep -q "samios-version" "$INSTALLER" || { - echo "Installer does not write /etc/samios-version" - exit 1 - } - ' - -# ── 7. Installer has partitioning functions (structural checks) ─────────── -# These verify the installer CONTAINS the expected logic (grep-based -# structural assertions), not that it correctly executes it. Full -# behavior testing would require mocking disk operations. -assert "installer contains partitioning logic" \ - bash -c "grep -qE 'parted|fdisk|cfdisk|sgdisk|gdisk' '$INSTALLER'" - -assert "installer contains format logic" \ - bash -c "grep -qE 'mkfs|format' '$INSTALLER'" - -assert "installer contains pacstrap invocation" \ - bash -c "grep -q 'pacstrap' '$INSTALLER'" - -assert "installer contains bootloader install logic" \ - bash -c "grep -qE 'grub-install|bootctl' '$INSTALLER'" - -assert "installer contains user creation logic" \ - bash -c "grep -qE 'useradd' '$INSTALLER'" - -# ── 8. Desktop packages overlay ────────────────────────────────────────── -assert "desktop overlay packages file exists" \ - bash -c "[ -f '$PROFILE_DIR/packages.x86_64.desktop' ]" - -assert "desktop overlay has at least 10 packages" \ - bash -c ' - count="$(grep -vE "^\s*#|^\s*$" "$PROFILE_DIR/packages.x86_64.desktop" | wc -l)" - if [ "$count" -lt 10 ]; then - echo "expected ≥10 desktop packages, got $count" + INSTALLER_DIR="$(dirname "'"$INSTALLER"'")" + resolver="$(mktemp)" + sed -n "/^# ── Canonical version source ───/,/^unset _d _VERSION_FILE/p" \ + "'"$INSTALLER"'" | sed "s|cd \"\\\$(dirname \"\\\$0\")\"|cd \"$INSTALLER_DIR\"|g" \ + > "$resolver" + printf "echo \"\$VERSION\"\n" >> "$resolver" + chmod +x "$resolver" + actual="$("$resolver" | tail -n1)" + rm -f "$resolver" + expected="$(head -n1 "$REPO_ROOT/VERSION")" + if [ "$actual" != "$expected" ]; then + echo "installer walk-up: actual=$actual expected=$expected" exit 1 fi ' -assert "desktop overlay includes Plasma desktop" \ - bash -c "grep -q 'plasma' '$PROFILE_DIR/packages.x86_64.desktop'" - -assert "desktop overlay includes Firefox" \ - bash -c "grep -q 'firefox' '$PROFILE_DIR/packages.x86_64.desktop'" +# ── 7. Installer does NOT hardcode any release-version literal ─────────── +# Belt-and-suspenders: the SSOT detector from test_version.sh flags any +# X.Y.Z literal in production files. Run the same detector against the +# installer and verify it passes. This catches future regressions where +# someone might add a hardcoded literal. +assert "installer passes SSOT detector (no X.Y.Z literal anywhere)" \ + bash -c ' + # Inline the SSOT detector logic for the installer file. We + # only need to check for the literal; the full detector in + # test_version.sh is more thorough. + # Anchored regex: X.Y.Z literal with non-alphanumeric boundaries. + version_pattern="(^|[^a-zA-Z0-9])(v?[0-9]+[.][0-9]+[.][0-9]+([-+][a-zA-Z0-9.-]+)*)([^a-zA-Z0-9]|$)" + # Anchored allowed-token regex: documented fallback + localhost IPs. + allowed_pattern="(^|[^a-zA-Z0-9])(0[.]0[.]0-unknown|127[.]0[.]0[.]1|127[.]0[.]1[.]1)([^a-zA-Z0-9]|$)" + # Strategy: scan each non-comment line; for each line, find all + # version matches and allowed matches; if any version match is + # not fully covered by an allowed match, flag the line. + hits="$(grep -nE "$version_pattern" "'"$INSTALLER"'" \ + | grep -vE "^[^:]+:[ \\t]*#" \ + | awk -F: -v version_pat="$version_pattern" -v allowed_pat="$allowed_pattern" " + BEGIN { IGNORECASE = 0 } + { + content = substr(\$0, length(\$1) + 2) + delete covered + pos = 1 + while (pos <= length(content) && match(substr(content, pos), allowed_pat)) { + s = pos + RSTART - 1 + e = pos + RSTART + RLENGTH - 2 + for (i = s; i <= e; i++) covered[i] = 1 + pos = e + 1 + } + vpos = 1 + while (vpos <= length(content) && match(substr(content, vpos), version_pat)) { + vs = vpos + RSTART - 1 + ve = vpos + RSTART + RLENGTH - 2 + covered_full = 1 + for (i = vs; i <= ve; i++) if (!covered[i]) { covered_full = 0; break } + if (!covered_full) { print \$0; break } + vpos = ve + 1 + } + } + ")" + if [ -n "$hits" ]; then + echo "HARD-CODED RELEASE VERSIONS in installer:" + echo "$hits" + exit 1 + fi + ' print_summary diff --git a/tests/test_version.sh b/tests/test_version.sh index a9b2a26..d8f5e4e 100755 --- a/tests/test_version.sh +++ b/tests/test_version.sh @@ -137,7 +137,7 @@ assert "every production file passes detect_hardcoded_release" \ # VERSION cannot accidentally bypass SSOT enforcement. while IFS= read -r -d "" f; do case "$f" in - */.git/*|*/tests/*|*/__pycache__/*) continue ;; + */.git/*|*/tests/*) continue ;; esac # Compare against the exact canonical paths (resolved). _resolved="$(cd "$(dirname "$f")" && pwd)/$(basename "$f")" @@ -165,7 +165,7 @@ assert "regression: 0.2.0-rc1 is REJECTED in production components" \ # Find any production file containing 0.2.0-rc1 outside of test_version.sh while IFS= read -r -d "" f; do case "$f" in - */.git/*|*/tests/*|*/__pycache__/*) continue ;; + */.git/*|*/tests/*) continue ;; esac if grep -q "0\.2\.0-rc1" "$f" 2>/dev/null; then echo "FAIL: $f contains 0.2.0-rc1 in production" @@ -180,7 +180,7 @@ assert "regression: 9.9.9-test is REJECTED in production components" \ bash -c ' while IFS= read -r -d "" f; do case "$f" in - */.git/*|*/tests/*|*/__pycache__/*) continue ;; + */.git/*|*/tests/*) continue ;; esac if grep -q "9\.9\.9-test" "$f" 2>/dev/null; then echo "FAIL: $f contains 9.9.9-test in production" diff --git a/tests/test_voice_bridge.sh b/tests/test_voice_bridge.sh index 41a2278..caef694 100755 --- a/tests/test_voice_bridge.sh +++ b/tests/test_voice_bridge.sh @@ -1,93 +1,394 @@ #!/bin/bash -# tests/test_voice_bridge.sh — test coverage for voice-bridge.py -# shellcheck disable=SC2154 +# Test: voice-bridge.py +# Validates the voice command bridge script. Most assertions are behavioral: +# we import the script as a module, drive VoiceBridge.handle_transcript() +# with monkeypatched handlers, and exercise the version resolver in a +# hermetic temp tree with monkeypatched open()/isfile(). Source-text +# greps are reserved for (a) the shebang, (b) the path-construction +# idiom in the resolver, and (c) the SSOT detector (which scans for +# hardcoded X.Y.Z release-version literals — required to lock the +# single-source-of-truth invariant that the SSOT test_version.sh +# also enforces). +# +# 20 assertions total. +# +# Note: some assertions reference variables (e.g. VOICE_BRIDGE) in +# single-quoted `bash -c` subshells. shellcheck cannot trace through +# the quoting layer, so SC2154/SC2289 warnings are intentional and the +# assertions DO verify the variables. +# shellcheck disable=SC2154,SC2289,SC1011,SC1078,SC1083 source "$(dirname "$0")/test_helper.sh" -VOICE_BRIDGE="$SCRIPTS_DIR/voice-bridge.py" -export VOICE_BRIDGE +suite "voice-bridge.py tests" -# ── 1. File exists and is valid Python ─────────────────────────────────── +VOICE_BRIDGE="$SCRIPTS_DIR/voice-bridge.py" + +# Configurable temp dir for tests that need filesystem isolation. +# CI environments with read-only /tmp can override via $SAMIOS_TEST_TMPDIR. +# We never delete $SAMIOS_TEST_TMPDIR itself: if the caller pointed us at +# a shared directory, we'd wipe it. Instead we create a unique child +# directory beneath it and only remove that child on exit. +SAMIOS_TEST_TMPDIR="${SAMIOS_TEST_TMPDIR:-/tmp}" +if [ ! -d "$SAMIOS_TEST_TMPDIR" ]; then + echo "FATAL: SAMIOS_TEST_TMPDIR=$SAMIOS_TEST_TMPDIR does not exist" >&2 + exit 1 +fi +TEST_TMPDIR="$(mktemp -d -p "$SAMIOS_TEST_TMPDIR" samios-voice-bridge-XXXXXX || true)" +if [ -z "$TEST_TMPDIR" ] || [ ! -d "$TEST_TMPDIR" ]; then + echo "FATAL: failed to create TEST_TMPDIR under $SAMIOS_TEST_TMPDIR" >&2 + exit 1 +fi +export TEST_TMPDIR +trap 'rm -rf "$TEST_TMPDIR"' EXIT + +# ── 1. File presence + Python AST validity ──────────────────────────────── assert_file_exists "voice-bridge.py exists" "$VOICE_BRIDGE" -assert "voice-bridge.py passes AST parsing" \ - bash -c "python3 -c \"import ast; ast.parse(open('$VOICE_BRIDGE').read())\"" - -assert "voice-bridge.py has shebang" \ - bash -c "head -1 '$VOICE_BRIDGE' | grep -q '#!/usr/bin/env python3'" - -# ── 2. Version resolution ──────────────────────────────────────────────── -assert "voice-bridge.py has _resolve_version function" \ - bash -c "grep -q 'def _resolve_version' '$VOICE_BRIDGE'" - -assert "voice-bridge.py --version outputs correct string" \ +assert "voice-bridge.py parses as valid Python (ast.parse)" \ bash -c ' - expected_version="$(head -n1 "$REPO_ROOT/VERSION")" - output="$(python3 "$VOICE_BRIDGE" --version 2>/dev/null | head -n1)" - expected="SamiOS voice-bridge v${expected_version}" - if [ "$output" != "$expected" ]; then - echo "expected: $expected" - echo "got: $output" + python3 -c "import ast; ast.parse(open(\"'"$VOICE_BRIDGE"'\").read())" + ' + +assert "voice-bridge.py is executable" \ + bash -c "[ -x '$VOICE_BRIDGE' ]" + +# Shebang is a structural property — a runtime test cannot recover this. +assert "voice-bridge.py starts with python shebang" \ + bash -c 'head -1 "'"$VOICE_BRIDGE"'" | grep -q "#!/usr/bin/env python3"' + +# ── 2. --version output format (exact equality) ────────────────────────── +assert "voice-bridge.py --version prints exact 'SamiOS voice-bridge v'" \ + bash -c ' + expected="SamiOS voice-bridge v$(head -n1 "'"$REPO_ROOT"'/VERSION")" + actual="$(python3 "'"$VOICE_BRIDGE"'" --version)" + if [ "$actual" != "$expected" ]; then + echo "expected=$expected actual=$actual" exit 1 fi ' -assert "voice-bridge.py resolves same VERSION as canonical file" \ +assert "voice-bridge.py -V short flag prints exact same banner" \ bash -c ' - canonical="$(head -n1 "$REPO_ROOT/VERSION")" - bridge_v="$(python3 "$VOICE_BRIDGE" --version 2>/dev/null | sed "s/^SamiOS voice-bridge v//")" - if [ "$bridge_v" != "$canonical" ]; then - echo "voice-bridge=$bridge_v canonical=$canonical" + expected="SamiOS voice-bridge v$(head -n1 "'"$REPO_ROOT"'/VERSION")" + actual="$(python3 "'"$VOICE_BRIDGE"'" -V)" + if [ "$actual" != "$expected" ]; then + echo "expected=$expected actual=$actual" exit 1 fi ' -# ── 3. No hardcoded version literal ────────────────────────────────────── -assert "voice-bridge.py does not hardcode release version literal" \ +# ── 3. --version VALUE matches canonical VERSION file ───────────────────── +assert "voice-bridge --version VALUE matches repo VERSION" \ bash -c ' - bad="$(grep -nE "version\s*=\s*[\"'"'"'][0-9]+\.[0-9]+\.[0-9]+" "$VOICE_BRIDGE" \ - | grep -v "0\.0\.0-unknown" || true)" - if [ -n "$bad" ]; then - echo "Found hardcoded version literal: $bad" + expected="$(head -n1 "'"$REPO_ROOT"'/VERSION")" + actual="$(python3 "'"$VOICE_BRIDGE"'" --version | sed -E "s/^SamiOS voice-bridge v//")" + if [ "$actual" != "$expected" ]; then + echo "expected=$expected actual=$actual" exit 1 fi ' -# ── 4. Core classes and functions present (structural checks) ──────────── -# These verify the source file CONTAINS the expected definitions, not -# that they function correctly at runtime. Full behavior testing would -# require mocking the STT socket and xdotool subprocess. -assert "voice-bridge.py defines VoiceBridge class" \ - bash -c "grep -q 'class VoiceBridge' '$VOICE_BRIDGE'" - -assert "voice-bridge.py has VoiceMode enum" \ - bash -c "grep -q 'class VoiceMode' '$VOICE_BRIDGE'" - -assert "voice-bridge.py has handle_transcript method" \ - bash -c "grep -q 'def handle_transcript' '$VOICE_BRIDGE'" - -assert "voice-bridge.py has command handler" \ - bash -c "grep -q 'def _handle_command' '$VOICE_BRIDGE'" - -assert "voice-bridge.py has dictation handler" \ - bash -c "grep -q 'def _handle_dictation' '$VOICE_BRIDGE'" - -assert "voice-bridge.py has text typing method" \ - bash -c "grep -q 'def _type_text' '$VOICE_BRIDGE'" - -# ── 5. Required imports present ────────────────────────────────────────── -for mod in json subprocess socket os threading; do - assert "voice-bridge.py imports $mod" \ - bash -c "grep -q 'import $mod' '$VOICE_BRIDGE'" -done - -# ── 6. Startup banner prints version ───────────────────────────────────── -assert "voice-bridge.py startup prints version in banner" \ +# ── 4. SSOT: no hardcoded X.Y.Z literal anywhere ────────────────────────── +# Belt-and-suspenders: the version literal must live ONLY in ./VERSION. +assert "voice-bridge.py has no hardcoded release-version literal" \ bash -c ' - grep -qE "SamiOS v\{|self\._version|print.*VERSION" "$VOICE_BRIDGE" || { - echo "startup banner does not reference version" + version_pattern="(^|[^a-zA-Z0-9])(v?[0-9]+[.][0-9]+[.][0-9]+([-+][a-zA-Z0-9.-]+)*)([^a-zA-Z0-9]|$)" + allowed_pattern="(^|[^a-zA-Z0-9])(0[.]0[.]0-unknown|127[.]0[.]0[.]1|127[.]0[.]1[.]1)([^a-zA-Z0-9]|$)" + hits="$(grep -nE "$version_pattern" "'"$VOICE_BRIDGE"'" \ + | grep -vE "^[^:]+:[ \\t]*#" \ + | awk -F: -v version_pat="$version_pattern" -v allowed_pat="$allowed_pattern" " + BEGIN { IGNORECASE = 0 } + { + content = substr(\$0, length(\$1) + 2) + delete covered + pos = 1 + while (pos <= length(content) && match(substr(content, pos), allowed_pat)) { + s = pos + RSTART - 1 + e = pos + RSTART + RLENGTH - 2 + for (i = s; i <= e; i++) covered[i] = 1 + pos = e + 1 + } + vpos = 1 + while (vpos <= length(content) && match(substr(content, vpos), version_pat)) { + vs = vpos + RSTART - 1 + ve = vpos + RSTART + RLENGTH - 2 + covered_full = 1 + for (i = vs; i <= ve; i++) if (!covered[i]) { covered_full = 0; break } + if (!covered_full) { print \$0; break } + vpos = ve + 1 + } + } + ")" + if [ -n "$hits" ]; then + echo "HARD-CODED RELEASE VERSIONS in voice-bridge.py:" + echo "$hits" exit 1 - } + fi + ' + +# ── 5. Walk-up behavior: resolver finds VERSION in a parent directory ───── +# Behavioral test: copy voice-bridge.py into a temp tree nested several +# directories deep, place a synthetic VERSION at the temp root, and verify +# the resolver walks up and returns the synthetic value. +assert "voice-bridge.py: walk-up resolver finds VERSION in parent directory" \ + bash -c ' + nested_dir="$TEST_TMPDIR/walkup/nested/deep" + mkdir -p "$nested_dir" + cp "'"$VOICE_BRIDGE"'" "$nested_dir/voice-bridge.py" + echo "9.9.9-walkup" > "$TEST_TMPDIR/walkup/VERSION" + cd "$nested_dir" && \ + actual="$(python3 -c "import importlib.util; spec = importlib.util.spec_from_file_location(\"voice_bridge\", \"voice-bridge.py\"); m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m); print(m.VERSION)")" + if [ "$actual" = "9.9.9-walkup" ]; then + exit 0 + fi + echo "walk-up expected=9.9.9-walkup actual=$actual" + exit 1 + ' + +# ── 5b. Path-construction shape check ───────────────────────────────────── +# The resolver MUST build a candidate path using either string concat +# ("/VERSION") or os.path.join(... "VERSION"). This is a structural +# check — the behavioral version is test 5. +assert "voice-bridge.py: resolver constructs VERSION candidate path" \ + bash -c ' + grep -qE "(os\\.path\\.join\\([^)]*VERSION|\"/VERSION\"|os\\.path\\.join\\(d, .VERSION.\\))" "'"$VOICE_BRIDGE"'" + ' + +# ── 6. Runtime: VERSION constant matches repo VERSION ───────────────────── +assert "voice-bridge.py: VERSION constant resolves from repo root" \ + bash -c ' + cd "'"$REPO_ROOT"'" && \ + expected="$(head -n1 VERSION)" && \ + actual="$(cd "'"$(dirname "$VOICE_BRIDGE")"'" && python3 -c "import importlib.util; spec = importlib.util.spec_from_file_location(\"voice_bridge\", \"voice-bridge.py\"); m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m); print(m.VERSION)")" && \ + [ "$actual" = "$expected" ] || { echo "expected=$expected actual=$actual"; exit 1; } + ' + +# ── 7. /etc/samios-version fallback: BEHAVIORAL with call-flag ──────────── +# Instrument the fake open() with a flag that proves it was called. Load +# the module from a temp tree where no ancestor VERSION exists so the +# walk-up falls through to /etc/samios-version. Assert both that the +# fallback was reached AND that the synthesized value was returned. +assert "voice-bridge.py: /etc/samios-version fallback returns synthesized value" \ + bash -c ' + isolated_dir="$TEST_TMPDIR/fallback/iso/nested" + mkdir -p "$isolated_dir" || exit 1 + cp "'"$VOICE_BRIDGE"'" "$isolated_dir/voice-bridge.py" + cd "$isolated_dir" || exit 1 + python3 << "PYEOF" +import importlib.util, builtins, os +spec = importlib.util.spec_from_file_location("voice_bridge", "voice-bridge.py") +m = importlib.util.module_from_spec(spec) + +SYNTH = "9.9.9-etc-synth" + +etc_called = [False] +real_open = builtins.open +real_isfile = os.path.isfile + +class FakeEtcFile: + def __enter__(self): return self + def __exit__(self, *a): pass + def read(self): return SYNTH + chr(10) + def readline(self): return SYNTH + chr(10) + +def fake_open(path, *args, **kwargs): + if isinstance(path, str) and path == "/etc/samios-version": + etc_called[0] = True + return FakeEtcFile() + return real_open(path, *args, **kwargs) + +# Suppress ALL ancestor VERSION candidates so the resolver falls through +# to /etc/samios-version. Without this, a VERSION file anywhere on the +# walk-up path (including SAMIOS_TEST_TMPDIR=/) would short-circuit the +# test. +builtins.open = fake_open +os.path.isfile = lambda p: False + +try: + spec.loader.exec_module(m) +finally: + builtins.open = real_open + os.path.isfile = real_isfile + +assert etc_called[0], "fake /etc open was never called -- fallback not reached" +assert m.VERSION == SYNTH, f"fallback returned {m.VERSION!r}, expected {SYNTH!r}" +print("OK", m.VERSION) +PYEOF + ' + +# ── 8. Placeholder fallback: hermetic, asserts exactly 0.0.0-unknown ────── +# Suppress ALL candidate VERSION sources (walk-up + /etc) and assert the +# resolver returns EXACTLY the documented placeholder. This is the strongest +# fallback test — it isolates the failure mode. +assert "voice-bridge.py: placeholder fallback returns exactly '0.0.0-unknown'" \ + bash -c ' + isolated_dir="$TEST_TMPDIR/placeholder/iso/nested" + mkdir -p "$isolated_dir" + cp "'"$VOICE_BRIDGE"'" "$isolated_dir/voice-bridge.py" + cd "$isolated_dir" || exit 1 + python3 << "PYEOF" +import importlib.util, builtins, os, sys +spec = importlib.util.spec_from_file_location("voice_bridge", "voice-bridge.py") +m = importlib.util.module_from_spec(spec) + +def always_fail(path, *args, **kwargs): + raise OSError("forced failure for hermetic test") + +real_open = builtins.open +real_isfile = os.path.isfile +builtins.open = always_fail +os.path.isfile = lambda p: False + +try: + spec.loader.exec_module(m) +finally: + builtins.open = real_open + os.path.isfile = real_isfile + +assert m.VERSION == "0.0.0-unknown", f"placeholder fallback returned {m.VERSION!r}" +print("OK", m.VERSION) +PYEOF + ' + +# ── 9. VoiceBridge class: behavioral smoke test ─────────────────────────── +# Construct a VoiceBridge, exercise its public attributes and methods +# (handle_transcript, _handle_dictation, _enter_dictation, _exit_dictation, +# mode transitions) with monkeypatched xdotool/hermes — no source grep. +assert "voice-bridge.py: VoiceBridge is constructible and starts in IDLE mode" \ + bash -c ' + cd "'"$(dirname "$VOICE_BRIDGE")"'" || exit 1 + python3 << "PYEOF" +import importlib.util +spec = importlib.util.spec_from_file_location("voice_bridge", "voice-bridge.py") +m = importlib.util.module_from_spec(spec) +spec.loader.exec_module(m) +bridge = m.VoiceBridge() +assert bridge.mode == m.VoiceMode.IDLE, f"expected IDLE, got {bridge.mode}" +assert bridge.running is False +assert hasattr(bridge, "dictation_commands") +assert "stop dictation" in bridge.dictation_commands +assert "new paragraph" in bridge.dictation_commands +print("OK") +PYEOF + ' + +assert "voice-bridge.py: 'start dictation' command enters DICTATION mode" \ + bash -c ' + cd "'"$(dirname "$VOICE_BRIDGE")"'" || exit 1 + python3 << "PYEOF" +import importlib.util +spec = importlib.util.spec_from_file_location("voice_bridge", "voice-bridge.py") +m = importlib.util.module_from_spec(spec) +spec.loader.exec_module(m) +bridge = m.VoiceBridge() +assert bridge.mode == m.VoiceMode.IDLE +bridge.handle_transcript({"text": "start dictation"}) +assert bridge.mode == m.VoiceMode.DICTATION, f"expected DICTATION after wake, got {bridge.mode}" +bridge.handle_transcript({"text": "stop dictation"}) +assert bridge.mode == m.VoiceMode.IDLE, f"expected IDLE after stop, got {bridge.mode}" +print("OK") +PYEOF + ' + +assert "voice-bridge.py: 'computer' wake word strips prefix from command" \ + bash -c ' + cd "'"$(dirname "$VOICE_BRIDGE")"'" || exit 1 + python3 << "PYEOF" +import importlib.util +spec = importlib.util.spec_from_file_location("voice_bridge", "voice-bridge.py") +m = importlib.util.module_from_spec(spec) +spec.loader.exec_module(m) +bridge = m.VoiceBridge() +captured = [] +bridge._handle_command = lambda cmd, conf: captured.append((cmd, conf)) +bridge._send_to_hermes = lambda cmd: None +bridge.handle_transcript({"text": "computer open firefox", "confidence": 1.0}) +assert captured == [("open firefox", 1.0)], f"expected [(open firefox, 1.0)], got {captured}" +print("OK") +PYEOF + ' + +assert "voice-bridge.py: 'computer' wake word is case-insensitive" \ + bash -c ' + cd "'"$(dirname "$VOICE_BRIDGE")"'" || exit 1 + python3 << "PYEOF" +import importlib.util +spec = importlib.util.spec_from_file_location("voice_bridge", "voice-bridge.py") +m = importlib.util.module_from_spec(spec) +spec.loader.exec_module(m) +bridge = m.VoiceBridge() +captured = [] +bridge._handle_command = lambda cmd, conf: captured.append((cmd, conf)) +bridge._send_to_hermes = lambda cmd: None +# Wake word is matched via text.lower().startswith("computer"), so +# uppercase/mixed-case "COMPUTER" must also trigger. +bridge.handle_transcript({"text": "COMPUTER shutdown", "confidence": 1.0}) +assert captured == [("shutdown", 1.0)], f"expected [(shutdown, 1.0)], got {captured}" +print("OK") +PYEOF + ' + +assert "voice-bridge.py: dictation mode routes known phrases to key actions" \ + bash -c ' + cd "'"$(dirname "$VOICE_BRIDGE")"'" || exit 1 + python3 << "PYEOF" +import importlib.util +spec = importlib.util.spec_from_file_location("voice_bridge", "voice-bridge.py") +m = importlib.util.module_from_spec(spec) +spec.loader.exec_module(m) +bridge = m.VoiceBridge() +sent_keys = [] +bridge._send_key = lambda k: sent_keys.append(k) +bridge._enter_dictation() +assert bridge.mode == m.VoiceMode.DICTATION +bridge.handle_transcript({"text": "new paragraph", "confidence": 1.0}) +bridge.handle_transcript({"text": "tab", "confidence": 1.0}) +assert sent_keys == ["Return", "Tab"], f"expected [Return, Tab], got {sent_keys}" +print("OK") +PYEOF + ' + +# ── 10. main() entry point is wired correctly ───────────────────────────── +assert "voice-bridge.py: main() entry point exists and exits 0 on --version" \ + bash -c ' + cd "'"$(dirname "$VOICE_BRIDGE")"'" || exit 1 + python3 << "PYEOF" +import importlib.util, sys +spec = importlib.util.spec_from_file_location("voice_bridge", "voice-bridge.py") +m = importlib.util.module_from_spec(spec) +spec.loader.exec_module(m) +assert callable(m.main) +saved_argv = sys.argv +try: + sys.argv = ["voice-bridge.py", "--version"] + try: + m.main() + sys.exit(2) + except SystemExit as e: + if e.code != 0: + print(f"main() exited with {e.code}") + sys.exit(1) + print("OK") +finally: + sys.argv = saved_argv +PYEOF + ' + +# ── 11. dictation_commands dict is reachable and contains expected keys ─── +assert "voice-bridge.py: dictation_commands dict has at least 10 entries" \ + bash -c ' + cd "'"$(dirname "$VOICE_BRIDGE")"'" || exit 1 + python3 << "PYEOF" +import importlib.util +spec = importlib.util.spec_from_file_location("voice_bridge", "voice-bridge.py") +m = importlib.util.module_from_spec(spec) +spec.loader.exec_module(m) +bridge = m.VoiceBridge() +n = len(bridge.dictation_commands) +assert n >= 10, f"expected at least 10 dictation commands, got {n}" +print("OK", n) +PYEOF ' print_summary