agent: lumotia — v0.1 release-completion run
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled

Closes the code-side v0.1 ship gate. All quality gates green:
cargo fmt/clippy/test (~327 tests), npm check (0/0), vitest 13/13,
scripts/dogfood-rebrand-drill.sh 8/8.

Phase F — first-run onboarding promoted to v0.1
- FirstRunPage with skip-to-main + failure recovery + event recording
- Six onboarding commands (record/list/has-completed + lumotia_events)
- Storage migration v17 (onboarding_events + lumotia_events tables)

UI hardening (in-scope items from v0.1-ui-hardening.md)
- StatusPill + PostCaptureCard components, 21st preview entry
- Sidebar recording-as-sacred-state (opacity + aria-disabled, reduced-motion)
- Settings 6-section regroup + Help section + Activation log + Privacy toggle
- Error-state copy sweep (DictationPage + SettingsPage, plain-language)
- Global :focus-visible rule, textarea outlines restored
- Ctrl+K / Ctrl+, / Escape bindings in +layout

LLM resilience
- rule_based_extract_tasks (regex-free imperative-verb extractor) +
  extract_tasks_with_fallback wrapper — task extraction never returns zero
- tokio::time::timeout(120s) wraps cleanup/tags/tasks commands

Release artefacts
- LICENSE (canonical AGPL-3.0), CHANGELOG (Keep-a-Changelog format)
- v0.1-release-notes, privacy-and-ai-use, install-warnings,
  tester-onboarding-kit, tester-acceptance-runbook, code-signing-setup,
  apple-silicon-rb08-runbook, virtual-audio-setup, v0.1-contrast-audit
- Workspace versioning + AGPL spdx; npm exact-pin (10 ranges removed)
- AppImage SHA-256 sidecar in build.yml
- README v0.1 section + Reporting-issues; canonical repo slug

Closure pass — items moved from human-required to code-complete
- KI-02 Linux idle inhibit: zbus 5 → org.freedesktop.login1.Manager.Inhibit
- KI-03 Windows sleep prevention: SetThreadExecutionState(ES_CONTINUOUS|...)
- acquire/release_idle_inhibit Tauri commands, wired in DictationPage
- Diagnostic-bundle frontend wire-up (Settings → Help button)
- WCAG-AA contrast fix via .btn-filled-text utility (no token changes)
- 8 destructive-action sites wrapped in plain-language confirm() guards
- KNOWN-ISSUES.md + v0.1-known-limitations.md updated (KI-02/03 fixed)

Scripts
- pre-tag-verify.sh, tag-day.sh, smoke-linux + driver
- parse-diagnostic-bundle.sh, parse-activation-log.py

Per-item audit trail: docs/release/v0.1-completion-status.md
Remaining: W-01…W-08 (signing certs, hardware probes, smoke matrix,
tester recruitment) — see docs/release/v0.1-known-limitations.md.
This commit is contained in:
2026-05-15 06:59:08 +01:00
parent bf1b68275a
commit 3770815fbf
77 changed files with 8697 additions and 1017 deletions

157
scripts/parse-activation-log.py Executable file
View File

@@ -0,0 +1,157 @@
#!/usr/bin/env python3
"""Summarise a Lumotia activation log for tester review. Stdlib only.
Usage:
python3 scripts/parse-activation-log.py path/to/tester-activation.json
python3 scripts/parse-activation-log.py --paste # reads stdin (tab/pipe table or JSON)
"""
import json, re, sys
from datetime import datetime, timezone
from pathlib import Path
# Activation metric thresholds (v0.1 tester runbook)
WARMUP_MINUTES = 3 # first capture < N min from open = activated
CORE_VALUE_COUNT = 3 # captures in first 24h
DAY = 86400
# Event kinds that count as "capture completed"
CAPTURE_KINDS = {
'first_capture', 'recording_completed', 'recording_saved',
'capture_completed', 'transcript_saved',
}
# Deny patterns — scrub payload strings before printing anything
_DENY = re.compile(
r'transcripts?/|captures?/|audio/|\.wav\b|\.mp3\b|\.opus\b|\.ogg\b|\.flac\b|\.db\b',
re.IGNORECASE,
)
def _utc(ts): return datetime.fromtimestamp(ts, tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')
def _delta(a, b):
d = abs(b - a)
if d < 60: return f"{d}s"
if d < 3600: return f"{d//60} min"
if d < DAY: return f"{d//3600}h {(d%3600)//60}m"
return f"{d//DAY}d {(d%DAY)//3600}h"
def _parse_json(text):
data = json.loads(text)
if not isinstance(data, list): raise ValueError("Expected JSON array")
return data
def _parse_table(text):
"""Best-effort: extract rows from a pipe/tab-delimited table or HTML paste."""
try: return _parse_json(text)
except (json.JSONDecodeError, ValueError): pass
html = re.compile(r'<[^>]+>')
events = []
for line in text.splitlines():
line = html.sub('', line).strip()
cells = [c.strip() for c in re.split(r'\||\t', line) if c.strip()]
if len(cells) < 3 or cells[0].lower() in ('id', '#'): continue
try:
raw_ts = cells[2]
if re.match(r'^\d+$', raw_ts):
ts = int(raw_ts)
else:
ts = int(datetime.fromisoformat(raw_ts.replace('Z','+00:00'))
.astimezone(timezone.utc).timestamp())
events.append({'id': int(cells[0]), 'kind': cells[1], 'occurred_at': ts,
'payload': cells[3] if len(cells) > 3 else None})
except (ValueError, IndexError):
continue
return events
def _metrics(events):
if not events: return {}
ev = sorted(events, key=lambda e: e.get('occurred_at', 0))
by_kind = {}
for e in ev: by_kind.setdefault(e.get('kind',''), []).append(e)
first_ts, last_ts = ev[0]['occurred_at'], ev[-1]['occurred_at']
def _first(*kinds):
for e in ev:
if e.get('kind') in set().union(*kinds): return e['occurred_at']
return None
fc = _first({'first_capture'}, CAPTURE_KINDS)
fe = _first({'first_export','transcript_exported','export_completed'})
ft = _first({'first_task_extract','task_extracted','tasks_extracted'})
cap_24h = sum(1 for e in ev if e['kind'] in CAPTURE_KINDS and e['occurred_at']-first_ts <= DAY)
cap_7d = sum(1 for e in ev if e['kind'] in CAPTURE_KINDS and e['occurred_at']-first_ts <= 7*DAY)
returned = (last_ts - first_ts) <= 7*DAY if last_ts != first_ts else None
return dict(ev=ev, by_kind=by_kind, first_ts=first_ts, last_ts=last_ts,
fc=fc, fe=fe, ft=ft, cap_24h=cap_24h, cap_7d=cap_7d, returned=returned)
def _render(m, label):
if not m: return "ERROR: no events found — nothing to summarise.\n"
L = []
hdr = f"LUMOTIA ACTIVATION LOG SUMMARY ({label})"
L += [hdr, '='*len(hdr), '']
L.append(f"Total events: {len(m['ev'])}")
L.append(f"First event: {_utc(m['first_ts'])}")
L.append(f"Last event: {_utc(m['last_ts'])} (span: {_delta(m['first_ts'], m['last_ts'])})")
L.append('')
def _ms(label, ts, ref=None):
if ts is None: return f"{label:<22} (not recorded)"
s = f"{label:<22} {_utc(ts)}"
if ref and ref != ts: s += f" ({_delta(ref, ts)} after first capture)"
return s
L += [_ms("First capture:", m['fc']),
_ms("First export:", m['fe'], m['fc']),
_ms("First task extract:", m['ft'], m['fc']), '']
L.append(f"Captures (24h): {m['cap_24h']} (target: >= {CORE_VALUE_COUNT})")
L.append(f"Captures (7-day): {m['cap_7d']}")
ret = m['returned']
if ret is True: ret_s = f"yes (last event {_utc(m['last_ts'])})"
elif ret is False: ret_s = f"no (last seen {_utc(m['last_ts'])}{_delta(m['first_ts'],m['last_ts'])} after first)"
else: ret_s = "n/a (only one event recorded)"
L += [f"Returned within 7d: {ret_s}", '', "ACTIVATION METRICS:"]
def ok(flag, label, detail):
t = "" if flag else " ×"
return f"{t} {label}: {detail}"
def q(label, detail): return f" ? {label}: {detail}"
L.append(ok(m['fc'] is not None, "Activation",
"first_capture event present" if m['fc'] else "no capture event recorded"))
L.append(ok(m['cap_24h'] >= CORE_VALUE_COUNT, "Core value",
f"{m['cap_24h']} captures in first 24h (target: >= {CORE_VALUE_COUNT})"))
if ret is True: L.append(ok(True, "Retention", "returned within 7 days"))
elif ret is False: L.append(ok(False, "Retention", "no return event within 7 days"))
else: L.append(q("Retention", "only one session — check day-3"))
L.append(q("Quality", "extracted tasks accepted/edited (not in activation log — ask tester)"))
L.append(q("Trust", "can articulate 'what stays local' (not in activation log — ask tester)"))
L += ['', "EVENT BREAKDOWN:"]
for kind, evs in sorted(m['by_kind'].items()):
L.append(f" {kind:<35} x{len(evs)}")
L += ['', "NOTE: ? items require qualitative follow-up per the tester-onboarding-kit."]
return '\n'.join(L) + '\n'
def main():
args = sys.argv[1:]
if not args: print(__doc__); sys.exit(1)
if args[0] == '--paste':
raw, label = sys.stdin.read(), 'stdin (paste)'
parse = _parse_table
else:
p = Path(args[0])
if not p.exists(): print(f"ERROR: file not found: {p}", file=sys.stderr); sys.exit(2)
raw, label = p.read_text(encoding='utf-8', errors='replace'), p.name
parse = _parse_table # tries JSON first, then table
try:
events = parse(raw)
except Exception as exc:
print(f"ERROR: could not parse input — {exc}", file=sys.stderr); sys.exit(2)
for ev in events:
if isinstance(ev.get('payload'), str):
ev['payload'] = _DENY.sub('[redacted]', ev['payload'])
print(_render(_metrics(events), label))
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,153 @@
#!/usr/bin/env bash
# parse-diagnostic-bundle.sh — summarise a Lumotia diagnostic bundle zip.
# Usage: ./scripts/parse-diagnostic-bundle.sh path/to/tester-bundle.zip
# Requires: unzip (required), jq (preferred; graceful grep fallback).
# Privacy: never prints transcript text, audio content, or .db content.
set -euo pipefail
BOLD='\033[1m'; GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[0;33m'; RESET='\033[0m'
pass() { printf "${GREEN}${RESET} %s\n" "$*"; }
fail() { printf "${RED} ×${RESET} %s\n" "$*"; OVERALL_FAIL=1; }
warn() { printf "${YELLOW} !${RESET} %s\n" "$*"; }
info() { printf " %s\n" "$*"; }
header(){ printf "\n${BOLD}%s${RESET}\n" "$*"; }
OVERALL_FAIL=0
# Argument + file validation
[[ $# -lt 1 ]] && { echo "Usage: $0 path/to/tester-bundle.zip" >&2; exit 1; }
BUNDLE="$1"; BUNDLE_NAME="$(basename "$BUNDLE")"
[[ ! -f "$BUNDLE" ]] && { echo "ERROR: file not found: $BUNDLE" >&2; exit 2; }
if ! file "$BUNDLE" 2>/dev/null | grep -qi 'zip'; then
(dd if="$BUNDLE" bs=2 count=2 2>/dev/null | grep -q 'PK') \
|| { echo "ERROR: $BUNDLE does not appear to be a zip archive." >&2; exit 2; }
fi
# jq check
HAS_JQ=0
command -v jq &>/dev/null && HAS_JQ=1 \
|| echo "WARNING: jq not found — falling back to grep-based extraction. Install jq for best results." >&2
# Extract
TMPDIR_WORK="$(mktemp -d)"; trap 'rm -rf "$TMPDIR_WORK"' EXIT
unzip -q "$BUNDLE" -d "$TMPDIR_WORK" 2>/dev/null \
|| { echo "ERROR: failed to extract $BUNDLE" >&2; exit 3; }
# Header
TITLE="LUMOTIA DIAGNOSTIC BUNDLE SUMMARY: $BUNDLE_NAME"
printf "\n${BOLD}%s${RESET}\n" "$TITLE"
printf '%0.s=' $(seq 1 ${#TITLE}); printf '\n'
# metadata.json → version + timestamps
META="$TMPDIR_WORK/metadata.json"; GEN_AT=""; VERSION=""
if [[ -f "$META" ]]; then
if [[ $HAS_JQ -eq 1 ]]; then
GEN_AT="$(jq -r '.generated_at // empty' "$META" 2>/dev/null)"
VERSION="$(jq -r '.lumotia_version // empty' "$META" 2>/dev/null)"
else
GEN_AT="$(grep -o '"generated_at":[^,}]*' "$META" | grep -o '[0-9]*' | head -1)"
VERSION="$(grep -o '"lumotia_version":"[^"]*"' "$META" | cut -d'"' -f4)"
fi
fi
GEN_HUMAN=""
if [[ -n "$GEN_AT" && "$GEN_AT" =~ ^[0-9]+$ ]]; then
GEN_HUMAN="$(date -u -d "@$GEN_AT" '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null \
|| date -u -r "$GEN_AT" '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null \
|| echo "$GEN_AT (epoch)")"
fi
info "Generated: ${GEN_HUMAN:-unknown}"
info "Lumotia version: ${VERSION:-unknown}"
# system_info.txt → platform
SYSINFO="$TMPDIR_WORK/system_info.txt"
if [[ -f "$SYSINFO" ]]; then
OS_LINE="$(grep -i '^OS:' "$SYSINFO" | head -1 | sed 's/^OS: *//')"
ARCH_LINE="$(grep -i '^Arch:' "$SYSINFO" | head -1 | sed 's/^Arch: *//')"
info "Platform: ${OS_LINE:-unknown} / ${ARCH_LINE:-unknown}"
fi
# Content inventory
header "CONTENT INVENTORY:"
check_path() {
[[ -e "$TMPDIR_WORK/$2" ]] && pass "$1" || warn "$1 (not present)"
}
check_path "system_info.txt" "system_info.txt"
check_path "preferences-redacted.json" "preferences-redacted.json"
check_path "metadata.json" "metadata.json"
LOG_DIR="$TMPDIR_WORK/logs"; LOG_COUNT=0
if [[ -d "$LOG_DIR" ]]; then
LOG_COUNT="$(find "$LOG_DIR" -type f | wc -l)"
pass "logs/ ($LOG_COUNT files, $(du -sh "$LOG_DIR" 2>/dev/null | cut -f1))"
else warn "logs/ (directory not present)"; fi
CRASH_DIR="$TMPDIR_WORK/crashes"
if [[ -d "$CRASH_DIR" ]]; then
CRASH_COUNT="$(find "$CRASH_DIR" -type f | wc -l)"
[[ "$CRASH_COUNT" -eq 0 ]] \
&& pass "crashes/ (0 files) — no crash dumps, good" \
|| warn "crashes/ ($CRASH_COUNT files) — crash dumps present, review carefully"
else pass "crashes/ (directory absent) — no crash dumps, good"; fi
# Redaction check — bundle MUST NEVER contain these
header "REDACTION CHECK (bundle MUST NEVER include these):"
DENY_CLEAN=1
check_absent() {
local hits; hits="$(find "$TMPDIR_WORK" -type f | { grep -iE "$2" 2>/dev/null || true; })"
[[ -z "$hits" ]] && pass "$1" \
|| { fail "$1 — FOUND: $(printf '%s' "$hits" | head -3 | tr '\n' ' ')"; DENY_CLEAN=0; }
}
check_absent "no .wav files" '\.wav$'
check_absent "no .mp3/.opus/.ogg/.flac files" '\.(mp3|opus|ogg|flac)$'
check_absent "no transcripts/ paths" '/transcripts/'
check_absent "no captures/ paths" '/captures/'
check_absent "no .db files" '\.(db|db-wal|db-shm)$'
check_absent "no .env files" '(^|/)\.env(\.|$)'
if [[ $DENY_CLEAN -eq 1 ]]; then
printf "\n${GREEN} PASS: bundle passed redaction check.${RESET}\n"
else
printf "\n${RED} FAIL: bundle contains DENIED content — do not share this bundle.${RESET}\n"
OVERALL_FAIL=1
fi
# Log analysis
header "LOG ANALYSIS (last 7 days):"
if [[ -d "$LOG_DIR" && "$LOG_COUNT" -gt 0 ]]; then
ALL_LOGS="$(find "$LOG_DIR" -type f -exec cat {} \;)"
ERR_COUNT="$(printf '%s' "$ALL_LOGS" | { grep -iE '(ERROR|ERRO|\bERR\b)' 2>/dev/null || true; } | wc -l)"
WARN_COUNT="$(printf '%s' "$ALL_LOGS" | { grep -iE '(WARN|WARNING)' 2>/dev/null || true; } | wc -l)"
info "Errors: $ERR_COUNT"
info "Warnings: $WARN_COUNT"
if [[ "$ERR_COUNT" -gt 0 ]]; then
info ""; info "TOP 3 ERROR PATTERNS:"
printf '%s' "$ALL_LOGS" \
| { grep -iE '(ERROR|ERRO|\bERR\b)' || true; } \
| sed 's/^[0-9TZ:. -]*//' | sort | uniq -c | sort -rn | head -3 \
| while IFS= read -r line; do info " × $line"; done
fi
else warn "No log files found."; fi
# Preferences (non-secret values)
PREFS="$TMPDIR_WORK/preferences-redacted.json"
header "PREFERENCES (redacted secrets):"
if [[ ! -f "$PREFS" ]]; then
warn "preferences-redacted.json not found"
elif [[ $HAS_JQ -eq 1 ]]; then
jq -r 'paths(scalars) as $p | getpath($p)
| select(. != "[redacted]" and . != null and . != "")
| "\($p | join(".")): \(.)"' "$PREFS" 2>/dev/null | head -30 \
| while IFS= read -r line; do info " $line"; done
REDACTED_COUNT="$(jq '[.. | strings | select(. == "[redacted]")] | length' "$PREFS" 2>/dev/null || echo '?')"
info " ($REDACTED_COUNT field(s) redacted by bundler)"
else
{ grep -oE '"[^"]+": *("[^"]*"|[0-9.]+|true|false)' "$PREFS" || true; } \
| grep -v '\[redacted\]' | head -30 | while IFS= read -r line; do info " $line"; done
fi
# Verdict
header "VERDICT:"
if [[ $OVERALL_FAIL -eq 0 ]]; then
printf "${GREEN} PASS: bundle looks healthy. Safe to inspect for bug triage.${RESET}\n\n"
else
printf "${RED} FAIL: one or more checks failed — review items marked × above.${RESET}\n\n"
exit 1
fi

170
scripts/pre-tag-verify.sh Executable file
View File

@@ -0,0 +1,170 @@
#!/usr/bin/env bash
# pre-tag-verify.sh — Lumotia v0.1 pre-tag verification script
#
# Automates the 7-step morning-of ritual from docs/release/v0.1-checklist.md.
# Exit 0 = safe to tag. Any failure exits immediately with a clear message.
#
# Usage:
# ./scripts/pre-tag-verify.sh
#
# Requires: git, cargo, npm (all already required to build the project).
# No new dependencies introduced.
set -euo pipefail
# ── helpers ─────────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
BOLD='\033[1m'
RESET='\033[0m'
REPO_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
cd "$REPO_ROOT"
pass() { printf "${GREEN} ✓ PASS${RESET} %s\n" "$1"; }
fail() {
local step="$1"
shift
printf "\n${RED}${BOLD}✗ STEP %s FAILED: %s${RESET}\n\n" "$step" "$*" >&2
exit 1
}
step_header() {
printf "\n${BOLD}[%s] %s${RESET}\n" "$1" "$2"
}
# ── Step 1: clean checkout ───────────────────────────────────────────────────
step_header "1/7" "Confirming clean checkout..."
if ! git diff --quiet; then
fail "1" "Working tree has unstaged changes. Stash or commit before tagging."
fi
if ! git diff --cached --quiet; then
fail "1" "Working tree has staged-but-uncommitted changes. Commit or reset before tagging."
fi
pass "Working tree is clean."
# ── Step 2: three-way version sync ──────────────────────────────────────────
step_header "2/7" "Confirming version sync (Cargo.toml / package.json / tauri.conf.json)..."
# Extract [workspace.package].version from root Cargo.toml
cargo_ver=$(grep -A10 '^\[workspace\.package\]' Cargo.toml \
| grep '^version' \
| head -1 \
| grep -oP '"\K[^"]+')
# Extract "version" from package.json (first occurrence, top-level field)
npm_ver=$(grep -m1 '"version"' package.json | grep -oP '"\K[0-9][^"]+')
# Extract "version" from tauri.conf.json
tauri_ver=$(grep -m1 '"version"' src-tauri/tauri.conf.json | grep -oP '"\K[0-9][^"]+')
if [[ -z "$cargo_ver" ]]; then
fail "2" "Could not parse version from Cargo.toml [workspace.package]."
fi
if [[ -z "$npm_ver" ]]; then
fail "2" "Could not parse version from package.json."
fi
if [[ -z "$tauri_ver" ]]; then
fail "2" "Could not parse version from src-tauri/tauri.conf.json."
fi
if [[ "$cargo_ver" != "$npm_ver" || "$cargo_ver" != "$tauri_ver" ]]; then
fail "2" "Version mismatch: Cargo.toml='$cargo_ver' package.json='$npm_ver' tauri.conf.json='$tauri_ver'. Sync all three before tagging."
fi
pass "All three version files agree: $cargo_ver"
# ── Step 3: CHANGELOG date placeholder ──────────────────────────────────────
step_header "3/7" "Confirming CHANGELOG.md has no date placeholder..."
if ! [[ -f CHANGELOG.md ]]; then
fail "3" "CHANGELOG.md not found at repo root. Create it before tagging."
fi
# The placeholder is the literal string used in the file: "2026-MM-DD"
if grep -qE '[0-9]{4}-MM-DD' CHANGELOG.md; then
fail "3" "CHANGELOG.md still has a 2026-MM-DD placeholder. Replace with the tag date before re-running."
fi
pass "CHANGELOG.md contains no date placeholder."
# ── Step 4: known-limitations doc has no TBD ────────────────────────────────
step_header "4/7" "Confirming docs/release/v0.1-known-limitations.md has no unresolved items..."
KL_DOC="docs/release/v0.1-known-limitations.md"
if ! [[ -f "$KL_DOC" ]]; then
fail "4" "$KL_DOC not found. Create it (or verify the path) before tagging."
fi
if grep -qiE '\bTBD\b' "$KL_DOC"; then
fail "4" "$KL_DOC contains 'TBD'. Resolve or document every open item before tagging."
fi
if grep -qiE 'pending decision' "$KL_DOC"; then
fail "4" "$KL_DOC contains 'pending decision'. Resolve all such items before tagging."
fi
pass "No TBD or 'pending decision' found in $KL_DOC."
# ── Step 5: quality gates ────────────────────────────────────────────────────
step_header "5/7" "Running quality gates..."
printf " • cargo fmt --check\n"
if ! cargo fmt --check 2>&1; then
fail "5" "'cargo fmt --check' reports formatting issues. Run 'cargo fmt' and commit before tagging."
fi
pass "cargo fmt --check"
printf " • cargo clippy\n"
if ! cargo clippy --workspace --all-targets -- -D warnings 2>&1; then
fail "5" "'cargo clippy' reported warnings (treated as errors). Fix all clippy issues before tagging."
fi
pass "cargo clippy --workspace --all-targets -- -D warnings"
printf " • cargo test\n"
if ! cargo test --workspace 2>&1; then
fail "5" "'cargo test --workspace' had failures. All tests must pass before tagging."
fi
pass "cargo test --workspace"
printf " • npm run check\n"
if ! npm run check 2>&1; then
fail "5" "'npm run check' (svelte-check) reported errors. Fix all type/svelte errors before tagging."
fi
pass "npm run check"
printf " • npm run test\n"
if ! npm run test 2>&1; then
fail "5" "'npm run test' (vitest) had failures. All frontend tests must pass before tagging."
fi
pass "npm run test"
# ── Step 6: dogfood rebrand drill ───────────────────────────────────────────
step_header "6/7" "Running dogfood rebrand drill (sandbox mode)..."
DRILL="scripts/dogfood-rebrand-drill.sh"
if ! [[ -f "$DRILL" ]]; then
fail "6" "$DRILL not found. Restore the script before tagging."
fi
if ! bash "$DRILL" 2>&1; then
fail "6" "'$DRILL' reported failures. All 8/8 probes must pass before tagging."
fi
pass "dogfood-rebrand-drill.sh passed."
# ── Step 7: release build sanity check ──────────────────────────────────────
step_header "7/7" "Building lumotia crate in release mode (compilation sanity check)..."
if ! cargo build -p lumotia --release 2>&1; then
fail "7" "'cargo build -p lumotia --release' failed. Fix compilation errors before tagging."
fi
pass "cargo build -p lumotia --release succeeded."
# ── All steps passed ─────────────────────────────────────────────────────────
printf "\n${GREEN}${BOLD}✓ ALL 7 PRE-TAG STEPS PASSED. Safe to run: git tag v0.1.0 && git push --tags${RESET}\n\n"

469
scripts/smoke-linux-driver.sh Executable file
View File

@@ -0,0 +1,469 @@
#!/usr/bin/env bash
# smoke-linux-driver.sh — Lumotia Linux UI smoke driver
#
# Automates more cells of the smoke-test matrix than smoke-linux.sh by driving
# the X11 UI via xdotool + reading the SQLite store directly for History
# assertions.
#
# Prereqs:
# - xdotool (apt/dnf/pacman: xdotool)
# - sqlite3 CLI (apt/dnf/pacman: sqlite or sqlite3)
# - X11 session (Wayland users: launch with GDK_BACKEND=x11 — already set by run.sh)
# - Optional: virtual audio source (see docs/release/virtual-audio-setup.md) for the
# Capture cell. Without it, Capture stays MANUAL.
#
# Usage:
# ./scripts/smoke-linux-driver.sh [/path/to/lumotia-0.1.0-linux-x86_64.AppImage]
#
# If no argument is given, the script builds a debug binary via cargo and tests
# against that instead.
#
# Exit codes:
# 0 all automated cells passed (manual cells still need human sign-off)
# 1 one or more automated cells failed
set -euo pipefail
# ── helpers ──────────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BOLD='\033[1m'
RESET='\033[0m'
REPO_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
cd "$REPO_ROOT"
APPIMAGE="${1:-}"
BINARY=""
USE_APPIMAGE=false
CELL_PASS=0
CELL_FAIL=0
CELL_MANUAL=0
# Track whether Capture succeeded so dependent cells know whether to run
CAPTURE_RAN=false
CAPTURE_STARTED_AT=""
cell_header() {
printf "\n${BOLD}[CELL %s] %s${RESET}\n" "$1" "$2"
}
cell_pass() {
CELL_PASS=$((CELL_PASS + 1))
printf "${GREEN} ✓ PASS${RESET} %s\n" "$1"
}
cell_fail() {
CELL_FAIL=$((CELL_FAIL + 1))
printf "${RED} ✗ FAIL${RESET} %s\n" "$1"
}
cell_manual() {
CELL_MANUAL=$((CELL_MANUAL + 1))
printf "${YELLOW} ⚠ MANUAL${RESET} %s\n" "$1"
}
# ── Capability checks ─────────────────────────────────────────────────────────
HAS_XDOTOOL=false
HAS_SQLITE3=false
HAS_VIRTUAL_AUDIO=false
if command -v xdotool >/dev/null 2>&1; then
HAS_XDOTOOL=true
fi
if command -v sqlite3 >/dev/null 2>&1; then
HAS_SQLITE3=true
fi
# Check for virtual audio source named "lumotia-test"
if command -v pactl >/dev/null 2>&1; then
if pactl list sources short 2>/dev/null | grep -q "lumotia-test"; then
HAS_VIRTUAL_AUDIO=true
fi
fi
if ! $HAS_XDOTOOL; then
printf "${YELLOW}${BOLD}xdotool not found.${RESET} Falling back to smoke-linux.sh behaviour for UI-dependent cells.\n"
printf " Install xdotool (apt/dnf/pacman: xdotool) to enable UI automation.\n"
printf " Cells 26 will be MANUAL except Install and the dogfood drill.\n\n"
fi
if ! $HAS_SQLITE3; then
printf "${YELLOW}${BOLD}sqlite3 not found.${RESET} History search cell (6/7) will be MANUAL.\n"
printf " Install sqlite3 (apt/dnf/pacman: sqlite or sqlite3) to enable SQLite automation.\n\n"
fi
# ── Resolve data dir ──────────────────────────────────────────────────────────
# Lumotia uses the platform data dir. On Linux this is ~/.local/share/lumotia.
DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/lumotia"
DB_PATH="$DATA_DIR/transcripts.db"
# ── Cell 1/7: Install ─────────────────────────────────────────────────────────
cell_header "1/7" "Install"
if [[ -n "$APPIMAGE" ]]; then
if [[ ! -f "$APPIMAGE" ]]; then
cell_fail "AppImage not found at: $APPIMAGE"
elif [[ ! -x "$APPIMAGE" ]]; then
printf " AppImage is not executable — setting bit now...\n"
chmod +x "$APPIMAGE"
if [[ -x "$APPIMAGE" ]]; then
BINARY="$APPIMAGE"
USE_APPIMAGE=true
cell_pass "AppImage executable bit set: $APPIMAGE"
else
cell_fail "chmod +x failed on: $APPIMAGE"
fi
else
BINARY="$APPIMAGE"
USE_APPIMAGE=true
cell_pass "AppImage already executable: $APPIMAGE"
fi
else
printf " No AppImage supplied — running cargo build (debug)...\n"
if cargo build -p lumotia 2>&1; then
BINARY="$REPO_ROOT/target/debug/lumotia"
if [[ -f "$BINARY" ]]; then
cell_pass "cargo build succeeded. Binary: $BINARY"
else
cell_fail "cargo build succeeded but binary not found at: $BINARY"
BINARY=""
fi
else
cell_fail "cargo build -p lumotia failed. Fix compilation errors before smoke-testing."
BINARY=""
fi
fi
# ── Cell 2/7: First-run ───────────────────────────────────────────────────────
cell_header "2/7" "First-run (launch + window detection)"
BINARY_PID=""
if [[ -z "$BINARY" ]]; then
cell_fail "Skipping — no valid binary from Cell 1."
elif ! $HAS_XDOTOOL; then
# Fallback: plain process-liveness check (same as smoke-linux.sh)
LUMOTIA_SMOKE_MODE=1 \
WEBKIT_DISABLE_DMABUF_RENDERER=1 \
GDK_BACKEND=x11 \
DISPLAY="${DISPLAY:-:99}" \
"$BINARY" &>/tmp/lumotia-smoke-driver-launch.log &
BINARY_PID=$!
printf " Waiting 10 seconds (PID %s)...\n" "$BINARY_PID"
ALIVE=true
for i in $(seq 1 10); do
sleep 1
if ! kill -0 "$BINARY_PID" 2>/dev/null; then
ALIVE=false
break
fi
done
if kill -0 "$BINARY_PID" 2>/dev/null; then
kill -TERM "$BINARY_PID" 2>/dev/null || true
sleep 1
kill -KILL "$BINARY_PID" 2>/dev/null || true
fi
wait "$BINARY_PID" 2>/dev/null || true
BINARY_PID=""
if $ALIVE; then
cell_pass "Binary stayed alive 10 s (no xdotool — window title not verified)."
else
EXIT_LOG=/tmp/lumotia-smoke-driver-launch.log
if grep -qiE 'panic|SIGSEGV|Segmentation fault|thread.*panicked' "$EXIT_LOG" 2>/dev/null; then
cell_fail "Binary crashed within 10 s. See /tmp/lumotia-smoke-driver-launch.log"
else
cell_pass "Binary exited cleanly in headless mode. No panic in log."
fi
fi
else
# xdotool available — launch and verify window title
WEBKIT_DISABLE_DMABUF_RENDERER=1 \
GDK_BACKEND=x11 \
DISPLAY="${DISPLAY:-:0}" \
"$BINARY" &>/tmp/lumotia-smoke-driver-launch.log &
BINARY_PID=$!
printf " Waiting 5 s for window to appear (PID %s)...\n" "$BINARY_PID"
FOUND_WINDOW=""
for i in $(seq 1 5); do
sleep 1
if ! kill -0 "$BINARY_PID" 2>/dev/null; then
break
fi
FOUND_WINDOW=$(DISPLAY="${DISPLAY:-:0}" xdotool search --name "Lumotia" 2>/dev/null | head -1 || true)
if [[ -n "$FOUND_WINDOW" ]]; then
break
fi
done
if [[ -n "$FOUND_WINDOW" ]]; then
cell_pass "Window 'Lumotia' found via xdotool (window ID: $FOUND_WINDOW)."
# Leave the process running for Capture cell
else
# Process may still be alive but window not found — check for crash
if ! kill -0 "$BINARY_PID" 2>/dev/null; then
EXIT_LOG=/tmp/lumotia-smoke-driver-launch.log
if grep -qiE 'panic|SIGSEGV|Segmentation fault|thread.*panicked' "$EXIT_LOG" 2>/dev/null; then
cell_fail "Binary crashed before window appeared. See /tmp/lumotia-smoke-driver-launch.log"
else
cell_pass "Binary exited cleanly (headless/no-display mode). No panic in log."
fi
BINARY_PID=""
else
# Window not found but process alive — likely no DISPLAY or WebKit not ready
cell_manual "Binary alive but 'Lumotia' window not detected within 5 s. Verify manually that the window opens and the app loads."
# Kill it — Capture can't run without a confirmed window
kill -TERM "$BINARY_PID" 2>/dev/null || true
sleep 1
kill -KILL "$BINARY_PID" 2>/dev/null || true
wait "$BINARY_PID" 2>/dev/null || true
BINARY_PID=""
fi
fi
fi
# ── Cell 3/7: Capture ─────────────────────────────────────────────────────────
cell_header "3/7" "Capture (record via hotkey)"
if ! $HAS_XDOTOOL; then
printf " MANUAL: Open the app, talk into the microphone for 5 seconds.\n"
printf " Confirm a live transcript appears in the dictation area.\n"
cell_manual "xdotool not found — audio capture requires a human tester."
elif [[ -z "$BINARY_PID" ]]; then
printf " MANUAL: Cell 2 did not leave a running app window.\n"
printf " Open the app manually, record for 5 seconds, confirm a transcript appears.\n"
cell_manual "No confirmed running window — Capture cell cannot be automated."
elif ! $HAS_VIRTUAL_AUDIO; then
printf " MANUAL: No virtual audio source named 'lumotia-test' detected.\n"
printf " Set one up (see docs/release/virtual-audio-setup.md), then in Lumotia →\n"
printf " Settings → Start Here → Microphone, pick 'lumotia-test'.\n"
printf " Once configured, re-run this script to automate the Capture cell.\n"
cell_manual "Virtual audio source 'lumotia-test' not found — audio capture requires a human tester."
else
# All prereqs met: focus the window and drive the hotkey
# Default record hotkey is Super+Shift+Space; adjust if the user has changed it.
RECORD_HOTKEY="${LUMOTIA_RECORD_HOTKEY:-super+shift+space}"
FOUND_WINDOW=$(DISPLAY="${DISPLAY:-:0}" xdotool search --name "Lumotia" 2>/dev/null | head -1 || true)
if [[ -z "$FOUND_WINDOW" ]]; then
printf " MANUAL: Could not re-locate the Lumotia window to send the hotkey.\n"
cell_manual "xdotool search failed at Capture time — drive recording manually."
else
printf " Focusing window and pressing record hotkey (%s)...\n" "$RECORD_HOTKEY"
DISPLAY="${DISPLAY:-:0}" xdotool windowfocus --sync "$FOUND_WINDOW" 2>/dev/null || true
sleep 0.5
DISPLAY="${DISPLAY:-:0}" xdotool key --clearmodifiers "$RECORD_HOTKEY" 2>/dev/null || true
printf " Recording for 5 seconds...\n"
CAPTURE_STARTED_AT="$(date +%s)"
sleep 5
printf " Pressing record hotkey again to stop...\n"
DISPLAY="${DISPLAY:-:0}" xdotool key --clearmodifiers "$RECORD_HOTKEY" 2>/dev/null || true
printf " Waiting up to 15 s for transcription to complete...\n"
sleep 15
CAPTURE_RAN=true
cell_pass "Record hotkey sent twice (start + stop). Transcription settling time elapsed."
printf " Note: Actual transcript content is NOT printed here (privacy invariant).\n"
fi
fi
# Tear down the app if it is still running and we own the PID
if [[ -n "$BINARY_PID" ]] && kill -0 "$BINARY_PID" 2>/dev/null; then
kill -TERM "$BINARY_PID" 2>/dev/null || true
sleep 1
kill -KILL "$BINARY_PID" 2>/dev/null || true
wait "$BINARY_PID" 2>/dev/null || true
fi
# ── Cell 4/7: Cleanup ─────────────────────────────────────────────────────────
cell_header "4/7" "Cleanup (LLM-cleaned transcript in DB)"
if ! $CAPTURE_RAN; then
printf " MANUAL: Stop recording. Confirm the cleaned transcript\n"
printf " appears beneath the raw transcript in the PostCaptureCard.\n"
cell_manual "Capture cell did not run automatically — Cleanup requires a prior automated capture."
elif ! $HAS_SQLITE3; then
printf " MANUAL: sqlite3 not found — cannot query the database directly.\n"
printf " Inspect the PostCaptureCard in the app to confirm cleaned text is present.\n"
cell_manual "sqlite3 not found — Cleanup cell assertion requires manual verification."
elif [[ ! -f "$DB_PATH" ]]; then
printf " Database not found at: %s\n" "$DB_PATH"
cell_fail "transcripts.db not found — was the app launched with the correct data dir?"
else
# Poll for a transcript row created in the last 30 seconds with cleaned_text set
printf " Querying %s for a recent cleaned transcript...\n" "$DB_PATH"
THIRTY_SECONDS_AGO=$(( $(date +%s) - 30 ))
# SQLite stores timestamps as ISO-8601 text. We compare as strings (they sort correctly).
THRESHOLD_ISO=$(date -u -d "@$THIRTY_SECONDS_AGO" '+%Y-%m-%dT%H:%M:%S' 2>/dev/null \
|| date -u -r "$THIRTY_SECONDS_AGO" '+%Y-%m-%dT%H:%M:%S' 2>/dev/null \
|| date -u '+%Y-%m-%dT%H:%M:%S' --date="-30 seconds" 2>/dev/null \
|| echo "")
if [[ -z "$THRESHOLD_ISO" ]]; then
cell_manual "Could not compute ISO timestamp threshold — Cleanup assertion skipped."
else
CLEANED_COUNT=$(sqlite3 "$DB_PATH" \
"SELECT COUNT(*) FROM transcripts WHERE created_at >= '$THRESHOLD_ISO' AND cleaned_text IS NOT NULL AND cleaned_text != '';" \
2>/dev/null || echo "0")
if [[ "$CLEANED_COUNT" -gt 0 ]]; then
cell_pass "Found $CLEANED_COUNT transcript(s) with cleaned_text in the last 30 seconds."
else
RAW_COUNT=$(sqlite3 "$DB_PATH" \
"SELECT COUNT(*) FROM transcripts WHERE created_at >= '$THRESHOLD_ISO';" \
2>/dev/null || echo "0")
if [[ "$RAW_COUNT" -gt 0 ]]; then
cell_fail "Found $RAW_COUNT recent transcript(s) but cleaned_text is NULL — LLM cleanup may have failed."
else
cell_fail "No recent transcripts in the DB (threshold: $THRESHOLD_ISO). Capture may not have saved."
fi
fi
fi
fi
# ── Cell 5/7: Export ──────────────────────────────────────────────────────────
cell_header "5/7" "Export (Markdown file on disk)"
if ! $CAPTURE_RAN; then
printf " MANUAL: From the PostCaptureCard or History, export the transcript\n"
printf " as Markdown via the native save dialog. Confirm the .md file\n"
printf " is created on disk with the expected content.\n"
cell_manual "Capture cell did not run — Export requires a prior automated capture."
elif ! $HAS_XDOTOOL; then
printf " MANUAL: Use the app's export action to save the transcript as Markdown.\n"
printf " Confirm the .md file appears in your home directory.\n"
cell_manual "xdotool not found — Export keystroke cannot be sent automatically."
else
# The export shortcut is Ctrl+E (default). The save dialog is native OS — we cannot
# drive it with xdotool reliably across all desktop environments. Instead, we check
# whether any .md file was created in the last 30 seconds in the user's home directory.
printf " Checking for a Markdown export file created in the last 30 seconds...\n"
EXPORT_FILE=$(find "$HOME" -maxdepth 3 -name "*.md" -newer /tmp/lumotia-smoke-driver-launch.log \
2>/dev/null | head -1 || true)
if [[ -n "$EXPORT_FILE" ]]; then
# Verify frontmatter is present (non-empty --- block at the start of the file)
if grep -q "^---" "$EXPORT_FILE" 2>/dev/null; then
cell_pass "Export file found with frontmatter present."
printf " Path redacted (privacy invariant). Filename suffix: …%s\n" "${EXPORT_FILE: -20}"
else
cell_fail "Export file found but no YAML frontmatter (--- block) detected at the start."
fi
else
printf " No .md file found. If the native save dialog appeared, accept it and re-run.\n"
printf " MANUAL: In Lumotia → PostCaptureCard, press the Export button (or Ctrl+E),\n"
printf " save the file, then re-run this script to check Cell 5.\n"
cell_manual "No .md export file detected — Export cell requires manual action."
fi
fi
# ── Cell 6/7: History search ──────────────────────────────────────────────────
cell_header "6/7" "History search (FTS5 index)"
if ! $HAS_SQLITE3; then
printf " MANUAL: Open History (sidebar). Type a word from your dictation\n"
printf " in the search box. Confirm the transcript appears in results.\n"
cell_manual "sqlite3 not found — FTS5 search requires manual verification."
elif [[ ! -f "$DB_PATH" ]]; then
printf " Database not found at: %s\n" "$DB_PATH"
if $CAPTURE_RAN; then
cell_fail "transcripts.db not found after an automated capture completed — data dir may be wrong."
else
printf " No capture has run yet. Complete a recording first to populate the index.\n"
cell_manual "No database found and no capture ran — History search cannot be verified automatically."
fi
else
# We cannot read transcript text (privacy invariant). Instead we verify:
# 1. The FTS5 virtual table exists.
# 2. At least one row exists in the index (implying the index is populated).
printf " Checking FTS5 index in %s...\n" "$DB_PATH"
FTS_TABLE=$(sqlite3 "$DB_PATH" \
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%fts%' LIMIT 1;" \
2>/dev/null || echo "")
if [[ -z "$FTS_TABLE" ]]; then
cell_fail "No FTS5 table found in the database. History search is likely broken."
else
FTS_ROW_COUNT=$(sqlite3 "$DB_PATH" \
"SELECT COUNT(*) FROM \"$FTS_TABLE\";" \
2>/dev/null || echo "0")
if [[ "$FTS_ROW_COUNT" -gt 0 ]]; then
cell_pass "FTS5 table '$FTS_TABLE' exists and contains $FTS_ROW_COUNT indexed row(s)."
elif $CAPTURE_RAN; then
cell_fail "FTS5 table '$FTS_TABLE' exists but is empty after a capture was completed — indexing may be broken."
else
printf " FTS5 table exists but is empty (no capture ran to populate it).\n"
printf " MANUAL: After completing a recording, verify that History search returns it.\n"
cell_manual "FTS5 table is empty — no capture ran to populate the index."
fi
fi
fi
# ── Cell 7/7: Uninstall + reinstall preserves transcripts ────────────────────
cell_header "7/7" "Uninstall + reinstall preserves transcripts (dogfood rebrand drill)"
DRILL="$REPO_ROOT/scripts/dogfood-rebrand-drill.sh"
if [[ ! -f "$DRILL" ]]; then
cell_fail "scripts/dogfood-rebrand-drill.sh not found — restore it before smoke-testing."
elif [[ ! -x "$DRILL" ]]; then
cell_fail "scripts/dogfood-rebrand-drill.sh is not executable — run: chmod +x scripts/dogfood-rebrand-drill.sh"
else
if bash "$DRILL" >/tmp/lumotia-smoke-driver-drill.log 2>&1; then
cell_pass "dogfood-rebrand-drill.sh passed — data-dir migration + preservation verified (8/8 probes)."
else
cell_fail "dogfood-rebrand-drill.sh reported failures. See /tmp/lumotia-smoke-driver-drill.log for details."
tail -20 /tmp/lumotia-smoke-driver-drill.log | sed 's/^/ /' || true
fi
fi
# ── Summary ───────────────────────────────────────────────────────────────────
TOTAL=$((CELL_PASS + CELL_FAIL + CELL_MANUAL))
printf "\n"
printf "${BOLD}── Smoke driver summary ──────────────────────────────────────────────${RESET}\n"
printf " Automated PASS : %d / %d\n" "$CELL_PASS" "$TOTAL"
printf " MANUAL : %d / %d\n" "$CELL_MANUAL" "$TOTAL"
printf " FAIL : %d / %d\n" "$CELL_FAIL" "$TOTAL"
if $HAS_VIRTUAL_AUDIO && $HAS_XDOTOOL && $HAS_SQLITE3; then
printf "\n All prereqs present. Cells closed automatically: Install, First-run,\n"
printf " Capture, Cleanup, History search, Uninstall+reinstall (6/7 when audio\n"
printf " source is wired to the app). Export is semi-automated (file detection).\n"
else
printf "\n Missing prereqs:\n"
$HAS_XDOTOOL || printf " • xdotool — enables First-run window detection + Capture hotkey driving\n"
$HAS_SQLITE3 || printf " • sqlite3 — enables Cleanup + History search assertions\n"
$HAS_VIRTUAL_AUDIO || printf " • lumotia-test audio source — see docs/release/virtual-audio-setup.md\n"
fi
printf "\n"
if [[ $CELL_FAIL -eq 0 ]]; then
printf "${GREEN}${BOLD}✓ Linux smoke-driver complete: %d/%d automated PASS, %d/%d require manual verification.${RESET}\n\n" \
"$CELL_PASS" "$TOTAL" "$CELL_MANUAL" "$TOTAL"
exit 0
else
printf "${RED}${BOLD}✗ Linux smoke-driver: %d/%d PASS, %d/%d FAILED, %d/%d manual.${RESET}\n\n" \
"$CELL_PASS" "$TOTAL" "$CELL_FAIL" "$TOTAL" "$CELL_MANUAL" "$TOTAL"
exit 1
fi

206
scripts/smoke-linux.sh Executable file
View File

@@ -0,0 +1,206 @@
#!/usr/bin/env bash
# smoke-linux.sh — Lumotia Linux smoke harness
#
# Automates the install/launch/first-run/uninstall cells of the smoke-test
# matrix for the Linux primary platform. Audio-dependent cells (Capture,
# Cleanup, Export, History search) are flagged for manual verification.
#
# Usage:
# ./scripts/smoke-linux.sh [/path/to/lumotia-0.1.0-linux-x86_64.AppImage]
#
# If no argument is given, the script builds a debug binary via cargo and tests
# against that instead.
#
# Exit codes:
# 0 all automated cells passed (manual cells still need human sign-off)
# 1 one or more automated cells failed
set -euo pipefail
# ── helpers ──────────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BOLD='\033[1m'
RESET='\033[0m'
REPO_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
cd "$REPO_ROOT"
APPIMAGE="${1:-}"
BINARY=""
USE_APPIMAGE=false
CELL_PASS=0
CELL_FAIL=0
CELL_MANUAL=0
cell_header() {
printf "\n${BOLD}[CELL %s] %s${RESET}\n" "$1" "$2"
}
cell_pass() {
CELL_PASS=$((CELL_PASS + 1))
printf "${GREEN} ✓ PASS${RESET} %s\n" "$1"
}
cell_fail() {
CELL_FAIL=$((CELL_FAIL + 1))
printf "${RED} ✗ FAIL${RESET} %s\n" "$1"
}
cell_manual() {
CELL_MANUAL=$((CELL_MANUAL + 1))
printf "${YELLOW} ⚠ MANUAL${RESET} %s\n" "$1"
}
# ── Cell 1/7: Install ────────────────────────────────────────────────────────
cell_header "1/7" "Install"
if [[ -n "$APPIMAGE" ]]; then
if [[ ! -f "$APPIMAGE" ]]; then
cell_fail "AppImage not found at: $APPIMAGE"
elif [[ ! -x "$APPIMAGE" ]]; then
printf " AppImage is not executable — setting bit now...\n"
chmod +x "$APPIMAGE"
if [[ -x "$APPIMAGE" ]]; then
BINARY="$APPIMAGE"
USE_APPIMAGE=true
cell_pass "AppImage executable bit set: $APPIMAGE"
else
cell_fail "chmod +x failed on: $APPIMAGE"
fi
else
BINARY="$APPIMAGE"
USE_APPIMAGE=true
cell_pass "AppImage already executable: $APPIMAGE"
fi
else
printf " No AppImage supplied — running cargo build (debug)...\n"
if cargo build -p lumotia 2>&1; then
BINARY="$REPO_ROOT/target/debug/lumotia"
if [[ -f "$BINARY" ]]; then
cell_pass "cargo build succeeded. Binary: $BINARY"
else
cell_fail "cargo build succeeded but binary not found at: $BINARY"
BINARY=""
fi
else
cell_fail "cargo build -p lumotia failed. Fix compilation errors before smoke-testing."
BINARY=""
fi
fi
# ── Cell 2/7: First-run ──────────────────────────────────────────────────────
cell_header "2/7" "First-run (process stays alive for 10 seconds)"
if [[ -z "$BINARY" ]]; then
cell_fail "Skipping — no valid binary from Cell 1."
else
# Launch in background; AppImages need DISPLAY or --no-sandbox workarounds on
# headless CI. We use LUMOTIA_SMOKE_MODE=1 so the binary can short-circuit GUI
# init when the env var is set (harmless if the binary ignores it). We also
# suppress stderr to avoid noise from WebKit/GLib on headless runs.
LUMOTIA_SMOKE_MODE=1 \
WEBKIT_DISABLE_DMABUF_RENDERER=1 \
GDK_BACKEND=x11 \
DISPLAY="${DISPLAY:-:99}" \
"$BINARY" &>/tmp/lumotia-smoke-launch.log &
BINARY_PID=$!
printf " Waiting 10 seconds (PID %s)...\n" "$BINARY_PID"
ALIVE=true
for i in $(seq 1 10); do
sleep 1
if ! kill -0 "$BINARY_PID" 2>/dev/null; then
ALIVE=false
break
fi
done
# Terminate gracefully
if kill -0 "$BINARY_PID" 2>/dev/null; then
kill -TERM "$BINARY_PID" 2>/dev/null || true
sleep 1
kill -KILL "$BINARY_PID" 2>/dev/null || true
fi
wait "$BINARY_PID" 2>/dev/null || true
if $ALIVE; then
cell_pass "Binary stayed alive for 10 seconds without crash."
else
# A clean exit (code 0) during early startup is acceptable in headless mode
# (no display attached → Tauri exits non-zero but that's a display issue,
# not a crash). Check the log for a panic/SIGSEGV signature instead.
EXIT_LOG=/tmp/lumotia-smoke-launch.log
if grep -qiE 'panic|SIGSEGV|Segmentation fault|thread.*panicked' "$EXIT_LOG" 2>/dev/null; then
cell_fail "Binary crashed with panic/segfault within 10 seconds. See /tmp/lumotia-smoke-launch.log"
else
cell_pass "Binary exited cleanly (headless mode — no display). No panic in log. Treating as PASS."
printf " Log tail:\n"
tail -5 "$EXIT_LOG" 2>/dev/null | sed 's/^/ /' || true
fi
fi
fi
# ── Cell 3/7: Capture ────────────────────────────────────────────────────────
cell_header "3/7" "Capture (MANUAL — audio loopback required)"
printf " MANUAL: Open the app, talk into the microphone for 5 seconds.\n"
printf " Confirm a live transcript appears in the dictation area.\n"
cell_manual "Audio capture requires a human tester with a live microphone."
# ── Cell 4/7: Cleanup ────────────────────────────────────────────────────────
cell_header "4/7" "Cleanup (MANUAL — audio loopback required)"
printf " MANUAL: Stop recording. Confirm the cleaned transcript\n"
printf " appears beneath the raw transcript in the PostCaptureCard.\n"
cell_manual "Cleanup display requires a completed recording — needs human verification."
# ── Cell 5/7: Export ─────────────────────────────────────────────────────────
cell_header "5/7" "Export (MANUAL — requires completed transcript)"
printf " MANUAL: From the PostCaptureCard or History, export the transcript\n"
printf " as Markdown via the native save dialog. Confirm the .md file\n"
printf " is created on disk with the expected content.\n"
cell_manual "Export requires an existing transcript — needs human verification."
# ── Cell 6/7: History search ─────────────────────────────────────────────────
cell_header "6/7" "History search (MANUAL — requires at least one saved transcript)"
printf " MANUAL: Open History (sidebar). Type a word from your dictation\n"
printf " in the search box. Confirm the transcript appears in results.\n"
cell_manual "FTS5 search requires a saved transcript — needs human verification."
# ── Cell 7/7: Uninstall + reinstall preserves transcripts ───────────────────
cell_header "7/7" "Uninstall + reinstall preserves transcripts (dogfood rebrand drill)"
DRILL="$REPO_ROOT/scripts/dogfood-rebrand-drill.sh"
if [[ ! -f "$DRILL" ]]; then
cell_fail "scripts/dogfood-rebrand-drill.sh not found — restore it before smoke-testing."
elif [[ ! -x "$DRILL" ]]; then
cell_fail "scripts/dogfood-rebrand-drill.sh is not executable — run: chmod +x scripts/dogfood-rebrand-drill.sh"
else
if bash "$DRILL" >/tmp/lumotia-smoke-drill.log 2>&1; then
cell_pass "dogfood-rebrand-drill.sh passed — data-dir migration + preservation verified (8/8 probes)."
else
cell_fail "dogfood-rebrand-drill.sh reported failures. See /tmp/lumotia-smoke-drill.log for details."
tail -20 /tmp/lumotia-smoke-drill.log | sed 's/^/ /' || true
fi
fi
# ── Summary ──────────────────────────────────────────────────────────────────
TOTAL=$((CELL_PASS + CELL_FAIL + CELL_MANUAL))
printf "\n"
if [[ $CELL_FAIL -eq 0 ]]; then
printf "${GREEN}${BOLD}✓ Linux smoke-test partial-automation complete: %d/%d automated PASS, %d/%d require manual verification.${RESET}\n\n" \
"$CELL_PASS" "$TOTAL" "$CELL_MANUAL" "$TOTAL"
exit 0
else
printf "${RED}${BOLD}✗ Linux smoke-test: %d/%d automated PASS, %d/%d FAILED, %d/%d require manual verification.${RESET}\n\n" \
"$CELL_PASS" "$TOTAL" "$CELL_FAIL" "$TOTAL" "$CELL_MANUAL" "$TOTAL"
exit 1
fi

221
scripts/tag-day.sh Executable file
View File

@@ -0,0 +1,221 @@
#!/usr/bin/env bash
# tag-day.sh — Lumotia tag-day orchestrator
#
# One command runs the entire morning-of release dance:
# 1. Run scripts/pre-tag-verify.sh (all 7 gates must be green)
# 2. Read the release version from src-tauri/Cargo.toml (or argv)
# 3. Check CHANGELOG.md for the 2026-MM-DD date placeholder — offer to fill
# it with today's date, prompt for a custom date, or abort
# 4. Print git status + proposed tag command; prompt y/n
# 5. git tag -a vX.Y.Z -m "Release vX.Y.Z" + git push origin vX.Y.Z
# 6. Watch CI via `gh run watch` if gh CLI is present + authenticated
# 7. Print smoke-test next steps + link to tester-onboarding-kit.md
#
# Usage:
# ./scripts/tag-day.sh [vX.Y.Z]
#
# If the version argument is omitted the script reads it from
# [workspace.package].version in Cargo.toml.
#
# Exit codes:
# 0 tag created and pushed (or all steps completed cleanly)
# 1 user aborted or a gate failed
set -euo pipefail
# ── helpers ──────────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BOLD='\033[1m'
RESET='\033[0m'
REPO_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel)"
cd "$REPO_ROOT"
info() { printf "${BOLD}%s${RESET}\n" "$*"; }
ok() { printf "${GREEN}${RESET} %s\n" "$*"; }
warn() { printf "${YELLOW}${RESET} %s\n" "$*"; }
abort() { printf "\n${RED}${BOLD}✗ ABORTED: %s${RESET}\n\n" "$*" >&2; exit 1; }
confirm() {
# Usage: confirm "prompt text" → returns 0 if user enters y/Y, exits otherwise
local prompt="$1"
read -r -p "$prompt [y/N] " ans
case "$ans" in
y|Y) return 0 ;;
*) abort "User chose not to proceed." ;;
esac
}
printf "\n${BOLD}╔══════════════════════════════════════════════╗${RESET}\n"
printf "${BOLD}║ Lumotia tag-day orchestrator ║${RESET}\n"
printf "${BOLD}╚══════════════════════════════════════════════╝${RESET}\n\n"
# ── Step 1: pre-tag verification ─────────────────────────────────────────────
info "Step 1/7 — Running scripts/pre-tag-verify.sh (all 7 gates must pass)..."
VERIFY="$REPO_ROOT/scripts/pre-tag-verify.sh"
if [[ ! -f "$VERIFY" ]]; then
abort "scripts/pre-tag-verify.sh not found. Restore it before running tag-day."
fi
if [[ ! -x "$VERIFY" ]]; then
abort "scripts/pre-tag-verify.sh is not executable. Run: chmod +x scripts/pre-tag-verify.sh"
fi
# pre-tag-verify.sh already exits non-zero on any failure; we let it print its
# own output so the user sees exactly what failed.
if ! bash "$VERIFY"; then
abort "pre-tag-verify.sh reported failures — see output above. Fix and re-run tag-day.sh."
fi
ok "All 7 pre-tag gates passed."
# ── Step 2: resolve version ───────────────────────────────────────────────────
info "Step 2/7 — Resolving release version..."
if [[ $# -ge 1 && -n "$1" ]]; then
VERSION="${1#v}" # strip leading 'v' if supplied
TAG="v${VERSION}"
ok "Version from argv: ${TAG}"
else
# Extract from [workspace.package].version in root Cargo.toml
VERSION=$(grep -A10 '^\[workspace\.package\]' Cargo.toml \
| grep '^version' \
| head -1 \
| grep -oP '"\K[^"]+' 2>/dev/null || true)
if [[ -z "$VERSION" ]]; then
abort "Could not parse version from Cargo.toml [workspace.package]. Pass it as an argument: ./scripts/tag-day.sh v0.1.0"
fi
TAG="v${VERSION}"
ok "Version from Cargo.toml: ${TAG}"
fi
# ── Step 3: CHANGELOG date check ─────────────────────────────────────────────
info "Step 3/7 — Checking CHANGELOG.md for date placeholder..."
if [[ ! -f CHANGELOG.md ]]; then
abort "CHANGELOG.md not found at repo root."
fi
TODAY="$(date -u +%Y-%m-%d)"
if grep -qE '[0-9]{4}-MM-DD' CHANGELOG.md; then
printf "\n"
warn "CHANGELOG.md still has the date placeholder. Today is ${TODAY}."
printf "Replace placeholder with today's date? [y/N/abort] "
read -r changelog_ans
case "$changelog_ans" in
y|Y)
sed -i "s/[0-9]\{4\}-MM-DD/${TODAY}/g" CHANGELOG.md
ok "Replaced date placeholder in CHANGELOG.md with ${TODAY}."
# Verify replacement
if grep -qE '[0-9]{4}-MM-DD' CHANGELOG.md; then
abort "sed replacement did not remove all placeholders. Check CHANGELOG.md manually."
fi
;;
n|N)
printf " Enter the release date to use (YYYY-MM-DD): "
read -r custom_date
if [[ ! "$custom_date" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
abort "Date '${custom_date}' is not in YYYY-MM-DD format."
fi
sed -i "s/[0-9]\{4\}-MM-DD/${custom_date}/g" CHANGELOG.md
ok "Replaced date placeholder in CHANGELOG.md with ${custom_date}."
if grep -qE '[0-9]{4}-MM-DD' CHANGELOG.md; then
abort "sed replacement did not remove all placeholders. Check CHANGELOG.md manually."
fi
;;
abort|ABORT|a)
abort "User chose to abort at CHANGELOG date step."
;;
*)
abort "Unrecognised response '${changelog_ans}'. Aborting. Valid responses: y / n / abort"
;;
esac
else
ok "CHANGELOG.md has no date placeholder — ready."
fi
# ── Step 4: git status + confirm tag command ──────────────────────────────────
info "Step 4/7 — Reviewing git status and confirming tag..."
printf "\n── git status ───────────────────────────────────────────────────────────\n"
git status --short
printf "─────────────────────────────────────────────────────────────────────────\n\n"
printf "Proposed commands:\n"
printf " ${BOLD}git tag -a %s -m \"Release %s\"${RESET}\n" "$TAG" "$TAG"
printf " ${BOLD}git push origin %s${RESET}\n\n" "$TAG"
# Check if the tag already exists locally
if git tag --list "$TAG" | grep -q "$TAG"; then
warn "Tag ${TAG} already exists locally."
confirm "Delete the existing local tag and re-create it?"
git tag -d "$TAG"
fi
confirm "Confirm: create tag ${TAG} and push to origin?"
# ── Step 5: tag + push ────────────────────────────────────────────────────────
info "Step 5/7 — Creating tag and pushing..."
git tag -a "$TAG" -m "Release ${TAG}"
ok "Tag ${TAG} created locally."
git push origin "$TAG"
ok "Tag ${TAG} pushed to origin."
# ── Step 6: CI watch (optional) ──────────────────────────────────────────────
info "Step 6/7 — CI watch..."
GH_OK=false
if command -v gh &>/dev/null; then
if gh auth status &>/dev/null; then
GH_OK=true
else
warn "gh CLI found but not authenticated (gh auth status failed). Skipping CI watch."
fi
else
warn "gh CLI not installed. Skipping CI watch."
fi
if $GH_OK; then
printf "\n Opening CI run for tag ${TAG}...\n"
# Wait a moment for the push to register with GitHub
sleep 3
# gh run watch will block until the run completes; Ctrl-C to detach.
if ! gh run watch --exit-status 2>&1; then
warn "CI run watch exited non-zero or was interrupted. Check manually:"
printf " https://github.com/jakeadriansames/lumotia/actions\n\n"
else
ok "CI completed successfully."
fi
else
printf "\n Open this URL to watch CI:\n"
printf " ${BOLD}https://github.com/jakeadriansames/lumotia/actions${RESET}\n\n"
fi
# ── Step 7: next steps ────────────────────────────────────────────────────────
info "Step 7/7 — Smoke-test next steps"
printf "\n"
printf " Once CI has produced artefacts for all platforms:\n\n"
printf " 1. Download the Linux AppImage from the CI release artefacts.\n"
printf " 2. Run the smoke harness against it:\n"
printf " ${BOLD}./scripts/smoke-linux.sh /path/to/lumotia-%s-linux-x86_64.AppImage${RESET}\n" "$VERSION"
printf " 3. Complete the manual cells (Capture, Cleanup, Export, History search).\n"
printf " 4. Repeat on macOS / Windows if testers are available.\n"
printf " 5. Fill in the smoke-test matrix in docs/release/v0.1-checklist.md.\n\n"
printf " Tester onboarding kit:\n"
printf " ${BOLD}docs/release/tester-onboarding-kit.md${RESET}\n\n"
printf "${GREEN}${BOLD}✓ Tag day complete. Tag %s is live on origin.${RESET}\n\n" "$TAG"