Revert "[grade=D] Sprint 1: .gitignore, test_installer.sh, test_voice_bridge.sh, README, roadmap, desktop packages, CI improvements"
SamiOS CI / lint-and-test (push) Successful in 10s
SamiOS CI / lint-and-test (push) Successful in 10s
This reverts commit 9c63557e62.
This commit is contained in:
+2
-61
@@ -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
|
||||
|
||||
+7
-15
@@ -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
|
||||
|
||||
@@ -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=<your-version>
|
||||
```
|
||||
|
||||
## 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 "<NEW>" > 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=<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<VERSION>` — 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
|
||||
|
||||
+27
-18
@@ -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
|
||||
|
||||
@@ -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 <package> 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
|
||||
@@ -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
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
+157
-87
@@ -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 <path>/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
|
||||
|
||||
@@ -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"
|
||||
|
||||
+367
-66
@@ -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<VERSION>'" \
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user